diff --git a/requirements.txt b/requirements.txt index 1f2a8398..cceba64b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,4 +66,3 @@ werkzeug==3.1.8 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.2.2 diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 4c02b6b5..04e1732b 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -73,13 +73,26 @@ def upload_file() -> flask.Response: if not os.path.exists(UPLOAD_FOLDER_PATH): os.makedirs(UPLOAD_FOLDER_PATH, exist_ok=True) - file = flask.request.files["file"] - if file.filename is None: - flask.abort(400, "Filename is required") - filename = werkzeug.utils.secure_filename(os.path.basename(file.filename)) + # Multipart callers (e.g. Vease) still send the file as a "file" form part; + # streaming callers PUT the raw bytes as the body with ?filename= as a query param. + if flask.request.mimetype == "multipart/form-data": + file = flask.request.files["file"] + if file.filename is None: + flask.abort(400, "Filename is required") + filename = werkzeug.utils.secure_filename(os.path.basename(file.filename)) + file_path = os.path.join(UPLOAD_FOLDER_PATH, filename) + file.save(file_path) + else: + raw_filename = flask.request.args.get("filename") + if not raw_filename: + flask.abort(400, "Filename is required") + filename = werkzeug.utils.secure_filename(os.path.basename(raw_filename)) + file_path = os.path.join(UPLOAD_FOLDER_PATH, filename) + chunk_size = 1024 * 1024 + with open(file_path, "wb") as destination: + while chunk := flask.request.stream.read(chunk_size): + destination.write(chunk) print(f"{filename=}", flush=True) - file_path = os.path.join(UPLOAD_FOLDER_PATH, filename) - file.save(file_path) if filename.lower().endswith(".csv.json"): shutil.copyfile( file_path, os.path.join(UPLOAD_FOLDER_PATH, filename[:-9] + ".json") diff --git a/tests/test_routes.py b/tests/test_routes.py index 7178dd15..3204ef48 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -79,6 +79,36 @@ def test_upload_file(client: FlaskClient, filename: str = "test.og_brep") -> Non assert response.status_code == 201 +def test_upload_file_raw(client: FlaskClient, filename: str = "test.og_brep") -> None: + file = os.path.join(data_dir, filename) + with open(file, "rb") as opened_file: + file_bytes = opened_file.read() + + raw_filename = "raw_upload_test.og_brep" + response = client.put( + f"/opengeodeweb_back/upload_file?filename={raw_filename}", + data=file_bytes, + content_type="application/octet-stream", + ) + assert response.status_code == 201 + + uploaded_path = os.path.join(data_dir, raw_filename) + try: + with open(uploaded_path, "rb") as uploaded_file: + assert uploaded_file.read() == file_bytes + finally: + os.remove(uploaded_path) + + +def test_upload_file_raw_missing_filename(client: FlaskClient) -> None: + response = client.put( + f"/opengeodeweb_back/upload_file", + data=b"some raw bytes", + content_type="application/octet-stream", + ) + assert response.status_code == 400 + + def test_missing_files(client: FlaskClient) -> None: route = f"/opengeodeweb_back/missing_files" @@ -495,7 +525,7 @@ def test_model_components(client: FlaskClient) -> None: assert isinstance(mesh_components, list) assert len(mesh_components) > 0 for mesh_component in mesh_components: - assert isinstance(mesh_component, object) + assert isinstance(mesh_component, dict) assert isinstance(mesh_component["geode_id"], str) assert isinstance(mesh_component["viewer_id"], int) assert isinstance(mesh_component["name"], str) @@ -511,7 +541,7 @@ def test_model_components(client: FlaskClient) -> None: collection_components = response.get_json()["collection_components"] assert isinstance(collection_components, list) for collection_component in collection_components: - assert isinstance(collection_component, object) + assert isinstance(collection_component, dict) assert isinstance(collection_component["geode_id"], str) assert isinstance(collection_component["name"], str) assert isinstance(collection_component["items"], list) @@ -659,15 +689,18 @@ def _load_brep_components(client: FlaskClient) -> tuple[str, dict[str, list[str] response = test_save_viewable_file(client, "BRep", "cube.og_brep") assert response.status_code == 200 model_id: str = response.get_json()["id"] - mesh_components: list[dict] = response.get_json()["mesh_components"] + mesh_components: list[dict[str, object]] = response.get_json()["mesh_components"] by_type: dict[str, list[str]] = {} - for mc in mesh_components: - component_type = mc["type"] - by_type.setdefault(component_type, []).append(mc["geode_id"]) + for mesh_component in mesh_components: + component_type = mesh_component["type"] + geode_id = mesh_component["geode_id"] + assert isinstance(component_type, str) + assert isinstance(geode_id, str) + by_type.setdefault(component_type, []).append(geode_id) return model_id, by_type -def _assert_attributes_response(response) -> None: +def _assert_attributes_response(response: TestResponse) -> None: assert response.status_code == 200 attributes = response.get_json()["attributes"] assert isinstance(attributes, list)