From e39e8c17a2f925b31cc73d696bbe465602e2e011 Mon Sep 17 00:00:00 2001 From: Taki Elias <38932580+takielias@users.noreply.github.com> Date: Sat, 7 Sep 2024 03:29:43 +0600 Subject: [PATCH] Add S3 and Wasabi as storage providers (#281) --- app/Enums/StorageProvider.php | 4 + app/SSH/HasS3Storage.php | 20 ++ app/SSH/Storage/S3.php | 78 +++++++ app/SSH/Storage/S3AbstractStorage.php | 32 +++ app/SSH/Storage/Wasabi.php | 84 ++++++++ app/SSH/Storage/scripts/s3/download.sh | 32 +++ app/SSH/Storage/scripts/s3/upload.sh | 59 ++++++ app/SSH/Storage/scripts/wasabi/download.sh | 31 +++ app/SSH/Storage/scripts/wasabi/upload.sh | 59 ++++++ app/StorageProviders/S3.php | 57 +++++ .../S3AbstractStorageProvider.php | 74 +++++++ app/StorageProviders/S3ClientInterface.php | 10 + app/StorageProviders/S3StorageInterface.php | 14 ++ app/StorageProviders/Wasabi.php | 85 ++++++++ config/core.php | 5 + public/static/images/s3.svg | 1 + public/static/images/wasabi.svg | 9 + .../partials/connect-provider.blade.php | 143 ++----------- .../providers/dropbox.blade.php | 15 ++ .../storage-providers/providers/ftp.blade.php | 71 +++++++ .../providers/local.blade.php | 9 + .../storage-providers/providers/s3.blade.php | 49 +++++ .../providers/wasabi.blade.php | 49 +++++ tests/Feature/StorageProvidersTest.php | 195 ++++++++++++++++++ 24 files changed, 1055 insertions(+), 130 deletions(-) create mode 100644 app/SSH/HasS3Storage.php create mode 100644 app/SSH/Storage/S3.php create mode 100644 app/SSH/Storage/S3AbstractStorage.php create mode 100644 app/SSH/Storage/Wasabi.php create mode 100644 app/SSH/Storage/scripts/s3/download.sh create mode 100644 app/SSH/Storage/scripts/s3/upload.sh create mode 100644 app/SSH/Storage/scripts/wasabi/download.sh create mode 100644 app/SSH/Storage/scripts/wasabi/upload.sh create mode 100644 app/StorageProviders/S3.php create mode 100644 app/StorageProviders/S3AbstractStorageProvider.php create mode 100644 app/StorageProviders/S3ClientInterface.php create mode 100644 app/StorageProviders/S3StorageInterface.php create mode 100644 app/StorageProviders/Wasabi.php create mode 100644 public/static/images/s3.svg create mode 100644 public/static/images/wasabi.svg create mode 100644 resources/views/settings/storage-providers/providers/dropbox.blade.php create mode 100644 resources/views/settings/storage-providers/providers/ftp.blade.php create mode 100644 resources/views/settings/storage-providers/providers/local.blade.php create mode 100644 resources/views/settings/storage-providers/providers/s3.blade.php create mode 100644 resources/views/settings/storage-providers/providers/wasabi.blade.php diff --git a/app/Enums/StorageProvider.php b/app/Enums/StorageProvider.php index 84c2275..9165848 100644 --- a/app/Enums/StorageProvider.php +++ b/app/Enums/StorageProvider.php @@ -9,4 +9,8 @@ final class StorageProvider const FTP = 'ftp'; const LOCAL = 'local'; + + const S3 = 's3'; + + const WASABI = 'wasabi'; } diff --git a/app/SSH/HasS3Storage.php b/app/SSH/HasS3Storage.php new file mode 100644 index 0000000..fa01565 --- /dev/null +++ b/app/SSH/HasS3Storage.php @@ -0,0 +1,20 @@ +setBucketRegion($this->storageProvider->credentials['region']); + $this->setApiUrl(); + } + + /** + * @throws SSHCommandError + */ + public function upload(string $src, string $dest): array + { + $uploadCommand = $this->getScript('s3/upload.sh', [ + 'src' => $src, + 'bucket' => $this->storageProvider->credentials['bucket'], + 'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest), + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + 'region' => $this->getBucketRegion(), + 'endpoint' => $this->getApiUrl(), + ]); + + $upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-s3'); + + if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) { + Log::error('Failed to upload to S3', ['output' => $upload]); + throw new SSHCommandError('Failed to upload to S3: '.$upload); + } + + return [ + 'size' => null, // You can parse the size from the output if needed + ]; + + } + + /** + * @throws SSHCommandError + */ + public function download(string $src, string $dest): void + { + $downloadCommand = $this->getScript('s3/download.sh', [ + 'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src), + 'dest' => $dest, + 'bucket' => $this->storageProvider->credentials['bucket'], + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + 'region' => $this->getBucketRegion(), + 'endpoint' => $this->getApiUrl(), + ]); + + $download = $this->server->ssh()->exec($downloadCommand, 'download-from-s3'); + + if (! str_contains($download, 'Download successful')) { + Log::error('Failed to download from S3', ['output' => $download]); + throw new SSHCommandError('Failed to download from S3: '.$download); + } + } + + /** + * @TODO Implement delete method + */ + public function delete(string $path): void {} +} diff --git a/app/SSH/Storage/S3AbstractStorage.php b/app/SSH/Storage/S3AbstractStorage.php new file mode 100644 index 0000000..2b0f7d7 --- /dev/null +++ b/app/SSH/Storage/S3AbstractStorage.php @@ -0,0 +1,32 @@ +apiUrl; + } + + public function setApiUrl(?string $region = null): void + { + $this->bucketRegion = $region ?? $this->bucketRegion; + $this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com"; + } + + // Getter and Setter for $bucketRegion + public function getBucketRegion(): string + { + return $this->bucketRegion; + } + + public function setBucketRegion(string $region): void + { + $this->bucketRegion = $region; + } +} diff --git a/app/SSH/Storage/Wasabi.php b/app/SSH/Storage/Wasabi.php new file mode 100644 index 0000000..9f8714b --- /dev/null +++ b/app/SSH/Storage/Wasabi.php @@ -0,0 +1,84 @@ +setBucketRegion($this->storageProvider->credentials['region']); + $this->setApiUrl(); + } + + /** + * @throws SSHCommandError + */ + public function upload(string $src, string $dest): array + { + $uploadCommand = $this->getScript('wasabi/upload.sh', [ + 'src' => $src, + 'bucket' => $this->storageProvider->credentials['bucket'], + 'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest), + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + 'region' => $this->storageProvider->credentials['region'], + 'endpoint' => $this->getApiUrl(), + ]); + + $upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-wasabi'); + + if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) { + Log::error('Failed to upload to wasabi', ['output' => $upload]); + throw new SSHCommandError('Failed to upload to wasabi: '.$upload); + } + + return [ + 'size' => null, // You can parse the size from the output if needed + ]; + + } + + /** + * @throws SSHCommandError + */ + public function download(string $src, string $dest): void + { + $downloadCommand = $this->getScript('wasabi/download.sh', [ + 'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src), + 'dest' => $dest, + 'bucket' => $this->storageProvider->credentials['bucket'], + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + 'region' => $this->storageProvider->credentials['region'], + 'endpoint' => $this->getApiUrl(), + ]); + + $download = $this->server->ssh()->exec($downloadCommand, 'download-from-wasabi'); + + if (! str_contains($download, 'Download successful')) { + Log::error('Failed to download from wasabi', ['output' => $download]); + throw new SSHCommandError('Failed to download from wasabi: '.$download); + } + } + + /** + * @TODO Implement delete method + */ + public function delete(string $path): void {} + + public function setApiUrl(?string $region = null): void + { + $this->bucketRegion = $region ?? $this->bucketRegion; + $this->apiUrl = "https://{$this->storageProvider->credentials['bucket']}.s3.{$this->getBucketRegion()}.wasabisys.com"; + } +} diff --git a/app/SSH/Storage/scripts/s3/download.sh b/app/SSH/Storage/scripts/s3/download.sh new file mode 100644 index 0000000..a2b5298 --- /dev/null +++ b/app/SSH/Storage/scripts/s3/download.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Configure AWS CLI with provided credentials +/usr/local/bin/aws configure set aws_access_key_id "__key__" +/usr/local/bin/aws configure set aws_secret_access_key "__secret__" +/usr/local/bin/aws configure set default.region "__region__" + +# Use the provided endpoint in the correct format +ENDPOINT="__endpoint__" +BUCKET="__bucket__" +REGION="__region__" + +# Ensure that DEST does not have a trailing slash +SRC="__src__" +DEST="__dest__" + +# Download the file from S3 +echo "Downloading s3://__bucket__/__src__ to __dest__" +download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1) +download_exit_code=$? + +# Log output and exit code +echo "Download command output: $download_output" +echo "Download command exit code: $download_exit_code" + +# Check if the download was successful +if [ $download_exit_code -eq 0 ]; then + echo "Download successful" +else + echo "Download failed" + exit 1 +fi diff --git a/app/SSH/Storage/scripts/s3/upload.sh b/app/SSH/Storage/scripts/s3/upload.sh new file mode 100644 index 0000000..7969018 --- /dev/null +++ b/app/SSH/Storage/scripts/s3/upload.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Check if AWS CLI is installed +if ! command -v aws &> /dev/null +then + echo "AWS CLI is not installed. Installing..." + + # Detect system architecture + ARCH=$(uname -m) + if [ "$ARCH" == "x86_64" ]; then + CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" + elif [ "$ARCH" == "aarch64" ]; then + CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" + else + echo "Unsupported architecture: $ARCH" + exit 1 + fi + + # Download and install AWS CLI + sudo curl "$CLI_URL" -o "awscliv2.zip" + sudo unzip awscliv2.zip + sudo ./aws/install --update + sudo rm -rf awscliv2.zip aws + + echo "AWS CLI installation completed." +else + echo "AWS CLI is already installed." + /usr/local/bin/aws --version +fi + +# Configure AWS CLI with provided credentials +/usr/local/bin/aws configure set aws_access_key_id "__key__" +/usr/local/bin/aws configure set aws_secret_access_key "__secret__" + +# Use the provided endpoint in the correct format +ENDPOINT="__endpoint__" +BUCKET="__bucket__" +REGION="__region__" + +# Ensure that DEST does not have a trailing slash +SRC="__src__" +DEST="__dest__" + +# Upload the file +echo "Uploading __src__ to s3://$BUCKET/$DEST" +upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1) +upload_exit_code=$? + +# Log output and exit code +echo "Upload command output: $upload_output" +echo "Upload command exit code: $upload_exit_code" + +# Check if the upload was successful +if [ $upload_exit_code -eq 0 ]; then + echo "Upload successful" +else + echo "Upload failed" + exit 1 +fi diff --git a/app/SSH/Storage/scripts/wasabi/download.sh b/app/SSH/Storage/scripts/wasabi/download.sh new file mode 100644 index 0000000..4e33e7e --- /dev/null +++ b/app/SSH/Storage/scripts/wasabi/download.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Configure AWS CLI with provided credentials +/usr/local/bin/aws configure set aws_access_key_id "__key__" +/usr/local/bin/aws configure set aws_secret_access_key "__secret__" + +# Use the provided endpoint in the correct format +ENDPOINT="__endpoint__" +BUCKET="__bucket__" +REGION="__region__" + +# Ensure that DEST does not have a trailing slash +SRC="__src__" +DEST="__dest__" + +# Download the file from S3 +echo "Downloading s3://__bucket____src__ to __dest__" +download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1) +download_exit_code=$? + +# Log output and exit code +echo "Download command output: $download_output" +echo "Download command exit code: $download_exit_code" + +# Check if the download was successful +if [ $download_exit_code -eq 0 ]; then + echo "Download successful" +else + echo "Download failed" + exit 1 +fi diff --git a/app/SSH/Storage/scripts/wasabi/upload.sh b/app/SSH/Storage/scripts/wasabi/upload.sh new file mode 100644 index 0000000..d174043 --- /dev/null +++ b/app/SSH/Storage/scripts/wasabi/upload.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Check if AWS CLI is installed +if ! command -v aws &> /dev/null +then + echo "AWS CLI is not installed. Installing..." + + # Detect system architecture + ARCH=$(uname -m) + if [ "$ARCH" == "x86_64" ]; then + CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" + elif [ "$ARCH" == "aarch64" ]; then + CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" + else + echo "Unsupported architecture: $ARCH" + exit 1 + fi + + # Download and install AWS CLI + sudo curl "$CLI_URL" -o "awscliv2.zip" + sudo unzip awscliv2.zip + sudo ./aws/install --update + sudo rm -rf awscliv2.zip aws + + echo "AWS CLI installation completed." +else + echo "AWS CLI is already installed." + aws --version +fi + +# Configure AWS CLI with provided credentials +/usr/local/bin/aws configure set aws_access_key_id "__key__" +/usr/local/bin/aws configure set aws_secret_access_key "__secret__" + +# Use the provided endpoint in the correct format +ENDPOINT="__endpoint__" +BUCKET="__bucket__" +REGION="__region__" + +# Ensure that DEST does not have a trailing slash +SRC="__src__" +DEST="__dest__" + +# Upload the file +echo "Uploading __src__ to s3://$BUCKET/$DEST" +upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1) +upload_exit_code=$? + +# Log output and exit code +echo "Upload command output: $upload_output" +echo "Upload command exit code: $upload_exit_code" + +# Check if the upload was successful +if [ $upload_exit_code -eq 0 ]; then + echo "Upload successful" +else + echo "Upload failed" + exit 1 +fi diff --git a/app/StorageProviders/S3.php b/app/StorageProviders/S3.php new file mode 100644 index 0000000..62ff5d7 --- /dev/null +++ b/app/StorageProviders/S3.php @@ -0,0 +1,57 @@ + 'required|string', + 'secret' => 'required|string', + 'region' => 'required|string', + 'bucket' => 'required|string', + 'path' => 'required|string', + ]; + } + + public function credentialData(array $input): array + { + return [ + 'key' => $input['key'], + 'secret' => $input['secret'], + 'region' => $input['region'], + 'bucket' => $input['bucket'], + 'path' => $input['path'], + ]; + } + + public function connect(): bool + { + try { + $this->setBucketRegion($this->storageProvider->credentials['region']); + $this->setApiUrl(); + $this->buildClientConfig(); + $this->getClient()->listBuckets(); + + return true; + } catch (S3Exception $e) { + Log::error('Failed to connect to S3', ['exception' => $e]); + + return false; + } + } + + public function ssh(Server $server): Storage + { + return new S3Storage($server, $this->storageProvider); + } + + public function delete(array $paths): void {} +} diff --git a/app/StorageProviders/S3AbstractStorageProvider.php b/app/StorageProviders/S3AbstractStorageProvider.php new file mode 100644 index 0000000..806b98b --- /dev/null +++ b/app/StorageProviders/S3AbstractStorageProvider.php @@ -0,0 +1,74 @@ +apiUrl; + } + + public function setApiUrl(?string $region = null): void + { + $this->bucketRegion = $region ?? $this->bucketRegion; + $this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com"; + } + + public function getBucketRegion(): string + { + return $this->bucketRegion; + } + + public function setBucketRegion(string $region): void + { + $this->bucketRegion = $region; + } + + public function getClient(): S3Client + { + return new S3Client($this->clientConfig); + } + + /** + * Build the configuration array for the S3 client. + * This method can be overridden by child classes to modify the configuration. + */ + public function buildClientConfig(): array + { + $this->clientConfig = [ + 'credentials' => [ + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + ], + 'region' => $this->getBucketRegion(), + 'version' => 'latest', + 'endpoint' => $this->getApiUrl(), + ]; + + return $this->clientConfig; + } + + /** + * Set or update a configuration parameter for the S3 client. + */ + public function setConfigParam(array $param): void + { + foreach ($param as $key => $value) { + $this->clientConfig[$key] = $value; + } + } +} diff --git a/app/StorageProviders/S3ClientInterface.php b/app/StorageProviders/S3ClientInterface.php new file mode 100644 index 0000000..2e73f74 --- /dev/null +++ b/app/StorageProviders/S3ClientInterface.php @@ -0,0 +1,10 @@ + 'required|string', + 'secret' => 'required|string', + 'region' => 'required|string', + 'bucket' => 'required|string', + 'path' => 'required|string', + ]; + } + + public function credentialData(array $input): array + { + return [ + 'key' => $input['key'], + 'secret' => $input['secret'], + 'region' => $input['region'], + 'bucket' => $input['bucket'], + 'path' => $input['path'], + ]; + } + + public function connect(): bool + { + try { + $this->setBucketRegion(self::DEFAULT_REGION); + $this->setApiUrl(); + $this->buildClientConfig(); + $this->getClient()->listBuckets(); + + return true; + } catch (S3Exception $e) { + Log::error('Failed to connect to S3', ['exception' => $e]); + + return false; + } + } + + /** + * Build the configuration array for the S3 client. + * This method can be overridden by child classes to modify the configuration. + */ + public function buildClientConfig(): array + { + $this->clientConfig = [ + 'credentials' => [ + 'key' => $this->storageProvider->credentials['key'], + 'secret' => $this->storageProvider->credentials['secret'], + ], + 'region' => $this->getBucketRegion(), + 'version' => 'latest', + 'endpoint' => $this->getApiUrl(), + 'use_path_style_endpoint' => true, + ]; + + return $this->clientConfig; + } + + public function ssh(Server $server): Storage + { + return new WasabiStorage($server, $this->storageProvider); + } + + public function setApiUrl(?string $region = null): void + { + $this->bucketRegion = $region ?? $this->bucketRegion; + $this->apiUrl = "https://s3.{$this->bucketRegion}.wasabisys.com"; + } + + public function delete(array $paths): void {} +} diff --git a/config/core.php b/config/core.php index 745e5c0..8b14974 100755 --- a/config/core.php +++ b/config/core.php @@ -427,11 +427,16 @@ \App\Enums\StorageProvider::DROPBOX, \App\Enums\StorageProvider::FTP, \App\Enums\StorageProvider::LOCAL, + \App\Enums\StorageProvider::S3, + \App\Enums\StorageProvider::WASABI, ], 'storage_providers_class' => [ \App\Enums\StorageProvider::DROPBOX => \App\StorageProviders\Dropbox::class, \App\Enums\StorageProvider::FTP => \App\StorageProviders\FTP::class, \App\Enums\StorageProvider::LOCAL => \App\StorageProviders\Local::class, + \App\Enums\StorageProvider::S3 => \App\StorageProviders\S3::class, + \App\Enums\StorageProvider::WASABI => \App\StorageProviders\Wasabi::class, + ], 'ssl_types' => [ diff --git a/public/static/images/s3.svg b/public/static/images/s3.svg new file mode 100644 index 0000000..43ebabd --- /dev/null +++ b/public/static/images/s3.svg @@ -0,0 +1 @@ +AWS Simple Storage Service (S3) \ No newline at end of file diff --git a/public/static/images/wasabi.svg b/public/static/images/wasabi.svg new file mode 100644 index 0000000..60be377 --- /dev/null +++ b/public/static/images/wasabi.svg @@ -0,0 +1,9 @@ + + nubes_wasabi + + + + + + \ No newline at end of file diff --git a/resources/views/settings/storage-providers/partials/connect-provider.blade.php b/resources/views/settings/storage-providers/partials/connect-provider.blade.php index 235b8b0..e03eeba 100644 --- a/resources/views/settings/storage-providers/partials/connect-provider.blade.php +++ b/resources/views/settings/storage-providers/partials/connect-provider.blade.php @@ -1,3 +1,14 @@ +@use(App\Enums\StorageProvider) +@php + $storageProviders = [ + StorageProvider::DROPBOX, + StorageProvider::FTP, + StorageProvider::LOCAL, + StorageProvider::S3, + StorageProvider::WASABI, + ]; +@endphp +
{{ __("Connect") }} @@ -69,136 +80,8 @@ class="p-6" @enderror
- @if ($provider == \App\Enums\StorageProvider::DROPBOX) -
- - - @error("token") - - @enderror - - - How to generate? - -
- @endif - - @if ($provider == \App\Enums\StorageProvider::FTP) -
-
-
- - - @error("host") - - @enderror -
-
- - - @error("port") - - @enderror -
-
-
- - - @error("path") - - @enderror -
-
-
- - - @error("username") - - @enderror -
-
- - - @error("password") - - @enderror -
-
-
-
- - - - - - @error("ssl") - - @enderror -
-
- - - - - - @error("passive") - - @enderror -
-
-
- @endif - - @if ($provider == \App\Enums\StorageProvider::LOCAL) -
- - - - The absolute path on your server that the database exists. like `/home/vito/db-backups` - - - Make sure that the path exists and the `vito` user has permission to write to it. - - @error("path") - - @enderror -
+ @if (in_array($provider, $storageProviders)) + @include("settings.storage-providers.providers.{$provider}") @endif
diff --git a/resources/views/settings/storage-providers/providers/dropbox.blade.php b/resources/views/settings/storage-providers/providers/dropbox.blade.php new file mode 100644 index 0000000..16f27ac --- /dev/null +++ b/resources/views/settings/storage-providers/providers/dropbox.blade.php @@ -0,0 +1,15 @@ +
+ + + @error("token") + + @enderror + + + How to generate? + +
diff --git a/resources/views/settings/storage-providers/providers/ftp.blade.php b/resources/views/settings/storage-providers/providers/ftp.blade.php new file mode 100644 index 0000000..9314085 --- /dev/null +++ b/resources/views/settings/storage-providers/providers/ftp.blade.php @@ -0,0 +1,71 @@ +
+
+
+ + + @error("host") + + @enderror +
+
+ + + @error("port") + + @enderror +
+
+
+ + + @error("path") + + @enderror +
+
+
+ + + @error("username") + + @enderror +
+
+ + + @error("password") + + @enderror +
+
+
+
+ + + + + + @error("ssl") + + @enderror +
+
+ + + + + + @error("passive") + + @enderror +
+
+
diff --git a/resources/views/settings/storage-providers/providers/local.blade.php b/resources/views/settings/storage-providers/providers/local.blade.php new file mode 100644 index 0000000..0c65384 --- /dev/null +++ b/resources/views/settings/storage-providers/providers/local.blade.php @@ -0,0 +1,9 @@ +
+ + + The absolute path on your server that the database exists. like `/home/vito/db-backups` + Make sure that the path exists and the `vito` user has permission to write to it. + @error("path") + + @enderror +
diff --git a/resources/views/settings/storage-providers/providers/s3.blade.php b/resources/views/settings/storage-providers/providers/s3.blade.php new file mode 100644 index 0000000..11392c0 --- /dev/null +++ b/resources/views/settings/storage-providers/providers/s3.blade.php @@ -0,0 +1,49 @@ +
+
+ + + @error("path") + + @enderror +
+
+
+ + + @error("key") + + @enderror +
+
+ + + @error("secret") + + @enderror +
+ + + How to generate? + +
+
+
+ + + @error("region") + + @enderror +
+
+ + + @error("bucket") + + @enderror +
+
+
diff --git a/resources/views/settings/storage-providers/providers/wasabi.blade.php b/resources/views/settings/storage-providers/providers/wasabi.blade.php new file mode 100644 index 0000000..7ebf5c7 --- /dev/null +++ b/resources/views/settings/storage-providers/providers/wasabi.blade.php @@ -0,0 +1,49 @@ +
+
+ + + @error("path") + + @enderror +
+
+
+ + + @error("key") + + @enderror +
+
+ + + @error("secret") + + @enderror +
+ + + How to generate? + +
+
+
+ + + @error("region") + + @enderror +
+
+ + + @error("bucket") + + @enderror +
+
+
diff --git a/tests/Feature/StorageProvidersTest.php b/tests/Feature/StorageProvidersTest.php index 872c16e..a3ff436 100644 --- a/tests/Feature/StorageProvidersTest.php +++ b/tests/Feature/StorageProvidersTest.php @@ -6,6 +6,11 @@ use App\Facades\FTP; use App\Models\Backup; use App\Models\Database; +use App\Models\StorageProvider as StorageProviderModel; +use App\StorageProviders\S3; +use App\StorageProviders\Wasabi; +use Aws\S3\Exception\S3Exception; +use Aws\S3\S3Client; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -52,6 +57,24 @@ public function test_see_providers_list(): void 'provider' => StorageProvider::DROPBOX, ]); + $this->get(route('settings.storage-providers')) + ->assertSuccessful() + ->assertSee($provider->profile); + + $provider = \App\Models\StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => StorageProvider::S3, + ]); + + $this->get(route('settings.storage-providers')) + ->assertSuccessful() + ->assertSee($provider->profile); + + $provider = \App\Models\StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => StorageProvider::WASABI, + ]); + $this->get(route('settings.storage-providers')) ->assertSuccessful() ->assertSee($provider->profile); @@ -101,6 +124,178 @@ public function test_cannot_delete_provider(): void ]); } + public function test_s3_connect_successful() + { + $storageProvider = StorageProviderModel::factory()->create([ + 'provider' => StorageProvider::S3, + 'credentials' => [ + 'key' => 'fake-key', + 'secret' => 'fake-secret', + 'region' => 'us-east-1', + 'bucket' => 'fake-bucket', + 'path' => '/', + ], + ]); + + // Mock the S3Client + $s3ClientMock = $this->getMockBuilder(S3Client::class) + ->disableOriginalConstructor() + ->onlyMethods(['getCommand', 'execute']) + ->getMock(); + + // Mock the getCommand method + $s3ClientMock->expects($this->once()) + ->method('getCommand') + ->with('listBuckets') + ->willReturn(new \Aws\Command('listBuckets')); + + // Mock the execute method + $s3ClientMock->expects($this->once()) + ->method('execute') + ->willReturn(['Buckets' => []]); + + // Mock the S3 class to return the mocked S3Client + $s3 = $this->getMockBuilder(S3::class) + ->setConstructorArgs([$storageProvider]) + ->onlyMethods(['getClient']) + ->getMock(); + + $s3->expects($this->once()) + ->method('getClient') + ->willReturn($s3ClientMock); + + $this->assertTrue($s3->connect()); + } + + public function test_s3_connect_failure() + { + $storageProvider = StorageProviderModel::factory()->create([ + 'provider' => StorageProvider::S3, + 'credentials' => [ + 'key' => 'fake-key', + 'secret' => 'fake-secret', + 'region' => 'us-east-1', + 'bucket' => 'fake-bucket', + 'path' => '/', + ], + ]); + + // Mock the S3Client + $s3ClientMock = $this->getMockBuilder(S3Client::class) + ->disableOriginalConstructor() + ->onlyMethods(['getCommand', 'execute']) + ->getMock(); + + // Mock the getCommand method + $s3ClientMock->expects($this->once()) + ->method('getCommand') + ->with('listBuckets') + ->willReturn(new \Aws\Command('listBuckets')); + + // Mock the execute method to throw an S3Exception + $s3ClientMock->expects($this->once()) + ->method('execute') + ->willThrowException(new S3Exception('Error', new \Aws\Command('ListBuckets'))); + + // Mock the S3 class to return the mocked S3Client + $s3 = $this->getMockBuilder(S3::class) + ->setConstructorArgs([$storageProvider]) + ->onlyMethods(['getClient']) + ->getMock(); + + $s3->expects($this->once()) + ->method('getClient') + ->willReturn($s3ClientMock); + + $this->assertFalse($s3->connect()); + } + + public function test_wasabi_connect_successful() + { + $storageProvider = StorageProviderModel::factory()->create([ + 'provider' => StorageProvider::WASABI, + 'credentials' => [ + 'key' => 'fake-key', + 'secret' => 'fake-secret', + 'region' => 'us-east-1', + 'bucket' => 'fake-bucket', + 'path' => '/', + ], + ]); + + // Mock the S3Client (Wasabi uses S3-compatible API) + $s3ClientMock = $this->getMockBuilder(S3Client::class) + ->disableOriginalConstructor() + ->onlyMethods(['getCommand', 'execute']) + ->getMock(); + + // Mock the getCommand method + $s3ClientMock->expects($this->once()) + ->method('getCommand') + ->with('listBuckets') + ->willReturn(new \Aws\Command('listBuckets')); + + // Mock the execute method + $s3ClientMock->expects($this->once()) + ->method('execute') + ->willReturn(['Buckets' => []]); + + // Mock the Wasabi class to return the mocked S3Client + $wasabi = $this->getMockBuilder(Wasabi::class) + ->setConstructorArgs([$storageProvider]) + ->onlyMethods(['getClient']) + ->getMock(); + + $wasabi->expects($this->once()) + ->method('getClient') + ->willReturn($s3ClientMock); + + $this->assertTrue($wasabi->connect()); + } + + public function test_wasabi_connect_failure() + { + $storageProvider = StorageProviderModel::factory()->create([ + 'provider' => StorageProvider::WASABI, + 'credentials' => [ + 'key' => 'fake-key', + 'secret' => 'fake-secret', + 'region' => 'us-east-1', + 'bucket' => 'fake-bucket', + 'path' => '/', + ], + ]); + + // Mock the S3Client (Wasabi uses S3-compatible API) + $s3ClientMock = $this->getMockBuilder(S3Client::class) + ->disableOriginalConstructor() + ->onlyMethods(['getCommand', 'execute']) + ->getMock(); + + // Mock the getCommand method + $s3ClientMock->expects($this->once()) + ->method('getCommand') + ->with('listBuckets') + ->willReturn(new \Aws\Command('listBuckets')); + + // Mock the execute method to throw an S3Exception + $s3ClientMock->expects($this->once()) + ->method('execute') + ->willThrowException(new S3Exception('Error', new \Aws\Command('ListBuckets'))); + + // Mock the Wasabi class to return the mocked S3Client + $wasabi = $this->getMockBuilder(Wasabi::class) + ->setConstructorArgs([$storageProvider]) + ->onlyMethods(['getClient']) + ->getMock(); + + $wasabi->expects($this->once()) + ->method('getClient') + ->willReturn($s3ClientMock); + + $this->assertFalse($wasabi->connect()); + } + /** * @TODO: complete FTP tests */