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