mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-04 21:43:57 +00:00
4524b75c9d
A new capability, granted by both editions. Self-hosted installs keep zip downloads as they are. A platform removes it through PROJECTSEND_CAPABILITIES_DISABLED. The free shared instances do this, because building an archive holds the zips worker, the disk and a CPU on a server that thousands of accounts share. - The three zip routes sit behind capability:downloads.zip, so a hand-made request gets a 404, not just a missing button. - BuildZipDownloadJob refuses a build that was queued before the key went away. The row ends failed and is never stamped as started. StalledZipBuilds stays quiet when the key is off, so leftover rows raise no worker banner. - The zip buttons are hidden. In the portal, the checkboxes and the selection bar are hidden too, since they exist only to pick files for a zip. Staff /files keeps its checkboxes, which also drive bulk edit. - Archives already built are not touched. They expire on the normal purge schedule. - A guard test walks the router. It fails if any route that reaches ZipDownloadsController, or dispatches the build job, lacks the middleware. No API route builds zips today. The case goes last in the enum, because the control plane reads keys in enum order.
92 lines
3.3 KiB
PHP
92 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Queue;
|
|
|
|
use App\Modules\Files\Models\ZipDownload;
|
|
use App\Modules\Platform\Capabilities\Capability;
|
|
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* Whether anything is consuming the `zips` queue.
|
|
*
|
|
* The application cannot see its own worker processes; it can only see
|
|
* whether work gets done. So the question is asked from the other end —
|
|
* a build that was requested a while ago and that no worker ever picked
|
|
* up means nobody is listening to that queue.
|
|
*
|
|
* Which is a real configuration, not a hypothetical one. Zip building
|
|
* moved onto its own queue, and a manual install whose worker command
|
|
* still reads a plain `queue:work` consumes `default` and nothing else.
|
|
* It goes on sending every email perfectly while no zip download ever
|
|
* finishes, and nothing in any log says why — the worst shape a
|
|
* misconfiguration can take, and the reason this is worth a banner
|
|
* rather than a line in a release note.
|
|
*
|
|
* Two conditions, because one of them alone cries wolf:
|
|
*
|
|
* - a build has been waiting past GRACE and was never started; and
|
|
* - no other build is in hand right now.
|
|
*
|
|
* The second matters because one worker builds one archive at a time. A
|
|
* queue behind a large build is a healthy queue, and its waiting rows
|
|
* look exactly like abandoned ones until you notice something running.
|
|
* "In hand" is itself bounded by the job's own timeout: a build that
|
|
* started three hours ago is not in progress, it is a worker that died
|
|
* holding it.
|
|
*/
|
|
class StalledZipBuilds
|
|
{
|
|
/**
|
|
* Long enough that an ordinary wait never trips it, short enough to
|
|
* be found on the day the install is upgraded rather than the week.
|
|
*/
|
|
private const GRACE_MINUTES = 5;
|
|
|
|
/**
|
|
* Matches BuildZipDownloadJob::$timeout. Past it, a build that
|
|
* started is not running any more — the worker died holding it, and
|
|
* the queue is as unattended as if it had never begun.
|
|
*/
|
|
private const IN_HAND_MINUTES = 60;
|
|
|
|
/**
|
|
* The oldest build nothing ever picked up, or null when the queue is
|
|
* being served.
|
|
*/
|
|
public function oldestUnstarted(): ?Carbon
|
|
{
|
|
// An installation that does not offer zips has no reason to be
|
|
// serving their queue, and one that stopped offering them may
|
|
// still hold rows queued before it did. BuildZipDownloadJob fails
|
|
// those when a worker reaches them; until one does, they are not
|
|
// a worker problem worth a banner.
|
|
if (! app(CapabilityRegistry::class)->has(Capability::ZipDownloads)) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->buildInHand()) {
|
|
return null;
|
|
}
|
|
|
|
$waiting = ZipDownload::query()
|
|
->where('status', ZipDownload::STATUS_PENDING)
|
|
->whereNull('started_at')
|
|
->where('created_at', '<', now()->subMinutes(self::GRACE_MINUTES))
|
|
->min('created_at');
|
|
|
|
return $waiting === null ? null : Carbon::parse($waiting);
|
|
}
|
|
|
|
private function buildInHand(): bool
|
|
{
|
|
return ZipDownload::query()
|
|
->where('status', ZipDownload::STATUS_PENDING)
|
|
->whereNotNull('started_at')
|
|
->where('started_at', '>', now()->subMinutes(self::IN_HAND_MINUTES))
|
|
->exists();
|
|
}
|
|
}
|