Compare commits

..

26 commits

Author SHA1 Message Date
Bozhidar
023560c09d Update app-code-coverage.yml 2024-05-03 00:10:55 +03:00
Bozhidar
726f151d8a update 2024-05-03 00:01:05 +03:00
Bozhidar
6184a6dcc3 update 2024-05-02 23:54:02 +03:00
Bozhidar
77f6e11350 update 2024-05-02 23:47:49 +03:00
Bozhidar
8db638d4f5 update 2024-05-02 23:22:17 +03:00
Bozhidar
8fece790aa Update BackupTest.php 2024-05-02 23:07:26 +03:00
Bozhidar
4cc2fbb40d update 2024-05-02 23:06:52 +03:00
Bozhidar
70b51841eb update 2024-05-02 22:50:24 +03:00
Bozhidar
2327b7d1ad update 2024-05-02 22:44:30 +03:00
Bozhidar
971a7b3b19 Update ZHostingSubscriptionBackupTest.php 2024-05-02 22:00:04 +03:00
Bozhidar
4129a0875a Update ZBackupTest.php 2024-05-02 21:56:05 +03:00
Bozhidar
c0678b17fa Update BackupStats.php 2024-05-02 21:34:39 +03:00
Bozhidar
aecf11c084 Update BackupTest.php 2024-05-02 21:15:48 +03:00
Bozhidar
a1359933e0 Update MasterDomain.php 2024-05-02 21:11:57 +03:00
Bozhidar
ee4587f81d Update HostingSubscriptionBackup.php 2024-05-02 20:59:16 +03:00
Bozhidar
35d8bf58a5 update 2024-05-02 20:56:10 +03:00
Bozhidar
28a9f2392d update 2024-05-02 20:35:37 +03:00
Bozhidar
093ac611b7 update 2024-05-02 20:15:25 +03:00
Bozhidar
489ae3ee08 Update HostingSubscriptionBackup.php 2024-05-02 19:41:51 +03:00
Bozhidar
d738974329 Update HostingSubscriptionBackup.php 2024-05-02 19:41:20 +03:00
Bozhidar
fc6aac0f53 update 2024-05-02 19:33:47 +03:00
Bozhidar
33adbec9e4 Update Backup.php 2024-05-02 18:43:54 +03:00
Bozhidar
13b5e89f81 update 2024-05-02 18:38:42 +03:00
Bozhidar
98efa1b42c update 2024-05-02 17:51:26 +03:00
Bozhidar
e1d7d50a1f Update Backup.php 2024-05-02 16:53:53 +03:00
Bozhidar
f8f0460bb9 Update Backup.php 2024-05-02 16:53:05 +03:00
29 changed files with 295 additions and 157 deletions

View file

@ -10,7 +10,7 @@ jobs:
matrix:
os: [ubuntu-22.04]
runs-on: ${{ matrix.os }}
runs-on: hetzner-${{ matrix.os }}
steps:
- name: Checkout Repository
@ -65,6 +65,7 @@ jobs:
cd /usr/local/phyre/web/
sudo chmod -R 777 vendor
apt install composer -y
composer test:coverage
- name: Code Cov

View file

@ -11,7 +11,7 @@ jobs:
#os: [ubuntu-24.04, ubuntu-22.04, ubuntu-20.04]
os: [ubuntu-22.04]
runs-on: ${{ matrix.os }}
runs-on: hetzner-${{ matrix.os }}
steps:
- name: Checkout Repository

View file

@ -98,7 +98,7 @@ RUN cp .env.example .env \
&& sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PANEL_DB_USER/" .env \
&& sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PANEL_DB_PASSWORD/" .env \
&& sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env \
&& sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_ROOT_USERNAME/" .env \
&& sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_ROOT_USERNAME/" .env \
&& sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD/" .env
# Generate application key and migrate database

View file

@ -130,7 +130,7 @@ sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PANEL_DB_USER/" .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PANEL_DB_PASSWORD/" .env
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD/" .env
phyre-php artisan key:generate

View file

@ -54,7 +54,7 @@ sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PHYRE_PANEL_DB_USER/" .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PHYRE_PANEL_DB_PASSWORD/" .env
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_PHYRE_ROOT_PASSWORD/" .env
phyre-php artisan key:generate

View file

@ -135,7 +135,7 @@ sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PHYRE_PANEL_DB_USER/" .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PHYRE_PANEL_DB_PASSWORD/" .env
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_PHYRE_ROOT_PASSWORD/" .env
phyre-php artisan key:generate

View file

@ -54,7 +54,7 @@ sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PHYRE_PANEL_DB_USER/" .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PHYRE_PANEL_DB_PASSWORD/" .env
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_PHYRE_ROOT_PASSWORD/" .env
phyre-php artisan key:generate

View file

