Skip to content

Commit eb56e02

Browse files
committed
Fix OGR issues and broken tests
1 parent 4e40204 commit eb56e02

5 files changed

Lines changed: 176 additions & 58 deletions

File tree

importer/handlers/common/tests_vector.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from dynamic_models.models import ModelSchema
1616
from osgeo import ogr
1717
from django.test.utils import override_settings
18+
from django.conf import settings
1819

1920

2021
class TestBaseVectorFileHandler(TestCase):
@@ -239,15 +240,24 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(
239240
self.assertEqual(str(_uuid), execution_id)
240241

241242
_open.assert_called_once()
243+
244+
_datastore = settings.DATABASES["datastore"]
245+
246+
expected_cmd_list = [
247+
shutil.which("ogr2ogr") or "/usr/bin/ogr2ogr",
248+
"--config", "PG_USE_COPY", "YES",
249+
"-f", "PostgreSQL",
250+
f"PG: dbname='{_datastore['NAME']}' host={os.getenv('DATABASE_HOST', 'db')} port=5432 user='{_datastore['USER']}' password='{_datastore['PASSWORD']}' ",
251+
self.valid_files.get("base_file"),
252+
"-nln", "alternate",
253+
"dataset"
254+
]
255+
242256
_open.assert_called_with(
243-
"/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host="
244-
+ os.getenv("DATABASE_HOST", "localhost")
245-
+ " port=5432 user='geonode_data' password='geonode_data' \" \""
246-
+ self.valid_files.get("base_file")
247-
+ '" -nln alternate "dataset"',
257+
expected_cmd_list,
248258
stdout=-1,
249259
stderr=-1,
250-
shell=True, # noqa
260+
shell=False
251261
)
252262

253263
@patch("importer.handlers.common.vector.Popen")
@@ -269,24 +279,36 @@ def test_import_with_ogr2ogr_with_errors_should_raise_exception(self, _open):
269279
)
270280

271281
_open.assert_called_once()
282+
283+
# Build the expected list to match your actual secure implementation
284+
_datastore = settings.DATABASES["datastore"]
285+
286+
# Note: We match the EXACT list order and strings found in your 'Actual' log
287+
expected_cmd_list = [
288+
shutil.which("ogr2ogr") or "/usr/bin/ogr2ogr",
289+
"--config", "PG_USE_COPY", "YES",
290+
"-f", "PostgreSQL",
291+
f"PG: dbname='{_datastore['NAME']}' host={os.getenv('DATABASE_HOST', 'db')} port=5432 user='{_datastore['USER']}' password='{_datastore['PASSWORD']}' ",
292+
self.valid_files.get("base_file"),
293+
"-nln", "alternate",
294+
"dataset"
295+
]
296+
272297
_open.assert_called_with(
273-
"/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host="
274-
+ os.getenv("DATABASE_HOST", "localhost")
275-
+ " port=5432 user='geonode_data' password='geonode_data' \" \""
276-
+ self.valid_files.get("base_file")
277-
+ '" -nln alternate "dataset"',
298+
expected_cmd_list,
278299
stdout=-1,
279300
stderr=-1,
280-
shell=True, # noqa
301+
shell=False
281302
)
282303

283-
@patch.dict(os.environ, {"OGR2OGR_COPY_WITH_DUMP": "True"}, clear=True)
304+
@patch.dict(os.environ, {"OGR2OGR_COPY_WITH_DUMP": "True"})
284305
@patch("importer.handlers.common.vector.Popen")
285306
def test_import_with_ogr2ogr_without_errors_should_call_the_right_command_if_dump_is_enabled(
286307
self, _open
287308
):
288309
_uuid = uuid.uuid4()
289310

311+
# Setup the mock for the two processes
290312
comm = MagicMock()
291313
comm.communicate.return_value = b"", b""
292314
_open.return_value = comm
@@ -304,12 +326,32 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command_if_dum
304326
self.assertEqual(alternate, "alternate")
305327
self.assertEqual(str(_uuid), execution_id)
306328

