mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 17:45: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.
89 lines
3.4 KiB
PHP
89 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Scanning;
|
|
|
|
use App\Modules\Files\Events\FileBecameAvailable;
|
|
use App\Modules\Files\Models\File;
|
|
use Illuminate\Support\Facades\Event;
|
|
|
|
/**
|
|
* Whether a file may be seen and served, and what happens the moment it
|
|
* may be.
|
|
*
|
|
* The one predicate every other rule asks. Three states mean yes and
|
|
* three mean no (see ScanStatus), and the reason this is a class rather
|
|
* than a comparison at each call site is that the list of "yes" states
|
|
* has already changed once — `released` was added when quarantine gained
|
|
* an override — and the day it changes again, it has to change in one
|
|
* place or a file becomes downloadable through one route and not another.
|
|
*
|
|
* "Available" is about everyone *other than* staff and the uploader. Staff
|
|
* see their library at all times, with each file's state on it; what
|
|
* availability governs is whether recipients and visitors see a file at
|
|
* all, and whether its bytes may leave the server.
|
|
*/
|
|
class FileAvailability
|
|
{
|
|
public function isAvailable(File $file): bool
|
|
{
|
|
return $file->scan_status->isAvailable();
|
|
}
|
|
|
|
/**
|
|
* Refuse to serve a file's bytes unless it is available.
|
|
*
|
|
* Called by every route that puts bytes on the wire — the download,
|
|
* the thumbnail, the preview, the share link, the public listing and
|
|
* the zip builder. Not by the listings: a staff member's library shows
|
|
* a pending file with its state on it, and the uploader sees their own.
|
|
* What this governs is the bytes.
|
|
*
|
|
* It refuses everybody, including staff and the file's own uploader.
|
|
* A file the scanner has not cleared is not one this application
|
|
* hands out, and an administrator who wants it anyway has a way to say
|
|
* so on the record: release it from quarantine.
|
|
*
|
|
* 423 rather than 403: the refusal is about the file's state and it is
|
|
* temporary in the pending case, which is exactly what "Locked" means
|
|
* and what "Forbidden" does not. ProblemDetails renders it as JSON for
|
|
* the API, which shares these controllers.
|
|
*/
|
|
public function guardDelivery(File $file): void
|
|
{
|
|
if ($this->isAvailable($file)) {
|
|
return;
|
|
}
|
|
|
|
abort(423, match ($file->scan_status) {
|
|
ScanStatus::Pending => __('This file is still being checked for viruses.'),
|
|
// Said plainly, because it is not a refusal: there is nothing
|
|
// to serve, and whoever hits this can stop looking for a
|
|
// permission that would let them through.
|
|
ScanStatus::Missing => __('This file is no longer on the server.'),
|
|
default => __('This file is not available.'),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* A file has finished being checked, one way or another.
|
|
*
|
|
* Three roads lead here and they are not interchangeable: the scan
|
|
* passed, the scanner could not be reached and this installation lets
|
|
* files through, or an administrator released it from quarantine. What
|
|
* they share is the only thing this announces — the file can now be
|
|
* had by the people it was shared with, which is when everything that
|
|
* was waiting on it (a share email, a new-version notice) is allowed
|
|
* to go out.
|
|
*/
|
|
public function markAvailable(File $file): void
|
|
{
|
|
if (! $this->isAvailable($file)) {
|
|
return;
|
|
}
|
|
|
|
Event::dispatch(new FileBecameAvailable($file));
|
|
}
|
|
}
|