Skip to content
Open
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
53 changes: 27 additions & 26 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/async-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Asynchronous tasks are started by following requests:
- [PATCH Layer](rest.md#patch-layer)
- tasks related to patched layer
- tasks related to each map that points to patched layer
- [POST Workspace Maps](rest.md#post-workspace-maps)
- [POST Maps](rest.md#post-maps)
- tasks related to newly published map
- [PATCH Map](rest.md#patch-map)
- tasks related to patched map
Expand Down
6 changes: 3 additions & 3 deletions doc/client-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ then response will change to
```

Currently, value of X-Forwarded headers affects following URLs:
* [GET Publications](rest.md#get-publications), [GET Layers](rest.md#get-layers), [GET Maps](rest.md#get-maps), and [GET Workspace Maps](rest.md#get-workspace-maps)
* [GET Publications](rest.md#get-publications), [GET Layers](rest.md#get-layers), and [GET Maps](rest.md#get-maps)
* `url` key
* [POST Layers](rest.md#post-layers) and [POST Workspace Maps](rest.md#post-workspace-maps)
* [POST Layers](rest.md#post-layers) and [POST Maps](rest.md#post-maps)
* `url` key
* [GET Layer](rest.md#get-layer) and [PATCH Layer](rest.md#patch-layer)
* `url` key
Expand All @@ -72,7 +72,7 @@ Currently, value of X-Forwarded headers affects following URLs:
* each `legends` key if its HTTP protocol and netloc corresponds with `url` or `protocol`.`url`
* `style` key if its HTTP protocol and netloc corresponds with `url` or `protocol`.`url`
* NOTE: If client proxy protocol, host, or URL path prefix was used in URLs in uploaded file, then such values are also replaced with values according to X-Forwarded header values. Default values are used for requests without X-Forwarded headers (protocol is the one from [LAYMAN_CLIENT_PUBLIC_URL](env-settings.md#layman_client_public_url), host is [LAYMAN_PROXY_SERVER_NAME](env-settings.md#layman_proxy_server_name), and path prefix is empty string).
* [DELETE Layers](rest.md#delete-layers), [DELETE Workspace Maps](rest.md#delete-workspace-maps), [DELETE Layer](rest.md#delete-layer) and [DELETE Map](rest.md#delete-map)
* [DELETE Layers](rest.md#delete-layers), [DELETE Maps](rest.md#delete-maps), [DELETE Layer](rest.md#delete-layer) and [DELETE Map](rest.md#delete-map)
* `url` key
* [OGC endpoints](endpoints.md)
* Headers `X-Forwarded-For`, `X-Forwarded-Path`, `Forwarded` and `Host` are ignored
Expand Down
2 changes: 1 addition & 1 deletion doc/data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ When user [patches existing layer](rest.md#patch-layer), data is saved in the sa
### Maps
Information about [maps](models.md#map) includes JSON definition.

When user [publishes new map](rest.md#post-workspace-maps)
When user [publishes new map](rest.md#post-maps)
- UUID and name is saved to [Redis](#redis),
- UUID, name, title, description and access rights are saved to [PostgreSQL](#postgresql),
- JSON file is saved to [filesystem](#filesystem),
Expand Down
6 changes: 3 additions & 3 deletions doc/publish-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ In QGIS, you need to implement following steps.

First, compose JSON valid against [map-composition schema](https://github.com/hslayers/map-compositions). For Layman, especially `describedBy`, `name`, `title`, `abstract`, `layers`, `projection`, and `extent attributes are important. Each layer must have `className` attribute equal to `HSLayers.Layer.WMS` or `WMS`.

Then save the file to Layman using [POST Workspace Maps](rest.md#post-workspace-maps) endpoint. Well-known [requests](https://requests.readthedocs.io/en/latest/) module can be used for sending HTTP requests. See especially
Then save the file to Layman using [POST Maps](rest.md#post-maps) endpoint. Well-known [requests](https://requests.readthedocs.io/en/latest/) module can be used for sending HTTP requests. See especially
- [More complicated POST requests](https://requests.readthedocs.io/en/latest/user/quickstart/#more-complicated-post-requests)
- [POST a Multipart-Encoded File](https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file)
- [POST Multiple Multipart-Encoded Files](https://requests.readthedocs.io/en/latest/user/advanced/#post-multiple-multipart-encoded-files)

In response of [POST Workspace Maps](rest.md#post-workspace-maps) you will obtain
In response of [POST Maps](rest.md#post-maps) you will obtain
- `name` of the map unique within all maps in used [workspace](models.md#workspace)
- `url` of the map pointing to [GET Map](rest.md#get-map)

Expand All @@ -27,7 +27,7 @@ In response of [POST Workspace Maps](rest.md#post-workspace-maps) you will obtai
- update the map using [PATCH Map](rest.md#patch-map)
- delete the map using [DELETE Map](rest.md#delete-map)

Also, you can obtain list of all maps using [GET Workspace Maps](rest.md#get-workspace-maps).
Also, you can obtain list of all maps using [GET Maps](rest.md#get-maps).


## Maps composed from vector files
Expand Down
37 changes: 18 additions & 19 deletions doc/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
|Layer Style|`/rest/layers/<uuid>/style`|[GET](#get-layer-style)| x | x | x |
|Layer Chunk|`/rest/layers/<uuid>/chunk`|[GET](#get-layer-chunk)| [POST](#post-layer-chunk) | x | x |
|Workspace Layer Metadata Comparison|`/rest/workspaces/<workspace_name>/layers/<layername>/metadata-comparison`|[GET](#get-workspace-layer-metadata-comparison) | x | x | x |
|Maps|`/rest/maps`|[GET](#get-maps)| x | x | x |
|Maps|`/rest/maps`|[GET](#get-maps)| [POST](#post-maps) | x | [DELETE](#delete-maps) |
|[Map](models.md#map)|`/rest/maps/<uuid>`|[GET](#get-map)| x | [PATCH](#patch-map) | [DELETE](#delete-map) |
|Map Thumbnail|`/rest/maps/<uuid>/thumbnail`|[GET](#get-map-thumbnail)| x | x | x |
|Map File|`/rest/maps/<uuid>/file`|[GET](#get-map-file)| x | x | x |
|Workspace Maps|`/rest/workspaces/<workspace_name>/maps`|[GET](#get-workspace-maps)| [POST](#post-workspace-maps) | x | [DELETE](#delete-workspace-maps) |
|Workspace Map Metadata Comparison|`/rest/workspaces/<workspace_name>/layers/<layername>/metadata-comparison`|[GET](#get-workspace-map-metadata-comparison) | x | x | x |
|Workspace Map Metadata Comparison|`/rest/workspaces/<workspace_name>/maps/<mapname>/metadata-comparison`|[GET](#get-workspace-map-metadata-comparison) | x | x | x |
|Users|`/rest/users`|[GET](#get-users)| x | x | x |
|User|`/rest/users/<username>`| x | x | x | [DELETE](#delete-user) |
|Current [User](models.md#user)|`/rest/current-user`|[GET](#get-current-user)| x | [PATCH](#patch-current-user) | [DELETE](#delete-current-user) |
Expand Down Expand Up @@ -533,16 +532,12 @@ Get list of published maps (map compositions).

Have the same request parameters and response structure and headers as [GET Publications](#get-publications), except only maps are returned.

## Workspace Maps
### URL
`/rest/workspaces/<workspace_name>/maps`

### GET Workspace Maps
Get list of published maps (map compositions).

Have the same request parameters and response structure and headers as [GET Maps](#get-maps).
Query parameters:
- *workspace*: String, optional
- workspace identifier
- if present, only maps from this workspace are returned

### POST Workspace Maps
### POST Maps
Publish new map composition. Accepts JSON valid against [map-composition schema](https://github.com/hslayers/map-compositions) version 2 or 3 used by [Hslayers-ng](https://github.com/hslayers/hslayers-ng). Exact version of schema is defined by `describedBy` key of JSON data file.

Processing chain consists of few steps:
Expand All @@ -563,6 +558,8 @@ Response to this request may be returned sooner than the processing chain is fin
Content-Type: `multipart/form-data`

Body parameters:
- **workspace**, string `^[a-z][a-z0-9]*(_[a-z0-9]+)*$`
- workspace where the map will be published
- *uuid*, string, e.g. `959c95fb-ab54-47a6-9694-402926b8fd29`
- map primary key
- used if specified, otherwise generated
Expand Down Expand Up @@ -603,11 +600,13 @@ JSON array of objects representing posted maps with following structure:
- **uuid**: String. UUID of the map.
- **url**: String. URL of the map. It points to [GET Map](#get-map).

### DELETE Workspace Maps
### DELETE Maps
Delete existing maps and all associated sources, including map-composition JSON file and map thumbnail for all maps in the workspace. The currently running [asynchronous tasks](async-tasks.md) of affected maps are aborted. Only maps on which user has [write access right](./security.md#access-to-multi-publication-endpoints) are deleted.

#### Request
No action parameters.
Query parameters:
- **workspace**, string `^[a-z][a-z0-9]*(_[a-z0-9]+)*$`
- workspace whose maps will be deleted

#### Response
Content-Type: `application/json`
Expand Down Expand Up @@ -674,7 +673,7 @@ JSON object with following structure:
- **native_bounding_box**: List of 4 floats. Bounding box coordinates [minx, miny, maxx, maxy] in native CRS.

### PATCH Map
Update information about existing map. First, it deletes sources of the map, and then it publishes them again with new parameters. The processing chain is similar to [POST Workspace Maps](#post-workspace-maps), including [asynchronous tasks](async-tasks.md),
Update information about existing map. First, it deletes sources of the map, and then it publishes them again with new parameters. The processing chain is similar to [POST Maps](#post-maps), including [asynchronous tasks](async-tasks.md),

Calling concurrent PATCH requests is not supported, as well as calling PATCH when [POST/PATCH async chain](async-tasks.md) is still running, is not allowed. In such cases, error is returned.

Expand All @@ -683,7 +682,7 @@ Calling PATCH request when [WFS-T async chain](async-tasks.md) is still running
#### Request
Content-Type: `multipart/form-data`, `application/x-www-form-urlencoded`

Parameters have same meaning as in case of [POST Workspace Maps](#post-workspace-maps).
Parameters have same meaning as in case of [POST Maps](#post-maps).

Body parameters:
- *file*, JSON file
Expand All @@ -702,7 +701,7 @@ Body parameters:
#### Response
Content-Type: `application/json`

JSON object, same as in case of [POST Workspace Maps](#post-workspace-maps).
JSON object, same as in case of [POST Maps](#post-maps).

### DELETE Map
Delete existing map and all associated sources, including map-composition JSON file and map thumbnail. The currently running [asynchronous tasks](async-tasks.md) of affected map are aborted.
Expand All @@ -727,8 +726,8 @@ Get JSON file describing the map valid against [map-composition schema](https://

Notice that some JSON properties are automatically updated by layman, so file obtained by this endpoint may be slightly different from file that was uploaded. Expected changes:
- **name** set to the map's name
- **title** obtained from [POST Workspace Maps](#post-workspace-maps) or [PATCH Map](#patch-map) as `title`
- **abstract** obtained from [POST Workspace Maps](#post-workspace-maps) or [PATCH Map](#patch-map) as `description`
- **title** obtained from [POST Maps](#post-maps) or [PATCH Map](#patch-map) as `title`
- **abstract** obtained from [POST Maps](#post-maps) or [PATCH Map](#patch-map) as `description`
- **user** updated on the fly during this request:
- **name** set to `<workspace_name>` in URL of this endpoint
- **email** set to email of the owner, or empty string if not known
Expand Down
2 changes: 0 additions & 2 deletions src/layman/map/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def get_map_patch_keys():
MAP_REST_PATH_NAME = f"{PUBLICATION_TYPE_NAME}s"


from .rest_workspace_maps import bp as workspace_maps_bp
from .rest_map_thumbnail import bp as map_thumbnail_bp
from .rest_map_file import bp as map_file_bp
from .rest_workspace_map_metadata_comparison import bp as workspace_map_metadata_comparison_bp
Expand All @@ -41,7 +40,6 @@ def get_map_patch_keys():
'name': PUBLICATION_TYPE_NAME,
'rest_path_name': MAP_REST_PATH_NAME,
'workspace_blueprints': [
workspace_maps_bp,
workspace_map_metadata_comparison_bp,
],
'blueprints': [
Expand Down
3 changes: 2 additions & 1 deletion src/layman/map/micka/csw_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def provide_map(client):
with app.app_context():
workspace = TEST_WORKSPACE
mapname = TEST_MAP
rest_path = url_for('rest_workspace_maps.post', workspace=workspace)
rest_path = url_for('rest_maps.post')
file_paths = [
'sample/layman.map/full.json',
]
Expand All @@ -66,6 +66,7 @@ def provide_map(client):
with ExitStack() as stack:
files = [(stack.enter_context(open(fp, 'rb')), os.path.basename(fp)) for fp in file_paths]
response = client.post(rest_path, data={
'workspace': workspace,
'file': files,
'name': mapname,
})
Expand Down
146 changes: 140 additions & 6 deletions src/layman/map/rest_maps.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from flask import Blueprint, g, request, current_app as app
import json
import io

from layman import util as layman_util
from flask import Blueprint, jsonify, request, g
from flask import current_app as app
from werkzeug.datastructures import FileStorage

from layman.http import LaymanError
from layman import authn, util as layman_util, uuid
from layman.authn import authenticate, get_authn_username
from layman.authz import authorize_publications_decorator
from layman.common import rest as rest_common
from . import MAP_TYPE, MAP_REST_PATH_NAME
from layman.authz import authorize_publications_decorator, authorize
from layman.common import redis as redis_util, rest as rest_common
from layman.util import url_for
from layman.uuid import register_publication_uuid_to_redis
from . import util, MAP_TYPE, MAP_REST_PATH_NAME
from .filesystem import input_file

bp = Blueprint('rest_maps', __name__)

Expand All @@ -22,4 +31,129 @@ def get():

actor = get_authn_username()
x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)
return rest_common.get_publications(MAP_TYPE, actor, request_args=request.args, x_forwarded_items=x_forwarded_items)
workspace = layman_util.get_workspace_from_request(request.args, required=False)
if workspace:
authorize(workspace, MAP_TYPE, None, request.method, actor)
return rest_common.get_publications(
MAP_TYPE,
actor,
request_args=request.args,
workspace=workspace,
x_forwarded_items=x_forwarded_items,
)


@bp.route(f"/{MAP_REST_PATH_NAME}", methods=['POST'])
def post():
app.logger.info(f"POST Maps, actor={g.user}")
x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)

actor_name = authn.get_authn_username()
workspace = layman_util.get_workspace_from_request(request.form, required=True)
authorize(workspace, MAP_TYPE, None, request.method, actor_name)

# UUID
input_uuid = request.form.get('uuid')
input_uuid = input_uuid if input_uuid else None
uuid.check_input_uuid(input_uuid)

# FILE
if 'file' in request.files and not request.files['file'].filename == '':
file = request.files["file"]
else:
raise LaymanError(1, {'parameter': 'file'})
file_json = util.check_file(file, x_forwarded_items=x_forwarded_items)

# NAME
unsafe_mapname = request.form.get('name', '')
if len(unsafe_mapname) == 0:
unsafe_mapname = input_file.get_unsafe_mapname(file_json)
mapname = util.to_safe_map_name(unsafe_mapname)
util.check_mapname(mapname)
info = layman_util.get_publication_info(workspace, MAP_TYPE, mapname)
if info:
raise LaymanError(24, {'mapname': mapname})

# TITLE
if len(request.form.get('title', '')) > 0:
title = request.form['title']
elif len(file_json.get('title', '')) > 0:
title = file_json['title']
else:
title = mapname

# DESCRIPTION
if len(request.form.get('description', '')) > 0:
description = request.form['description']
else:
description = file_json.get('abstract', '')

redis_util.create_lock(workspace, MAP_TYPE, mapname, request.method)

try:
map_result = {
'name': mapname,
}

kwargs = {
'title': title,
'description': description,
'actor_name': actor_name,
'x_forwarded_headers': x_forwarded_items.headers,
}

rest_common.setup_post_access_rights(request.form, kwargs, actor_name)
util.pre_publication_action_check(workspace,
mapname,
kwargs,
)
# register map uuid
uuid_str = register_publication_uuid_to_redis(workspace, MAP_TYPE, mapname, input_uuid)
kwargs['uuid'] = uuid_str

map_result.update({
'uuid': uuid_str,
'url': url_for('rest_map.get', uuid=uuid_str, x_forwarded_items=x_forwarded_items),
})

file = FileStorage(
io.BytesIO(json.dumps(file_json).encode()),
file.filename
)
input_file.save_map_files(uuid_str, [file])

util.post_map(
workspace,
mapname,
kwargs,
'layman.map.filesystem.input_file'
)
except Exception as exception:
try:
if util.is_map_chain_ready(workspace, mapname):
redis_util.unlock_publication(workspace, MAP_TYPE, mapname)
finally:
redis_util.unlock_publication(workspace, MAP_TYPE, mapname)
raise exception

# app.logger.info('uploaded map '+mapname)
return jsonify([map_result]), 200


@bp.route(f"/{MAP_REST_PATH_NAME}", methods=['DELETE'])
def delete():
app.logger.info(f"DELETE Maps, actor={g.user}")

actor_name = authn.get_authn_username()
workspace = layman_util.get_workspace_from_request(request.args, required=True)
authorize(workspace, MAP_TYPE, None, request.method, actor_name)

x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)
infos = layman_util.delete_publications(
workspace,
MAP_TYPE,
request.method,
x_forwarded_items=x_forwarded_items,
)

return infos, 200
Loading
Loading