config = $config ?? config('Storage'); StorageLogger::log('SUCCESS', 'File storage service initializing', $this->baseContext()); try { $this->driver = $this->createDriver($this->config->driver); StorageLogger::log('SUCCESS', 'File storage service initialized', $this->baseContext([ 'retain_local' => $this->config->retainLocal, ])); } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'File storage service initialization failed', $this->baseContext([ 'error' => $e->getMessage(), ])); throw $e; } } /** * @param array $extra * @return array */ private function baseContext(array $extra = []): array { return array_merge([ 'driver' => $this->config->driver, 'bucket' => $this->config->s3Bucket !== '' ? $this->config->s3Bucket : null, ], $extra); } public function driver(): FileStorageInterface { return $this->driver; } public function isS3(): bool { return $this->config->driver === 's3'; } public function resolveKey(string $module, string $subFolder, string $fileName): string { $module = trim($module, '/'); $subFolder = trim($subFolder, '/'); $fileName = ltrim(str_replace('\\', '/', $fileName), '/'); if ($subFolder === '') { return "uploads/{$module}/{$fileName}"; } return "uploads/{$module}/{$subFolder}/{$fileName}"; } public function upload(UploadedFile|string $source, string $key, array $options = []): string { StorageLogger::log('SUCCESS', 'Service upload started', $this->baseContext([ 'key' => $key, 'source' => $source instanceof UploadedFile ? 'uploaded_file' : 'path', ])); try { $storedKey = $this->driver->upload($source, $key, $options); if ($this->config->retainLocal && $this->isS3()) { StorageLogger::log('SUCCESS', 'Retain local copy started', $this->baseContext([ 'key' => $key, ])); try { (new LocalStorageDriver($this->config))->upload($source, $key, $options); StorageLogger::log('SUCCESS', 'Retain local copy complete', $this->baseContext([ 'key' => $key, ])); } catch (\Throwable $e) { StorageLogger::log('WARNING', 'Retain local copy failed after primary upload', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); } } StorageLogger::log('SUCCESS', 'Service upload complete', $this->baseContext([ 'key' => $storedKey, ])); return $storedKey; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service upload failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function uploadModuleFile( UploadedFile $file, string $module, string $subFolder = '', ?string $fileName = null ): string { $fileName = $fileName ?: (time() . '_' . $file->getRandomName()); $key = $this->resolveKey($module, $subFolder, $fileName); StorageLogger::log('SUCCESS', 'Module upload started', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, 'original_name' => $file->getClientName(), 'client_mime' => $file->getClientMimeType(), 'size' => $file->getSize(), ])); $this->upload($file, $key); StorageLogger::log('SUCCESS', 'Module upload complete', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, ])); return $fileName; } public function put(string $key, string $contents, array $options = []): string { StorageLogger::log('SUCCESS', 'Service put started', $this->baseContext([ 'key' => $key, 'size' => strlen($contents), ])); try { $storedKey = $this->driver->put($key, $contents, $options); if ($this->config->retainLocal && $this->isS3()) { StorageLogger::log('SUCCESS', 'Retain local put started', $this->baseContext([ 'key' => $key, ])); try { (new LocalStorageDriver($this->config))->put($key, $contents, $options); StorageLogger::log('SUCCESS', 'Retain local put complete', $this->baseContext([ 'key' => $key, ])); } catch (\Throwable $e) { StorageLogger::log('WARNING', 'Retain local put failed after primary put', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); } } StorageLogger::log('SUCCESS', 'Service put complete', $this->baseContext([ 'key' => $storedKey, ])); return $storedKey; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service put failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function exists(string $module, string $subFolder, string $fileName): bool { $key = $this->resolveKey($module, $subFolder, $fileName); $exists = $this->driver->exists($key); StorageLogger::log('SUCCESS', 'Service exists check complete', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, 'exists' => $exists, ])); return $exists; } public function existsKey(string $key): bool { $exists = $this->driver->exists($key); StorageLogger::log('SUCCESS', 'Service exists key check complete', $this->baseContext([ 'key' => $key, 'exists' => $exists, ])); return $exists; } public function delete(string $module, string $subFolder, string $fileName): bool { $key = $this->resolveKey($module, $subFolder, $fileName); $deleted = $this->driver->delete($key); StorageLogger::log($deleted ? 'SUCCESS' : 'WARNING', 'Service delete complete', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, 'deleted' => $deleted, ])); return $deleted; } public function deleteKey(string $key): bool { $deleted = $this->driver->delete($key); StorageLogger::log($deleted ? 'SUCCESS' : 'WARNING', 'Service delete key complete', $this->baseContext([ 'key' => $key, 'deleted' => $deleted, ])); return $deleted; } public function read(string $module, string $subFolder, string $fileName): string { $key = $this->resolveKey($module, $subFolder, $fileName); StorageLogger::log('SUCCESS', 'Service read started', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, ])); try { $contents = $this->driver->read($key); StorageLogger::log('SUCCESS', 'Service read complete', $this->baseContext([ 'key' => $key, 'size' => strlen($contents), ])); return $contents; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service read failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function readKey(string $key): string { StorageLogger::log('SUCCESS', 'Service read key started', $this->baseContext([ 'key' => $key, ])); try { $contents = $this->driver->read($key); StorageLogger::log('SUCCESS', 'Service read key complete', $this->baseContext([ 'key' => $key, 'size' => strlen($contents), ])); return $contents; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service read key failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function download(string $module, string $subFolder, string $fileName): ResponseInterface { $key = $this->resolveKey($module, $subFolder, $fileName); StorageLogger::log('SUCCESS', 'Service download started', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, ])); try { $response = $this->driver->streamDownload($key, $fileName); StorageLogger::log('SUCCESS', 'Service download complete', $this->baseContext([ 'key' => $key, 'file_name' => $fileName, ])); return $response; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service download failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function downloadKey(string $key, ?string $downloadName = null): ResponseInterface { StorageLogger::log('SUCCESS', 'Service download key started', $this->baseContext([ 'key' => $key, 'download_name' => $downloadName ?: basename($key), ])); try { $response = $this->driver->streamDownload($key, $downloadName ?: basename($key)); StorageLogger::log('SUCCESS', 'Service download key complete', $this->baseContext([ 'key' => $key, 'download_name' => $downloadName ?: basename($key), ])); return $response; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service download key failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function getLocalPathForProcessing(string $module, string $subFolder, string $fileName): string { return $this->getLocalPathForKey($this->resolveKey($module, $subFolder, $fileName)); } public function getLocalPathForKey(string $key): string { StorageLogger::log('SUCCESS', 'Service local path resolution started', $this->baseContext([ 'key' => $key, ])); try { if ($this->config->driver === 'local') { $path = rtrim($this->config->localRoot, '/\\') . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $key), '/\\'); } else { $path = $this->driver->downloadToTemp($key); } StorageLogger::log('SUCCESS', 'Service local path resolution complete', $this->baseContext([ 'key' => $key, 'path' => $path, ])); return $path; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service local path resolution failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } /** * Delete a path returned by getLocalPathForProcessing / downloadToTemp when it lives * under the OS temp directory (S3 working copies). Never deletes local writable/uploads files. */ public function cleanupTempPath(?string $path): bool { if ($path === null || $path === '') { return false; } if (! is_file($path)) { return false; } if (! $this->isS3TempProcessingPath($path)) { return false; } $deleted = @unlink($path); StorageLogger::log($deleted ? 'SUCCESS' : 'WARNING', 'Temp processing path cleanup', $this->baseContext([ 'path' => $path, 'deleted' => $deleted, ])); return $deleted; } /** * True for S3 downloadToTemp paths like /tmp/s3__. */ public function isS3TempProcessingPath(string $path): bool { $tempRoot = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, sys_get_temp_dir()), DIRECTORY_SEPARATOR); $normalized = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); $realTemp = realpath(sys_get_temp_dir()); if ($realTemp !== false) { $tempRoot = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $realTemp), DIRECTORY_SEPARATOR); } $realPath = realpath($path); $checkPath = $realPath !== false ? str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $realPath) : $normalized; if (! str_starts_with($checkPath, $tempRoot . DIRECTORY_SEPARATOR)) { return false; } return str_starts_with(basename($checkPath), 's3_'); } public function getTemporaryUrl(string $module, string $subFolder, string $fileName, ?int $ttlSeconds = null): string { return $this->getTemporaryUrlForKey( $this->resolveKey($module, $subFolder, $fileName), $ttlSeconds ); } public function getTemporaryUrlForKey(string $key, ?int $ttlSeconds = null): string { StorageLogger::log('SUCCESS', 'Service temporary URL started', $this->baseContext([ 'key' => $key, 'ttl_seconds' => $ttlSeconds ?? $this->config->presignedTtl, ])); try { $url = $this->driver->temporaryUrl($key, $ttlSeconds ?? $this->config->presignedTtl); StorageLogger::log('SUCCESS', 'Service temporary URL complete', $this->baseContext([ 'key' => $key, 'ttl_seconds' => $ttlSeconds ?? $this->config->presignedTtl, ])); return $url; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service temporary URL failed', $this->baseContext([ 'key' => $key, 'error' => $e->getMessage(), ])); throw $e; } } public function getUrl(string $module, string $subFolder, string $fileName): string { $key = $this->resolveKey($module, $subFolder, $fileName); $url = $this->driver->url($key); StorageLogger::log('SUCCESS', 'Service URL resolved', $this->baseContext([ 'module' => $module, 'sub_folder' => $subFolder, 'file_name' => $fileName, 'key' => $key, ])); return $url; } public function copy(string $fromModule, string $fromSubFolder, string $fromFileName, string $toModule, string $toSubFolder, string $toFileName): bool { $fromKey = $this->resolveKey($fromModule, $fromSubFolder, $fromFileName); $toKey = $this->resolveKey($toModule, $toSubFolder, $toFileName); StorageLogger::log('SUCCESS', 'Service copy started', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, ])); try { $copied = $this->driver->copy($fromKey, $toKey); StorageLogger::log($copied ? 'SUCCESS' : 'FAILURE', 'Service copy complete', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, 'copied' => $copied, ])); return $copied; } catch (\Throwable $e) { StorageLogger::log('FAILURE', 'Service copy failed', $this->baseContext([ 'from_key' => $fromKey, 'to_key' => $toKey, 'error' => $e->getMessage(), ])); throw $e; } } private function createDriver(string $driver): FileStorageInterface { return match ($driver) { 's3' => new S3StorageDriver($this->config), default => new LocalStorageDriver($this->config), }; } }