storageDir = $this->kernel->getProjectDir().'/var/storage/documents'; } public function getStorageDir(): string { return $this->storageDir; } public function getAbsolutePath(string $relativePath): string { $absolutePath = $this->storageDir.'/'.$relativePath; $realPath = realpath($absolutePath); if (false !== $realPath && !str_starts_with($realPath, realpath($this->storageDir))) { throw new RuntimeException(sprintf('Path traversal detected: "%s"', $relativePath)); } return $absolutePath; } /** * Store binary content and return the relative path. * Path format: {year}/{month}/{documentId}.{ext}. */ public function store(string $content, string $documentId, string $extension): string { $now = new DateTimeImmutable(); $subDir = $now->format('Y').'/'.$now->format('m'); $relativePath = $subDir.'/'.$documentId.'.'.$extension; $absolutePath = $this->storageDir.'/'.$relativePath; $dir = dirname($absolutePath); if (!is_dir($dir)) { if (!mkdir($dir, 0o775, true) && !is_dir($dir)) { throw new RuntimeException(sprintf('Cannot create directory "%s"', $dir)); } } $bytesWritten = file_put_contents($absolutePath, $content); if (false === $bytesWritten) { throw new RuntimeException(sprintf('Cannot write file "%s"', $absolutePath)); } return $relativePath; } /** * Store a file from a given source path (e.g., temp upload). */ public function storeFromPath(string $sourcePath, string $documentId, string $extension): string { $content = file_get_contents($sourcePath); if (false === $content) { throw new RuntimeException(sprintf('Cannot read source file "%s"', $sourcePath)); } return $this->store($content, $documentId, $extension); } public function read(string $relativePath): string { $absolutePath = $this->getAbsolutePath($relativePath); if (!file_exists($absolutePath)) { throw new RuntimeException(sprintf('File not found: "%s"', $absolutePath)); } $content = file_get_contents($absolutePath); if (false === $content) { throw new RuntimeException(sprintf('Cannot read file "%s"', $absolutePath)); } return $content; } public function delete(string $relativePath): bool { $absolutePath = $this->getAbsolutePath($relativePath); if (!file_exists($absolutePath)) { return false; } return @unlink($absolutePath); } public function exists(string $relativePath): bool { return file_exists($this->getAbsolutePath($relativePath)); } public function isBase64DataUri(string $path): bool { return str_starts_with($path, 'data:'); } public function extensionFromMimeType(string $mimeType): string { $map = [ 'application/pdf' => 'pdf', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/svg+xml' => 'svg', 'image/bmp' => 'bmp', 'text/plain' => 'txt', 'text/csv' => 'csv', 'application/json' => 'json', 'application/xml' => 'xml', 'application/zip' => 'zip', 'audio/mpeg' => 'mp3', 'audio/ogg' => 'ogg', 'video/mp4' => 'mp4', 'video/webm' => 'webm', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/msword' => 'doc', 'application/vnd.ms-excel' => 'xls', ]; return $map[$mimeType] ?? 'bin'; } public function extensionFromFilename(string $filename): string { $ext = pathinfo($filename, PATHINFO_EXTENSION); return '' !== $ext ? strtolower($ext) : 'bin'; } }