Skip to content

Commit 9c7f40e

Browse files
committed
feat: upload large backups
1 parent f35b7ab commit 9c7f40e

File tree

9 files changed

+287
-75
lines changed

9 files changed

+287
-75
lines changed
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Routing\Controller as BaseController;
6+
use Illuminate\Http\JsonResponse;
7+
use Pion\Laravel\ChunkUpload\Exceptions\UploadFailedException;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\UploadedFile;
10+
use Illuminate\Support\Facades\Storage;
11+
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
12+
use Pion\Laravel\ChunkUpload\Handler\AbstractHandler;
13+
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
14+
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
15+
16+
class UploadController extends BaseController
17+
{
18+
public function upload(Request $request)
19+
{
20+
$resource = getResourceByUuid(request()->route('databaseUuid'), data_get(auth()->user()->currentTeam(), 'id'));
21+
if (is_null($resource)) {
22+
return response()->json(['error' => 'You do not have permission for this database'], 500);
23+
}
24+
$receiver = new FileReceiver("file", $request, HandlerFactory::classFromRequest($request));
25+
26+
if ($receiver->isUploaded() === false) {
27+
throw new UploadMissingFileException();
28+
}
29+
30+
$save = $receiver->receive();
31+
32+
if ($save->isFinished()) {
33+
return $this->saveFile($save->getFile(), $resource);
34+
}
35+
36+
$handler = $save->handler();
37+
return response()->json([
38+
"done" => $handler->getPercentageDone(),
39+
'status' => true
40+
]);
41+
}
42+
// protected function saveFileToS3($file)
43+
// {
44+
// $fileName = $this->createFilename($file);
45+
46+
// $disk = Storage::disk('s3');
47+
// // It's better to use streaming Streaming (laravel 5.4+)
48+
// $disk->putFileAs('photos', $file, $fileName);
49+
50+
// // for older laravel
51+
// // $disk->put($fileName, file_get_contents($file), 'public');
52+
// $mime = str_replace('/', '-', $file->getMimeType());
53+
54+
// // We need to delete the file when uploaded to s3
55+
// unlink($file->getPathname());
56+
57+
// return response()->json([
58+
// 'path' => $disk->url($fileName),
59+
// 'name' => $fileName,
60+
// 'mime_type' => $mime
61+
// ]);
62+
// }
63+
protected function saveFile(UploadedFile $file, $resource)
64+
{
65+
$mime = str_replace('/', '-', $file->getMimeType());
66+
$filePath = "upload/{$resource->uuid}";
67+
$finalPath = storage_path("app/" . $filePath);
68+
$file->move($finalPath, 'restore');
69+
70+
return response()->json([
71+
'mime_type' => $mime
72+
]);
73+
}
74+
protected function createFilename(UploadedFile $file)
75+
{
76+
$extension = $file->getClientOriginalExtension();
77+
$filename = str_replace("." . $extension, "", $file->getClientOriginalName()); // Filename without extension
78+
79+
$filename .= "_" . md5(time()) . "." . $extension;
80+
81+
return $filename;
82+
}
83+
}

app/Livewire/Project/Database/Import.php

+20-26
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
namespace App\Livewire\Project\Database;
44

55
use Livewire\Component;
6-
use Livewire\WithFileUploads;
76
use App\Models\Server;
87
use Illuminate\Support\Facades\Storage;
98

109
class Import extends Component
1110
{
12-
use WithFileUploads;
1311

14-
public $file;
1512
public $resource;
1613
public $parameters;
1714
public $containers;
18-
public bool $validated = true;
1915
public bool $scpInProgress = false;
2016
public bool $importRunning = false;
21-
public string $validationMsg = '';
17+
18+
public ?string $filename = null;
19+
public ?string $filesize = null;
20+
public bool $isUploading = false;
21+
public int $progress = 0;
22+
public bool $error = false;
23+
2224
public Server $server;
2325
public string $container;
2426
public array $importCommands = [];
@@ -45,7 +47,7 @@ public function getContainers()
4547
if (!data_get($this->parameters, 'database_uuid')) {
4648
abort(404);
4749
}
48-
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(),'id'));
50+
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
4951
if (is_null($resource)) {
5052
abort(404);
5153
}
@@ -56,41 +58,34 @@ public function getContainers()
5658
$this->containers->push($this->container);
5759
}
5860

