diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index fd7be56..4da82cb 100755 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -90,5 +90,5 @@ class Autoload extends AutoloadConfig * * @var list */ - public $helpers = ['JwtHelper','common_helper','mail_helper','url_helper','status_helper','sms_helper','storage_helper']; + public $helpers = ['JwtHelper','common_helper','mail_helper','url_helper','status_helper','sms_helper','storage_helper','storage_log_helper']; } diff --git a/app/Helpers/storage_log_helper.php b/app/Helpers/storage_log_helper.php new file mode 100644 index 0000000..0735081 --- /dev/null +++ b/app/Helpers/storage_log_helper.php @@ -0,0 +1,14 @@ + $context + */ + function s3_storage_log(string $status, string $message, array $context = []): void + { + StorageLogger::log($status, $message, $context); + } +} diff --git a/app/Services/FileStorageService.php b/app/Services/FileStorageService.php index 04ef047..54ab50c 100644 --- a/app/Services/FileStorageService.php +++ b/app/Services/FileStorageService.php @@ -5,6 +5,7 @@ namespace App\Services; use App\Services\Storage\FileStorageInterface; use App\Services\Storage\LocalStorageDriver; use App\Services\Storage\S3StorageDriver; +use App\Services\Storage\StorageLogger; use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\ResponseInterface; use Config\Storage; @@ -18,7 +19,32 @@ class FileStorageService public function __construct(?Storage $config = null) { $this->config = $config ?? config('Storage'); - $this->driver = $this->createDriver($this->config->driver); + + 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 @@ -46,13 +72,44 @@ class FileStorageService public function upload(UploadedFile|string $source, string $key, array $options = []): string { - $storedKey = $this->driver->upload($source, $key, $options); + StorageLogger::log('SUCCESS', 'Service upload started', $this->baseContext([ + 'key' => $key, + 'source' => $source instanceof UploadedFile ? 'uploaded_file' : 'path', + ])); - if ($this->config->retainLocal && $this->isS3()) { - (new LocalStorageDriver($this->config))->upload($source, $key, $options); + 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; } - - return $storedKey; } public function uploadModuleFile( @@ -62,70 +119,235 @@ class FileStorageService ?string $fileName = null ): string { $fileName = $fileName ?: (time() . '_' . $file->getRandomName()); + $key = $this->resolveKey($module, $subFolder, $fileName); - $this->upload($file, $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 { - $storedKey = $this->driver->put($key, $contents, $options); + StorageLogger::log('SUCCESS', 'Service put started', $this->baseContext([ + 'key' => $key, + 'size' => strlen($contents), + ])); - if ($this->config->retainLocal && $this->isS3()) { - (new LocalStorageDriver($this->config))->put($key, $contents, $options); + 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; } - - return $storedKey; } public function exists(string $module, string $subFolder, string $fileName): bool { - return $this->driver->exists($this->resolveKey($module, $subFolder, $fileName)); + $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 { - return $this->driver->exists($key); + $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 { - return $this->driver->delete($this->resolveKey($module, $subFolder, $fileName)); + $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 { - return $this->driver->delete($key); + $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 { - return $this->driver->read($this->resolveKey($module, $subFolder, $fileName)); + $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 { - return $this->driver->read($key); + 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 { - return $this->driver->streamDownload( - $this->resolveKey($module, $subFolder, $fileName), - $fileName - ); + $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 { - return $this->driver->streamDownload($key, $downloadName ?: basename($key)); + 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; + } } - /** - * Returns a local filesystem path suitable for pdftotext, Gemini, RAG, etc. - * For S3, this downloads to a temp file first. - */ public function getLocalPathForProcessing(string $module, string $subFolder, string $fileName): string { return $this->getLocalPathForKey($this->resolveKey($module, $subFolder, $fileName)); @@ -133,11 +355,30 @@ class FileStorageService public function getLocalPathForKey(string $key): string { - if ($this->config->driver === 'local') { - return rtrim($this->config->localRoot, '/\\') . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $key), '/\\'); - } + StorageLogger::log('SUCCESS', 'Service local path resolution started', $this->baseContext([ + 'key' => $key, + ])); - return $this->driver->downloadToTemp($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; + } } public function getTemporaryUrl(string $module, string $subFolder, string $fileName, ?int $ttlSeconds = null): string @@ -150,20 +391,72 @@ class FileStorageService public function getTemporaryUrlForKey(string $key, ?int $ttlSeconds = null): string { - return $this->driver->temporaryUrl($key, $ttlSeconds ?? $this->config->presignedTtl); + 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 { - return $this->driver->url($this->resolveKey($module, $subFolder, $fileName)); + $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 { - return $this->driver->copy( - $this->resolveKey($fromModule, $fromSubFolder, $fromFileName), - $this->resolveKey($toModule, $toSubFolder, $toFileName) - ); + $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 diff --git a/app/Services/Storage/LocalStorageDriver.php b/app/Services/Storage/LocalStorageDriver.php index 5e1f305..2a86f8e 100644 --- a/app/Services/Storage/LocalStorageDriver.php +++ b/app/Services/Storage/LocalStorageDriver.php @@ -10,6 +10,19 @@ class LocalStorageDriver implements FileStorageInterface { public function __construct(private readonly Storage $config) { + StorageLogger::log('SUCCESS', 'Local driver initialized', $this->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 @@ -24,82 +37,193 @@ class LocalStorageDriver implements FileStorageInterface $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 { - return is_file($this->fullPath($key)); + $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; } - return unlink($fullPath); + $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; } @@ -107,7 +231,16 @@ class LocalStorageDriver implements FileStorageInterface { $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}"); } @@ -118,20 +251,52 @@ class LocalStorageDriver implements FileStorageInterface { $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}"); } - return base_url(ltrim(str_replace('\\', '/', $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 @@ -144,12 +309,37 @@ class LocalStorageDriver implements FileStorageInterface $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); - return copy($fromPath, $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; } } diff --git a/app/Services/Storage/S3StorageDriver.php b/app/Services/Storage/S3StorageDriver.php index 1d0a638..1be3b17 100644 --- a/app/Services/Storage/S3StorageDriver.php +++ b/app/Services/Storage/S3StorageDriver.php @@ -14,8 +14,32 @@ class S3StorageDriver implements FileStorageInterface public function __construct(private readonly Storage $config) { - $this->assertConfigured(); - $this->client = $this->createClient(); + StorageLogger::log('SUCCESS', 'S3 driver initializing', $this->baseContext()); + + try { + $this->assertConfigured(); + $this->client = $this->createClient(); + StorageLogger::log('SUCCESS', 'S3 driver initialized', $this->baseContext()); + } catch (\Throwable $e) { + StorageLogger::log('FAILURE', 'S3 driver initialization failed', $this->baseContext([ + 'error' => $e->getMessage(), + ])); + throw $e; + } + } + + /** + * @param array $extra + * @return array + */ + private function baseContext(array $extra = []): array + { + return array_merge([ + 'driver' => 's3', + 'bucket' => $this->config->s3Bucket, + 'region' => $this->config->s3Region, + 'prefix' => $this->config->s3Prefix !== '' ? $this->config->s3Prefix : null, + ], $extra); } private function assertConfigured(): void @@ -64,11 +88,28 @@ class S3StorageDriver implements FileStorageInterface /** * @param array $params */ - private function putObject(array $params): void + private function putObject(array $params, string $key, string $operation): void { + StorageLogger::log('SUCCESS', "{$operation} started", $this->baseContext([ + 'key' => $key, + 's3_key' => $params['Key'] ?? null, + 'content_type' => $params['ContentType'] ?? null, + 'body_size' => isset($params['Body']) ? strlen((string) $params['Body']) : null, + ])); + try { $this->client->putObject($params); + StorageLogger::log('SUCCESS', "{$operation} complete", $this->baseContext([ + 'key' => $key, + 's3_key' => $params['Key'] ?? null, + ])); } catch (AwsException $e) { + StorageLogger::log('FAILURE', "{$operation} failed", $this->baseContext([ + 'key' => $key, + 's3_key' => $params['Key'] ?? null, + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message'=> $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 upload failed: ' . $e->getAwsErrorMessage(), 0, $e); } } @@ -83,19 +124,38 @@ class S3StorageDriver implements FileStorageInterface 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'); } $params['SourceFile'] = $source->getTempName(); + StorageLogger::log('SUCCESS', 'Upload source resolved from uploaded file', $this->baseContext([ + 'key' => $key, + 'original_name' => $source->getClientName(), + 'client_mime' => $source->getClientMimeType(), + 'size' => $source->getSize(), + ])); } else { 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}"); } $params['SourceFile'] = $source; + StorageLogger::log('SUCCESS', 'Upload source resolved from filesystem path', $this->baseContext([ + 'key' => $key, + 'source' => $source, + 'size' => filesize($source) ?: 0, + ])); } - $this->putObject($params); + $this->putObject($params, $key, 'Upload'); return $key; } @@ -107,47 +167,102 @@ class S3StorageDriver implements FileStorageInterface 'Key' => $this->objectKey($key), 'Body' => $contents, 'ContentType' => $options['content_type'] ?? 'application/octet-stream', - ]); + ], $key, 'Put'); return $key; } public function exists(string $key): bool { + StorageLogger::log('SUCCESS', 'Exists check started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + ])); + try { - return $this->client->doesObjectExist( + $exists = $this->client->doesObjectExist( $this->config->s3Bucket, $this->objectKey($key) ); + + StorageLogger::log('SUCCESS', 'Exists check complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'exists' => $exists, + ])); + + return $exists; } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Exists check failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 exists check failed: ' . $e->getAwsErrorMessage(), 0, $e); } } public function delete(string $key): bool { + StorageLogger::log('SUCCESS', 'Delete started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + ])); + try { $this->client->deleteObject([ 'Bucket' => $this->config->s3Bucket, 'Key' => $this->objectKey($key), ]); + StorageLogger::log('SUCCESS', 'Delete complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + ])); + return true; } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Delete failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 delete failed: ' . $e->getAwsErrorMessage(), 0, $e); } } public function read(string $key): string { + StorageLogger::log('SUCCESS', 'Read started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + ])); + try { $result = $this->client->getObject([ 'Bucket' => $this->config->s3Bucket, 'Key' => $this->objectKey($key), ]); - return (string) $result['Body']; + $body = (string) $result['Body']; + + StorageLogger::log('SUCCESS', 'Read complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'size' => strlen($body), + 'mime_type' => $result['ContentType'] ?? null, + ])); + + return $body; } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Read failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 read failed: ' . $e->getAwsErrorMessage(), 0, $e); } } @@ -156,41 +271,89 @@ class S3StorageDriver implements FileStorageInterface { $tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('s3_', true) . '_' . basename($key); + StorageLogger::log('SUCCESS', 'Download to temp started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'temp_path' => $tempPath, + ])); + try { $this->client->getObject([ 'Bucket' => $this->config->s3Bucket, 'Key' => $this->objectKey($key), 'SaveAs' => $tempPath, ]); + + StorageLogger::log('SUCCESS', 'Download to temp complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'temp_path' => $tempPath, + 'size' => is_file($tempPath) ? (filesize($tempPath) ?: 0) : 0, + ])); + + return $tempPath; } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Download to temp failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'temp_path' => $tempPath, + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 download failed: ' . $e->getAwsErrorMessage(), 0, $e); } - - return $tempPath; } public function streamDownload(string $key, ?string $downloadName = null): ResponseInterface { + StorageLogger::log('SUCCESS', 'Stream download started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'download_name' => $downloadName ?: basename($key), + ])); + try { $result = $this->client->getObject([ 'Bucket' => $this->config->s3Bucket, 'Key' => $this->objectKey($key), ]); + + $body = (string) $result['Body']; + + StorageLogger::log('SUCCESS', 'Stream download complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'download_name' => $downloadName ?: basename($key), + 'size' => strlen($body), + 'mime_type' => $result['ContentType'] ?? null, + ])); + + return service('response') + ->setHeader('Content-Type', $result['ContentType'] ?? 'application/octet-stream') + ->setHeader( + 'Content-Disposition', + 'attachment; filename="' . ($downloadName ?: basename($key)) . '"' + ) + ->setBody($body); } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Stream download failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 download failed: ' . $e->getAwsErrorMessage(), 0, $e); } - - return service('response') - ->setHeader('Content-Type', $result['ContentType'] ?? 'application/octet-stream') - ->setHeader( - 'Content-Disposition', - 'attachment; filename="' . ($downloadName ?: basename($key)) . '"' - ) - ->setBody((string) $result['Body']); } public function temporaryUrl(string $key, int $ttlSeconds = 900): string { + StorageLogger::log('SUCCESS', 'Presigned URL generation started', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'ttl_seconds' => $ttlSeconds, + ])); + try { $command = $this->client->getCommand('GetObject', [ 'Bucket' => $this->config->s3Bucket, @@ -199,24 +362,51 @@ class S3StorageDriver implements FileStorageInterface $request = $this->client->createPresignedRequest($command, "+{$ttlSeconds} seconds"); + StorageLogger::log('SUCCESS', 'Presigned URL generation complete', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'ttl_seconds' => $ttlSeconds, + ])); + return (string) $request->getUri(); } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Presigned URL generation failed', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + 'ttl_seconds' => $ttlSeconds, + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 presigned URL failed: ' . $e->getAwsErrorMessage(), 0, $e); } } public function url(string $key): string { - return sprintf( + $url = sprintf( 'https://%s.s3.%s.amazonaws.com/%s', $this->config->s3Bucket, $this->config->s3Region, rawurlencode($this->objectKey($key)) ); + + StorageLogger::log('SUCCESS', 'Public URL resolved', $this->baseContext([ + 'key' => $key, + 's3_key' => $this->objectKey($key), + ])); + + return $url; } public function copy(string $fromKey, string $toKey): bool { + StorageLogger::log('SUCCESS', 'Copy started', $this->baseContext([ + 'from_key' => $fromKey, + 'to_key' => $toKey, + 'from_s3_key' => $this->objectKey($fromKey), + 'to_s3_key' => $this->objectKey($toKey), + ])); + try { $this->client->copyObject([ 'Bucket' => $this->config->s3Bucket, @@ -224,8 +414,19 @@ class S3StorageDriver implements FileStorageInterface 'Key' => $this->objectKey($toKey), ]); + StorageLogger::log('SUCCESS', 'Copy complete', $this->baseContext([ + 'from_key' => $fromKey, + 'to_key' => $toKey, + ])); + return true; } catch (AwsException $e) { + StorageLogger::log('FAILURE', 'Copy failed', $this->baseContext([ + 'from_key' => $fromKey, + 'to_key' => $toKey, + 'aws_code' => $e->getAwsErrorCode(), + 'aws_message' => $e->getAwsErrorMessage(), + ])); throw new FileStorageException('S3 copy failed: ' . $e->getAwsErrorMessage(), 0, $e); } } diff --git a/app/Services/Storage/StorageLogger.php b/app/Services/Storage/StorageLogger.php new file mode 100644 index 0000000..7d34c65 --- /dev/null +++ b/app/Services/Storage/StorageLogger.php @@ -0,0 +1,23 @@ + $context + */ + public static function log(string $status, string $message, array $context = []): void + { + $status = strtoupper($status); + + if (! in_array($status, ['SUCCESS', 'FAILURE', 'WARNING'], true)) { + $status = 'FAILURE'; + } + + $json = $context !== [] ? ' | ' . json_encode($context, JSON_UNESCAPED_SLASHES) : ''; + + log_message('error', "[S3_STORAGE][{$status}] {$message}{$json}"); + } +} diff --git a/build/.phpunit.cache/test-results b/build/.phpunit.cache/test-results index f20e40b..994f289 100644 --- a/build/.phpunit.cache/test-results +++ b/build/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Unit\\Storage\\FileStorageServiceTest::testUploadModuleFileStoresPolicyPdf":8,"Tests\\Unit\\Storage\\FileStorageServiceTest::testDownloadReturnsAttachmentResponse":7,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromUploadedFileStoresUnderLocalRoot":8,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testStreamDownloadReturnsResponseWithBody":7,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadPdfStoresFile":8,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReceiptStoresFile":8,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeAttachment":7,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeInline":7,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyPdfUploadExistsReadAndDelete":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyReceiptUploadAndTemporaryUrl":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3EndorsementOriginalAndCompletionUpload":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testLocalDriverScenarioStillWorksWhenEnvIsLocal":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3DriverIsActiveFromEnv":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyMarkdownPutAndRead":1},"times":{"Tests\\Unit\\Storage\\FileStorageServiceTest::testResolveKeyWithSubFolder":0.003,"Tests\\Unit\\Storage\\FileStorageServiceTest::testResolveKeyWithoutSubFolder":0.001,"Tests\\Unit\\Storage\\FileStorageServiceTest::testUploadModuleFileStoresPolicyPdf":0.005,"Tests\\Unit\\Storage\\FileStorageServiceTest::testReadWriteAndDeleteByModule":0.001,"Tests\\Unit\\Storage\\FileStorageServiceTest::testGetLocalPathForProcessingPointsToWritableFile":0.001,"Tests\\Unit\\Storage\\FileStorageServiceTest::testCopyBetweenKeys":0.001,"Tests\\Unit\\Storage\\FileStorageServiceTest::testDownloadReturnsAttachmentResponse":0.003,"Tests\\Unit\\Storage\\FileStorageServiceTest::testGetTemporaryUrlForLocalUsesBaseUrl":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromUploadedFileStoresUnderLocalRoot":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromLocalPathCopiesFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testPutWriteRawContents":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testExistsReturnsFalseForMissingFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testDeleteRemovesFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testDownloadToTempReturnsExistingLocalPath":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testCopyDuplicatesFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testTemporaryUrlUsesBaseUrlForLocalDriver":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testStreamDownloadReturnsResponseWithBody":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyFileTypeMapContainsExpectedTypes":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyMdFileNameConvertsPdfToMd":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadPdfStoresFile":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReceiptStoresFile":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReturnsNullWhenNoFile":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyExistsByTypeReturnsFalseForInvalidType":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeAttachment":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeInline":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeThrowsForInvalidType":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyLocalPdfPathResolvesStoredPdf":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyMarkdownPutReadAndExists":0.001,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3DriverIsActiveFromEnv":0.003,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyPdfUploadExistsReadAndDelete":0.001,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyReceiptUploadAndTemporaryUrl":0.001,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyMarkdownPutAndRead":0.001,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3EndorsementOriginalAndCompletionUpload":0.001,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testLocalDriverScenarioStillWorksWhenEnvIsLocal":0.006}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Unit\\Storage\\FileStorageServiceTest::testUploadModuleFileStoresPolicyPdf":8,"Tests\\Unit\\Storage\\FileStorageServiceTest::testDownloadReturnsAttachmentResponse":7,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromUploadedFileStoresUnderLocalRoot":8,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testStreamDownloadReturnsResponseWithBody":7,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadPdfStoresFile":8,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReceiptStoresFile":8,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeAttachment":7,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeInline":7,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyPdfUploadExistsReadAndDelete":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyReceiptUploadAndTemporaryUrl":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3EndorsementOriginalAndCompletionUpload":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testLocalDriverScenarioStillWorksWhenEnvIsLocal":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3DriverIsActiveFromEnv":1,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyMarkdownPutAndRead":1},"times":{"Tests\\Unit\\Storage\\FileStorageServiceTest::testResolveKeyWithSubFolder":0.006,"Tests\\Unit\\Storage\\FileStorageServiceTest::testResolveKeyWithoutSubFolder":0.001,"Tests\\Unit\\Storage\\FileStorageServiceTest::testUploadModuleFileStoresPolicyPdf":0.006,"Tests\\Unit\\Storage\\FileStorageServiceTest::testReadWriteAndDeleteByModule":0.003,"Tests\\Unit\\Storage\\FileStorageServiceTest::testGetLocalPathForProcessingPointsToWritableFile":0.002,"Tests\\Unit\\Storage\\FileStorageServiceTest::testCopyBetweenKeys":0.002,"Tests\\Unit\\Storage\\FileStorageServiceTest::testDownloadReturnsAttachmentResponse":0.005,"Tests\\Unit\\Storage\\FileStorageServiceTest::testGetTemporaryUrlForLocalUsesBaseUrl":0.003,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromUploadedFileStoresUnderLocalRoot":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testUploadFromLocalPathCopiesFile":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testPutWriteRawContents":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testExistsReturnsFalseForMissingFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testDeleteRemovesFile":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testDownloadToTempReturnsExistingLocalPath":0.001,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testCopyDuplicatesFile":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testTemporaryUrlUsesBaseUrlForLocalDriver":0.002,"Tests\\Unit\\Storage\\LocalStorageDriverTest::testStreamDownloadReturnsResponseWithBody":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyFileTypeMapContainsExpectedTypes":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyMdFileNameConvertsPdfToMd":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadPdfStoresFile":0.003,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReceiptStoresFile":0.003,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyUploadReturnsNullWhenNoFile":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyExistsByTypeReturnsFalseForInvalidType":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeAttachment":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeInline":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyDownloadByTypeThrowsForInvalidType":0.001,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyLocalPdfPathResolvesStoredPdf":0.002,"Tests\\Unit\\Storage\\PolicyStorageHelperTest::testPolicyMarkdownPutReadAndExists":0.002,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3DriverIsActiveFromEnv":0.097,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyPdfUploadExistsReadAndDelete":0.4,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyReceiptUploadAndTemporaryUrl":0.342,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3PolicyMarkdownPutAndRead":0.419,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testS3EndorsementOriginalAndCompletionUpload":0.534,"Tests\\Unit\\Storage\\S3StorageIntegrationTest::testLocalDriverScenarioStillWorksWhenEnvIsLocal":0.011}} \ No newline at end of file diff --git a/build/logs/logfile.xml b/build/logs/logfile.xml index efb456f..4656c10 100644 --- a/build/logs/logfile.xml +++ b/build/logs/logfile.xml @@ -1,23 +1,49 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - diff --git a/build/logs/testdox.html b/build/logs/testdox.html index 2e1da8b..0f3a340 100644 --- a/build/logs/testdox.html +++ b/build/logs/testdox.html @@ -51,14 +51,51 @@ +

