From 3f352b0d2644fb3217f114f35a0ebe43e1192d86 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 31 Aug 2026 11:56:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20cleanup=20page=20=E2=80=94=20rem?= =?UTF-8?q?ove=20'Fichiers=20temporaires'=20level,=20promote=20sections=20?= =?UTF-8?q?to=20h2=20TOC=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 25 ++ app/public/admin/README.md | 2 +- .../admin/actions/cleanup-stats-fragment.php | 80 ++---- .../actions/peertube-orphans-fragment.php | 6 +- app/public/admin/cleanup.php | 2 +- app/public/admin/index.php | 268 +++++++++++------- app/public/assets/css/admin.css | 34 +++ app/public/assets/css/components/buttons.css | 56 ++++ .../assets/js/app/admin-cleanup-bulk.js | 37 ++- app/public/assets/js/app/admin-toc.js | 120 +++++--- app/templates/admin/cleanup.php | 23 +- app/templates/admin/index-table.php | 10 +- app/templates/admin/index.php | 9 +- deploy/xamxam-tmp-cleanup.cron | 10 + docs/deployment.md | 27 +- docs/development.md | 2 + justfile | 29 +- scripts/cleanup-tmp-uploads.php | 162 +++++++++++ 18 files changed, 650 insertions(+), 252 deletions(-) create mode 100644 deploy/xamxam-tmp-cleanup.cron create mode 100644 scripts/cleanup-tmp-uploads.php diff --git a/TODO.md b/TODO.md index 7ca3535..bbceb4e 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,32 @@ ## Pending - [x] #home-htmx-lazy-cover-images [!medium] Add htmx lazyloading on home page cover images: `revealed`-triggered cover-fragment endpoint, swap-in ``, settle fade CSS, load htmx on home +## In Progress +- [x] #cleanup-toc-fix-duplicate-id [+admin] Clear TOC links after cleanup restructure. Root cause: admin-toc.js assigned `sec.id = headingId` to sections, and on rebuild-because-of-htmx-swap the section + h2 ended up sharing the same id, so getElementById returned the
whose textContent dragged the whole data table into the TOC label. Fix: anchor links to the heading id and never give the section that id (no duplicate ids); verified via Playwright — TOC now lists exactly Tiléversements abandonnés / Corbeille / Vidéos PeerTube with no table text. + +## In Progress +- [x] #cleanup-stats-under-headings [+admin] On the cleanup page, move each section's stat/count out of the heading to a line directly under it, and merge PeerTube into a single heading. cleanup-stats-fragment: "Téléversements abandonnés" / "Corbeille" h2 now bare, stat emitted as `

