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..8f7bdba1 100644 --- a/src/Storage/Device/Local.php +++ b/src/Storage/Device/Local.php @@ -126,6 +126,66 @@ 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 (! $recursive && $file instanceof \DirectoryIterator && $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++; + } 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()); + } + } + } + + return $uploadedCount; + } + /** * Upload Data. * diff --git a/src/Storage/Device/S3.php b/src/Storage/Device/S3.php index f24e6cf6..7f0ed4ef 100644 --- a/src/Storage/Device/S3.php +++ b/src/Storage/Device/S3.php @@ -227,6 +227,60 @@ 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 (! $recursive && $file instanceof \DirectoryIterator && $file->isDot()) { + continue; + } + + if ($file->isFile()) { + try { + $relativePath = $recursive + ? substr($file->getPathname(), strlen($source) + 1) + : $file->getFilename(); + + $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); + $destinationPath = $this->getPath($path.'/'.$relativePath); + + $chunksUploaded = $this->upload($file->getPathname(), $destinationPath); + if ($chunksUploaded > 0) { + $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..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 */ @@ -410,4 +415,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); + } }