From d334eb44913d9d1a00d32863d86706be3dab62cb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 1 Dec 2025 22:09:41 +0530 Subject: [PATCH] fix: S3 exists() should not match directories for file paths Previously, exists() would incorrectly add a trailing slash to all paths, causing file paths to match directories with the same prefix. This led to exists('/path/to/file.html') returning true when only the directory '/path/to/file.html/' existed, resulting in NoSuchKey errors when trying to read the file. Now, trailing slashes are only added if the original path ends with '/', ensuring: - exists('file.html') only returns true if the file exists - exists('directory/') correctly checks for directory existence Added test testFileExistsDoesNotMatchDirectoryPrefix() to verify the fix. --- src/Storage/Device/S3.php | 4 ++-- tests/Storage/S3Base.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index cab37c82..36bc48b6 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -636,8 +636,8 @@ public function exists(string $path): bool $prefix = $root.'/'.ltrim($path, '/'); } - if (! empty($path) && ! str_ends_with($prefix, '/')) { - $prefix .= '/'; + if (! empty($path) && str_ends_with($path, '/')) { + $prefix = rtrim($prefix, '/').'/'; } $objects = $this->listObjects($prefix, 1); diff --git a/tests/Storage/S3Base.php b/tests/Storage/S3Base.php index 338537a3..ec70fe8c 100644 --- a/tests/Storage/S3Base.php +++ b/tests/Storage/S3Base.php @@ -165,6 +165,25 @@ public function testDirectoryExists() $this->assertEquals(false, $this->object->exists($this->object->getPath('nested/deep/structure'))); } + /** + * Test that file paths without trailing slash don't incorrectly match directories + * Verifies fix: exists('file.html') should not return true when directory 'file.html/' exists + */ + public function testFileExistsDoesNotMatchDirectoryPrefix() + { + $this->object->write($this->object->getPath('builds/index.html'), 'content', 'text/html'); + $this->object->write($this->object->getPath('builds/other.css'), 'body {}', 'text/css'); + + $this->assertEquals(true, $this->object->exists($this->object->getPath('builds/index.html'))); + + $this->object->delete($this->object->getPath('builds/index.html')); + + // File should not exist even though directory 'builds/' still contains other.css + $this->assertEquals(false, $this->object->exists($this->object->getPath('builds/index.html'))); + + $this->object->delete($this->object->getPath('builds/other.css')); + } + public function testMove() { $this->assertEquals(true, $this->object->write($this->object->getPath('text-for-move.txt'), 'Hello World', 'text/plain'));