Add polymorphic volume backup scheduling for persistent volumes and directories, expose schedule management via API, and reorganize backup configuration and execution views.
298 lines
10 KiB
PHP
298 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Service;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\LocalFileVolume;
|
|
use App\Models\LocalPersistentVolume;
|
|
use App\Support\ValidationPatterns;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Livewire\Component;
|
|
|
|
class Storage extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public $resource;
|
|
|
|
public $fileStorage;
|
|
|
|
public $isSwarm = false;
|
|
|
|
public string $name = '';
|
|
|
|
public string $mount_path = '';
|
|
|
|
public ?string $host_path = null;
|
|
|
|
public string $file_storage_path = '';
|
|
|
|
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 = '';
|
|
|
|
public function getListeners()
|
|
{
|
|
$teamId = auth()->user()->currentTeam()->id;
|
|
|
|
return [
|
|
"echo-private:team.{$teamId},FileStorageChanged" => 'refreshStoragesFromEvent',
|
|
'refreshStorages',
|
|
'addNewVolume',
|
|
];
|
|
}
|
|
|
|
public function mount()
|
|
{
|
|
if (str($this->resource->getMorphClass())->contains('Standalone')) {
|
|
$this->file_storage_directory_source = database_configuration_dir()."/{$this->resource->uuid}";
|
|
} else {
|
|
$this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}";
|
|
}
|
|
|
|
if ($this->resource->getMorphClass() === Application::class) {
|
|
if ($this->resource->destination->server->isSwarm()) {
|
|
$this->isSwarm = true;
|
|
}
|
|
}
|
|
|
|
$this->refreshStorages();
|
|
}
|
|
|
|
public function refreshStoragesFromEvent()
|
|
{
|
|
$this->refreshStorages();
|
|
$this->dispatch('warning', 'File storage changed. Usually it means that the file / directory is already defined on the server, so Coolify set it up for you properly on the UI.');
|
|
}
|
|
|
|
public function refreshStorages()
|
|
{
|
|
$this->fileStorage = $this->resource->fileStorages()->get()->each(function (LocalFileVolume $fs) {
|
|
if (strlen((string) $fs->content) > LocalFileVolume::MAX_CONTENT_SIZE) {
|
|
$fs->content = LocalFileVolume::TOO_LARGE_PLACEHOLDER;
|
|
}
|
|
});
|
|
$this->resource->load('persistentStorages.resource');
|
|
}
|
|
|
|
public function getFilesProperty()
|
|
{
|
|
return $this->fileStorage->where('is_directory', false);
|
|
}
|
|
|
|
public function getDirectoriesProperty()
|
|
{
|
|
return $this->fileStorage->where('is_directory', true);
|
|
}
|
|
|
|
public function getVolumeCountProperty()
|
|
{
|
|
return $this->resource->persistentStorages()->count();
|
|
}
|
|
|
|
public function getFileCountProperty()
|
|
{
|
|
return $this->files->count();
|
|
}
|
|
|
|
public function getDirectoryCountProperty()
|
|
{
|
|
return $this->directories->count();
|
|
}
|
|
|
|
public function submitPersistentVolume()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->resource);
|
|
|
|
$this->validate([
|
|
'name' => ValidationPatterns::volumeNameRules(),
|
|
'mount_path' => 'required|string',
|
|
'host_path' => $this->isSwarm
|
|
? ['required', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN]
|
|
: ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
|
], array_merge(ValidationPatterns::volumeNameMessages(), [
|
|
'host_path.regex' => 'Host path must start with / and only contain safe path characters.',
|
|
]));
|
|
|
|
$name = $this->resource->uuid.'-'.$this->name;
|
|
|
|
LocalPersistentVolume::create([
|
|
'name' => $name,
|
|
'mount_path' => $this->mount_path,
|
|
'host_path' => $this->host_path,
|
|
'resource_id' => $this->resource->id,
|
|
'resource_type' => $this->resource->getMorphClass(),
|
|
]);
|
|
$this->resource->refresh();
|
|
$this->dispatch('configurationChanged');
|
|
$this->dispatch('success', 'Volume added successfully');
|
|
$this->dispatch('closeStorageModal', 'volume');
|
|
$this->clearForm();
|
|
$this->refreshStorages();
|
|
$this->dispatch('refreshStorages');
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function submitFileStorage()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->resource);
|
|
|
|
$this->validate([
|
|
'file_storage_path' => 'required|string',
|
|
'file_storage_content' => 'nullable|string',
|
|
]);
|
|
|
|
$this->file_storage_path = validateFileMountPath($this->file_storage_path, 'file storage path');
|
|
|
|
$fs_path = confineFileMountPath($this->fileStorageHostPath(), $this->file_storage_path, 'file storage path');
|
|
|
|
LocalFileVolume::create([
|
|
'fs_path' => $fs_path,
|
|
'mount_path' => $this->file_storage_path,
|
|
'content' => $this->file_storage_content,
|
|
'is_directory' => false,
|
|
'resource_id' => $this->resource->id,
|
|
'resource_type' => get_class($this->resource),
|
|
]);
|
|
|
|
$this->dispatch('configurationChanged');
|
|
$this->dispatch('success', 'File mount added successfully');
|
|
$this->dispatch('closeStorageModal', 'file');
|
|
$this->clearForm();
|
|
$this->refreshStorages();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
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('configurationChanged');
|
|
$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 {
|
|
$this->authorize('update', $this->resource);
|
|
|
|
$this->validate([
|
|
'file_storage_directory_source' => 'required|string',
|
|
'file_storage_directory_destination' => 'required|string',
|
|
]);
|
|
|
|
$this->file_storage_directory_source = trim($this->file_storage_directory_source);
|
|
$this->file_storage_directory_source = str($this->file_storage_directory_source)->start('/')->value();
|
|
$this->file_storage_directory_destination = trim($this->file_storage_directory_destination);
|
|
$this->file_storage_directory_destination = str($this->file_storage_directory_destination)->start('/')->value();
|
|
|
|
// Validate paths to prevent command injection
|
|
validateShellSafePath($this->file_storage_directory_source, 'storage source path');
|
|
validateShellSafePath($this->file_storage_directory_destination, 'storage destination path');
|
|
|
|
LocalFileVolume::create([
|
|
'fs_path' => $this->file_storage_directory_source,
|
|
'mount_path' => $this->file_storage_directory_destination,
|
|
'is_directory' => true,
|
|
'resource_id' => $this->resource->id,
|
|
'resource_type' => get_class($this->resource),
|
|
]);
|
|
|
|
$this->dispatch('configurationChanged');
|
|
$this->dispatch('success', 'Directory mount added successfully');
|
|
$this->dispatch('closeStorageModal', 'directory');
|
|
$this->clearForm();
|
|
$this->refreshStorages();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function clearForm()
|
|
{
|
|
$this->name = '';
|
|
$this->mount_path = '';
|
|
$this->host_path = null;
|
|
$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}";
|
|
} else {
|
|
$this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}";
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|