mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 02:16:43 +00:00
Merge remote-tracking branch 'origin/next' into improve-s3-storage-handling
This commit is contained in:
commit
20b5b90cf9
49 files changed
+1979
-257
No files matched your search
@@ -3,10 +3,12 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -18,7 +20,7 @@ class BackupEdit extends Component
|
||||
public ScheduledDatabaseBackup $backup;
|
||||
|
||||
#[Locked]
|
||||
public $s3s;
|
||||
public $availableS3Storages;
|
||||
|
||||
#[Locked]
|
||||
public $parameters;
|
||||
@@ -69,7 +71,7 @@ class BackupEdit extends Component
|
||||
public bool $disableLocalBackup = false;
|
||||
|
||||
#[Validate(['nullable', 'integer'])]
|
||||
public ?int $s3StorageId = 1;
|
||||
public ?int $s3StorageId = null;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $databasesToBackup = null;
|
||||
@@ -129,7 +131,7 @@ class BackupEdit extends Component
|
||||
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
|
||||
$this->saveS3 = $this->backup->save_s3;
|
||||
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
|
||||
$this->s3StorageId = $this->backup->s3_storage_id;
|
||||
$this->s3StorageId = $this->backup->s3_storage_id ?? $this->availableS3StorageIds()->first();
|
||||
$this->databasesToBackup = $this->backup->databases_to_backup;
|
||||
$this->dumpAll = $this->backup->dump_all;
|
||||
$this->timeout = $this->backup->timeout;
|
||||
@@ -215,6 +217,11 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedS3StorageId(): void
|
||||
{
|
||||
$this->instantSave();
|
||||
}
|
||||
|
||||
private function customValidate()
|
||||
{
|
||||
if (! is_numeric($this->backup->s3_storage_id)) {
|
||||
@@ -222,10 +229,14 @@ class BackupEdit extends Component
|
||||
}
|
||||
|
||||
// S3 backup cannot be enabled without a valid S3 storage owned by the team
|
||||
$availableS3Ids = collect($this->s3s)->pluck('id');
|
||||
if ($this->backup->save_s3 && ! $availableS3Ids->contains($this->backup->s3_storage_id)) {
|
||||
$this->backup->save_s3 = $this->saveS3 = false;
|
||||
$availableS3Ids = $this->availableS3StorageIds();
|
||||
if ($availableS3Ids->isEmpty()) {
|
||||
$this->backup->s3_storage_id = $this->s3StorageId = null;
|
||||
if ($this->backup->save_s3) {
|
||||
$this->backup->save_s3 = $this->saveS3 = false;
|
||||
}
|
||||
} elseif (! $availableS3Ids->contains($this->backup->s3_storage_id)) {
|
||||
$this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first();
|
||||
}
|
||||
|
||||
// Validate that disable_local_backup can only be true when S3 backup is enabled
|
||||
@@ -240,6 +251,28 @@ class BackupEdit extends Component
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
private function availableS3StorageIds(): Collection
|
||||
{
|
||||
$storages = collect($this->availableS3Storages);
|
||||
$storageIds = $storages->pluck('id')->filter()->all();
|
||||
|
||||
if (empty($storageIds)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$teamIds = $storages->pluck('team_id')->reject(fn ($teamId) => $teamId === null)->unique()->values()->all();
|
||||
|
||||
if (empty($teamIds)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return S3Storage::query()
|
||||
->whereKey($storageIds)
|
||||
->whereIn('team_id', $teamIds)
|
||||
->where('is_usable', true)
|
||||
->pluck('id');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -94,6 +94,10 @@ class FileStorage extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if ($this->fileStorage->is_host_file) {
|
||||
throw new \Exception('Host file mounts are bind-only and cannot be converted.');
|
||||
}
|
||||
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = true;
|
||||
$this->fileStorage->content = null;
|
||||
@@ -112,6 +116,10 @@ class FileStorage extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if ($this->fileStorage->is_host_file) {
|
||||
throw new \Exception('Host file mounts are bind-only and cannot be loaded from the server.');
|
||||
}
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'File storage loaded from server.');
|
||||
@@ -127,6 +135,10 @@ class FileStorage extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if ($this->fileStorage->is_host_file) {
|
||||
throw new \Exception('Host file mounts are bind-only and cannot be converted.');
|
||||
}
|
||||
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = false;
|
||||
$this->fileStorage->content = null;
|
||||
@@ -154,8 +166,10 @@ class FileStorage extends Component
|
||||
$message = 'File deleted.';
|
||||
if ($this->fileStorage->is_directory) {
|
||||
$message = 'Directory deleted.';
|
||||
} elseif ($this->fileStorage->is_host_file) {
|
||||
$message = 'Host file mount removed.';
|
||||
}
|
||||
if ($this->permanently_delete) {
|
||||
if ($this->permanently_delete && ! $this->fileStorage->is_host_file) {
|
||||
$message = 'Directory deleted from the server.';
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
}
|
||||
@@ -174,6 +188,12 @@ class FileStorage extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if ($this->fileStorage->is_host_file) {
|
||||
$this->dispatch('error', 'Host file mounts are bind-only and cannot be edited from the UI.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->fileStorage->is_too_large) {
|
||||
$this->dispatch('error', 'File on server is too large to edit from the UI.');
|
||||
|
||||
@@ -205,6 +225,12 @@ class FileStorage extends Component
|
||||
public function instantSave(): void
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
if ($this->fileStorage->is_host_file) {
|
||||
$this->dispatch('error', 'Host file mounts are bind-only and cannot be edited from the UI.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->fileStorage->is_too_large) {
|
||||
$this->dispatch('error', 'File on server is too large to edit from the UI.');
|
||||
|
||||
@@ -223,6 +249,9 @@ class FileStorage extends Component
|
||||
'fileDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
|
||||
],
|
||||
'hostFileDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'Only the mount configuration will be removed. The host file will not be deleted.'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ class Storage extends Component
|
||||
|
||||
public ?string $file_storage_content = null;
|
||||
|
||||
public string $host_file_storage_source = '';
|
||||
|
||||
public string $host_file_storage_destination = '';
|
||||
|
||||
public string $file_storage_directory_source = '';
|
||||
|
||||
public string $file_storage_directory_destination = '';
|
||||
@@ -146,19 +150,9 @@ class Storage extends Component
|
||||
'file_storage_content' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$this->file_storage_path = trim($this->file_storage_path);
|
||||
$this->file_storage_path = str($this->file_storage_path)->start('/')->value();
|
||||
$this->file_storage_path = validateFileMountPath($this->file_storage_path, 'file storage path');
|
||||
|
||||
// Validate path to prevent command injection
|
||||
validateShellSafePath($this->file_storage_path, 'file storage path');
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
|
||||
} elseif (str($this->resource->getMorphClass())->contains('Standalone')) {
|
||||
$fs_path = database_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
|
||||
} else {
|
||||
throw new \Exception('No valid resource type for file mount storage type!');
|
||||
}
|
||||
$fs_path = confineFileMountPath($this->fileStorageHostPath(), $this->file_storage_path, 'file storage path');
|
||||
|
||||
LocalFileVolume::create([
|
||||
'fs_path' => $fs_path,
|
||||
@@ -178,6 +172,38 @@ class Storage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function submitHostFileStorage()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->validate([
|
||||
'host_file_storage_source' => 'required|string',
|
||||
'host_file_storage_destination' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->host_file_storage_source = validateHostFileMountPath($this->host_file_storage_source, 'host file source path');
|
||||
$this->host_file_storage_destination = validateFileMountPath($this->host_file_storage_destination, 'host file destination path');
|
||||
|
||||
LocalFileVolume::create([
|
||||
'fs_path' => $this->host_file_storage_source,
|
||||
'mount_path' => $this->host_file_storage_destination,
|
||||
'content' => null,
|
||||
'is_directory' => false,
|
||||
'is_host_file' => true,
|
||||
'resource_id' => $this->resource->id,
|
||||
'resource_type' => get_class($this->resource),
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Host file mount added successfully');
|
||||
$this->dispatch('closeStorageModal', 'host-file');
|
||||
$this->clearForm();
|
||||
$this->refreshStorages();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submitFileStorageDirectory()
|
||||
{
|
||||
try {
|
||||
@@ -222,6 +248,8 @@ class Storage extends Component
|
||||
$this->file_storage_path = '';
|
||||
$this->file_storage_content = null;
|
||||
$this->file_storage_directory_destination = '';
|
||||
$this->host_file_storage_source = '';
|
||||
$this->host_file_storage_destination = '';
|
||||
|
||||
if (str($this->resource->getMorphClass())->contains('Standalone')) {
|
||||
$this->file_storage_directory_source = database_configuration_dir()."/{$this->resource->uuid}";
|
||||
@@ -230,6 +258,34 @@ class Storage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function fileStorageHostPath(): string
|
||||
{
|
||||
if (method_exists($this->resource, 'workdir')) {
|
||||
return $this->resource->workdir();
|
||||
}
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
return application_configuration_dir().'/'.$this->resource->uuid;
|
||||
}
|
||||
|
||||
if (str($this->resource->getMorphClass())->contains('Standalone')) {
|
||||
return database_configuration_dir().'/'.$this->resource->uuid;
|
||||
}
|
||||
|
||||
throw new \Exception('No valid resource type for file mount storage type!');
|
||||
}
|
||||
|
||||
public function fileStoragePreviewPath(): string
|
||||
{
|
||||
$path = str($this->file_storage_path)->trim();
|
||||
|
||||
if ($path->isEmpty()) {
|
||||
return $this->fileStorageHostPath().'/';
|
||||
}
|
||||
|
||||
return $this->fileStorageHostPath().$path->start('/')->value();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.storage');
|
||||
|
||||
Reference in new issue
Block a user