diff --git a/src/Storage/Device/Local.php b/src/Storage/Device/Local.php index 84afb7b4..ef92d815 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -4,6 +4,7 @@ use Exception; use Utopia\Storage\Device; +use Utopia\Storage\Exception\NotFoundException; use Utopia\Storage\Storage; class Local extends Device @@ -272,7 +273,7 @@ public function abort(string $path, string $extra = ''): bool public function read(string $path, int $offset = 0, int $length = null): string { if (! $this->exists($path)) { - throw new Exception('File Not Found'); + throw new NotFoundException('File not found'); } return \file_get_contents($path, use_include_path: false, context: null, offset: $offset, length: $length); diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index f24e6cf6..c6425ae5 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -4,6 +4,7 @@ use Exception; use Utopia\Storage\Device; +use Utopia\Storage\Exception\NotFoundException; use Utopia\Storage\Storage; class S3 extends Device @@ -284,7 +285,7 @@ public function transfer(string $path, string $destination, Device $device): boo try { $response = $this->getInfo($path); } catch (\Throwable $e) { - throw new Exception('File not found'); + throw new NotFoundException('File not found'); } $size = (int) ($response['content-length'] ?? 0); $contentType = $response['content-type'] ?? ''; @@ -903,7 +904,7 @@ protected function call(string $operation, string $method, string $uri, string $ } if ($response->code >= 400) { - throw new Exception($response->body, $response->code); + $this->parseAndThrowS3Error($response->body, $response->code); } // Parse body into XML @@ -927,6 +928,36 @@ protected function call(string $operation, string $method, string $uri, string $ } } + /** + * Parse S3 XML error response and throw appropriate exception + * + * @param string $errorBody The error response body + * @param int $statusCode The HTTP status code + * + * @throws NotFoundException When the error is NoSuchKey + * @throws Exception For other S3 errors + */ + private function parseAndThrowS3Error(string $errorBody, int $statusCode): void + { + if (str_starts_with($errorBody, 'Code ?? ''); + $errorMessage = (string) ($xml->Message ?? ''); + + if ($errorCode === 'NoSuchKey') { + throw new NotFoundException($errorMessage ?: 'File not found', $statusCode); + } + } catch (NotFoundException $e) { + throw $e; + } catch (\Throwable $e) { + // If XML parsing fails, fall through to original error + } + } + + throw new Exception($errorBody, $statusCode); + } + /** * Sort compare for meta headers * diff --git a/src/Storage/Exception/NotFoundException.php b/src/Storage/Exception/NotFoundException.php new file mode 100644 index 00000000..d09ca44a --- /dev/null +++ b/src/Storage/Exception/NotFoundException.php @@ -0,0 +1,9 @@ +object->delete($this->object->getPath('text-for-read.txt')); } + public function testReadNonExistentFile() + { + $this->expectException(NotFoundException::class); + $this->object->read($this->object->getPath('non-existent-file.txt')); + } + public function testFileExists() { $this->assertEquals($this->object->write($this->object->getPath('text-for-test-exists.txt'), 'Hello World'), true); diff --git a/tests/Storage/S3Base.php b/tests/Storage/S3Base.php index 2218ce0c..f7535a53 100644 --- a/tests/Storage/S3Base.php +++ b/tests/Storage/S3Base.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Utopia\Storage\Device\Local; use Utopia\Storage\Device\S3; +use Utopia\Storage\Exception\NotFoundException; abstract class S3Base extends TestCase { @@ -20,11 +21,6 @@ abstract protected function getAdapterName(): string; */ abstract protected function getAdapterDescription(): string; - /** - * @return string - */ - abstract protected function getAdapterType(): string; - /** * @var S3 */ @@ -138,6 +134,12 @@ public function testRead() $this->object->delete($this->object->getPath('text-for-read.txt')); } + public function testReadNonExistentFile() + { + $this->expectException(NotFoundException::class); + $this->object->read($this->object->getPath('non-existent-file.txt')); + } + public function testFileExists() { $this->assertEquals(true, $this->object->exists($this->object->getPath('testing/kitten-1.jpg'))); @@ -415,4 +417,15 @@ public function testTransferSmall() $this->object->delete($path); $device->delete($destination); } + + public function testTransferNonExistentFile() + { + $device = new Local(__DIR__.'/../resources/disk-a'); + + $path = $this->object->getPath('non-existent-file.txt'); + $destination = $device->getPath('hello.txt'); + + $this->expectException(NotFoundException::class); + $this->object->transfer($path, $destination, $device); + } }