` under the h2. peertube-orphans-fragment: removed the separate "Vidéos orphelines" h3; the orphan count now appears as the stat line under the static "Vidéos PeerTube" h2. Verified via Playwright: TOC still lists exactly 3 clean entries; stats render under headings. + +## In Progress +- [x] #cleanup-stat-card [+admin] Render the cleanup-page section stats as a proper card box (`cleanup-stat-card`, bigger than the old inline n-meta): padded panel (bg-secondary + border, radius 10px), step-1/700/accent text. Applied to the filepond, trash and PeerTube stat lines. Verified computed style via Playwright. + +## In Progress +- [x] #cleanup-desc-and-tooltips [+admin] Cleanup page: add short French descriptions under each section heading (Téléversements abandonnés / Corbeille / Vidéos PeerTube) explaining what the data is and how safe deletion is (`.cleanup-section-desc`), and native title tooltips on the Supprimer and Restaurer buttons of each row. Verified via Playwright (descriptions + tooltips render on all rows). + +## In Progress +- [x] #cleanup-auto-gc-uploads [+scripts/deploy] Abandoned FilePond uploads are now garbage-collected automatically (they are risk-free staging dirs never linked to a TFE). Added `scripts/cleanup-tmp-uploads.php` (dry-run by default, mirrors the admin cleanup filepond logic: missing-session via manifest.json + 2h fallback), hourly cron `deploy/xamxam-tmp-cleanup.cron`, `just deploy-tmp-cleanup-cron` + `just deploy-check-tmp-cleanup-log`, wired into `deploy-all-first`, and documented in docs/deployment.md. Tested dry-run + --no-dry-run + active-session protection locally. + +## In Progress +- [x] #cleanup-remove-filepond-section [+admin] Since abandoned FilePond uploads are GC'd by cron, removed the "Téléversements abandonnés" section from the cleanup page and the index "Nettoyer" badge now counts trash only. cleanup-stats-fragment.php renders only the Corbeille section (and "Corbeille vide" when empty); index.php badge is trash-only; admin-cleanup-bulk.js simplified to trash_files[] only. Verified via Playwright: only Corbeille + Vidéos PeerTube sections/TOC, no filepond checkboxes, tooltips intact. + +## In Progress +- [x] #cleanup-page-title [+admin] Simplify cleanup page title from "Nettoyer les fichiers temporaires" to "Nettoyage" (page now mostly manages the Corbeille). Updated `app/public/admin/cleanup.php` pageTitle and `app/templates/admin/cleanup.php` h1. + +## In Progress +- [x] #docs-cleanup-changes [docs] Update docs for the cleanup-page + GC changes made: admin README cleanup.php entry -> "Cleanup page (Corbeille — restore/delete trashed files)"; deployment.md adds a paragraph explaining abandoned uploads are auto-GC'd hourly and the page (titled Nettoyage) now only shows the Corbeille; added `just cleanup-tmp-uploads` recipe and documented it in development.md alongside cleanup-drafts. + ## Completed +- [x] #cleanup-toc [+admin] Add sidebar TOC to admin cleanup page; drop the "Fichiers temporaires" section level and promote "Téléversements abandonnés" / "Corbeille" from h3 to h2 top-level sections (now TOC entries). Made admin-toc.js rebuild on htmx swap/settle so async-loaded fragment sections enter the TOC. - [x] #audit-all-docs-and [!high] Audit all docs/ and classify accurate vs stale - [x] #rewrite-development-md-to-match [!high] Rewrite development.md to match current just dev / app/ layout / PHPUnit - [x] #rewrite-deployment-md-to-match [!high] Rewrite deployment.md to match just deploy / /var/www/xamxam/ / backup diff --git a/app/public/admin/README.md b/app/public/admin/README.md index 628f681..c606751 100644 --- a/app/public/admin/README.md +++ b/app/public/admin/README.md @@ -11,7 +11,7 @@ the XAMXAM TFE database. | `add.php` | Add new thesis form | | `edit.php` | Existing thesis form | | `recapitulatif.php` | Post-submission recap | -| `cleanup.php` | Orphaned-draft / storage cleanup | +| `cleanup.php` | Cleanup page (Corbeille — restore/delete trashed files) | | `system.php` | System dashboard (logs, SMTP/PeerTube status) | | `contenus.php` | Editable content (pages, contacts) | | `contenus-edit.php` | Edit a content page | diff --git a/app/public/admin/actions/cleanup-stats-fragment.php b/app/public/admin/actions/cleanup-stats-fragment.php index 91179a1..a450b58 100644 --- a/app/public/admin/actions/cleanup-stats-fragment.php +++ b/app/public/admin/actions/cleanup-stats-fragment.php @@ -13,34 +13,22 @@ AdminAuth::requireLogin(); require_once __DIR__ . '/_cleanup-stats-data.php'; $d = getCleanupStats(); -$fpStale = $d['filepond_stale_count'] ?? 0; -$fpActive = $d['filepond_active_count'] ?? 0; -$trStale = $d['trash_stale_count'] ?? 0; -$trActive = $d['trash_active_count'] ?? 0; +// Abandoned FilePond uploads are purged automatically by a cron job, so the +// cleanup page only surfaces the manually-managed trash (Corbeille). +$hasTrash = (($d['trash_stale_count'] ?? 0) + ($d['trash_active_count'] ?? 0)) > 0; -$totalStale = $fpStale + $trStale; -$totalFiles = $fpStale + $fpActive + $trStale + $trActive; - -$staleSize = ($d['filepond_stale_size'] ?? 0) + ($d['trash_stale_size'] ?? 0); -$staleHuman = ''; -if ($staleSize >= 1073741824) { $staleHuman = number_format($staleSize / 1073741824, 1) . ' GB'; } -elseif ($staleSize >= 1048576) { $staleHuman = number_format($staleSize / 1048576, 1) . ' MB'; } -elseif ($staleSize >= 1024) { $staleHuman = number_format($staleSize / 1024, 0) . ' KB'; } -elseif ($staleSize > 0) { $staleHuman = $staleSize . ' B'; } - -$fpMeta = $fpStale . ' dossier' . ($fpStale > 1 ? 's' : ''); -if ($fpStale > 0) { - $fpMeta .= ' · ' . ($d['filepond_stale_human'] ?? ''); -} -$trMeta = ($trStale + $trActive) . ' fichier' . (($trStale + $trActive) > 1 ? 's' : ''); -$trTotalSize = ($d['trash_stale_size'] ?? 0) + ($d['trash_active_size'] ?? 0); -$trTotalHuman = humanBytes($trTotalSize); -if (($trStale + $trActive) > 0) { - $trMeta .= ' · ' . $trTotalHuman; +$trMeta = ''; +if ($hasTrash) { + $trCount = ($d['trash_stale_count'] ?? 0) + ($d['trash_active_count'] ?? 0); + $trMeta = $trCount . ' fichier' . ($trCount > 1 ? 's' : ''); + $trTotalSize = ($d['trash_stale_size'] ?? 0) + ($d['trash_active_size'] ?? 0); + if ($trTotalSize > 0) { + $trMeta .= ' · ' . humanBytes($trTotalSize); + } } ?> - -

✓ Aucun fichier temporaire.

+ +

✓ Corbeille vide — aucun fichier à nettoyer.

@@ -78,35 +66,6 @@ if (($trStale + $trActive) > 0) {
- 0): ?> -

Téléversements abandonnés

- - - - - - - - - - - - - -
NomTailleÂge
~ min - -
- - -

Corbeille

+
+

Corbeille

+

Fichiers retirés d'un TFE. Ceux marqués « Restaurable » peuvent être remis dans le TFE d'origine ; les autres sont orphelins (plus aucun TFE lié). La suppression est définitive et sans retour.

+

@@ -147,6 +109,7 @@ if ($hasTrash): ?>
NomTailleÂgeStatut
+
- 0): ?> -

Conservés : - -

- diff --git a/app/public/admin/actions/peertube-orphans-fragment.php b/app/public/admin/actions/peertube-orphans-fragment.php index 4097d89..d101327 100644 --- a/app/public/admin/actions/peertube-orphans-fragment.php +++ b/app/public/admin/actions/peertube-orphans-fragment.php @@ -22,8 +22,9 @@ if (!($d['configured'] ?? false)): ?> 0): ?> -

Vidéos orphelines vidéos orphelines

- +

Vidéos toujours présentes sur l'instance PeerTube mais plus rattachées à aucun TFE du site. Supprimer une vidéo est définitif et sans retour.

+

vidéo 1 ? 's' : '') ?> orpheline 1 ? 's' : '') ?>

+
@@ -35,6 +36,7 @@ if (!($d['configured'] ?? false)): ?>
NomDate
- - - - + + + + + diff --git a/app/templates/admin/index.php b/app/templates/admin/index.php index d5c1d8d..abcbf9b 100644 --- a/app/templates/admin/index.php +++ b/app/templates/admin/index.php @@ -55,20 +55,21 @@ - ✕ Réinitialiser + ✕ Réinitialiser diff --git a/deploy/xamxam-tmp-cleanup.cron b/deploy/xamxam-tmp-cleanup.cron new file mode 100644 index 0000000..d3c4239 --- /dev/null +++ b/deploy/xamxam-tmp-cleanup.cron @@ -0,0 +1,10 @@ +# XAMXAM — abandoned upload garbage-collection cron job +# Installed to /etc/cron.d/xamxam-tmp-cleanup (system cron format: minute hour dom month dow user command) +# +# Deletes staging upload dirs in /tmp/filepond/ that are abandoned: +# - whose referencing PHP session no longer exists, or +# - older than the 2h safety fallback. +# These are never linked to a published TFE, so removing them is risk-free GC. +# Script is a dry-run unless --no-dry-run is passed. +# Age threshold overridable via TMP_UPLOAD_MAX_AGE_SECONDS (default 7200). +5 * * * * www-data php /tmp/cleanup-tmp-uploads.php --no-dry-run >> /var/log/xamxam-tmp-cleanup-$(date +\%Y-\%m-\%d).log 2>&1 diff --git a/docs/deployment.md b/docs/deployment.md index b6340c8..9206b67 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -32,8 +32,8 @@ This chains (each step is also runnable individually): `/var/www/xamxam/.env` (idempotent; see below). 2. `just deploy` — code + Composer deps + migrations + permissions. 3. `just deploy-nginx` — install + apply the nginx config and fix permissions. -4. `just deploy-backup`, `just deploy-cleanup-cron`, `just deploy-logrotate` — - install backup + cleanup cron jobs and log rotation. +4. `just deploy-backup`, `just deploy-cleanup-cron`, `just deploy-tmp-cleanup-cron`, `just deploy-logrotate` — + install backup + cleanup (drafts + abandoned uploads) cron jobs and log rotation. It finishes by telling you what's left to do in `/admin/account` (set the admin password — a fresh DB starts unauthenticated — and configure SMTP/PeerTube @@ -50,7 +50,7 @@ For a full initial rollout *without* the env/nginx steps (backup + cleanup cron only), when those are already handled: ```bash -just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron + deploy-logrotate +just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron + deploy-tmp-cleanup-cron + deploy-logrotate ``` ### Server `APP_KEY` — idempotent, never overwrites @@ -89,8 +89,9 @@ If you ever rotate `APP_KEY`, re-encrypt the SMTP password with | `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) | | `just deploy-verify-permissions` | Check ownership / permissions on the server | -> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` and -> `just deploy-cleanup-cron` to install the backup/cleanup cron jobs, and +> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup`, +> `just deploy-cleanup-cron` and `just deploy-tmp-cleanup-cron` to install the +> backup/cleanup cron jobs, and > `just deploy-logrotate` to install log rotation. A single `just deploy-all-first` > chains all of these together. > The app log directory `/var/log/xamxam/` is provisioned automatically by @@ -136,6 +137,18 @@ Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`), installed via `just deploy-cleanup-cron`, logging to `/var/log/xamxam-cleanup-YYYY-MM-DD.log`. Verify with `just deploy-check-cleanup-log`. +Abandoned FilePond uploads (staging dirs never linked to a published TFE) are +garbage-collected hourly by `/etc/cron.d/xamxam-tmp-cleanup`, installed via +`just deploy-tmp-cleanup-cron`, logging to +`/var/log/xamxam-tmp-cleanup-YYYY-MM-DD.log`. Verify with +`just deploy-check-tmp-cleanup-log`. + +Because abandoned uploads are removed automatically, the admin cleanup page +(`/admin/cleanup.php`, titled *Nettoyage*) now only surfaces the manually-managed +**Corbeille** (trashed files, with per-row restore/delete and bulk actions). The +manual "Téléversements abandonnés" table was removed from the UI; those files +are purged by the cron above. + --- ## Permissions model @@ -226,7 +239,8 @@ just deploy-verify-permissions # expect "All permissions OK" unchanged; if that still fails, force a reinstall. - **Backup/schedule not running** Confirm the cron files are installed (`/etc/cron.d/xamxam-backup`, - `/etc/cron.d/xamxam-cleanup`) and the log files are writable by `www-data`. + `/etc/cron.d/xamxam-cleanup`, `/etc/cron.d/xamxam-tmp-cleanup`) and the log + files are writable by `www-data`. --- @@ -238,6 +252,7 @@ just deploy-verify-permissions # expect "All permissions OK" | `just deploy-nginx` | Apply nginx config | | `just deploy-backup` | Install backup script + cron | | `just deploy-cleanup-cron` | Install orphaned-draft cleanup cron | +| `just deploy-tmp-cleanup-cron` | Install abandoned-upload GC cron | | `just deploy-list-backups` | List server backups | | `just trigger-backup` | Run backup now | | `just reencrypt-password ` | Re-encrypt SMTP password after key rotation | diff --git a/docs/development.md b/docs/development.md index cf4d3e9..6aa0841 100644 --- a/docs/development.md +++ b/docs/development.md @@ -185,6 +185,8 @@ just backup # SQL dump into app/storage/backup_.sql just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/ just cleanup-drafts [--no-dry-run] # remove orphaned drafts > 7 days (default) # OLDER_THAN_HOURS=24 to change the threshold +just cleanup-tmp-uploads [--no-dry-run] # GC abandoned FilePond uploads (>2h or missing session) + # TMP_UPLOAD_MAX_AGE_SECONDS to change the threshold ``` See [database.md](database.md) for the full schema reference. diff --git a/justfile b/justfile index c394f7c..5364e99 100644 --- a/justfile +++ b/justfile @@ -452,6 +452,26 @@ deploy-cleanup-cron: deploy-check-cleanup-log: ssh xamxam "tail -20 /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" +deploy-tmp-cleanup-cron: + # Install cron job for abandoned-upload garbage collection (hourly, 2h threshold). + # Creates /etc/cron.d/xamxam-tmp-cleanup and log file on the server. + @echo "📋 Installing abandoned-upload cleanup cron job…" + rsync -v scripts/cleanup-tmp-uploads.php xamxam:/tmp/cleanup-tmp-uploads.php + ssh xamxam "chmod 755 /tmp/cleanup-tmp-uploads.php" + rsync -v deploy/xamxam-tmp-cleanup.cron xamxam:/tmp/xamxam-tmp-cleanup.cron + ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-tmp-cleanup.cron /etc/cron.d/xamxam-tmp-cleanup && rm -f /tmp/xamxam-tmp-cleanup.cron" + ssh -t xamxam "sudo touch /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log" + @echo "✅ Abandoned-upload cleanup cron installed." + @echo " Cron file: /etc/cron.d/xamxam-tmp-cleanup" + @echo " Script: /tmp/cleanup-tmp-uploads.php" + @echo " Log file: /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log" + @echo "" + @echo "Verify with: just deploy-check-tmp-cleanup-log" + +[group('deploy')] +deploy-check-tmp-cleanup-log: + ssh xamxam "tail -20 /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" + [group('deploy')] deploy-logrotate: # Install /etc/logrotate.d/xamxam for app + nginx + cron logs. @@ -493,7 +513,7 @@ deploy-migrate-storage dry_run='' target_host='xamxam': ssh {{target_host}} 'rm -f /var/www/xamxam/migrate-storage-paths.php' [group('deploy')] -deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-logrotate +deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-tmp-cleanup-cron deploy-logrotate # One-shot: full initial deploy including backup and cleanup cron jobs. # One-shot remote provisioning for a fresh xamxam server. @@ -638,3 +658,10 @@ cleanup-drafts dry_run='': # just cleanup-drafts --no-dry-run # OLDER_THAN_HOURS=24 just cleanup-drafts @php scripts/cleanup-drafts.php {{dry_run}} + +cleanup-tmp-uploads dry_run='': + # List (dry-run) or delete abandoned FilePond uploads (>2h or missing session). + # Pass --no-dry-run to actually delete. Override the age with + # TMP_UPLOAD_MAX_AGE_SECONDS: + # just cleanup-tmp-uploads --no-dry-run + @php scripts/cleanup-tmp-uploads.php {{dry_run}} diff --git a/scripts/cleanup-tmp-uploads.php b/scripts/cleanup-tmp-uploads.php new file mode 100644 index 0000000..db8d4ec --- /dev/null +++ b/scripts/cleanup-tmp-uploads.php @@ -0,0 +1,162 @@ +#!/usr/bin/env php +/tmp/filepond/ (or a private per-user + * subfolder when the uploader uses a named uploader directory). A finished, + * valid upload is moved out of this staging area as soon as its thesis is + * saved, so anything still sitting there is either: + * - an upload whose PHP session no longer exists (importer/editor abandoned + * mid-file, browser closed, form abandoned), or + * - any leftover older than the 2h safety fallback. + * + * These staging dirs are never referenced by any published TFE, so removing + * them is risk-free garbage collection — much like clearing a recycle bin. + * + * The eligibility logic mirrors the admin "cleanup" page so behaviour is + * identical: + * - Strategy 1: a manifest.json that references a now-missing PHP session. + * - Strategy 2: dir older than 2 hours (time-based fallback). + * + * Usage (mirrors cleanup-drafts.php): + * php /tmp/cleanup-tmp-uploads.php # dry-run (list candidates) + * php /tmp/cleanup-tmp-uploads.php --no-dry-run # actually delete + * + * The 2h fallback threshold is overridable via TMP_UPLOAD_MAX_AGE_SECONDS. + * + * Exit codes: 0 on success, 1 on error. + */ + +declare(strict_types=1); + +// Resolve APP_ROOT robustly. In production the app code is deployed flat under +// /var/www/xamxam/ (src/, storage/, templates/ at the root, no app/ subdir), +// and this script may itself live in /tmp. Point at /var/www/xamxam in +// non-CLI-SAPI contexts; local dev (cli-server) keeps the app/ subdir layout. +$prodRoot = '/var/www/xamxam'; +if (is_dir($prodRoot . '/src') && is_file($prodRoot . '/src/Database.php')) { + define('APP_ROOT', $prodRoot); +} else { + define('APP_ROOT', dirname(__DIR__) . '/app'); +} + +// No app classes needed: abandoned-upload GC is purely filesystem-based, +// mirroring the filepond section of the admin cleanup endpoint. +$dryRun = !in_array('--no-dry-run', $argv, true); + +// Storage root: APP_ROOT/storage in both dev (app/storage) and prod +// (/var/www/xamxam/storage), mirroring bootstrap.php's STORAGE_ROOT. +$storageRoot = APP_ROOT . '/storage'; +$filepondDir = $storageRoot . '/tmp/filepond'; + +// Stability threshold (mirrors cleanup-tmp.php): 2 hours by default. +$maxAgeSeconds = (int) (getenv('TMP_UPLOAD_MAX_AGE_SECONDS') ?: 7200); +if ($maxAgeSeconds < 60) { + $maxAgeSeconds = 7200; +} + +// PHP session save path — the web process and cron (both www-data) share it. +$sessionSavePath = session_save_path(); +if (!$sessionSavePath || $sessionSavePath === '') { + $sessionSavePath = sys_get_temp_dir(); +} + +$now = time(); +$removed = 0; +$details = []; + +if (!is_dir($filepondDir)) { + exit(0); // nothing to collect — quiet exit +} + +$items = @scandir($filepondDir); +if ($items === false) { + error_log('[cleanup-tmp-uploads] Unable to read ' . $filepondDir); + exit(1); +} + +foreach ($items as $item) { + if ($item === '.' || $item === '..' || $item === '.gitkeep') { + continue; + } + $dirPath = $filepondDir . '/' . $item; + if (!is_dir($dirPath)) { + continue; + } + + $shouldDelete = false; + $reason = ''; + + $manifestPath = $dirPath . '/manifest.json'; + $ageSeconds = $now - filemtime($dirPath); + + // Strategy 1: session-based (preferred) — the uploader's PHP session is gone. + if (file_exists($manifestPath)) { + $manifest = json_decode((string) file_get_contents($manifestPath), true); + if (is_array($manifest) && !empty($manifest['session_id'])) { + $sessionFile = $sessionSavePath . '/sess_' . $manifest['session_id']; + if (!file_exists($sessionFile)) { + $shouldDelete = true; + $reason = 'session expirée (' . $manifest['session_id'] . ')'; + } + } + } + + // Strategy 2: time-based fallback (no manifest, or session still alive but old). + if (!$shouldDelete && $ageSeconds > $maxAgeSeconds) { + $shouldDelete = true; + $reason = 'plus de ' . intdiv($maxAgeSeconds, 3600) . 'h'; + } + + if (!$shouldDelete) { + continue; + } + + // Candidate found — report it in dry-run, delete it otherwise. + if ($dryRun) { + printf("DRY-RUN → %s (%s)\n", $item, $reason); + continue; + } + + rmdirRecursive($dirPath); + $details[] = "filepond/$item: $reason"; + $removed++; +} + +if ($dryRun) { + exit(0); // nothing deleted — candidates already listed above +} + +if ($removed > 0) { + foreach ($details as $line) { + echo "Deleted {$line}\n"; + } + printf("Garbage-collected %d abandoned upload(s).\n", $removed); +} +// Nothing to collect — quiet exit. +exit(0); + +function rmdirRecursive(string $dir): void +{ + if (!is_dir($dir)) { + return; + } + $entries = @scandir($dir); + if ($entries === false) { + return; + } + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $path = $dir . '/' . $entry; + if (is_dir($path)) { + rmdirRecursive($path); + } else { + @unlink($path); + } + } + @rmdir($dir); +}