Skip to content

Commit ac85cf4

Browse files
committed
feat(export_and_import_routes): add export, import route and schemas for save and load feature
1 parent 2bc4304 commit ac85cf4

7 files changed

Lines changed: 133 additions & 2 deletions

File tree

src/opengeodeweb_back/routes/blueprint_routes.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# Third party imports
66
import flask
77
import werkzeug
8+
import zipfile
9+
import glob
810
from opengeodeweb_microservice.schemas import get_schemas_dict
911

1012
# Local application imports
@@ -267,3 +269,29 @@ def kill() -> flask.Response:
267269
print("Manual server kill, shutting down...", flush=True)
268270
os._exit(0)
269271
return flask.make_response({"message": "Flask server is dead"}, 200)
272+
273+
274+
@routes.route(
275+
schemas_dict["export_project"]["route"],
276+
methods=schemas_dict["export_project"]["methods"],
277+
)
278+
def export_project() -> flask.Response:
279+
utils_functions.validate_request(flask.request, schemas_dict["export_project"])
280+
params = schemas.ExportProject.from_dict(flask.request.get_json())
281+
282+
data_folder_path: str = flask.current_app.config["DATA_FOLDER_PATH"]
283+
upload_folder: str = flask.current_app.config["UPLOAD_FOLDER"]
284+
os.makedirs(upload_folder, exist_ok=True)
285+
286+
filename: str = params.filename or f"project_{int(time.time())}.zip"
287+
export_zip_path = os.path.join(upload_folder, filename)
288+
289+
with zipfile.ZipFile(export_zip_path, "w", compression=8) as zipf:
290+
pattern = os.path.join(data_folder_path, "**", "*")
291+
for full_path in glob.glob(pattern, recursive=True):
292+
if os.path.isfile(full_path):
293+
archive_name = os.path.relpath(full_path, start=data_folder_path)
294+
zipf.write(full_path, archive_name)
295+
zipf.writestr("snapshot.json", flask.json.dumps(params.snapshot))
296+
297+
return utils_functions.send_file(upload_folder, [export_zip_path], filename)

src/opengeodeweb_back/routes/schemas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
from .geode_objects_and_output_extensions import *
1313
from .allowed_objects import *
1414
from .allowed_files import *
15+
from .export_project import *
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"route": "/export_project",
3+
"methods": [
4+
"POST"
5+
],
6+
"type": "object",
7+
"properties": {
8+
"snapshot": {
9+
"type": "object"
10+
},
11+
"filename": {
12+
"type": "string",
13+
"minLength": 1
14+
}
15+
},
16+
"required": [
17+
"snapshot"
18+
],
19+
"additionalProperties": false
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from dataclasses_json import DataClassJsonMixin
2+
from dataclasses import dataclass
3+
from typing import Dict, Any, Optional
4+
5+
6+
@dataclass
7+
class ExportProject(DataClassJsonMixin):
8+
snapshot: Dict[str, Any]
9+
filename: Optional[str] = None