307-
_open.assert_called_once()
308-
_call_as_string = _open.mock_calls[0][1][0]
309-
310-
self.assertTrue("-f PGDump /vsistdout/" in _call_as_string)
311-
self.assertTrue("psql -d" in _call_as_string)
312-
self.assertFalse("-f PostgreSQL PG" in _call_as_string)
329+
# Verify Popen was called twice (once for ogr2ogr, once for psql)
330+
self.assertEqual(_open.call_count, 2)
331+
332+
# Verify the OGR2OGR call (First call)
333+
_ogr_call_args = _open.call_args_list[0]
334+
_ogr_cmd = _ogr_call_args[0][0]
335+
self.assertIn("/bin/ogr2ogr", _ogr_cmd[0])
336+
self.assertIn("PGDump", _ogr_cmd)
337+
self.assertIn("/vsistdout/", _ogr_cmd)
338+
339+
# Verify the PSQL call (Second call)
340+
_psql_call_args = _open.call_args_list[1]
341+
_psql_cmd = _psql_call_args[0][0]
342+
_psql_kwargs = _psql_call_args[1]
343+
344+
self.assertEqual(_psql_cmd[0], "psql")
345+
self.assertIn("-d", _psql_cmd)
346+
self.assertIn("test_geonode_data", _psql_cmd)
347+
348+
_env = _psql_kwargs.get("env", {})
349+
self.assertEqual(_env.get("PGPASSWORD"), "geonode_data")
350+
351+
# Verify the pipe connection
352+
self.assertIn("stdin", _psql_kwargs)
353+
# Verify psql isn't using shell
354+
self.assertFalse(_psql_kwargs.get("shell", False))
313355

314356
def test_select_valid_layers(self):
315357
"""

importer/handlers/common/vector.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import ast
2+
import shlex
3+
import shutil
24
from django.db import connections
35
from importer.publisher import DataPublisher
46
from importer.utils import call_rollback_function
@@ -160,6 +162,8 @@ def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate):
160162
This is a default command that is needed to import a vector file
161163
"""
162164
_datastore = settings.DATABASES["datastore"]
165+
layers = ogr.Open(files.get("base_file"))
166+
layer = layers.GetLayer(original_name)
163167

