Track original artwork file availability more accurately.
Resolve local and object-storage originals, persist download status, and audit missing files without guessing from a single path.
This commit is contained in:
@@ -18,6 +18,9 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
{--id= : Audit only this artwork ID}
|
||||
{--limit= : Stop after processing this many artworks}
|
||||
{--chunk=500 : Number of artworks to scan per batch}
|
||||
{--status= : Only inspect records with this persisted download status}
|
||||
{--missing-only : Only report unresolved records}
|
||||
{--mark-status : Persist the resolved availability status (explicit write)}
|
||||
{--restore-missing : Copy missing local originals from object storage when available}';
|
||||
|
||||
protected $description = 'Scan artworks in descending ID order and report missing local download files with full URLs.';
|
||||
@@ -28,6 +31,8 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
$limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
|
||||
$chunkSize = max(1, min((int) $this->option('chunk'), 2000));
|
||||
$restoreMissing = (bool) $this->option('restore-missing');
|
||||
$markStatus = (bool) $this->option('mark-status');
|
||||
$statusFilter = $this->option('status');
|
||||
|
||||
$this->info(sprintf(
|
||||
'Starting download file audit. order=desc include_trashed=yes chunk=%d limit=%s restore_missing=%s',
|
||||
@@ -41,10 +46,11 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
$unresolved = 0;
|
||||
$restored = 0;
|
||||
$restoreFailed = 0;
|
||||
$classifications = [];
|
||||
$lastSeenId = null;
|
||||
|
||||
do {
|
||||
$artworks = $this->nextChunk($artworkId, $chunkSize, $lastSeenId);
|
||||
$artworks = $this->nextChunk($artworkId, $chunkSize, $lastSeenId, is_string($statusFilter) ? $statusFilter : null);
|
||||
if ($artworks->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -54,41 +60,59 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
break 2;
|
||||
}
|
||||
|
||||
$localPath = $locator->resolveLocalPath($artwork);
|
||||
$missingReason = null;
|
||||
|
||||
if ($localPath === '') {
|
||||
$missingReason = 'unresolved_local_path';
|
||||
$resolution = $locator->resolve($artwork);
|
||||
$classification = match ($resolution['status']) {
|
||||
'available' => strtoupper((string) $resolution['source']).'_OK',
|
||||
'pending' => 'PENDING',
|
||||
'unknown' => 'AMBIGUOUS',
|
||||
default => 'MISSING_EVERYWHERE',
|
||||
};
|
||||
$classifications[$classification] = ($classifications[$classification] ?? 0) + 1;
|
||||
$localPath = (string) $resolution['path'];
|
||||
$missingReason = $resolution['status'] === 'missing' ? 'missing_everywhere' : null;
|
||||
if ($missingReason !== null) {
|
||||
$unresolved++;
|
||||
} elseif (! File::isFile($localPath)) {
|
||||
$missingReason = 'missing_local_file';
|
||||
}
|
||||
|
||||
if ($markStatus) {
|
||||
$this->persistStatus($artwork, $resolution);
|
||||
}
|
||||
|
||||
if ($missingReason === null && (bool) $this->option('missing-only')) {
|
||||
$processed++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($missingReason === null) {
|
||||
$this->line(sprintf('Artwork %d %s source=%s', (int) $artwork->id, $classification, (string) $resolution['source']));
|
||||
}
|
||||
|
||||
if ($missingReason !== null) {
|
||||
$objectPath = $locator->resolveObjectPath($artwork);
|
||||
$objectPath = (string) $resolution['object_key'];
|
||||
$objectUrl = $locator->resolveObjectUrl($artwork);
|
||||
|
||||
$missing++;
|
||||
$this->warn(sprintf('Artwork %d %s', (int) $artwork->id, $missingReason));
|
||||
$this->line(' artwork_url: ' . route('art.show', [
|
||||
$this->line(' artwork_url: '.route('art.show', [
|
||||
'id' => (int) $artwork->id,
|
||||
'slug' => (string) ($artwork->slug ?? ''),
|
||||
]));
|
||||
$this->line(' download_url: ' . route('art.download', ['id' => (int) $artwork->id]));
|
||||
$this->line(' download_url: '.route('art.download', ['id' => (int) $artwork->id]));
|
||||
|
||||
if ($objectPath !== '') {
|
||||
$this->line(' object_path: ' . $objectPath);
|
||||
$this->line(' object_path: '.$objectPath);
|
||||
}
|
||||
|
||||
if ($objectUrl !== null && $objectUrl !== '') {
|
||||
$this->line(' object_url: ' . $objectUrl);
|
||||
$this->line(' object_url: '.$objectUrl);
|
||||
}
|
||||
|
||||
if ($localPath !== '') {
|
||||
$this->line(' local_path: ' . $localPath);
|
||||
$this->line(' local_path: '.$localPath);
|
||||
}
|
||||
|
||||
if ($restoreMissing && $missingReason === 'missing_local_file' && $localPath !== '') {
|
||||
if ($restoreMissing && $missingReason === 'missing_everywhere' && $localPath !== '') {
|
||||
$restoreResult = $this->restoreLocalFile($storage, $objectPath, $localPath);
|
||||
|
||||
if ($restoreResult === 'restored') {
|
||||
@@ -120,6 +144,9 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
$restored,
|
||||
$restoreFailed,
|
||||
));
|
||||
foreach ($classifications as $classification => $count) {
|
||||
$this->line(sprintf('classification=%s count=%d', $classification, $count));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
@@ -127,11 +154,15 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
/**
|
||||
* @return Collection<int, Artwork>
|
||||
*/
|
||||
private function nextChunk(?int $artworkId, int $chunkSize, ?int $lastSeenId): Collection
|
||||
private function nextChunk(?int $artworkId, int $chunkSize, ?int $lastSeenId, ?string $status): Collection
|
||||
{
|
||||
if ($artworkId !== null && $lastSeenId !== null) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = Artwork::query()
|
||||
->withTrashed()
|
||||
->select(['id', 'slug', 'file_path', 'hash', 'file_ext'])
|
||||
->select(['id', 'slug', 'file_name', 'file_path', 'hash', 'file_ext'])
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($artworkId !== null) {
|
||||
@@ -140,9 +171,30 @@ final class AuditArtworkDownloadFilesCommand extends Command
|
||||
$query->where('id', '<', $lastSeenId);
|
||||
}
|
||||
|
||||
if ($status !== null && $status !== '') {
|
||||
$query->where('download_status', $status);
|
||||
}
|
||||
|
||||
return $query->limit($chunkSize)->get();
|
||||
}
|
||||
|
||||
/** @param array{status:string,source:?string} $resolution */
|
||||
private function persistStatus(Artwork $artwork, array $resolution): void
|
||||
{
|
||||
$status = match ($resolution['status']) {
|
||||
'available' => 'available',
|
||||
'pending' => 'pending',
|
||||
'unknown' => 'unknown',
|
||||
default => 'missing',
|
||||
};
|
||||
|
||||
$artwork->forceFill([
|
||||
'download_status' => $status,
|
||||
'download_source' => $resolution['source'],
|
||||
'download_checked_at' => now(),
|
||||
])->saveQuietly();
|
||||
}
|
||||
|
||||
private function restoreLocalFile(UploadStorageService $storage, string $objectPath, string $localPath): string
|
||||
{
|
||||
if ($objectPath === '') {
|
||||
|
||||
Reference in New Issue
Block a user