59-
if ($this->containers->count() > 1) {
60-
$this->validated = false;
61-
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
62-
}
63-
6461
if (
6562
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
6663
$this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' ||
6764
$this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' ||
6865
$this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' ||
6966
$this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
7067
) {
71-
$this->validated = false;
72-
$this->validationMsg = 'This database type is not currently supported.';
68+
$this->dispatch('error', 'Import is not supported for this resource.');
7369
}
7470
}
7571

7672
public function runImport()
7773
{
78-
$this->validate([
79-
'file' => 'required|file|max:102400'
80-
]);
81-
82-
$this->importRunning = true;
83-
$this->scpInProgress = true;
8474

75+
if ($this->filename == '') {
76+
$this->dispatch('error', 'Please select a file to import.');
77+
return;
78+
}
8579
try {
86-
$uploadedFilename = $this->file->store('backup-import');
80+
$uploadedFilename = "upload/{$this->resource->uuid}/restore";
8781
$path = Storage::path($uploadedFilename);
82+
if (!Storage::exists($uploadedFilename)) {
83+
$this->dispatch('error', 'The file does not exist or has been deleted.');
84+
return;
85+
}
8886
$tmpPath = '/tmp/' . basename($uploadedFilename);
89-
90-
// SCP the backup file to the server.
9187
instant_scp($path, $tmpPath, $this->server);
92-
$this->scpInProgress = false;
93-
88+
Storage::delete($uploadedFilename);
9489
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
9590

9691
switch ($this->resource->getMorphClass()) {
@@ -116,8 +111,7 @@ public function runImport()
116111
$this->dispatch('activityMonitor', $activity->id);
117112
}
118113
} catch (\Throwable $e) {
119-
$this->validated = false;
120-
$this->validationMsg = $e->getMessage();
114+
return handleError($e, $this);
121115
}
122116
}
123117
}

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"lorisleiva/laravel-actions": "^2.7",
2828
"nubs/random-name-generator": "^2.2",
2929
"phpseclib/phpseclib": "~3.0",
30+
"pion/laravel-chunk-upload": "^1.5",
3031
"poliander/cron": "^3.0",
3132
"purplepixie/phpdns": "^2.1",
3233
"pusher/pusher-php-server": "^7.2",

composer.lock

+67-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/chunk-upload.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
/**
3+
* @see https://github.com/pionl/laravel-chunk-upload
4+
*/
5+
6+
return [
7+
/*
8+
* The storage config
9+
*/
10+
'storage' => [
11+
/*
12+
* Returns the folder name of the chunks. The location is in storage/app/{folder_name}
13+
*/
14+
'chunks' => 'chunks',
15+
'disk' => 'local',
16+
],
17+
'clear' => [
18+
/*
19+
* How old chunks we should delete
20+
*/
21+
'timestamp' => '-1 HOURS',
22+
'schedule' => [
23+
'enabled' => true,
24+
'cron' => '25 * * * *', // run every hour on the 25th minute
25+
],
26+
],
27+
'chunk' => [
28+
// setup for the chunk naming setup to ensure same name upload at same time
29+
'name' => [
30+
'use' => [
31+
'session' => true, // should the chunk name use the session id? The uploader must send cookie!,
32+
'browser' => false, // instead of session we can use the ip and browser?
33+
],
34+
],
35+
],
36+
'handlers' => [
37+
// A list of handlers/providers that will be appended to existing list of handlers
38+
'custom' => [],
39+
// Overrides the list of handlers - use only what you really want
40+
'override' => [
41+
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
42+
],
43+
],
44+
];

resources/css/app.css

+4
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,7 @@ tr td:first-child {
289289
.toast {
290290
z-index: 1;
291291
}
292+
293+
.dz-button {
294+
@apply w-full p-4 py-10 my-4 font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent hover:dark:bg-coolgray-400;
295+
}

resources/views/livewire/project/database/backup-executions.blade.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ class="relative flex flex-col p-4 bg-white box-without-bg dark:bg-coolgray-100"
3434
</x-modal-confirmation>
3535
</div>
3636
</form>
37-
<script>
38-
function download_file(executionId) {
39-
window.open('/download/backup/' + executionId, '_blank');
40-
}
41-
</script>
37+
4238
@empty
4339
<div>No executions found.</div>
4440
@endforelse
41+
<script>
42+
function download_file(executionId) {
43+
window.open('/download/backup/' + executionId, '_blank');
44+
}
45+
</script>
4546
</div>

0 commit comments

Comments
 (0)