Fixed #32670 -- Allowed GDALRasters to use any GDAL virtual filesystem.

This commit is contained in:
Jordi Castells 2021-04-14 16:07:36 +02:00 committed by Mariusz Felisiak
parent 028f10fac6
commit 205c36b58f
6 changed files with 91 additions and 11 deletions

View File

@ -65,8 +65,11 @@ GDAL_COLOR_TYPES = {
16: 'GCI_YCbCr_CrBand', # Cr Chroma, also GCI_Max 16: 'GCI_YCbCr_CrBand', # Cr Chroma, also GCI_Max
} }
# GDAL virtual filesystems prefix.
VSI_FILESYSTEM_PREFIX = '/vsi'
# Fixed base path for buffer-based GDAL in-memory files. # Fixed base path for buffer-based GDAL in-memory files.
VSI_FILESYSTEM_BASE_PATH = '/vsimem/' VSI_MEM_FILESYSTEM_BASE_PATH = '/vsimem/'
# Should the memory file system take ownership of the buffer, freeing it when # Should the memory file system take ownership of the buffer, freeing it when
# the file is deleted? (No, GDALRaster.__del__() will delete the buffer.) # the file is deleted? (No, GDALRaster.__del__() will delete the buffer.)

View File

@ -12,8 +12,8 @@ from django.contrib.gis.gdal.prototypes import raster as capi
from django.contrib.gis.gdal.raster.band import BandList from django.contrib.gis.gdal.raster.band import BandList
from django.contrib.gis.gdal.raster.base import GDALRasterBase from django.contrib.gis.gdal.raster.base import GDALRasterBase
from django.contrib.gis.gdal.raster.const import ( from django.contrib.gis.gdal.raster.const import (
GDAL_RESAMPLE_ALGORITHMS, VSI_DELETE_BUFFER_ON_READ, GDAL_RESAMPLE_ALGORITHMS, VSI_DELETE_BUFFER_ON_READ, VSI_FILESYSTEM_PREFIX,
VSI_FILESYSTEM_BASE_PATH, VSI_TAKE_BUFFER_OWNERSHIP, VSI_MEM_FILESYSTEM_BASE_PATH, VSI_TAKE_BUFFER_OWNERSHIP,
) )
from django.contrib.gis.gdal.srs import SpatialReference, SRSException from django.contrib.gis.gdal.srs import SpatialReference, SRSException
from django.contrib.gis.geometry import json_regex from django.contrib.gis.geometry import json_regex
@ -74,7 +74,7 @@ class GDALRaster(GDALRasterBase):
# If input is a valid file path, try setting file as source. # If input is a valid file path, try setting file as source.
if isinstance(ds_input, str): if isinstance(ds_input, str):
if ( if (
not ds_input.startswith(VSI_FILESYSTEM_BASE_PATH) and not ds_input.startswith(VSI_FILESYSTEM_PREFIX) and
not os.path.exists(ds_input) not os.path.exists(ds_input)
): ):
raise GDALException( raise GDALException(
@ -95,7 +95,7 @@ class GDALRaster(GDALRasterBase):
# deleted. # deleted.
self._ds_input = c_buffer(ds_input) self._ds_input = c_buffer(ds_input)
# Create random name to reference in vsimem filesystem. # Create random name to reference in vsimem filesystem.
vsi_path = os.path.join(VSI_FILESYSTEM_BASE_PATH, str(uuid.uuid4())) vsi_path = os.path.join(VSI_MEM_FILESYSTEM_BASE_PATH, str(uuid.uuid4()))
# Create vsimem file from buffer. # Create vsimem file from buffer.
capi.create_vsi_file_from_mem_buffer( capi.create_vsi_file_from_mem_buffer(
force_bytes(vsi_path), force_bytes(vsi_path),
@ -217,7 +217,10 @@ class GDALRaster(GDALRasterBase):
@property @property
def vsi_buffer(self): def vsi_buffer(self):
if not self.is_vsi_based: if not (
self.is_vsi_based and
self.name.startswith(VSI_MEM_FILESYSTEM_BASE_PATH)
):
return None return None
# Prepare an integer that will contain the buffer length. # Prepare an integer that will contain the buffer length.
out_length = c_int() out_length = c_int()
@ -232,7 +235,7 @@ class GDALRaster(GDALRasterBase):
@cached_property @cached_property
def is_vsi_based(self): def is_vsi_based(self):
return self._ptr and self.name.startswith(VSI_FILESYSTEM_BASE_PATH) return self._ptr and self.name.startswith(VSI_FILESYSTEM_PREFIX)
@property @property
def name(self): def name(self):
@ -432,7 +435,7 @@ class GDALRaster(GDALRasterBase):
elif self.driver.name != 'MEM': elif self.driver.name != 'MEM':
clone_name = self.name + '_copy.' + self.driver.name clone_name = self.name + '_copy.' + self.driver.name
else: else:
clone_name = os.path.join(VSI_FILESYSTEM_BASE_PATH, str(uuid.uuid4())) clone_name = os.path.join(VSI_MEM_FILESYSTEM_BASE_PATH, str(uuid.uuid4()))
return GDALRaster( return GDALRaster(
capi.copy_ds( capi.copy_ds(
self.driver._ptr, self.driver._ptr,

View File

@ -1111,9 +1111,9 @@ blue.
raster should be opened in write mode. For newly-created rasters, the second raster should be opened in write mode. For newly-created rasters, the second
parameter is ignored and the new raster is always created in write mode. parameter is ignored and the new raster is always created in write mode.
The first parameter can take three forms: a string representing a file The first parameter can take three forms: a string representing a file path
path, a dictionary with values defining a new raster, or a bytes object (filesystem or GDAL virtual filesystem), a dictionary with values defining
representing a raster file. a new raster, or a bytes object representing a raster file.
If the input is a file path, the raster is opened from there. If the input If the input is a file path, the raster is opened from there. If the input
is raw data in a dictionary, the parameters ``width``, ``height``, and is raw data in a dictionary, the parameters ``width``, ``height``, and
@ -1164,6 +1164,10 @@ blue.
>>> rst.name # Stored in a random path in the vsimem filesystem. >>> rst.name # Stored in a random path in the vsimem filesystem.
'/vsimem/da300bdb-129d-49a8-b336-e410a9428dad' '/vsimem/da300bdb-129d-49a8-b336-e410a9428dad'
.. versionchanged:: 4.0
Creating rasters in any GDAL virtual filesystem was allowed.
.. attribute:: name .. attribute:: name
The name of the source which is equivalent to the input file path or the name The name of the source which is equivalent to the input file path or the name
@ -1772,6 +1776,13 @@ Key Default Usage
Using GDAL's Virtual Filesystem Using GDAL's Virtual Filesystem
------------------------------- -------------------------------
GDAL can access files stored in the filesystem, but also supports virtual
filesystems to abstract accessing other kind of files, such as compressed,
encrypted, or remote files.
Using memory-based Virtual Filesystem
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GDAL has an internal memory-based filesystem, which allows treating blocks of GDAL has an internal memory-based filesystem, which allows treating blocks of
memory as files. It can be used to read and write :class:`GDALRaster` objects memory as files. It can be used to read and write :class:`GDALRaster` objects
to and from binary file buffers. to and from binary file buffers.
@ -1817,6 +1828,53 @@ Here's how to create a raster and return it as a file in an
... }) ... })
>>> HttpResponse(rast.vsi_buffer, 'image/tiff') >>> HttpResponse(rast.vsi_buffer, 'image/tiff')
Using other Virtual Filesystems
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 4.0
Depending on the local build of GDAL other virtual filesystems may be
supported. You can use them by prepending the provided path with the
appropriate ``/vsi*/`` prefix. See the `GDAL Virtual Filesystems
documentation`_ for more details.
.. warning:
Rasters with names starting with `/vsi*/` will be treated as rasters from
the GDAL virtual filesystems. Django doesn't perform any extra validation.
Compressed rasters
^^^^^^^^^^^^^^^^^^
Instead decompressing the file and instantiating the resulting raster, GDAL can
directly access compressed files using the ``/vsizip/``, ``/vsigzip/``, or
``/vsitar/`` virtual filesystems::
>>> from django.contrib.gis.gdal import GDALRaster
>>> rst = GDALRaster('/vsizip/path/to/your/file.zip/path/to/raster.tif')
>>> rst = GDALRaster('/vsigzip/path/to/your/file.gz')
>>> rst = GDALRaster('/vsitar/path/to/your/file.tar/path/to/raster.tif')
Network rasters
^^^^^^^^^^^^^^^
GDAL can support online resources and storage providers transparently. As long
as it's built with such capabilities.
To access a public raster file with no authentication, you can use
``/vsicurl/``::
>>> from django.contrib.gis.gdal import GDALRaster
>>> rst = GDALRaster('/vsicurl/https://example.com/raster.tif')
>>> rst.name
'/vsicurl/https://example.com/raster.tif'
For commercial storage providers (e.g. ``/vsis3/``) the system should be
previously configured for authentication and possibly other settings (see the
`GDAL Virtual Filesystems documentation`_ for available options).
.. _`GDAL Virtual Filesystems documentation`: https://gdal.org/user/virtual_file_systems.html
Settings Settings
======== ========

View File

@ -102,6 +102,9 @@ Minor features
* Added support for SpatiaLite 5. * Added support for SpatiaLite 5.
* :class:`~django.contrib.gis.gdal.GDALRaster` now allows creating rasters in
any GDAL virtual filesystem.
:mod:`django.contrib.messages` :mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -217,6 +217,7 @@ fieldsets
filename filename
filenames filenames
filesystem filesystem
filesystems
fk fk
flatpage flatpage
flatpages flatpages

View File

@ -2,6 +2,7 @@ import os
import shutil import shutil
import struct import struct
import tempfile import tempfile
import zipfile
from unittest import mock from unittest import mock
from django.contrib.gis.gdal import GDALRaster, SpatialReference from django.contrib.gis.gdal import GDALRaster, SpatialReference
@ -229,6 +230,17 @@ class GDALRasterTests(SimpleTestCase):
# The vsi buffer is None for rasters that are not vsi based. # The vsi buffer is None for rasters that are not vsi based.
self.assertIsNone(self.rs.vsi_buffer) self.assertIsNone(self.rs.vsi_buffer)
def test_vsi_vsizip_filesystem(self):
rst_zipfile = tempfile.NamedTemporaryFile(suffix='.zip')
with zipfile.ZipFile(rst_zipfile, mode='w') as zf:
zf.write(self.rs_path, 'raster.tif')
rst_path = '/vsizip/' + os.path.join(rst_zipfile.name, 'raster.tif')
rst = GDALRaster(rst_path)
self.assertEqual(rst.driver.name, self.rs.driver.name)
self.assertEqual(rst.name, rst_path)
self.assertIs(rst.is_vsi_based, True)
self.assertIsNone(rst.vsi_buffer)
def test_offset_size_and_shape_on_raster_creation(self): def test_offset_size_and_shape_on_raster_creation(self):
rast = GDALRaster({ rast = GDALRaster({
'datatype': 1, 'datatype': 1,