Skip to content

Commit 6d9f8ce

Browse files
flacoonbandi34
authored andcommitted
feat(ftp): implement asynchronous FTP/SFTP upload queue with improved remote gallery
This commit introduces a fully asynchronous upload pipeline for FTP/SFTP remote storage, decoupling image upload from the photo capture workflow so the Photobooth UI is never blocked while files are transferred. Core architecture ----------------- - New SQLite-backed UploadQueueService (src/Service/UploadQueueService.php) manages a persistent job queue (pending → in_progress → completed/failed). Each job stores a random 32-hex-character remote filename so local and remote filenames are never the same, preventing enumeration. - New EncryptionService (src/Service/EncryptionService.php) encrypts the FTP password at rest using libsodium secretbox; key is stored in var/run/. - New Symfony Console command UploadWorkerCommand (src/Command/UploadWorkerCommand.php) runs as a long-lived background worker (or once with --once). Picks up jobs, calls RemoteStorageService, retries up to 5 times on failure, and reloads config on every run so admin panel changes are picked up without restarting. - New api/ftpListFolders.php lets the admin panel browse remote directories before saving configuration. - resources/config/photobooth-upload-worker.service: systemd unit for running the worker as www-data. API / backend changes --------------------- - api/applyEffects.php: instead of uploading directly, enqueues a job via UploadQueueService and triggers the worker process in the background. - api/deletePhoto.php: resolves the remote random filename via UploadQueueService::getRemoteFilename() before deleting on FTP, so the correct remote file is removed. - api/qrcode.php, api/print.php: QR URLs now use UploadQueueService to look up the remote filename when FTP + useForQr is enabled. - api/admin.php: exposes upload queue status (pending/failed counts) for the debug panel. - api/serverInfo.php: includes FTP queue stats in server-info response. - RemoteStorageService::getWebpageUri() correctly appends baseFolder to the website URL. - RemoteStorageService::createWebpage() removes legacy .htaccess files (caused 403 on hosts without AllowOverride Options) and instead uploads index.php redirect guards to images/ and thumbs/. - ConfigurationService: decrypts FTP password on load via EncryptionService. - FtpConfiguration: password field marked as sensitive. - src/Console/Application.php: registers UploadWorkerCommand. Admin UI -------- - lib/configsetup.inc.php: removed deprecated folder/urlTemplate fields; added folder browser (ftpListFolders API), queue status display, and "Test Connection" improvements. - assets/js/admin/buttons.js: folder-browser modal, queue status polling, FTP test-connection wiring. - resources/lang/en.json: removed ftp:folder / ftp:urlTemplate keys; updated manual:ftp:website description. Remote gallery (resources/template/index.php) --------------------------------------------- - Single-image view: image is rendered immediately with a JS onload/onerror spinner; onerror auto-reloads every 3 s while upload is in progress. Removed the PHP-side file_exists() upload-wait loop. - Gallery mode: fixed guard so gallery is only rendered when gallery_enabled=true (was falling through to else). - Images sorted newest-first via usort + filemtime. - ZIP download: fixed corrupt archive by using ZipArchive::CREATE | ZipArchive::OVERWRITE (tempnam creates an existing file). - Share button: uses Web Share API with native file sharing where supported; falls back to wa.me deep-link. Icon changed from fa-brands fa-whatsapp to fa-solid fa-share-nodes. - Lightbox navigation: prev/next arrow buttons with wrap-around and keyboard support (ArrowLeft / ArrowRight / Escape). Code style ---------- - PHP CS Fixer: expanded single-line closures in Configuration/Section/* to multi-line form for consistency with project style rules.
1 parent 274b5d1 commit 6d9f8ce

22 files changed

+1131
-157
lines changed

admin/debug/index.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
echo getNavItemDebug('remotebuzzerlog');
5252
echo getNavItemDebug('synctodrivelog');
5353
echo getNavItemDebug('remotestoragelog');
54+
echo getNavItemDebug('uploadworkerlog');
5455
echo getNavItemDebug('rembglog');
5556
echo getNavItemDebug('devlog');
5657
if (Environment::isLinux()) {

api/admin.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Photobooth\Service\DatabaseManagerService;
1313
use Photobooth\Service\ImageMetadataCacheService;
1414
use Photobooth\Service\LoggerService;
15+
use Photobooth\Service\EncryptionService;
1516
use Photobooth\Service\MailService;
1617
use Photobooth\Service\PrintManagerService;
1718
use Photobooth\Service\ProcessService;
@@ -182,6 +183,14 @@
182183
$newConfig['login']['pin'] = $keepExistingSecret('pin', $newConfig['login']['pin'] ?? null, $config);
183184
$newConfig['login']['rental_pin'] = $keepExistingSecret('rental_pin', $newConfig['login']['rental_pin'] ?? null, $config);
184185

186+
// Keep existing FTP/Mail passwords when the form sends an empty value
187+
if (($newConfig['ftp']['password'] ?? '') === '' && !empty($config['ftp']['password'])) {
188+
$newConfig['ftp']['password'] = $config['ftp']['password'];
189+
}
190+
if (($newConfig['mail']['password'] ?? '') === '' && !empty($config['mail']['password'])) {
191+
$newConfig['mail']['password'] = $config['mail']['password'];
192+
}
193+
185194
// Hash password early when a new value is provided
186195
if (!empty($newConfig['login']['password']) && $newConfig['login']['password'] !== ($config['login']['password'] ?? null)) {
187196
$newConfig['login']['password'] = password_hash($newConfig['login']['password'], PASSWORD_DEFAULT);
@@ -334,6 +343,15 @@
334343
}
335344
}
336345

346+
// Encrypt FTP and Mail passwords before saving to config file
347+
$encryptionService = EncryptionService::getInstance();
348+
if (!empty($newConfig['ftp']['password']) && !$encryptionService->isEncrypted($newConfig['ftp']['password'])) {
349+
$newConfig['ftp']['password'] = $encryptionService->encrypt($newConfig['ftp']['password']);
350+
}
351+
if (!empty($newConfig['mail']['password']) && !$encryptionService->isEncrypted($newConfig['mail']['password'])) {
352+
$newConfig['mail']['password'] = $encryptionService->encrypt($newConfig['mail']['password']);
353+
}
354+
337355
if ($newConfig['logo']['enabled']) {
338356
$logoPath = $newConfig['logo']['path'];
339357

api/applyEffects.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Photobooth\Service\DatabaseManagerService;
1414
use Photobooth\Service\LoggerService;
1515
use Photobooth\Service\RemoteStorageService;
16+
use Photobooth\Service\UploadQueueService;
1617
use Photobooth\Utility\ImageUtility;
1718
use Photobooth\Utility\PathUtility;
1819

@@ -336,13 +337,14 @@
336337
}
337338
}
338339

339-
// Store images on remote storage
340+
// Queue images for async remote storage upload
340341
if ($config['ftp']['enabled']) {
341-
$remoteStorage->write($remoteStorage->getStorageFolder() . '/images/' . $vars['singleImageFile'], (string) file_get_contents($vars['resultFile']));
342-
$remoteStorage->write($remoteStorage->getStorageFolder() . '/thumbs/' . $vars['singleImageFile'], (string) file_get_contents($vars['thumbFile']));
343-
if ($config['ftp']['create_webpage']) {
344-
$remoteStorage->createWebpage();
345-
}
342+
$uploadQueue = UploadQueueService::getInstance();
343+
$uploadQueue->enqueue(
344+
$vars['singleImageFile'],
345+
$vars['singleImageFile'],
346+
(bool) $config['ftp']['create_webpage']
347+
);
346348
}
347349

348350
// Change permissions

api/deletePhoto.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
use Photobooth\Service\ImageMetadataCacheService;
1111
use Photobooth\Service\LoggerService;
1212
use Photobooth\Service\RemoteStorageService;
13+
use Photobooth\Service\UploadQueueService;
1314

1415
header('Content-Type: application/json');
1516

1617
$logger = LoggerService::getInstance()->getLogger('main');
1718
$logger->debug(basename($_SERVER['PHP_SELF']));
1819

19-
$remoteStorage = RemoteStorageService::getInstance();
20-
2120
try {
2221
if (empty($_POST['file'])) {
2322
throw new \Exception('No file provided');
@@ -81,8 +80,11 @@
8180
}
8281

8382
if ($config['ftp']['enabled'] && $config['ftp']['delete']) {
84-
$remoteStorage->delete($remoteStorage->getStorageFolder() . '/images/' . $fileName);
85-
$remoteStorage->delete($remoteStorage->getStorageFolder() . '/thumbs/' . $fileName);
83+
$remoteStorage = RemoteStorageService::getInstance();
84+
$uploadQueue = UploadQueueService::getInstance();
85+
$remoteFilename = $uploadQueue->getRemoteFilename($fileName) ?? $fileName;
86+
$remoteStorage->delete('images/' . $remoteFilename);
87+
$remoteStorage->delete('thumbs/' . $remoteFilename);
8688
}
8789
}
8890

api/ftpListFolders.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
require_once '../lib/boot.php';
4+
5+
use Photobooth\Service\ConfigurationService;
6+
use Photobooth\Service\RemoteStorageService;
7+
8+
header('Content-Type: application/json');
9+
checkCsrfOrFail($_POST);
10+
11+
$ftpData = $_POST['ftp'] ?? [];
12+
13+
$type = (string) ($ftpData['type'] ?? 'ftp');
14+
$host = (string) ($ftpData['baseURL'] ?? '');
15+
$port = (int) ($ftpData['port'] ?? 21);
16+
$username = (string) ($ftpData['username'] ?? '');
17+
$password = (string) ($ftpData['password'] ?? '');
18+
$path = (string) ($_POST['path'] ?? '/');
19+
20+
if ($host === '' || $username === '') {
21+
echo json_encode(['error' => 'Missing connection parameters']);
22+
exit();
23+
}
24+
25+
// If password is empty, use the saved (decrypted) config password
26+
if ($password === '') {
27+
$savedConfig = ConfigurationService::getInstance()->getConfiguration();
28+
$password = (string) ($savedConfig['ftp']['password'] ?? '');
29+
}
30+
31+
try {
32+
$folders = RemoteStorageService::listFolders([
33+
'type' => $type,
34+
'baseURL' => $host,
35+
'port' => $port,
36+
'username' => $username,
37+
'password' => $password,
38+
], $path);
39+
40+
echo json_encode(['folders' => $folders]);
41+
} catch (\Throwable $e) {
42+
echo json_encode(['error' => $e->getMessage()]);
43+
}
44+
exit();

api/print.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Photobooth\Service\LoggerService;
1111
use Photobooth\Service\PrintManagerService;
1212
use Photobooth\Service\RemoteStorageService;
13+
use Photobooth\Service\UploadQueueService;
1314
use Photobooth\Utility\PathUtility;
1415

1516
header('Content-Type: application/json');
@@ -156,10 +157,11 @@
156157
$remoteStorageService = RemoteStorageService::getInstance();
157158
$url = $remoteStorageService->getWebpageUri();
158159
if ($config['qr']['append_filename']) {
159-
$url .= '/images/';
160+
$uploadQueue = UploadQueueService::getInstance();
161+
$remoteFilename = $uploadQueue->getRemoteFilename($vars['fileName']) ?? $vars['fileName'];
162+
$url .= '/?img=' . rawurlencode($remoteFilename);
160163
}
161-
}
162-
if ($config['qr']['append_filename']) {
164+
} elseif ($config['qr']['append_filename']) {
163165
$url .= $vars['fileName'];
164166
}
165167
$imageHandler->qrUrl = PathUtility::getPublicPath($url, true);

api/qrcode.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/** @var array $config */
44