@ -137,7 +137,7 @@ sed -i "s/^DB_USERNAME=.*/DB_USERNAME=$PHYRE_PANEL_DB_USER/" .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$PHYRE_PANEL_DB_PASSWORD/" .env
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/^MYSQl_ROOT_USERNAME=.*/MYSQl_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_USERNAME=.*/MYSQL_ROOT_USERNAME=$MYSQL_PHYRE_ROOT_USERNAME/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$MYSQL_PHYRE_ROOT_PASSWORD/" .env
phyre-php artisan key:generate

View file

@ -17,7 +17,7 @@ DB_PASSWORD=
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQl_ROOT_USERNAME=root
MYSQL_ROOT_USERNAME=root
MYSQL_ROOT_PASSWORD=root
BROADCAST_DRIVER=log

47
web/app/BackupStorage.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App;
use App\Models\Domain;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
class BackupStorage
{
public static function getPath()
{
$rootPath = '/var/lib/phyre/backups/system';
$customBackupPath = setting('general.backup_path');
if (!empty($customBackupPath)) {
$rootPath = $customBackupPath;
}
return $rootPath;
}
public static function getInstance($path = false)
{
$rootPath = self::getPath();
if ($path) {
$rootPath = $path;
}
$storageBuild = Storage::build([
'driver' => 'local',
'throw' => false,
'root' => $rootPath,
]);
$storageBuild->buildTemporaryUrlsUsing(function ($path, $expiration, $options) use($rootPath) {
return URL::temporarySignedRoute(
'backup.download',
$expiration,
array_merge($options, [
'path' => $path,
'root_path' => $rootPath,
])
);
});
return $storageBuild;
}
}

View file

@ -4,11 +4,16 @@ namespace App\Console\Commands;
use App\Models\Backup;
use App\Models\HostingSubscription;
use App\Models\HostingSubscriptionBackup;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Migrations\Migration;
class CreateDailyFullHostingSubscriptionsBackup extends Command
{
/**
@ -30,7 +35,27 @@ class CreateDailyFullHostingSubscriptionsBackup extends Command
*/
public function handle()
{
// Find Hosting Subscriptions
$findHostingSubscriptions = HostingSubscription::all();
if ($findHostingSubscriptions->count() > 0) {
foreach ($findHostingSubscriptions as $hostingSubscription) {
$findBackup = HostingSubscriptionBackup::where('hosting_subscription_id', $hostingSubscription->id)
->where('backup_type', 'full')
->where('created_at', '>=', Carbon::now()->subHours(24))
->first();
if (! $findBackup) {
$backup = new HostingSubscriptionBackup();
$backup->hosting_subscription_id = $hostingSubscription->id;
$backup->backup_type = 'full';
$backup->save();
} else {
$this->error('Backup already exists for ' . $hostingSubscription->domain);
$this->error('Created before: ' . $findBackup->created_at->diffForHumans());
}
}
}
}
}

View file

@ -37,28 +37,6 @@ class RunHostingSubscriptionsBackupChecks extends Command
$backup->delete();
}
// // Find Hosting Subscriptions
// $findHostingSubscriptions = HostingSubscription::all();
// if ($findHostingSubscriptions->count() > 0) {
// foreach ($findHostingSubscriptions as $hostingSubscription) {
//
// $findBackup = HostingSubscriptionBackup::where('hosting_subscription_id', $hostingSubscription->id)
// ->where('backup_type', 'full')
// ->where('created_at', '>=', Carbon::now()->subHours(24))
// ->first();
// if (! $findBackup) {
// $backup = new HostingSubscriptionBackup();
// $backup->hosting_subscription_id = $hostingSubscription->id;
// $backup->backup_type = 'full';
// $backup->save();
// } else {
// $this->error('Backup already exists for ' . $hostingSubscription->domain);
// $this->error('Created before: ' . $findBackup->created_at->diffForHumans());
// }
// }
// }
// Check for pending backups
$getPendingBackups = HostingSubscriptionBackup::where('status', 'pending')
->get();

View file

@ -10,7 +10,9 @@ use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Illuminate\Support\Facades\Storage;
use Outerweb\FilamentSettings\Filament\Pages\Settings as BaseSettings;
use Symfony\Component\Console\Input\Input;
class Settings extends BaseSettings
{
@ -29,6 +31,7 @@ class Settings extends BaseSettings
public function schema(): array|Closure
{
return [
Tabs::make('Settings')
->schema([
@ -57,6 +60,12 @@ class Settings extends BaseSettings
Textarea::make('general.domain_suspend_page_html'),
Textarea::make('general.domain_created_page_html'),
]),
Tabs\Tab::make('Backups')
->schema([
TextInput::make('general.backup_path')
->default(Storage::path('backups'))
]),
]),
];
}

View file

