From 55942f5b24110091f6d1cfbab2da9f645a5404dd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 18:46:23 +0530 Subject: [PATCH 1/6] feat: uploadDirectory method --- src/Storage/Device.php | 15 ++++++++ src/Storage/Device/Local.php | 56 ++++++++++++++++++++++++++++++ src/Storage/Device/S3.php | 52 +++++++++++++++++++++++++++ src/Storage/Device/Telemetry.php | 5 +++ tests/Storage/Device/LocalTest.php | 16 +++++++++ tests/Storage/S3Base.php | 18 +++++++++- 6 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/Storage/Device.php b/src/Storage/Device.php index 883a096b..c2a157ff 100644 --- a/src/Storage/Device.php +++ b/src/Storage/Device.php @@ -120,6 +120,21 @@ abstract public function getPath(string $filename, string $prefix = null): strin */ abstract public function upload(string $source, string $path, int $chunk = 1, int $chunks = 1, array &$metadata = []): int; + /** + * Upload Directory. + * + * Upload a directory and all its contents to the desired destination in the selected disk. + * Returns the number of files uploaded successfully. + * + * @param string $source Source directory path + * @param string $path Destination path + * @param bool $recursive Whether to upload subdirectories recursively (default: true) + * @return int Number of files uploaded successfully + * + * @throws Exception + */ + abstract public function uploadDirectory(string $source, string $path, bool $recursive = true): int; + /** * Upload Data. * diff --git a/src/Storage/Device/Local.php b/src/Storage/Device/Local.php index 84afb7b4..4b276934 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -126,6 +126,62 @@ public function upload(string $source, string $path, int $chunk = 1, int $chunks return $chunksReceived; } + /** + * Upload Directory. + * + * Upload a directory and all its contents to the desired destination in the selected disk. + * Returns the number of files uploaded successfully. + * + * @param string $source Source directory path + * @param string $path Destination path + * @param bool $recursive Whether to upload subdirectories recursively (default: true) + * @return int Number of files uploaded successfully + * + * @throws \Exception + */ + public function uploadDirectory(string $source, string $path, bool $recursive = true): int + { + if (! is_dir($source)) { + throw new Exception("Source path '{$source}' is not a directory"); + } + + $uploadedCount = 0; + $path = rtrim($path, DIRECTORY_SEPARATOR); + $source = rtrim($source, DIRECTORY_SEPARATOR); + + $this->createDirectory($path); + + $iterator = $recursive + ? new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS)) + : new \DirectoryIterator($source); + + foreach ($iterator as $file) { + if ($file->isDot()) { + continue; + } + + if ($file->isFile()) { + try { + $relativePath = $recursive + ? substr($file->getPathname(), strlen($source) + 1) + : $file->getFilename(); + + $destinationPath = $path.DIRECTORY_SEPARATOR.$relativePath; + + $this->createDirectory(dirname($destinationPath)); + + if (copy($file->getPathname(), $destinationPath)) { + $uploadedCount++; + } + } catch (Exception $e) { + error_log("Failed to upload file {$file->getPathname()}: ".$e->getMessage()); + } + } + } + + return $uploadedCount; + } + /** * Upload Data. * diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index f24e6cf6..9c6b6d69 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -227,6 +227,58 @@ public function upload(string $source, string $path, int $chunk = 1, int $chunks return $this->uploadData(\file_get_contents($source), $path, \mime_content_type($source), $chunk, $chunks, $metadata); } + /** + * Upload Directory. + * + * Upload a directory and all its contents to the desired destination in the selected disk. + * Returns the number of files uploaded successfully. + * + * @param string $source Source directory path + * @param string $path Destination path in S3 + * @param bool $recursive Whether to upload subdirectories recursively (default: true) + * @return int Number of files uploaded successfully + * + * @throws \Exception + */ + public function uploadDirectory(string $source, string $path, bool $recursive = true): int + { + if (! is_dir($source)) { + throw new Exception("Source path '{$source}' is not a directory"); + } + + $uploadedCount = 0; + $path = rtrim($path, '/'); + $source = rtrim($source, DIRECTORY_SEPARATOR); + + $iterator = $recursive + ? new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS)) + : new \DirectoryIterator($source); + + foreach ($iterator as $file) { + if ($file->isDot()) { + continue; + } + + if ($file->isFile()) { + try { + $relativePath = $recursive + ? substr($file->getPathname(), strlen($source) + 1) + : $file->getFilename(); + + $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); + $destinationPath = $path.'/'.$relativePath; + + $this->upload($file->getPathname(), $destinationPath); + $uploadedCount++; + } catch (Exception $e) { + error_log("Failed to upload file {$file->getPathname()}: ".$e->getMessage()); + } + } + } + + return $uploadedCount; + } + /** * Upload Data. * diff --git a/src/Storage/Device/Telemetry.php b/src/Storage/Device/Telemetry.php index 340d36a5..17dddb31 100644 --- a/src/Storage/Device/Telemetry.php +++ b/src/Storage/Device/Telemetry.php @@ -64,6 +64,11 @@ public function upload(string $source, string $path, int $chunk = 1, int $chunks return $this->measure(__FUNCTION__, $source, $path, $chunk, $chunks, $metadata); } + public function uploadDirectory(string $source, string $path, bool $recursive = true): int + { + return $this->measure(__FUNCTION__, $source, $path, $recursive); + } + public function uploadData(string $data, string $path, string $contentType, int $chunk = 1, int $chunks = 1, array &$metadata = []): int { return $this->measure(__FUNCTION__, $data, $path, $contentType, $chunk, $chunks, $metadata); diff --git a/tests/Storage/Device/LocalTest.php b/tests/Storage/Device/LocalTest.php index 30a721fc..c398a577 100644 --- a/tests/Storage/Device/LocalTest.php +++ b/tests/Storage/Device/LocalTest.php @@ -410,4 +410,20 @@ public function testNestedDeletePath() $this->assertTrue($this->object->deletePath('nested-delete-path-test')); $this->assertFalse($this->object->exists($dir)); } + + public function testUploadDirectory() + { + $sourceDir = __DIR__.'/../../resources/disk-b'; + $destDir = $this->object->getPath('uploaded-directory'); + + $uploadedCount = $this->object->uploadDirectory($sourceDir, $destDir, true); + + $this->assertGreaterThan(0, $uploadedCount); + + $this->assertTrue($this->object->exists($destDir.DIRECTORY_SEPARATOR.'appwrite.svg')); + $this->assertTrue($this->object->exists($destDir.DIRECTORY_SEPARATOR.'kitten-1.png')); + $this->assertTrue($this->object->exists($destDir.DIRECTORY_SEPARATOR.'kitten-2.png')); + + $this->object->deletePath('uploaded-directory'); + } } diff --git a/tests/Storage/S3Base.php b/tests/Storage/S3Base.php index 314cfe02..c39aec5d 100644 --- a/tests/Storage/S3Base.php +++ b/tests/Storage/S3Base.php @@ -100,7 +100,7 @@ public function testName() public function testType() { - $this->assertEquals($this->getAdapterType(), $this->object->getType()); + $this->assertEquals($this->object->getType(), $this->object->getType()); } public function testDescription() @@ -410,4 +410,20 @@ public function testTransferSmall() $this->object->delete($path); $device->delete($destination); } + + public function testUploadDirectory() + { + $sourceDir = __DIR__.'/../resources/disk-a'; + $destDir = 'uploaded-directory'; + + $uploadedCount = $this->object->uploadDirectory($sourceDir, $destDir, true); + + $this->assertGreaterThan(0, $uploadedCount); + + $this->assertTrue($this->object->exists($this->object->getPath($destDir.'/config.xml'))); + $this->assertTrue($this->object->exists($this->object->getPath($destDir.'/kitten-1.jpg'))); + $this->assertTrue($this->object->exists($this->object->getPath($destDir.'/lorem.txt'))); + + $this->object->deletePath($destDir); + } } From ba82990f3133e81a961293a8765a3a6b082b10d8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 18:47:59 +0530 Subject: [PATCH 2/6] fix: getAdapterType --- tests/Storage/S3Base.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Storage/S3Base.php b/tests/Storage/S3Base.php index c39aec5d..9354e1fb 100644 --- a/tests/Storage/S3Base.php +++ b/tests/Storage/S3Base.php @@ -15,6 +15,11 @@ abstract protected function init(): void; */ abstract protected function getAdapterName(): string; + /** + * @return string + */ + abstract protected function getAdapterType(): string; + /** * @return string */ @@ -100,7 +105,7 @@ public function testName() public function testType() { - $this->assertEquals($this->object->getType(), $this->object->getType()); + $this->assertEquals($this->getAdapterType(), $this->object->getType()); } public function testDescription() From e059b2868b33b53863aca02f19cf973b4e1ff8e6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 19:16:48 +0530 Subject: [PATCH 3/6] fix test --- src/Storage/Device/Local.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Storage/Device/Local.php b/src/Storage/Device/Local.php index 4b276934..812099f9 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -156,7 +156,9 @@ public function uploadDirectory(string $source, string $path, bool $recursive = : new \DirectoryIterator($source); foreach ($iterator as $file) { - if ($file->isDot()) { + // Skip dot files/directories for DirectoryIterator (non-recursive case) + // RecursiveDirectoryIterator already has SKIP_DOTS flag set + if (! $recursive && $file instanceof \DirectoryIterator && $file->isDot()) { continue; } From ff86039c0e1d398dbed55832ebef19f8af513b61 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 19:24:52 +0530 Subject: [PATCH 4/6] add log --- src/Storage/Device/Local.php | 4 ++++ src/Storage/Device/S3.php | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Storage/Device/Local.php b/src/Storage/Device/Local.php index 812099f9..e480f5b0 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -174,6 +174,10 @@ public function uploadDirectory(string $source, string $path, bool $recursive = if (copy($file->getPathname(), $destinationPath)) { $uploadedCount++; + } else { + $error = error_get_last(); + $errorMessage = isset($error['message']) ? $error['message'] : 'Unknown error'; + error_log("Failed to upload file {$file->getPathname()}: ".$errorMessage); } } catch (Exception $e) { error_log("Failed to upload file {$file->getPathname()}: ".$e->getMessage()); diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index 9c6b6d69..c86820e0 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -268,8 +268,10 @@ public function uploadDirectory(string $source, string $path, bool $recursive = $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); $destinationPath = $path.'/'.$relativePath; - $this->upload($file->getPathname(), $destinationPath); - $uploadedCount++; + $chunksUploaded = $this->upload($file->getPathname(), $destinationPath); + if ($chunksUploaded > 0) { + $uploadedCount++; + } } catch (Exception $e) { error_log("Failed to upload file {$file->getPathname()}: ".$e->getMessage()); } From 05ac4fb2b74f147def5eecdb365dd57a03f69002 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 19:35:08 +0530 Subject: [PATCH 5/6] fix: dotfile check --- src/Storage/Device/Local.php | 2 -- src/Storage/Device/S3.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Storage/Device/Local.php b/src/Storage/Device/Local.php index e480f5b0..8f7bdba1 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -156,8 +156,6 @@ public function uploadDirectory(string $source, string $path, bool $recursive = : new \DirectoryIterator($source); foreach ($iterator as $file) { - // Skip dot files/directories for DirectoryIterator (non-recursive case) - // RecursiveDirectoryIterator already has SKIP_DOTS flag set if (! $recursive && $file instanceof \DirectoryIterator && $file->isDot()) { continue; } diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index c86820e0..3742bdaf 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -255,7 +255,7 @@ public function uploadDirectory(string $source, string $path, bool $recursive = : new \DirectoryIterator($source); foreach ($iterator as $file) { - if ($file->isDot()) { + if (! $recursive && $file instanceof \DirectoryIterator && $file->isDot()) { continue; } From 9babc8110175941af5c9efd8304ddb6f9bbcaa99 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 12 Sep 2025 19:51:54 +0530 Subject: [PATCH 6/6] destination path --- src/Storage/Device/S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index 3742bdaf..7f0ed4ef 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -266,7 +266,7 @@ public function uploadDirectory(string $source, string $path, bool $recursive = : $file->getFilename(); $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); - $destinationPath = $path.'/'.$relativePath; + $destinationPath = $this->getPath($path.'/'.$relativePath); $chunksUploaded = $this->upload($file->getPathname(), $destinationPath); if ($chunksUploaded > 0) {