Skip to content

Commit e6ff518

Browse files
committed
feat(extensions): add new blueprint and tests for extension imports
1 parent d8631fa commit e6ff518

File tree

5 files changed

+177
-0
lines changed

5 files changed

+177
-0
lines changed

src/opengeodeweb_back/routes/blueprint_routes.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,80 @@ def import_project() -> flask.Response:
502502
except KeyError:
503503
snapshot = {}
504504
return flask.make_response({"snapshot": snapshot}, 200)
505+
506+
507+
@routes.route(
508+
schemas_dict["import_extension"]["route"],
509+
methods=schemas_dict["import_extension"]["methods"],
510+
)
511+
def import_extension() -> flask.Response:
512+
"""Import a .vext extension file and extract its contents."""
513+
utils_functions.validate_request(flask.request, schemas_dict["import_extension"])
514+
515+
if "file" not in flask.request.files:
516+
flask.abort(400, "No .vext file provided under 'file'")
517+
518+
vext_file = flask.request.files["file"]
519+
assert vext_file.filename is not None
520+
filename = werkzeug.utils.secure_filename(os.path.basename(vext_file.filename))
521+
522+
if not filename.lower().endswith(".vext"):
523+
flask.abort(400, "Uploaded file must be a .vext")
524+
525+
# Create extensions directory in the data folder
526+
data_folder_path: str = flask.current_app.config.get("DATA_FOLDER_PATH", "")
527+
extensions_folder = os.path.join(data_folder_path, "extensions")
528+
os.makedirs(extensions_folder, exist_ok=True)
529+
530+
# Extract extension name from filename (e.g., "vease-modeling-0.0.0.vext" -> "vease-modeling")
531+
extension_name = (
532+
filename.rsplit("-", 1)[0] if "-" in filename else filename.replace(".vext", "")
533+
)
534+
extension_path = os.path.join(extensions_folder, extension_name)
535+
536+
# Remove existing extension if present
537+
if os.path.exists(extension_path):
538+
shutil.rmtree(extension_path)
539+
540+
os.makedirs(extension_path, exist_ok=True)
541+
542+
# Extract the .vext file
543+
vext_file.stream.seek(0)
544+
with zipfile.ZipFile(vext_file.stream) as zip_archive:
545+
zip_archive.extractall(extension_path)
546+
547+
# Find the extracted files
548+
dist_path = os.path.join(extension_path, "dist")
549+
if not os.path.exists(dist_path):
550+
flask.abort(400, "Invalid .vext file: missing dist folder")
551+
552+
# Look for the backend executable and frontend JS
553+
backend_executable = None
554+
frontend_file = None
555+
556+
for file in os.listdir(dist_path):
557+
file_path = os.path.join(dist_path, file)
558+
if os.path.isfile(file_path):
559+
if file.endswith(".es.js"):
560+
frontend_file = file_path
561+
elif not file.endswith(".js") and not file.endswith(".css"):
562+
# Assume it's the backend executable
563+
backend_executable = file_path
564+
# Make it executable
565+
os.chmod(backend_executable, 0o755)
566+
567+
if not frontend_file:
568+
flask.abort(400, "Invalid .vext file: missing frontend JavaScript")
569+
570+
if not backend_executable:
571+
flask.abort(400, "Invalid .vext file: missing backend executable")
572+
573+
return flask.make_response(
574+
{
575+
"extension_name": extension_name,
576+
"frontend_path": frontend_file,
577+
"backend_path": backend_executable,
578+
"extension_folder": extension_path,
579+
},
580+
200,
581+
)

src/opengeodeweb_back/routes/schemas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
from .cell_attribute_names import *
1616
from .allowed_objects import *
1717
from .allowed_files import *
18+
from .import_extension import *
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"route": "/import_extension",
3+
"methods": [
4+
"POST"
5+
],
6+
"type": "object",
7+
"properties": {},
8+
"required": [],
9+
"additionalProperties": false
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from dataclasses_json import DataClassJsonMixin
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class ImportExtension(DataClassJsonMixin):
7+
def __post_init__(self) -> None:
8+
print(self, flush=True)
9+
10+
pass

