Compare commits

..

13 Commits

Author SHA1 Message Date
c2625a7352 added overflow to dropdown projects list (#315) 2024-11-08 21:31:07 +01:00
b72a2ddb1c feat: add missing ufw (#346) 2024-11-08 21:24:12 +01:00
0e8e6ef56f Bump symfony/process from 7.1.1 to 7.1.7 (#351)
Bumps [symfony/process](https://github.com/symfony/process) from 7.1.1 to 7.1.7.
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/7.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.1.1...v7.1.7)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2024-11-08 20:53:01 +01:00
39fa25aee7 Bump symfony/http-foundation from 7.1.1 to 7.1.7 (#352) 2024-11-07 20:12:01 +01:00
945c2e75c0 Fix Privileges for public Schema in PostgreSQL 15+ (#339) 2024-11-07 20:02:26 +01:00
82933e29ff Bump rollup from 3.29.4 to 3.29.5 (#301)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 23:55:07 +02:00
82c1f36ef6 Update README.md (#295) 2024-10-02 21:35:18 +02:00
e06d23b31a delete feature request template 2024-10-02 21:30:10 +02:00
f0e7faa0e7 Create config.yml 2024-10-02 21:28:21 +02:00
319fdb44e7 Bump vite from 4.5.3 to 4.5.5 (#290)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-21 17:29:12 +02:00
b62c40c97d Bump micromatch from 4.0.5 to 4.0.8 (#287)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-07 18:58:45 +02:00
e39e8c17a2 Add S3 and Wasabi as storage providers (#281) 2024-09-06 23:29:43 +02:00
1391eb32d8 Fix Discord connection check (#285) 2024-09-03 21:33:17 +02:00
36 changed files with 1161 additions and 290 deletions

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://github.com/vitodeploy/vito/discussions/new?category=ideas
about: Share ideas for new features
- name: Support
url: https://github.com/vitodeploy/vito/discussions/new?category=q-a
about: Ask the community for help
- name: Discord
url: https://discord.gg/uZeeHZZnm5
about: Join the community

View File

@ -1,12 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
To request a feature or suggest an idea please add it to the feedback boards
https://vitodeploy.featurebase.app/

View File

@ -34,8 +34,7 @@ ## Useful Links
- [Documentation](https://vitodeploy.com)
- [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended)
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Roadmap](https://github.com/orgs/vitodeploy/projects/5)
- [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md)

View File

@ -9,4 +9,8 @@ final class StorageProvider
const FTP = 'ftp';
const LOCAL = 'local';
const S3 = 's3';
const WASABI = 'wasabi';
}

View File

@ -56,7 +56,7 @@ private function checkConnection(string $subject, string $text): bool
'content' => '*'.$subject.'*'."\n".$text,
]);
return $connect->ok();
return $connect->successful();
}
public function send(object $notifiable, NotificationInterface $notification): void

20
app/SSH/HasS3Storage.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\SSH;
trait HasS3Storage
{
private function prepareS3Path(string $path, string $prefix = ''): string
{
$path = trim($path);
$path = ltrim($path, '/');
$path = preg_replace('/[^a-zA-Z0-9\-_\.\/]/', '_', $path);
$path = preg_replace('/\/+/', '/', $path);
if ($prefix) {
$path = trim($prefix, '/').'/'.$path;
}
return $path;
}
}

View File

@ -1,8 +1,8 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl ufw
git config --global user.email "__email__"
git config --global user.name "__name__"
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -;
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y

View File

@ -117,6 +117,7 @@ public function deleteUser(string $username, string $host): void
public function link(string $username, string $host, array $databases): void
{
$ssh = $this->service->server->ssh();
$version = $this->service->version;
foreach ($databases as $database) {
$ssh->exec(
@ -124,6 +125,7 @@ public function link(string $username, string $host, array $databases): void
'username' => $username,
'host' => $host,
'database' => $database,
'version' => $version,
]),
'link-user-to-database'
);
@ -132,10 +134,13 @@ public function link(string $username, string $host, array $databases): void
public function unlink(string $username, string $host): void
{
$version = $this->service->version;
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/unlink.sh', [
'username' => $username,
'host' => $host,
'version' => $version,
]),
'unlink-user-from-databases'
);

View File

@ -1,5 +1,16 @@
if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE __database__ TO __username__;"; then
USER_TO_LINK='__username__'
DB_NAME='__database__'
DB_VERSION='__version__'
if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"$DB_NAME\" TO $USER_TO_LINK;"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Linking to __database__ finished"
# Check if PostgreSQL version is 15 or greater
if [ "$DB_VERSION" -ge 15 ]; then
if ! sudo -u postgres psql -d "$DB_NAME" -c "GRANT USAGE, CREATE ON SCHEMA public TO $USER_TO_LINK;"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
fi
echo "Linking to $DB_NAME finished"

View File

@ -1,10 +1,16 @@
USER_TO_REVOKE='__username__'
DB_VERSION='__version__'
DATABASES=$(sudo -u postgres psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;")
for DB in $DATABASES; do
echo "Revoking privileges in database: $DB"
sudo -u postgres psql -d "$DB" -c "REVOKE ALL PRIVILEGES ON DATABASE \"$DB\" FROM $USER_TO_REVOKE;"
# Check if PostgreSQL version is 15 or greater
if [ "$DB_VERSION" -ge 15 ]; then
sudo -u postgres psql -d "$DB" -c "REVOKE USAGE, CREATE ON SCHEMA public FROM $USER_TO_REVOKE;"
fi
done
echo "Privileges revoked from $USER_TO_REVOKE"

78
app/SSH/Storage/S3.php Normal file
View File

@ -0,0 +1,78 @@
<?php
namespace App\SSH\Storage;
use App\Exceptions\SSHCommandError;
use App\Models\Server;
use App\Models\StorageProvider;
use App\SSH\HasS3Storage;
use App\SSH\HasScripts;
use Illuminate\Support\Facades\Log;
class S3 extends S3AbstractStorage
{
use HasS3Storage, HasScripts;
public function __construct(Server $server, StorageProvider $storageProvider)
{
parent::__construct($server, $storageProvider);
$this->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 {}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\SSH\Storage;
abstract class S3AbstractStorage extends AbstractStorage
{
protected ?string $apiUrl = null;
protected ?string $bucketRegion = null;
public function getApiUrl(): string
{
return $this->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;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\SSH\Storage;
use App\Exceptions\SSHCommandError;
use App\Models\Server;
use App\Models\StorageProvider;
use App\SSH\HasS3Storage;
use App\SSH\HasScripts;
use Illuminate\Support\Facades\Log;
class Wasabi extends S3AbstractStorage
{
use HasS3Storage, HasScripts;
public function __construct(Server $server, StorageProvider $storageProvider)
{
parent::__construct($server, $storageProvider);
$this->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";
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\S3 as S3Storage;
use App\SSH\Storage\Storage;
use Aws\S3\Exception\S3Exception;
use Illuminate\Support\Facades\Log;
class S3 extends S3AbstractStorageProvider
{
public function validationRules(): array
{
return [
'key' => '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 {}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\StorageProviders;
use App\Models\StorageProvider;
use Aws\S3\S3Client;
abstract class S3AbstractStorageProvider extends AbstractStorageProvider implements S3ClientInterface, S3StorageInterface
{
protected ?string $apiUrl = null;
protected ?string $bucketRegion = null;
protected ?S3Client $client = null;
protected StorageProvider $storageProvider;
protected array $clientConfig = [];
public function getApiUrl(): string
{
return $this->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;
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\StorageProviders;
use Aws\S3\S3Client;
interface S3ClientInterface
{
public function getClient(): S3Client;
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\StorageProviders;
interface S3StorageInterface
{
public function getApiUrl(): string;
public function setApiUrl(?string $region = null): void;
public function getBucketRegion(): string;
public function setBucketRegion(string $region): void;
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\Storage;
use App\SSH\Storage\Wasabi as WasabiStorage;
use Aws\S3\Exception\S3Exception;
use Illuminate\Support\Facades\Log;
class Wasabi extends S3AbstractStorageProvider
{
private const DEFAULT_REGION = 'us-east-1';
public function validationRules(): array
{
return [
'key' => '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 {}
}

182
composer.lock generated
View File

@ -4366,16 +4366,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.1.1",
"version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa"
"reference": "5183b61657807099d98f3367bcccb850238b17a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/74d171d5b6a1d9e4bfee09a41937c17a7536acfa",
"reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9",
"reference": "5183b61657807099d98f3367bcccb850238b17a9",
"shasum": ""
},
"require": {
@ -4423,7 +4423,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.1.1"
"source": "https://github.com/symfony/http-foundation/tree/v7.1.7"
},
"funding": [
{
@ -4439,7 +4439,7 @@
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
"time": "2024-11-06T09:02:46+00:00"
},
{
"name": "symfony/http-kernel",
@ -4637,16 +4637,16 @@
},
{
"name": "symfony/mime",
"version": "v7.1.1",
"version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "21027eaacc1a8a20f5e616c25c3580f5dd3a15df"
"reference": "caa1e521edb2650b8470918dfe51708c237f0598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/21027eaacc1a8a20f5e616c25c3580f5dd3a15df",
"reference": "21027eaacc1a8a20f5e616c25c3580f5dd3a15df",
"url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
"reference": "caa1e521edb2650b8470918dfe51708c237f0598",
"shasum": ""
},
"require": {
@ -4701,7 +4701,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.1.1"
"source": "https://github.com/symfony/mime/tree/v7.1.6"
},
"funding": [
{
@ -4717,7 +4717,7 @@
"type": "tidelift"
}
],
"time": "2024-06-04T06:40:14+00:00"
"time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -4878,22 +4878,21 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c"
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
"shasum": ""
},
"require": {
"php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
"symfony/polyfill-php72": "^1.10"
"php": ">=7.2",
"symfony/polyfill-intl-normalizer": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
@ -4942,7 +4941,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
},
"funding": [
{
@ -4958,24 +4957,24 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
@ -5023,7 +5022,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
},
"funding": [
{
@ -5039,24 +5038,24 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@ -5103,7 +5102,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@ -5119,97 +5118,24 @@
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "10112722600777e02d2745716b70c5db4ca70442"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442",
"reference": "10112722600777e02d2745716b70c5db4ca70442",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
@ -5256,7 +5182,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@ -5272,24 +5198,24 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
@ -5332,7 +5258,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
},
"funding": [
{
@ -5348,7 +5274,7 @@
"type": "tidelift"
}
],
"time": "2024-06-19T12:35:24+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-uuid",
@ -5431,16 +5357,16 @@
},
{
"name": "symfony/process",
"version": "v7.1.1",
"version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "febf90124323a093c7ee06fdb30e765ca3c20028"
"reference": "9b8a40b7289767aa7117e957573c2a535efe6585"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028",
"reference": "febf90124323a093c7ee06fdb30e765ca3c20028",
"url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585",
"reference": "9b8a40b7289767aa7117e957573c2a535efe6585",
"shasum": ""
},
"require": {
@ -5472,7 +5398,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.1.1"
"source": "https://github.com/symfony/process/tree/v7.1.7"
},
"funding": [
{
@ -5488,7 +5414,7 @@
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
"time": "2024-11-06T09:25:12+00:00"
},
{
"name": "symfony/routing",

View File

@ -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' => [

22
package-lock.json generated
View File

@ -22,7 +22,7 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"vite": "^4.5.3"
"vite": "^4.5.5"
}
},
"node_modules/@alloc/quick-lru": {
@ -1283,12 +1283,12 @@
}
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@ -1798,9 +1798,9 @@
}
},
"node_modules/rollup": {
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@ -2234,9 +2234,9 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",

View File

@ -24,6 +24,6 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"vite": "^4.5.3"
"vite": "^4.5.5"
}
}

View File

@ -0,0 +1 @@
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><title>AWS Simple Storage Service (S3)</title><defs><linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="a"><stop stop-color="#1B660F" offset="0%"/><stop stop-color="#6CAE3E" offset="100%"/></linearGradient></defs><g><rect fill="url(#a)" width="256" height="256"/><path d="M194.67488,137.25632 L195.90368,128.60352 C207.23488,135.39072 207.38208,138.19392 207.378964,138.27072 C207.35968,138.28672 205.42688,139.89952 194.67488,137.25632 L194.67488,137.25632 Z M188.45728,135.52832 C168.87328,129.60192 141.59968,117.08992 130.56288,111.87392 C130.56288,111.82912 130.57568,111.78752 130.57568,111.74272 C130.57568,107.50272 127.12608,104.05312 122.88288,104.05312 C118.64608,104.05312 115.19648,107.50272 115.19648,111.74272 C115.19648,115.98272 118.64608,119.43232 122.88288,119.43232 C124.74528,119.43232 126.43488,118.73792 127.76928,117.63392 C140.75488,123.78112 167.81728,136.11072 187.54528,141.93472 L179.74368,196.99392 C179.72128,197.14432 179.71168,197.29472 179.71168,197.44512 C179.71168,202.29312 158.24928,211.19872 123.18048,211.19872 C87.74048,211.19872 66.05088,202.29312 66.05088,197.44512 C66.05088,197.29792 66.04128,197.15392 66.02208,197.00992 L49.72128,77.94752 C63.83008,87.65952 94.17568,92.79872 123.19968,92.79872 C152.17888,92.79872 182.47328,87.67872 196.61088,77.99552 L188.45728,135.52832 Z M47.99968,65.52832 C48.23008,61.31712 72.42848,44.79872 123.19968,44.79872 C173.96448,44.79872 198.16608,61.31392 198.39968,65.52832 L198.39968,66.96512 C195.61568,76.40832 164.25568,86.39872 123.19968,86.39872 C82.07328,86.39872 50.69728,76.37632 47.99968,66.92032 L47.99968,65.52832 Z M204.79968,65.59872 C204.79968,54.51072 173.01088,38.39872 123.19968,38.39872 C73.38848,38.39872 41.59968,54.51072 41.59968,65.59872 L41.90048,68.01152 L59.65408,197.68832 C60.07968,212.19072 98.75488,217.59872 123.18048,217.59872 C153.49088,217.59872 185.69248,210.62912 186.10848,197.69792 L193.77568,143.62752 C198.04128,144.64832 201.55168,145.16992 204.37088,145.16992 C208.15648,145.16992 210.71648,144.24512 212.26848,142.39552 C213.54208,140.87872 214.02848,139.04192 213.66368,137.08672 C212.83488,132.65792 207.57728,127.88352 196.87008,121.77472 L204.47328,68.13632 L204.79968,65.59872 Z" fill="#FFFFFF"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -57,7 +57,7 @@ class="{{ $width }} {{ $alignmentClasses }} absolute z-50 mt-2 rounded-md"
style="display: none"
@if ($closeOnClick) @click="open = false" @endif
>
<div class="{{ $contentClasses }} rounded-md">
<div class="{{ $contentClasses }} max-h-80 overflow-y-auto rounded-md">
{{ $content }}
</div>
</div>

View File

@ -1,3 +1,14 @@
@use(App\Enums\StorageProvider)
@php
$storageProviders = [
StorageProvider::DROPBOX,
StorageProvider::FTP,
StorageProvider::LOCAL,
StorageProvider::S3,
StorageProvider::WASABI,
];
@endphp
<div>
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'connect-provider')">
{{ __("Connect") }}
@ -69,136 +80,8 @@ class="p-6"
@enderror
</div>
@if ($provider == \App\Enums\StorageProvider::DROPBOX)
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" />
@error("token")
<x-input-error class="mt-2" :messages="$message" />
@enderror
<a
class="mt-1 text-primary-500"
href="https://dropbox.tech/developers/generate-an-access-token-for-your-own-account"
target="_blank"
>
How to generate?
</a>
</div>
@endif
@if ($provider == \App\Enums\StorageProvider::FTP)
<div class="mt-6">
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="host" value="Host" />
<x-text-input
value="{{ old('host') }}"
id="host"
name="host"
type="text"
class="mt-1 w-full"
/>
@error("host")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="port" value="Port" />
<x-text-input
value="{{ old('port') }}"
id="port"
name="port"
type="text"
class="mt-1 w-full"
/>
@error("port")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="mt-6">
<x-input-label for="path" value="Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="username" value="Username" />
<x-text-input
value="{{ old('username') }}"
id="username"
name="username"
type="text"
class="mt-1 w-full"
/>
@error("username")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input
value="{{ old('password') }}"
id="password"
name="password"
type="text"
class="mt-1 w-full"
/>
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="ssl" :value="__('SSL')" />
<x-select-input id="ssl" name="ssl" class="mt-1 w-full">
<option value="1" @if(old('ssl')) selected @endif>
{{ __("Yes") }}
</option>
<option value="0" @if(!old('ssl')) selected @endif>
{{ __("No") }}
</option>
</x-select-input>
@error("ssl")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="passive" :value="__('Passive')" />
<x-select-input id="passive" name="passive" class="mt-1 w-full">
<option value="1" @if(old('passive')) selected @endif>
{{ __("Yes") }}
</option>
<option value="0" @if(!old('passive')) selected @endif>
{{ __("No") }}
</option>
</x-select-input>
@error("passive")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
</div>
@endif
@if ($provider == \App\Enums\StorageProvider::LOCAL)
<div class="mt-6">
<x-input-label for="path" value="Absolute Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
<x-input-help>
The absolute path on your server that the database exists. like `/home/vito/db-backups`
</x-input-help>
<x-input-help>
Make sure that the path exists and the `vito` user has permission to write to it.
</x-input-help>
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@if (in_array($provider, $storageProviders))
@include("settings.storage-providers.providers.{$provider}")
@endif
<div class="mt-6">

View File

@ -0,0 +1,15 @@
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" />
@error("token")
<x-input-error class="mt-2" :messages="$message" />
@enderror
<a
class="mt-1 text-primary-500"
href="https://dropbox.tech/developers/generate-an-access-token-for-your-own-account"
target="_blank"
>
How to generate?
</a>
</div>

View File

@ -0,0 +1,71 @@
<div class="mt-6">
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="host" value="Host" />
<x-text-input value="{{ old('host') }}" id="host" name="host" type="text" class="mt-1 w-full" />
@error("host")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="port" value="Port" />
<x-text-input value="{{ old('port') }}" id="port" name="port" type="text" class="mt-1 w-full" />
@error("port")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="mt-6">
<x-input-label for="path" value="Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="username" value="Username" />
<x-text-input value="{{ old('username') }}" id="username" name="username" type="text" class="mt-1 w-full" />
@error("username")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input value="{{ old('password') }}" id="password" name="password" type="text" class="mt-1 w-full" />
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="ssl" :value="__('SSL')" />
<x-select-input id="ssl" name="ssl" class="mt-1 w-full">
<option value="1" @if(old('ssl')) selected @endif>
{{ __("Yes") }}
</option>
<option value="0" @if(!old('ssl')) selected @endif>
{{ __("No") }}
</option>
</x-select-input>
@error("ssl")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="passive" :value="__('Passive')" />
<x-select-input id="passive" name="passive" class="mt-1 w-full">
<option value="1" @if(old('passive')) selected @endif>
{{ __("Yes") }}
</option>
<option value="0" @if(!old('passive')) selected @endif>
{{ __("No") }}
</option>
</x-select-input>
@error("passive")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="mt-6">
<x-input-label for="path" value="Absolute Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
<x-input-help>The absolute path on your server that the database exists. like `/home/vito/db-backups`</x-input-help>
<x-input-help>Make sure that the path exists and the `vito` user has permission to write to it.</x-input-help>
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,49 @@
<div class="mt-6">
<div class="mt-6">
<x-input-label for="path" value="Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="key" value="Key" />
<x-text-input value="{{ old('key') }}" id="key" name="key" type="text" class="mt-1 w-full" />
@error("key")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="secret" value="Secret" />
<x-text-input value="{{ old('secret') }}" id="secret" name="secret" type="text" class="mt-1 w-full" />
@error("secret")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<a
class="mt-1 text-primary-500"
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/configuring-bucket-key.html"
target="_blank"
>
How to generate?
</a>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="region" value="Region" />
<x-text-input value="{{ old('region') }}" id="region" name="region" type="text" class="mt-1 w-full" />
@error("region")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="bucket" value="Bucket" />
<x-text-input value="{{ old('bucket') }}" id="bucket" name="bucket" type="text" class="mt-1 w-full" />
@error("bucket")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
</div>

View File

@ -0,0 +1,49 @@
<div class="mt-6">
<div class="mt-6">
<x-input-label for="path" value="Path" />
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="key" value="Key" />
<x-text-input value="{{ old('key') }}" id="key" name="key" type="text" class="mt-1 w-full" />
@error("key")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="secret" value="Secret" />
<x-text-input value="{{ old('secret') }}" id="secret" name="secret" type="text" class="mt-1 w-full" />
@error("secret")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<a
class="mt-1 text-primary-500"
href="https://docs.wasabi.com/docs/creating-a-user-account-and-access-key"
target="_blank"
>
How to generate?
</a>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="region" value="Region" />
<x-text-input value="{{ old('region') }}" id="region" name="region" type="text" class="mt-1 w-full" />
@error("region")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="bucket" value="Bucket" />
<x-text-input value="{{ old('bucket') }}" id="bucket" name="bucket" type="text" class="mt-1 w-full" />
@error("bucket")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
</div>

View File

@ -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
*/