@ -2,6 +2,7 @@
namespace App\Filament\Resources;
use App\BackupStorage;
use App\Filament\Enums\BackupStatus;
use App\Filament\Enums\BackupType;
use App\Filament\Resources\BackupResource\Pages;
@ -101,10 +102,15 @@ class BackupResource extends Resource
->actions([
Tables\Actions\Action::make('download')
->icon('heroicon-o-arrow-down-tray')
->hidden(function (Backup $backup) {
return $backup->status !== BackupStatus::Completed;
})
->action(function (Backup $backup) {
$url = Storage::disk('backups')
->temporaryUrl($backup->filepath, Carbon::now()->addMinutes(5));
return redirect($url);
$backupStorage = BackupStorage::getInstance($backup->root_path);
$tempUrl = $backupStorage->temporaryUrl($backup->file_name, Carbon::now()->addMinutes(5));
return redirect($tempUrl);
}),
Tables\Actions\ViewAction::make(),
])

View file

@ -2,6 +2,7 @@
namespace app\Filament\Resources\BackupResource\Widgets;
use App\BackupStorage;
use App\Filament\Resources\BackupResource\Pages\ListBackups;
use App\Filament\Resources\Shop\OrderResource\Pages\ListOrders;
use App\Models\Backup;
@ -31,7 +32,7 @@ class BackupStats extends BaseWidget
}
}
$usedSpace = 0;
$backupPath = storage_path('backups');
$backupPath = BackupStorage::getPath();
if (is_dir($backupPath)) {
$usedSpace = $this->getDirectorySize($backupPath);
}

View file

