[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
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 ) )
{

View File

@ -952,7 +952,14 @@ QList<QgsGdalUtils::VsiNetworkFileSystemDetails> QgsGdalUtils::vsiNetworkFileSys
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()

View File

@ -17,6 +17,12 @@ import sys
import tempfile
import math
from datetime import datetime
import http.server
import os
import socketserver
import threading
import time
import shutil
from osgeo import gdal, ogr # NOQA
from qgis.PyQt.QtCore import QByteArray, QTemporaryDir, QVariant
@ -119,6 +125,19 @@ class PyQgsOGRProvider(QgisTestCase):
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
def tearDownClass(cls):
"""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_PROXYUSERPWD"), "username")
settings.setValue("proxy/proxyEnabled", False)
QgsNetworkAccessManager.instance().setupDefaultProxyAndCache()
gdal.SetConfigOption("GDAL_HTTP_PROXY", "")
gdal.SetConfigOption("GDAL_HTTP_PROXYUSERPWD", "")
def testEditGeoJsonRemoveField(self):
"""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():
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(
int(gdal.VersionInfo("VERSION_NUM")) < GDAL_COMPUTE_VERSION(3, 4, 0),
"GDAL 3.4 required",