added FTP support to storage providers (#58)

* added FTP support to storage providers

* build and code style fix
This commit is contained in:
Saeed Vaziry 2023-09-24 12:50:01 +02:00 committed by GitHub
parent 2c81e324f6
commit 7d98986f52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 409 additions and 27 deletions

View File

@ -21,13 +21,15 @@ public function create(User $user, array $input): void
'user_id' => $user->id,
'provider' => $input['provider'],
'profile' => $input['name'],
'credentials' => [
'token' => $input['token'],
],
]);
$this->validateProvider($input, $storageProvider->provider()->validationRules());
$storageProvider->credentials = $storageProvider->provider()->credentialData($input);
if (! $storageProvider->provider()->connect()) {
throw ValidationException::withMessages([
'token' => __("Couldn't connect to the provider"),
'provider' => __("Couldn't connect to the provider"),
]);
}
$storageProvider->save();
@ -44,9 +46,11 @@ private function validate(User $user, array $input): void
'required',
Rule::unique('storage_providers', 'profile')->where('user_id', $user->id),
],
'token' => [
'required',
],
])->validate();
}
private function validateProvider(array $input, array $rules): void
{
Validator::make($input, $rules)->validate();
}
}

View File

@ -6,6 +6,10 @@
interface StorageProvider
{
public function validationRules(): array;
public function credentialData(array $input): array;
public function connect(): bool;
public function upload(Server $server, string $src, string $dest): array;

View File

@ -6,7 +6,7 @@
final class StorageProvider extends Enum
{
const GOOGLE = 'google';
const DROPBOX = 'dropbox';
const FTP = 'ftp';
}

View File

@ -14,6 +14,20 @@ class ConnectProvider extends Component
public string $token;
public string $host;
public string $port;
public string $path = '/';
public string $username;
public string $password;
public int $ssl = 1;
public int $passive = 0;
public function connect(): void
{
app(CreateStorageProvider::class)->create(auth()->user(), $this->all());

View File

@ -76,7 +76,7 @@ public function getPathAttribute(): string
public function getStoragePathAttribute(): string
{
return '/'.$this->backup->database->name.'/'.$this->name.'.zip';
return '/'.$this->name.'.zip';
}
public function restore(Database $database): void

View File

@ -0,0 +1,40 @@
<?php
namespace App\SSHCommands\Storage;
use App\SSHCommands\Command;
use Illuminate\Support\Facades\File;
class DownloadFromFTPCommand extends Command
{
public function __construct(
protected string $src,
protected string $dest,
protected string $host,
protected string $port,
protected string $username,
protected string $password,
protected bool $ssl,
protected bool $passive,
) {
}
public function file(): string
{
return File::get(resource_path('commands/storage/download-from-ftp.sh'));
}
public function content(): string
{
return str($this->file())
->replace('__src__', $this->src)
->replace('__dest__', $this->dest)
->replace('__host__', $this->host)
->replace('__port__', $this->port)
->replace('__username__', $this->username)
->replace('__password__', $this->password)
->replace('__ssl__', $this->ssl ? 's' : '')
->replace('__passive__', $this->passive ? '--ftp-pasv' : '')
->toString();
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\SSHCommands\Storage;
use App\SSHCommands\Command;
use Illuminate\Support\Facades\File;
class UploadToFTPCommand extends Command
{
public function __construct(
protected string $src,
protected string $dest,
protected string $host,
protected string $port,
protected string $username,
protected string $password,
protected bool $ssl,
protected bool $passive,
) {
}
public function file(): string
{
return File::get(resource_path('commands/storage/upload-to-ftp.sh'));
}
public function content(): string
{
return str($this->file())
->replace('__src__', $this->src)
->replace('__dest__', $this->dest)
->replace('__host__', $this->host)
->replace('__port__', $this->port)
->replace('__username__', $this->username)
->replace('__password__', $this->password)
->replace('__ssl__', $this->ssl ? 's' : '')
->replace('__passive__', $this->passive ? '--ftp-pasv' : '')
->toString();
}
}

View File

@ -13,6 +13,20 @@ class Dropbox extends AbstractStorageProvider
{
protected string $apiUrl = 'https://api.dropboxapi.com/2';
public function validationRules(): array
{
return [
'token' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'token' => $input['token'],
];
}
public function connect(): bool
{
$res = Http::withToken($this->storageProvider->credentials['token'])

View File

@ -0,0 +1,129 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSHCommands\Storage\DownloadFromFTPCommand;
use App\SSHCommands\Storage\UploadToFTPCommand;
use FTP\Connection;
use Throwable;
class FTP extends AbstractStorageProvider
{
public function validationRules(): array
{
return [
'host' => 'required',
'port' => 'required|numeric',
'path' => 'required',
'username' => 'required',
'password' => 'required',
'ssl' => 'required',
'passive' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'host' => $input['host'],
'port' => $input['port'],
'path' => $input['path'],
'username' => $input['username'],
'password' => $input['password'],
'ssl' => (bool) $input['ssl'],
'passive' => (bool) $input['passive'],
];
}
public function connect(): bool
{
$connection = $this->connection();
$isConnected = $connection && $this->login($connection);
if ($isConnected) {
ftp_close($connection);
}
return $isConnected;
}
/**
* @throws Throwable
*/
public function upload(Server $server, string $src, string $dest): array
{
$server->ssh()->exec(
new UploadToFTPCommand(
$src,
$this->storageProvider->credentials['path'].'/'.$dest,
$this->storageProvider->credentials['host'],
$this->storageProvider->credentials['port'],
$this->storageProvider->credentials['username'],
$this->storageProvider->credentials['password'],
$this->storageProvider->credentials['ssl'],
$this->storageProvider->credentials['passive'],
),
'upload-to-ftp'
);
return [
'size' => null,
];
}
/**
* @throws Throwable
*/
public function download(Server $server, string $src, string $dest): void
{
$server->ssh()->exec(
new DownloadFromFTPCommand(
$this->storageProvider->credentials['path'].'/'.$src,
$dest,
$this->storageProvider->credentials['host'],
$this->storageProvider->credentials['port'],
$this->storageProvider->credentials['username'],
$this->storageProvider->credentials['password'],
$this->storageProvider->credentials['ssl'],
$this->storageProvider->credentials['passive'],
),
'download-from-ftp'
);
}
public function delete(array $paths): void
{
$connection = $this->connection();
if ($connection && $this->login($connection)) {
if ($this->storageProvider->credentials['passive']) {
ftp_pasv($connection, true);
}
foreach ($paths as $path) {
ftp_delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
}
}
ftp_close($connection);
}
private function connection(): bool|Connection
{
$credentials = $this->storageProvider->credentials;
if ($credentials['ssl']) {
return ftp_ssl_connect($credentials['host'], $credentials['port'], 5);
}
return ftp_connect($credentials['host'], $credentials['port'], 5);
}
private function login(Connection $connection): bool
{
$credentials = $this->storageProvider->credentials;
return ftp_login($connection, $credentials['username'], $credentials['password']);
}
}

View File

@ -15,7 +15,8 @@
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.8",
"livewire/livewire": "^2.12",
"phpseclib/phpseclib": "~3.0"
"phpseclib/phpseclib": "~3.0",
"ext-ftp": "*"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

5
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "41386d67a21e3c07c635228527722815",
"content-hash": "53be6925a69aeafb21d079b82e51c1c4",
"packages": [
{
"name": "aws/aws-crt-php",
@ -9080,7 +9080,8 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.1"
"php": "^8.1",
"ext-ftp": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"

View File

@ -30,6 +30,7 @@
use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab;
use App\StorageProviders\Dropbox;
use App\StorageProviders\FTP;
return [
/*
@ -355,8 +356,10 @@
*/
'storage_providers' => [
'dropbox',
'ftp',
],
'storage_providers_class' => [
'dropbox' => Dropbox::class,
'ftp' => FTP::class,
],
];

View File

@ -13,7 +13,7 @@
|
*/
'default' => env('QUEUE_CONNECTION', 'sync'),
'default' => env('QUEUE_CONNECTION', 'default'),
/*
|--------------------------------------------------------------------------

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-c65c56fa.css",
"file": "assets/app-99c9ce18.css",
"isEntry": true,
"src": "resources/css/app.css"
},

View File

@ -0,0 +1 @@
curl __passive__ -u "__username__:__password__" ftp__ssl__://__host__:__port__/__src__ -o "__dest__"

View File

@ -0,0 +1 @@
curl __passive__ -T "__src__" -u "__username__:__password__" ftp__ssl__://__host__:__port__/__dest__

View File

@ -18,7 +18,7 @@
<tr>
<x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Size") }}</x-th>
{{--<x-th>{{ __("Size") }}</x-th>--}}
<x-th>{{ __("Status") }}</x-th>
<x-th>{{ __("Restored") }}</x-th>
<x-th>{{ __("Restored To") }}</x-th>
@ -30,7 +30,7 @@
<x-td>
<x-datetime :value="$file->created_at" />
</x-td>
<x-td>{{ $file->size }}</x-td>
{{--<x-td>{{ $file->size }}</x-td>--}}
<x-td>
<div class="inline-flex">
@include('livewire.databases.partials.backup-file-status', ['status' => $file->status])

View File

@ -32,13 +32,79 @@
@enderror
</div>
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" />
@error('token')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@if($provider == \App\Enums\StorageProvider::DROPBOX)
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" />
@error('token')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
@if($provider == \App\Enums\StorageProvider::FTP)
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="host" value="Host" />
<x-text-input wire:model.defer="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 wire:model.defer="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 wire:model.defer="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 wire:model.defer="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 wire:model.defer="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 wire:model="ssl" id="ssl" name="ssl" class="mt-1 w-full">
<option value="1" @if($ssl) selected @endif>{{ __("Yes") }}</option>
<option value="0" @if(!$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 wire:model="passive" id="passive" name="passive" class="mt-1 w-full">
<option value="1" @if($passive) selected @endif>{{ __("Yes") }}</option>
<option value="0" @if(!$passive) selected @endif>{{ __("No") }}</option>
</x-select-input>
@error('passive')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
@endif
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">

View File

@ -11,12 +11,18 @@
@foreach($providers as $provider)
<x-item-card>
<div class="flex-none">
<img src="{{ asset('static/images/' . $provider->provider . '.svg') }}" class="h-10 w-10" alt="">
@if($provider->provider == \App\Enums\StorageProvider::FTP)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-gray-600 dark:text-gray-200">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
@else
<img src="{{ asset('static/images/' . $provider->provider . '.svg') }}" class="h-10 w-10" alt="">
@endif
</div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $provider->profile }}</span>
<span class="text-sm text-gray-400">
<x-datetime :value="$provider->created_at"/>
<x-datetime :value="$provider->created_at" />
</span>
</div>
<div class="flex items-center">

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\SSHCommands\Storage;
use App\SSHCommands\Storage\DownloadFromFTPCommand;
use Tests\TestCase;
class DownloadFromFTPCommandTest extends TestCase
{
public function test_generate_command()
{
$command = new DownloadFromFTPCommand(
'src',
'dest',
'1.1.1.1',
'21',
'username',
'password',
false,
true,
);
$expected = <<<'EOD'
curl --ftp-pasv -u "username:password" ftp://1.1.1.1:21/src -o "dest"
EOD;
$this->assertStringContainsString($expected, $command->content());
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\SSHCommands\Storage;
use App\SSHCommands\Storage\UploadToFTPCommand;
use Tests\TestCase;
class UploadToFTPCommandTest extends TestCase
{
public function test_generate_command()
{
$command = new UploadToFTPCommand(
'src',
'dest',
'1.1.1.1',
'21',
'username',
'password',
true,
true
);
$expected = <<<'EOD'
curl --ftp-pasv -T "src" -u "username:password" ftps://1.1.1.1:21/dest
EOD;
$this->assertStringContainsString($expected, $command->content());
}
}