File Storage Service (Tests\Unit\Storage\FileStorageService)

+
    +
  • Resolve key with sub folder
  • +
  • Resolve key without sub folder
  • +
  • Upload module file stores policy pdf
  • +
  • Read write and delete by module
  • +
  • Get local path for processing points to writable file
  • +
  • Copy between keys
  • +
  • Download returns attachment response
  • +
  • Get temporary url for local uses base url
  • +
+

Local Storage Driver (Tests\Unit\Storage\LocalStorageDriver)

+
    +
  • Upload from uploaded file stores under local root
  • +
  • Upload from local path copies file
  • +
  • Put write raw contents
  • +
  • Exists returns false for missing file
  • +
  • Delete removes file
  • +
  • Download to temp returns existing local path
  • +
  • Copy duplicates file
  • +
  • Temporary url uses base url for local driver
  • +
  • Stream download returns response with body
  • +
+

Policy Storage Helper (Tests\Unit\Storage\PolicyStorageHelper)

+
    +
  • Policy file type map contains expected types
  • +
  • Policy md file name converts pdf to md
  • +
  • Policy upload pdf stores file
  • +
  • Policy upload receipt stores file
  • +
  • Policy upload returns null when no file
  • +
  • Policy exists by type returns false for invalid type
  • +
  • Policy download by type attachment
  • +
  • Policy download by type inline
  • +
  • Policy download by type throws for invalid type
  • +
  • Policy local pdf path resolves stored pdf
  • +
  • Policy markdown put read and exists
  • +