@ -2,6 +2,8 @@
namespace app\Filament\Resources\HostingSubscriptionResource\Pages;
use App\BackupStorage;
use App\Filament\Enums\BackupStatus;
use App\Filament\Enums\HostingSubscriptionBackupType;
use App\Filament\Resources\Blog\PostResource;
use App\Filament\Resources\HostingSubscriptionResource;
@ -18,6 +20,7 @@ use Filament\Resources\Pages\ManageRelatedRecords;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Carbon;
use JaOcero\RadioDeck\Forms\Components\RadioDeck;
class ManageHostingSubscriptionBackups extends ManageRelatedRecords
@ -126,6 +129,18 @@ class ManageHostingSubscriptionBackups extends ManageRelatedRecords
//
])
->actions([
Tables\Actions\Action::make('download')
->icon('heroicon-o-arrow-down-tray')
->hidden(function (HostingSubscriptionBackup $backup) {
return $backup->status !== BackupStatus::Completed;
})
->action(function (HostingSubscriptionBackup $backup) {
$backupStorage = BackupStorage::getInstance($backup->root_path);
$tempUrl = $backupStorage->temporaryUrl($backup->file_name, Carbon::now()->addMinutes(5));
return redirect($tempUrl);
}),
Tables\Actions\ViewAction::make(),
// Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),

View file

@ -4,6 +4,15 @@ namespace App;
class Helpers
{
public static function extractZip($tarFile, $extractPath)
{
shell_exec('mkdir -p ' . $extractPath);
$exec = shell_exec('unzip -o ' . $tarFile . ' -d ' . $extractPath);
return $exec;
}
public static function extractTar($tarFile, $extractPath)
{
shell_exec('mkdir -p ' . $extractPath);

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\BackupStorage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
@ -19,7 +20,9 @@ class BackupDownloadController extends Controller
return response('Invalid URL provided');
}
return Storage::disk('backups')->download($request->get('path'));
$backupStorage = BackupStorage::getInstance($request->get('root_path'));
return $backupStorage->download($request->get('path'));
}
}

View file

@ -46,12 +46,16 @@ class MasterDomain
$apacheBaseConfig = $apacheVirtualHostBuilder->buildConfig();
shell_exec('mkdir -p /var/www/logs/apache2');
shell_exec('touch /var/www/logs/apache2/bytes.log');
shell_exec('touch /var/www/logs/apache2/access.log');
shell_exec('touch /var/www/logs/apache2/error.log');
if (!empty($apacheBaseConfig)) {
file_put_contents('/etc/apache2/sites-available/'.$this->domain.'.conf', $apacheBaseConfig);
shell_exec('ln -s /etc/apache2/sites-available/'.$this->domain.'-default.conf /etc/apache2/sites-enabled/'.$this->domain.'-default.conf');
}
// install SSL
$findDomainSSLCertificate = null;

View file

@ -2,12 +2,14 @@
namespace App\Models;
use App\BackupStorage;
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\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Jackiedo\DotenvEditor\Facades\DotenvEditor;
@ -87,7 +89,6 @@ class Backup extends Model
public function checkBackup()
{
if ($this->status == BackupStatus::Processing) {
$backupDoneFile = $this->path.'/backup.done';
if (file_exists($backupDoneFile)) {
@ -96,7 +97,7 @@ class Backup extends Model
mkdir($tempValidatePath);
}
shell_exec('cd '.$tempValidatePath.' && unzip -o '.Storage::disk('backups')->path($this->filepath));
shell_exec('cd '.$tempValidatePath.' && unzip -o '.$this->file_path);
$validateDatabaseFile = $tempValidatePath.'/database.sql';
$validateEnvFile = $tempValidatePath.'/.env';
@ -110,6 +111,7 @@ class Backup extends Model
}
if (count($errorsBag) > 0) {
$this->status = 'failed';
$this->backup_log = 'Backup failed. Database or env file missing.';
$this->save();
return [
'status' => 'failed',
@ -125,6 +127,7 @@ class Backup extends Model
if (count($errorsBag) > 0) {
$this->status = 'failed';
$this->backup_log = 'Backup failed. Database or env file content mismatch.';
$this->save();
return [
'status' => 'failed',
@ -134,13 +137,17 @@ class Backup extends Model
}
ShellApi::safeDelete($this->path,[
Storage::path('backups')
$this->root_path
]);
ShellApi::safeDelete($this->temp_path,[
$this->root_path
]);
$this->size = filesize(Storage::disk('backups')->path($this->filepath));
$this->size = filesize($this->file_path);
$this->status = 'completed';
$this->completed = true;
$this->completed_at = now();
$this->backup_log = 'Backup completed';
$this->save();
return [
@ -152,7 +159,9 @@ class Backup extends Model
$checkProcess = shell_exec('ps -p ' . $this->process_id . ' | grep ' . $this->process_id);
if (Str::contains($checkProcess, $this->process_id)) {
$this->size = Helpers::checkPathSize($this->path);
$this->backup_log = file_get_contents($this->path.'/backup.log');
$this->save();
return [
@ -160,6 +169,7 @@ class Backup extends Model
'message' => 'Backup is still processing'
];
} else {
$this->backup_log = 'Backup failed. Process not found';
$this->status = 'failed';
$this->save();
return [
@ -179,20 +189,11 @@ class Backup extends Model
];
}
$storagePath = Storage::path('backups');
if (! is_dir($storagePath)) {
mkdir($storagePath);
}
$storagePath = BackupStorage::getPath();
$backupPath = $storagePath.'/'.$this->id;
if (!is_dir(dirname($backupPath))) {
mkdir(dirname($backupPath));
}
if (! is_dir($backupPath)) {
mkdir($backupPath);
}
$backupTempPath = $backupPath.'/temp';
if (! is_dir($backupTempPath)) {
mkdir($backupTempPath);
shell_exec('mkdir -p '.$backupTempPath);
}
$backupFilename = 'phyre-backup-'.date('Ymd-His').'.zip';
@ -211,7 +212,13 @@ class Backup extends Model
$shellFileContent .= 'echo "Backup Phyre Panel files"'. PHP_EOL;
// Export Phyre Panel database
$shellFileContent .= 'mysqldump -u "'.env('MYSQl_ROOT_USERNAME').'" -p"'.env('MYSQL_ROOT_PASSWORD').'" "'.env('DB_DATABASE').'" > '.$databaseBackupPath . PHP_EOL;
$mysqlAuthConf = '/root/.phyre-mysql.cnf';
$mysqlAuthContent = '[client]' . PHP_EOL;
$mysqlAuthContent .= 'user="' . env('MYSQL_ROOT_USERNAME') .'"'. PHP_EOL;
$mysqlAuthContent .= 'password="' . env('MYSQL_ROOT_PASSWORD') . '"' . PHP_EOL;
file_put_contents($mysqlAuthConf, $mysqlAuthContent);
$shellFileContent .= 'mysqldump --defaults-extra-file='.$mysqlAuthConf.' "'.env('DB_DATABASE').'" > '.$databaseBackupPath . PHP_EOL;
// Export Phyre Panel ENV
$getEnv = Dotenv::createArrayBacked(base_path())->load();
@ -226,11 +233,14 @@ class Backup extends Model
$findHostingSubscription = HostingSubscription::all();
if ($findHostingSubscription->count() > 0) {
foreach ($findHostingSubscription as $hostingSubscription) {
$hostingSubscriptionPath = $backupTempPath .'/hosting_subscriptions/'.$hostingSubscription->system_username;
$hostingSubscriptionsMainPath = $backupTempPath .'/hosting_subscriptions';
$hostingSubscriptionPath = $hostingSubscriptionsMainPath .'/'. $hostingSubscription->system_username;
$shellFileContent .= PHP_EOL;
$shellFileContent .= 'echo "Backup up hosting subscription: ' . $hostingSubscription->system_username .'" '. PHP_EOL;
$shellFileContent .= 'mkdir -p '.$hostingSubscriptionPath.PHP_EOL;
$shellFileContent .= 'cp -r /home/'.$hostingSubscription->system_username.'/* .[^.]* ' . $hostingSubscriptionPath .PHP_EOL;
// cp -r (copy recursively, also copy hidden files)
$shellFileContent .= 'cp -r /home/'.$hostingSubscription->system_username.'/ ' . $hostingSubscriptionsMainPath .PHP_EOL;
$shellFileContent .= 'mkdir -p '.$hostingSubscriptionPath.'/databases'.PHP_EOL;
@ -246,7 +256,7 @@ class Backup extends Model
$databaseName = $database->database_name_prefix . $database->database_name;
$shellFileContent .= 'echo "Backup up database: ' . $databaseName . '" ' . PHP_EOL;
$databaseBackupPath = $hostingSubscriptionPath . '/databases/' . $databaseName . '.sql';
$shellFileContent .= 'mysqldump -u "' . env('MYSQl_ROOT_USERNAME') . '" -p"' . env('MYSQL_ROOT_PASSWORD') . '" "' . $databaseName . '" > ' . $databaseBackupPath . PHP_EOL;
$shellFileContent .= 'mysqldump --defaults-extra-file='.$mysqlAuthConf.' "' . $databaseName . '" > ' . $databaseBackupPath . PHP_EOL;
}
}
@ -254,7 +264,8 @@ class Backup extends Model
}
}
$shellFileContent .= 'cd '.$backupTempPath .' && zip -r '.$backupFilePath.' ./* .[^.]* '. PHP_EOL;
// With find, we can search for all files,directories (including hidden) in the current directory and zip them
$shellFileContent .= 'cd '.$backupTempPath .' && find . -exec zip -r '.$backupFilePath.' {} \;'. PHP_EOL;
$shellFileContent .= 'rm -rf '.$backupTempPath.PHP_EOL;
$shellFileContent .= 'echo "Backup complete"' . PHP_EOL;
@ -269,11 +280,16 @@ class Backup extends Model
if ($processId > 0 && is_numeric($processId)) {
$this->path = $backupPath;
$this->filepath = $backupFilename;
$this->root_path = $storagePath;
$this->temp_path = $backupTempPath;
$this->file_path = $backupFilePath;
$this->file_name = $backupFilename;
$this->status = 'processing';
$this->queued = true;
$this->queued_at = now();
$this->process_id = $processId;
$this->save();
return [

View file

@ -62,7 +62,7 @@ class Database extends Model
$universalDatabaseExecutor = new UniversalDatabaseExecutor(
env('MYSQL_HOST'),
env('MYSQL_PORT'),
env('MYSQl_ROOT_USERNAME'),
env('MYSQL_ROOT_USERNAME'),
env('MYSQL_ROOT_PASSWORD'),
);
$createDatabase = $universalDatabaseExecutor->createDatabase($databaseName);

View file

@ -58,7 +58,7 @@ class DatabaseUser extends Model
$universalDatabaseExecutor = new UniversalDatabaseExecutor(
env('MYSQL_HOST'),
env('MYSQL_PORT'),
env('MYSQl_ROOT_USERNAME'),
env('MYSQL_ROOT_USERNAME'),
env('MYSQL_ROOT_PASSWORD'),
$findDatabase->database_name_prefix . $findDatabase->database_name
);

View file

@ -2,8 +2,10 @@
namespace App\Models;
use App\BackupStorage;
use App\Filament\Enums\BackupStatus;
use App\Helpers;
use App\ShellApi;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -33,16 +35,16 @@ class HostingSubscriptionBackup extends Model
'status' => BackupStatus::class,
];
protected static function booted(): void
{
static::addGlobalScope('customer', function (Builder $query) {
if (auth()->check() && auth()->guard()->name == 'web_customer') {
$query->whereHas('hostingSubscription', function ($query) {
$query->where('customer_id', auth()->user()->id);
});
}
});
}
// protected static function booted(): void
// {
// static::addGlobalScope('customer', function (Builder $query) {
// if (auth()->check() && auth()->guard()->name == 'web_customer') {
// $query->whereHas('hostingSubscription', function ($query) {
// $query->where('customer_id', auth()->user()->id);
// });
// }
// });
// }
public static function boot()
{
parent::boot();
@ -57,9 +59,9 @@ class HostingSubscriptionBackup extends Model
});
static::deleting(function ($model) {
if (is_file($model->filepath)) {
shell_exec('rm -rf ' . $model->filepath);
}
// if (is_file($model->filepath)) {
// shell_exec('rm -rf ' . $model->filepath);
// }
});
}
@ -67,6 +69,7 @@ class HostingSubscriptionBackup extends Model
{
$cronJobCommand = 'phyre-php /usr/local/phyre/web/artisan phyre:run-hosting-subscriptions-backup-checks';
$findCronJob = CronJob::where('command', $cronJobCommand)->first();
if (! $findCronJob) {
$cronJob = new CronJob();
$cronJob->schedule = '*/5 * * * *';
@ -93,6 +96,7 @@ class HostingSubscriptionBackup extends Model
$findHostingSubscription = HostingSubscription::select(['id'])
->where('id', $this->hosting_subscription_id)
->first();
if (! $findHostingSubscription) {
$this->delete();
return [
@ -103,16 +107,23 @@ class HostingSubscriptionBackup extends Model
if ($this->status == BackupStatus::Processing) {
$backupDoneFile = $this->path.'/backup-'.$this->id.'.done';
if (file_exists($backupDoneFile)) {
$backupDoneFile = $this->path.'/backup.done';
$backupZipFile = $this->file_path;
$this->size = Helpers::checkPathSize($this->path);
if (file_exists($backupDoneFile) && file_exists($backupZipFile)) {
$this->size = filesize($this->file_path);
$this->status = 'completed';
$this->completed = true;
$this->completed_at = now();
$this->save();
shell_exec('rm -rf ' . $backupDoneFile);
ShellApi::safeDelete($this->path,[
$this->root_path
]);
ShellApi::safeDelete($this->temp_path,[
$this->root_path
]);
return [
'status' => 'completed',
@ -123,7 +134,7 @@ class HostingSubscriptionBackup extends Model
$checkProcess = shell_exec('ps -p ' . $this->process_id . ' | grep ' . $this->process_id);
if (Str::contains($checkProcess, $this->process_id)) {
$this->size = Helpers::checkPathSize($this->path);
$this->size = Helpers::checkPathSize($this->temp_path);
$this->save();
return [
@ -170,32 +181,37 @@ class HostingSubscriptionBackup extends Model
];
}
$storagePath = storage_path('backups');
$backupPath = $storagePath.'/hosting_subscriptions/'.$this->backup_type.'/'.$this->id;
$backupStorageRootPath = '/var/lib/phyre/backups/hosting_subscriptions';
$backupPath = $backupStorageRootPath . '/' . $findHostingSubscription->customer_id;
$backupTempPath = $backupPath.'/temp';
shell_exec('mkdir -p ' . $backupTempPath);
$backupFileName = Str::slug($findHostingSubscription->system_username .'-'. date('Ymd-His')) . '.zip';
$backupFilePath = $backupStorageRootPath.'/'.$backupFileName;
$backupFileName = Str::slug($findHostingSubscription->system_username .'-'. date('Ymd-His')) . '.tar.gz';
$backupFilePath = $backupPath.'/'.$backupFileName;
$backupLogFileName = 'backup.log';
$backupLogFilePath = $backupPath.'/'.$backupLogFileName;
$backupTargetPath = $findMainDomain->domain_root . '/backups';
$backupTargetFilePath = $backupTargetPath.'/'.$backupFileName;
$backupLogFilePath = $backupPath.'/backup.log';
$backupTempScript = '/tmp/backup-script-'.$this->id.'.sh';
$shellFileContent = '';
$shellFileContent .= 'mkdir -p '. $backupTargetPath.PHP_EOL;
$shellFileContent .= 'echo "Backup up domain: '.$findHostingSubscription->domain .'"'. PHP_EOL;
$shellFileContent .= 'echo "Backup up user: '.$findHostingSubscription->system_username .'"'. PHP_EOL;
$shellFileContent .= 'echo "Backup filename: '.$backupFileName.'"' . PHP_EOL;
if ($this->backup_type == 'full') {
$shellFileContent .= 'cp -r /home/' . $findHostingSubscription->system_username . ' ' . $backupTempPath . PHP_EOL;
$shellFileContent .= 'rsync -azP '. $backupTempPath . '/' . $findHostingSubscription->system_username . '/ ' . $backupTempPath . PHP_EOL;
$shellFileContent .= 'rm -rf '. $backupTempPath . '/' . $findHostingSubscription->system_username . '/' . PHP_EOL;
}
if ($this->backup_type == 'full' || $this->backup_type == 'database') {
// Export Phyre Panel database
$mysqlAuthConf = '/root/.phyre-mysql.cnf';
$mysqlAuthContent = '[client]' . PHP_EOL;
$mysqlAuthContent .= 'user="' . env('MYSQL_ROOT_USERNAME') .'"'. PHP_EOL;
$mysqlAuthContent .= 'password="' . env('MYSQL_ROOT_PASSWORD') . '"' . PHP_EOL;
file_put_contents($mysqlAuthConf, $mysqlAuthContent);
$getDatabases = Database::where('hosting_subscription_id', $findHostingSubscription->id)
->where(function ($query) {
$query->where('is_remote_database_server', '0')
@ -205,30 +221,23 @@ class HostingSubscriptionBackup extends Model
if ($getDatabases->count() > 0) {
foreach ($getDatabases as $database) {
// $findDatabaseUser = DatabaseUser::where('database_id', $database->id)
// ->first();
// if (!$findDatabaseUser) {
// continue;
// }
$databaseName = $database->database_name_prefix . $database->database_name;
// $databaseUser = $findDatabaseUser->username_prefix . $findDatabaseUser->username;
// $databaseUserPassword = $findDatabaseUser->password;
$shellFileContent .= 'echo "Backup up database: ' . $databaseName .'" '. PHP_EOL;
// $shellFileContent .= 'echo "Backup up database user: ' . $databaseUser .'" '. PHP_EOL;
$databaseBackupPath = $backupTempPath . '/' . $databaseName . '.sql';
$shellFileContent .= 'mysqldump -u "'.env('MYSQl_ROOT_USERNAME').'" -p"'.env('MYSQL_ROOT_PASSWORD').'" "'.$databaseName.'" > '.$databaseBackupPath . PHP_EOL;
$shellFileContent .= 'mkdir -p '.$backupTempPath . '/databases' . PHP_EOL;
$databaseBackupPath = $backupTempPath . '/databases/' . $databaseName . '.sql';
$shellFileContent .= 'mysqldump --defaults-extra-file='.$mysqlAuthConf.' "'.$databaseName.'" > '.$databaseBackupPath . PHP_EOL;
}
}
}
$shellFileContent .= 'cd '.$backupTempPath .' && tar -czvf '.$backupFilePath.' ./* '. PHP_EOL;
// With find, we can search for all files,directories (including hidden) in the current directory and zip them
$shellFileContent .= 'cd '.$backupTempPath .' && find . -exec zip -r '.$backupFilePath.' {} \;'. PHP_EOL;
$shellFileContent .= 'rm -rf '.$backupTempPath.PHP_EOL;
$shellFileContent .= 'echo "Backup complete"' . PHP_EOL;
$shellFileContent .= 'touch ' . $backupTargetPath. '/backup-'.$this->id.'.done' . PHP_EOL;
$shellFileContent .= 'mv '.$backupFilePath.' '. $backupTargetFilePath.PHP_EOL;
$shellFileContent .= 'touch ' . $backupPath. '/backup.done' . PHP_EOL;
$shellFileContent .= 'rm -rf ' . $backupTempScript . PHP_EOL;
file_put_contents($backupTempScript, $shellFileContent);
@ -238,8 +247,12 @@ class HostingSubscriptionBackup extends Model
if ($processId > 0 && is_numeric($processId)) {
$this->path = $findMainDomain->domain_root . '/backups';
$this->filepath = $backupTargetFilePath;
$this->path = $backupPath;
$this->root_path = $backupStorageRootPath;
$this->temp_path = $backupTempPath;
$this->file_path = $backupFilePath;
$this->file_name = $backupFileName;
$this->status = 'processing';
$this->queued = true;
$this->queued_at = now();

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use App\BackupStorage;
use App\Events\ModelDomainCreated;
use App\Events\ModelDomainDeleting;
use App\Events\ModelHostingSubscriptionCreated;
@ -16,7 +17,9 @@ use App\Models\HostingSubscription;
use App\Policies\CustomerPolicy;
use BladeUI\Icons\Factory;
use Filament\Facades\Filament;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
@ -32,15 +35,6 @@ 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', [
@ -72,16 +66,5 @@ 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',
];
}
}
}
}
}

View file

@ -44,12 +44,6 @@ return [
'throw' => false,
],
'backups' => [
'driver' => 'local',
'root' => storage_path('app/backups'),
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),

View file

@ -16,8 +16,14 @@ return new class extends Migration
$table->string('backup_type')->nullable();
$table->string('status')->nullable();
$table->string('backup_log')->nullable();
$table->string('path')->nullable();
$table->string('filepath')->nullable();
$table->string('root_path')->nullable();
$table->string('temp_path')->nullable();
$table->string('file_path')->nullable();
$table->string('file_name')->nullable();
$table->string('size')->nullable();
$table->string('disk')->nullable();
$table->string('process_id')->nullable();

View file

@ -17,8 +17,14 @@ return new class extends Migration
$table->bigInteger('hosting_subscription_id')->nullable();
$table->string('backup_type')->nullable();
$table->string('status')->nullable();
$table->string('backup_log')->nullable();
$table->string('path')->nullable();
$table->string('filepath')->nullable();
$table->string('root_path')->nullable();
$table->string('temp_path')->nullable();
$table->string('file_path')->nullable();
$table->string('file_name')->nullable();
$table->string('size')->nullable();
$table->string('disk')->nullable();
$table->string('process_id')->nullable();

View file

@ -28,20 +28,25 @@ class BackupTest extends ActionTestCase
$this->assertNotEmpty($findLastBackup->created_at);
$this->assertSame($findLastBackup->backup_type, 'full');
$backupFinished = false;
for ($i = 0; $i < 100; $i++) {
$findLastBackup = Backup::orderBy('id', 'desc')->first();
$findLastBackup->checkBackup();
Artisan::call('phyre:run-backup-checks');
$findLastBackup = Backup::where('id', $findLastBackup->id)->first();
if ($findLastBackup->status == BackupStatus::Completed) {
$backupFinished = true;
break;
}
sleep(1);
}
$this->assertTrue($backupFinished);
$this->assertSame($findLastBackup->status, BackupStatus::Completed);
$this->assertNotEmpty($findLastBackup->filepath);
$this->assertTrue(file_exists(Storage::disk('backups')->path($findLastBackup->filepath)));
$this->assertNotEmpty($findLastBackup->file_path);
$this->assertTrue(file_exists($findLastBackup->file_path));
$backup = new Backup();
$checkCronJob = $backup->checkCronJob();
@ -81,24 +86,36 @@ class BackupTest extends ActionTestCase
$findBackup = false;
$backupCompleted = false;
for ($i = 0; $i < 100; $i++) {
Artisan::call('phyre:run-backup-checks');
$findBackup = Backup::where('id', $backupId)->first();
$findBackup->checkBackup();
if ($findBackup->status == BackupStatus::Completed) {
$backupCompleted = true;
break;
}
if ($findBackup->status == BackupStatus::Failed) {
$this->fail('Backup failed: '.$findBackup->backup_log);
break;
}
sleep(1);
}
if (!$backupCompleted) {
$findBackup = Backup::where('id', $backupId)->first();
$this->fail('Backup not completed: '.$findBackup->backup_log);
}
$this->assertTrue($backupCompleted);
$this->assertNotEmpty($findBackup->filepath);
$this->assertTrue(file_exists(Storage::disk('backups')->path($findBackup->filepath)));
$this->assertNotEmpty($findBackup->file_path);
$this->assertTrue(file_exists($findBackup->file_path));
$getFilesize = filesize(Storage::disk('backups')->path($findBackup->filepath));
$getFilesize = filesize($findBackup->file_path);
$this->assertGreaterThan(0, $getFilesize);
$this->assertSame($getFilesize, $findBackup->size);
$this->assertSame($getFilesize, intval($findBackup->size));
Helpers::extractTar(Storage::disk('backups')->path($findBackup->filepath), $findBackup->path . '/unit-test');
Helpers::extractZip($findBackup->file_path, $findBackup->path . '/unit-test');
}

View file

@ -28,7 +28,7 @@ class HostingSubscriptionBackupTest extends ActionTestCase
$chs = $this->_createHostingSubscription();
Artisan::call('phyre:run-hosting-subscriptions-backup');
Artisan::call('phyre:create-daily-full-hosting-subscriptions-backup');
$findLastBackup = HostingSubscriptionBackup::where('hosting_subscription_id', $chs['hostingSubscriptionId'])
->first();
@ -39,20 +39,23 @@ class HostingSubscriptionBackupTest extends ActionTestCase
$this->assertSame($findLastBackup->backup_type, 'full');
$backupFinished = false;
for ($i = 0; $i < 50; $i++) {
for ($i = 0; $i < 100; $i++) {
Artisan::call('phyre:run-hosting-subscriptions-backup-checks');
$findLastBackup = HostingSubscriptionBackup::where('id', $findLastBackup->id)->first();
$findLastBackup->checkBackup();
if ($findLastBackup->status == BackupStatus::Completed) {
$backupFinished = true;
break;
}
sleep(1);
}
$this->assertTrue($backupFinished);
$this->assertSame($findLastBackup->status, BackupStatus::Completed);
$this->assertNotEmpty($findLastBackup->filepath);
$this->assertTrue(file_exists($findLastBackup->filepath));
$this->assertNotEmpty($findLastBackup->file_path);
$this->assertTrue(file_exists($findLastBackup->file_path));
$backup = new HostingSubscriptionBackup();
$checkCronJob = $backup->checkCronJob();
@ -69,10 +72,12 @@ class HostingSubscriptionBackupTest extends ActionTestCase
$findBackup = false;
$backupCompleted = false;
for ($i = 0; $i < 50; $i++) {
for ($i = 0; $i < 100; $i++) {
Artisan::call('phyre:run-hosting-subscriptions-backup-checks');
$findBackup = HostingSubscriptionBackup::where('id', $backupId)->first();
if ($findBackup) {
$status = $findBackup->checkBackup();
if ($findBackup->status == BackupStatus::Completed) {
$backupCompleted = true;
break;
@ -82,19 +87,19 @@ class HostingSubscriptionBackupTest extends ActionTestCase
}
$this->assertTrue($backupCompleted);
$this->assertNotEmpty($findBackup->filepath);
$this->assertTrue(file_exists($findBackup->filepath));
$this->assertNotEmpty($findBackup->file_path);
$this->assertTrue(file_exists($findBackup->file_path));
$getFilesize = filesize($findBackup->filepath);
$getFilesize = filesize($findBackup->file_path);
$this->assertGreaterThan(0, $getFilesize);
$this->assertSame(Helpers::checkPathSize($findBackup->path), $findBackup->size);
$this->assertSame($getFilesize, intval($findBackup->size));
Helpers::extractTar($findBackup->filepath, $findBackup->path . '/unit-test');
Helpers::extractZip($findBackup->file_path, $findBackup->path . '/unit-test');
//
// dd($chs);
$findDatabase = Database::where('id', $chs['databaseId'])->first();
$extractedDatabase = $findBackup->path . '/unit-test/' . $findDatabase->database_name_prefix . $findDatabase->database_name . '.sql';
$extractedDatabase = $findBackup->path . '/unit-test/databases/' . $findDatabase->database_name_prefix . $findDatabase->database_name . '.sql';
$this->assertTrue(file_exists($extractedDatabase));
$extractedDatabaseContent = file_get_contents($extractedDatabase);
$this->assertNotEmpty($extractedDatabaseContent);