*/ private const ALLOWED_EXTENSIONS = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'zip', 'rar', '7z', 'tar', 'gz', ]; public function __construct( private readonly UploadStorageService $storage, ) {} public function resolveLocalPath(Artwork $artwork): string { $objectPath = $this->resolveObjectPath($artwork); $prefix = $this->originalObjectPrefix(); if ($objectPath !== '' && str_starts_with($objectPath, $prefix)) { $suffix = substr($objectPath, strlen($prefix)); $root = rtrim($this->storage->localOriginalsRoot(), DIRECTORY_SEPARATOR); return $root . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, (string) $suffix); } $hash = strtolower((string) $artwork->hash); $ext = $this->safeExtension($artwork); if (! $this->isValidHash($hash) || $ext === '') { return ''; } $root = rtrim($this->storage->localOriginalsRoot(), DIRECTORY_SEPARATOR); return $root . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2, 2) . DIRECTORY_SEPARATOR . $hash . '.' . $ext; } /** @return array{status:string,source:?string,disk:?string,path:string,object_key:string,exists:bool,size:?int,file_name:string,file_ext:string} */ public function resolve(Artwork $artwork): array { $fileName = trim((string) ($artwork->file_name ?? '')); $extension = $this->safeExtension($artwork); $localPath = $this->resolveLocalPath($artwork); if ($localPath !== '' && is_file($localPath)) { return $this->result('available', 'local', null, $localPath, '', true, (int) filesize($localPath), $fileName, $extension); } if ($this->isTruePending($artwork)) { return $this->result('pending', null, null, $localPath, '', false, null, $fileName, $extension); } $objectKey = $this->resolveObjectPath($artwork); if ($objectKey !== '') { try { $diskName = $this->storage->objectDiskName(); $disk = Storage::disk($diskName); if ($disk->exists($objectKey)) { return $this->result('available', 'object', $diskName, '', $objectKey, true, (int) $disk->size($objectKey), $fileName, $extension); } } catch (\Throwable) { // Treat unavailable storage as unresolved; the route logs context. } } $legacyPath = $this->resolveReadonlyLegacyPath($artwork); if ($legacyPath !== '' && is_file($legacyPath)) { return $this->result('available', 'legacy', null, $legacyPath, '', true, (int) filesize($legacyPath), $fileName, $extension); } if ($this->hasIncompleteLegacyMetadata($artwork)) { return $this->result('unknown', null, null, $localPath, $objectKey, false, null, $fileName, $extension); } return $this->result('missing', null, null, $localPath, $objectKey, false, null, $fileName, $extension); } private function isTruePending(Artwork $artwork): bool { return strtolower(trim((string) ($artwork->file_name ?? ''))) === 'pending' && trim((string) ($artwork->hash ?? '')) === '' && trim((string) ($artwork->file_ext ?? '')) === '' && trim((string) ($artwork->file_path ?? '')) === ''; } private function hasIncompleteLegacyMetadata(Artwork $artwork): bool { return ! $this->isValidHash(strtolower((string) ($artwork->hash ?? ''))) || $this->safeExtension($artwork) === ''; } private function resolveReadonlyLegacyPath(Artwork $artwork): string { $legacyRoot = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, trim((string) config('uploads.legacy_originals_root', ''))), DIRECTORY_SEPARATOR); $legacyRelative = $this->legacyRelativePath($artwork); if ($legacyRoot !== '' && $legacyRelative !== '') { return $legacyRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $legacyRelative); } $root = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, trim((string) config('uploads.readonly_backup_originals_root', ''))), DIRECTORY_SEPARATOR); $hash = strtolower((string) $artwork->hash); $extension = $this->safeExtension($artwork); if ($root === '' || ! $this->isValidHash($hash) || $extension === '') { return ''; } return $root . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2, 2) . DIRECTORY_SEPARATOR . $hash . '.' . $extension; } private function legacyRelativePath(Artwork $artwork): string { $path = trim((string) ($artwork->file_path ?? ''), '/\\'); if ($path === '' || ! str_starts_with(strtolower($path), 'legacy/uploads/')) { return ''; } $relative = substr($path, strlen('legacy/uploads/')); if ($relative === false || $relative === '' || str_contains($relative, '..')) { return ''; } return 'legacy/uploads/' . ltrim($relative, '/\\'); } private function safeExtension(Artwork $artwork): string { $stored = strtolower(ltrim(trim((string) ($artwork->file_ext ?? '')), '.')); if (in_array($stored, self::ALLOWED_EXTENSIONS, true)) { return $stored; } foreach ([(string) ($artwork->file_name ?? ''), (string) ($artwork->file_path ?? '')] as $candidate) { $inferred = strtolower(ltrim((string) pathinfo($candidate, PATHINFO_EXTENSION), '.')); if (in_array($inferred, self::ALLOWED_EXTENSIONS, true)) { return $inferred; } } return ''; } private function result(string $status, ?string $source, ?string $disk, string $path, string $objectKey, bool $exists, ?int $size, string $fileName, string $extension): array { return [ 'status' => $status, 'source' => $source, 'disk' => $disk, 'path' => $path, 'object_key' => $objectKey, 'exists' => $exists, 'size' => $size, 'file_name' => $fileName, 'file_ext' => $extension, ]; } public function resolveObjectPath(Artwork $artwork): string { $relative = trim((string) $artwork->file_path, '/'); $prefix = $this->originalObjectPrefix(); if ($relative !== '' && (str_starts_with($relative, $prefix) || str_starts_with(strtolower($relative), 'legacy/uploads/'))) { return $relative; } $hash = strtolower((string) $artwork->hash); $ext = $this->safeExtension($artwork); if (! $this->isValidHash($hash) || $ext === '') { return ''; } return $this->storage->objectPathForVariant('original', $hash, $hash . '.' . $ext); } public function resolveObjectUrl(Artwork $artwork): ?string { $objectPath = $this->resolveObjectPath($artwork); if ($objectPath === '') { return null; } return Storage::disk($this->storage->objectDiskName())->url($objectPath); } private function originalObjectPrefix(): string { return trim($this->storage->objectBasePrefix(), '/') . '/original/'; } private function isValidHash(string $hash): bool { return preg_match('/^[a-f0-9]{40}$/', $hash) === 1; } }