From ac85cf447e60ec59c38d72babd32f42bb13a4d80 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Oct 2025 13:45:35 +0100 Subject: [PATCH 01/31] feat(export_and_import_routes): add export, import route and schemas for save and load feature --- .../routes/blueprint_routes.py | 28 ++++++++++++ .../routes/schemas/__init__.py | 1 + .../routes/schemas/export_project.json | 20 +++++++++ .../routes/schemas/export_project.py | 9 ++++ src/opengeodeweb_back/utils_functions.py | 6 ++- tests/test_models_routes.py | 26 +++++++++++ tests/test_utils_functions.py | 45 +++++++++++++++++++ 7 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/opengeodeweb_back/routes/schemas/export_project.json create mode 100644 src/opengeodeweb_back/routes/schemas/export_project.py diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 1f17f826..32817fd7 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -5,6 +5,8 @@ # Third party imports import flask import werkzeug +import zipfile +import glob from opengeodeweb_microservice.schemas import get_schemas_dict # Local application imports @@ -267,3 +269,29 @@ def kill() -> flask.Response: print("Manual server kill, shutting down...", flush=True) os._exit(0) return flask.make_response({"message": "Flask server is dead"}, 200) + + +@routes.route( + schemas_dict["export_project"]["route"], + methods=schemas_dict["export_project"]["methods"], +) +def export_project() -> flask.Response: + utils_functions.validate_request(flask.request, schemas_dict["export_project"]) + params = schemas.ExportProject.from_dict(flask.request.get_json()) + + data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] + upload_folder: str = flask.current_app.config["UPLOAD_FOLDER"] + os.makedirs(upload_folder, exist_ok=True) + + filename: str = params.filename or f"project_{int(time.time())}.zip" + export_zip_path = os.path.join(upload_folder, filename) + + with zipfile.ZipFile(export_zip_path, "w", compression=8) as zipf: + pattern = os.path.join(data_folder_path, "**", "*") + for full_path in glob.glob(pattern, recursive=True): + if os.path.isfile(full_path): + archive_name = os.path.relpath(full_path, start=data_folder_path) + zipf.write(full_path, archive_name) + zipf.writestr("snapshot.json", flask.json.dumps(params.snapshot)) + + return utils_functions.send_file(upload_folder, [export_zip_path], filename) diff --git a/src/opengeodeweb_back/routes/schemas/__init__.py b/src/opengeodeweb_back/routes/schemas/__init__.py index 406ed4cb..440a9dad 100644 --- a/src/opengeodeweb_back/routes/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/schemas/__init__.py @@ -12,3 +12,4 @@ from .geode_objects_and_output_extensions import * from .allowed_objects import * from .allowed_files import * +from .export_project import * diff --git a/src/opengeodeweb_back/routes/schemas/export_project.json b/src/opengeodeweb_back/routes/schemas/export_project.json new file mode 100644 index 00000000..bd271b7a --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/export_project.json @@ -0,0 +1,20 @@ +{ + "route": "/export_project", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "snapshot": { + "type": "object" + }, + "filename": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "snapshot" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/schemas/export_project.py b/src/opengeodeweb_back/routes/schemas/export_project.py new file mode 100644 index 00000000..b49ec582 --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/export_project.py @@ -0,0 +1,9 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass +from typing import Dict, Any, Optional + + +@dataclass +class ExportProject(DataClassJsonMixin): + snapshot: Dict[str, Any] + filename: Optional[str] = None \ No newline at end of file diff --git a/src/opengeodeweb_back/utils_functions.py b/src/opengeodeweb_back/utils_functions.py index 1e81f7a2..1f70ccd8 100644 --- a/src/opengeodeweb_back/utils_functions.py +++ b/src/opengeodeweb_back/utils_functions.py @@ -136,7 +136,9 @@ def send_file( else: mimetype = "application/zip" new_file_name = os.path.splitext(new_file_name)[0] + ".zip" - with zipfile.ZipFile(os.path.join(upload_folder, new_file_name), "w") as zipObj: + with zipfile.ZipFile( + os.path.join(os.path.abspath(upload_folder), new_file_name), "w" + ) as zipObj: for saved_file_path in saved_files: zipObj.write( saved_file_path, @@ -144,7 +146,7 @@ def send_file( ) response = flask.send_from_directory( - directory=upload_folder, + directory=os.path.abspath(upload_folder), path=new_file_name, as_attachment=True, mimetype=mimetype, diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index e1f916b7..8533bb4e 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -5,6 +5,9 @@ from opengeodeweb_back import geode_functions from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session +import zipfile +import json +import io def test_model_mesh_components(client, test_id): @@ -55,3 +58,26 @@ def test_extract_brep_uuids(client, test_id): assert "uuid_dict" in response.json uuid_dict = response.json["uuid_dict"] assert isinstance(uuid_dict, dict) + + +def test_export_project_route(client): + route = "/opengeodeweb_back/export_project" + snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + filename = "export_project_test.zip" + response = client.post(route, json={"snapshot": snapshot, "filename": filename}) + assert response.status_code == 200 + assert response.headers.get("new-file-name") == filename + assert response.mimetype == "application/octet-binary" + response.direct_passthrough = False + data = response.get_data() + with zipfile.ZipFile(io.BytesIO(data), "r") as zf: + names = zf.namelist() + assert "snapshot.json" in names + parsed = json.loads(zf.read("snapshot.json").decode("utf-8")) + assert parsed == snapshot + assert "1/project.db" in names + response.close() + upload_folder = client.application.config["UPLOAD_FOLDER"] + export_path = os.path.join(upload_folder, filename) + if os.path.exists(export_path): + os.remove(export_path) diff --git a/tests/test_utils_functions.py b/tests/test_utils_functions.py index 162af7bc..8f1d2ae1 100644 --- a/tests/test_utils_functions.py +++ b/tests/test_utils_functions.py @@ -6,6 +6,9 @@ import flask import shutil import uuid +import glob +import zipfile +import io # Local application imports from opengeodeweb_microservice.database.data import Data @@ -206,3 +209,45 @@ def test_generate_native_viewable_and_light_viewable_from_file(client): assert isinstance(result["object_type"], str) assert isinstance(result["binary_light_viewable"], str) assert isinstance(result["input_file"], str) + + +def test_send_file_multiple_returns_zip(client, tmp_path): + app = client.application + with app.app_context(): + app.config["UPLOAD_FOLDER"] = str(tmp_path) + file_paths = [] + for i, content in [(1, b"hello 1"), (2, b"hello 2")]: + file_path = tmp_path / f"tmp_send_file_{i}.txt" + file_path.write_bytes(content) + file_paths.append(str(file_path)) + with app.test_request_context(): + response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], file_paths, "bundle") + assert response.status_code == 200 + assert response.mimetype == "application/zip" + assert response.headers.get("new-file-name") == "bundle.zip" + + response.direct_passthrough = False + zip_bytes = response.get_data() + with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zip_file: + zip_entries = zip_file.namelist() + assert "tmp_send_file_1.txt" in zip_entries + assert "tmp_send_file_2.txt" in zip_entries + response.close() + + +def test_send_file_single_returns_octet_binary(client, tmp_path): + app = client.application + with app.app_context(): + app.config["UPLOAD_FOLDER"] = str(tmp_path) + file_path = tmp_path / "tmp_send_file_1.txt" + file_path.write_bytes(b"hello 1") + with app.test_request_context(): + response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], [str(file_path)], "tmp_send_file_1.txt") + assert response.status_code == 200 + assert response.mimetype == "application/octet-binary" + assert response.headers.get("new-file-name") == "tmp_send_file_1.txt" + + response.direct_passthrough = False + file_bytes = response.get_data() + assert file_bytes == b"hello 1" + response.close() From 875590cb2b3ba279edf7a8d83b9529383dce4d2c Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Oct 2025 15:11:42 +0100 Subject: [PATCH 02/31] import route and schema --- .../routes/blueprint_routes.py | 56 ++++++++++++++++++- .../routes/schemas/__init__.py | 1 + .../routes/schemas/import_project.json | 10 ++++ .../routes/schemas/import_project.py | 7 +++ tests/test_models_routes.py | 43 ++++++++++++-- 5 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 src/opengeodeweb_back/routes/schemas/import_project.json create mode 100644 src/opengeodeweb_back/routes/schemas/import_project.py diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 32817fd7..fc925ace 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -1,6 +1,7 @@ # Standard library imports import os import time +import shutil # Third party imports import flask @@ -286,12 +287,61 @@ def export_project() -> flask.Response: filename: str = params.filename or f"project_{int(time.time())}.zip" export_zip_path = os.path.join(upload_folder, filename) - with zipfile.ZipFile(export_zip_path, "w", compression=8) as zipf: + with zipfile.ZipFile(export_zip_path, "w", compression=8) as zip_file: pattern = os.path.join(data_folder_path, "**", "*") for full_path in glob.glob(pattern, recursive=True): if os.path.isfile(full_path): archive_name = os.path.relpath(full_path, start=data_folder_path) - zipf.write(full_path, archive_name) - zipf.writestr("snapshot.json", flask.json.dumps(params.snapshot)) + zip_file.write(full_path, archive_name) + zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) return utils_functions.send_file(upload_folder, [export_zip_path], filename) + + +@routes.route( + schemas_dict["import_project"]["route"], + methods=schemas_dict["import_project"]["methods"], +) +def import_project() -> flask.Response: + if flask.request.method == "OPTIONS": + return flask.make_response({}, 200) + if "file" not in flask.request.files: + flask.abort(400, "No zip file provided under 'file'") + + zip_file = flask.request.files["file"] + filename = werkzeug.utils.secure_filename(os.path.basename(zip_file.filename)) + if not filename.lower().endswith(".zip"): + flask.abort(400, "Uploaded file must be a .zip") + + data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] + os.makedirs(data_folder_path, exist_ok=True) + for entry in os.listdir(data_folder_path): + entry_path = os.path.join(data_folder_path, entry) + try: + if os.path.isdir(entry_path): + shutil.rmtree(entry_path, ignore_errors=True) + else: + os.remove(entry_path) + except FileNotFoundError: + pass + except PermissionError: + flask.abort(423, "Project files are locked; cannot overwrite") + + zip_file.stream.seek(0) + with zipfile.ZipFile(zip_file.stream) as zf: + base = os.path.abspath(data_folder_path) + for member in zf.namelist(): + target = os.path.abspath( + os.path.normpath(os.path.join(base, member)) + ) + if not (target == base or target.startswith(base + os.sep)): + flask.abort(400, "Zip contains unsafe paths") + zf.extractall(data_folder_path) + snapshot = {} + try: + raw = zf.read("snapshot.json").decode("utf-8") + snapshot = flask.json.loads(raw) + except KeyError: + snapshot = {} + + return flask.make_response({"snapshot": snapshot}, 200) diff --git a/src/opengeodeweb_back/routes/schemas/__init__.py b/src/opengeodeweb_back/routes/schemas/__init__.py index 440a9dad..2d97273a 100644 --- a/src/opengeodeweb_back/routes/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/schemas/__init__.py @@ -13,3 +13,4 @@ from .allowed_objects import * from .allowed_files import * from .export_project import * +from .import_project import * diff --git a/src/opengeodeweb_back/routes/schemas/import_project.json b/src/opengeodeweb_back/routes/schemas/import_project.json new file mode 100644 index 00000000..9ae597c9 --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/import_project.json @@ -0,0 +1,10 @@ +{ + "route": "/import_project", + "methods": [ + "POST" + ], + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/schemas/import_project.py b/src/opengeodeweb_back/routes/schemas/import_project.py new file mode 100644 index 00000000..7659d00f --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/import_project.py @@ -0,0 +1,7 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass + + +@dataclass +class ImportProject(DataClassJsonMixin): + pass \ No newline at end of file diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 8533bb4e..7f96df0d 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -60,7 +60,7 @@ def test_extract_brep_uuids(client, test_id): assert isinstance(uuid_dict, dict) -def test_export_project_route(client): +def test_export_project_route(client, tmp_path): route = "/opengeodeweb_back/export_project" snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} filename = "export_project_test.zip" @@ -69,11 +69,13 @@ def test_export_project_route(client): assert response.headers.get("new-file-name") == filename assert response.mimetype == "application/octet-binary" response.direct_passthrough = False - data = response.get_data() - with zipfile.ZipFile(io.BytesIO(data), "r") as zf: - names = zf.namelist() + zip_bytes = response.get_data() + tmp_zip_path = tmp_path / filename + tmp_zip_path.write_bytes(zip_bytes) + with zipfile.ZipFile(tmp_zip_path, "r") as zip_file: + names = zip_file.namelist() assert "snapshot.json" in names - parsed = json.loads(zf.read("snapshot.json").decode("utf-8")) + parsed = json.loads(zip_file.read("snapshot.json").decode("utf-8")) assert parsed == snapshot assert "1/project.db" in names response.close() @@ -81,3 +83,34 @@ def test_export_project_route(client): export_path = os.path.join(upload_folder, filename) if os.path.exists(export_path): os.remove(export_path) + + +def test_import_project_route(client, tmp_path): + route = "/opengeodeweb_back/import_project" + snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + + data_folder = client.application.config["DATA_FOLDER_PATH"] + pre_existing_db_path = os.path.join(data_folder, "1", "project.db") + os.makedirs(os.path.dirname(pre_existing_db_path), exist_ok=True) + with open(pre_existing_db_path, "wb") as file: + file.write(b"old_db_content") + + tmp_zip = tmp_path / "import_project_test.zip" + new_database_bytes = b"new_db_content" + with zipfile.ZipFile(tmp_zip, "w", compression=zipfile.ZIP_DEFLATED) as zip_file: + zip_file.writestr("snapshot.json", json.dumps(snapshot)) + zip_file.writestr("1/project.db", new_database_bytes) + + with open(tmp_zip, "rb") as file: + response = client.post( + route, + data={"file": (file, "import_project_test.zip")}, + content_type="multipart/form-data", + ) + + assert response.status_code == 200 + assert response.json.get("snapshot") == snapshot + + assert os.path.exists(pre_existing_db_path) + with open(pre_existing_db_path, "rb") as file: + assert file.read() == new_database_bytes From 86c0dcc5f691f766862b6d0fb508c68174b71653 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:04:17 +0000 Subject: [PATCH 03/31] Apply prepare changes --- opengeodeweb_back_schemas.json | 32 +++++++++++++++++++ requirements.txt | 1 - .../routes/blueprint_routes.py | 4 +-- .../routes/schemas/__init__.py | 4 +-- .../routes/schemas/export_project.py | 2 +- .../routes/schemas/import_project.py | 2 +- tests/test_models_routes.py | 8 +++-- tests/test_utils_functions.py | 8 +++-- 8 files changed, 49 insertions(+), 12 deletions(-) diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index ee3d0db3..8d12d63b 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -296,6 +296,17 @@ ], "additionalProperties": false }, + "import_project": { + "$id": "opengeodeweb_back/import_project", + "route": "/import_project", + "methods": [ + "POST" + ], + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }, "geographic_coordinate_systems": { "$id": "opengeodeweb_back/geographic_coordinate_systems", "route": "/geographic_coordinate_systems", @@ -337,6 +348,27 @@ ], "additionalProperties": false }, + "export_project": { + "$id": "opengeodeweb_back/export_project", + "route": "/export_project", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "snapshot": { + "type": "object" + }, + "filename": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "snapshot" + ], + "additionalProperties": false + }, "allowed_objects": { "$id": "opengeodeweb_back/allowed_objects", "route": "/allowed_objects", diff --git a/requirements.txt b/requirements.txt index 89b35fa5..4e85944d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.6rc1 diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index fc925ace..6899583f 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -331,9 +331,7 @@ def import_project() -> flask.Response: with zipfile.ZipFile(zip_file.stream) as zf: base = os.path.abspath(data_folder_path) for member in zf.namelist(): - target = os.path.abspath( - os.path.normpath(os.path.join(base, member)) - ) + target = os.path.abspath(os.path.normpath(os.path.join(base, member))) if not (target == base or target.startswith(base + os.sep)): flask.abort(400, "Zip contains unsafe paths") zf.extractall(data_folder_path) diff --git a/src/opengeodeweb_back/routes/schemas/__init__.py b/src/opengeodeweb_back/routes/schemas/__init__.py index 2d97273a..339f8961 100644 --- a/src/opengeodeweb_back/routes/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/schemas/__init__.py @@ -8,9 +8,9 @@ from .missing_files import * from .kill import * from .inspect_file import * +from .import_project import * from .geographic_coordinate_systems import * from .geode_objects_and_output_extensions import * +from .export_project import * from .allowed_objects import * from .allowed_files import * -from .export_project import * -from .import_project import * diff --git a/src/opengeodeweb_back/routes/schemas/export_project.py b/src/opengeodeweb_back/routes/schemas/export_project.py index b49ec582..97242a3b 100644 --- a/src/opengeodeweb_back/routes/schemas/export_project.py +++ b/src/opengeodeweb_back/routes/schemas/export_project.py @@ -6,4 +6,4 @@ @dataclass class ExportProject(DataClassJsonMixin): snapshot: Dict[str, Any] - filename: Optional[str] = None \ No newline at end of file + filename: Optional[str] = None diff --git a/src/opengeodeweb_back/routes/schemas/import_project.py b/src/opengeodeweb_back/routes/schemas/import_project.py index 7659d00f..2fd045e0 100644 --- a/src/opengeodeweb_back/routes/schemas/import_project.py +++ b/src/opengeodeweb_back/routes/schemas/import_project.py @@ -4,4 +4,4 @@ @dataclass class ImportProject(DataClassJsonMixin): - pass \ No newline at end of file + pass diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 7f96df0d..d061d87a 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -62,7 +62,9 @@ def test_extract_brep_uuids(client, test_id): def test_export_project_route(client, tmp_path): route = "/opengeodeweb_back/export_project" - snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + snapshot = { + "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} + } filename = "export_project_test.zip" response = client.post(route, json={"snapshot": snapshot, "filename": filename}) assert response.status_code == 200 @@ -87,7 +89,9 @@ def test_export_project_route(client, tmp_path): def test_import_project_route(client, tmp_path): route = "/opengeodeweb_back/import_project" - snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + snapshot = { + "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} + } data_folder = client.application.config["DATA_FOLDER_PATH"] pre_existing_db_path = os.path.join(data_folder, "1", "project.db") diff --git a/tests/test_utils_functions.py b/tests/test_utils_functions.py index 8f1d2ae1..110cbe89 100644 --- a/tests/test_utils_functions.py +++ b/tests/test_utils_functions.py @@ -221,7 +221,9 @@ def test_send_file_multiple_returns_zip(client, tmp_path): file_path.write_bytes(content) file_paths.append(str(file_path)) with app.test_request_context(): - response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], file_paths, "bundle") + response = utils_functions.send_file( + app.config["UPLOAD_FOLDER"], file_paths, "bundle" + ) assert response.status_code == 200 assert response.mimetype == "application/zip" assert response.headers.get("new-file-name") == "bundle.zip" @@ -242,7 +244,9 @@ def test_send_file_single_returns_octet_binary(client, tmp_path): file_path = tmp_path / "tmp_send_file_1.txt" file_path.write_bytes(b"hello 1") with app.test_request_context(): - response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], [str(file_path)], "tmp_send_file_1.txt") + response = utils_functions.send_file( + app.config["UPLOAD_FOLDER"], [str(file_path)], "tmp_send_file_1.txt" + ) assert response.status_code == 200 assert response.mimetype == "application/octet-binary" assert response.headers.get("new-file-name") == "tmp_send_file_1.txt" From 1ffd4ef7616e5199c06d80cd5e66751712dfd9e0 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Oct 2025 17:00:48 +0100 Subject: [PATCH 04/31] zip_path --- tests/test_utils_functions.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_utils_functions.py b/tests/test_utils_functions.py index 8f1d2ae1..89d430c7 100644 --- a/tests/test_utils_functions.py +++ b/tests/test_utils_functions.py @@ -6,7 +6,6 @@ import flask import shutil import uuid -import glob import zipfile import io @@ -224,11 +223,10 @@ def test_send_file_multiple_returns_zip(client, tmp_path): response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], file_paths, "bundle") assert response.status_code == 200 assert response.mimetype == "application/zip" - assert response.headers.get("new-file-name") == "bundle.zip" - - response.direct_passthrough = False - zip_bytes = response.get_data() - with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zip_file: + new_file_name = response.headers.get("new-file-name") + assert new_file_name == "bundle.zip" + zip_path = os.path.join(app.config["UPLOAD_FOLDER"], new_file_name) + with zipfile.ZipFile(zip_path, "r") as zip_file: zip_entries = zip_file.namelist() assert "tmp_send_file_1.txt" in zip_entries assert "tmp_send_file_2.txt" in zip_entries @@ -245,9 +243,10 @@ def test_send_file_single_returns_octet_binary(client, tmp_path): response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], [str(file_path)], "tmp_send_file_1.txt") assert response.status_code == 200 assert response.mimetype == "application/octet-binary" - assert response.headers.get("new-file-name") == "tmp_send_file_1.txt" - - response.direct_passthrough = False - file_bytes = response.get_data() + new_file_name = response.headers.get("new-file-name") + assert new_file_name == "tmp_send_file_1.txt" + zip_path = os.path.join(app.config["UPLOAD_FOLDER"], new_file_name) + with open(zip_path, "rb") as f: + file_bytes = f.read() assert file_bytes == b"hello 1" response.close() From e90175a51cbb4d70fc6233326e27a47dfbaf24e3 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Oct 2025 10:20:52 +0100 Subject: [PATCH 05/31] ask database for data entries to generate the zip file --- .../routes/blueprint_routes.py | 92 ++++++++++++------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 6899583f..6aa73c8b 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -1,4 +1,5 @@ # Standard library imports +from posixpath import relpath import os import time import shutil @@ -14,6 +15,8 @@ from opengeodeweb_back import geode_functions, utils_functions from .models import blueprint_models from . import schemas +from opengeodeweb_microservice.database.data import Data +from opengeodeweb_microservice.database.connection import get_session routes = flask.Blueprint("routes", __name__, url_prefix="/opengeodeweb_back") @@ -280,22 +283,46 @@ def export_project() -> flask.Response: utils_functions.validate_request(flask.request, schemas_dict["export_project"]) params = schemas.ExportProject.from_dict(flask.request.get_json()) - data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] - upload_folder: str = flask.current_app.config["UPLOAD_FOLDER"] - os.makedirs(upload_folder, exist_ok=True) + project_folder: str = flask.current_app.config["DATA_FOLDER_PATH"] + os.makedirs(project_folder, exist_ok=True) + + if not params.filename: + flask.abort(400, "filename is required") + filename: str = werkzeug.utils.secure_filename(os.path.basename(params.filename)) + export_zip_path = os.path.join(project_folder, filename) + + with get_session() as session: + entries = [ + { + "id": entry.id, + "input_file": entry.input_file, + "additional_files": entry.additional_files, + } + for entry in session.query(Data).all() + ] + + with zipfile.ZipFile(export_zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file: + database_root_path = os.path.join(project_folder, "project.db") + if os.path.isfile(database_root_path): + zip_file.write(database_root_path, "project.db") + + for entry in entries: + base_dir = os.path.join(project_folder, entry["id"]) + + input_file = entry["input_file"] + if input_file: + in_path = os.path.join(base_dir, input_file) + if os.path.isfile(in_path): + zip_file.write(in_path, os.path.join(entry["id"], input_file)) + + for relative_path in entry["additional_files"] or []: + add_path = os.path.join(base_dir, relative_path) + if os.path.isfile(add_path): + zip_file.write(add_path, os.path.join(entry["id"], relative_path)) - filename: str = params.filename or f"project_{int(time.time())}.zip" - export_zip_path = os.path.join(upload_folder, filename) - - with zipfile.ZipFile(export_zip_path, "w", compression=8) as zip_file: - pattern = os.path.join(data_folder_path, "**", "*") - for full_path in glob.glob(pattern, recursive=True): - if os.path.isfile(full_path): - archive_name = os.path.relpath(full_path, start=data_folder_path) - zip_file.write(full_path, archive_name) zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) - return utils_functions.send_file(upload_folder, [export_zip_path], filename) + return utils_functions.send_file(project_folder, [export_zip_path], filename) @routes.route( @@ -313,31 +340,30 @@ def import_project() -> flask.Response: if not filename.lower().endswith(".zip"): flask.abort(400, "Uploaded file must be a .zip") - data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] - os.makedirs(data_folder_path, exist_ok=True) - for entry in os.listdir(data_folder_path): - entry_path = os.path.join(data_folder_path, entry) - try: - if os.path.isdir(entry_path): - shutil.rmtree(entry_path, ignore_errors=True) - else: - os.remove(entry_path) - except FileNotFoundError: - pass - except PermissionError: - flask.abort(423, "Project files are locked; cannot overwrite") + project_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] + try: + if os.path.exists(project_folder_path): + shutil.rmtree(project_folder_path) + os.makedirs(project_folder_path, exist_ok=True) + except PermissionError: + flask.abort(423, "Project files are locked; cannot overwrite") zip_file.stream.seek(0) - with zipfile.ZipFile(zip_file.stream) as zf: - base = os.path.abspath(data_folder_path) - for member in zf.namelist(): - target = os.path.abspath(os.path.normpath(os.path.join(base, member))) - if not (target == base or target.startswith(base + os.sep)): + with zipfile.ZipFile(zip_file.stream) as zip_archive: + project_folder = os.path.abspath(project_folder_path) + for member in zip_archive.namelist(): + target = os.path.abspath(os.path.normpath(os.path.join(project_folder, member))) + if not (target == project_folder or target.startswith(project_folder + os.sep)): flask.abort(400, "Zip contains unsafe paths") - zf.extractall(data_folder_path) + zip_archive.extractall(project_folder) + + database_root_path = os.path.join(project_folder, "project.db") + if not os.path.isfile(database_root_path): + flask.abort(400, "Missing project.db at project root") + snapshot = {} try: - raw = zf.read("snapshot.json").decode("utf-8") + raw = zip_archive.read("snapshot.json").decode("utf-8") snapshot = flask.json.loads(raw) except KeyError: snapshot = {} From 035802b7acd4f2bc922836c425ee28701e944f69 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:21:28 +0000 Subject: [PATCH 06/31] Apply prepare changes --- src/opengeodeweb_back/routes/blueprint_routes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 6aa73c8b..cf49be72 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -301,7 +301,9 @@ def export_project() -> flask.Response: for entry in session.query(Data).all() ] - with zipfile.ZipFile(export_zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file: + with zipfile.ZipFile( + export_zip_path, "w", compression=zipfile.ZIP_DEFLATED + ) as zip_file: database_root_path = os.path.join(project_folder, "project.db") if os.path.isfile(database_root_path): zip_file.write(database_root_path, "project.db") @@ -352,8 +354,12 @@ def import_project() -> flask.Response: with zipfile.ZipFile(zip_file.stream) as zip_archive: project_folder = os.path.abspath(project_folder_path) for member in zip_archive.namelist(): - target = os.path.abspath(os.path.normpath(os.path.join(project_folder, member))) - if not (target == project_folder or target.startswith(project_folder + os.sep)): + target = os.path.abspath( + os.path.normpath(os.path.join(project_folder, member)) + ) + if not ( + target == project_folder or target.startswith(project_folder + os.sep) + ): flask.abort(400, "Zip contains unsafe paths") zip_archive.extractall(project_folder) From 3840fc33bf9d45ac06a5a1f8848943971b82ca55 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Oct 2025 10:39:35 +0100 Subject: [PATCH 07/31] tests updated --- .../routes/blueprint_routes.py | 10 +++++----- tests/test_models_routes.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 6aa73c8b..f8067256 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -340,17 +340,17 @@ def import_project() -> flask.Response: if not filename.lower().endswith(".zip"): flask.abort(400, "Uploaded file must be a .zip") - project_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] + data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] try: - if os.path.exists(project_folder_path): - shutil.rmtree(project_folder_path) - os.makedirs(project_folder_path, exist_ok=True) + if os.path.exists(data_folder_path): + shutil.rmtree(data_folder_path) + os.makedirs(data_folder_path, exist_ok=True) except PermissionError: flask.abort(423, "Project files are locked; cannot overwrite") zip_file.stream.seek(0) with zipfile.ZipFile(zip_file.stream) as zip_archive: - project_folder = os.path.abspath(project_folder_path) + project_folder = os.path.abspath(data_folder_path) for member in zip_archive.namelist(): target = os.path.abspath(os.path.normpath(os.path.join(project_folder, member))) if not (target == project_folder or target.startswith(project_folder + os.sep)): diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index d061d87a..2a76ced2 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -66,6 +66,11 @@ def test_export_project_route(client, tmp_path): "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} } filename = "export_project_test.zip" + project_folder = client.application.config["DATA_FOLDER_PATH"] + os.makedirs(project_folder, exist_ok=True) + database_root_path = os.path.join(project_folder, "project.db") + with open(database_root_path, "wb") as f: + f.write(b"test_project_db") response = client.post(route, json={"snapshot": snapshot, "filename": filename}) assert response.status_code == 200 assert response.headers.get("new-file-name") == filename @@ -79,10 +84,9 @@ def test_export_project_route(client, tmp_path): assert "snapshot.json" in names parsed = json.loads(zip_file.read("snapshot.json").decode("utf-8")) assert parsed == snapshot - assert "1/project.db" in names + assert "project.db" in names response.close() - upload_folder = client.application.config["UPLOAD_FOLDER"] - export_path = os.path.join(upload_folder, filename) + export_path = os.path.join(project_folder, filename) if os.path.exists(export_path): os.remove(export_path) @@ -93,17 +97,15 @@ def test_import_project_route(client, tmp_path): "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} } + client.application.config["DATA_FOLDER_PATH"] = os.path.join(str(tmp_path), "project_data") data_folder = client.application.config["DATA_FOLDER_PATH"] - pre_existing_db_path = os.path.join(data_folder, "1", "project.db") - os.makedirs(os.path.dirname(pre_existing_db_path), exist_ok=True) - with open(pre_existing_db_path, "wb") as file: - file.write(b"old_db_content") + pre_existing_db_path = os.path.join(data_folder, "project.db") tmp_zip = tmp_path / "import_project_test.zip" new_database_bytes = b"new_db_content" with zipfile.ZipFile(tmp_zip, "w", compression=zipfile.ZIP_DEFLATED) as zip_file: zip_file.writestr("snapshot.json", json.dumps(snapshot)) - zip_file.writestr("1/project.db", new_database_bytes) + zip_file.writestr("project.db", new_database_bytes) with open(tmp_zip, "rb") as file: response = client.post( From 9901c9a3e533ee2a21708eec293ce2dd7bdf9680 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:40:14 +0000 Subject: [PATCH 08/31] Apply prepare changes --- tests/test_models_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 2a76ced2..4245a8ea 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -97,7 +97,9 @@ def test_import_project_route(client, tmp_path): "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} } - client.application.config["DATA_FOLDER_PATH"] = os.path.join(str(tmp_path), "project_data") + client.application.config["DATA_FOLDER_PATH"] = os.path.join( + str(tmp_path), "project_data" + ) data_folder = client.application.config["DATA_FOLDER_PATH"] pre_existing_db_path = os.path.join(data_folder, "project.db") From ce99d0af82b643f70669e1a1ab9913b7a85d3d52 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Oct 2025 11:02:17 +0100 Subject: [PATCH 09/31] test --- .../routes/blueprint_routes.py | 32 +++++++------------ tests/test_models_routes.py | 1 - 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index e0294e21..03165879 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -8,7 +8,6 @@ import flask import werkzeug import zipfile -import glob from opengeodeweb_microservice.schemas import get_schemas_dict # Local application imports @@ -292,14 +291,7 @@ def export_project() -> flask.Response: export_zip_path = os.path.join(project_folder, filename) with get_session() as session: - entries = [ - { - "id": entry.id, - "input_file": entry.input_file, - "additional_files": entry.additional_files, - } - for entry in session.query(Data).all() - ] + rows = session.query(Data.id, Data.input_file, Data.additional_files).all() with zipfile.ZipFile( export_zip_path, "w", compression=zipfile.ZIP_DEFLATED @@ -308,19 +300,18 @@ def export_project() -> flask.Response: if os.path.isfile(database_root_path): zip_file.write(database_root_path, "project.db") - for entry in entries: - base_dir = os.path.join(project_folder, entry["id"]) + for data_id, input_file, additional_files in rows: + base_dir = os.path.join(project_folder, data_id) - input_file = entry["input_file"] - if input_file: - in_path = os.path.join(base_dir, input_file) - if os.path.isfile(in_path): - zip_file.write(in_path, os.path.join(entry["id"], input_file)) + if isinstance(input_file, str): + input_path = os.path.join(base_dir, input_file) + if os.path.isfile(input_path): + zip_file.write(input_path, os.path.join(data_id, input_file)) - for relative_path in entry["additional_files"] or []: - add_path = os.path.join(base_dir, relative_path) - if os.path.isfile(add_path): - zip_file.write(add_path, os.path.join(entry["id"], relative_path)) + for relative_path in (additional_files or []): + additional_path = os.path.join(base_dir, relative_path) + if os.path.isfile(additional_path): + zip_file.write(additional_path, os.path.join(data_id, relative_path)) zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) @@ -338,6 +329,7 @@ def import_project() -> flask.Response: flask.abort(400, "No zip file provided under 'file'") zip_file = flask.request.files["file"] + assert zip_file.filename is not None filename = werkzeug.utils.secure_filename(os.path.basename(zip_file.filename)) if not filename.lower().endswith(".zip"): flask.abort(400, "Uploaded file must be a .zip") diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 2a76ced2..2c8c65f1 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -7,7 +7,6 @@ from opengeodeweb_microservice.database.connection import get_session import zipfile import json -import io def test_model_mesh_components(client, test_id): From 38a82bff2d952a99afe7d7c3c962727e0b60cd47 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:03:08 +0000 Subject: [PATCH 10/31] Apply prepare changes --- src/opengeodeweb_back/routes/blueprint_routes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 03165879..b74c338e 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -308,10 +308,12 @@ def export_project() -> flask.Response: if os.path.isfile(input_path): zip_file.write(input_path, os.path.join(data_id, input_file)) - for relative_path in (additional_files or []): + for relative_path in additional_files or []: additional_path = os.path.join(base_dir, relative_path) if os.path.isfile(additional_path): - zip_file.write(additional_path, os.path.join(data_id, relative_path)) + zip_file.write( + additional_path, os.path.join(data_id, relative_path) + ) zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) From 4240888af55919cbaaa16dcc4098fbde7598bbcc Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Oct 2025 15:38:52 +0100 Subject: [PATCH 11/31] validate_request --- src/opengeodeweb_back/routes/blueprint_routes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 03165879..54372455 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -303,10 +303,9 @@ def export_project() -> flask.Response: for data_id, input_file, additional_files in rows: base_dir = os.path.join(project_folder, data_id) - if isinstance(input_file, str): - input_path = os.path.join(base_dir, input_file) - if os.path.isfile(input_path): - zip_file.write(input_path, os.path.join(data_id, input_file)) + input_path = os.path.join(base_dir, input_file) + if os.path.isfile(input_path): + zip_file.write(input_path, os.path.join(data_id, input_file)) for relative_path in (additional_files or []): additional_path = os.path.join(base_dir, relative_path) @@ -325,6 +324,7 @@ def export_project() -> flask.Response: def import_project() -> flask.Response: if flask.request.method == "OPTIONS": return flask.make_response({}, 200) + utils_functions.validate_request(flask.request, schemas_dict["import_project"]) if "file" not in flask.request.files: flask.abort(400, "No zip file provided under 'file'") From ef772eeaf0b1419cc6397ea4912727ad11270577 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Oct 2025 15:56:51 +0100 Subject: [PATCH 12/31] fix --- src/opengeodeweb_back/routes/blueprint_routes.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 0867e569..68ff49cd 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -303,16 +303,14 @@ def export_project() -> flask.Response: for data_id, input_file, additional_files in rows: base_dir = os.path.join(project_folder, data_id) - input_path = os.path.join(base_dir, input_file) + input_path = os.path.join(base_dir, str(input_file)) if os.path.isfile(input_path): - zip_file.write(input_path, os.path.join(data_id, input_file)) + zip_file.write(input_path, os.path.join(data_id, str(input_file))) - for relative_path in additional_files or []: + for relative_path in additional_files if isinstance(additional_files, list) else []: additional_path = os.path.join(base_dir, relative_path) if os.path.isfile(additional_path): - zip_file.write( - additional_path, os.path.join(data_id, relative_path) - ) + zip_file.write(additional_path, os.path.join(data_id, relative_path)) zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) From 14c6bf66ef6e2e0af307a8e69a530d0455ab9df4 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:57:56 +0000 Subject: [PATCH 13/31] Apply prepare changes --- src/opengeodeweb_back/routes/blueprint_routes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 68ff49cd..7ce0992e 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -307,10 +307,14 @@ def export_project() -> flask.Response: if os.path.isfile(input_path): zip_file.write(input_path, os.path.join(data_id, str(input_file))) - for relative_path in additional_files if isinstance(additional_files, list) else []: + for relative_path in ( + additional_files if isinstance(additional_files, list) else [] + ): additional_path = os.path.join(base_dir, relative_path) if os.path.isfile(additional_path): - zip_file.write(additional_path, os.path.join(data_id, relative_path)) + zip_file.write( + additional_path, os.path.join(data_id, relative_path) + ) zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) From b2cf2e8546af0252791d79666c87871f9927c217 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 31 Oct 2025 11:23:52 +0100 Subject: [PATCH 14/31] test --- .../routes/blueprint_routes.py | 2 - tests/test_models_routes.py | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 68ff49cd..b7bd2294 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -285,8 +285,6 @@ def export_project() -> flask.Response: project_folder: str = flask.current_app.config["DATA_FOLDER_PATH"] os.makedirs(project_folder, exist_ok=True) - if not params.filename: - flask.abort(400, "filename is required") filename: str = werkzeug.utils.secure_filename(os.path.basename(params.filename)) export_zip_path = os.path.join(project_folder, filename) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 9831e614..a787e78f 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -121,3 +121,50 @@ def test_import_project_route(client, tmp_path): assert os.path.exists(pre_existing_db_path) with open(pre_existing_db_path, "rb") as file: assert file.read() == new_database_bytes + + +def test_save_viewable_workflow_from_file(client): + route = "/opengeodeweb_back/save_viewable_file" + payload = {"input_geode_object": "BRep", "filename": "cube.og_brep"} + + response = client.post(route, json=payload) + assert response.status_code == 200 + + data_id = response.json["id"] + assert isinstance(data_id, str) and len(data_id) > 0 + assert response.json["viewable_file_name"].endswith(".vtm") + + comp_resp = client.post( + "/opengeodeweb_back/models/vtm_component_indices", json={"id": data_id} + ) + assert comp_resp.status_code == 200 + + refreshed = Data.get(data_id) + assert refreshed is not None + + +def test_save_viewable_workflow_from_object(client): + # Chemin “from object” : passe par un endpoint de création qui génère/sauvegarde via save_viewable. + route = "/opengeodeweb_back/create/create_aoi" + aoi_data = { + "name": "workflow_aoi", + "points": [ + {"x": 0.0, "y": 0.0}, + {"x": 1.0, "y": 0.0}, + {"x": 1.0, "y": 1.0}, + {"x": 0.0, "y": 1.0}, + ], + "z": 0.0, + } + + response = client.post(route, json=aoi_data) + assert response.status_code == 200 + + data_id = response.json["id"] + assert isinstance(data_id, str) and len(data_id) > 0 + assert response.json["geode_object"] == "EdgedCurve3D" + assert response.json["viewable_file_name"].endswith(".vtp") + + attr_resp = client.post("/opengeodeweb_back/vertex_attribute_names", json={"id": data_id}) + assert attr_resp.status_code == 200 + assert isinstance(attr_resp.json.get("vertex_attribute_names", []), list) From 76aa2b0863f04a06b9e11f49ea94a27f81f4d7f2 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:24:35 +0000 Subject: [PATCH 15/31] Apply prepare changes --- tests/test_models_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index a787e78f..095ff615 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -165,6 +165,8 @@ def test_save_viewable_workflow_from_object(client): assert response.json["geode_object"] == "EdgedCurve3D" assert response.json["viewable_file_name"].endswith(".vtp") - attr_resp = client.post("/opengeodeweb_back/vertex_attribute_names", json={"id": data_id}) + attr_resp = client.post( + "/opengeodeweb_back/vertex_attribute_names", json={"id": data_id} + ) assert attr_resp.status_code == 200 assert isinstance(attr_resp.json.get("vertex_attribute_names", []), list) From 2266b97dafc989a9733400f85488d5a5918a8eb5 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 31 Oct 2025 11:54:37 +0100 Subject: [PATCH 16/31] test --- src/opengeodeweb_back/routes/blueprint_routes.py | 1 - src/opengeodeweb_back/routes/schemas/export_project.json | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 1696da67..102e7c77 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -1,5 +1,4 @@ # Standard library imports -from posixpath import relpath import os import time import shutil diff --git a/src/opengeodeweb_back/routes/schemas/export_project.json b/src/opengeodeweb_back/routes/schemas/export_project.json index bd271b7a..154709d4 100644 --- a/src/opengeodeweb_back/routes/schemas/export_project.json +++ b/src/opengeodeweb_back/routes/schemas/export_project.json @@ -14,7 +14,8 @@ } }, "required": [ - "snapshot" + "snapshot", + "filename" ], "additionalProperties": false } \ No newline at end of file From 65bc03761b90225c2959da7ee82492bf6ecd7778 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:55:22 +0000 Subject: [PATCH 17/31] Apply prepare changes --- opengeodeweb_back_schemas.json | 3 ++- src/opengeodeweb_back/routes/schemas/export_project.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index 8d12d63b..e5c8d4c2 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -365,7 +365,8 @@ } }, "required": [ - "snapshot" + "snapshot", + "filename" ], "additionalProperties": false }, diff --git a/src/opengeodeweb_back/routes/schemas/export_project.py b/src/opengeodeweb_back/routes/schemas/export_project.py index 97242a3b..dcf3636d 100644 --- a/src/opengeodeweb_back/routes/schemas/export_project.py +++ b/src/opengeodeweb_back/routes/schemas/export_project.py @@ -1,9 +1,9 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass -from typing import Dict, Any, Optional +from typing import Dict, Any @dataclass class ExportProject(DataClassJsonMixin): + filename: str snapshot: Dict[str, Any] - filename: Optional[str] = None From 44b879dfe601ca44240b12687ec9533552da7159 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 31 Oct 2025 23:16:23 +0100 Subject: [PATCH 18/31] test --- tests/test_models_routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 095ff615..7f05d4e0 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -144,7 +144,6 @@ def test_save_viewable_workflow_from_file(client): def test_save_viewable_workflow_from_object(client): - # Chemin “from object” : passe par un endpoint de création qui génère/sauvegarde via save_viewable. route = "/opengeodeweb_back/create/create_aoi" aoi_data = { "name": "workflow_aoi", From c306246ac4ebfef73fcaa895a67d194b5b65886c Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 3 Nov 2025 08:19:37 +0100 Subject: [PATCH 19/31] removed Any --- src/opengeodeweb_back/routes/schemas/export_project.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/routes/schemas/export_project.py b/src/opengeodeweb_back/routes/schemas/export_project.py index dcf3636d..02a72181 100644 --- a/src/opengeodeweb_back/routes/schemas/export_project.py +++ b/src/opengeodeweb_back/routes/schemas/export_project.py @@ -1,9 +1,8 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass -from typing import Dict, Any @dataclass class ExportProject(DataClassJsonMixin): filename: str - snapshot: Dict[str, Any] + snapshot: dict[str, object] From 85af818d5f0110570d4c35eb061ad85d5a1807b4 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:20:21 +0000 Subject: [PATCH 20/31] Apply prepare changes --- src/opengeodeweb_back/routes/schemas/export_project.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/opengeodeweb_back/routes/schemas/export_project.py b/src/opengeodeweb_back/routes/schemas/export_project.py index 02a72181..dcf3636d 100644 --- a/src/opengeodeweb_back/routes/schemas/export_project.py +++ b/src/opengeodeweb_back/routes/schemas/export_project.py @@ -1,8 +1,9 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass +from typing import Dict, Any @dataclass class ExportProject(DataClassJsonMixin): filename: str - snapshot: dict[str, object] + snapshot: Dict[str, Any] From ef26ea5c21781b72f6d5e6dd7a8fe3b673e8416f Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 3 Nov 2025 15:50:44 +0100 Subject: [PATCH 21/31] generate viewables and session. removed stopped request --- .../routes/blueprint_routes.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 102e7c77..0da6e9a2 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -15,6 +15,7 @@ from . import schemas from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session +from opengeodeweb_microservice.database import connection routes = flask.Blueprint("routes", __name__, url_prefix="/opengeodeweb_back") @@ -336,6 +337,14 @@ def import_project() -> flask.Response: flask.abort(400, "Uploaded file must be a .zip") data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] + + # 423 Locked bypass : remove stopped requests + if connection.scoped_session_registry: + connection.scoped_session_registry.remove() + if connection.engine: + connection.engine.dispose() + connection.engine = connection.session_factory = connection.scoped_session_registry = None + try: if os.path.exists(data_folder_path): shutil.rmtree(data_folder_path) @@ -360,11 +369,37 @@ def import_project() -> flask.Response: if not os.path.isfile(database_root_path): flask.abort(400, "Missing project.db at project root") + # Reset database to the imported project.db + connection.init_database(database_root_path, create_tables=False) + + with get_session() as session: + for data_entry in session.query(Data).all(): + data_path = geode_functions.data_file_path(data_entry.id) + + viewable_name = data_entry.viewable_file_name + if viewable_name: + vpath = geode_functions.data_file_path(data_entry.id, viewable_name) + if os.path.isfile(vpath): + continue + + input_file = str(data_entry.input_file or "") + if not input_file: + continue + + input_full = geode_functions.data_file_path(data_entry.id, input_file) + if not os.path.isfile(input_full): + continue + + data_object = geode_functions.load(data_entry.geode_object, input_full) + utils_functions.save_all_viewables_and_return_info( + data_entry.geode_object, data_object, data_entry, data_path + ) + session.commit() + snapshot = {} try: raw = zip_archive.read("snapshot.json").decode("utf-8") snapshot = flask.json.loads(raw) except KeyError: snapshot = {} - return flask.make_response({"snapshot": snapshot}, 200) From bde6708bb02c8e30bdea8f5a3e64e1dc0b4f5bbe Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:51:27 +0000 Subject: [PATCH 22/31] Apply prepare changes --- src/opengeodeweb_back/routes/blueprint_routes.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 0da6e9a2..803457cb 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -343,7 +343,9 @@ def import_project() -> flask.Response: connection.scoped_session_registry.remove() if connection.engine: connection.engine.dispose() - connection.engine = connection.session_factory = connection.scoped_session_registry = None + connection.engine = connection.session_factory = ( + connection.scoped_session_registry + ) = None try: if os.path.exists(data_folder_path): @@ -375,21 +377,21 @@ def import_project() -> flask.Response: with get_session() as session: for data_entry in session.query(Data).all(): data_path = geode_functions.data_file_path(data_entry.id) - + viewable_name = data_entry.viewable_file_name if viewable_name: vpath = geode_functions.data_file_path(data_entry.id, viewable_name) if os.path.isfile(vpath): continue - + input_file = str(data_entry.input_file or "") if not input_file: continue - + input_full = geode_functions.data_file_path(data_entry.id, input_file) if not os.path.isfile(input_full): continue - + data_object = geode_functions.load(data_entry.geode_object, input_full) utils_functions.save_all_viewables_and_return_info( data_entry.geode_object, data_object, data_entry, data_path From 7afe531b832e84b958212caa888eff52e6990e44 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 3 Nov 2025 16:50:21 +0100 Subject: [PATCH 23/31] update tests and import_project --- .../routes/blueprint_routes.py | 22 +++++--- tests/test_models_routes.py | 56 ++++++++++--------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 0da6e9a2..12be8cf2 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -369,28 +369,36 @@ def import_project() -> flask.Response: if not os.path.isfile(database_root_path): flask.abort(400, "Missing project.db at project root") - # Reset database to the imported project.db connection.init_database(database_root_path, create_tables=False) + try: + with get_session() as session: + rows = session.query(Data).all() + except Exception: + connection.init_database(database_root_path, create_tables=True) + with get_session() as session: + rows = session.query(Data).all() + with get_session() as session: - for data_entry in session.query(Data).all(): + for data_entry in rows: data_path = geode_functions.data_file_path(data_entry.id) - viewable_name = data_entry.viewable_file_name if viewable_name: vpath = geode_functions.data_file_path(data_entry.id, viewable_name) if os.path.isfile(vpath): continue - + input_file = str(data_entry.input_file or "") if not input_file: continue - + input_full = geode_functions.data_file_path(data_entry.id, input_file) if not os.path.isfile(input_full): continue - - data_object = geode_functions.load(data_entry.geode_object, input_full) + + data_object = geode_functions.load( + data_entry.geode_object, input_full + ) utils_functions.save_all_viewables_and_return_info( data_entry.geode_object, data_object, data_entry, data_path ) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 7f05d4e0..a8b5b38c 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -92,36 +92,42 @@ def test_export_project_route(client, tmp_path): def test_import_project_route(client, tmp_path): route = "/opengeodeweb_back/import_project" - snapshot = { - "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} - } - - client.application.config["DATA_FOLDER_PATH"] = os.path.join( - str(tmp_path), "project_data" + snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + + original_data_folder = client.application.config["DATA_FOLDER_PATH"] + client.application.config["DATA_FOLDER_PATH"] = os.path.join(str(tmp_path), "project_data") + db_path = os.path.join(client.application.config["DATA_FOLDER_PATH"], "project.db") + + import sqlite3, zipfile, json + temp_db = tmp_path / "temp_project.db" + conn = sqlite3.connect(str(temp_db)) + conn.execute( + "CREATE TABLE datas (id TEXT PRIMARY KEY, geode_object TEXT, viewer_object TEXT, native_file_name TEXT, " + "viewable_file_name TEXT, light_viewable TEXT, input_file TEXT, additional_files TEXT)" ) - data_folder = client.application.config["DATA_FOLDER_PATH"] - pre_existing_db_path = os.path.join(data_folder, "project.db") - - tmp_zip = tmp_path / "import_project_test.zip" - new_database_bytes = b"new_db_content" - with zipfile.ZipFile(tmp_zip, "w", compression=zipfile.ZIP_DEFLATED) as zip_file: - zip_file.writestr("snapshot.json", json.dumps(snapshot)) - zip_file.writestr("project.db", new_database_bytes) - - with open(tmp_zip, "rb") as file: - response = client.post( - route, - data={"file": (file, "import_project_test.zip")}, - content_type="multipart/form-data", + conn.commit(); conn.close() + + z = tmp_path / "import_project_test.zip" + with zipfile.ZipFile(z, "w", compression=zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr("snapshot.json", json.dumps(snapshot)) + zipf.write(str(temp_db), "project.db") + + with open(z, "rb") as f: + resp = client.post( + route, data={"file": (f, "import_project_test.zip")}, content_type="multipart/form-data" ) - assert response.status_code == 200 - assert response.json.get("snapshot") == snapshot + assert resp.status_code == 200 + assert resp.json.get("snapshot") == snapshot + assert os.path.exists(db_path) - assert os.path.exists(pre_existing_db_path) - with open(pre_existing_db_path, "rb") as file: - assert file.read() == new_database_bytes + from opengeodeweb_microservice.database import connection + client.application.config["DATA_FOLDER_PATH"] = original_data_folder + test_db_path = os.environ.get("TEST_DB_PATH") + if test_db_path: + connection.init_database(test_db_path, create_tables=True) + client.application.config["DATA_FOLDER_PATH"] = original_data_folder def test_save_viewable_workflow_from_file(client): route = "/opengeodeweb_back/save_viewable_file" From 7d98acc47f4b0fcb3260f064518007f6cfde2c58 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:51:25 +0000 Subject: [PATCH 24/31] Apply prepare changes --- .../routes/blueprint_routes.py | 6 +----- tests/test_models_routes.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index cf6ab717..3271686d 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -390,19 +390,15 @@ def import_project() -> flask.Response: if os.path.isfile(vpath): continue - input_file = str(data_entry.input_file or "") if not input_file: continue - input_full = geode_functions.data_file_path(data_entry.id, input_file) if not os.path.isfile(input_full): continue - data_object = geode_functions.load( - data_entry.geode_object, input_full - ) + data_object = geode_functions.load(data_entry.geode_object, input_full) utils_functions.save_all_viewables_and_return_info( data_entry.geode_object, data_object, data_entry, data_path ) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index a8b5b38c..8bfd0151 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -92,20 +92,26 @@ def test_export_project_route(client, tmp_path): def test_import_project_route(client, tmp_path): route = "/opengeodeweb_back/import_project" - snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}} + snapshot = { + "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} + } original_data_folder = client.application.config["DATA_FOLDER_PATH"] - client.application.config["DATA_FOLDER_PATH"] = os.path.join(str(tmp_path), "project_data") + client.application.config["DATA_FOLDER_PATH"] = os.path.join( + str(tmp_path), "project_data" + ) db_path = os.path.join(client.application.config["DATA_FOLDER_PATH"], "project.db") import sqlite3, zipfile, json + temp_db = tmp_path / "temp_project.db" conn = sqlite3.connect(str(temp_db)) conn.execute( "CREATE TABLE datas (id TEXT PRIMARY KEY, geode_object TEXT, viewer_object TEXT, native_file_name TEXT, " "viewable_file_name TEXT, light_viewable TEXT, input_file TEXT, additional_files TEXT)" ) - conn.commit(); conn.close() + conn.commit() + conn.close() z = tmp_path / "import_project_test.zip" with zipfile.ZipFile(z, "w", compression=zipfile.ZIP_DEFLATED) as zipf: @@ -114,7 +120,9 @@ def test_import_project_route(client, tmp_path): with open(z, "rb") as f: resp = client.post( - route, data={"file": (f, "import_project_test.zip")}, content_type="multipart/form-data" + route, + data={"file": (f, "import_project_test.zip")}, + content_type="multipart/form-data", ) assert resp.status_code == 200 @@ -122,6 +130,7 @@ def test_import_project_route(client, tmp_path): assert os.path.exists(db_path) from opengeodeweb_microservice.database import connection + client.application.config["DATA_FOLDER_PATH"] = original_data_folder test_db_path = os.environ.get("TEST_DB_PATH") if test_db_path: @@ -129,6 +138,7 @@ def test_import_project_route(client, tmp_path): client.application.config["DATA_FOLDER_PATH"] = original_data_folder + def test_save_viewable_workflow_from_file(client): route = "/opengeodeweb_back/save_viewable_file" payload = {"input_geode_object": "BRep", "filename": "cube.og_brep"} From df305decd483bd56b7b708fe3fac53467ebfce6e Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:53:51 +0000 Subject: [PATCH 25/31] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 531947b5..4e85944d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.7 From d02e079b6b9c4e1434c24a9d0b5e5c80322a8750 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:51:40 +0000 Subject: [PATCH 26/31] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 531947b5..4e85944d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.7 From f4bc1d6098d1a379b41987e709d8f541c36a7c83 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:17:56 +0000 Subject: [PATCH 27/31] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dfc7c9b3..4e85944d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.8 From 01c0152f174a1c15678cd253d5738a83c73b53d4 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Nov 2025 15:47:36 +0100 Subject: [PATCH 28/31] no longer .zip files --- .../routes/blueprint_routes.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 3499bd71..797cd462 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -287,13 +287,15 @@ def export_project() -> flask.Response: os.makedirs(project_folder, exist_ok=True) filename: str = werkzeug.utils.secure_filename(os.path.basename(params.filename)) - export_zip_path = os.path.join(project_folder, filename) + if not filename.lower().endswith(".vease"): + flask.abort(400, "Requested filename must end with .vease") + export_vease_path = os.path.join(project_folder, filename) with get_session() as session: rows = session.query(Data.id, Data.input_file, Data.additional_files).all() with zipfile.ZipFile( - export_zip_path, "w", compression=zipfile.ZIP_DEFLATED + export_vease_path, "w", compression=zipfile.ZIP_DEFLATED ) as zip_file: database_root_path = os.path.join(project_folder, "project.db") if os.path.isfile(database_root_path): @@ -317,7 +319,7 @@ def export_project() -> flask.Response: zip_file.writestr("snapshot.json", flask.json.dumps(params.snapshot)) - return utils_functions.send_file(project_folder, [export_zip_path], filename) + return utils_functions.send_file(project_folder, [export_vease_path], filename) @routes.route( @@ -325,17 +327,16 @@ def export_project() -> flask.Response: methods=schemas_dict["import_project"]["methods"], ) def import_project() -> flask.Response: - if flask.request.method == "OPTIONS": - return flask.make_response({}, 200) + # if flask.request.method == "OPTIONS": + # return flask.make_response({}, 200) utils_functions.validate_request(flask.request, schemas_dict["import_project"]) if "file" not in flask.request.files: - flask.abort(400, "No zip file provided under 'file'") - + flask.abort(400, "No .vease file provided under 'file'") zip_file = flask.request.files["file"] assert zip_file.filename is not None filename = werkzeug.utils.secure_filename(os.path.basename(zip_file.filename)) - if not filename.lower().endswith(".zip"): - flask.abort(400, "Uploaded file must be a .zip") + if not filename.lower().endswith(".vease"): + flask.abort(400, "Uploaded file must be a .vease") data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"] From 7d194771cac202c44d66d1a6bb7ec3bd7037303b Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Nov 2025 15:53:12 +0100 Subject: [PATCH 29/31] clean --- src/opengeodeweb_back/routes/blueprint_routes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 797cd462..f44788f5 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -327,8 +327,6 @@ def export_project() -> flask.Response: methods=schemas_dict["import_project"]["methods"], ) def import_project() -> flask.Response: - # if flask.request.method == "OPTIONS": - # return flask.make_response({}, 200) utils_functions.validate_request(flask.request, schemas_dict["import_project"]) if "file" not in flask.request.files: flask.abort(400, "No .vease file provided under 'file'") @@ -366,7 +364,7 @@ def import_project() -> flask.Response: if not ( target == project_folder or target.startswith(project_folder + os.sep) ): - flask.abort(400, "Zip contains unsafe paths") + flask.abort(400, "Vease file contains unsafe paths") zip_archive.extractall(project_folder) database_root_path = os.path.join(project_folder, "project.db") From a7675537646fb37848bb1514e818c03c3db772a6 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Nov 2025 15:55:39 +0100 Subject: [PATCH 30/31] test From 1e3a21064af3c0c35b674d9303e4175f62c91b77 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Nov 2025 16:34:28 +0100 Subject: [PATCH 31/31] tests --- tests/test_models_routes.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 08b363a6..9507316c 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -1,10 +1,10 @@ import os import shutil -import flask from opengeodeweb_back import geode_functions from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session +from werkzeug.datastructures import FileStorage import zipfile import json @@ -60,7 +60,7 @@ def test_export_project_route(client, tmp_path): snapshot = { "styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}} } - filename = "export_project_test.zip" + filename = "export_project_test.vease" project_folder = client.application.config["DATA_FOLDER_PATH"] os.makedirs(project_folder, exist_ok=True) database_root_path = os.path.join(project_folder, "project.db") @@ -109,7 +109,7 @@ def test_import_project_route(client, tmp_path): conn.commit() conn.close() - z = tmp_path / "import_project_test.zip" + z = tmp_path / "import_project_test.vease" with zipfile.ZipFile(z, "w", compression=zipfile.ZIP_DEFLATED) as zipf: zipf.writestr("snapshot.json", json.dumps(snapshot)) zipf.write(str(temp_db), "project.db") @@ -117,7 +117,7 @@ def test_import_project_route(client, tmp_path): with open(z, "rb") as f: resp = client.post( route, - data={"file": (f, "import_project_test.zip")}, + data={"file": (f, "import_project_test.vease")}, content_type="multipart/form-data", ) @@ -136,6 +136,13 @@ def test_import_project_route(client, tmp_path): def test_save_viewable_workflow_from_file(client): + file = os.path.join(data_dir, "cube.og_brep") + upload_resp = client.put( + "/opengeodeweb_back/upload_file", + data={"file": FileStorage(open(file, "rb"))}, + ) + assert upload_resp.status_code == 201 + route = "/opengeodeweb_back/save_viewable_file" payload = {"input_geode_object": "BRep", "filename": "cube.og_brep"}