2023-10-04 07:58:39 +00:00
< ? php
2023-12-07 18:06:32 +00:00
namespace App\Livewire\Project\Service ;
2023-10-04 07:58:39 +00:00
2026-08-28 07:26:57 +00:00
use App\Livewire\Project\Shared\Storages\All as StorageList ;
2026-03-25 22:44:37 +00:00
use App\Models\Application ;
use App\Models\LocalFileVolume ;
2023-10-04 07:58:39 +00:00
use App\Models\LocalPersistentVolume ;
2026-03-26 10:06:30 +00:00
use App\Support\ValidationPatterns ;
2025-08-26 08:27:31 +00:00
use Illuminate\Foundation\Auth\Access\AuthorizesRequests ;
2023-10-04 07:58:39 +00:00
use Livewire\Component ;
class Storage extends Component
{
2025-08-26 08:27:31 +00:00
use AuthorizesRequests ;
2023-10-04 07:58:39 +00:00
public $resource ;
2024-06-10 20:43:34 +00:00
2024-08-05 18:00:57 +00:00
public $fileStorage ;
2025-10-01 16:46:21 +00:00
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 ;
2026-07-02 12:37:39 +00:00
public string $host_file_storage_source = '' ;
public string $host_file_storage_destination = '' ;
2025-10-01 16:46:21 +00:00
public string $file_storage_directory_source = '' ;
public string $file_storage_directory_destination = '' ;
2026-08-05 11:50:11 +00:00
public string $activeTab = 'volumes' ;
public int $cachedVolumeCount = 0 ;
public int $cachedFileCount = 0 ;
public int $cachedDirectoryCount = 0 ;
2024-04-15 17:47:17 +00:00
public function getListeners ()
2023-10-04 07:58:39 +00:00
{
2024-08-05 18:00:57 +00:00
$teamId = auth () -> user () -> currentTeam () -> id ;
2024-04-15 17:47:17 +00:00
return [
2024-08-05 18:00:57 +00:00
" echo-private:team. { $teamId } ,FileStorageChanged " => 'refreshStoragesFromEvent' ,
2026-08-28 07:26:57 +00:00
'storageCountsChanged' => 'refreshStorages' ,
2024-04-15 17:47:17 +00:00
'addNewVolume' ,
];
2023-10-04 07:58:39 +00:00
}
2024-06-10 20:43:34 +00:00
2024-08-05 18:00:57 +00:00
public function mount ()
{
2025-10-01 16:46:21 +00:00
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 } " ;
}
2026-03-25 22:44:37 +00:00
if ( $this -> resource -> getMorphClass () === Application :: class ) {
2026-08-05 11:50:11 +00:00
$this -> resource -> loadMissing ( 'destination.server' , 'environment.project' );
if ( $this -> resource -> destination ? -> server ? -> isSwarm ()) {
2025-10-01 16:46:21 +00:00
$this -> isSwarm = true ;
}
}
2026-08-05 11:50:11 +00:00
// Counts only on mount — child All (volumes) / file list load their own payloads.
$this -> loadVolumeCount ();
$this -> loadFileStorageMetaCounts ();
$this -> activeTab = $this -> resolveDefaultTab ();
$this -> fileStorage = collect ();
$this -> loadFileStorageForActiveTab ();
2024-08-05 18:00:57 +00:00
}
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 ()
{
2026-08-28 07:26:57 +00:00
$hadVolumes = $this -> cachedVolumeCount > 0 ;
2026-08-05 11:50:11 +00:00
// Avoid loading full volume models onto this parent (child All owns that snapshot).
$this -> resource -> unsetRelation ( 'persistentStorages' );
$this -> loadVolumeCount ();
$this -> loadFileStorageMetaCounts ();
$this -> loadFileStorageForActiveTab ();
2026-08-28 07:26:57 +00:00
if ( $this -> activeTab === 'volumes' && $hadVolumes && $this -> cachedVolumeCount > 0 ) {
$this -> dispatch ( 'refreshVolumeList' ) -> to ( StorageList :: class );
}
2026-08-05 11:50:11 +00:00
}
public function setActiveTab ( string $tab ) : void
{
if ( ! in_array ( $tab , [ 'volumes' , 'files' , 'directories' ], true )) {
return ;
}
$this -> activeTab = $tab ;
$this -> loadFileStorageForActiveTab ();
}
private function resolveDefaultTab () : string
{
if ( $this -> volumeCount > 0 ) {
return 'volumes' ;
}
if ( $this -> fileCount > 0 ) {
return 'files' ;
}
if ( $this -> directoryCount > 0 ) {
return 'directories' ;
}
return 'volumes' ;
}
private function loadVolumeCount () : void
{
$this -> cachedVolumeCount = $this -> resource -> persistentStorages () -> count ();
}
/**
* Counts only — avoids loading file contents into the Livewire snapshot on the volumes tab .
*/
private function loadFileStorageMetaCounts () : void
{
$this -> cachedFileCount = $this -> resource -> fileStorages () -> where ( 'is_directory' , false ) -> count ();
$this -> cachedDirectoryCount = $this -> resource -> fileStorages () -> where ( 'is_directory' , true ) -> count ();
}
/**
* Load full file / directory mounts only for the active tab ( content only on files ) .
*/
private function loadFileStorageForActiveTab () : void
{
if ( $this -> activeTab === 'volumes' ) {
// Keep snapshot small while the volumes tab is shown.
$this -> fileStorage = collect ();
return ;
}
$query = $this -> resource -> fileStorages ();
if ( $this -> activeTab === 'files' ) {
$query -> where ( 'is_directory' , false );
} else {
$query -> where ( 'is_directory' , true );
}
$this -> fileStorage = $query -> get () -> each ( function ( LocalFileVolume $fs ) : void {
if ( $this -> activeTab !== 'files' ) {
$fs -> content = null ;
return ;
}
2026-04-28 20:36:56 +00:00
if ( strlen (( string ) $fs -> content ) > LocalFileVolume :: MAX_CONTENT_SIZE ) {
$fs -> content = LocalFileVolume :: TOO_LARGE_PLACEHOLDER ;
}
});
2025-10-01 06:23:21 +00:00
}
public function getFilesProperty ()
{
2026-08-05 11:50:11 +00:00
return collect ( $this -> fileStorage ) -> where ( 'is_directory' , false );
2025-10-01 06:23:21 +00:00
}
public function getDirectoriesProperty ()
{
2026-08-05 11:50:11 +00:00
return collect ( $this -> fileStorage ) -> where ( 'is_directory' , true );
2025-10-01 06:23:21 +00:00
}
public function getVolumeCountProperty ()
{
2026-08-05 11:50:11 +00:00
return $this -> cachedVolumeCount ;
2025-10-01 06:23:21 +00:00
}
public function getFileCountProperty ()
{
2026-08-05 11:50:11 +00:00
return $this -> cachedFileCount ;
2025-10-01 06:23:21 +00:00
}
public function getDirectoryCountProperty ()
{
2026-08-05 11:50:11 +00:00
return $this -> cachedDirectoryCount ;
2024-08-05 18:00:57 +00:00
}
2025-10-01 16:46:21 +00:00
public function submitPersistentVolume ()
2023-10-04 07:58:39 +00:00
{
try {
2025-08-26 08:27:31 +00:00
$this -> authorize ( 'update' , $this -> resource );
2025-10-01 16:46:21 +00:00
$this -> validate ([
2026-03-26 10:06:30 +00:00
'name' => ValidationPatterns :: volumeNameRules (),
2025-10-01 16:46:21 +00:00
'mount_path' => 'required|string' ,
2026-08-21 10:12:56 +00:00
'host_path' => $this -> isSwarm
? [ 'required' , 'string' , 'regex:' . ValidationPatterns :: DIRECTORY_PATH_PATTERN ]
: [ 'nullable' , 'string' , 'regex:' . ValidationPatterns :: DIRECTORY_PATH_PATTERN ],
2026-04-20 09:27:10 +00:00
], array_merge ( ValidationPatterns :: volumeNameMessages (), [
'host_path.regex' => 'Host path must start with / and only contain safe path characters.' ,
]));
2025-10-01 16:46:21 +00:00
$name = $this -> resource -> uuid . '-' . $this -> name ;
2023-10-04 07:58:39 +00:00
LocalPersistentVolume :: create ([
2025-10-01 16:46:21 +00:00
'name' => $name ,
'mount_path' => $this -> mount_path ,
'host_path' => $this -> host_path ,
2023-10-04 07:58:39 +00:00
'resource_id' => $this -> resource -> id ,
'resource_type' => $this -> resource -> getMorphClass (),
]);
2026-08-05 11:50:11 +00:00
$this -> clearForm ();
$this -> activeTab = 'volumes' ;
$this -> refreshStorages ();
2026-07-15 15:34:22 +00:00
$this -> dispatch ( 'configurationChanged' );
2025-10-01 16:46:21 +00:00
$this -> dispatch ( 'success' , 'Volume added successfully' );
$this -> dispatch ( 'closeStorageModal' , 'volume' );
} 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' ,
]);
2026-07-02 12:37:39 +00:00
$this -> file_storage_path = validateFileMountPath ( $this -> file_storage_path , 'file storage path' );
2026-03-25 22:44:37 +00:00
2026-07-02 12:37:39 +00:00
$fs_path = confineFileMountPath ( $this -> fileStorageHostPath (), $this -> file_storage_path , 'file storage path' );
2025-10-01 16:46:21 +00:00
2026-03-25 22:44:37 +00:00
LocalFileVolume :: create ([
2025-10-01 16:46:21 +00:00
'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 ),
]);
2026-08-05 11:50:11 +00:00
$this -> clearForm ();
$this -> activeTab = 'files' ;
$this -> refreshStorages ();
2026-07-15 15:34:22 +00:00
$this -> dispatch ( 'configurationChanged' );
2025-10-01 16:46:21 +00:00
$this -> dispatch ( 'success' , 'File mount added successfully' );
$this -> dispatch ( 'closeStorageModal' , 'file' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
2026-07-02 12:37:39 +00:00
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 ),
]);
2026-08-05 11:50:11 +00:00
$this -> clearForm ();
$this -> activeTab = 'files' ;
$this -> refreshStorages ();
2026-07-15 15:34:22 +00:00
$this -> dispatch ( 'configurationChanged' );
2026-07-02 12:37:39 +00:00
$this -> dispatch ( 'success' , 'Host file mount added successfully' );
$this -> dispatch ( 'closeStorageModal' , 'host-file' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
2025-10-01 16:46:21 +00:00
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 ();
2025-11-27 13:36:31 +00:00
// Validate paths to prevent command injection
validateShellSafePath ( $this -> file_storage_directory_source , 'storage source path' );
validateShellSafePath ( $this -> file_storage_directory_destination , 'storage destination path' );
2026-03-25 22:44:37 +00:00
LocalFileVolume :: create ([
2025-10-01 16:46:21 +00:00
'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 ),
]);
2026-08-05 11:50:11 +00:00
$this -> clearForm ();
$this -> activeTab = 'directories' ;
$this -> refreshStorages ();
2026-07-15 15:34:22 +00:00
$this -> dispatch ( 'configurationChanged' );
2025-10-01 16:46:21 +00:00
$this -> dispatch ( 'success' , 'Directory mount added successfully' );
$this -> dispatch ( 'closeStorageModal' , 'directory' );
2023-10-04 07:58:39 +00:00
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
2024-06-10 20:43:34 +00:00
2025-10-01 16:46:21 +00:00
public function clearForm ()
{
2026-08-21 10:12:56 +00:00
$this -> name = '' ;
2025-10-01 16:46:21 +00:00
$this -> mount_path = '' ;
$this -> host_path = null ;
$this -> file_storage_path = '' ;
$this -> file_storage_content = null ;
$this -> file_storage_directory_destination = '' ;
2026-07-02 12:37:39 +00:00
$this -> host_file_storage_source = '' ;
$this -> host_file_storage_destination = '' ;
2025-10-01 16:46:21 +00:00
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 } " ;
}
}
2026-07-02 12:37:39 +00:00
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 ();
}
2024-04-15 17:47:17 +00:00
public function render ()
{
return view ( 'livewire.project.service.storage' );
}
2023-10-04 07:58:39 +00:00
}