filesystem = $filesystem; $this->diskName = $diskName; $this->disk = $this->filesystem->disk($this->diskName); } public function createDraft(array $attributes = []): array { $id = (string) Str::uuid(); $path = trim($this->basePath, '/') . '/' . $id; if (! $this->disk->exists($path)) { $this->disk->makeDirectory($path); } $meta = array_merge(['id' => $id, 'created_at' => Carbon::now()->toISOString()], $attributes); DB::table('uploads')->insert([ 'id' => $id, 'user_id' => (int) ($attributes['user_id'] ?? 0), 'type' => (string) ($attributes['type'] ?? 'image'), 'status' => 'draft', 'moderation_status' => 'pending', 'processing_state' => 'pending_scan', 'expires_at' => null, 'created_at' => now(), 'updated_at' => now(), ]); $this->writeMeta($id, $meta); return ['id' => $id, 'path' => $path, 'meta' => $meta]; } public function storeMainFile(string $draftId, UploadedFile $file): array { $dir = trim($this->basePath, '/') . '/' . $draftId . '/main'; if (! $this->disk->exists($dir)) { $this->disk->makeDirectory($dir); } $filename = time() . '_' . preg_replace('/[^A-Za-z0-9_\.-]/', '_', $file->getClientOriginalName()); $storedPath = $this->disk->putFileAs($dir, $file, $filename); $size = $this->safeSize($storedPath, $file); $mime = $file->getClientMimeType() ?? $this->safeMimeType($storedPath); $hash = $this->calculateHash($file->getRealPath() ?: $storedPath); $info = ['path' => $storedPath, 'size' => $size, 'mime' => $mime, 'hash' => $hash]; $meta = $this->readMeta($draftId); $meta['main_file'] = $info; $this->writeMeta($draftId, $meta); DB::table('upload_files')->insert([ 'upload_id' => $draftId, 'path' => $storedPath, 'type' => 'main', 'hash' => $hash, 'size' => $size, 'mime' => $mime, 'created_at' => now(), ]); return $info; } public function storeScreenshot(string $draftId, UploadedFile $file): array { $dir = trim($this->basePath, '/') . '/' . $draftId . '/screenshots'; if (! $this->disk->exists($dir)) { $this->disk->makeDirectory($dir); } $filename = time() . '_' . preg_replace('/[^A-Za-z0-9_\.-]/', '_', $file->getClientOriginalName()); $storedPath = $this->disk->putFileAs($dir, $file, $filename); $size = $this->safeSize($storedPath, $file); $mime = $file->getClientMimeType() ?? $this->safeMimeType($storedPath); $hash = $this->calculateHash($file->getRealPath() ?: $storedPath); $info = ['path' => $storedPath, 'size' => $size, 'mime' => $mime, 'hash' => $hash]; $meta = $this->readMeta($draftId); $meta['screenshots'][] = $info; $this->writeMeta($draftId, $meta); DB::table('upload_files')->insert([ 'upload_id' => $draftId, 'path' => $storedPath, 'type' => 'screenshot', 'hash' => $hash, 'size' => $size, 'mime' => $mime, 'created_at' => now(), ]); return $info; } public function calculateHash(string $filePath): string { // If path points to a local filesystem file if (is_file($filePath)) { return hash_file('sha256', $filePath); } // If path is a storage-relative path if ($this->disk->exists($filePath)) { $contents = $this->disk->get($filePath); return hash('sha256', $contents); } throw new \RuntimeException('File not found for hashing: ' . $filePath); } public function setExpiration(string $draftId, ?Carbon $expiresAt = null): bool { $meta = $this->readMeta($draftId); $meta['expires_at'] = $expiresAt?->toISOString(); $this->writeMeta($draftId, $meta); DB::table('uploads')->where('id', $draftId)->update([ 'expires_at' => $expiresAt, 'updated_at' => now(), ]); return true; } protected function metaPath(string $draftId): string { return trim($this->basePath, '/') . '/' . $draftId . '/meta.json'; } protected function readMeta(string $draftId): array { $path = $this->metaPath($draftId); if (! $this->disk->exists($path)) { return []; } $raw = $this->disk->get($path); $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } protected function writeMeta(string $draftId, array $meta): void { $path = $this->metaPath($draftId); $this->disk->put($path, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } protected function safeSize(string $storedPath, UploadedFile $file): int { try { return $this->disk->size($storedPath); } catch (\Throwable $e) { return (int) $file->getSize(); } } protected function safeMimeType(string $storedPath): ?string { try { return $this->disk->mimeType($storedPath); } catch (\Throwable $e) { return null; } } }