159 lines
5.0 KiB
PHP
159 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Helpers\ProfileAuthorizationHelper;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
use ZipArchive;
|
|
|
|
class BackupChunkUploadController extends Controller
|
|
{
|
|
/**
|
|
* Receive a single chunk of the backup file.
|
|
* POST /posts/backup-upload/chunk
|
|
*/
|
|
public function uploadChunk(Request $request): JsonResponse
|
|
{
|
|
$this->authorizeAdminAccess();
|
|
|
|
$request->validate([
|
|
'uploadId' => 'required|string|uuid',
|
|
'chunkIndex' => 'required|integer|min:0|max:999',
|
|
'totalChunks' => 'required|integer|min:1|max:1000',
|
|
'chunk' => 'required|file|max:3072', // 3MB max per chunk
|
|
]);
|
|
|
|
$uploadId = $request->input('uploadId');
|
|
$chunkIndex = (int) $request->input('chunkIndex');
|
|
$totalChunks = (int) $request->input('totalChunks');
|
|
|
|
if ($chunkIndex >= $totalChunks) {
|
|
return response()->json(['error' => 'Invalid chunk index'], 422);
|
|
}
|
|
|
|
$chunkDir = storage_path("app/temp/chunks/{$uploadId}");
|
|
if (!File::isDirectory($chunkDir)) {
|
|
File::makeDirectory($chunkDir, 0755, true);
|
|
}
|
|
|
|
$request->file('chunk')->move($chunkDir, "chunk_{$chunkIndex}");
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'chunkIndex' => $chunkIndex,
|
|
'received' => count(File::files($chunkDir)),
|
|
'total' => $totalChunks,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Finalize: reassemble chunks into the complete file.
|
|
* POST /posts/backup-upload/finalize
|
|
*/
|
|
public function finalize(Request $request): JsonResponse
|
|
{
|
|
$this->authorizeAdminAccess();
|
|
|
|
$request->validate([
|
|
'uploadId' => 'required|string|uuid',
|
|
'totalChunks' => 'required|integer|min:1|max:1000',
|
|
'fileName' => 'required|string|max:255',
|
|
]);
|
|
|
|
$uploadId = $request->input('uploadId');
|
|
$totalChunks = (int) $request->input('totalChunks');
|
|
$fileName = basename($request->input('fileName'));
|
|
|
|
$chunkDir = storage_path("app/temp/chunks/{$uploadId}");
|
|
$outputDir = storage_path('app/temp');
|
|
|
|
if (!File::isDirectory($outputDir)) {
|
|
File::makeDirectory($outputDir, 0755, true);
|
|
}
|
|
|
|
// Verify all chunks exist
|
|
for ($i = 0; $i < $totalChunks; $i++) {
|
|
if (!File::exists("{$chunkDir}/chunk_{$i}")) {
|
|
return response()->json(['error' => "Missing chunk {$i}"], 422);
|
|
}
|
|
}
|
|
|
|
// Reassemble
|
|
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, ['zip', 'json'])) {
|
|
File::deleteDirectory($chunkDir);
|
|
return response()->json(['error' => 'Invalid file type'], 422);
|
|
}
|
|
|
|
$assembledName = 'restore_' . uniqid() . '.' . $ext;
|
|
$assembledPath = "{$outputDir}/{$assembledName}";
|
|
|
|
$output = fopen($assembledPath, 'wb');
|
|
for ($i = 0; $i < $totalChunks; $i++) {
|
|
$chunkPath = "{$chunkDir}/chunk_{$i}";
|
|
$input = fopen($chunkPath, 'rb');
|
|
stream_copy_to_stream($input, $output);
|
|
fclose($input);
|
|
}
|
|
fclose($output);
|
|
|
|
// Clean up chunks
|
|
File::deleteDirectory($chunkDir);
|
|
|
|
// Validate ZIP if applicable
|
|
if ($ext === 'zip') {
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($assembledPath) !== true) {
|
|
@unlink($assembledPath);
|
|
return response()->json(['error' => 'Invalid ZIP file'], 422);
|
|
}
|
|
if ($zip->getFromName('backup.json') === false) {
|
|
$zip->close();
|
|
@unlink($assembledPath);
|
|
return response()->json(['error' => 'ZIP does not contain backup.json'], 422);
|
|
}
|
|
$zip->close();
|
|
}
|
|
|
|
// Store path in session for Livewire to retrieve
|
|
session(["backup_restore_file_{$uploadId}" => $assembledPath]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'uploadId' => $uploadId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Authorization check matching RequiresAdminAuthorization trait logic.
|
|
*/
|
|
private function authorizeAdminAccess(): void
|
|
{
|
|
$activeProfileType = session('activeProfileType');
|
|
$activeProfileId = session('activeProfileId');
|
|
|
|
if (!$activeProfileType || !$activeProfileId) {
|
|
abort(403, 'No active profile selected');
|
|
}
|
|
|
|
$profile = $activeProfileType::find($activeProfileId);
|
|
if (!$profile) {
|
|
abort(403, 'Active profile not found');
|
|
}
|
|
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
if ($profile instanceof \App\Models\Admin) {
|
|
return;
|
|
}
|
|
if ($profile instanceof \App\Models\Bank && $profile->level === 0) {
|
|
return;
|
|
}
|
|
|
|
abort(403, 'Admin or central bank access required');
|
|
}
|
|
}
|