mirror of
https://github.com/PhyreApps/PhyrePanel.git
synced 2024-11-21 23:20:24 +00:00
Compare commits
14 commits
f50776e8b6
...
908d987551
Author | SHA1 | Date | |
---|---|---|---|
|
908d987551 | ||
|
b27886a6ff | ||
|
7a3f581b7b | ||
|
e8ff2c3bc4 | ||
|
96bc319cb7 | ||
|
d1a1bfbfff | ||
|
319931585a | ||
|
6ed95760b5 | ||
|
72e7229539 | ||
|
02bfdbb1ee | ||
|
e90c6dca01 | ||
|
7eb9c3e624 | ||
|
d25cff070d | ||
|
dd3c93db37 |
13 changed files with 197 additions and 72 deletions
37
web/app/Console/Commands/CreateDailyFullBackup.php
Normal file
37
web/app/Console/Commands/CreateDailyFullBackup.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace app\Console\Commands;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Models\HostingSubscription;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateDailyFullBackup extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'phyre:create-daily-full-backup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$backup = new Backup();
|
||||
$backup->backup_type = 'full';
|
||||
$backup->save();
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ class RunBackup extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'phyre:run-backup';
|
||||
protected $signature = 'phyre:run-backup-checks';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -36,21 +36,6 @@ class RunBackup extends Command
|
|||
$backup->delete();
|
||||
}
|
||||
|
||||
$findBackupsToday = Backup::where('created_at', '>=', Carbon::now()->subHours(24))
|
||||
->where(function ($query) {
|
||||
$query->where('status', 'completed')
|
||||
->orWhere('status', 'processing');
|
||||
})
|
||||
->first();
|
||||
|
||||
if (! $findBackupsToday) {
|
||||
$backup = new Backup();
|
||||
$backup->backup_type = 'full';
|
||||
$backup->save();
|
||||
} else {
|
||||
$this->info('We already have a backup for today.');
|
||||
}
|
||||
|
||||
// Check for pending backups
|
||||
$getPendingBackups = Backup::where('status', 'pending')
|
||||
->get();
|
||||
|
|
|
@ -14,6 +14,8 @@ use Filament\Forms\Get;
|
|||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Number;
|
||||
use JaOcero\RadioDeck\Forms\Components\RadioDeck;
|
||||
|
||||
|
@ -89,6 +91,13 @@ class BackupResource extends Resource
|
|||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('download')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->action(function (Backup $backup) {
|
||||
$url = Storage::disk('backups')
|
||||
->temporaryUrl($backup->filepath, Carbon::now()->addMinutes(5));
|
||||
return redirect($url);
|
||||
}),
|
||||
Tables\Actions\ViewAction::make(),
|
||||
])
|
||||
->defaultSort('id', 'desc')
|
||||
|
|
25
web/app/Http/Controllers/BackupDownloadController.php
Normal file
25
web/app/Http/Controllers/BackupDownloadController.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class BackupDownloadController extends Controller
|
||||
{
|
||||
public function download(Request $request)
|
||||
{
|
||||
if (!URL::signatureHasNotExpired($request)) {
|
||||
return response('The URL has expired.');
|
||||
}
|
||||
|
||||
if (!URL::hasCorrectSignature($request)) {
|
||||
return response('Invalid URL provided');
|
||||
}
|
||||
|
||||
return Storage::disk('backups')->download($request->get('path'));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,11 @@ namespace App\Models;
|
|||
|
||||
use App\Filament\Enums\BackupStatus;
|
||||
use App\Helpers;
|
||||
use App\ShellApi;
|
||||
use Dotenv\Dotenv;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Env;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Backup extends Model
|
||||
|
@ -48,15 +47,18 @@ class Backup extends Model
|
|||
});
|
||||
|
||||
static::deleting(function ($model) {
|
||||
if (is_dir($model->path)) {
|
||||
shell_exec('rm -rf ' . $model->path);
|
||||
ShellApi::safeDelete($model->path,[
|
||||
Storage::path('backups')
|
||||
]);
|
||||
if (Storage::disk('backups')->exists($model->filepath)) {
|
||||
Storage::disk('backups')->delete($model->filepath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function checkCronJob()
|
||||
{
|
||||
$cronJobCommand = 'phyre-php /usr/local/phyre/web/artisan phyre:run-backup';
|
||||
$cronJobCommand = 'phyre-php /usr/local/phyre/web/artisan phyre:run-backup-checks';
|
||||
$findCronJob = CronJob::where('command', $cronJobCommand)->first();
|
||||
if (! $findCronJob) {
|
||||
$cronJob = new CronJob();
|
||||
|
@ -64,8 +66,18 @@ class Backup extends Model
|
|||
$cronJob->command = $cronJobCommand;
|
||||
$cronJob->user = 'root';
|
||||
$cronJob->save();
|
||||
return false;
|
||||
}
|
||||
|
||||
$cronJobCommand = 'phyre-php /usr/local/phyre/web/artisan phyre:create-daily-full-backup';
|
||||
$findCronJob = CronJob::where('command', $cronJobCommand)->first();
|
||||
if (! $findCronJob) {
|
||||
$cronJob = new CronJob();
|
||||
$cronJob->schedule = '0 0 * * *';
|
||||
$cronJob->command = $cronJobCommand;
|
||||
$cronJob->user = 'root';
|
||||
$cronJob->save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -80,7 +92,9 @@ class Backup extends Model
|
|||
if (! is_dir($tempValidatePath)) {
|
||||
mkdir($tempValidatePath);
|
||||
}
|
||||
shell_exec('tar -xzf '.$this->filepath.' -C '.$tempValidatePath);
|
||||
|
||||
shell_exec('cd '.$tempValidatePath.' && unzip -o '.Storage::disk('backups')->path($this->filepath));
|
||||
|
||||
$validateDatabaseFile = $tempValidatePath.'/database.sql';
|
||||
$validateEnvFile = $tempValidatePath.'/env.json';
|
||||
|
||||
|
@ -116,7 +130,11 @@ class Backup extends Model
|
|||
];
|
||||
}
|
||||
|
||||
$this->size = Helpers::checkPathSize($this->path);
|
||||
ShellApi::safeDelete($this->path,[
|
||||
Storage::path('backups')
|
||||
]);
|
||||
|
||||
$this->size = filesize(Storage::disk('backups')->path($this->filepath));
|
||||
$this->status = 'completed';
|
||||
$this->completed = true;
|
||||
$this->completed_at = now();
|
||||
|
@ -158,11 +176,11 @@ class Backup extends Model
|
|||
];
|
||||
}
|
||||
|
||||
$storagePath = storage_path('backups');
|
||||
$storagePath = Storage::path('backups');
|
||||
if (! is_dir($storagePath)) {
|
||||
mkdir($storagePath);
|
||||
}
|
||||
$backupPath = $storagePath.'/'.$this->backup_type.'/'.$this->id;
|
||||
$backupPath = $storagePath.'/'.$this->id;
|
||||
if (!is_dir(dirname($backupPath))) {
|
||||
mkdir(dirname($backupPath));
|
||||
}
|
||||
|
@ -174,14 +192,14 @@ class Backup extends Model
|
|||
mkdir($backupTempPath);
|
||||
}
|
||||
|
||||
$backupFilename = 'phyre-panel-'.date('Ymd-His').'.zip';
|
||||
$backupFilePath = $storagePath.'/' . $backupFilename;
|
||||
|
||||
if ($this->backup_type == 'full') {
|
||||
|
||||
// Export Phyre Panel database
|
||||
$databaseBackupPath = $backupTempPath.'/database.sql';
|
||||
|
||||
// Export Phyre Panel files
|
||||
$backupFilePath = $backupPath.'/phyre-panel-'.date('Ymd-His').'.tar.gz';
|
||||
|
||||
$backupLogFileName = 'backup.log';
|
||||
$backupLogFilePath = $backupPath.'/'.$backupLogFileName;
|
||||
|
||||
|
@ -197,7 +215,7 @@ class Backup extends Model
|
|||
$getEnv = Dotenv::createArrayBacked(base_path())->load();
|
||||
file_put_contents($backupTempPath.'/env.json', json_encode($getEnv, JSON_PRETTY_PRINT));
|
||||
|
||||
$shellFileContent .= 'cd '.$backupTempPath .' && tar -pczf '.$backupFilePath.' ./* '. PHP_EOL;
|
||||
$shellFileContent .= 'cd '.$backupTempPath .' && zip -r '.$backupFilePath.' ./* '. PHP_EOL;
|
||||
|
||||
$shellFileContent .= 'rm -rf '.$backupTempPath.PHP_EOL;
|
||||
$shellFileContent .= 'echo "Backup complete"' . PHP_EOL;
|
||||
|
@ -212,7 +230,7 @@ class Backup extends Model
|
|||
if ($processId > 0 && is_numeric($processId)) {
|
||||
|
||||
$this->path = $backupPath;
|
||||
$this->filepath = $backupFilePath;
|
||||
$this->filepath = $backupFilename;
|
||||
$this->status = 'processing';
|
||||
$this->queued = true;
|
||||
$this->queued_at = now();
|
||||
|
|
|
@ -95,21 +95,30 @@ class Domain extends Model
|
|||
if (empty($model->domain_public)) {
|
||||
return;
|
||||
}
|
||||
$findHostingSubscription = HostingSubscription::where('id', $model->hosting_subscription_id)->first();
|
||||
if (! $findHostingSubscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
shell_exec('rm -rf '.$model->domain_public);
|
||||
ShellApi::safeDelete($model->domain_root, ['/home/' . $findHostingSubscription->system_username]);
|
||||
|
||||
$whiteListedPathsForDelete = [
|
||||
'/etc/apache2/sites-available',
|
||||
'/etc/apache2/sites-enabled',
|
||||
];
|
||||
|
||||
$apacheConf = '/etc/apache2/sites-available/'.$model->domain.'.conf';
|
||||
shell_exec('rm -rf '.$apacheConf);
|
||||
ShellApi::safeDelete($apacheConf, $whiteListedPathsForDelete);
|
||||
|
||||
$apacheConfEnabled = '/etc/apache2/sites-enabled/'.$model->domain.'.conf';
|
||||
shell_exec('rm -rf '.$apacheConfEnabled);
|
||||
ShellApi::safeDelete($apacheConfEnabled, $whiteListedPathsForDelete);
|
||||
|
||||
// SSL
|
||||
$apacheSSLConf = '/etc/apache2/sites-available/'.$model->domain.'-ssl.conf';
|
||||
shell_exec('rm -rf '.$apacheSSLConf);
|
||||
ShellApi::safeDelete($apacheSSLConf, $whiteListedPathsForDelete);
|
||||
|
||||
$apacheSSLConfEnabled = '/etc/apache2/sites-enabled/'.$model->domain.'-ssl.conf';
|
||||
shell_exec('rm -rf '.$apacheSSLConfEnabled);
|
||||
ShellApi::safeDelete($apacheSSLConfEnabled, $whiteListedPathsForDelete);
|
||||
|
||||
});
|
||||
|
||||
|
@ -135,16 +144,7 @@ class Domain extends Model
|
|||
}
|
||||
|
||||
if (empty($this->domain_root)) {
|
||||
if ($this->is_main == 1) {
|
||||
$this->domain_root = '/home/'.$findHostingSubscription->system_username;
|
||||
$this->domain_public = '/home/'.$findHostingSubscription->system_username.'/public_html';
|
||||
$this->home_root = '/home/'.$findHostingSubscription->system_username;
|
||||
} else {
|
||||
$this->domain_root = '/home/'.$findHostingSubscription->system_username.'/domains/'.$this->domain;
|
||||
$this->domain_public = $this->domain_root.'/public_html';
|
||||
$this->home_root = '/home/'.$findHostingSubscription->system_username;
|
||||
}
|
||||
$this->save();
|
||||
throw new \Exception('Domain root not found');
|
||||
}
|
||||
|
||||
if (!is_dir($this->domain_root)) {
|
||||
|
|
|
@ -61,6 +61,10 @@ class HostingSubscription extends Model
|
|||
|
||||
static::deleting(function ($model) {
|
||||
|
||||
if (empty($model->system_username)) {
|
||||
throw new \Exception('System username is empty');
|
||||
}
|
||||
|
||||
$getLinuxUser = new GetLinuxUser();
|
||||
$getLinuxUser->setUsername($model->system_username);
|
||||
$getLinuxUserStatus = $getLinuxUser->handle();
|
||||
|
@ -100,6 +104,11 @@ class HostingSubscription extends Model
|
|||
return $this->hasMany(HostingSubscriptionBackup::class);
|
||||
}
|
||||
|
||||
public function domain()
|
||||
{
|
||||
return $this->hasMany(Domain::class);
|
||||
}
|
||||
|
||||
private function _createLinuxWebUser($model): array
|
||||
{
|
||||
$findCustomer = Customer::where('id', $model->customer_id)->first();
|
||||
|
|
|
@ -11,13 +11,18 @@ use App\Listeners\ModelDomainDeletingListener;
|
|||
use App\Listeners\ModelHostingSubscriptionCreatingListener;
|
||||
use App\Listeners\ModelHostingSubscriptionDeletingListener;
|
||||
use App\Livewire\Components\QuickServiceRestartMenu;
|
||||
use App\Models\Domain;
|
||||
use App\Models\HostingSubscription;
|
||||
use App\Policies\CustomerPolicy;
|
||||
use BladeUI\Icons\Factory;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
@ -27,6 +32,15 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// This allows us to generate a temporary url for backups downloading
|
||||
Storage::disk('backups')->buildTemporaryUrlsUsing(function ($path, $expiration, $options) {
|
||||
return URL::temporarySignedRoute(
|
||||
'backup.download',
|
||||
$expiration,
|
||||
array_merge($options, ['path' => $path])
|
||||
);
|
||||
});
|
||||
|
||||
// Register Phyre Icons set
|
||||
$this->callAfterResolving(Factory::class, function (Factory $factory) {
|
||||
$factory->add('phyre', [
|
||||
|
@ -58,5 +72,16 @@ class AppServiceProvider extends ServiceProvider
|
|||
|
||||
Gate::define('delete-customer', [CustomerPolicy::class, 'delete']);
|
||||
|
||||
if (is_file(storage_path('installed'))) {
|
||||
$getDomains = Domain::all();
|
||||
if ($getDomains->count() > 0) {
|
||||
foreach ($getDomains as $domain) {
|
||||
$this->app['config']["filesystems.disks.backups_" . Str::slug($domain->domain)] = [
|
||||
'driver' => 'local',
|
||||
'root' => $domain->domain_root . '/backups',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,33 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ShellApi
|
||||
{
|
||||
public static function safeDelete($pathOrFile, $whiteListedPaths = [])
|
||||
{
|
||||
if (empty($whiteListedPaths)) {
|
||||
throw new \Exception('Whitelist paths cannot be empty');
|
||||
}
|
||||
|
||||
$canIDeleteFile = false;
|
||||
foreach ($whiteListedPaths as $whiteListedPath) {
|
||||
if (Str::of($pathOrFile)->startsWith($whiteListedPath)) {
|
||||
$canIDeleteFile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$canIDeleteFile) {
|
||||
throw new \Exception('Cannot delete this path:' . $pathOrFile . '. Allowed paths are:' . implode(',', $whiteListedPaths));
|
||||
}
|
||||
|
||||
$exec = shell_exec('rm -rf ' . $pathOrFile);
|
||||
|
||||
return $exec;
|
||||
}
|
||||
|
||||
public static function exec($command, $argsArray = [])
|
||||
{
|
||||
$args = '';
|
||||
|
@ -15,27 +40,9 @@ class ShellApi
|
|||
|
||||
$fullCommand = $command.' '.$args;
|
||||
|
||||
// Run the command as sudo "/usr/bin/sudo "
|
||||
$execOutput = shell_exec('/usr/bin/sudo '.$fullCommand);
|
||||
$execOutput = str_replace(PHP_EOL, '', $execOutput);
|
||||
$execOutput = shell_exec($fullCommand);
|
||||
|
||||
return $execOutput;
|
||||
}
|
||||
|
||||
public static function callBin($command, $argsArray = [])
|
||||
{
|
||||
$args = '';
|
||||
if (! empty($argsArray)) {
|
||||
foreach ($argsArray as $arg) {
|
||||
$args .= escapeshellarg($arg).' ';
|
||||
}
|
||||
}
|
||||
|
||||
$fullCommand = escapeshellarg('/usr/local/phyre/bin/'.$command.'.sh').' '.$args;
|
||||
$commandAsSudo = '/usr/bin/sudo '.$fullCommand;
|
||||
|
||||
$execOutput = shell_exec($commandAsSudo);
|
||||
|
||||
return $execOutput;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,12 @@ return [
|
|||
'throw' => false,
|
||||
],
|
||||
|
||||
'backups' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/backups'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
|
|
|
@ -28,3 +28,6 @@ if (!file_exists(storage_path('installed'))) {
|
|||
}
|
||||
|
||||
Route::get('/installer', \App\Livewire\Installer::class);
|
||||
|
||||
Route::get('backup/download', [\App\Http\Controllers\BackupDownloadController::class, 'download'])
|
||||
->name('backup.download');
|
||||
|
|
|
@ -10,6 +10,7 @@ use App\Models\HostingPlan;
|
|||
use App\Models\HostingSubscription;
|
||||
use Faker\Factory;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\Feature\Api\ActionTestCase;
|
||||
|
||||
class BackupTest extends ActionTestCase
|
||||
|
@ -19,7 +20,7 @@ class BackupTest extends ActionTestCase
|
|||
ini_set('memory_limit', '-1');
|
||||
ini_set('max_execution_time', 0);
|
||||
|
||||
Artisan::call('phyre:run-backup');
|
||||
Artisan::call('phyre:create-daily-full-backup');
|
||||
|
||||
$findLastBackup = Backup::orderBy('id', 'asc')->first();
|
||||
$this->assertNotEmpty($findLastBackup);
|
||||
|
@ -40,7 +41,7 @@ class BackupTest extends ActionTestCase
|
|||
$this->assertTrue($backupFinished);
|
||||
$this->assertSame($findLastBackup->status, BackupStatus::Completed);
|
||||
$this->assertNotEmpty($findLastBackup->filepath);
|
||||
$this->assertTrue(file_exists($findLastBackup->filepath));
|
||||
$this->assertTrue(file_exists(Storage::disk('backups')->path($findLastBackup->filepath)));
|
||||
|
||||
$backup = new Backup();
|
||||
$checkCronJob = $backup->checkCronJob();
|
||||
|
@ -91,13 +92,13 @@ class BackupTest extends ActionTestCase
|
|||
|
||||
$this->assertTrue($backupCompleted);
|
||||
$this->assertNotEmpty($findBackup->filepath);
|
||||
$this->assertTrue(file_exists($findBackup->filepath));
|
||||
$this->assertTrue(file_exists(Storage::disk('backups')->path($findBackup->filepath)));
|
||||
|
||||
$getFilesize = filesize($findBackup->filepath);
|
||||
$getFilesize = filesize(Storage::disk('backups')->path($findBackup->filepath));
|
||||
$this->assertGreaterThan(0, $getFilesize);
|
||||
$this->assertSame(Helpers::checkPathSize($findBackup->path), $findBackup->size);
|
||||
$this->assertSame($getFilesize, $findBackup->size);
|
||||
|
||||
Helpers::extractTar($findBackup->filepath, $findBackup->path . '/unit-test');
|
||||
Helpers::extractTar(Storage::disk('backups')->path($findBackup->filepath), $findBackup->path . '/unit-test');
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue