From 63a0840fd6578fd194a15bfbcf6ebdaf6ae9fb71 Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Wed, 26 Aug 2026 21:38:39 +0200
Subject: [PATCH 1/3] Implement `GET`/`PUT`/`DELETE` for
`/shells/asset-information/thumbnail`
Currently, server endpoint `/asset-information/thumbnail`,
methods=["GET", "PUT", "DELETE"], endpoint=self.not_implemented is
not implemented.
According to the AAS Repository Service OpenAPI Specification, the
endpoint should return, replace or delete the thumbnail file of a
selected AAS.
This issue specifies #439.
This change implements `GET`, `PUT` and `DELETE` for
`/shells/{aasIdentifier}/asset-information/thumbnail` according to
the specification. `GET` returns the thumbnail file referenced by
`AssetInformation.default_thumbnail`. `PUT` accepts a
`multipart/form-data` upload (`fileName` and `file`), stores it via
the file store, and updates `default_thumbnail` accordingly, removing
the previous thumbnail file if the path changes. `DELETE` removes the
stored file and clears `default_thumbnail`. All three reject external
(non-local) thumbnail references, since the server cannot serve or
manage files it does not store itself.
Fixes #493
---
server/app/interfaces/repository.py | 76 +++++++++++++++++++++++++++--
1 file changed, 73 insertions(+), 3 deletions(-)
diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py
index 8f931c78..38eeeece 100644
--- a/server/app/interfaces/repository.py
+++ b/server/app/interfaces/repository.py
@@ -79,8 +79,18 @@ def __init__(
),
Rule(
"/asset-information/thumbnail",
- methods=["GET", "PUT", "DELETE"],
- endpoint=self.not_implemented,
+ methods=["GET"],
+ endpoint=self.get_aas_thumbnail,
+ ),
+ Rule(
+ "/asset-information/thumbnail",
+ methods=["PUT"],
+ endpoint=self.put_aas_thumbnail,
+ ),
+ Rule(
+ "/asset-information/thumbnail",
+ methods=["DELETE"],
+ endpoint=self.delete_aas_thumbnail,
),
Rule("/submodel-refs", methods=["GET"], endpoint=self.get_aas_submodel_refs),
Rule("/submodel-refs", methods=["POST"], endpoint=self.post_aas_submodel_refs),
@@ -586,6 +596,66 @@ def put_aas_asset_information(
self.object_store.commit(aas)
return response_t()
+ def get_aas_thumbnail(
+ self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
+ ) -> Response:
+ shell = self._get_shell(url_args)
+ thumbnail = shell.asset_information.default_thumbnail
+ if thumbnail is None or not thumbnail.path:
+ raise NotFound(f"{shell!r} has no default thumbnail set!")
+ if not thumbnail.path.startswith("/"):
+ raise BadRequest(f"{shell!r} references an external thumbnail: {thumbnail.path}")
+ bytes_io = io.BytesIO()
+ try:
+ self.file_store.write_file(thumbnail.path, bytes_io)
+ except KeyError:
+ raise NotFound(f"No thumbnail file found at path: {thumbnail.path}")
+ return Response(bytes_io.getvalue(), content_type=thumbnail.content_type or "application/octet-stream")
+
+ def put_aas_thumbnail(
+ self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
+ ) -> Response:
+ shell = self._get_shell(url_args)
+ filename = request.form.get("fileName")
+ if filename is None:
+ raise BadRequest("No 'fileName' specified!")
+ elif not filename.startswith("/"):
+ raise BadRequest(f"Given 'fileName' doesn't start with a slash (/): {filename}")
+
+ file_storage: Optional[FileStorage] = request.files.get("file")
+ if file_storage is None:
+ raise BadRequest("Missing file to upload")
+
+ old_thumbnail = shell.asset_information.default_thumbnail
+ new_path = self.file_store.add_file(filename, file_storage.stream, file_storage.mimetype)
+ if old_thumbnail is not None and old_thumbnail.path and old_thumbnail.path.startswith("/") \
+ and old_thumbnail.path != new_path:
+ try:
+ self.file_store.delete_file(old_thumbnail.path)
+ except KeyError:
+ pass
+
+ shell.asset_information.default_thumbnail = model.Resource(new_path, file_storage.mimetype)
+ self.object_store.commit(shell)
+ return response_t()
+
+ def delete_aas_thumbnail(
+ self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
+ ) -> Response:
+ shell = self._get_shell(url_args)
+ thumbnail = shell.asset_information.default_thumbnail
+ if thumbnail is None or not thumbnail.path:
+ raise NotFound(f"{shell!r} has no default thumbnail set!")
+ if not thumbnail.path.startswith("/"):
+ raise BadRequest(f"{shell!r} references an external thumbnail: {thumbnail.path}")
+ try:
+ self.file_store.delete_file(thumbnail.path)
+ except KeyError:
+ pass
+ shell.asset_information.default_thumbnail = None
+ self.object_store.commit(shell)
+ return response_t()
+
def get_aas_submodel_refs(
self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
) -> Response:
@@ -1028,4 +1098,4 @@ def delete_concept_description(
WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()),
use_debugger=True,
use_reloader=True,
- )
+ )
\ No newline at end of file
From 7b16a4125a0553ad06f4afbffa14ca32ab34e413 Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Wed, 26 Aug 2026 21:49:47 +0200
Subject: [PATCH 2/3] Fix whitespace
---
server/app/interfaces/repository.py | 2 +-
server/test/.DS_Store | Bin 0 -> 8196 bytes
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 server/test/.DS_Store
diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py
index 38eeeece..55112583 100644
--- a/server/app/interfaces/repository.py
+++ b/server/app/interfaces/repository.py
@@ -1098,4 +1098,4 @@ def delete_concept_description(
WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()),
use_debugger=True,
use_reloader=True,
- )
\ No newline at end of file
+ )
diff --git a/server/test/.DS_Store b/server/test/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..c180929621a058fac70883d17b0cb2877e9480e9
GIT binary patch
literal 8196
zcmeHMU2GIp6u#fI&{;ar0a_`z1Dh^HzyfP2@}F$~6l|f`ZRxfYmf4+w4oqjt&g>Q}
zHI0cc0vew*{zaesA@ZQ&--{%QqK{flFvbUr#-9gG^ab_7GjnJ8Y3Y;1K%AS*J@?#m
z&b{}X`Ocl5En^JL1??WjY8hi9Jsy=Ssk=e*O%2*3DOYt1PP3Zd<8j@#ZDvbjHM`r;M>2ACi*328Vd;6_z|eG38R#`E%^qpC
z3zp{iwka+UBVr^YFB~0hXlzK-#~SZ#7^{zsZsg;}v9X9)6JN8rD|6T!vg{Mw9|U&-
zn?1?DlhZk8I>i?5Tv@8*+}9=NBC{%`*-CG^uU{I-$n%QiMdz?<*p{dXs6UjE8iZt8#LA+cbCCj^QoSmv;4|t|yx!
zO7}Z<-fi(0%gA!U(Yth7%P8utw4#$XXiLwqdXl4voSCy=$d^He~*|_Ck<$HxE;3U?V^eDSz3p7U#8S4dpt=?UZ-9Q3|1(2
zOWh9nAj&FNTrDM}?!$zKi0ts0wbDAJ$5gYr8Id)<&jx9e(p%*5kI3zOa;sFMq;vMr
z5V`0?_*|3HqU`gw?NA-Jb6B^$BV8aFxprTjqV##PCQb7Xw5WDrmC~;45arLserHh4
z>zyM6)QMpJK~ePIHO+ggwP}WHUlI0rLfNp22IT;~R-OuCy!2|~$(4KfarC32m%nmFpdDW-QsT%n0(SWd
zV%z1JoQ*(LTP`;Glgh@XS`sKs4K5YC&i4Lg0Lr_h6a$Y4LR&@coGHjWVHk7EoEV;qkV
z;!hIhpTLuN2G8O-!u`v51+U^XUMJMQgLm;hKEy{jk52;pe?#d18NWs0;~1%WHpzgFb4oMR{_`IKZt9=i;rBoM{@
Date: Wed, 26 Aug 2026 21:53:42 +0200
Subject: [PATCH 3/3] Remove accidentally committed .DS_Store
---
server/test/.DS_Store | Bin 8196 -> 0 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 server/test/.DS_Store
diff --git a/server/test/.DS_Store b/server/test/.DS_Store
deleted file mode 100644
index c180929621a058fac70883d17b0cb2877e9480e9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 8196
zcmeHMU2GIp6u#fI&{;ar0a_`z1Dh^HzyfP2@}F$~6l|f`ZRxfYmf4+w4oqjt&g>Q}
zHI0cc0vew*{zaesA@ZQ&--{%QqK{flFvbUr#-9gG^ab_7GjnJ8Y3Y;1K%AS*J@?#m
z&b{}X`Ocl5En^JL1??WjY8hi9Jsy=Ssk=e*O%2*3DOYt1PP3Zd<8j@#ZDvbjHM`r;M>2ACi*328Vd;6_z|eG38R#`E%^qpC
z3zp{iwka+UBVr^YFB~0hXlzK-#~SZ#7^{zsZsg;}v9X9)6JN8rD|6T!vg{Mw9|U&-
zn?1?DlhZk8I>i?5Tv@8*+}9=NBC{%`*-CG^uU{I-$n%QiMdz?<*p{dXs6UjE8iZt8#LA+cbCCj^QoSmv;4|t|yx!
zO7}Z<-fi(0%gA!U(Yth7%P8utw4#$XXiLwqdXl4voSCy=$d^He~*|_Ck<$HxE;3U?V^eDSz3p7U#8S4dpt=?UZ-9Q3|1(2
zOWh9nAj&FNTrDM}?!$zKi0ts0wbDAJ$5gYr8Id)<&jx9e(p%*5kI3zOa;sFMq;vMr
z5V`0?_*|3HqU`gw?NA-Jb6B^$BV8aFxprTjqV##PCQb7Xw5WDrmC~;45arLserHh4
z>zyM6)QMpJK~ePIHO+ggwP}WHUlI0rLfNp22IT;~R-OuCy!2|~$(4KfarC32m%nmFpdDW-QsT%n0(SWd
zV%z1JoQ*(LTP`;Glgh@XS`sKs4K5YC&i4Lg0Lr_h6a$Y4LR&@coGHjWVHk7EoEV;qkV
z;!hIhpTLuN2G8O-!u`v51+U^XUMJMQgLm;hKEy{jk52;pe?#d18NWs0;~1%WHpzgFb4oMR{_`IKZt9=i;rBoM{@