diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..e6e65e21 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,14 @@ +FROM tiangolo/uwsgi-nginx:python3.11 + +# Should match client_body_buffer_size defined in nginx/body-buffer-size.conf +ENV NGINX_MAX_UPLOAD 1M + +# object stores aren't thread-safe yet +# https://github.com/eclipse-basyx/basyx-python-sdk/issues/205 +ENV UWSGI_CHEAPER 0 +ENV UWSGI_PROCESSES 1 + +RUN pip install --no-cache-dir git+https://github.com/rwth-iat/basyx-python-sdk@feature/http_api + +COPY ./app /app +COPY ./nginx /etc/nginx/conf.d diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..853f45ef --- /dev/null +++ b/server/README.md @@ -0,0 +1,71 @@ +# Eclipse BaSyx Python SDK - HTTP Server + +This repository contains a Dockerfile to spin up an exemplary HTTP/REST server following the [Specification of the AAS Part 2 API][6] with ease. +The server currently implements the following interfaces: + +- [Asset Administration Shell Repository Service][4] +- [Submodel Repository Service][5] + +It uses the [HTTP API][1] and the [AASX][7], [JSON][8], and [XML][9] Adapters of the [BaSyx Python SDK][3], to serve regarding files from a given directory. +The files are only read, chages won't persist. + +Alternatively, the container can also be told to use the [Local-File Backend][2] instead, which stores AAS and Submodels as individual JSON files and allows for persistent changes (except supplementary files, i.e. files referenced by `File` submodel elements). +See [below](#options) on how to configure this. + +## Building +The container image can be built via: +``` +$ docker buildx build -t basyx-python-sdk-http-server . +``` + +## Running + +### Storage +The container needs to be provided with the directory `/storage` to store AAS and Submodel files: AASX, JSON, XML or JSON files of Local-File Backend. + +This directory can be mapped via the `-v` option from another image or a local directory. +To map the directory `storage` inside the container, `-v ./storage:/storage` can be used. +The directory `storage` will be created in the current working directory, if it doesn't already exist. + +### Port +The HTTP server inside the container listens on port 80 by default. +To expose it on the host on port 8080, use the option `-p 8080:80` when running it. + +### Options +The container can be configured via environment variables: +- `API_BASE_PATH` determines the base path under which all other API paths are made available. + Default: `/api/v3.0` +- `STORAGE_TYPE` can be one of `LOCAL_FILE_READ_ONLY` or `LOCAL_FILE_BACKEND`: + - When set to `LOCAL_FILE_READ_ONLY` (the default), the server will read and serve AASX, JSON, XML files from the storage directory. + The files are not modified, all changes done via the API are only stored in memory. + - When instead set to `LOCAL_FILE`, the server makes use of the [LocalFileBackend][2], where AAS and Submodels are persistently stored as JSON files. + Supplementary files, i.e. files referenced by `File` submodel elements, are not stored in this case. +- `STORAGE_PATH` sets the directory to read the files from *within the container*. If you bind your files to a directory different from the default `/storage`, you can use this variable to adjust the server accordingly. + +### Running Examples + +Putting it all together, the container can be started via the following command: +``` +$ docker run -p 8080:80 -v ./storage:/storage basyx-python-sdk-http-server +``` + +Since Windows uses backslashes instead of forward slashes in paths, you'll have to adjust the path to the storage directory there: +``` +> docker run -p 8080:80 -v .\storage:/storage basyx-python-sdk-http-server +``` + +Per default, the server will use the `LOCAL_FILE_READ_ONLY` storage type and serve the API under `/api/v3.0` and read files from `/storage`. If you want to change this, you can do so like this: +``` +$ docker run -p 8080:80 -v ./storage2:/storage2 -e API_BASE_PATH=/api/v3.1 -e STORAGE_TYPE=LOCAL_FILE_BACKEND -e STORAGE_PATH=/storage2 basyx-python-sdk-http-server +``` + + +[1]: https://github.com/eclipse-basyx/basyx-python-sdk/pull/238 +[2]: https://basyx-python-sdk.readthedocs.io/en/latest/backend/local_file.html +[3]: https://github.com/eclipse-basyx/basyx-python-sdk +[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.0.1_SSP-001 +[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.0.1_SSP-001 +[6]: https://industrialdigitaltwin.org/content-hub/aasspecifications/idta_01002-3-0_application_programming_interfaces +[7]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/aasx.html#adapter-aasx +[8]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/json.html +[9]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/xml.html diff --git a/server/app/main.py b/server/app/main.py new file mode 100644 index 00000000..6662c868 --- /dev/null +++ b/server/app/main.py @@ -0,0 +1,46 @@ +import os +import pathlib +import sys + +from basyx.aas import model, adapter +from basyx.aas.adapter import aasx + +from basyx.aas.backend.local_file import LocalFileObjectStore +from basyx.aas.adapter.http import WSGIApp + +storage_path = os.getenv("STORAGE_PATH", "storage") +storage_type = os.getenv("STORAGE_TYPE", "LOCAL_FILE_READ_ONLY") +base_path = os.getenv("API_BASE_PATH") + +wsgi_optparams = {} + +if base_path is not None: + wsgi_optparams["base_path"] = base_path + +if storage_type == "LOCAL_FILE_BACKEND": + application = WSGIApp(LocalFileObjectStore(storage_path), aasx.DictSupplementaryFileContainer(), **wsgi_optparams) + +elif storage_type in "LOCAL_FILE_READ_ONLY": + object_store: model.DictObjectStore = model.DictObjectStore() + file_store: aasx.DictSupplementaryFileContainer = aasx.DictSupplementaryFileContainer() + + for file in pathlib.Path(storage_path).iterdir(): + if not file.is_file(): + continue + print(f"Loading {file}") + + if file.suffix.lower() == ".json": + with open(file) as f: + adapter.json.read_aas_json_file_into(object_store, f) + elif file.suffix.lower() == ".xml": + with open(file) as f: + adapter.xml.read_aas_xml_file_into(object_store, file) + elif file.suffix.lower() == ".aasx": + with aasx.AASXReader(file) as reader: + reader.read_into(object_store=object_store, file_store=file_store) + + application = WSGIApp(object_store, file_store, **wsgi_optparams) + +else: + print(f"STORAGE_TYPE must be either LOCAL_FILE or LOCAL_FILE_READ_ONLY! Current value: {storage_type}", + file=sys.stderr) diff --git a/server/nginx/body-buffer-size.conf b/server/nginx/body-buffer-size.conf new file mode 100644 index 00000000..423e8cd6 --- /dev/null +++ b/server/nginx/body-buffer-size.conf @@ -0,0 +1,6 @@ +# While the Dockerfile sets client_max_body_size, it doesn't +# allow us to define client_body_buffer_size, which is 16k +# by default. This results in temporary buffer files being +# created, in case the request body exceeds this limit. Thus, +# we override it here to match client_max_body_size. +client_body_buffer_size 1M; diff --git a/server/nginx/cors-header.conf b/server/nginx/cors-header.conf new file mode 100644 index 00000000..af78db3a --- /dev/null +++ b/server/nginx/cors-header.conf @@ -0,0 +1 @@ +add_header Access-Control-Allow-Origin *;