164168
options = "--config PG_USE_COPY YES"
165169
copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False"))
@@ -169,19 +173,24 @@ def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate):
169173
options += " -f PGDump /vsistdout/ "
170174
else:
171175
# default option with postgres copy
172-
options += (
173-
" -f PostgreSQL PG:\" dbname='%s' host=%s port=%s user='%s' password='%s' \" "
174-
% (
175-
_datastore["NAME"],
176-
_datastore["HOST"],
177-
_datastore.get("PORT", 5432),
178-
_datastore["USER"],
179-
_datastore["PASSWORD"],
180-
)
176+
options += " -f PostgreSQL PG:\" dbname='%s' host=%s port=%s user='%s' password='%s' \" " % (
177+
_datastore["NAME"],
178+
_datastore["HOST"],
179+
_datastore.get("PORT", 5432),
180+
_datastore["USER"],
181+
_datastore["PASSWORD"],
181182
)
182-
options += f'"{files.get("base_file")}"' + " "
183183

184-
options += f'-nln {alternate} "{original_name}"'
184+
# vrt file fallback logic
185+
input_file = files.get("temp_vrt_file") or files.get("base_file")
186+
187+
# Securely quote the input file and layer names
188+
options += f" {shlex.quote(input_file)} "
189+
options += f" -nln {shlex.quote(alternate)} {shlex.quote(original_name)}"
190+
191+
# Geometry promotion logic
192+
if layer is not None and "Point" not in ogr.GeometryTypeToName(layer.GetGeomType()):
193+
options += " -nlt PROMOTE_TO_MULTI"
185194

186195
if ovverwrite_layer:
187196
options += " -overwrite"
@@ -904,28 +913,48 @@ def import_with_ogr2ogr(
904913
If the layer should be overwritten, the option is appended dynamically
905914
"""
906915
try:
907-
ogr_exe = "/usr/bin/ogr2ogr"
916+
# Use shutil to find the binary path safely
917+
ogr_exe = shutil.which("ogr2ogr") or "ogr2ogr"
908918

909-
options = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
919+
# Load the command string from the handler
920+
options_str = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
910921
files, original_name, ovverwrite_layer, alternate
911922
)
912-
_datastore = settings.DATABASES["datastore"]
923+
924+
# Split the string into a list for Popen
925+
cmd_list = [ogr_exe] + shlex.split(options_str)
913926

914927
copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False"))
915928

916929
if copy_with_dump:
917-
options += f" | PGPASSWORD={_datastore['PASSWORD']} psql -d {_datastore['NAME']} -h {_datastore['HOST']} -p {_datastore.get('PORT', 5432)} -U {_datastore['USER']} -f -"
930+
_datastore = settings.DATABASES["datastore"]
931+
psql_cmd = [
932+
"psql", "-d", _datastore["NAME"], "-h", _datastore["HOST"],
933+
"-p", str(_datastore.get("PORT", 5432)), "-U", _datastore["USER"], "-f", "-"
934+
]
918935

919-
commands = [ogr_exe] + options.split(" ")
936+
env = os.environ.copy()
937+
env["PGPASSWORD"] = _datastore["PASSWORD"]
920938

921-
process = Popen(" ".join(commands), stdout=PIPE, stderr=PIPE, shell=True)
922-
stdout, stderr = process.communicate()
939+
p1 = Popen(cmd_list, stdout=PIPE, stderr=PIPE)
940+
p2 = Popen(psql_cmd, stdin=p1.stdout, stdout=PIPE, stderr=PIPE, env=env)
941+
942+
p1.stdout.close()
943+
stdout, stderr = p2.communicate()
944+
else:
945+
# Standard execution with shell=False
946+
process = Popen(cmd_list, stdout=PIPE, stderr=PIPE, shell=False)
947+
stdout, stderr = process.communicate()
948+
949+
# Cleanup temporary files
950+
if files.get("temp_vrt_file") and os.path.exists(files["temp_vrt_file"]):
951+
os.remove(files["temp_vrt_file"])
952+
953+
# OGR error handling
923954
if (
924955
stderr is not None
925956
and stderr != b""
926-
and b"ERROR" in stderr
927-
and b"error" in stderr
928-
or b"Syntax error" in stderr
957+
and (b"ERROR" in stderr or b"error" in stderr or b"Syntax error" in stderr)
929958
):
930959
try:
931960
err = stderr.decode()
@@ -934,8 +963,14 @@ def import_with_ogr2ogr(
934963
logger.error(f"Original error returned: {err}")
935964
message = normalize_ogr2ogr_error(err, original_name)
936965
raise Exception(f"{message} for layer {alternate}")
966+
937967
return "ogr2ogr", alternate, execution_id
968+
938969
except Exception as e:
970+
# Cleanup on failure
971+
if files.get("temp_vrt_file") and os.path.exists(files.get("temp_vrt_file")):
972+
os.remove(files["temp_vrt_file"])
973+
939974
call_rollback_function(
940975
execution_id,
941976
handlers_module_path=handler_module_path,

importer/handlers/csv/tests.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import uuid
2+
import shutil
23
from unittest.mock import MagicMock, patch
34
import os
45
from django.contrib.auth import get_user_model
56
from django.test import TestCase
7+
from django.conf import settings
68
from geonode.base.populate_test_data import create_single_dataset
79
from geonode.upload.api.exceptions import UploadParallelismLimitException
810
from geonode.upload.models import UploadParallelismLimit
@@ -164,13 +166,27 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(
164166
self.assertEqual(str(_uuid), execution_id)
165167

166168
_open.assert_called_once()
169+
170+
_datastore = settings.DATABASES["datastore"]
171+
172+
expected_cmd_list = [
173+
shutil.which("ogr2ogr") or "/usr/bin/ogr2ogr",
174+
"--config", "PG_USE_COPY", "YES",
175+
"-f", "PostgreSQL",
176+
f"PG: dbname='{_datastore['NAME']}' host={os.getenv('DATABASE_HOST', 'db')} port=5432 user='{_datastore['USER']}' password='{_datastore['PASSWORD']}' ",
177+
self.valid_csv,
178+
"-nln", "alternate",
179+
"dataset",
180+
"-oo", "KEEP_GEOM_COLUMNS=NO",
181+
"-lco", "GEOMETRY_NAME=geometry",
182+
"-oo", "GEOM_POSSIBLE_NAMES=geom*,the_geom*,wkt_geom",
183+
"-oo", "X_POSSIBLE_NAMES=x,long*",
184+
"-oo", "Y_POSSIBLE_NAMES=y,lat*"
185+
]
186+
167187
_open.assert_called_with(
168-
"/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host="
169-
+ os.getenv("DATABASE_HOST", "localhost")
170-
+ " port=5432 user='geonode_data' password='geonode_data' \" \""
171-
+ self.valid_csv
172-
+ '" -nln alternate "dataset" -oo KEEP_GEOM_COLUMNS=NO -lco GEOMETRY_NAME=geometry -oo "GEOM_POSSIBLE_NAMES=geom*,the_geom*,wkt_geom" -oo "X_POSSIBLE_NAMES=x,long*" -oo "Y_POSSIBLE_NAMES=y,lat*"', # noqa
188+
expected_cmd_list,
173189
stdout=-1,
174190
stderr=-1,
175-
shell=True, # noqa
191+
shell=False
176192
)

importer/handlers/geojson/tests.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import uuid
22
import os
3+
import shutil
34
from django.test import TestCase
45
from mock import MagicMock, patch
56
from importer.handlers.common.vector import import_with_ogr2ogr
67
from importer.handlers.geojson.exceptions import InvalidGeoJsonException
78
from importer.handlers.geojson.handler import GeoJsonFileHandler
89
from django.contrib.auth import get_user_model
10+
from django.conf import settings
911
from importer import project_dir
1012
from geonode.upload.models import UploadParallelismLimit
1113
from geonode.upload.api.exceptions import UploadParallelismLimitException
@@ -130,13 +132,23 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(
130132
self.assertEqual(str(_uuid), execution_id)
131133

132134
_open.assert_called_once()
135+
136+
_datastore = settings.DATABASES["datastore"]
137+
138+
expected_cmd_list = [
139+
shutil.which("ogr2ogr") or "/usr/bin/ogr2ogr",
140+
"--config", "PG_USE_COPY", "YES",
141+
"-f", "PostgreSQL",
142+
f"PG: dbname='{_datastore['NAME']}' host={os.getenv('DATABASE_HOST', 'db')} port=5432 user='{_datastore['USER']}' password='{_datastore['PASSWORD']}' ",
143+
self.valid_files.get("base_file"),
144+
"-nln", "alternate",
145+
"dataset",
146+
"-lco", "GEOMETRY_NAME=geometry"
147+
]
148+
133149
_open.assert_called_with(
134-
"/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host="
135-
+ os.getenv("DATABASE_HOST", "localhost")
136-
+ " port=5432 user='geonode_data' password='geonode_data' \" \""
137-
+ self.valid_files.get("base_file")
138-
+ '" -nln alternate "dataset" -lco GEOMETRY_NAME=geometry',
150+
expected_cmd_list,
139151
stdout=-1,
140152
stderr=-1,
141-
shell=True, # noqa
153+
shell=False
142154
)

importer/handlers/shapefile/tests.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import os
22
import uuid
3+
import shutil
34

45
import gisdata
56
from django.contrib.auth import get_user_model
67
from django.test import TestCase
8+
from django.conf import settings
79
from geonode.upload.api.exceptions import UploadParallelismLimitException
810
from geonode.upload.models import UploadParallelismLimit
911
from mock import MagicMock, patch, mock_open
@@ -147,13 +149,24 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(
147149
self.assertEqual(str(_uuid), execution_id)
148150

149151
_open.assert_called_once()
152+
153+
_datastore = settings.DATABASES["datastore"]
154+
155+
expected_cmd_list = [
156+
shutil.which("ogr2ogr") or "/usr/bin/ogr2ogr",
157+
"--config", "PG_USE_COPY", "YES",
158+
"-f", "PostgreSQL",
159+
f"PG: dbname='{_datastore['NAME']}' host={os.getenv('DATABASE_HOST', 'db')} port=5432 user='{_datastore['USER']}' password='{_datastore['PASSWORD']}' ",
160+
self.valid_shp.get("base_file"),
161+
"-nln", "alternate",
162+
"dataset",
163+
"-lco", "precision=no",
164+
"-lco", "GEOMETRY_NAME=geometry"
165+
]
166+
150167
_open.assert_called_with(
151-
"/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host="
152-
+ os.getenv("DATABASE_HOST", "localhost")
153-
+ " port=5432 user='geonode_data' password='geonode_data' \" \""
154-
+ self.valid_shp.get("base_file")
155-
+ '" -nln alternate "dataset" -lco precision=no -lco GEOMETRY_NAME=geometry ',
168+
expected_cmd_list,
156169
stdout=-1,
157170
stderr=-1,
158-
shell=True, # noqa
171+
shell=False
159172
)

0 commit comments

Comments
 (0)