authorize('uploadBackup', $resource); $validator = Validator::make($request->all(), ['upload_id' => ['required', 'uuid'], 'file' => ['required', 'file']]); if ($validator->fails()) { return response()->json(['message' => 'Validation failed.', 'errors' => $validator->errors()], 422); } $originalName = $request->file('file')?->getClientOriginalName(); if (! $originalName || ! DatabaseBackupFileValidator::hasAllowedExtension($originalName)) { return response()->json(['message' => 'Validation failed.', 'errors' => ['file' => ['Unsupported backup file extension.']]], 422); } if ((int) $request->input('dzTotalFilesize', 0) > StartDatabaseImport::MAX_BYTES) { return response()->json(['message' => 'Validation failed.', 'errors' => ['file' => ['The backup exceeds the 10 GiB limit.']]], 422); } $request->merge(['dzuuid' => $request->input('dzuuid', $request->string('upload_id')->value())]); $receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request)); $save = $receiver->receive(); if (! $save->isFinished()) { return response()->json(['upload_id' => $request->string('upload_id')->value(), 'done' => $save->handler()->getPercentageDone(), 'status' => true]); } $file = $save->getFile(); if (! $file instanceof UploadedFile || ! DatabaseBackupFileValidator::isUploadAllowed($file, StartDatabaseImport::MAX_BYTES)) { @unlink($file->getPathname()); return response()->json(['message' => 'Validation failed.', 'errors' => ['file' => ['Uploaded file failed validation.']]], 422); } $mimeType = $file->getMimeType(); $size = $file->getSize(); $directory = "upload/imports/{$teamId}/{$resource->uuid}/{$request->string('upload_id')->value()}"; Storage::makeDirectory($directory); $file->move(Storage::path($directory), 'restore'); return response()->json(['upload_id' => $request->string('upload_id')->value(), 'filename' => $originalName, 'mime_type' => $mimeType, 'size' => $size], 201); } protected function startDatabaseImport(Request $request, Model $resource, int $teamId, string $statusRoute, array $routeParameters): JsonResponse { $this->authorize('update', $resource); $payload = $request->json()->all() ?: $request->request->all(); $allowed = ['source', 'upload_id', 's3_storage_uuid', 'path', 'dump_all', 'replace_existing', 'keep_owners', 'restore_mysql_users', 'sqlite_database']; $validator = Validator::make($payload, [ 'source' => ['required', Rule::in(['upload', 's3', 'server'])], 'upload_id' => ['required_if:source,upload', 'prohibited_unless:source,upload', 'uuid'], 's3_storage_uuid' => ['required_if:source,s3', 'prohibited_unless:source,s3', 'string'], 'path' => ['required_if:source,s3,server', 'prohibited_if:source,upload', 'string', 'max:4096'], 'dump_all' => ['sometimes', 'boolean'], 'replace_existing' => ['sometimes', 'boolean'], 'keep_owners' => ['sometimes', 'boolean'], 'restore_mysql_users' => ['sometimes', 'boolean'], 'sqlite_database' => ['sometimes', 'string', 'max:255'], ]); $extraFields = array_diff(array_keys($payload), $allowed); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); foreach ($extraFields as $field) { $errors->add($field, 'This field is not allowed.'); } return response()->json(['message' => 'Validation failed.', 'errors' => $errors], 422); } if ($response = $this->deployAbilityForbiddenResponse($request, 'An import runs the dump in the database container, so it needs a token with the deploy permission.')) { return $response; } try { $source = new DatabaseImportSource((string) $payload['source'], $payload['upload_id'] ?? null, $payload['path'] ?? null, $payload['s3_storage_uuid'] ?? null, (bool) ($payload['dump_all'] ?? false), (bool) ($payload['replace_existing'] ?? false), (bool) ($payload['keep_owners'] ?? false), isset($payload['sqlite_database']) ? (string) $payload['sqlite_database'] : null, (bool) ($payload['restore_mysql_users'] ?? false)); $activity = app(StartDatabaseImport::class)->handle($resource, $source, $teamId); } catch (DatabaseImportException $exception) { return response()->json(['message' => $exception->getMessage()], $exception->status); } auditLog('api.database.import_started', [ 'team_id' => $teamId, 'database_uuid' => $resource->uuid, 'database_name' => $resource->name, 'source' => $source->type, 'replace_existing' => $source->replaceExisting, 'keep_owners' => $source->keepOwners, 'restore_mysql_users' => $source->restoreMysqlUsers, 'activity_id' => $activity->id, ]); $url = route($statusRoute, [...$routeParameters, 'activity_id' => $activity->id], false); return response()->json(['id' => $activity->id, 'status' => data_get($activity, 'properties.status'), 'message' => 'Database import queued.', 'status_url' => $url], 202)->header('Location', $url); } protected function showDatabaseImport(Model $resource, int $teamId, int $activityId): JsonResponse { $this->authorize('view', $resource); $activity = Activity::query()->whereKey($activityId) ->where('properties->team_id', $teamId) ->where('properties->type_uuid', $resource->uuid) ->where('properties->operation', 'database_import')->first(); if (! $activity) { return response()->json(['message' => 'Database import not found.'], 404); } $status = data_get($activity, 'properties.status'); $terminal = in_array($status, ['finished', 'error', 'killed', 'cancelled', 'closed'], true); return response()->json([ 'id' => $activity->id, 'status' => $status, 'exit_code' => data_get($activity, 'properties.exitCode'), 'output' => request()->attributes->get('can_read_sensitive', false) === true && request()->user()->can('update', $resource) ? remove_iip(RunRemoteProcess::decodeOutput($activity)) : null, 'created_at' => $activity->created_at, 'updated_at' => $activity->updated_at, 'finished_at' => $terminal ? $activity->updated_at : null, ]); } }