Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
### Changes
- [#1168](https://github.com/LayerManager/layman/issues/1168) Extend [PATCH Workspace Layer](doc/rest.md#patch-workspace-layer) with ability of appending data to existing time-series layer.
- When publishing a layer or map to Micka via CSW, Layman sends the creating user (Layman username) in the SOAP request header (`CreateUser`), so the metadata record in Micka is associated with the user who created the publication.
- [#1185](https://github.com/LayerManager/layman/issues/1185) POST Workspace [Layers](doc/rest.md#post-workspace-layers) supports import of raster layers from an existing server-side directory via the file_path parameter, including ImageMosaic timeseries layers.
- [#1185](https://github.com/LayerManager/layman/issues/1185) POST Workspace [Layers](doc/rest.md#post-workspace-layers) supports import of raster layers from existing server-side data via `file_path` (directory for ImageMosaic timeseries layers, or direct GeoTIFF file path).
- [#1185](https://github.com/LayerManager/layman/issues/1185)[GET Workspace Layer](doc/rest.md#get-workspace-layer) returns `file_path` key for raster layers published using this parameter.

## v2.3.0
Expand Down
9 changes: 6 additions & 3 deletions doc/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,16 @@ Body parameters:
- exactly one of `file`, `file_path`, or `external_table_uri` must be set
- relative path to a directory that already exists on the server
- the path must be relative to the root of the GeoServer data directory
- the referenced directory must be physically located inside the GeoServer data directory
- the directory must contain at least one GeoTIFF file (with extension `.tif` or `.tiff`)
- the referenced path must be physically located inside the GeoServer data directory
- if directory is used, it must contain at least one GeoTIFF file (with extension `.tif` or `.tiff`)
- if directory contains more than one raster file, `time_regex` parameter is required
- if file is used, it must be a GeoTIFF file (with extension `.tif` or `.tiff`)

- for raster layers:
- supported only for GeoTIFF files (`.tif` or `.tiff` extension)
- raster files are not normalized when using `file_path` parameter
- this may result in different styling behavior compared to layers published via `file` parameter
- may point directly to a single raster file (published as a single coverage)
- may point to a directory containing a single raster file (published as a single coverage)
- may point to a directory containing multiple raster files:
- if `time_regex` parameter is provided, files are treated as a time series and published as an ImageMosaic
Expand Down Expand Up @@ -346,7 +349,7 @@ JSON object with following structure:
- *status*: Status information about publishing style. See [GET Workspace Layer](#get-workspace-layer) **wms** property for meaning.
- *error*: If status is FAILURE, this may contain error object.
- **original_data_source**: String. Either `file` if layer was published from file, or `database_table` if layer was published from external database table
- **file_path**: String. Available only for raster layers published using `file_path` parameter. Relative path to the directory containing raster files, relative to the root of the GeoServer data directory.
- **file_path**: String. Available only for raster layers published using `file_path` parameter. Relative path (to directory or direct GeoTIFF file) inside the root of the GeoServer data directory.
- *metadata*
- *identifier*: String. Identifier of metadata record in CSW instance.
- *record_url*: String. URL of metadata record accessible by web browser, probably with some editing capabilities.
Expand Down
10 changes: 1 addition & 9 deletions src/layman/layer/filesystem/input_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,7 @@ def get_file_path_info(publ_uuid):

abs_path = os.path.join(settings.GEOSERVER_DATADIR, file_path_relative)

if not os.path.isdir(abs_path):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Path is not a directory',
'expected': 'Relative path to directory containing raster files',
'found': file_path_relative,
})

tifs = layer_util.get_geotiff_files(abs_path)
tifs = layer_util.get_file_path_geotiff_files(abs_path)
if not tifs:
return None
return [{'absolute': tif, 'gdal': tif, 'file_path': file_path_relative} for tif in tifs]
Expand Down
10 changes: 7 additions & 3 deletions src/layman/layer/rest_workspace_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,15 @@ def post(workspace):
time_regex = request.form.get('time_regex') or None
time_regex_format = request.form.get('time_regex_format') or None

file_path_relative, file_path_absolute, _, _ = util.validate_and_process_file_path(file_path, check_crs=check_crs)
file_path_relative, file_path_absolute, file_path_type, tif_files = util.validate_and_process_file_path(file_path, check_crs=check_crs)
if file_path_relative:
file_path = file_path_absolute
util.validate_file_path_requires_time_regex(file_path_absolute, time_regex)
util.validate_file_path_requires_time_regex(
file_path_absolute,
time_regex,
file_path_type=file_path_type,
tif_files=tif_files,
)

util.validate_time_regex(time_regex, time_regex_format)
slugified_time_regex = input_file.slugify_timeseries_filename_pattern(time_regex) if time_regex else None
Expand Down Expand Up @@ -147,7 +152,6 @@ def post(workspace):
if file_path:
geodata_type = settings.GEODATA_TYPE_RASTER
if not crs_id:
tif_files = util.get_geotiff_files(file_path)
crs_id = input_file.get_raster_crs_id(tif_files[0]) if tif_files else None
if not crs_id:
raise LaymanError(4, {'found': None, 'supported_values': settings.INPUT_SRS_LIST})
Expand Down
138 changes: 96 additions & 42 deletions src/layman/layer/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from functools import wraps, partial
from urllib import parse
import os
import glob
import re
import logging
import shutil
Expand All @@ -28,6 +27,9 @@

EXTERNAL_TABLE_URI_PATTERN = 'postgresql://<username>:<password>@<host>:<port>/<dbname>?schema=<schema_name>&table=<table_name>&geo_column=<geo_column_name>'
logger = logging.getLogger(__name__)
FILE_PATH_TYPE_DIRECTORY = 'directory'
FILE_PATH_TYPE_FILE = 'file'
FILE_PATH_MAIN_EXTENSIONS = {ext.lower() for ext in settings.FILE_PATH_MAIN_FILE_EXTENSIONS}


def to_safe_layer_name(value):
Expand Down Expand Up @@ -254,11 +256,78 @@ def layer_info_to_metadata_properties(info):


def get_geotiff_files(directory):
files = []
for ext in settings.FILE_PATH_MAIN_FILE_EXTENSIONS:
files.extend(glob.glob(os.path.join(directory, f'*{ext}')))
files.extend(glob.glob(os.path.join(directory, f'*{ext.upper()}')))
return files
return [
os.path.join(directory, f)
for f in os.listdir(directory)
if os.path.splitext(f)[1].lower() in FILE_PATH_MAIN_EXTENSIONS
]


def get_file_path_type(file_path_absolute):
if os.path.isdir(file_path_absolute):
return FILE_PATH_TYPE_DIRECTORY
if os.path.isfile(file_path_absolute):
return FILE_PATH_TYPE_FILE
return None


def get_file_path_geotiff_files(file_path_absolute, *, file_path_type=None):
file_path_type = file_path_type or get_file_path_type(file_path_absolute)
if file_path_type == FILE_PATH_TYPE_DIRECTORY:
return get_geotiff_files(file_path_absolute)
if file_path_type == FILE_PATH_TYPE_FILE:
ext = os.path.splitext(file_path_absolute)[1].lower()
if ext in FILE_PATH_MAIN_EXTENSIONS:
return [file_path_absolute]
return []


def validate_file_path_source(file_path, file_path_absolute):
file_path_type = get_file_path_type(file_path_absolute)
if file_path_type == FILE_PATH_TYPE_DIRECTORY:
if not (
os.access(file_path_absolute, os.R_OK)
and os.access(file_path_absolute, os.X_OK)
):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Directory is not readable',
'expected': 'Readable directory containing raster files',
'found': file_path,
})
elif file_path_type == FILE_PATH_TYPE_FILE:
ext = os.path.splitext(file_path_absolute)[1].lower()
if ext not in FILE_PATH_MAIN_EXTENSIONS:
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Path is not a supported raster file',
'expected': f'Relative path to GeoTIFF file with extension in {settings.FILE_PATH_MAIN_FILE_EXTENSIONS}',
'found': file_path,
})
if not os.access(file_path_absolute, os.R_OK):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Raster file is not readable',
'expected': 'Readable GeoTIFF file',
'found': file_path,
})
else:
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Path is not a directory or file',
'expected': 'Relative path to existing directory or GeoTIFF file',
'found': file_path,
})

