Initial commit
This commit is contained in:
158
app/Http/Controllers/BackupChunkUploadController.php
Normal file
158
app/Http/Controllers/BackupChunkUploadController.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user