Files
projectsend/app/Modules/Files/Console/CheckMissingFilesCommand.php
T
ignacionelson 7119435c3c Make "New scan" mean a new scan, and colour a result by what it is
The button was disabled on a library that had already been scanned
once, which is most of the time and exactly when somebody would press
it — after updating definitions, say. It now re-checks everything
rather than only what was never looked at, which is what its name says.
A rescan keeps each file available until its new verdict arrives, so a
full pass takes nothing offline. The two states it skips are a file
already waiting for its first verdict and one whose bytes are gone.

It is disabled for two honest reasons now — scanning is off, or a scan
is already running — and says which.

Results are green, amber and red: checked and fine, checked and could
not be read, checked and something was found. The badge gained a
warning variant to say the middle one, matching the amber the warning
alert already uses; before this a missing file wore the same red as a
virus.

A file found missing is also stamped with the time it was checked, so
it appears in the Activity list. It is a verdict like any other, and
without the stamp it was decided somewhere nobody could see.
2026-09-16 23:20:34 -03:00

78 lines
3.0 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\MissingFileScanner;
use App\Modules\Files\Models\File;
use App\Modules\Files\Scanning\NotScannedReason;
use App\Modules\Files\Scanning\ScanningConfig;
use App\Modules\Files\Scanning\ScanStatus;
use Illuminate\Console\Command;
/**
* Checks daily that the files this installation lists are actually there.
*
* Its own command rather than part of scanning, because a missing file is
* not a virus question and an installation with no scanner has exactly
* the same problem. It was only ever noticed when something tried to read
* the bytes — a download, or a scan — which means the first person to
* find out was a client clicking a link.
*
* Recovery is part of the job: storage comes back, and a library that
* kept insisting every file was gone would be its own bug.
*/
class CheckMissingFilesCommand extends Command
{
protected $signature = 'projectsend:check-missing-files';
protected $description = 'Check that every file in the database is still on disk (runs daily)';
public function handle(MissingFileScanner $scanner, ActivityLogger $activity, ScanningConfig $scanning): int
{
$gone = $scanner->scan();
$newlyGone = 0;
foreach (array_chunk($gone, 200) as $chunk) {
foreach (File::query()->whereIn('id', $chunk)->where('scan_status', '!=', ScanStatus::Missing)->get() as $file) {
// Stamped like any other verdict: this is the moment the
// file was last looked at, and without it a missing file
// never appears in the Activity list — which is exactly
// where somebody watching would look for it.
$file->forceFill([
'scan_status' => ScanStatus::Missing,
'scan_note' => null,
'scanned_at' => now(),
])->save();
$activity->logSystem(Action::FileMissing, ['id' => $file->id, 'name' => $file->name]);
$newlyGone++;
}
}
$back = $scanner->recovered();
foreach (array_chunk($back, 200) as $chunk) {
// Back to the start rather than to whatever it was before:
// nothing here knows what the scanner had decided about bytes
// that have since been away, and re-checking them is cheap
// next to trusting a verdict about a file that left.
File::query()->whereIn('id', $chunk)->update($scanning->enabled()
? ['scan_status' => ScanStatus::Pending->value, 'scan_note' => null]
: ['scan_status' => ScanStatus::NotScanned->value, 'scan_note' => NotScannedReason::BeforeScanning->value]);
}
$this->info(sprintf(
'%d file(s) are missing from storage (%d newly), %d came back.',
count($gone),
$newlyGone,
count($back),
));
return self::SUCCESS;
}
}