|
1 | 1 | # Standard library imports |
2 | 2 | import os |
3 | 3 | import time |
| 4 | +import shutil |
4 | 5 |
|
5 | 6 | # Third party imports |
6 | 7 | import flask |
7 | 8 | import werkzeug |
| 9 | +import zipfile |
8 | 10 | from opengeodeweb_microservice.schemas import get_schemas_dict |
9 | 11 |
|
10 | 12 | # Local application imports |
11 | 13 | from opengeodeweb_back import geode_functions, utils_functions |
12 | 14 | from .models import blueprint_models |
13 | 15 | from . import schemas |
| 16 | +from opengeodeweb_microservice.database.data import Data |
| 17 | +from opengeodeweb_microservice.database.connection import get_session |
| 18 | +from opengeodeweb_microservice.database import connection |
14 | 19 |
|
15 | 20 | routes = flask.Blueprint("routes", __name__, url_prefix="/opengeodeweb_back") |
16 | 21 |
|
@@ -268,3 +273,141 @@ def kill() -> flask.Response: |
268 | 273 | print("Manual server kill, shutting down...", flush=True) |
269 | 274 | os._exit(0) |
270 | 275 | return flask.make_response({"message": "Flask server is dead"}, 200) |
| 276 | + |
| 277 | + |
| 278 | +@routes.route( |
| 279 | + schemas_dict["export_project"]["route"], |
| 280 | + methods=schemas_dict["export_project"]["methods"], |
| 281 | +) |
| 282 | +def export_project() -> flask.Response: |
| 283 | + utils_functions.validate_request(flask.request, schemas_dict["export_project"]) |
| 284 | + params = schemas.ExportProject.from_dict(flask.request.get_json()) |
| 285 | + |
| 286 | + project_folder: str = flask.current_app.config["DATA_FOLDER_PATH"] |
| 287 | + os.makedirs(project_folder, exist_ok=True) |
| 288 | + |
| 289 | + filename: str = werkzeug.utils.secure_filename(os.path.basename(params.filename)) |
| 290 | + if not filename.lower().endswith(".vease"): |
| 291 | + flask.abort(400, "Requested filename must end with .vease") |
| 292 | + export_vease_path = os.path.join(project_folder, filename) |
| 293 | + |
| 294 | + with get_session() as session: |
| 295 | + rows = session.query(Data.id, Data.input_file, Data.additional_files).all() |
| 296 | + |
| 297 | + with zipfile.ZipFile( |
| 298 | + export_vease_path, "w", compression=zipfile.ZIP_DEFLATED |
| 299 | + ) as zip_file: |
| 300 | + database_root_path = os.path.join(project_folder, "project.db") |
| 301 | + if os.path.isfile(database_root_path): |
| 302 | + zip_file.write(database_root_path, "project.db") |
| 303 | + |
| 304 | + for data_id, input_file, additional_files in rows: |
| 305 | + base_dir = os.path.join(project_folder, data_id) |
| 306 | + |
| 307 | + input_path = os.path.join(base_dir, str(input_file)) |
| 308 | + if os.path.isfile(input_path): |
| 309 | + zip_file.write(input_path, os.path.join(data_id, str(input_file))) |
| 310 | + |
| 311 | + for relative_path in ( |
| 312 | + additional_files if isinstance(additional_files, list) else [] |
| 313 | + ): |
| 314 | + additional_path = os.path.join(base_dir, relative_path) |
| 315 | + if os.path.isfile(additional_path): |
| 316 | + zip_file.write( |
| 317 | + additional_path, os.path.join(data_id, relative_path) |
| 318 | + ) |
| 319 | + |
| 320 | + zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) |
| 321 | + |
| 322 | + return utils_functions.send_file(project_folder, [export_vease_path], filename) |
| 323 | + |
| 324 | + |
| 325 | +@routes.route( |
| 326 | + schemas_dict["import_project"]["route"], |
| 327 | + methods=schemas_dict["import_project"]["methods"], |
| 328 | +) |
| 329 | +def import_project() -> flask.Response: |
| 330 | + utils_functions.validate_request(flask.request, schemas_dict["import_project"]) |
| 331 | + if "file" not in flask.request.files: |
| 332 | + flask.abort(400, "No .vease file provided under 'file'") |
| 333 | + zip_file = flask.request.files["file"] |
| 334 | + assert zip_file.filename is not None |
| 335 | + filename = werkzeug.utils.secure_filename(os.path.basename(zip_file.filename)) |
| 336 | + if not filename.lower().endswith(".vease"): |
| 337 | + flask.abort(400, "Uploaded file must be a .vease") |
| 338 | + |
| 339 | + data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] |
| 340 | + |
| 341 | + # 423 Locked bypass : remove stopped requests |
| 342 | + if connection.scoped_session_registry: |
| 343 | + connection.scoped_session_registry.remove() |
| 344 | + if connection.engine: |
| 345 | + connection.engine.dispose() |
| 346 | + connection.engine = connection.session_factory = ( |
| 347 | + connection.scoped_session_registry |
| 348 | + ) = None |
| 349 | + |
| 350 | + try: |
| 351 | + if os.path.exists(data_folder_path): |
| 352 | + shutil.rmtree(data_folder_path) |
| 353 | + os.makedirs(data_folder_path, exist_ok=True) |
| 354 | + except PermissionError: |
| 355 | + flask.abort(423, "Project files are locked; cannot overwrite") |
| 356 | + |
| 357 | + zip_file.stream.seek(0) |
| 358 | + with zipfile.ZipFile(zip_file.stream) as zip_archive: |
| 359 | + project_folder = os.path.abspath(data_folder_path) |
| 360 | + for member in zip_archive.namelist(): |
| 361 | + target = os.path.abspath( |
| 362 | + os.path.normpath(os.path.join(project_folder, member)) |
| 363 | + ) |
| 364 | + if not ( |
| 365 | + target == project_folder or target.startswith(project_folder + os.sep) |
| 366 | + ): |
| 367 | + flask.abort(400, "Vease file contains unsafe paths") |
| 368 | + zip_archive.extractall(project_folder) |
| 369 | + |
| 370 | + database_root_path = os.path.join(project_folder, "project.db") |
| 371 | + if not os.path.isfile(database_root_path): |
| 372 | + flask.abort(400, "Missing project.db at project root") |
| 373 | + |
| 374 | + connection.init_database(database_root_path, create_tables=False) |
| 375 | + |
| 376 | + try: |
| 377 | + with get_session() as session: |
| 378 | + rows = session.query(Data).all() |
| 379 | + except Exception: |
| 380 | + connection.init_database(database_root_path, create_tables=True) |
| 381 | + with get_session() as session: |
| 382 | + rows = session.query(Data).all() |
| 383 | + |
| 384 | + with get_session() as session: |
| 385 | + for data_entry in rows: |
| 386 | + data_path = geode_functions.data_file_path(data_entry.id) |
| 387 | + viewable_name = data_entry.viewable_file_name |
| 388 | + if viewable_name: |
| 389 | + vpath = geode_functions.data_file_path(data_entry.id, viewable_name) |
| 390 | + if os.path.isfile(vpath): |
| 391 | + continue |
| 392 | + |
| 393 | + input_file = str(data_entry.input_file or "") |
| 394 | + if not input_file: |
| 395 | + continue |
| 396 | + |
| 397 | + input_full = geode_functions.data_file_path(data_entry.id, input_file) |
| 398 | + if not os.path.isfile(input_full): |
| 399 | + continue |
| 400 | + |
| 401 | + data_object = geode_functions.load(data_entry.geode_object, input_full) |
| 402 | + utils_functions.save_all_viewables_and_return_info( |
| 403 | + data_entry.geode_object, data_object, data_entry, data_path |
| 404 | + ) |
| 405 | + session.commit() |
| 406 | + |
| 407 | + snapshot = {} |
| 408 | + try: |
| 409 | + raw = zip_archive.read("snapshot.json").decode("utf-8") |
| 410 | + snapshot = flask.json.loads(raw) |
| 411 | + except KeyError: |
| 412 | + snapshot = {} |
| 413 | + return flask.make_response({"snapshot": snapshot}, 200) |
0 commit comments