storage->ensureSection('tmp'); $filename = Str::uuid()->toString() . '.upload'; $tempPath = $dir . DIRECTORY_SEPARATOR . $filename; File::put($tempPath, ''); $sessionId = (string) Str::uuid(); $session = $this->sessions->create($sessionId, $userId, $tempPath, UploadSessionStatus::INIT, $ip); $token = $this->tokens->generate($sessionId, $userId); $this->audit->log($userId, 'upload_init', $ip, [ 'session_id' => $sessionId, ]); return new UploadInitResult($session->id, $token, $session->status); } public function receiveToTmp(UploadedFile $file, int $userId, string $ip): UploadSessionData { $stored = $this->storage->storeUploadedFile($file, 'tmp'); $sessionId = (string) Str::uuid(); $session = $this->sessions->create($sessionId, $userId, $stored->path, UploadSessionStatus::TMP, $ip); $this->sessions->updateProgress($sessionId, 10); $this->audit->log($userId, 'upload_received', $ip, [ 'session_id' => $sessionId, 'size' => $stored->size, ]); return $session; } public function validateAndHash(string $sessionId): UploadValidatedFile { $session = $this->sessions->getOrFail($sessionId); $validation = $this->validator->validate($session->tempPath); if (! $validation->ok) { $this->quarantine($session, $validation->reason); return new UploadValidatedFile($validation, null); } $hash = $this->hasher->hashFile($session->tempPath); $this->sessions->updateStatus($sessionId, UploadSessionStatus::VALIDATED); $this->sessions->updateProgress($sessionId, 30); $this->audit->log($session->userId, 'upload_validated', $session->ip, [ 'session_id' => $sessionId, 'hash' => $hash, ]); return new UploadValidatedFile($validation, $hash); } public function scan(string $sessionId): UploadScanResult { $session = $this->sessions->getOrFail($sessionId); $result = $this->scanner->scan($session->tempPath); if (! $result->ok) { $this->quarantine($session, $result->reason); return $result; } $this->sessions->updateStatus($sessionId, UploadSessionStatus::SCANNED); $this->sessions->updateProgress($sessionId, 50); $this->audit->log($session->userId, 'upload_scanned', $session->ip, [ 'session_id' => $sessionId, ]); return $result; } public function processAndPublish(string $sessionId, string $hash, int $artworkId, ?string $originalFileName = null): array { $session = $this->sessions->getOrFail($sessionId); $originalPath = $this->derivatives->storeOriginal($session->tempPath, $hash, $originalFileName); $origFilename = basename($originalPath); $originalRelative = $this->storage->sectionRelativePath('original', $hash, $origFilename); $origMime = File::exists($originalPath) ? File::mimeType($originalPath) : 'application/octet-stream'; $this->artworkFiles->upsert($artworkId, 'orig', $originalRelative, $origMime, (int) filesize($originalPath)); $publicAbsolute = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash); $publicRelative = []; foreach ($publicAbsolute as $variant => $absolutePath) { $filename = $hash . '.webp'; $relativePath = $this->storage->sectionRelativePath($variant, $hash, $filename); $this->artworkFiles->upsert($artworkId, $variant, $relativePath, 'image/webp', (int) filesize($absolutePath)); $publicRelative[$variant] = $relativePath; } $dimensions = @getimagesize($session->tempPath); $width = is_array($dimensions) && isset($dimensions[0]) ? (int) $dimensions[0] : 1; $height = is_array($dimensions) && isset($dimensions[1]) ? (int) $dimensions[1] : 1; $origExt = strtolower(pathinfo($originalPath, PATHINFO_EXTENSION) ?: ''); $downloadFileName = $origFilename; if (is_string($originalFileName) && trim($originalFileName) !== '') { $candidate = basename(str_replace('\\', '/', $originalFileName)); $candidate = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $candidate) ?? ''; $candidate = trim((string) $candidate); if ($candidate !== '') { $candidateExt = strtolower((string) pathinfo($candidate, PATHINFO_EXTENSION)); if ($candidateExt === '' && $origExt !== '') { $candidate .= '.' . $origExt; } $downloadFileName = $candidate; } } Artwork::query()->whereKey($artworkId)->update([ 'file_name' => $downloadFileName, 'file_path' => '', 'file_size' => (int) filesize($originalPath), 'mime_type' => $origMime, 'hash' => $hash, 'file_ext' => $origExt, 'thumb_ext' => 'webp', 'width' => max(1, $width), 'height' => max(1, $height), ]); $this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED); $this->sessions->updateProgress($sessionId, 100); $this->audit->log($session->userId, 'upload_processed', $session->ip, [ 'session_id' => $sessionId, 'hash' => $hash, 'artwork_id' => $artworkId, ]); return [ 'orig' => $originalRelative, 'public' => $publicRelative, ]; } public function originalHashExists(string $hash): bool { return $this->storage->originalHashExists($hash); } private function quarantine(UploadSessionData $session, string $reason): void { $newPath = $this->storage->moveToSection($session->tempPath, 'quarantine'); $this->sessions->updateTempPath($session->id, $newPath); $this->sessions->updateStatus($session->id, UploadSessionStatus::QUARANTINED); $this->sessions->updateFailureReason($session->id, $reason); $this->sessions->updateProgress($session->id, 0); $this->audit->log($session->userId, 'upload_quarantined', $session->ip, [ 'session_id' => $session->id, 'reason' => $reason, ]); } }