Skip to content

Commit e2b9d18

Browse files
authored
Built-in File Manager (#458)
1 parent 75e554a commit e2b9d18

File tree

17 files changed

+907
-29
lines changed

17 files changed

+907
-29
lines changed

app/Actions/Database/ManageBackupFile.php

+5-9
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,12 @@ class ManageBackupFile
1515
*/
1616
public function download(BackupFile $file): StreamedResponse
1717
{
18-
$localFilename = "backup_{$file->id}_{$file->name}.zip";
18+
$file->backup->server->ssh()->download(
19+
Storage::disk('tmp')->path(basename($file->path())),
20+
$file->path()
21+
);
1922

20-
if (! Storage::disk('backups')->exists($localFilename)) {
21-
$file->backup->server->ssh()->download(
22-
Storage::disk('backups')->path($localFilename),
23-
$file->path()
24-
);
25-
}
26-
27-
return Storage::disk('backups')->download($localFilename, $file->name.'.zip');
23+
return Storage::disk('tmp')->download(basename($file->path()));
2824
}
2925

3026
public function delete(BackupFile $file): void
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Actions\FileManager;
4+
5+
use App\Exceptions\SSHError;
6+
use App\Models\File;
7+
use App\Models\Server;
8+
use App\Models\User;
9+
use Illuminate\Validation\Rule;
10+
11+
class FetchFiles
12+
{
13+
/**
14+
* @throws SSHError
15+
*/
16+
public function fetch(User $user, Server $server, array $input): void
17+
{
18+
File::parse(
19+
$user,
20+
$server,
21+
$input['path'],
22+
$input['user'],
23+
$server->os()->ls($input['path'], $input['user'])
24+
);
25+
}
26+
27+
public static function rules(Server $server): array
28+
{
29+
return [
30+
'path' => [
31+
'required',
32+
],
33+
'user' => [
34+
'required',
35+
Rule::in($server->getSshUsers()),
36+
],
37+
];
38+
}
39+
}

app/Helpers/SSH.php

+3-7
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,7 @@ public function connect(bool $sftp = false): void
9494
*/
9595
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
9696
{
97-
if (! $log) {
98-
$log = 'run-command';
99-
}
100-
101-
if (! $this->log) {
97+
if (! $this->log && $log) {
10298
$this->log = ServerLog::make($this->server, $log);
10399
if ($siteId) {
104100
$this->log->forSite($siteId);
@@ -122,7 +118,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
122118
$this->connection->setTimeout(0);
123119
if ($stream) {
124120
$this->connection->exec($command, function ($output) use ($streamCallback) {
125-
$this->log->write($output);
121+
$this->log?->write($output);
126122

127123
return $streamCallback($output);
128124
});
@@ -131,7 +127,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
131127
} else {
132128
$output = '';
133129
$this->connection->exec($command, function ($out) use (&$output) {
134-
$this->log->write($out);
130+
$this->log?->write($out);
135131
$output .= $out;
136132
});
137133

app/Models/File.php

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
/**
9+
* @property int $user_id
10+
* @property int $server_id
11+
* @property string $path
12+
* @property string $type
13+
* @property string $server_user
14+
* @property string $name
15+
* @property int $size
16+
* @property int $links
17+
* @property string $owner
18+
* @property string $group
19+
* @property string $date
20+
* @property string $permissions
21+
* @property User $user
22+
* @property Server $server
23+
*/
24+
class File extends AbstractModel
25+
{
26+
use HasFactory;
27+
28+
protected $fillable = [
29+
'user_id',
30+
'server_id',
31+
'server_user',
32+
'path',
33+
'type',
34+
'name',
35+
'size',
36+
'links',
37+
'owner',
38+
'group',
39+
'date',
40+
'permissions',
41+
];
42+
43+
protected $casts = [
44+
'user_id' => 'integer',
45+
'server_id' => 'integer',
46+
'size' => 'integer',
47+
'links' => 'integer',
48+
'created_at' => 'datetime',
49+
'updated_at' => 'datetime',
50+
];
51+
52+
protected static function boot(): void
53+
{
54+
parent::boot();
55+
56+
static::deleting(function (File $file) {
57+
if ($file->name === '.' || $file->name === '..') {
58+
return false;
59+
}
60+
61+
$file->server->os()->deleteFile($file->getFilePath(), $file->server_user);
62+
63+
return true;
64+
});
65+
}
66+
67+
public function server(): BelongsTo
68+
{
69+
return $this->belongsTo(Server::class);
70+
}
71+
72+
public function user(): BelongsTo
73+
{
74+
return $this->belongsTo(User::class);
75+
}
76+
77+
public static function path(User $user, Server $server, string $serverUser): string
78+
{
79+
$file = self::query()
80+
->where('user_id', $user->id)
81+
->where('server_id', $server->id)
82+
->where('server_user', $serverUser)
83+
->first();
84+
85+
if ($file) {
86+
return $file->path;
87+
}
88+
89+
return home_path($serverUser);
90+
}
91+
92+
public static function parse(User $user, Server $server, string $path, string $serverUser, string $listOutput): void
93+
{
94+
self::query()
95+
->where('user_id', $user->id)
96+
->where('server_id', $server->id)
97+
->delete();
98+
99+
// Split output by line
100+
$lines = explode("\n", trim($listOutput));
101+
102+
// Skip the first two lines (total count and . & .. directories)
103+
array_shift($lines);
104+
105+
foreach ($lines as $line) {
106+
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
107+
$type = match ($matches[1][0]) {
108+
'-' => 'file',
109+
'd' => 'directory',
110+
default => 'unknown',
111+
};
112+
if ($type === 'unknown') {
113+
continue;
114+
}
115+
if ($matches[7] === '.') {
116+
continue;
117+
}
118+
self::create([
119+
'user_id' => $user->id,
120+
'server_id' => $server->id,
121+
'server_user' => $serverUser,
122+
'path' => $path,
123+
'type' => $type,
124+
'name' => $matches[7],
125+
'size' => (int) $matches[5],
126+
'links' => (int) $matches[2],
127+
'owner' => $matches[3],
128+
'group' => $matches[4],
129+
'date' => $matches[6],
130+
'permissions' => $matches[1],
131+
]);
132+
}
133+
}
134+
}
135+
136+
public function getFilePath(): string
137+
{
138+
return $this->path.'/'.$this->name;
139+
}
140+
141+
public function isExtractable(): bool
142+
{
143+
$extension = pathinfo($this->name, PATHINFO_EXTENSION);
144+
145+
return in_array($extension, ['zip', 'tar', 'tar.gz', 'bz2', 'tar.bz2']);
146+
}
147+
}

app/Models/Server.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Actions\Server\CheckConnection;
66
use App\Enums\ServerStatus;
77
use App\Enums\ServiceStatus;
8+
use App\Exceptions\SSHError;
89
use App\Facades\SSH;
910
use App\ServerTypes\ServerType;
1011
use App\SSH\Cron\Cron;
@@ -22,6 +23,7 @@
2223
use Illuminate\Support\Facades\File;
2324
use Illuminate\Support\Facades\Storage;
2425
use Illuminate\Support\Str;
26+
use Throwable;
2527

2628
/**
2729
* @property int $project_id
@@ -146,7 +148,7 @@ public static function boot(): void
146148
}
147149
$server->provider()->delete();
148150
DB::commit();
149-
} catch (\Throwable $e) {
151+
} catch (Throwable $e) {
150152
DB::rollBack();
151153
throw $e;
152154
}
@@ -465,6 +467,9 @@ public function cron(): Cron
465467
return new Cron($this);
466468
}
467469

470+
/**
471+
* @throws SSHError
472+
*/
468473
public function checkForUpdates(): void
469474
{
470475
$this->updates = $this->os()->availableUpdates();
@@ -480,4 +485,15 @@ public function getAvailableUpdatesAttribute(?int $value): int
480485

481486
return $value;
482487
}
488+
489+
/**
490+
* @throws Throwable
491+
*/
492+
public function download(string $path, string $disk = 'tmp'): void
493+
{
494+
$this->ssh()->download(
495+
Storage::disk($disk)->path(basename($path)),
496+
$path
497+
);
498+
}
483499
}

app/SSH/OS/OS.php

+36-5
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,14 @@ public function download(string $url, string $path): string
263263
/**
264264
* @throws SSHError
265265
*/
266-
public function unzip(string $path): string
266+
public function extract(string $path, ?string $destination = null, ?string $user = null): void
267267
{
268-
return $this->server->ssh()->exec(
269-
'unzip '.$path
268+
$this->server->ssh($user)->exec(
269+
view('ssh.os.extract', [
270+
'path' => $path,
271+
'destination' => $destination,
272+
]),
273+
'extract'
270274
);
271275
}
272276

@@ -304,16 +308,43 @@ public function resourceInfo(): array
304308
/**
305309
* @throws SSHError
306310
*/
307-
public function deleteFile(string $path): void
311+
public function deleteFile(string $path, ?string $user = null): void
308312
{
309-
$this->server->ssh()->exec(
313+
$this->server->ssh($user)->exec(
310314
view('ssh.os.delete-file', [
311315
'path' => $path,
312316
]),
313317
'delete-file'
314318
);
315319
}
316320

321+
/**
322+
* @throws SSHError
323+
*/
324+
public function ls(string $path, ?string $user = null): string
325+
{
326+
return $this->server->ssh($user)->exec('ls -la '.$path);
327+
}
328+
329+
/**
330+
* @throws SSHError
331+
*/
332+
public function write(string $path, string $content, ?string $user = null): void
333+
{
334+
$this->server->ssh($user)->write(
335+
$path,
336+
$content
337+
);
338+
}
339+
340+
/**
341+
* @throws SSHError
342+
*/
343+
public function mkdir(string $path, ?string $user = null): string
344+
{
345+
return $this->server->ssh($user)->exec('mkdir -p '.$path);
346+
}
347+
317348
private function deleteTempFile(string $name): void
318349
{
319350
if (Storage::disk('local')->exists($name)) {

app/SSH/Services/Webserver/Nginx.php

+7-5
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,13 @@ public function setupSSL(Ssl $ssl): void
199199
*/
200200
public function removeSSL(Ssl $ssl): void
201201
{
202-
$this->service->server->ssh()->exec(
203-
'sudo rm -rf '.dirname($ssl->certificate_path).'*',
204-
'remove-ssl',
205-
$ssl->site_id
206-
);
202+
if ($ssl->certificate_path) {
203+
$this->service->server->ssh()->exec(
204+
'sudo rm -rf '.dirname($ssl->certificate_path),
205+
'remove-ssl',
206+
$ssl->site_id
207+
);
208+
}
207209

208210
$this->updateVHost($ssl->site);
209211
}

0 commit comments

Comments
 (0)