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): array { $session = $this->sessions->getOrFail($sessionId); $originalPath = $this->derivatives->storeOriginal($session->tempPath, $hash); $originalRelative = $this->storage->sectionRelativePath('originals', $hash, 'orig.webp'); $this->artworkFiles->upsert($artworkId, 'orig', $originalRelative, 'image/webp', (int) filesize($originalPath)); $publicAbsolute = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash); $publicRelative = []; foreach ($publicAbsolute as $variant => $absolutePath) { $filename = $variant . '.webp'; $relativePath = $this->storage->publicRelativePath($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; Artwork::query()->whereKey($artworkId)->update([ 'file_name' => basename($originalRelative), 'file_path' => '', 'file_size' => (int) filesize($originalPath), 'mime_type' => 'image/webp', 'hash' => $hash, 'file_ext' => 'webp', '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, ]; } 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, ]); } }