tif_files = get_file_path_geotiff_files(file_path_absolute, file_path_type=file_path_type)
if not tif_files:
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Directory does not contain any raster files',
'expected': 'Directory containing at least one GeoTIFF file (.tif or .tiff) or a GeoTIFF file path',
'found': file_path,
})
return file_path_type, tif_files


def validate_and_process_file_path(file_path, *, check_crs=True):
Expand All @@ -271,7 +340,7 @@ def validate_and_process_file_path(file_path, *, check_crs=True):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Absolute path is not allowed',
'expected': 'Relative path to directory (relative to GEOSERVER_DATADIR)',
'expected': 'Relative path to directory or GeoTIFF file (relative to GEOSERVER_DATADIR)',
'found': file_path,
})

Expand All @@ -282,61 +351,46 @@ def validate_and_process_file_path(file_path, *, check_crs=True):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Path is outside GeoServer data directory',
'expected': 'Relative path to directory inside GeoServer data directory',
'expected': 'Relative path to directory or GeoTIFF file inside GeoServer data directory',
'found': file_path,
})

if not os.path.exists(file_path_absolute):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Directory does not exist',
'expected': 'Relative path to existing directory on server',
'message': 'Path does not exist',
'expected': 'Relative path to existing directory or GeoTIFF file on server',
'found': file_path,
})

