Skip to content

Commit 9c19863

Browse files
authored
Merge pull request #200 from Geode-solutions/feat/save_and_load
feat(save_and_load): add import/export methods
2 parents b3a7091 + 1e3a210 commit 9c19863

File tree

11 files changed

+415
-4
lines changed

11 files changed

+415
-4
lines changed

opengeodeweb_back_schemas.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,17 @@
332332
],
333333
"additionalProperties": false
334334
},
335+
"import_project": {
336+
"$id": "opengeodeweb_back/import_project",
337+
"route": "/import_project",
338+
"methods": [
339+
"POST"
340+
],
341+
"type": "object",
342+
"properties": {},
343+
"required": [],
344+
"additionalProperties": false
345+
},
335346
"geographic_coordinate_systems": {
336347
"$id": "opengeodeweb_back/geographic_coordinate_systems",
337348
"route": "/geographic_coordinate_systems",
@@ -373,6 +384,28 @@
373384
],
374385
"additionalProperties": false
375386
},
387+
"export_project": {
388+
"$id": "opengeodeweb_back/export_project",
389+
"route": "/export_project",
390+
"methods": [
391+
"POST"
392+
],
393+
"type": "object",
394+
"properties": {
395+
"snapshot": {
396+
"type": "object"
397+
},
398+
"filename": {
399+
"type": "string",
400+
"minLength": 1
401+
}
402+
},
403+
"required": [
404+
"snapshot",
405+
"filename"
406+
],
407+
"additionalProperties": false
408+
},
376409
"allowed_objects": {
377410
"$id": "opengeodeweb_back/allowed_objects",
378411
"route": "/allowed_objects",

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,3 @@ werkzeug==3.1.2
6060
# flask
6161
# flask-cors
6262

63-
opengeodeweb-microservice==1.*,>=1.0.8

src/opengeodeweb_back/routes/blueprint_routes.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
# Standard library imports
22
import os
33
import time
4+
import shutil
45

56
# Third party imports
67
import flask
78
import werkzeug
9+
import zipfile
810
from opengeodeweb_microservice.schemas import get_schemas_dict
911

1012
# Local application imports
1113
from opengeodeweb_back import geode_functions, utils_functions
1214
from .models import blueprint_models
1315
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
1419

1520
routes = flask.Blueprint("routes", __name__, url_prefix="/opengeodeweb_back")
1621

@@ -268,3 +273,141 @@ def kill() -> flask.Response:
268273
print("Manual server kill, shutting down...", flush=True)
269274
os._exit(0)
270275
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)

src/opengeodeweb_back/routes/schemas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from .missing_files import *
99
from .kill import *
1010
from .inspect_file import *
11+
from .import_project import *
1112
from .geographic_coordinate_systems import *
1213
from .geode_objects_and_output_extensions import *
14+
from .export_project import *
1315
from .allowed_objects import *
1416
from .allowed_files import *
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
"filename"
19+
],
20+
"additionalProperties": false
21+
}
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
4+
5+
6+
@dataclass
7+
class ExportProject(DataClassJsonMixin):
8+
filename: str
9+
snapshot: Dict[str, Any]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"route": "/import_project",
3+
"methods": [
4+
"POST"
5+
],
6+
"type": "object",
7+
"properties": {},
8+
"required": [],
9+
"additionalProperties": false
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from dataclasses_json import DataClassJsonMixin
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class ImportProject(DataClassJsonMixin):
7+
pass

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,

0 commit comments

Comments
 (0)