src/opengeodeweb_back/utils_functions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,17 @@ def send_file(
136136
else:
137137
mimetype = "application/zip"
138138
new_file_name = os.path.splitext(new_file_name)[0] + ".zip"
139-
with zipfile.ZipFile(os.path.join(upload_folder, new_file_name), "w") as zipObj:
139+
with zipfile.ZipFile(
140+
os.path.join(os.path.abspath(upload_folder), new_file_name), "w"
141+
) as zipObj:
140142
for saved_file_path in saved_files:
141143
zipObj.write(
142144
saved_file_path,
143145
os.path.basename(saved_file_path),
144146
)
145147

146148
response = flask.send_from_directory(
147-
directory=upload_folder,
149+
directory=os.path.abspath(upload_folder),
148150
path=new_file_name,
149151
as_attachment=True,
150152
mimetype=mimetype,

tests/test_models_routes.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from opengeodeweb_back import geode_functions
66
from opengeodeweb_microservice.database.data import Data
77
from opengeodeweb_microservice.database.connection import get_session
8+
import zipfile
9+
import json
10+
import io
811

912

1013
def test_model_mesh_components(client, test_id):
@@ -55,3 +58,26 @@ def test_extract_brep_uuids(client, test_id):
5558
assert "uuid_dict" in response.json
5659
uuid_dict = response.json["uuid_dict"]
5760
assert isinstance(uuid_dict, dict)
61+
62+
63+
def test_export_project_route(client):
64+
route = "/opengeodeweb_back/export_project"
65+
snapshot = {"styles": {"1": {"visibility": True, "opacity": 1.0, "color": [0.2, 0.6, 0.9]}}}
66+
filename = "export_project_test.zip"
67+
response = client.post(route, json={"snapshot": snapshot, "filename": filename})
68+
assert response.status_code == 200
69+
assert response.headers.get("new-file-name") == filename
70+
assert response.mimetype == "application/octet-binary"
71+
response.direct_passthrough = False
72+
data = response.get_data()
73+
with zipfile.ZipFile(io.BytesIO(data), "r") as zf:
74+
names = zf.namelist()
75+
assert "snapshot.json" in names
76+
parsed = json.loads(zf.read("snapshot.json").decode("utf-8"))
77+
assert parsed == snapshot
78+
assert "1/project.db" in names
79+
response.close()
80+
upload_folder = client.application.config["UPLOAD_FOLDER"]
81+
export_path = os.path.join(upload_folder, filename)
82+
if os.path.exists(export_path):
83+
os.remove(export_path)

tests/test_utils_functions.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import flask
77
import shutil
88
import uuid
9+
import glob
10+
import zipfile
11+
import io
912

1013
# Local application imports
1114
from opengeodeweb_microservice.database.data import Data
@@ -206,3 +209,45 @@ def test_generate_native_viewable_and_light_viewable_from_file(client):
206209
assert isinstance(result["object_type"], str)
207210
assert isinstance(result["binary_light_viewable"], str)
208211
assert isinstance(result["input_file"], str)
212+
213+
214+
def test_send_file_multiple_returns_zip(client, tmp_path):
215+
app = client.application
216+
with app.app_context():
217+
app.config["UPLOAD_FOLDER"] = str(tmp_path)
218+
file_paths = []
219+
for i, content in [(1, b"hello 1"), (2, b"hello 2")]:
220+
file_path = tmp_path / f"tmp_send_file_{i}.txt"
221+
file_path.write_bytes(content)
222+
file_paths.append(str(file_path))
223+
with app.test_request_context():
224+
response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], file_paths, "bundle")
225+
assert response.status_code == 200
226+
assert response.mimetype == "application/zip"
227+
assert response.headers.get("new-file-name") == "bundle.zip"
228+
229+
response.direct_passthrough = False
230+
zip_bytes = response.get_data()
231+
with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zip_file:
232+
zip_entries = zip_file.namelist()
233+
assert "tmp_send_file_1.txt" in zip_entries
234+
assert "tmp_send_file_2.txt" in zip_entries
235+
response.close()
236+
237+
238+
def test_send_file_single_returns_octet_binary(client, tmp_path):
239+
app = client.application
240+
with app.app_context():
241+
app.config["UPLOAD_FOLDER"] = str(tmp_path)
242+
file_path = tmp_path / "tmp_send_file_1.txt"
243+
file_path.write_bytes(b"hello 1")
244+
with app.test_request_context():
245+
response = utils_functions.send_file(app.config["UPLOAD_FOLDER"], [str(file_path)], "tmp_send_file_1.txt")
246+
assert response.status_code == 200
247+
assert response.mimetype == "application/octet-binary"
248+
assert response.headers.get("new-file-name") == "tmp_send_file_1.txt"
249+
250+
response.direct_passthrough = False
251+
file_bytes = response.get_data()
252+
assert file_bytes == b"hello 1"
253+
response.close()

0 commit comments

Comments
 (0)