tests/test_models_routes.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,82 @@ def test_save_viewable_workflow_from_object(client: FlaskClient) -> None:
185185
assert isinstance(data_id, str) and len(data_id) > 0
186186
assert response.get_json()["geode_object_type"] == "EdgedCurve3D"
187187
assert response.get_json()["viewable_file"].endswith(".vtp")
188+
189+
190+
def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None:
191+
"""Test importing a .vext extension file."""
192+
route = "/opengeodeweb_back/import_extension"
193+
original_data_folder = client.application.config["DATA_FOLDER_PATH"]
194+
client.application.config["DATA_FOLDER_PATH"] = os.path.join(
195+
str(tmp_path), "extension_test_data"
196+
)
197+
vext_path = tmp_path / "test-extension-1.0.0.vext"
198+
with zipfile.ZipFile(vext_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
199+
zipf.writestr(
200+
"dist/test-extension-extension.es.js",
201+
"export const metadata = { id: 'test-extension', name: 'Test Extension' };",
202+
)
203+
zipf.writestr("dist/test-extension-back", "#!/bin/bash\necho 'mock backend'")
204+
zipf.writestr("dist/test-extension.css", ".test { color: red; }")
205+
with open(vext_path, "rb") as f:
206+
response = client.post(
207+
route,
208+
data={"file": (f, "test-extension-1.0.0.vext")},
209+
content_type="multipart/form-data",
210+
)
211+
assert response.status_code == 200
212+
json_data = response.get_json()
213+
assert "extension_name" in json_data
214+
assert "frontend_path" in json_data
215+
assert "backend_path" in json_data
216+
assert "extension_folder" in json_data
217+
assert json_data["extension_name"] == "test-extension"
218+
extensions_folder = os.path.join(
219+
client.application.config["DATA_FOLDER_PATH"], "extensions"
220+
)
221+
extension_path = os.path.join(extensions_folder, "test-extension")
222+
assert os.path.exists(extension_path)
223+
dist_path = os.path.join(extension_path, "dist")
224+
assert os.path.exists(dist_path)
225+
frontend_js = json_data["frontend_path"]
226+
assert os.path.exists(frontend_js)
227+
assert frontend_js.endswith("-extension.es.js")
228+
backend_exec = json_data["backend_path"]
229+
assert os.path.exists(backend_exec)
230+
assert os.access(backend_exec, os.X_OK)
231+
client.application.config["DATA_FOLDER_PATH"] = original_data_folder
232+
233+
234+
def test_import_extension_invalid_file(client: FlaskClient, tmp_path: Path) -> None:
235+
"""Test importing an invalid .vext file (missing dist folder)."""
236+
route = "/opengeodeweb_back/import_extension"
237+
original_data_folder = client.application.config["DATA_FOLDER_PATH"]
238+
client.application.config["DATA_FOLDER_PATH"] = os.path.join(
239+
str(tmp_path), "extension_invalid_test"
240+
)
241+
vext_path = tmp_path / "invalid-extension.vext"
242+
with zipfile.ZipFile(vext_path, "w") as zipf:
243+
zipf.writestr("README.md", "This is invalid")
244+
with open(vext_path, "rb") as f:
245+
response = client.post(
246+
route,
247+
data={"file": (f, "invalid-extension.vext")},
248+
content_type="multipart/form-data",
249+
)
250+
assert response.status_code == 400
251+
client.application.config["DATA_FOLDER_PATH"] = original_data_folder
252+
253+
254+
def test_import_extension_wrong_extension(client: FlaskClient, tmp_path: Path) -> None:
255+
"""Test uploading a file with wrong extension."""
256+
route = "/opengeodeweb_back/import_extension"
257+
wrong_file = tmp_path / "not-an-extension.zip"
258+
with open(wrong_file, "wb") as f:
259+
f.write(b"test content")
260+
with open(wrong_file, "rb") as f:
261+
response = client.post(
262+
route,
263+
data={"file": (f, "not-an-extension.zip")},
264+
content_type="multipart/form-data",
265+
)
266+
assert response.status_code == 400

0 commit comments

Comments
 (0)