if not os.path.isdir(file_path_absolute):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Path is not a directory',
'expected': 'Relative path to existing directory containing raster files',
'found': file_path,
})

if not (
os.access(file_path_absolute, os.R_OK)
and os.access(file_path_absolute, os.X_OK)
):
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Directory is not readable',
'expected': 'Readable directory containing raster files',
'found': file_path,
})

tif_files = get_geotiff_files(file_path_absolute)
if not tif_files:
raise LaymanError(2, {
'parameter': 'file_path',
'message': 'Directory does not contain any raster files',
'expected': 'Directory containing at least one GeoTIFF file (.tif or .tiff)',
'found': file_path,
})
file_path_type, tif_files = validate_file_path_source(file_path, file_path_absolute)

if check_crs:
for tif_file in tif_files:
input_file.check_raster_layer_crs(tif_file)

return file_path, file_path_absolute, None, None
return file_path, file_path_absolute, file_path_type, tif_files


def validate_file_path_requires_time_regex(file_path_absolute, time_regex, *, file_path_type, tif_files):
if file_path_type == FILE_PATH_TYPE_FILE and time_regex:
file_path_relative = os.path.relpath(file_path_absolute, settings.GEOSERVER_DATADIR)
raise LaymanError(48, {
'parameters': ['file_path', 'time_regex'],
'message': 'time_regex is not allowed for file_path pointing to a file',
'expected': 'Provide file_path to a directory when using time_regex',
'found': {
'file_path': file_path_relative,
'time_regex': time_regex,
},
})

def validate_file_path_requires_time_regex(file_path_absolute, time_regex):
tif_files = get_geotiff_files(file_path_absolute)
if len(tif_files) > 1 and not time_regex:
if file_path_type == FILE_PATH_TYPE_DIRECTORY and len(tif_files) > 1 and not time_regex:
file_path_relative = os.path.relpath(file_path_absolute, settings.GEOSERVER_DATADIR)
raise LaymanError(48, {
'parameters': ['file_path', 'time_regex'],
'message': 'Directory contains multiple raster files, but time_regex is not provided',
'expected': 'Provide time_regex parameter for image mosaic when directory contains multiple raster files',
'expected': 'Provide time_regex for image mosaic or specify file_path to a single raster file',
'found': {
'file_path': file_path_relative,
'raster_files_count': len(tif_files),
Expand Down
Loading
Loading