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'); } }