mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 09:51:37 +00:00
Edit & Download (local) Backups (#436)
* Allow editing of backups * pint updates * setup of backup download * allow download for local backup files * delete uploaded files on delete of BackupFile * pint updates * S3 upload & download fixes * Deletion of backup files * support $ARCH selector for s3 installation * delete files when deleting backup * fixed ui issue * adjustment * Use system temp path for downloads --------- Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
This commit is contained in:
parent
465951fd1e
commit
a73476c1dd
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Enums\BackupFileStatus;
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Enums\DatabaseStatus;
|
||||
use App\Models\Backup;
|
||||
@ -10,7 +11,7 @@
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CreateBackup
|
||||
class ManageBackup
|
||||
{
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
@ -34,6 +35,31 @@ public function create(Server $server, array $input): Backup
|
||||
return $backup;
|
||||
}
|
||||
|
||||
public function update(Backup $backup, array $input): void
|
||||
{
|
||||
$backup->interval = $input['interval'] == 'custom' ? $input['custom_interval'] : $input['interval'];
|
||||
$backup->keep_backups = $input['keep'];
|
||||
$backup->save();
|
||||
}
|
||||
|
||||
public function delete(Backup $backup): void
|
||||
{
|
||||
$backup->status = BackupStatus::DELETING;
|
||||
$backup->save();
|
||||
|
||||
dispatch(function () use ($backup) {
|
||||
$files = $backup->files;
|
||||
foreach ($files as $file) {
|
||||
$file->status = BackupFileStatus::DELETING;
|
||||
$file->save();
|
||||
|
||||
$file->deleteFile();
|
||||
}
|
||||
|
||||
$backup->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public static function rules(Server $server, array $input): array
|
||||
{
|
||||
$rules = [
|
39
app/Actions/Database/ManageBackupFile.php
Normal file
39
app/Actions/Database/ManageBackupFile.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Enums\BackupFileStatus;
|
||||
use App\Models\BackupFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Throwable;
|
||||
|
||||
class ManageBackupFile
|
||||
{
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download(BackupFile $file): StreamedResponse
|
||||
{
|
||||
$localFilename = "backup_{$file->id}_{$file->name}.zip";
|
||||
|
||||
if (! Storage::disk('backups')->exists($localFilename)) {
|
||||
$file->backup->server->ssh()->download(
|
||||
Storage::disk('backups')->path($localFilename),
|
||||
$file->path()
|
||||
);
|
||||
}
|
||||
|
||||
return Storage::disk('backups')->download($localFilename, $file->name.'.zip');
|
||||
}
|
||||
|
||||
public function delete(BackupFile $file): void
|
||||
{
|
||||
$file->status = BackupFileStatus::DELETING;
|
||||
$file->save();
|
||||
|
||||
dispatch(function () use ($file) {
|
||||
$file->deleteFile();
|
||||
});
|
||||
}
|
||||
}
|
@ -7,4 +7,6 @@ final class BackupStatus
|
||||
const RUNNING = 'running';
|
||||
|
||||
const FAILED = 'failed';
|
||||
|
||||
const DELETING = 'deleting';
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ public function download(string $local, string $remote): void
|
||||
$this->connect(true);
|
||||
}
|
||||
|
||||
$this->connection->get($remote, $local, SFTP::SOURCE_LOCAL_FILE);
|
||||
$this->connection->get($remote, $local);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,8 +56,17 @@ public static function boot(): void
|
||||
public static array $statusColors = [
|
||||
BackupStatus::RUNNING => 'success',
|
||||
BackupStatus::FAILED => 'danger',
|
||||
BackupStatus::DELETING => 'warning',
|
||||
];
|
||||
|
||||
public function isCustomInterval(): bool
|
||||
{
|
||||
$intervals = array_keys(config('core.cronjob_intervals'));
|
||||
$intervals = array_filter($intervals, fn ($interval) => $interval !== 'custom');
|
||||
|
||||
return ! in_array($this->interval, $intervals);
|
||||
}
|
||||
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Database\ManageBackupFile;
|
||||
use App\Enums\BackupFileStatus;
|
||||
use App\Enums\StorageProvider as StorageProviderAlias;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -46,19 +48,11 @@ protected static function booted(): void
|
||||
->where('id', '<=', $lastFileToKeep->id)
|
||||
->get();
|
||||
foreach ($files as $file) {
|
||||
$file->delete();
|
||||
app(ManageBackupFile::class)->delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function (BackupFile $backupFile) {
|
||||
$provider = $backupFile->backup->storage->provider();
|
||||
$path = $backupFile->storagePath();
|
||||
dispatch(function () use ($provider, $path) {
|
||||
$provider->delete([$path]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static array $statusColors = [
|
||||
@ -71,18 +65,52 @@ protected static function booted(): void
|
||||
BackupFileStatus::RESTORE_FAILED => 'danger',
|
||||
];
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return ! in_array(
|
||||
$this->status,
|
||||
[BackupFileStatus::CREATING, BackupFileStatus::FAILED, BackupFileStatus::DELETING]
|
||||
);
|
||||
}
|
||||
|
||||
public function isLocal(): bool
|
||||
{
|
||||
return $this->backup->storage->provider === StorageProviderAlias::LOCAL;
|
||||
}
|
||||
|
||||
public function backup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Backup::class);
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
public function tempPath(): string
|
||||
{
|
||||
return '/home/'.$this->backup->server->getSshUser().'/'.$this->name.'.zip';
|
||||
}
|
||||
|
||||
public function storagePath(): string
|
||||
public function path(): string
|
||||
{
|
||||
return '/'.$this->backup->database->name.'/'.$this->name.'.zip';
|
||||
$storage = $this->backup->storage;
|
||||
$databaseName = $this->backup->database->name;
|
||||
|
||||
return match ($storage->provider) {
|
||||
StorageProviderAlias::DROPBOX => '/'.$databaseName.'/'.$this->name.'.zip',
|
||||
StorageProviderAlias::S3, StorageProviderAlias::FTP, StorageProviderAlias::LOCAL => implode('/', [
|
||||
rtrim($storage->credentials['path'], '/'),
|
||||
$databaseName,
|
||||
$this->name.'.zip',
|
||||
]),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
public function deleteFile(): void
|
||||
{
|
||||
try {
|
||||
$storage = $this->backup->storage->provider()->ssh($this->backup->server);
|
||||
$storage->delete($this->path());
|
||||
} finally {
|
||||
$this->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,6 +243,16 @@ public function resourceInfo(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function deleteFile(string $path): void
|
||||
{
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('delete-file.sh', [
|
||||
'path' => $path,
|
||||
]),
|
||||
'delete-file'
|
||||
);
|
||||
}
|
||||
|
||||
private function deleteTempFile(string $name): void
|
||||
{
|
||||
if (Storage::disk('local')->exists($name)) {
|
||||
|
1
app/SSH/OS/scripts/delete-file.sh
Normal file
1
app/SSH/OS/scripts/delete-file.sh
Normal file
@ -0,0 +1 @@
|
||||
rm -f __path__
|
@ -159,12 +159,12 @@ public function runBackup(BackupFile $backupFile): void
|
||||
|
||||
// upload to storage
|
||||
$upload = $backupFile->backup->storage->provider()->ssh($this->service->server)->upload(
|
||||
$backupFile->tempPath(),
|
||||
$backupFile->path(),
|
||||
$backupFile->storagePath(),
|
||||
);
|
||||
|
||||
// cleanup
|
||||
$this->service->server->ssh()->exec('rm '.$backupFile->path());
|
||||
$this->service->server->ssh()->exec('rm '.$backupFile->tempPath());
|
||||
|
||||
$backupFile->size = $upload['size'];
|
||||
$backupFile->save();
|
||||
@ -174,14 +174,14 @@ public function restoreBackup(BackupFile $backupFile, string $database): void
|
||||
{
|
||||
// download
|
||||
$backupFile->backup->storage->provider()->ssh($this->service->server)->download(
|
||||
$backupFile->storagePath(),
|
||||
$backupFile->name.'.zip',
|
||||
$backupFile->path(),
|
||||
$backupFile->tempPath(),
|
||||
);
|
||||
|
||||
$this->service->server->ssh()->exec(
|
||||
$this->getScript($this->getScriptsDir().'/restore.sh', [
|
||||
'database' => $database,
|
||||
'file' => $backupFile->name,
|
||||
'file' => rtrim($backupFile->tempPath(), '.zip'),
|
||||
]),
|
||||
'restore-database'
|
||||
);
|
||||
|
@ -45,11 +45,14 @@ public function download(string $src, string $dest): void
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void
|
||||
public function delete(string $src): void
|
||||
{
|
||||
//
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('dropbox/delete-file.sh', [
|
||||
'src' => $src,
|
||||
'token' => $this->storageProvider->credentials['token'],
|
||||
]),
|
||||
'delete-from-dropbox'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ public function upload(string $src, string $dest): array
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('ftp/upload.sh', [
|
||||
'src' => $src,
|
||||
'dest' => $this->storageProvider->credentials['path'].'/'.$dest,
|
||||
'dest' => $dest,
|
||||
'host' => $this->storageProvider->credentials['host'],
|
||||
'port' => $this->storageProvider->credentials['port'],
|
||||
'username' => $this->storageProvider->credentials['username'],
|
||||
@ -33,7 +33,7 @@ public function download(string $src, string $dest): void
|
||||
{
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('ftp/download.sh', [
|
||||
'src' => $this->storageProvider->credentials['path'].'/'.$src,
|
||||
'src' => $src,
|
||||
'dest' => $dest,
|
||||
'host' => $this->storageProvider->credentials['host'],
|
||||
'port' => $this->storageProvider->credentials['port'],
|
||||
@ -46,11 +46,19 @@ public function download(string $src, string $dest): void
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void
|
||||
public function delete(string $src): void
|
||||
{
|
||||
//
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('ftp/delete-file.sh', [
|
||||
'src' => $src,
|
||||
'host' => $this->storageProvider->credentials['host'],
|
||||
'port' => $this->storageProvider->credentials['port'],
|
||||
'username' => $this->storageProvider->credentials['username'],
|
||||
'password' => $this->storageProvider->credentials['password'],
|
||||
'ssl' => $this->storageProvider->credentials['ssl'],
|
||||
'passive' => $this->storageProvider->credentials['passive'],
|
||||
]),
|
||||
'delete-from-ftp'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,13 +10,12 @@ class Local extends AbstractStorage
|
||||
|
||||
public function upload(string $src, string $dest): array
|
||||
{
|
||||
$destDir = dirname($this->storageProvider->credentials['path'].$dest);
|
||||
$destFile = basename($this->storageProvider->credentials['path'].$dest);
|
||||
$destDir = dirname($dest);
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('local/upload.sh', [
|
||||
'src' => $src,
|
||||
'dest_dir' => $destDir,
|
||||
'dest_file' => $destFile,
|
||||
'dest_file' => $dest,
|
||||
]),
|
||||
'upload-to-local'
|
||||
);
|
||||
@ -30,20 +29,15 @@ public function download(string $src, string $dest): void
|
||||
{
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('local/download.sh', [
|
||||
'src' => $this->storageProvider->credentials['path'].$src,
|
||||
'src' => $src,
|
||||
'dest' => $dest,
|
||||
]),
|
||||
'download-from-local'
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(string $path): void
|
||||
public function delete(string $src): void
|
||||
{
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('local/delete.sh', [
|
||||
'path' => $this->storageProvider->credentials['path'].$path,
|
||||
]),
|
||||
'delete-from-local'
|
||||
);
|
||||
$this->server->os()->deleteFile($src);
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ 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),
|
||||
'dest' => $this->prepareS3Path($dest),
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->storageProvider->credentials['region'],
|
||||
@ -52,7 +52,7 @@ public function download(string $src, string $dest): void
|
||||
$provider = $this->storageProvider->provider();
|
||||
|
||||
$downloadCommand = $this->getScript('s3/download.sh', [
|
||||
'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src),
|
||||
'src' => $this->prepareS3Path($src),
|
||||
'dest' => $dest,
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
@ -71,8 +71,21 @@ public function download(string $src, string $dest): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void {}
|
||||
public function delete(string $src): void
|
||||
{
|
||||
/** @var \App\StorageProviders\S3 $provider */
|
||||
$provider = $this->storageProvider->provider();
|
||||
|
||||
$this->server->ssh()->exec(
|
||||
$this->getScript('s3/delete-file.sh', [
|
||||
'src' => $this->prepareS3Path($src),
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->storageProvider->credentials['region'],
|
||||
'endpoint' => $provider->getApiUrl(),
|
||||
]),
|
||||
'delete-from-s3'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,5 @@ public function upload(string $src, string $dest): array;
|
||||
|
||||
public function download(string $src, string $dest): void;
|
||||
|
||||
public function delete(string $path): void;
|
||||
public function delete(string $src): void;
|
||||
}
|
||||
|
6
app/SSH/Storage/scripts/dropbox/delete-file.sh
Normal file
6
app/SSH/Storage/scripts/dropbox/delete-file.sh
Normal file
@ -0,0 +1,6 @@
|
||||
curl --location --request POST 'https://api.dropboxapi.com/2/files/delete_v2' \
|
||||
--header 'Authorization: Bearer __token__' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"path": "__src__"
|
||||
}'
|
1
app/SSH/Storage/scripts/ftp/delete-file.sh
Normal file
1
app/SSH/Storage/scripts/ftp/delete-file.sh
Normal file
@ -0,0 +1 @@
|
||||
curl __passive__ -u "__username__:__password__" ftp__ssl__://__host__:__port__/__src__ -Q "DELE /__src__"
|
@ -1 +0,0 @@
|
||||
rm __path__
|
@ -1,2 +1,2 @@
|
||||
mkdir -p __dest_dir__
|
||||
cp __src__ __dest_dir__/__dest_file__
|
||||
cp __src__ __dest_file__
|
||||
|
30
app/SSH/Storage/scripts/s3/delete-file.sh
Normal file
30
app/SSH/Storage/scripts/s3/delete-file.sh
Normal file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_aws_cli() {
|
||||
echo "Installing AWS CLI"
|
||||
ARCH=$(uname -m)
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-$ARCH.zip" -o "aws.zip"
|
||||
unzip -q aws.zip
|
||||
sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
|
||||
rm -rf aws.zip aws
|
||||
}
|
||||
|
||||
if ! command_exists aws; then
|
||||
install_aws_cli
|
||||
fi
|
||||
|
||||
if ! command_exists aws; then
|
||||
echo "Error: AWS CLI installation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AWS_ACCESS_KEY_ID=__key__
|
||||
export AWS_SECRET_ACCESS_KEY=__secret__
|
||||
export AWS_DEFAULT_REGION=__region__
|
||||
export AWS_ENDPOINT_URL=__endpoint__
|
||||
|
||||
aws s3 rm s3://__bucket__/__src__
|
@ -1,32 +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__"
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Use the provided endpoint in the correct format
|
||||
ENDPOINT="__endpoint__"
|
||||
BUCKET="__bucket__"
|
||||
REGION="__region__"
|
||||
install_aws_cli() {
|
||||
echo "Installing AWS CLI"
|
||||
ARCH=$(uname -m)
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-$ARCH.zip" -o "aws.zip"
|
||||
unzip -q aws.zip
|
||||
sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
|
||||
rm -rf aws.zip aws
|
||||
}
|
||||
|
||||
# Ensure that DEST does not have a trailing slash
|
||||
SRC="__src__"
|
||||
DEST="__dest__"
|
||||
if ! command_exists aws; then
|
||||
install_aws_cli
|
||||
fi
|
||||
|
||||
# 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"
|
||||
if ! command_exists aws; then
|
||||
echo "Error: AWS CLI installation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AWS_ACCESS_KEY_ID=__key__
|
||||
export AWS_SECRET_ACCESS_KEY=__secret__
|
||||
export AWS_DEFAULT_REGION=__region__
|
||||
export AWS_ENDPOINT_URL=__endpoint__
|
||||
|
||||
if aws s3 cp s3://__bucket__/__src__ __dest__; then
|
||||
echo "Download successful"
|
||||
fi
|
||||
|
@ -1,59 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if AWS CLI is installed
|
||||
if ! command -v aws &> /dev/null
|
||||
then
|
||||
echo "AWS CLI is not installed. Installing..."
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Detect system architecture
|
||||
install_aws_cli() {
|
||||
echo "Installing AWS CLI"
|
||||
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"
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-$ARCH.zip" -o "aws.zip"
|
||||
unzip -q aws.zip
|
||||
sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
|
||||
rm -rf aws.zip aws
|
||||
}
|
||||
|
||||
if ! command_exists aws; then
|
||||
install_aws_cli
|
||||
fi
|
||||
|
||||
if ! command_exists aws; then
|
||||
echo "Error: AWS CLI installation failed"
|
||||
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
|
||||
export AWS_ACCESS_KEY_ID=__key__
|
||||
export AWS_SECRET_ACCESS_KEY=__secret__
|
||||
export AWS_DEFAULT_REGION=__region__
|
||||
export AWS_ENDPOINT_URL=__endpoint__
|
||||
|
||||
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
|
||||
if aws s3 cp __src__ s3://__bucket__/__dest__; then
|
||||
echo "Upload successful"
|
||||
else
|
||||
echo "Upload failed"
|
||||
exit 1
|
||||
fi
|
||||
|
@ -38,19 +38,4 @@ public function ssh(Server $server): Storage
|
||||
{
|
||||
return new \App\SSH\Storage\Dropbox($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function delete(array $paths): void
|
||||
{
|
||||
$data = [];
|
||||
foreach ($paths as $path) {
|
||||
$data[] = ['path' => $path];
|
||||
}
|
||||
Http::withToken($this->storageProvider->credentials['token'])
|
||||
->withHeaders([
|
||||
'Content-Type:application/json',
|
||||
])
|
||||
->post($this->apiUrl.'/files/delete_batch', [
|
||||
'entries' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -57,23 +57,6 @@ public function ssh(Server $server): Storage
|
||||
return new \App\SSH\Storage\FTP($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function delete(array $paths): void
|
||||
{
|
||||
$connection = $this->connection();
|
||||
|
||||
if ($connection && $this->login($connection)) {
|
||||
if ($this->storageProvider->credentials['passive']) {
|
||||
\App\Facades\FTP::passive($connection, true);
|
||||
}
|
||||
|
||||
foreach ($paths as $path) {
|
||||
\App\Facades\FTP::delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
|
||||
}
|
||||
}
|
||||
|
||||
\App\Facades\FTP::close($connection);
|
||||
}
|
||||
|
||||
private function connection(): bool|Connection
|
||||
{
|
||||
$credentials = $this->storageProvider->credentials;
|
||||
|
@ -30,9 +30,4 @@ public function ssh(Server $server): Storage
|
||||
{
|
||||
return new \App\SSH\Storage\Local($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function delete(array $paths): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,4 @@ public function ssh(Server $server): Storage
|
||||
{
|
||||
return new S3Storage($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function delete(array $paths): void {}
|
||||
}
|
||||
|
@ -14,6 +14,4 @@ public function credentialData(array $input): array;
|
||||
public function connect(): bool;
|
||||
|
||||
public function ssh(Server $server): Storage;
|
||||
|
||||
public function delete(array $paths): void;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases;
|
||||
|
||||
use App\Actions\Database\CreateBackup;
|
||||
use App\Actions\Database\ManageBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\StorageProvider;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
@ -38,12 +38,12 @@ protected function getHeaderActions(): array
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->options($this->server->databases()->pluck('name', 'id')->toArray())
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['database'])
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['database'])
|
||||
->searchable(),
|
||||
Select::make('storage')
|
||||
->label('Storage')
|
||||
->options(StorageProvider::getByProjectId($this->server->project_id)->pluck('profile', 'id')->toArray())
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['storage'])
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['storage'])
|
||||
->suffixAction(
|
||||
\Filament\Forms\Components\Actions\Action::make('connect')
|
||||
->form(Create::form())
|
||||
@ -59,21 +59,21 @@ protected function getHeaderActions(): array
|
||||
->label('Interval')
|
||||
->options(config('core.cronjob_intervals'))
|
||||
->reactive()
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['interval']),
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['interval']),
|
||||
TextInput::make('custom_interval')
|
||||
->label('Custom Interval (Cron)')
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['custom_interval'])
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['custom_interval'])
|
||||
->visible(fn (callable $get) => $get('interval') === 'custom')
|
||||
->placeholder('0 * * * *'),
|
||||
TextInput::make('keep')
|
||||
->label('Backups to Keep')
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['keep'])
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['keep'])
|
||||
->helperText('How many backups to keep before deleting the oldest one'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data) {
|
||||
run_action($this, function () use ($data) {
|
||||
app(CreateBackup::class)->create($this->server, $data);
|
||||
app(ManageBackup::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\ManageBackupFile;
|
||||
use App\Actions\Database\RestoreBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
@ -59,11 +60,21 @@ public function table(Table $table): Table
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('download')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->visible(fn (BackupFile $record) => $record->isAvailable() && $record->isLocal())
|
||||
->tooltip('Download')
|
||||
->action(function (BackupFile $record) {
|
||||
return app(ManageBackupFile::class)->download($record);
|
||||
})
|
||||
->authorize(fn (BackupFile $record) => auth()->user()->can('view', $record)),
|
||||
Action::make('restore')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->modalHeading('Restore Backup')
|
||||
->tooltip('Restore Backup')
|
||||
->disabled(fn (BackupFile $record) => ! $record->isAvailable())
|
||||
->authorize(fn (BackupFile $record) => auth()->user()->can('update', $record->backup))
|
||||
->form([
|
||||
Select::make('database')
|
||||
@ -95,16 +106,15 @@ public function table(Table $table): Table
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database')
|
||||
->modalHeading('Delete Backup File')
|
||||
->color('danger')
|
||||
->disabled(fn (BackupFile $record) => ! $record->isAvailable())
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (BackupFile $record) => auth()->user()->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (BackupFile $record) {
|
||||
run_action($this, function () use ($record) {
|
||||
$record->delete();
|
||||
app(ManageBackupFile::class)->delete($record);
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\ManageBackup;
|
||||
use App\Actions\Database\RunBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@ -57,12 +61,53 @@ public function table(Table $table): Table
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('edit')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-pencil')
|
||||
->tooltip('Edit Configuration')
|
||||
->disabled(fn (Backup $record) => ! in_array($record->status, ['running', 'failed']))
|
||||
->authorize(fn (Backup $record) => auth()->user()->can('update', $record))
|
||||
->modelLabel('Edit Backup')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalSubmitActionLabel('Update')
|
||||
->form([
|
||||
Select::make('interval')
|
||||
->label('Interval')
|
||||
->options(config('core.cronjob_intervals'))
|
||||
->reactive()
|
||||
->default(fn (Backup $record) => $record->isCustomInterval() ? 'custom' : $record->interval)
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['interval']),
|
||||
TextInput::make('custom_interval')
|
||||
->label('Custom Interval (Cron)')
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['custom_interval'])
|
||||
->visible(fn (callable $get) => $get('interval') === 'custom')
|
||||
->default(fn (Backup $record) => $record->isCustomInterval() ? $record->interval : '')
|
||||
->placeholder('0 * * * *'),
|
||||
TextInput::make('keep')
|
||||
->label('Backups to Keep')
|
||||
->default(fn (Backup $record) => $record->keep_backups)
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['keep'])
|
||||
->helperText('How many backups to keep before deleting the oldest one'),
|
||||
])
|
||||
->action(function (Backup $backup, array $data) {
|
||||
run_action($this, function () use ($data, $backup) {
|
||||
app(ManageBackup::class)->update($backup, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup updated!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('files')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->modalHeading('Backup Files')
|
||||
->color('gray')
|
||||
->tooltip('Show backup files')
|
||||
->disabled(fn (Backup $record) => ! in_array($record->status, ['running', 'failed']))
|
||||
->authorize(fn (Backup $record) => auth()->user()->can('viewAny', [BackupFile::class, $record]))
|
||||
->modalContent(fn (Backup $record) => view('components.dynamic-widget', [
|
||||
'widget' => BackupFilesList::class,
|
||||
@ -88,16 +133,16 @@ public function table(Table $table): Table
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database')
|
||||
->modalHeading('Delete Backup & Files')
|
||||
->disabled(fn (Backup $record) => ! in_array($record->status, ['running', 'failed']))
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (Backup $record) => auth()->user()->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (Backup $record) {
|
||||
run_action($this, function () use ($record) {
|
||||
$record->delete();
|
||||
app(ManageBackup::class)->delete($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
@ -62,6 +62,11 @@
|
||||
'root' => storage_path('app/key-pairs'),
|
||||
],
|
||||
|
||||
'backups' => [
|
||||
'driver' => 'local',
|
||||
'root' => sys_get_temp_dir(),
|
||||
],
|
||||
|
||||
// @deprecated
|
||||
'key-pairs-local' => [
|
||||
'driver' => 'local',
|
||||
|
@ -115,6 +115,43 @@ public function test_see_backups_list(): void
|
||||
->assertSee($backup->database->name);
|
||||
}
|
||||
|
||||
public function test_update_backup(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$database = Database::factory()->create([
|
||||
'server_id' => $this->server,
|
||||
]);
|
||||
|
||||
$storage = StorageProvider::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'provider' => \App\Enums\StorageProvider::DROPBOX,
|
||||
]);
|
||||
|
||||
$backup = Backup::factory()->create([
|
||||
'server_id' => $this->server->id,
|
||||
'database_id' => $database->id,
|
||||
'storage_id' => $storage->id,
|
||||
'interval' => '0 * * * *',
|
||||
'keep_backups' => 5,
|
||||
]);
|
||||
|
||||
Livewire::test(BackupsList::class, [
|
||||
'server' => $this->server,
|
||||
])
|
||||
->callTableAction('edit', $backup->id, [
|
||||
'interval' => '0 0 * * *',
|
||||
'keep' => '10',
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('backups', [
|
||||
'id' => $backup->id,
|
||||
'interval' => '0 0 * * *',
|
||||
'keep_backups' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_backup(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
Loading…
x
Reference in New Issue
Block a user