Built-in File Manager (#458)

This commit is contained in:
Saeed Vaziry 2025-02-16 19:56:21 +01:00 committed by GitHub
parent 75e554ad74
commit e2b9d18a71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 907 additions and 29 deletions

View File

@ -15,16 +15,12 @@ class ManageBackupFile
*/ */
public function download(BackupFile $file): StreamedResponse public function download(BackupFile $file): StreamedResponse
{ {
$localFilename = "backup_{$file->id}_{$file->name}.zip"; $file->backup->server->ssh()->download(
Storage::disk('tmp')->path(basename($file->path())),
$file->path()
);
if (! Storage::disk('backups')->exists($localFilename)) { return Storage::disk('tmp')->download(basename($file->path()));
$file->backup->server->ssh()->download(
Storage::disk('backups')->path($localFilename),
$file->path()
);
}
return Storage::disk('backups')->download($localFilename, $file->name.'.zip');
} }
public function delete(BackupFile $file): void public function delete(BackupFile $file): void

View 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()),
],
];
}
}

View File

@ -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 public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
{ {
if (! $log) { if (! $this->log && $log) {
$log = 'run-command';
}
if (! $this->log) {
$this->log = ServerLog::make($this->server, $log); $this->log = ServerLog::make($this->server, $log);
if ($siteId) { if ($siteId) {
$this->log->forSite($siteId); $this->log->forSite($siteId);
@ -122,7 +118,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->connection->setTimeout(0); $this->connection->setTimeout(0);
if ($stream) { if ($stream) {
$this->connection->exec($command, function ($output) use ($streamCallback) { $this->connection->exec($command, function ($output) use ($streamCallback) {
$this->log->write($output); $this->log?->write($output);
return $streamCallback($output); return $streamCallback($output);
}); });
@ -131,7 +127,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
} else { } else {
$output = ''; $output = '';
$this->connection->exec($command, function ($out) use (&$output) { $this->connection->exec($command, function ($out) use (&$output) {
$this->log->write($out); $this->log?->write($out);
$output .= $out; $output .= $out;
}); });

147
app/Models/File.php Normal file
View 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']);
}
}

View File

@ -5,6 +5,7 @@
use App\Actions\Server\CheckConnection; use App\Actions\Server\CheckConnection;
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Exceptions\SSHError;
use App\Facades\SSH; use App\Facades\SSH;
use App\ServerTypes\ServerType; use App\ServerTypes\ServerType;
use App\SSH\Cron\Cron; use App\SSH\Cron\Cron;
@ -22,6 +23,7 @@
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable;
/** /**
* @property int $project_id * @property int $project_id
@ -146,7 +148,7 @@ public static function boot(): void
} }
$server->provider()->delete(); $server->provider()->delete();
DB::commit(); DB::commit();
} catch (\Throwable $e) { } catch (Throwable $e) {
DB::rollBack(); DB::rollBack();
throw $e; throw $e;
} }
@ -465,6 +467,9 @@ public function cron(): Cron
return new Cron($this); return new Cron($this);
} }
/**
* @throws SSHError
*/
public function checkForUpdates(): void public function checkForUpdates(): void
{ {
$this->updates = $this->os()->availableUpdates(); $this->updates = $this->os()->availableUpdates();
@ -480,4 +485,15 @@ public function getAvailableUpdatesAttribute(?int $value): int
return $value; return $value;
} }
/**
* @throws Throwable
*/
public function download(string $path, string $disk = 'tmp'): void
{
$this->ssh()->download(
Storage::disk($disk)->path(basename($path)),
$path
);
}
} }

View File

@ -263,10 +263,14 @@ public function download(string $url, string $path): string
/** /**
* @throws SSHError * @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( $this->server->ssh($user)->exec(
'unzip '.$path view('ssh.os.extract', [
'path' => $path,
'destination' => $destination,
]),
'extract'
); );
} }
@ -304,9 +308,9 @@ public function resourceInfo(): array
/** /**
* @throws SSHError * @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', [ view('ssh.os.delete-file', [
'path' => $path, '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 private function deleteTempFile(string $name): void
{ {
if (Storage::disk('local')->exists($name)) { if (Storage::disk('local')->exists($name)) {

View File

@ -199,11 +199,13 @@ public function setupSSL(Ssl $ssl): void
*/ */
public function removeSSL(Ssl $ssl): void public function removeSSL(Ssl $ssl): void
{ {
$this->service->server->ssh()->exec( if ($ssl->certificate_path) {
'sudo rm -rf '.dirname($ssl->certificate_path).'*', $this->service->server->ssh()->exec(
'remove-ssl', 'sudo rm -rf '.dirname($ssl->certificate_path),
$ssl->site_id 'remove-ssl',
); $ssl->site_id
);
}
$this->updateVHost($ssl->site); $this->updateVHost($ssl->site);
} }

View File

@ -178,3 +178,31 @@ function get_from_route(string $modelName, string $routeKey): mixed
return null; 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;
}

View 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]],
];
}
}

View 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();
});
});
}
}

View File

@ -15,6 +15,7 @@
use App\Web\Pages\Servers\Console\Index as ConsoleIndex; use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex; use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex; 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\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex; use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex; use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
@ -59,6 +60,13 @@ public function getSubNavigation(): array
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server])); ->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])) { if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel()) $items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
->icon('icon-php-alt') ->icon('icon-php-alt')

View File

@ -62,7 +62,7 @@
'root' => storage_path('app/key-pairs'), 'root' => storage_path('app/key-pairs'),
], ],
'backups' => [ 'tmp' => [
'driver' => 'local', 'driver' => 'local',
'root' => sys_get_temp_dir(), 'root' => sys_get_temp_dir(),
], ],

View 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(),
];
}
}

View 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');
}
};

View File

@ -1 +1 @@
rm -f {{ $path }} rm -rf {{ $path }}

View 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

View 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();
}
}