mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-20 02:11:36 +00:00
Built-in File Manager (#458)
This commit is contained in:
parent
75e554ad74
commit
e2b9d18a71
@ -15,16 +15,12 @@ class ManageBackupFile
|
||||
*/
|
||||
public function download(BackupFile $file): StreamedResponse
|
||||
{
|
||||
$localFilename = "backup_{$file->id}_{$file->name}.zip";
|
||||
|
||||
if (! Storage::disk('backups')->exists($localFilename)) {
|
||||
$file->backup->server->ssh()->download(
|
||||
Storage::disk('backups')->path($localFilename),
|
||||
Storage::disk('tmp')->path(basename($file->path())),
|
||||
$file->path()
|
||||
);
|
||||
}
|
||||
|
||||
return Storage::disk('backups')->download($localFilename, $file->name.'.zip');
|
||||
return Storage::disk('tmp')->download(basename($file->path()));
|
||||
}
|
||||
|
||||
public function delete(BackupFile $file): void
|
||||
|
39
app/Actions/FileManager/FetchFiles.php
Normal file
39
app/Actions/FileManager/FetchFiles.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\FileManager;
|
||||
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class FetchFiles
|
||||
{
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function fetch(User $user, Server $server, array $input): void
|
||||
{
|
||||
File::parse(
|
||||
$user,
|
||||
$server,
|
||||
$input['path'],
|
||||
$input['user'],
|
||||
$server->os()->ls($input['path'], $input['user'])
|
||||
);
|
||||
}
|
||||
|
||||
public static function rules(Server $server): array
|
||||
{
|
||||
return [
|
||||
'path' => [
|
||||
'required',
|
||||
],
|
||||
'user' => [
|
||||
'required',
|
||||
Rule::in($server->getSshUsers()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -94,11 +94,7 @@ public function connect(bool $sftp = false): void
|
||||
*/
|
||||
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
|
||||
{
|
||||
if (! $log) {
|
||||
$log = 'run-command';
|
||||
}
|
||||
|
||||
if (! $this->log) {
|
||||
if (! $this->log && $log) {
|
||||
$this->log = ServerLog::make($this->server, $log);
|
||||
if ($siteId) {
|
||||
$this->log->forSite($siteId);
|
||||
@ -122,7 +118,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
|
||||
$this->connection->setTimeout(0);
|
||||
if ($stream) {
|
||||
$this->connection->exec($command, function ($output) use ($streamCallback) {
|
||||
$this->log->write($output);
|
||||
$this->log?->write($output);
|
||||
|
||||
return $streamCallback($output);
|
||||
});
|
||||
@ -131,7 +127,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
|
||||
} else {
|
||||
$output = '';
|
||||
$this->connection->exec($command, function ($out) use (&$output) {
|
||||
$this->log->write($out);
|
||||
$this->log?->write($out);
|
||||
$output .= $out;
|
||||
});
|
||||
|
||||
|
147
app/Models/File.php
Normal file
147
app/Models/File.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $user_id
|
||||
* @property int $server_id
|
||||
* @property string $path
|
||||
* @property string $type
|
||||
* @property string $server_user
|
||||
* @property string $name
|
||||
* @property int $size
|
||||
* @property int $links
|
||||
* @property string $owner
|
||||
* @property string $group
|
||||
* @property string $date
|
||||
* @property string $permissions
|
||||
* @property User $user
|
||||
* @property Server $server
|
||||
*/
|
||||
class File extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'server_id',
|
||||
'server_user',
|
||||
'path',
|
||||
'type',
|
||||
'name',
|
||||
'size',
|
||||
'links',
|
||||
'owner',
|
||||
'group',
|
||||
'date',
|
||||
'permissions',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'server_id' => 'integer',
|
||||
'size' => 'integer',
|
||||
'links' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function (File $file) {
|
||||
if ($file->name === '.' || $file->name === '..') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file->server->os()->deleteFile($file->getFilePath(), $file->server_user);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public static function path(User $user, Server $server, string $serverUser): string
|
||||
{
|
||||
$file = self::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('server_id', $server->id)
|
||||
->where('server_user', $serverUser)
|
||||
->first();
|
||||
|
||||
if ($file) {
|
||||
return $file->path;
|
||||
}
|
||||
|
||||
return home_path($serverUser);
|
||||
}
|
||||
|
||||
public static function parse(User $user, Server $server, string $path, string $serverUser, string $listOutput): void
|
||||
{
|
||||
self::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('server_id', $server->id)
|
||||
->delete();
|
||||
|
||||
// Split output by line
|
||||
$lines = explode("\n", trim($listOutput));
|
||||
|
||||
// Skip the first two lines (total count and . & .. directories)
|
||||
array_shift($lines);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
|
||||
$type = match ($matches[1][0]) {
|
||||
'-' => 'file',
|
||||
'd' => 'directory',
|
||||
default => 'unknown',
|
||||
};
|
||||
if ($type === 'unknown') {
|
||||
continue;
|
||||
}
|
||||
if ($matches[7] === '.') {
|
||||
continue;
|
||||
}
|
||||
self::create([
|
||||
'user_id' => $user->id,
|
||||
'server_id' => $server->id,
|
||||
'server_user' => $serverUser,
|
||||
'path' => $path,
|
||||
'type' => $type,
|
||||
'name' => $matches[7],
|
||||
'size' => (int) $matches[5],
|
||||
'links' => (int) $matches[2],
|
||||
'owner' => $matches[3],
|
||||
'group' => $matches[4],
|
||||
'date' => $matches[6],
|
||||
'permissions' => $matches[1],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->path.'/'.$this->name;
|
||||
}
|
||||
|
||||
public function isExtractable(): bool
|
||||
{
|
||||
$extension = pathinfo($this->name, PATHINFO_EXTENSION);
|
||||
|
||||
return in_array($extension, ['zip', 'tar', 'tar.gz', 'bz2', 'tar.bz2']);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
use App\Actions\Server\CheckConnection;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Enums\ServiceStatus;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Facades\SSH;
|
||||
use App\ServerTypes\ServerType;
|
||||
use App\SSH\Cron\Cron;
|
||||
@ -22,6 +23,7 @@
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @property int $project_id
|
||||
@ -146,7 +148,7 @@ public static function boot(): void
|
||||
}
|
||||
$server->provider()->delete();
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
@ -465,6 +467,9 @@ public function cron(): Cron
|
||||
return new Cron($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function checkForUpdates(): void
|
||||
{
|
||||
$this->updates = $this->os()->availableUpdates();
|
||||
@ -480,4 +485,15 @@ public function getAvailableUpdatesAttribute(?int $value): int
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download(string $path, string $disk = 'tmp'): void
|
||||
{
|
||||
$this->ssh()->download(
|
||||
Storage::disk($disk)->path(basename($path)),
|
||||
$path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -263,10 +263,14 @@ public function download(string $url, string $path): string
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function unzip(string $path): string
|
||||
public function extract(string $path, ?string $destination = null, ?string $user = null): void
|
||||
{
|
||||
return $this->server->ssh()->exec(
|
||||
'unzip '.$path
|
||||
$this->server->ssh($user)->exec(
|
||||
view('ssh.os.extract', [
|
||||
'path' => $path,
|
||||
'destination' => $destination,
|
||||
]),
|
||||
'extract'
|
||||
);
|
||||
}
|
||||
|
||||
@ -304,9 +308,9 @@ public function resourceInfo(): array
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function deleteFile(string $path): void
|
||||
public function deleteFile(string $path, ?string $user = null): void
|
||||
{
|
||||
$this->server->ssh()->exec(
|
||||
$this->server->ssh($user)->exec(
|
||||
view('ssh.os.delete-file', [
|
||||
'path' => $path,
|
||||
]),
|
||||
@ -314,6 +318,33 @@ public function deleteFile(string $path): void
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function ls(string $path, ?string $user = null): string
|
||||
{
|
||||
return $this->server->ssh($user)->exec('ls -la '.$path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function write(string $path, string $content, ?string $user = null): void
|
||||
{
|
||||
$this->server->ssh($user)->write(
|
||||
$path,
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function mkdir(string $path, ?string $user = null): string
|
||||
{
|
||||
return $this->server->ssh($user)->exec('mkdir -p '.$path);
|
||||
}
|
||||
|
||||
private function deleteTempFile(string $name): void
|
||||
{
|
||||
if (Storage::disk('local')->exists($name)) {
|
||||
|
@ -199,11 +199,13 @@ public function setupSSL(Ssl $ssl): void
|
||||
*/
|
||||
public function removeSSL(Ssl $ssl): void
|
||||
{
|
||||
if ($ssl->certificate_path) {
|
||||
$this->service->server->ssh()->exec(
|
||||
'sudo rm -rf '.dirname($ssl->certificate_path).'*',
|
||||
'sudo rm -rf '.dirname($ssl->certificate_path),
|
||||
'remove-ssl',
|
||||
$ssl->site_id
|
||||
);
|
||||
}
|
||||
|
||||
$this->updateVHost($ssl->site);
|
||||
}
|
||||
|
@ -178,3 +178,31 @@ function get_from_route(string $modelName, string $routeKey): mixed
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function absolute_path(string $path): string
|
||||
{
|
||||
$parts = explode('/', $path);
|
||||
$absoluteParts = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '' || $part === '.') {
|
||||
continue; // Skip empty and current directory parts
|
||||
}
|
||||
if ($part === '..') {
|
||||
array_pop($absoluteParts); // Move up one directory
|
||||
} else {
|
||||
$absoluteParts[] = $part; // Add valid directory parts
|
||||
}
|
||||
}
|
||||
|
||||
return '/'.implode('/', $absoluteParts);
|
||||
}
|
||||
|
||||
function home_path(string $user): string
|
||||
{
|
||||
if ($user === 'root') {
|
||||
return '/root';
|
||||
}
|
||||
|
||||
return '/home/'.$user;
|
||||
}
|
||||
|
26
app/Web/Pages/Servers/FileManager/Index.php
Normal file
26
app/Web/Pages/Servers/FileManager/Index.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\FileManager;
|
||||
|
||||
use App\Web\Pages\Servers\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/file-manager';
|
||||
|
||||
protected static ?string $title = 'File Manager';
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\FilesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
371
app/Web/Pages/Servers/FileManager/Widgets/FilesList.php
Normal file
371
app/Web/Pages/Servers/FileManager/Widgets/FilesList.php
Normal file
@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\FileManager\Widgets;
|
||||
|
||||
use App\Actions\FileManager\FetchFiles;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use App\Web\Pages\Servers\FileManager\Index;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Support\Enums\ActionSize;
|
||||
use Filament\Support\Enums\IconPosition;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FilesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public string $serverUser;
|
||||
|
||||
public string $path;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->serverUser = $this->server->ssh_user;
|
||||
$this->path = home_path($this->serverUser);
|
||||
if (request()->has('path') && request()->has('user')) {
|
||||
$this->path = request('path');
|
||||
$this->serverUser = request('user');
|
||||
}
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
protected function getTableHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->homeAction(),
|
||||
$this->userAction(),
|
||||
ActionGroup::make([
|
||||
$this->refreshAction(),
|
||||
$this->newFileAction(),
|
||||
$this->newDirectoryAction(),
|
||||
$this->uploadAction(),
|
||||
])
|
||||
->tooltip('Toolbar')
|
||||
->icon('heroicon-o-ellipsis-vertical')
|
||||
->color('gray')
|
||||
->size(ActionSize::Large)
|
||||
->iconPosition(IconPosition::After)
|
||||
->dropdownPlacement('bottom-end'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return File::query()
|
||||
->where('user_id', auth()->id())
|
||||
->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->headerActions($this->getTableHeaderActions())
|
||||
->heading(str($this->path)->substr(-50)->start(str($this->path)->length() > 50 ? '...' : ''))
|
||||
->columns([
|
||||
IconColumn::make('type')
|
||||
->sortable()
|
||||
->icon(fn (File $file) => $this->getIcon($file)),
|
||||
TextColumn::make('name')
|
||||
->sortable(),
|
||||
TextColumn::make('size')
|
||||
->sortable(),
|
||||
TextColumn::make('owner')
|
||||
->sortable(),
|
||||
TextColumn::make('group')
|
||||
->sortable(),
|
||||
TextColumn::make('date')
|
||||
->sortable(),
|
||||
TextColumn::make('permissions')
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) {
|
||||
if ($file->type === 'directory') {
|
||||
return Index::getUrl([
|
||||
'server' => $this->server->id,
|
||||
'user' => $file->server_user,
|
||||
'path' => absolute_path($file->path.'/'.$file->name),
|
||||
]);
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->defaultSort('type')
|
||||
->actions([
|
||||
$this->extractAction(),
|
||||
$this->downloadAction(),
|
||||
$this->editAction(),
|
||||
$this->deleteAction(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(
|
||||
fn (File $file): bool => $file->name !== '..',
|
||||
)
|
||||
->bulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->requiresConfirmation(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeUser(string $user): void
|
||||
{
|
||||
$this->redirect(
|
||||
Index::getUrl([
|
||||
'server' => $this->server->id,
|
||||
'user' => $user,
|
||||
'path' => home_path($user),
|
||||
]),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
try {
|
||||
app(FetchFiles::class)->fetch(
|
||||
auth()->user(),
|
||||
$this->server,
|
||||
[
|
||||
'user' => $this->serverUser,
|
||||
'path' => $this->path,
|
||||
]
|
||||
);
|
||||
} catch (SSHError) {
|
||||
abort(404);
|
||||
}
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
protected function getIcon(File $file): string
|
||||
{
|
||||
if ($file->type === 'directory') {
|
||||
return 'heroicon-o-folder';
|
||||
}
|
||||
|
||||
if (str($file->name)->endsWith('.blade.php')) {
|
||||
return 'laravel';
|
||||
}
|
||||
|
||||
if (str($file->name)->endsWith('.php')) {
|
||||
return 'php';
|
||||
}
|
||||
|
||||
return 'heroicon-o-document-text';
|
||||
}
|
||||
|
||||
protected function homeAction(): Action
|
||||
{
|
||||
return Action::make('home')
|
||||
->label('Home')
|
||||
->size(ActionSize::Small)
|
||||
->icon('heroicon-o-home')
|
||||
->action(function () {
|
||||
$this->path = home_path($this->serverUser);
|
||||
$this->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function userAction(): ActionGroup
|
||||
{
|
||||
$users = [];
|
||||
foreach ($this->server->getSshUsers() as $user) {
|
||||
$users[] = Action::make('user-'.$user)
|
||||
->action(fn () => $this->changeUser($user))
|
||||
->label($user);
|
||||
}
|
||||
|
||||
return ActionGroup::make($users)
|
||||
->tooltip('Change user')
|
||||
->label($this->serverUser)
|
||||
->button()
|
||||
->size(ActionSize::Small)
|
||||
->color('gray')
|
||||
->icon('heroicon-o-chevron-up-down')
|
||||
->iconPosition(IconPosition::After)
|
||||
->dropdownPlacement('bottom-end');
|
||||
}
|
||||
|
||||
protected function refreshAction(): Action
|
||||
{
|
||||
return Action::make('refresh')
|
||||
->label('Refresh')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->action(fn () => $this->refresh());
|
||||
}
|
||||
|
||||
protected function newFileAction(): Action
|
||||
{
|
||||
return Action::make('new-file')
|
||||
->label('New File')
|
||||
->icon('heroicon-o-document-text')
|
||||
->action(function (array $data) {
|
||||
run_action($this, function () use ($data) {
|
||||
$this->server->os()->write(
|
||||
$this->path.'/'.$data['name'],
|
||||
str_replace("\r\n", "\n", $data['content']),
|
||||
$this->serverUser
|
||||
);
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(function () {
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->placeholder('file-name.txt'),
|
||||
CodeEditorField::make('content'),
|
||||
];
|
||||
})
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New File')
|
||||
->modalWidth('4xl');
|
||||
}
|
||||
|
||||
protected function newDirectoryAction(): Action
|
||||
{
|
||||
return Action::make('new-directory')
|
||||
->label('New Directory')
|
||||
->icon('heroicon-o-folder')
|
||||
->action(function (array $data) {
|
||||
run_action($this, function () use ($data) {
|
||||
$this->server->os()->mkdir(
|
||||
$this->path.'/'.$data['name'],
|
||||
$this->serverUser
|
||||
);
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(function () {
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->placeholder('directory name'),
|
||||
];
|
||||
})
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New Directory')
|
||||
->modalWidth('lg');
|
||||
}
|
||||
|
||||
protected function uploadAction(): Action
|
||||
{
|
||||
return Action::make('upload')
|
||||
->label('Upload File')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->action(function (array $data) {
|
||||
//
|
||||
})
|
||||
->after(function (array $data) {
|
||||
run_action($this, function () use ($data) {
|
||||
foreach ($data['file'] as $file) {
|
||||
$this->server->ssh($this->serverUser)->upload(
|
||||
Storage::disk('tmp')->path($file),
|
||||
$this->path.'/'.$file,
|
||||
);
|
||||
}
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(function () {
|
||||
return [
|
||||
FileUpload::make('file')
|
||||
->disk('tmp')
|
||||
->multiple()
|
||||
->preserveFilenames(),
|
||||
];
|
||||
})
|
||||
->modalSubmitActionLabel('Upload to Server')
|
||||
->modalHeading('Upload File')
|
||||
->modalWidth('xl');
|
||||
}
|
||||
|
||||
protected function extractAction(): Action
|
||||
{
|
||||
return Action::make('extract')
|
||||
->tooltip('Extract')
|
||||
->icon('heroicon-o-archive-box')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file) => $file->isExtractable())
|
||||
->action(function (File $file) {
|
||||
$file->server->os()->extract($file->getFilePath(), $file->path, $file->server_user);
|
||||
$this->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function downloadAction(): Action
|
||||
{
|
||||
return Action::make('download')
|
||||
->tooltip('Download')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file) => $file->type === 'file')
|
||||
->action(function (File $file) {
|
||||
$file->server->ssh($file->server_user)->download(
|
||||
Storage::disk('tmp')->path($file->name),
|
||||
$file->getFilePath()
|
||||
);
|
||||
|
||||
return Storage::disk('tmp')->download($file->name);
|
||||
});
|
||||
}
|
||||
|
||||
protected function editAction(): Action
|
||||
{
|
||||
return Action::make('edit')
|
||||
->tooltip('Edit')
|
||||
->icon('heroicon-o-pencil')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file) => $file->type === 'file')
|
||||
->action(function (File $file, array $data) {
|
||||
$file->server->os()->write(
|
||||
$file->getFilePath(),
|
||||
str_replace("\r\n", "\n", $data['content']),
|
||||
$file->server_user
|
||||
);
|
||||
$this->refresh();
|
||||
})
|
||||
->form(function (File $file) {
|
||||
return [
|
||||
CodeEditorField::make('content')
|
||||
->formatStateUsing(function () use ($file) {
|
||||
$file->server->ssh($file->server_user)->download(
|
||||
Storage::disk('tmp')->path($file->name),
|
||||
$file->getFilePath()
|
||||
);
|
||||
|
||||
return Storage::disk('tmp')->get(basename($file->getFilePath()));
|
||||
}),
|
||||
];
|
||||
})
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalHeading('Edit')
|
||||
->modalWidth('4xl');
|
||||
}
|
||||
|
||||
protected function deleteAction(): Action
|
||||
{
|
||||
return Action::make('delete')
|
||||
->tooltip('Delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->hiddenLabel()
|
||||
->requiresConfirmation()
|
||||
->visible(fn (File $file) => $file->name !== '..')
|
||||
->action(function (File $file) {
|
||||
run_action($this, function () use ($file) {
|
||||
$file->delete();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
|
||||
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
|
||||
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
|
||||
use App\Web\Pages\Servers\FileManager\Index as FileManagerIndex;
|
||||
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
|
||||
use App\Web\Pages\Servers\Logs\Index as LogsIndex;
|
||||
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
|
||||
@ -59,6 +60,13 @@ public function getSubNavigation(): array
|
||||
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if (auth()->user()->can('update', $this->server)) {
|
||||
$items[] = NavigationItem::make(FileManagerIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-folder')
|
||||
->isActiveWhen(fn () => request()->routeIs(FileManagerIndex::getRouteName().'*'))
|
||||
->url(FileManagerIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
|
||||
->icon('icon-php-alt')
|
||||
|
@ -62,7 +62,7 @@
|
||||
'root' => storage_path('app/key-pairs'),
|
||||
],
|
||||
|
||||
'backups' => [
|
||||
'tmp' => [
|
||||
'driver' => 'local',
|
||||
'root' => sys_get_temp_dir(),
|
||||
],
|
||||
|
32
database/factories/FileFactory.php
Normal file
32
database/factories/FileFactory.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\File;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class FileFactory extends Factory
|
||||
{
|
||||
protected $model = File::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => $this->faker->randomNumber(), //
|
||||
'server_id' => $this->faker->randomNumber(),
|
||||
'server_user' => $this->faker->word(),
|
||||
'path' => $this->faker->word(),
|
||||
'type' => 'file',
|
||||
'name' => $this->faker->name(),
|
||||
'size' => $this->faker->randomNumber(),
|
||||
'links' => $this->faker->randomNumber(),
|
||||
'owner' => $this->faker->word(),
|
||||
'group' => $this->faker->word(),
|
||||
'date' => $this->faker->word(),
|
||||
'permissions' => $this->faker->word(),
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
}
|
33
database/migrations/2025_02_02_124012_create_files_table.php
Normal file
33
database/migrations/2025_02_02_124012_create_files_table.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('files', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('server_id');
|
||||
$table->string('server_user');
|
||||
$table->string('path');
|
||||
$table->string('type');
|
||||
$table->string('name');
|
||||
$table->unsignedBigInteger('size');
|
||||
$table->unsignedBigInteger('links');
|
||||
$table->string('owner');
|
||||
$table->string('group');
|
||||
$table->string('date');
|
||||
$table->string('permissions');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('files');
|
||||
}
|
||||
};
|
@ -1 +1 @@
|
||||
rm -f {{ $path }}
|
||||
rm -rf {{ $path }}
|
||||
|
15
resources/views/ssh/os/extract.blade.php
Normal file
15
resources/views/ssh/os/extract.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
@php
|
||||
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
||||
@endphp
|
||||
|
||||
@if($extension === 'zip')
|
||||
unzip -o {{ $path }} -d {{ $destination }}
|
||||
@elseif($extension === 'tar'))
|
||||
tar -xf {{ $path }} -C {{ $destination }}
|
||||
@elseif(in_array($extension, ['gz', 'tar.gz']))
|
||||
tar -xzf {{ $path }} -C {{ $destination }}
|
||||
@elseif(in_array($extension, ['bz2', 'tar.bz2']))
|
||||
tar -xjf {{ $path }} -C {{ $destination }}
|
||||
@else
|
||||
echo "Unsupported archive format: {{ $extension }}"
|
||||
@endif
|
138
tests/Feature/FileManagerTest.php
Normal file
138
tests/Feature/FileManagerTest.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Facades\SSH;
|
||||
use App\Models\File;
|
||||
use App\Web\Pages\Servers\FileManager\Index;
|
||||
use App\Web\Pages\Servers\FileManager\Widgets\FilesList;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FileManagerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_see_files(): void
|
||||
{
|
||||
SSH::fake(<<<'EOF'
|
||||
total 32
|
||||
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
|
||||
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
|
||||
drwx------ 3 vito vito 4096 Feb 1 18:45 .cache
|
||||
drwxrwxr-x 3 vito vito 4096 Feb 1 18:45 .config
|
||||
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 .gitconfig
|
||||
drwxrwxr-x 3 vito vito 4096 Feb 1 18:45 .local
|
||||
drwxr-xr-x 2 vito vito 4096 Feb 2 14:13 .ssh
|
||||
drwxrwxr-x 3 vito vito 4096 Feb 2 21:25 test.vitodeploy.com
|
||||
EOF
|
||||
);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$this->get(
|
||||
Index::getUrl([
|
||||
'server' => $this->server,
|
||||
])
|
||||
)
|
||||
->assertSuccessful()
|
||||
->assertSee('.cache')
|
||||
->assertSee('.config');
|
||||
}
|
||||
|
||||
public function test_upload_file(): void
|
||||
{
|
||||
SSH::fake();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
Livewire::test(FilesList::class, [
|
||||
'server' => $this->server,
|
||||
])
|
||||
->callTableAction('upload', null, [
|
||||
'file' => UploadedFile::fake()->create('test.txt'),
|
||||
])
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_create_file(): void
|
||||
{
|
||||
SSH::fake(<<<'EOF'
|
||||
total 3
|
||||
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
|
||||
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
|
||||
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 test.txt
|
||||
EOF
|
||||
);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
Livewire::test(FilesList::class, [
|
||||
'server' => $this->server,
|
||||
])
|
||||
->callTableAction('new-file', null, [
|
||||
'name' => 'test.txt',
|
||||
'content' => 'Hello, world!',
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('files', [
|
||||
'name' => 'test.txt',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_directory(): void
|
||||
{
|
||||
SSH::fake(<<<'EOF'
|
||||
total 3
|
||||
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
|
||||
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
|
||||
drwxr-xr-x 2 vito vito 4096 Feb 2 14:13 test
|
||||
EOF
|
||||
);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
Livewire::test(FilesList::class, [
|
||||
'server' => $this->server,
|
||||
])
|
||||
->callTableAction('new-directory', null, [
|
||||
'name' => 'test',
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('files', [
|
||||
'name' => 'test',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_download_file(): void
|
||||
{
|
||||
SSH::fake(<<<'EOF'
|
||||
total 3
|
||||
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
|
||||
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
|
||||
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 test.txt
|
||||
EOF
|
||||
);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$this->get(
|
||||
Index::getUrl([
|
||||
'server' => $this->server,
|
||||
])
|
||||
)->assertSuccessful();
|
||||
|
||||
$file = File::query()->where('name', 'test.txt')->firstOrFail();
|
||||
|
||||
Livewire::test(FilesList::class, [
|
||||
'server' => $this->server,
|
||||
])
|
||||
->assertTableActionVisible('download', $file)
|
||||
->callTableAction('download', $file)
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user