[ogr] Handle auto addition of vsizip prefix for vsicurl archives

Fixes #61561
This commit is contained in:
Nyall Dawson 2025-05-16 11:00:53 +10:00
parent 2d41b76b7e
commit e9a0bd332e
3 changed files with 100 additions and 2 deletions

View File

@ -790,7 +790,8 @@ QList<QgsProviderSublayerDetails> QgsOgrProviderMetadata::querySublayers( const
// Try to open using VSIFileHandler // Try to open using VSIFileHandler
const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( uriParts.value( QStringLiteral( "path" ) ).toString() ); const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( uriParts.value( QStringLiteral( "path" ) ).toString() );
if ( !vsiPrefix.isEmpty() && uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString().isEmpty() ) if ( !vsiPrefix.isEmpty() && ( uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString().isEmpty()
|| ( QgsGdalUtils::isVsiArchivePrefix( vsiPrefix ) && uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString() != vsiPrefix ) ) )
{ {
if ( !uri.startsWith( vsiPrefix ) ) if ( !uri.startsWith( vsiPrefix ) )
{ {

View File

@ -952,7 +952,14 @@ QList<QgsGdalUtils::VsiNetworkFileSystemDetails> QgsGdalUtils::vsiNetworkFileSys
bool QgsGdalUtils::isVsiArchivePrefix( const QString &prefix ) bool QgsGdalUtils::isVsiArchivePrefix( const QString &prefix )
{ {
return vsiArchivePrefixes().contains( prefix ); const QStringList prefixes = vsiArchivePrefixes();
for ( const QString &archivePrefix : prefixes )
{
// catch chained prefixes, eg "/vsizip/vsicurl"
if ( prefix.contains( archivePrefix ) )
return true;
}
return false;
} }
QStringList QgsGdalUtils::vsiArchiveFileExtensions() QStringList QgsGdalUtils::vsiArchiveFileExtensions()

View File

@ -17,6 +17,12 @@ import sys
import tempfile import tempfile
import math import math
from datetime import datetime from datetime import datetime
import http.server
import os
import socketserver
import threading
import time
import shutil
from osgeo import gdal, ogr # NOQA from osgeo import gdal, ogr # NOQA
from qgis.PyQt.QtCore import QByteArray, QTemporaryDir, QVariant from qgis.PyQt.QtCore import QByteArray, QTemporaryDir, QVariant
@ -119,6 +125,19 @@ class PyQgsOGRProvider(QgisTestCase):
cls.dirs_to_cleanup = [cls.basetestpath] cls.dirs_to_cleanup = [cls.basetestpath]
# Bring up a simple HTTP server, for vsicurl tests
os.chdir(unitTestDataPath() + "")
cls.httpd = socketserver.TCPServer(
("localhost", 0), http.server.SimpleHTTPRequestHandler
)
cls.port = cls.httpd.server_address[1]
cls.port = cls.httpd.server_address[1]
cls.httpd_thread = threading.Thread(target=cls.httpd.serve_forever)
cls.httpd_thread.daemon = True
cls.httpd_thread.start()
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
"""Run after all tests""" """Run after all tests"""
@ -661,6 +680,11 @@ class PyQgsOGRProvider(QgisTestCase):
self.assertEqual(gdal.GetConfigOption("GDAL_HTTP_PROXY"), "myproxyhostname.com") self.assertEqual(gdal.GetConfigOption("GDAL_HTTP_PROXY"), "myproxyhostname.com")
self.assertEqual(gdal.GetConfigOption("GDAL_HTTP_PROXYUSERPWD"), "username") self.assertEqual(gdal.GetConfigOption("GDAL_HTTP_PROXYUSERPWD"), "username")
settings.setValue("proxy/proxyEnabled", False)
QgsNetworkAccessManager.instance().setupDefaultProxyAndCache()
gdal.SetConfigOption("GDAL_HTTP_PROXY", "")
gdal.SetConfigOption("GDAL_HTTP_PROXYUSERPWD", "")
def testEditGeoJsonRemoveField(self): def testEditGeoJsonRemoveField(self):
"""Test bugfix of https://github.com/qgis/QGIS/issues/26484 (deleting an existing field)""" """Test bugfix of https://github.com/qgis/QGIS/issues/26484 (deleting an existing field)"""
@ -3271,6 +3295,72 @@ class PyQgsOGRProvider(QgisTestCase):
for feature in layer.getFeatures(): for feature in layer.getFeatures():
self.assertEqual(feature.geometry().wkbType(), QgsWkbTypes.MultiPolygon) self.assertEqual(feature.geometry().wkbType(), QgsWkbTypes.MultiPolygon)
# vsicurl
res = metadata.querySublayers(
f"/vsicurl/http://localhost:{self.port}/polys.shp"
)
self.assertEqual(len(res), 1)
self.assertEqual(res[0].layerNumber(), 0)
self.assertEqual(res[0].name(), "polys")
self.assertEqual(res[0].description(), "")
self.assertEqual(
res[0].uri(),
f"/vsicurl/http://localhost:{self.port}/polys.shp|layername=polys",
)
self.assertEqual(res[0].providerKey(), "ogr")
self.assertEqual(res[0].type(), QgsMapLayerType.VectorLayer)
self.assertEqual(res[0].featureCount(), Qgis.FeatureCountState.Uncounted)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.Type.Polygon)
self.assertEqual(res[0].geometryColumnName(), "")
self.assertEqual(res[0].driverName(), "ESRI Shapefile")
vl = res[0].toLayer(options)
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.MultiPolygon)
# vsicurl with zip
res = metadata.querySublayers(
f"/vsicurl/http://localhost:{self.port}/zip/points2.zip"
)
self.assertEqual(len(res), 1)
self.assertEqual(res[0].layerNumber(), 0)
self.assertEqual(res[0].name(), "points.shp")
self.assertEqual(res[0].description(), "")
self.assertEqual(
res[0].uri(),
f"/vsizip//vsicurl/http://localhost:{self.port}/zip/points2.zip/points.shp|layername=points",
)
self.assertEqual(res[0].providerKey(), "ogr")
self.assertEqual(res[0].type(), QgsMapLayerType.VectorLayer)
self.assertEqual(res[0].featureCount(), Qgis.FeatureCountState.Uncounted)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.Type.Point)
self.assertEqual(res[0].geometryColumnName(), "")
self.assertEqual(res[0].driverName(), "ESRI Shapefile")
vl = res[0].toLayer(options)
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.Point)
# vsicurl with zip, explicit vsizip prefix
res = metadata.querySublayers(
f"/vsizip//vsicurl/http://localhost:{self.port}/zip/points2.zip"
)
self.assertEqual(len(res), 1)
self.assertEqual(res[0].layerNumber(), 0)
self.assertEqual(res[0].name(), "points")
self.assertEqual(res[0].description(), "")
self.assertEqual(
res[0].uri(),
f"/vsizip//vsicurl/http://localhost:{self.port}/zip/points2.zip|layername=points",
)
self.assertEqual(res[0].providerKey(), "ogr")
self.assertEqual(res[0].type(), QgsMapLayerType.VectorLayer)
self.assertEqual(res[0].featureCount(), Qgis.FeatureCountState.Uncounted)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.Type.Point)
self.assertEqual(res[0].geometryColumnName(), "")
self.assertEqual(res[0].driverName(), "ESRI Shapefile")
vl = res[0].toLayer(options)
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.Point)
@unittest.skipIf( @unittest.skipIf(
int(gdal.VersionInfo("VERSION_NUM")) < GDAL_COMPUTE_VERSION(3, 4, 0), int(gdal.VersionInfo("VERSION_NUM")) < GDAL_COMPUTE_VERSION(3, 4, 0),
"GDAL 3.4 required", "GDAL 3.4 required",