Files
coolify/app/Livewire/Project/Shared/FileBrowser.php
T

340 lines
9.8 KiB
PHP

<?php
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Services\ContainerFilesystemService;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Livewire\WithFileUploads;
class FileBrowser extends Component
{
use AuthorizesRequests;
use WithFileUploads;
public $resource;
public string $type = '';
public bool $containerRunning = false;
public ?string $container = null;
/** @var array<int, string> */
public array $availableContainers = [];
public string $currentPath = '';
/** @var array<int, array{name:string,type:string,size:int,mtime:int}> */
public array $entries = [];
public ?string $editingPath = null;
public bool $editorOpen = false;
public string $editorContent = '';
public $upload;
/** @var array<string, mixed> */
public array $parameters = [];
public function mount($resource = null): void
{
$this->parameters = get_route_parameters();
$resource = $resource ?? $this->resolveResourceFromRoute();
// Security gate first, before any server or container resolution.
$this->authorize('canAccessTerminal');
$this->authorize('view', $resource);
$this->resource = $resource;
$this->type = $this->resolveType($resource);
$this->containerRunning = (bool) $resource->isRunning();
if (! $this->containerRunning) {
return;
}
$this->availableContainers = $this->resolveContainers();
$this->container = $this->availableContainers[0] ?? null;
if (is_null($this->container)) {
$this->containerRunning = false;
return;
}
$this->currentPath = $this->service()->defaultRoot();
$this->loadEntries();
}
public function selectContainer(string $name): void
{
if (! in_array($name, $this->availableContainers, true)) {
return;
}
$this->container = $name;
$this->currentPath = $this->service()->defaultRoot();
$this->loadEntries();
}
public function open(string $name): void
{
$this->currentPath = rtrim($this->currentPath, '/').'/'.$name;
$this->loadEntries();
}
public function goTo(string $path): void
{
if (! str_starts_with($path, '/')) {
return;
}
$this->currentPath = $path === '/' ? '/' : rtrim($path, '/');
$this->loadEntries();
}
public function refresh(): void
{
$this->loadEntries();
}
public function createDirectory(string $name): void
{
$this->guard();
try {
$this->service()->makeDirectory($this->childPath($name));
$this->loadEntries();
$this->dispatch('success', 'Folder created.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function renameEntry(string $from, string $newName): void
{
$this->guard();
try {
$this->service()->rename($this->childPath($from), $this->childPath($newName));
$this->loadEntries();
$this->dispatch('success', 'Renamed.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function deleteEntry(string $name): void
{
$this->guard();
try {
$this->service()->delete($this->childPath($name));
$this->loadEntries();
$this->dispatch('success', 'Deleted.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function openEditor(string $name): void
{
$this->guard();
$path = $this->childPath($name);
try {
if (! $this->service()->isEditable($path)) {
$this->dispatch('error', "Can't edit this file - it's binary or larger than 5 MB. Download it instead.");
return;
}
$this->editorContent = $this->service()->read($path);
$this->editingPath = $path;
$this->editorOpen = true;
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function saveEditor(): void
{
$this->guard();
if (is_null($this->editingPath)) {
return;
}
try {
$this->service()->write($this->editingPath, $this->editorContent);
$this->editingPath = null;
$this->editorOpen = false;
$this->editorContent = '';
$this->loadEntries();
$this->dispatch('success', 'Saved.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function closeEditor(): void
{
$this->editingPath = null;
$this->editorOpen = false;
$this->editorContent = '';
}
public function uploadFile(): void
{
$this->guard();
$this->validate(['upload' => 'required|file|max:1048576']); // 1 GB (KB units)
try {
$localTmp = $this->upload->getRealPath();
$this->service()->upload($localTmp, $this->childPath($this->upload->getClientOriginalName()));
$this->upload = null;
$this->loadEntries();
$this->dispatch('success', 'Uploaded.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function download(string $name)
{
$this->guard();
$localTmp = $this->service()->download($this->childPath($name));
$downloadName = str_ends_with($localTmp, '.tar.gz') ? $name.'.tar.gz' : $name;
return response()->streamDownload(function () use ($localTmp) {
readfile($localTmp);
@unlink($localTmp);
}, $downloadName);
}
protected function childPath(string $name): string
{
return rtrim($this->currentPath, '/').'/'.$name;
}
protected function guard(): void
{
$this->authorize('canAccessTerminal');
if (! $this->containerRunning || is_null($this->container)) {
throw new \RuntimeException('Container is not running.');
}
}
/**
* @return array<int, array{label:string,path:string}>
*/
public function breadcrumbs(): array
{
$crumbs = [['label' => '/', 'path' => '/']];
$accumulated = '';
foreach (array_filter(explode('/', $this->currentPath)) as $segment) {
$accumulated .= '/'.$segment;
$crumbs[] = ['label' => $segment, 'path' => $accumulated];
}
return $crumbs;
}
protected function loadEntries(): void
{
$entries = $this->service()->list($this->currentPath);
$this->entries = array_map(fn ($e) => [
'name' => $e->name,
'type' => $e->type,
'size' => $e->size,
'mtime' => $e->mtime,
], $entries);
}
protected function service(): ContainerFilesystemService
{
return new ContainerFilesystemService($this->server(), $this->container);
}
protected function server(): Server
{
return match ($this->type) {
'service' => $this->resource->server,
default => $this->resource->destination->server,
};
}
protected function resolveResourceFromRoute()
{
$parameters = get_route_parameters();
$teamId = data_get(auth()->user()->currentTeam(), 'id');
if ($uuid = data_get($parameters, 'application_uuid')) {
return Application::ownedByCurrentTeam()->where('uuid', $uuid)->firstOrFail();
}
if ($uuid = data_get($parameters, 'service_uuid')) {
return Service::ownedByCurrentTeam()->where('uuid', $uuid)->firstOrFail();
}
if ($uuid = data_get($parameters, 'database_uuid')) {
$resource = getResourceByUuid($uuid, $teamId);
if (is_null($resource)) {
abort(404);
}
return $resource;
}
abort(404);
}
protected function resolveType($resource): string
{
return match (true) {
$resource instanceof Application => 'application',
$resource instanceof Service => 'service',
default => 'database',
};
}
/**
* @return array<int, string>
*/
protected function resolveContainers(): array
{
if ($this->type === 'service') {
$names = [];
$this->resource->applications()->get()->each(function ($application) use (&$names) {
if ($application->isRunning()) {
$names[] = $application->name.'-'.$this->resource->uuid;
}
});
$this->resource->databases()->get()->each(function ($database) use (&$names) {
if ($database->isRunning()) {
$names[] = $database->name.'-'.$this->resource->uuid;
}
});
return array_values(array_filter($names, [ValidationPatterns::class, 'isValidContainerName']));
}
if ($this->type === 'application') {
$server = $this->server();
$names = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: false)
->filter(fn ($c) => data_get($c, 'State') === 'running')
->map(fn ($c) => data_get($c, 'Names'))
->filter()
->values()
->all();
return array_values(array_filter($names, [ValidationPatterns::class, 'isValidContainerName']));
}
// Standalone database: the container name is the resource uuid.
$name = $this->resource->uuid;
return ValidationPatterns::isValidContainerName($name) ? [$name] : [];
}
public function render()
{
return view('livewire.project.shared.file-browser');
}
}