CHANGE_ADD_LOG_FOR_EVERY_STEP

This commit is contained in:
VENKATESHWARAN 2026-07-21 09:40:56 +05:30
parent 4a350f0822
commit c9dfc05e9c
10 changed files with 908 additions and 90 deletions

View File

@ -90,5 +90,5 @@ class Autoload extends AutoloadConfig
*
* @var list<string>
*/
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'];
}

View File

@ -0,0 +1,14 @@
<?php
use App\Services\Storage\StorageLogger;
if (! function_exists('s3_storage_log')) {
/**
* @param 'SUCCESS'|'FAILURE'|'WARNING'|string $status
* @param array<string, mixed> $context
*/
function s3_storage_log(string $status, string $message, array $context = []): void
{
StorageLogger::log($status, $message, $context);
}
}

View File

@ -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<string, mixed> $extra
* @return array<string, mixed>
*/
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

View File

@ -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<string, mixed> $extra
* @return array<string, mixed>
*/
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;
}
}

View File

@ -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<string, mixed> $extra
* @return array<string, mixed>
*/
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<string, mixed> $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);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Services\Storage;
class StorageLogger
{
/**
* @param 'SUCCESS'|'FAILURE'|'WARNING'|string $status
* @param array<string, mixed> $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}");
}
}

View File

@ -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}}
{"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}}

View File

@ -1,23 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="CLI Arguments" tests="6" assertions="3" errors="0" failures="0" skipped="5" time="0.027320">
<testsuite name="Tests\Unit\Storage\S3StorageIntegrationTest" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" tests="6" assertions="3" errors="0" failures="0" skipped="5" time="0.027320">
<testcase name="testS3DriverIsActiveFromEnv" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="39" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.010998">
<testsuite name="CLI Arguments" tests="34" assertions="74" errors="0" failures="0" skipped="1" time="1.942890">
<testsuite name="Tests\Unit\Storage\FileStorageServiceTest" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" tests="8" assertions="15" errors="0" failures="0" skipped="0" time="0.051454">
<testcase name="testResolveKeyWithSubFolder" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="13" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="1" time="0.016696"/>
<testcase name="testResolveKeyWithoutSubFolder" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="20" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="1" time="0.002569"/>
<testcase name="testUploadModuleFileStoresPolicyPdf" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="27" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="2" time="0.007923"/>
<testcase name="testReadWriteAndDeleteByModule" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="37" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="4" time="0.005252"/>
<testcase name="testGetLocalPathForProcessingPointsToWritableFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="48" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="2" time="0.003537"/>
<testcase name="testCopyBetweenKeys" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="59" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="1" time="0.004232"/>
<testcase name="testDownloadReturnsAttachmentResponse" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="67" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="3" time="0.006791"/>
<testcase name="testGetTemporaryUrlForLocalUsesBaseUrl" file="/var/www/html/nhance_partner_be/tests/unit/Storage/FileStorageServiceTest.php" line="78" class="Tests\Unit\Storage\FileStorageServiceTest" classname="Tests.Unit.Storage.FileStorageServiceTest" assertions="1" time="0.004454"/>
</testsuite>
<testsuite name="Tests\Unit\Storage\LocalStorageDriverTest" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" tests="9" assertions="16" errors="0" failures="0" skipped="0" time="0.033338">
<testcase name="testUploadFromUploadedFileStoresUnderLocalRoot" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="101" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="2" time="0.003572"/>
<testcase name="testUploadFromLocalPathCopiesFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="112" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="2" time="0.003582"/>
<testcase name="testPutWriteRawContents" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="124" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="2" time="0.004164"/>
<testcase name="testExistsReturnsFalseForMissingFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="134" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="1" time="0.003338"/>
<testcase name="testDeleteRemovesFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="139" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="2" time="0.003199"/>
<testcase name="testDownloadToTempReturnsExistingLocalPath" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="148" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="1" time="0.003496"/>
<testcase name="testCopyDuplicatesFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="158" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="2" time="0.004179"/>
<testcase name="testTemporaryUrlUsesBaseUrlForLocalDriver" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="168" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="1" time="0.004374"/>
<testcase name="testStreamDownloadReturnsResponseWithBody" file="/var/www/html/nhance_partner_be/tests/unit/Storage/LocalStorageDriverTest.php" line="178" class="Tests\Unit\Storage\LocalStorageDriverTest" classname="Tests.Unit.Storage.LocalStorageDriverTest" assertions="3" time="0.003432"/>
</testsuite>
<testsuite name="Tests\Unit\Storage\PolicyStorageHelperTest" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" tests="11" assertions="24" errors="0" failures="0" skipped="0" time="0.040864">
<testcase name="testPolicyFileTypeMapContainsExpectedTypes" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="14" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="4" time="0.003233"/>
<testcase name="testPolicyMdFileNameConvertsPdfToMd" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="24" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.002691"/>
<testcase name="testPolicyUploadPdfStoresFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="30" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.005686"/>
<testcase name="testPolicyUploadReceiptStoresFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="40" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.004434"/>
<testcase name="testPolicyUploadReturnsNullWhenNoFile" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="50" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.002654"/>
<testcase name="testPolicyExistsByTypeReturnsFalseForInvalidType" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="56" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="1" time="0.002477"/>
<testcase name="testPolicyDownloadByTypeAttachment" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="61" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="3" time="0.004665"/>
<testcase name="testPolicyDownloadByTypeInline" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="72" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="3" time="0.003591"/>
<testcase name="testPolicyDownloadByTypeThrowsForInvalidType" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="83" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="1" time="0.003291"/>
<testcase name="testPolicyLocalPdfPathResolvesStoredPdf" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="90" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.003791"/>
<testcase name="testPolicyMarkdownPutReadAndExists" file="/var/www/html/nhance_partner_be/tests/unit/Storage/PolicyStorageHelperTest.php" line="100" class="Tests\Unit\Storage\PolicyStorageHelperTest" classname="Tests.Unit.Storage.PolicyStorageHelperTest" assertions="2" time="0.004351"/>
</testsuite>
<testsuite name="Tests\Unit\Storage\S3StorageIntegrationTest" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" tests="6" assertions="19" errors="0" failures="0" skipped="1" time="1.817235">
<testcase name="testS3DriverIsActiveFromEnv" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="39" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="1" time="0.099073"/>
<testcase name="testS3PolicyPdfUploadExistsReadAndDelete" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="46" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="7" time="0.401832"/>
<testcase name="testS3PolicyReceiptUploadAndTemporaryUrl" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="69" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="4" time="0.344284"/>
<testcase name="testS3PolicyMarkdownPutAndRead" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="87" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="2" time="0.422262"/>
<testcase name="testS3EndorsementOriginalAndCompletionUpload" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="101" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="5" time="0.536949"/>
<testcase name="testLocalDriverScenarioStillWorksWhenEnvIsLocal" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="121" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.012836">
<skipped/>
</testcase>
<testcase name="testS3PolicyPdfUploadExistsReadAndDelete" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="46" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.002448">
<skipped/>
</testcase>
<testcase name="testS3PolicyReceiptUploadAndTemporaryUrl" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="69" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.002169">
<skipped/>
</testcase>
<testcase name="testS3PolicyMarkdownPutAndRead" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="87" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.002258">
<skipped/>
</testcase>
<testcase name="testS3EndorsementOriginalAndCompletionUpload" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="101" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="0" time="0.002006">
<skipped/>
</testcase>
<testcase name="testLocalDriverScenarioStillWorksWhenEnvIsLocal" file="/var/www/html/nhance_partner_be/tests/unit/Storage/S3StorageIntegrationTest.php" line="121" class="Tests\Unit\Storage\S3StorageIntegrationTest" classname="Tests.Unit.Storage.S3StorageIntegrationTest" assertions="3" time="0.007442"/>
</testsuite>
</testsuite>
</testsuites>

View File

@ -51,14 +51,51 @@
</style>
</head>
<body>
<h2>File Storage Service (Tests\Unit\Storage\FileStorageService)</h2>
<ul>
<li class="success">Resolve key with sub folder</li>
<li class="success">Resolve key without sub folder</li>
<li class="success">Upload module file stores policy pdf</li>
<li class="success">Read write and delete by module</li>
<li class="success">Get local path for processing points to writable file</li>
<li class="success">Copy between keys</li>
<li class="success">Download returns attachment response</li>
<li class="success">Get temporary url for local uses base url</li>
</ul>
<h2>Local Storage Driver (Tests\Unit\Storage\LocalStorageDriver)</h2>
<ul>
<li class="success">Upload from uploaded file stores under local root</li>
<li class="success">Upload from local path copies file</li>
<li class="success">Put write raw contents</li>
<li class="success">Exists returns false for missing file</li>
<li class="success">Delete removes file</li>
<li class="success">Download to temp returns existing local path</li>
<li class="success">Copy duplicates file</li>
<li class="success">Temporary url uses base url for local driver</li>
<li class="success">Stream download returns response with body</li>
</ul>
<h2>Policy Storage Helper (Tests\Unit\Storage\PolicyStorageHelper)</h2>
<ul>
<li class="success">Policy file type map contains expected types</li>
<li class="success">Policy md file name converts pdf to md</li>
<li class="success">Policy upload pdf stores file</li>
<li class="success">Policy upload receipt stores file</li>
<li class="success">Policy upload returns null when no file</li>
<li class="success">Policy exists by type returns false for invalid type</li>
<li class="success">Policy download by type attachment</li>
<li class="success">Policy download by type inline</li>
<li class="success">Policy download by type throws for invalid type</li>
<li class="success">Policy local pdf path resolves stored pdf</li>
<li class="success">Policy markdown put read and exists</li>
</ul>
<h2>S3Storage Integration (Tests\Unit\Storage\S3StorageIntegration)</h2>
<ul>
<li class="defect">S 3 driver is active from env</li>
<li class="defect">S 3 policy pdf upload exists read and delete</li>
<li class="defect">S 3 policy receipt upload and temporary url</li>
<li class="defect">S 3 policy markdown put and read</li>
<li class="defect">S 3 endorsement original and completion upload</li>
<li class="success">Local driver scenario still works when env is local</li>
<li class="success">S 3 driver is active from env</li>
<li class="success">S 3 policy pdf upload exists read and delete</li>
<li class="success">S 3 policy receipt upload and temporary url</li>
<li class="success">S 3 policy markdown put and read</li>
<li class="success">S 3 endorsement original and completion upload</li>
<li class="defect">Local driver scenario still works when env is local</li>
</ul>
</body>
</html>

View File

@ -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