mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 01:25:09 +00:00
f937b4398d
A row whose bytes are gone was recorded as "the scanner could not be
reached". Wrong on screen, and wrong underneath: that is the one reason
the hourly sweep re-queues, so every orphaned row would have been
rescanned hourly forever.
It is its own state now, `missing`, and withheld rather than offered:
a client who sees a file listed and gets an error on the download is
worse off than one who never saw it. Staff still see it, marked, which
is the point — somebody has to decide what to do about it. The refusal
says what it is ("no longer on the server") instead of sending somebody
looking for a permission that would let them through.
A daily `projectsend:check-missing-files` finds them, whether or not
this installation scans for viruses: it is not a virus question, and an
installation with no scanner has exactly the same problem. It compares
one disk listing against the rows rather than asking "does this exist?"
per file, which on object storage would be a request per file per day.
Files that come back — a remount, a restored backup — are picked up on
the next run and re-checked rather than left for dead.
They are listed beside the orphans, which is the same fault seen from
the other end: bytes with no row, rows with no bytes. The tab carries
the count, each row says where the file should be, and removing one
takes the record with it through the deletion that already exists.
The dashboard says how many there are, and so does
`projectsend:status`, because a fleet-wide jump in this is a storage
fault nothing else in that document would show.
93 lines
2.9 KiB
PHP
93 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Scanning;
|
|
|
|
/**
|
|
* Where a file stands with the virus scanner.
|
|
*
|
|
* Availability is not a case here on purpose: three of these mean the
|
|
* file may be served and three mean it may not, and asking
|
|
* FileAvailability rather than comparing cases is what keeps that rule in
|
|
* one place. See docs/feature-virus-scanning.md.
|
|
*/
|
|
enum ScanStatus: string
|
|
{
|
|
/** Waiting to be scanned, or being scanned right now. */
|
|
case Pending = 'pending';
|
|
|
|
/** Scanned, nothing found. */
|
|
case Clean = 'clean';
|
|
|
|
/** A threat was found. Quarantined; `scan_note` is the threat name. */
|
|
case Infected = 'infected';
|
|
|
|
/** Was infected, and an administrator decided to allow it anyway. */
|
|
case Released = 'released';
|
|
|
|
/** Not checked, and allowed through. `scan_note` is a NotScannedReason. */
|
|
case NotScanned = 'not_scanned';
|
|
|
|
/** Could not be checked, and this installation blocks those. Quarantined. */
|
|
case UnscannableBlocked = 'unscannable_blocked';
|
|
|
|
/**
|
|
* The row is here and the bytes are not.
|
|
*
|
|
* Its own state rather than a kind of "not scanned", because what it
|
|
* means for the file is different: nothing can be served, so nothing
|
|
* is offered. A client listing it and getting an error on the
|
|
* download is worse than not seeing it, and staff need to see it
|
|
* precisely because somebody has to decide what to do about it.
|
|
*/
|
|
case Missing = 'missing';
|
|
|
|
/**
|
|
* Whether a file in this state may be seen and downloaded by people
|
|
* other than staff and its uploader.
|
|
*/
|
|
public function isAvailable(): bool
|
|
{
|
|
return match ($this) {
|
|
self::Clean, self::Released, self::NotScanned => true,
|
|
self::Pending, self::Infected, self::UnscannableBlocked, self::Missing => false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The states a query may hand to somebody other than staff.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public static function availableValues(): array
|
|
{
|
|
return array_values(array_map(
|
|
fn (self $status): string => $status->value,
|
|
array_filter(self::cases(), fn (self $status): bool => $status->isAvailable()),
|
|
));
|
|
}
|
|
|
|
/** Whether this state is waiting on an administrator's decision. */
|
|
public function isQuarantined(): bool
|
|
{
|
|
return $this === self::Infected || $this === self::UnscannableBlocked;
|
|
}
|
|
|
|
/**
|
|
* English, and the translation key — what staff see on the file.
|
|
*/
|
|
public function label(): string
|
|
{
|
|
return match ($this) {
|
|
self::Pending => 'Checking for viruses',
|
|
self::Clean => 'Checked',
|
|
self::Infected => 'Quarantined',
|
|
self::Released => 'Released by an administrator',
|
|
self::NotScanned => 'Not scanned',
|
|
self::UnscannableBlocked => 'Blocked: could not be scanned',
|
|
self::Missing => 'Missing from storage',
|
|
};
|
|
}
|
|
}
|