baseContext()); } /** * @param array $extra * @return array */ private function baseContext(array $extra = []): array { return array_merge([ 'driver' => 'local', 'root' => rtrim($this->config->localRoot, '/\\'), ], $extra); } private function fullPath(string $key): string { $key = ltrim(str_replace('\\', '/', $key), '/'); return $this->config->localRoot . str_replace('/', DIRECTORY_SEPARATOR, $key); } private function ensureDirectory(string $fullPath): void { $directory = dirname($fullPath); if (! is_dir($directory) && ! mkdir($directory, 0777, true) && ! is_dir($directory)) { StorageLogger::log('FAILURE', 'Unable to create directory', $this->baseContext([ 'directory' => $directory, ])); throw new FileStorageException("Unable to create directory: {$directory}"); } StorageLogger::log('SUCCESS', 'Directory ensured', $this->baseContext([ 'directory' => $directory, ])); } public function upload(UploadedFile|string $source, string $key, array $options = []): string { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Upload started', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); $this->ensureDirectory($fullPath); if ($source instanceof UploadedFile) { if (! $source->isValid()) { StorageLogger::log('FAILURE', 'Upload rejected: invalid uploaded file', $this->baseContext([ 'key' => $key, 'error' => $source->getErrorString() ?: 'Invalid uploaded file', ])); throw new FileStorageException($source->getErrorString() ?: 'Invalid uploaded file'); } if ($source->hasMoved()) { StorageLogger::log('FAILURE', 'Upload rejected: file already moved', $this->baseContext([ 'key' => $key, ])); throw new FileStorageException('Uploaded file has already been moved'); } $source->move(dirname($fullPath), basename($fullPath)); StorageLogger::log('SUCCESS', 'Upload complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'original_name' => $source->getClientName(), 'size' => is_file($fullPath) ? (filesize($fullPath) ?: 0) : 0, ])); return $key; } if (! is_file($source)) { StorageLogger::log('FAILURE', 'Upload rejected: source file not found', $this->baseContext([ 'key' => $key, 'source' => $source, ])); throw new FileStorageException("Source file not found: {$source}"); } if (! copy($source, $fullPath)) { StorageLogger::log('FAILURE', 'Upload failed while copying source file', $this->baseContext([ 'key' => $key, 'source' => $source, ])); throw new FileStorageException("Failed to copy file to {$fullPath}"); } StorageLogger::log('SUCCESS', 'Upload complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'source' => $source, 'size' => is_file($fullPath) ? (filesize($fullPath) ?: 0) : 0, ])); return $key; } public function put(string $key, string $contents, array $options = []): string { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Put started', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'size' => strlen($contents), ])); $this->ensureDirectory($fullPath); if (file_put_contents($fullPath, $contents) === false) { StorageLogger::log('FAILURE', 'Put failed', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); throw new FileStorageException("Failed to write file: {$fullPath}"); } StorageLogger::log('SUCCESS', 'Put complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'size' => strlen($contents), ])); return $key; } public function exists(string $key): bool { $fullPath = $this->fullPath($key); $exists = is_file($fullPath); StorageLogger::log('SUCCESS', 'Exists check complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'exists' => $exists, ])); return $exists; } public function delete(string $key): bool { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Delete started', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); if (! is_file($fullPath)) { StorageLogger::log('WARNING', 'Delete skipped: file not found', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); return false; } $deleted = unlink($fullPath); if ($deleted) { StorageLogger::log('SUCCESS', 'Delete complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); } else { StorageLogger::log('FAILURE', 'Delete failed', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); } return $deleted; } public function read(string $key): string { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Read started', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); if (! is_file($fullPath)) { StorageLogger::log('FAILURE', 'Read failed: file not found', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); throw new FileStorageException("File not found: {$key}"); } $contents = file_get_contents($fullPath); if ($contents === false) { StorageLogger::log('FAILURE', 'Read failed: unable to read file', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); throw new FileStorageException("Unable to read file: {$key}"); } StorageLogger::log('SUCCESS', 'Read complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'size' => strlen($contents), ])); return $contents; } public function downloadToTemp(string $key): string { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Local path resolved for processing', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); if (! is_file($fullPath)) { StorageLogger::log('FAILURE', 'Local path resolution failed: file not found', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); throw new FileStorageException("File not found: {$key}"); } return $fullPath; } public function streamDownload(string $key, ?string $downloadName = null): ResponseInterface { $fullPath = $this->fullPath($key); StorageLogger::log('SUCCESS', 'Stream download started', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'download_name' => $downloadName ?: basename($key), ])); if (! is_file($fullPath)) { StorageLogger::log('FAILURE', 'Stream download failed: file not found', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, ])); throw new FileStorageException("File not found: {$key}"); } StorageLogger::log('SUCCESS', 'Stream download complete', $this->baseContext([ 'key' => $key, 'full_path' => $fullPath, 'download_name' => $downloadName ?: basename($key), 'size' => filesize($fullPath) ?: 0, ])); return service('response')->download($fullPath, null)->setFileName($downloadName ?: basename($key)); } public function temporaryUrl(string $key, int $ttlSeconds = 900): string { StorageLogger::log('SUCCESS', 'Temporary URL generation started', $this->baseContext([ 'key' => $key, 'ttl_seconds' => $ttlSeconds, ])); if (! $this->exists($key)) { StorageLogger::log('FAILURE', 'Temporary URL generation failed: file not found', $this->baseContext([ 'key' => $key, ])); throw new FileStorageException("File not found: {$key}"); } $url = base_url(ltrim(str_replace('\\', '/', $key), '/')); StorageLogger::log('SUCCESS', 'Temporary URL generation complete', $this->baseContext([ 'key' => $key, 'ttl_seconds' => $ttlSeconds, ])); return $url; } public function url(string $key): string { return $this->temporaryUrl($key); } public function copy(string $fromKey, string $toKey): bool { $fromPath = $this->fullPath($fromKey); $toPath = $this->fullPath($toKey); StorageLogger::log('SUCCESS', 'Copy started', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, 'from_path' => $fromPath, 'to_path' => $toPath, ])); if (! is_file($fromPath)) { StorageLogger::log('FAILURE', 'Copy failed: source file not found', $this->baseContext([ 'from_key' => $fromKey, 'from_path' => $fromPath, ])); throw new FileStorageException("Source file not found: {$fromKey}"); } $this->ensureDirectory($toPath); $copied = copy($fromPath, $toPath); if ($copied) { StorageLogger::log('SUCCESS', 'Copy complete', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, ])); } else { StorageLogger::log('FAILURE', 'Copy failed', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, ])); } return $copied; } }