Files
projectsend/app/Modules/Files/Console/PurgeOrphanFilesCommand.php
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

72 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Files\Console;
use App\Modules\Audit\Action;
use App\Modules\Audit\ActivityLogger;
use App\Modules\Files\OrphanFileScanner;
use App\Modules\Platform\Settings\ExternalStorageConfigApplier;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class PurgeOrphanFilesCommand extends Command
{
protected $signature = 'projectsend:purge-orphan-files';
protected $description = 'Permanently delete local-disk orphan files found longer ago than the configured grace period (runs daily; never touches external storage)';
public function handle(
Settings $settings,
ActivityLogger $activity,
OrphanFileScanner $scanner,
ExternalStorageConfigApplier $externalStorage,
): int {
// Hardcoded off whenever external storage is active, regardless
// of the stored setting — this feature only ever operates on the
// local disk, on purpose (see FileRetentionSettingsController).
if ($externalStorage->isActive()) {
$this->info('External storage is active — orphan auto-delete only ever runs against local storage, so it is skipped.');
return self::SUCCESS;
}
if ($settings->get(Setting::OrphanFilesAutoDeleteEnabled) !== true) {
$this->info('Auto-delete of orphan files is disabled.');
return self::SUCCESS;
}
$cutoff = now()->subDays((int) $settings->get(Setting::OrphanFilesDeleteAfterDays));
$deleted = 0;
foreach ($scanner->scan() as $orphan) {
// Belt and suspenders: scannedDisks() only ever includes
// 'files_external' when external storage is active, and we
// already returned above when it is — but filter explicitly
// rather than relying on that alone.
if ($orphan['disk'] !== 'files' || $orphan['last_modified'] > $cutoff->timestamp) {
continue;
}
// Re-validate right before deleting — same defensive re-check
// the controller's own destroy() does, in case something
// claimed this path since the scan ran a moment ago.
if (! $scanner->isOrphan($orphan['disk'], $orphan['path'])) {
continue;
}
Storage::disk($orphan['disk'])->delete($orphan['path']);
$activity->logSystem(Action::OrphanFileAutoDeleted, ['name' => basename($orphan['path']), 'disk' => $orphan['disk']]);
$deleted++;
}
$this->info("Deleted {$deleted} orphan file(s).");
return self::SUCCESS;
}
}