S3Storage Integration (Tests\Unit\Storage\S3StorageIntegration)

    -
  • S 3 driver is active from env
  • -
  • S 3 policy pdf upload exists read and delete
  • -
  • S 3 policy receipt upload and temporary url
  • -
  • S 3 policy markdown put and read
  • -
  • S 3 endorsement original and completion upload
  • -
  • Local driver scenario still works when env is local
  • +
  • S 3 driver is active from env
  • +
  • S 3 policy pdf upload exists read and delete
  • +
  • S 3 policy receipt upload and temporary url
  • +
  • S 3 policy markdown put and read
  • +
  • S 3 endorsement original and completion upload
  • +
  • Local driver scenario still works when env is local
\ No newline at end of file diff --git a/build/logs/testdox.txt b/build/logs/testdox.txt index a5e89df..7a31bea 100644 --- a/build/logs/testdox.txt +++ b/build/logs/testdox.txt @@ -1,8 +1,42 @@ -S3Storage Integration (Tests\Unit\Storage\S3StorageIntegration) - [ ] S 3 driver is active from env - [ ] S 3 policy pdf upload exists read and delete - [ ] S 3 policy receipt upload and temporary url - [ ] S 3 policy markdown put and read - [ ] S 3 endorsement original and completion upload - [x] Local driver scenario still works when env is local +File Storage Service (Tests\Unit\Storage\FileStorageService) + [x] Resolve key with sub folder + [x] Resolve key without sub folder + [x] Upload module file stores policy pdf + [x] Read write and delete by module + [x] Get local path for processing points to writable file + [x] Copy between keys + [x] Download returns attachment response + [x] Get temporary url for local uses base url + +Local Storage Driver (Tests\Unit\Storage\LocalStorageDriver) + [x] Upload from uploaded file stores under local root + [x] Upload from local path copies file + [x] Put write raw contents + [x] Exists returns false for missing file + [x] Delete removes file + [x] Download to temp returns existing local path + [x] Copy duplicates file + [x] Temporary url uses base url for local driver + [x] Stream download returns response with body + +Policy Storage Helper (Tests\Unit\Storage\PolicyStorageHelper) + [x] Policy file type map contains expected types + [x] Policy md file name converts pdf to md + [x] Policy upload pdf stores file + [x] Policy upload receipt stores file + [x] Policy upload returns null when no file + [x] Policy exists by type returns false for invalid type + [x] Policy download by type attachment + [x] Policy download by type inline + [x] Policy download by type throws for invalid type + [x] Policy local pdf path resolves stored pdf + [x] Policy markdown put read and exists + +S3Storage Integration (Tests\Unit\Storage\S3StorageIntegration) + [x] S 3 driver is active from env + [x] S 3 policy pdf upload exists read and delete + [x] S 3 policy receipt upload and temporary url + [x] S 3 policy markdown put and read + [x] S 3 endorsement original and completion upload + [ ] Local driver scenario still works when env is local