55
use Photobooth\Service\RemoteStorageService;
6+
use Photobooth\Service\UploadQueueService;
67
use Photobooth\Utility\PathUtility;
78
use Photobooth\Utility\QrCodeUtility;
89

@@ -20,12 +21,15 @@
2021
if ($config['ftp']['enabled'] && $config['ftp']['useForQr']) {
2122
$remoteStorageService = RemoteStorageService::getInstance();
2223
$url = $remoteStorageService->getWebpageUri();
23-
if ($config['qr']['append_filename']) {
24-
$url .= '/images/';
25-
}
2624
}
2725
if ($config['qr']['append_filename']) {
28-
$url .= $filename;
26+
if ($config['ftp']['enabled'] && $config['ftp']['useForQr']) {
27+
$uploadQueue = UploadQueueService::getInstance();
28+
$remoteFilename = $uploadQueue->getRemoteFilename($filename) ?? $filename;
29+
$url .= '/?img=' . rawurlencode($remoteFilename);
30+
} else {
31+
$url .= $filename;
32+
}
2933
}
3034
$url = PathUtility::getPublicPath($url, true);
3135
try {

api/serverInfo.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ function handleDebugPanel(string $content, array $config): string|false
2020
return readFileContents(PathUtility::getAbsolutePath('var/log/synctodrive.log'));
2121
case 'nav-remotestoragelog':
2222
return readFileContents(PathUtility::getAbsolutePath('var/log/remotestorage.log'));
23+
case 'nav-uploadworkerlog':
24+
return readFileContents(PathUtility::getAbsolutePath('var/log/uploadworker.log'));
2325
case 'nav-rembglog':
2426
return readFileContents(PathUtility::getAbsolutePath('var/log/rembg.log'));
2527
case 'nav-myconfig':

0 commit comments

Comments
 (0)