diff --git a/web/app/Livewire/FileManager.php b/web/app/Livewire/FileManager.php index 3eb707c..0089ce1 100644 --- a/web/app/Livewire/FileManager.php +++ b/web/app/Livewire/FileManager.php @@ -2,141 +2,136 @@ namespace App\Livewire; -use App\Helpers; -use App\Models\Domain; -use App\Models\HostingSubscription; -use Illuminate\Support\Str; +use App\Models\FileItem; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\TextInput; +use Filament\Pages\Page; +use Filament\Tables\Actions\Action; +use Filament\Tables\Actions\BulkAction; +use Filament\Tables\Actions\DeleteAction; +use Filament\Tables\Actions\ViewAction; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Concerns\InteractsWithTable; +use Filament\Tables\Contracts\HasTable; +use Filament\Tables\Table; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Number; +use Livewire\Attributes\Url; use Livewire\Component; -class FileManager extends Component +class FileManager extends Page implements HasTable { - public $hostingSubscriptionId; + use InteractsWithTable; - public $hostingSubscriptionSystemUsername; + protected static ?string $navigationIcon = 'heroicon-o-folder-open'; - public $domainHomeRoot; + protected static string $view = 'filament.pages.file-manager'; - public $currentRealPath; + protected string $disk = 'local'; - public $currentPath; + #[Url(except: '')] + public string $path = ''; - public $folderName; + protected $listeners = ['updatePath' => '$refresh']; - public $canIBack = false; - - public function mount($hostingSubscriptionId) + public function table(Table $table): Table { - $this->hostingSubscriptionId = $hostingSubscriptionId; + return $table + ->heading($this->path ?: 'Root') + ->query( + FileItem::queryForDiskAndPath($this->disk, $this->path) + ) + ->paginated(false) + ->columns([ + TextColumn::make('name') + ->icon(fn ($record): string => match ($record->type) { + 'Folder' => 'heroicon-o-folder', + default => 'heroicon-o-document' + }) + ->iconColor(fn ($record): string => match ($record->type) { + 'Folder' => 'warning', + default => 'gray', + }) + ->action(function (FileItem $record) { + if ($record->isFolder()) { + $this->path = $record->path; - $findHostingSubscription = HostingSubscription::where('id', $this->hostingSubscriptionId)->first(); - $findDomain = Domain::where('hosting_subscription_id', $this->hostingSubscriptionId) - ->where('is_main', 1) - ->first(); + $this->dispatch('updatePath'); + } + }), + TextColumn::make('dateModified') + ->dateTime(), + TextColumn::make('size') + ->formatStateUsing(fn ($state) => $state ? Number::fileSize($state) : ''), + TextColumn::make('type'), + ]) + ->actions([ + ViewAction::make('open') + ->label('Open') + ->hidden(fn (FileItem $record): bool => ! $record->canOpen()) + ->url(fn (FileItem $record): string => Storage::disk($this->disk)->url($record->path)) + ->openUrlInNewTab(), + Action::make('download') + ->label('Download') + ->icon('heroicon-o-document-arrow-down') + ->hidden(fn (FileItem $record): bool => $record->isFolder()) + ->action(fn (FileItem $record) => Storage::disk($this->disk)->download($record->path)), + DeleteAction::make('delete') + ->successNotificationTitle('File deleted') + ->hidden(fn (FileItem $record): bool => $record->isPreviousPath()) + ->action(function (FileItem $record, Action $action) { + if ($record->delete()) { + $action->sendSuccessNotification(); + } - if (!$findHostingSubscription || !$findDomain) { - throw new \Exception('Hosting subscription not found'); - } + }), + ]) + ->bulkActions([ + BulkAction::make('delete') + ->icon('heroicon-o-trash') + ->color('danger') + ->requiresConfirmation() + ->successNotificationTitle('Files deleted') + ->deselectRecordsAfterCompletion() + ->action(function (Collection $records, BulkAction $action) { + $records->each(fn (FileItem $record) => $record->delete()); + $action->sendSuccessNotification(); + }), + ]) + ->checkIfRecordIsSelectableUsing(fn (FileItem $record): bool => ! $record->isPreviousPath()) + ->headerActions([ + Action::make('create_folder') + ->label('Create Folder') + ->icon('heroicon-o-folder-plus') + ->form([ + TextInput::make('name') + ->label('Folder name') + ->placeholder('Folder name') + ->required(), + ]) + ->successNotificationTitle('Folder created') + ->action(function (array $data, Component $livewire, Action $action): void { + Storage::disk($livewire->disk) + ->makeDirectory($livewire->path.'/'.$data['name']); - $this->hostingSubscriptionSystemUsername = $findHostingSubscription->system_username; - $this->domainHomeRoot = $findDomain->home_root; + $this->resetTable(); + $action->sendSuccessNotification(); + }), - } - - public function openDeleteModal() - { - $this->dispatch('open-modal', id: 'delete-file'); - } - - public function goto($dirOrFile) - { - $newPath = $this->currentRealPath . '/' . $dirOrFile; - if (is_dir($newPath)) { - $this->currentRealPath = $newPath; - } - - } - - public function back() - { - $this->canIBack = false; - - $newRealPath = dirname($this->currentRealPath); - if (Str::startsWith($newRealPath, $this->domainHomeRoot)) { - $this->currentRealPath = $newRealPath; - } - } - - public function canIAccess($realPath, $systemUsername) - { - $checkOwner = posix_getpwuid(fileowner($realPath)); - - if (isset($checkOwner['name']) && $checkOwner['name'] == $systemUsername) { - return true; - } - - return false; - - } - - public function createFolder() - { - $this->folderName = Str::slug($this->folderName); - $newPath = $this->currentRealPath . '/' . $this->folderName; - if (!is_dir($newPath)) { - mkdir($newPath); - $this->folderName = ''; - $this->dispatch('close-modal', id: 'create-folder'); - } - } - - public function render() - { - if (!$this->currentRealPath) { - $this->currentRealPath = $this->domainHomeRoot; - } - - $all = []; - $files = []; - $folders = []; - if ($this->currentRealPath) { - - if (Str::startsWith(dirname($this->currentRealPath), $this->domainHomeRoot)) { - $this->canIBack = true; - } - - $scanFiles = scandir($this->currentRealPath); - foreach ($scanFiles as $scanFile) { - if ($scanFile == '.' || $scanFile == '..') { - continue; - } - try { - $append = [ - 'extension' => pathinfo($scanFile, PATHINFO_EXTENSION), - 'name' => $scanFile, - 'path' => $this->currentRealPath . '/' . $scanFile, - 'is_dir' => is_dir($this->currentRealPath . '/' . $scanFile), - 'permission' => substr(sprintf('%o', fileperms($this->currentRealPath . '/' . $scanFile)), -4), - 'owner' => posix_getpwuid(fileowner($this->currentRealPath . '/' . $scanFile))['name'], - 'group' => posix_getgrgid(filegroup($this->currentRealPath . '/' . $scanFile))['name'], - 'size' => Helpers::getHumanReadableSize(filesize($this->currentRealPath . '/' . $scanFile)), - 'last_modified' => date('Y-m-d H:i:s', filemtime($this->currentRealPath . '/' . $scanFile)), - 'type' => filetype($this->currentRealPath . '/' . $scanFile), - ]; - if ($append['is_dir']) { - $folders[] = $append; - } else { - $files[] = $append; - } - } catch (\Exception $e) { - continue; - } - } - } - - $all = array_merge($folders, $files); - - return view('livewire.file-manager.index', [ - 'files'=>$all - ]); + Action::make('upload_file') + ->label('Upload files') + ->icon('heroicon-o-document-arrow-up') + ->color('info') + ->form([ + FileUpload::make('files') + ->required() + ->multiple() + ->previewable(false) + ->preserveFilenames() + ->disk($this->disk) + ->directory($this->path), + ]), + ]); } } diff --git a/web/app/Livewire/FileManagerOld.php b/web/app/Livewire/FileManagerOld.php new file mode 100644 index 0000000..c74e921 --- /dev/null +++ b/web/app/Livewire/FileManagerOld.php @@ -0,0 +1,142 @@ +hostingSubscriptionId = $hostingSubscriptionId; + + $findHostingSubscription = HostingSubscription::where('id', $this->hostingSubscriptionId)->first(); + $findDomain = Domain::where('hosting_subscription_id', $this->hostingSubscriptionId) + ->where('is_main', 1) + ->first(); + + if (!$findHostingSubscription || !$findDomain) { + throw new \Exception('Hosting subscription not found'); + } + + $this->hostingSubscriptionSystemUsername = $findHostingSubscription->system_username; + $this->domainHomeRoot = $findDomain->home_root; + + } + + public function openDeleteModal() + { + $this->dispatch('open-modal', id: 'delete-file'); + } + + public function goto($dirOrFile) + { + $newPath = $this->currentRealPath . '/' . $dirOrFile; + if (is_dir($newPath)) { + $this->currentRealPath = $newPath; + } + + } + + public function back() + { + $this->canIBack = false; + + $newRealPath = dirname($this->currentRealPath); + if (Str::startsWith($newRealPath, $this->domainHomeRoot)) { + $this->currentRealPath = $newRealPath; + } + } + + public function canIAccess($realPath, $systemUsername) + { + $checkOwner = posix_getpwuid(fileowner($realPath)); + + if (isset($checkOwner['name']) && $checkOwner['name'] == $systemUsername) { + return true; + } + + return false; + + } + + public function createFolder() + { + $this->folderName = Str::slug($this->folderName); + $newPath = $this->currentRealPath . '/' . $this->folderName; + if (!is_dir($newPath)) { + mkdir($newPath); + $this->folderName = ''; + $this->dispatch('close-modal', id: 'create-folder'); + } + } + + public function render() + { + if (!$this->currentRealPath) { + $this->currentRealPath = $this->domainHomeRoot; + } + + $all = []; + $files = []; + $folders = []; + if ($this->currentRealPath) { + + if (Str::startsWith(dirname($this->currentRealPath), $this->domainHomeRoot)) { + $this->canIBack = true; + } + + $scanFiles = scandir($this->currentRealPath); + foreach ($scanFiles as $scanFile) { + if ($scanFile == '.' || $scanFile == '..') { + continue; + } + try { + $append = [ + 'extension' => pathinfo($scanFile, PATHINFO_EXTENSION), + 'name' => $scanFile, + 'path' => $this->currentRealPath . '/' . $scanFile, + 'is_dir' => is_dir($this->currentRealPath . '/' . $scanFile), + 'permission' => substr(sprintf('%o', fileperms($this->currentRealPath . '/' . $scanFile)), -4), + 'owner' => posix_getpwuid(fileowner($this->currentRealPath . '/' . $scanFile))['name'], + 'group' => posix_getgrgid(filegroup($this->currentRealPath . '/' . $scanFile))['name'], + 'size' => Helpers::getHumanReadableSize(filesize($this->currentRealPath . '/' . $scanFile)), + 'last_modified' => date('Y-m-d H:i:s', filemtime($this->currentRealPath . '/' . $scanFile)), + 'type' => filetype($this->currentRealPath . '/' . $scanFile), + ]; + if ($append['is_dir']) { + $folders[] = $append; + } else { + $files[] = $append; + } + } catch (\Exception $e) { + continue; + } + } + } + + $all = array_merge($folders, $files); + + return view('livewire.file-manager.index', [ + 'files'=>$all + ]); + } +} diff --git a/web/app/Models/FileItem.php b/web/app/Models/FileItem.php new file mode 100644 index 0000000..746cb58 --- /dev/null +++ b/web/app/Models/FileItem.php @@ -0,0 +1,104 @@ + 'string', + 'dateModified' => 'datetime', + 'size' => 'integer', + 'type' => 'string', + ]; + + public static function queryForDiskAndPath(string $disk = 'public', string $path = ''): Builder + { + static::$disk = $disk; + static::$path = $path; + + return static::query(); + } + + public function isFolder(): bool + { + return $this->type === 'Folder' + && is_dir(Storage::disk(static::$disk)->path($this->path)); + } + + public function isPreviousPath(): bool + { + return $this->name === '..'; + } + + public function delete(): bool + { + if ($this->isFolder()) { + return Storage::disk(static::$disk)->deleteDirectory($this->path); + } + + return Storage::disk(static::$disk)->delete($this->path); + } + + public function canOpen(): bool + { + return $this->type !== 'Folder' + && Storage::disk(static::$disk)->exists($this->path) + && Storage::disk(static::$disk)->getVisibility($this->path) === FilesystemContract::VISIBILITY_PUBLIC; + } + + public function getRows(): array + { + $backPath = []; + if (self::$path) { + $path = Str::of(self::$path)->explode('/'); + + $backPath = [ + [ + 'name' => '..', + 'dateModified' => null, + 'size' => null, + 'type' => 'Folder', + 'path' => $path->count() > 1 ? $path->take($path->count() - 1)->join('/') : '', + ], + ]; + } + + $storage = Storage::disk(static::$disk); + + return collect($backPath)->push( + ...collect($storage->directories(static::$path)) + ->sort() + ->map(fn (string $directory): array => [ + 'name' => Str::remove(self::$path.'/', $directory), + 'dateModified' => $storage->lastModified($directory), + 'size' => null, + 'type' => 'Folder', + 'path' => $directory, + ] + ), + ...collect($storage->files(static::$path)) + ->sort() + ->map(fn (string $file): array => [ + 'name' => Str::remove(self::$path.'/', $file), + 'dateModified' => $storage->lastModified($file), + 'size' => $storage->size($file), + 'type' => $storage->mimeType($file) ?: null, + 'path' => $file, + ] + ) + )->toArray(); + } +} diff --git a/web/composer.json b/web/composer.json index ddf6f32..c7a8e1f 100644 --- a/web/composer.json +++ b/web/composer.json @@ -11,6 +11,7 @@ "php": "^8.1", "acmephp/core": "*", "archilex/filament-toggle-icon-column": "^3.1", + "calebporzio/sushi": "^2.5", "coolsam/modules": "^3.0@beta", "darkaonline/l5-swagger": "^8.6", "filament/filament": "^3.0", diff --git a/web/composer.lock b/web/composer.lock index f77b72a..0523aea 100644 --- a/web/composer.lock +++ b/web/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bc8f7e167f22d774934a3b1001c2fd83", + "content-hash": "f079cdfb4cd2f67cf4a0db0a51639be9", "packages": [ { "name": "acmephp/core", @@ -487,6 +487,60 @@ ], "time": "2023-11-29T23:19:16+00:00" }, + { + "name": "calebporzio/sushi", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/calebporzio/sushi.git", + "reference": "01dd34fe3374f5fb7ce63756c0419385e31cd532" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/calebporzio/sushi/zipball/01dd34fe3374f5fb7ce63756c0419385e31cd532", + "reference": "01dd34fe3374f5fb7ce63756c0419385e31cd532", + "shasum": "" + }, + "require": { + "ext-pdo_sqlite": "*", + "ext-sqlite3": "*", + "illuminate/database": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "php": "^7.1.3|^8.0" + }, + "require-dev": { + "doctrine/dbal": "^2.9 || ^3.1.4", + "orchestra/testbench": "3.8.* || 3.9.* || ^4.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpunit/phpunit": "^7.5 || ^8.4 || ^9.0 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sushi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "Eloquent's missing \"array\" driver.", + "support": { + "source": "https://github.com/calebporzio/sushi/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://github.com/calebporzio", + "type": "github" + } + ], + "time": "2024-04-24T15:23:03+00:00" + }, { "name": "carbonphp/carbon-doctrine-types", "version": "2.1.0", @@ -12434,5 +12488,5 @@ "php": "^8.1" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/web/resources/views/filament/pages/file-manager.blade.php b/web/resources/views/filament/pages/file-manager.blade.php new file mode 100644 index 0000000..ce096a2 --- /dev/null +++ b/web/resources/views/filament/pages/file-manager.blade.php @@ -0,0 +1,3 @@ + + {{ $this->table }} + diff --git a/web/resources/views/filament/pages/view-hosting-subscription.blade.php b/web/resources/views/filament/pages/view-hosting-subscription.blade.php index a9851f6..5796b01 100644 --- a/web/resources/views/filament/pages/view-hosting-subscription.blade.php +++ b/web/resources/views/filament/pages/view-hosting-subscription.blade.php @@ -1,7 +1,8 @@ - + {{-- hostingSubscriptionId="{{$this->data['id']}}"--}} +