Add allowlist of backup file extensions (sql, sql.gz, tar, tgz, zip, dump, bak, bson, archive, bz2, xz, and compound variants) and enforce a 10 GiB maximum file size on the backup upload endpoint. Validation runs early on each chunk using the dropzone metadata and again on the assembled file. Also drops the unused createFilename helper and the commented-out S3 block. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
129 lines
4 KiB
PHP
129 lines
4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Routing\Controller as BaseController;
|
|
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
|
|
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
|
|
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
|
|
|
|
class UploadController extends BaseController
|
|
{
|
|
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
|
|
|
|
private const ALLOWED_EXTENSIONS = [
|
|
'sql',
|
|
'sql.gz',
|
|
'gz',
|
|
'zip',
|
|
'tar',
|
|
'tar.gz',
|
|
'tgz',
|
|
'dump',
|
|
'bak',
|
|
'bson',
|
|
'bson.gz',
|
|
'archive',
|
|
'archive.gz',
|
|
'bz2',
|
|
'xz',
|
|
];
|
|
|
|
public function upload(Request $request)
|
|
{
|
|
$databaseIdentifier = request()->route('databaseUuid');
|
|
$resource = getResourceByUuid($databaseIdentifier, data_get(auth()->user()->currentTeam(), 'id'));
|
|
if (is_null($resource)) {
|
|
return response()->json(['error' => 'You do not have permission for this database'], 500);
|
|
}
|
|
|
|
$chunk = $request->file('file');
|
|
$originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null;
|
|
if (blank($originalName) || ! self::hasAllowedExtension($originalName)) {
|
|
return response()->json([
|
|
'error' => 'Unsupported file type. Allowed extensions: '.implode(', ', self::ALLOWED_EXTENSIONS),
|
|
], 422);
|
|
}
|
|
|
|
$declaredTotalSize = (int) $request->input('dzTotalFilesize', 0);
|
|
if ($declaredTotalSize > self::MAX_BYTES) {
|
|
return response()->json([
|
|
'error' => 'File exceeds maximum allowed size of '.self::formatMaxSize().'.',
|
|
], 422);
|
|
}
|
|
|
|
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
|
|
|
|
if ($receiver->isUploaded() === false) {
|
|
throw new UploadMissingFileException;
|
|
}
|
|
|
|
$save = $receiver->receive();
|
|
|
|
if ($save->isFinished()) {
|
|
// Use the original identifier from the route to maintain path consistency
|
|
// For ServiceDatabase: {name}-{service_uuid}
|
|
// For standalone databases: {uuid}
|
|
return $this->saveFile($save->getFile(), $databaseIdentifier);
|
|
}
|
|
|
|
$handler = $save->handler();
|
|
|
|
return response()->json([
|
|
'done' => $handler->getPercentageDone(),
|
|
'status' => true,
|
|
]);
|
|
}
|
|
|
|
protected function saveFile(UploadedFile $file, string $resourceIdentifier)
|
|
{
|
|
$originalName = $file->getClientOriginalName();
|
|
$size = $file->getSize();
|
|
|
|
if (! self::hasAllowedExtension($originalName) || $size === false || $size > self::MAX_BYTES) {
|
|
@unlink($file->getPathname());
|
|
|
|
return response()->json([
|
|
'error' => 'Uploaded file failed validation.',
|
|
], 422);
|
|
}
|
|
|
|
$mime = str_replace('/', '-', $file->getMimeType());
|
|
$filePath = "upload/{$resourceIdentifier}";
|
|
$finalPath = storage_path('app/'.$filePath);
|
|
$file->move($finalPath, 'restore');
|
|
|
|
return response()->json([
|
|
'mime_type' => $mime,
|
|
]);
|
|
}
|
|
|
|
private static function hasAllowedExtension(string $name): bool
|
|
{
|
|
$lower = strtolower($name);
|
|
$suffixes = array_map(fn ($ext) => '.'.$ext, self::ALLOWED_EXTENSIONS);
|
|
usort($suffixes, fn ($a, $b) => strlen($b) <=> strlen($a));
|
|
|
|
foreach ($suffixes as $suffix) {
|
|
if (! str_ends_with($lower, $suffix)) {
|
|
continue;
|
|
}
|
|
|
|
$stem = substr($lower, 0, -strlen($suffix));
|
|
if ($stem !== '' && ! str_ends_with($stem, '.')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static function formatMaxSize(): string
|
|
{
|
|
return (self::MAX_BYTES / (1024 * 1024 * 1024)).' GiB';
|
|
}
|
|
}
|