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 @@ + \ 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 @@ + \ 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 +