mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 09:53:08 +02:00
163 lines
5.1 KiB
PHP
163 lines
5.1 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* cleanup-tmp-uploads.php — Garbage-collect abandoned FilePond uploads.
|
|
*
|
|
* Abandoned uploads live in <storage>/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);
|
|
}
|