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{@