From cbe1a94546c0d55211165b8d533a5b5d09486cd9 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Sun, 28 Jan 2024 21:51:38 +0000 Subject: [PATCH] Resolve "Add teaser image per database for the frontend (e.g. university logo)" --- .docs/.swagger/api-metadata.yaml | 994 +++++++++--------- .../dbrepo-realm.json | 58 +- dbrepo-metadata-db/setup-schema.sql | 1 + dbrepo-metadata-service/Dockerfile | 1 + .../at/tuwien/api/database/DatabaseDto.java | 3 + .../api/database/DatabaseModifyImageDto.java | 17 + .../at/tuwien/entities/database/Database.java | 6 + .../main/java/at/tuwien/mapper/S3Mapper.java | 18 - .../at/tuwien/endpoints/DatabaseEndpoint.java | 76 +- .../tuwien/endpoints/MaintenanceEndpoint.java | 7 +- .../src/main/resources/application-local.yml | 2 + .../src/main/resources/application.yml | 2 + .../at/tuwien/annotations/MockListeners.java | 3 +- .../test/java/at/tuwien/config/S3Config.java | 26 +- .../endpoints/DatabaseEndpointUnitTest.java | 47 + .../tuwien/mvc/PrometheusEndpointMvcTest.java | 9 +- .../StorageServiceIntegrationTest.java | 79 ++ .../src/test/resources/application.properties | 6 +- .../main/java/at/tuwien/config/S3Config.java | 3 + .../at/tuwien/listener/StorageListener.java | 15 + .../listener/impl/StorageListenerImpl.java | 34 + .../at/tuwien/service/DatabaseService.java | 12 + .../at/tuwien/service/StorageService.java | 65 ++ .../service/impl/MariaDbServiceImpl.java | 16 +- .../tuwien/service/impl/QueryServiceImpl.java | 38 +- .../service/impl/SeaweedServiceImpl.java | 123 +++ .../main/java/at/tuwien/test/BaseTest.java | 2 +- dbrepo-ui/api/database.service.js | 14 + .../pages/database/_database_id/info.vue | 12 + .../pages/database/_database_id/settings.vue | 100 ++ .../_database_id/table/_table_id/info.vue | 4 +- docker-compose.yml | 1 + 32 files changed, 1224 insertions(+), 570 deletions(-) create mode 100644 dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java create mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java create mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java create mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java create mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java diff --git a/.docs/.swagger/api-metadata.yaml b/.docs/.swagger/api-metadata.yaml index e65ce0020b..df5304c003 100644 --- a/.docs/.swagger/api-metadata.yaml +++ b/.docs/.swagger/api-metadata.yaml @@ -38,14 +38,8 @@ paths: type: integer format: int64 responses: - "409": - description: Query store failed to query table history - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Find table history is not permitted + "404": + description: "Table, database or user could not be found" content: application/json: schema: @@ -64,8 +58,14 @@ paths: type: array items: $ref: '#/components/schemas/TableHistoryDto' - "404": - description: "Table, database or user could not be found" + "403": + description: Find table history is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "409": + description: Query store failed to query table history content: application/json: schema: @@ -92,14 +92,8 @@ paths: type: integer format: int64 responses: - "409": - description: Query store failed to query table history - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Find table history is not permitted + "404": + description: "Table, database or user could not be found" content: application/json: schema: @@ -118,8 +112,14 @@ paths: type: array items: $ref: '#/components/schemas/TableHistoryDto' - "404": - description: "Table, database or user could not be found" + "403": + description: Find table history is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "409": + description: Query store failed to query table history content: application/json: schema: @@ -190,24 +190,24 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Could not import csv via sidecar + "202": + description: Get table data successfully content: - application/json: + '*/*': schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/QueryResultDto' "403": description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Get table data successfully + "422": + description: Could not import csv via sidecar content: - '*/*': + application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -237,6 +237,12 @@ paths: $ref: '#/components/schemas/TableCsvDto' required: true responses: + "202": + description: Inserted data successfully + content: + '*/*': + schema: + type: object "404": description: Table or database could not be found content: @@ -249,12 +255,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Inserted data successfully - content: - '*/*': - schema: - type: object "403": description: Access to the database is forbidden content: @@ -290,22 +290,22 @@ paths: $ref: '#/components/schemas/TableCsvDeleteDto' required: true responses: + "202": + description: Deleted table data successfully "404": description: Table or database could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted table data successfully - "403": - description: Access to the database is forbidden + "400": + description: Table data or query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Table data or query is malformed + "403": + description: Access to the database is forbidden content: application/json: schema: @@ -375,24 +375,24 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Could not import csv via sidecar + "202": + description: Get table data successfully content: - application/json: + '*/*': schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/QueryResultDto' "403": description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Get table data successfully + "422": + description: Could not import csv via sidecar content: - '*/*': + application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -410,12 +410,6 @@ paths: type: string format: uuid responses: - "403": - description: Find user is not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "200": description: Found user content: @@ -428,6 +422,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "403": + description: Find user is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -450,20 +450,26 @@ paths: $ref: '#/components/schemas/UserUpdateDto' required: true responses: - "400": - description: Modify user query is malformed + "403": + description: Modify user is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Modified user information + content: + application/json: + schema: + $ref: '#/components/schemas/UserDto' "405": description: Foreign user modification content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Modify user is not permitted + "400": + description: Modify user query is malformed content: application/json: schema: @@ -474,12 +480,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modified user information - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' security: - bearerAuth: [] - basicAuth: [] @@ -503,8 +503,8 @@ paths: $ref: '#/components/schemas/UserThemeSetDto' required: true responses: - "405": - description: Foreign user modification + "404": + description: User or user attribute was not found content: application/json: schema: @@ -515,18 +515,18 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: User or user attribute was not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "202": description: Modified user theme content: application/json: schema: $ref: '#/components/schemas/UserDto' + "405": + description: Foreign user modification + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -550,14 +550,14 @@ paths: $ref: '#/components/schemas/UserPasswordDto' required: true responses: - "405": - description: Foreign user modification + "404": + description: User was not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Modify is not allowed + "503": + description: Authentication service does not respond content: application/json: schema: @@ -568,14 +568,14 @@ paths: application/json: schema: $ref: '#/components/schemas/UserDto' - "503": - description: Authentication service does not respond + "405": + description: Foreign user modification content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: User was not found + "403": + description: Modify is not allowed content: application/json: schema: @@ -656,16 +656,16 @@ paths: type: integer format: int64 responses: - "202": - description: Deleted ontology successfully - content: - application/json: {} "404": description: Could not find ontology content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Deleted ontology successfully + content: + application/json: {} security: - bearerAuth: [] - basicAuth: [] @@ -763,18 +763,18 @@ paths: type: integer format: int64 responses: - "200": - description: Found image - content: - application/json: - schema: - $ref: '#/components/schemas/ImageDto' "404": description: Image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found image + content: + application/json: + schema: + $ref: '#/components/schemas/ImageDto' put: tags: - image-endpoint @@ -853,24 +853,24 @@ paths: $ref: '#/components/schemas/DatabaseModifyVisibilityDto' required: true responses: - "202": - description: Visibility modified successfully + "404": + description: Database could not be found content: application/json: schema: - $ref: '#/components/schemas/DatabaseDto' + $ref: '#/components/schemas/ApiErrorDto' "403": description: Visibility modification is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database could not be found + "202": + description: Visibility modified successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/DatabaseDto' security: - bearerAuth: [] - basicAuth: [] @@ -894,12 +894,6 @@ paths: $ref: '#/components/schemas/DatabaseTransferDto' required: true responses: - "404": - description: Database or user could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Transfer of ownership is not permitted content: @@ -912,6 +906,12 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' + "404": + description: Database or user could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -958,12 +958,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "202": + description: Updated column semantics successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/ColumnDto' "400": description: Update semantic concept query is malformed or update unit of measurement query is malformed @@ -971,21 +971,21 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated column semantics successfully + "403": + description: Access to the database is forbidden content: application/json: schema: - $ref: '#/components/schemas/ColumnDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - /api/database/{id}/access/{userId}: + /api/database/{id}/image: put: tags: - - access-endpoint - summary: Modify access to some database - operationId: update_4 + - database-endpoint + summary: Modify database image + operationId: modifyImage parameters: - name: id in: path @@ -993,48 +993,46 @@ paths: schema: type: integer format: int64 - - name: userId - in: path - required: true - schema: - type: string - format: uuid requestBody: content: application/json: schema: - $ref: '#/components/schemas/DatabaseModifyAccessDto' + $ref: '#/components/schemas/DatabaseModifyImageDto' required: true responses: - "403": - description: Modify access not permitted when no access is granted in the - first place + "410": + description: File was not found in the Storage Service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database or user not found + "202": + description: Modify of image was successful + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' + "403": + description: Modify of image is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Modify access query or database connection is malformed + "404": + description: Database or user could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modify access succeeded security: - bearerAuth: [] - basicAuth: [] - post: + /api/database/{id}/access/{userId}: + put: tags: - access-endpoint - summary: Give access to some database - operationId: create_6 + summary: Modify access to some database + operationId: update_4 parameters: - name: id in: path @@ -1052,19 +1050,60 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DatabaseGiveAccessDto' + $ref: '#/components/schemas/DatabaseModifyAccessDto' required: true responses: - "404": + "202": + description: Modify access succeeded + "404": description: Database or user not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Granting access succeeded - "405": - description: Granting access not permitted + "403": + description: Modify access not permitted when no access is granted in the + first place + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "400": + description: Modify access query or database connection is malformed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + post: + tags: + - access-endpoint + summary: Give access to some database + operationId: create_6 + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + - name: userId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseGiveAccessDto' + required: true + responses: + "404": + description: Database or user not found content: application/json: schema: @@ -1081,6 +1120,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Granting access succeeded + "405": + description: Granting access not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1103,22 +1150,22 @@ paths: type: string format: uuid responses: - "400": - description: Modify access query or database connection is malformed + "202": + description: Revoked access successfully + "403": + description: Revoke of access not permitted as no access was found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "User, database with access was not found" + "400": + description: Modify access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Revoked access successfully - "403": - description: Revoke of access not permitted as no access was found + "404": + description: "User, database with access was not found" content: application/json: schema: @@ -1152,36 +1199,36 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Find query is not permitted + "504": + description: Query store failed to select query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed + "404": + description: "Database, query or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, query or user could not be found" + "200": + description: List queries content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "504": - description: Query store failed to select query + $ref: '#/components/schemas/QueryDto' + "503": + description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: List queries + "405": + description: Find query is not permitted content: application/json: schema: - $ref: '#/components/schemas/QueryDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1216,12 +1263,6 @@ paths: application/json: schema: $ref: '#/components/schemas/QueryDto' - "405": - description: Persist query is not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Not allowed to persist query content: @@ -1246,6 +1287,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "405": + description: Persist query is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1276,16 +1323,12 @@ paths: $ref: '#/components/schemas/SignupRequestDto' required: true responses: - "417": - description: User with e-mail already exists + "201": + description: Created user content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Parameters are not well-formed (likely email) - content: - application/json: {} + $ref: '#/components/schemas/UserBriefDto' "409": description: User with username already exists content: @@ -1298,12 +1341,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created user + "400": + description: Parameters are not well-formed (likely email) + content: + application/json: {} + "417": + description: User with e-mail already exists content: application/json: schema: - $ref: '#/components/schemas/UserBriefDto' + $ref: '#/components/schemas/ApiErrorDto' /api/semantic/ontology: get: tags: @@ -1400,12 +1447,6 @@ paths: $ref: '#/components/schemas/ImageCreateDto' required: true responses: - "201": - description: Created image - content: - application/json: - schema: - $ref: '#/components/schemas/ImageDto' "400": description: Image specification is invalid content: @@ -1418,6 +1459,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "201": + description: Created image + content: + application/json: + schema: + $ref: '#/components/schemas/ImageDto' security: - bearerAuth: [] - basicAuth: [] @@ -1483,26 +1530,20 @@ paths: $ref: '#/components/schemas/IdentifierSaveDto' required: true responses: - "400": - description: Identifier form contains invalid request data + "503": + description: DataCite system did not respond content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "406": - description: Creating identifier not allowed + "403": + description: Insufficient access rights or authorities content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created identifier - content: - application/json: - schema: - $ref: '#/components/schemas/IdentifierDto' - "502": - description: Query information could not be retrieved + "404": + description: "Failed to find database, table or view" content: application/json: schema: @@ -1513,32 +1554,38 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Insufficient access rights or authorities + "400": + description: Identifier form contains invalid request data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Creating identifier not permitted + "409": + description: Identifier for this resource already exists content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: DataCite system did not respond + "201": + description: Created identifier + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + "405": + description: Creating identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Failed to find database, table or view" + "502": + description: Query information could not be retrieved content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Identifier for this resource already exists + "406": + description: Creating identifier not allowed content: application/json: schema: @@ -1598,12 +1645,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "404": description: "Container, user or database could not be found" content: @@ -1622,6 +1663,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "503": + description: Connection to the database failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1665,12 +1712,6 @@ paths: type: integer format: int64 responses: - "404": - description: Database or user could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "200": description: Find views successfully content: @@ -1679,6 +1720,12 @@ paths: type: array items: $ref: '#/components/schemas/ViewBriefDto' + "404": + description: Database or user could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1701,14 +1748,20 @@ paths: $ref: '#/components/schemas/ViewCreateDto' required: true responses: - "400": - description: Create view query is malformed + "423": + description: Create view resulted in an invalid query statement content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database or user could not be found + "201": + description: Create view successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ViewBriefDto' + "401": + description: Credentials missing content: application/json: schema: @@ -1719,32 +1772,26 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "401": - description: Credentials missing + "503": + description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Create view successfully + "400": + description: Create view query is malformed content: application/json: schema: - $ref: '#/components/schemas/ViewBriefDto' + $ref: '#/components/schemas/ApiErrorDto' "403": description: Credentials missing content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "423": - description: Create view resulted in an invalid query statement + "404": + description: Database or user could not be found content: application/json: schema: @@ -1772,6 +1819,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "404": + description: Database could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "200": description: List tables content: @@ -1780,12 +1833,6 @@ paths: type: array items: $ref: '#/components/schemas/TableBriefDto' - "404": - description: Database could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1808,8 +1855,8 @@ paths: $ref: '#/components/schemas/TableCreateDto' required: true responses: - "400": - description: Create table query is malformed + "403": + description: Create table not permitted content: application/json: schema: @@ -1820,20 +1867,20 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created a new table + "404": + description: "Database, container or user could not be found" content: application/json: schema: - $ref: '#/components/schemas/TableBriefDto' - "403": - description: Create table not permitted + $ref: '#/components/schemas/ApiErrorDto' + "201": + description: Created a new table content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, container or user could not be found" + $ref: '#/components/schemas/TableBriefDto' + "400": + description: Create table query is malformed content: application/json: schema: @@ -1867,12 +1914,6 @@ paths: $ref: '#/components/schemas/ImportDto' required: true responses: - "404": - description: Table or database could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "202": description: Import table data successfully "400": @@ -1881,14 +1922,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Import failed in sidecar + "404": + description: Table or database could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Could not import csv via sidecar + "409": + description: Import failed in sidecar content: application/json: schema: @@ -1899,6 +1940,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "422": + description: Could not import csv via sidecar + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -1921,21 +1968,20 @@ paths: schema: type: boolean responses: - "405": - description: Find all queries is not permitted + "501": + description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "501": - description: Image is not supported + "404": + description: "Database, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "423": - description: Selection of time-versioned query resulted in an invalid query - statement + "504": + description: Query store failed to select query content: application/json: schema: @@ -1954,14 +2000,15 @@ paths: type: array items: $ref: '#/components/schemas/QueryBriefDto' - "504": - description: Query store failed to select query + "405": + description: Find all queries is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, container or user could not be found" + "423": + description: Selection of time-versioned query resulted in an invalid query + statement content: application/json: schema: @@ -2013,12 +2060,6 @@ paths: $ref: '#/components/schemas/ExecuteStatementDto' required: true responses: - "417": - description: Could not parse columns - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Execute query not permitted content: @@ -2031,8 +2072,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, query or user could not be found" + "417": + description: Could not parse columns content: application/json: schema: @@ -2043,6 +2084,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Database, query or user could not be found" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "202": description: Executed query content: @@ -2086,6 +2133,12 @@ paths: $ref: '#/components/schemas/ContainerCreateRequestDto' required: true responses: + "201": + description: Created a new container + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerBriefDto' "409": description: Container name already exists content: @@ -2098,12 +2151,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created a new container - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerBriefDto' security: - bearerAuth: [] - basicAuth: [] @@ -2146,22 +2193,14 @@ paths: schema: type: string responses: - "200": - description: Found entities - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/EntityDto' "400": description: Filter params are invalid content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Ontology does not have rdf or sparql endpoint + "417": + description: Generated query or uri is malformed content: application/json: schema: @@ -2172,12 +2211,20 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Generated query or uri is malformed + "422": + description: Ontology does not have rdf or sparql endpoint content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found entities + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EntityDto' security: - bearerAuth: [] - basicAuth: [] @@ -2262,6 +2309,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Suggested table column semantics successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TableColumnEntityDto' "404": description: Could not find the table column content: @@ -2274,14 +2329,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Suggested table column semantics successfully - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/TableColumnEntityDto' security: - bearerAuth: [] - basicAuth: [] @@ -2319,41 +2366,29 @@ paths: schema: type: string responses: - "409": - description: Exported resource was not found + "400": + description: "Identifier could not be exported, the requested style is not\ + \ known" content: - text/csv: + text/bibliography: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found identifier successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IdentifierDto' - text/csv: {} - text/xml: {} - text/bibliography: {} - text/bibliography; style=apa: {} - text/bibliography; style=ieee: {} - text/bibliography; style=bibtex: {} - "422": - description: Failed to retrieve from database sidecar + "503": + description: Identifier could not exported from database as it is not reachable content: text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Identifier could not be found + "409": + description: Exported resource was not found content: text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: "Identifier could not be exported, the requested style is not\ - \ known" + "422": + description: Failed to retrieve from database sidecar content: - text/bibliography: + text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' "410": @@ -2362,12 +2397,24 @@ paths: text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Identifier could not exported from database as it is not reachable + "404": + description: Identifier could not be found content: text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found identifier successfully + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + text/csv: {} + text/xml: {} + text/bibliography: {} + text/bibliography; style=apa: {} + text/bibliography; style=ieee: {} + text/bibliography; style=bibtex: {} /api/oai: get: tags: @@ -2441,18 +2488,18 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Database found successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseDto' "503": description: Connection to the broker service could not be established content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Database found successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' security: - bearerAuth: [] - basicAuth: [] @@ -2482,20 +2529,20 @@ paths: type: string format: date-time responses: - "201": - description: Created identifier + "422": + description: Sidecar operation could not be completed content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - "403": - description: Operation is not allowed + $ref: '#/components/schemas/ApiErrorDto' + "201": + description: Created identifier content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Database connection could not be established + $ref: '#/components/schemas/IdentifierDto' + "409": + description: Failed to export file from sidecar content: application/json: schema: @@ -2506,26 +2553,26 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or user was not found" + "410": + description: Blob storage operation could not be completed content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "410": - description: Blob storage operation could not be completed + $ref: '#/components/schemas/ApiErrorDto' + "503": + description: Database connection could not be established content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Failed to export file from sidecar + "404": + description: "Table, database or user was not found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Sidecar operation could not be completed + "403": + description: Operation is not allowed content: application/json: schema: @@ -2547,6 +2594,12 @@ paths: type: integer format: int64 responses: + "403": + description: No access to this database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "404": description: Database not found content: @@ -2559,12 +2612,6 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseAccessDto' - "403": - description: No access to this database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -2594,18 +2641,18 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Find view successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ViewDto' "404": description: "Database, view or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Find view successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ViewDto' security: - bearerAuth: [] - basicAuth: [] @@ -2628,22 +2675,14 @@ paths: type: integer format: int64 responses: - "200": - description: Delete view successfully "405": description: Delete view is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Deletion not allowed + "404": + description: "Database, view or user could not be found" content: application/json: schema: @@ -2660,8 +2699,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, view or user could not be found" + "200": + description: Delete view successfully + "503": + description: Connection to the database failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "403": + description: Deletion not allowed content: application/json: schema: @@ -2701,26 +2748,26 @@ paths: type: integer format: int64 responses: - "200": - description: Find data successfully + "400": + description: Pagination not in valid range or find data query is malformed content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' - "403": - description: View data not allowed + $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Find data successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Pagination not in valid range or find data query is malformed + $ref: '#/components/schemas/QueryResultDto' + "404": + description: "Database, view, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, view, container or user could not be found" + "403": + description: View data not allowed content: application/json: schema: @@ -2748,33 +2795,33 @@ paths: type: integer format: int64 responses: - "200": - description: Count data successfully + "400": + description: Pagination not in valid range or find data query is malformed content: application/json: schema: - type: integer - format: int64 - "403": - description: Count data not allowed + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Database, view, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Pagination not in valid range or find data query is malformed + "200": + description: Count data successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' + type: integer + format: int64 "409": description: Could not count query data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, view, container or user could not be found" + "403": + description: Count data not allowed content: application/json: schema: @@ -2802,26 +2849,26 @@ paths: type: integer format: int64 responses: - "503": - description: Could not communicate with the broker service + "200": + description: Find table successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or container could not be found" + $ref: '#/components/schemas/TableDto' + "503": + description: Could not communicate with the broker service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Find table successfully + "403": + description: Access to the database is forbidden content: application/json: schema: - $ref: '#/components/schemas/TableDto' - "403": - description: Access to the database is forbidden + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Table, database or container could not be found" content: application/json: schema: @@ -2856,14 +2903,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or container could not be found" + "403": + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "404": + description: "Table, database or container could not be found" content: application/json: schema: @@ -2909,25 +2956,25 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Could not import csv via sidecar + "202": + description: Get table data count successfully content: - application/json: + '*/*': schema: - $ref: '#/components/schemas/ApiErrorDto' + type: integer + format: int64 "403": description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Get table data count successfully + "422": + description: Could not import csv via sidecar content: - '*/*': + application/json: schema: - type: integer - format: int64 + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -2956,18 +3003,6 @@ paths: schema: type: string responses: - "409": - description: Export of query failed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Executed query - content: - '*/*': - schema: - type: object "403": description: Execute query not permitted content: @@ -2986,14 +3021,26 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Executed query + content: + '*/*': + schema: + type: object + "404": + description: Database or query could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "422": description: Sidecar failed to export content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database or query could not be found + "409": + description: Export of query failed content: application/json: schema: @@ -3046,12 +3093,6 @@ paths: schema: type: string responses: - "417": - description: Could not parse columns - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Execute query not permitted content: @@ -3064,6 +3105,18 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "417": + description: Could not parse columns + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: Database or query could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "409": description: Could not store query in query store content: @@ -3076,12 +3129,6 @@ paths: application/json: schema: $ref: '#/components/schemas/QueryResultDto' - "404": - description: Database or query could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] @@ -3105,12 +3152,6 @@ paths: type: integer format: int64 responses: - "417": - description: Could not parse columns - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Execute query not permitted content: @@ -3123,18 +3164,24 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Executed query + "417": + description: Could not parse columns content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' "404": description: Database or query could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Executed query + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' security: - bearerAuth: [] - basicAuth: [] @@ -3167,18 +3214,18 @@ paths: type: integer format: int64 responses: - "404": - description: Container image could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "200": description: Found container content: application/json: schema: $ref: '#/components/schemas/ContainerDto' + "404": + description: Container image could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' delete: tags: - container-endpoint @@ -3192,18 +3239,18 @@ paths: type: integer format: int64 responses: - "202": - description: Deleted container successfully - content: - '*/*': - schema: - type: object "404": description: Container not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Deleted container successfully + content: + '*/*': + schema: + type: object security: - bearerAuth: [] - basicAuth: [] @@ -4080,6 +4127,9 @@ components: $ref: '#/components/schemas/UserDto' owner: $ref: '#/components/schemas/UserDto' + image: + type: string + format: byte created: type: string format: date-time @@ -5200,6 +5250,11 @@ components: type: string unit_uri: type: string + DatabaseModifyImageDto: + type: object + properties: + key: + type: string DatabaseModifyAccessDto: required: - type @@ -6732,10 +6787,10 @@ components: type: string parametersString: type: string - untilDate: + fromDate: type: string format: date-time - fromDate: + untilDate: type: string format: date-time BannerMessageDto: @@ -6927,12 +6982,12 @@ components: type: string identifier: $ref: '#/components/schemas/Identifier' - apaName: - type: string bibtexName: type: string ieeeName: type: string + apaName: + type: string Database: type: object properties: @@ -6989,6 +7044,9 @@ components: $ref: '#/components/schemas/DatabaseAccess' isPublic: type: boolean + image: + type: string + format: byte created: type: string format: date-time diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json index c861fbd662..b2730612a7 100644 --- a/dbrepo-authentication-service/dbrepo-realm.json +++ b/dbrepo-authentication-service/dbrepo-realm.json @@ -99,7 +99,7 @@ "description" : "${default-database-handling}", "composite" : true, "composites" : { - "realm" : [ "modify-database-owner", "update-database-access", "create-database", "list-databases", "create-database-access", "find-database", "modify-database-visibility", "import-database-data", "delete-database-access", "check-database-access" ] + "realm" : [ "modify-database-image", "modify-database-owner", "update-database-access", "create-database", "list-databases", "create-database-access", "find-database", "modify-database-visibility", "import-database-data", "delete-database-access", "check-database-access" ] }, "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", @@ -220,6 +220,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "1f0a9b13-c2b8-474c-bc08-59dbd71835a6", + "name" : "modify-database-image", + "description" : "${modify-database-image}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "a7ad038c-5c06-42fc-951c-15ac09d4df66", "name" : "modify-database-owner", @@ -2049,7 +2057,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -2058,7 +2066,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -2110,7 +2118,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "b8378805-a082-46a0-9e28-a1e5d4db7e41", + "id" : "05f92ecb-5a34-416a-a9a4-b4aeab2704c4", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2132,7 +2140,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2652bbd9-bd49-465c-8595-690099333bf7", + "id" : "e85f1d42-30c8-4878-ab0c-3cb9baaa308f", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2161,7 +2169,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "967c3248-c2e9-45a9-b770-b02e965b958a", + "id" : "754e6269-c096-41d6-88df-44bd2652ec82", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2183,7 +2191,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f78ad348-c3e1-476e-a916-fce0c383376a", + "id" : "5b2a16dd-7192-4558-931a-a67dfa7b14e1", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2205,7 +2213,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "788cf02b-5744-4ea6-940a-96bc762da4bd", + "id" : "c12d7c33-256e-486f-8fb8-c8594eafd64e", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2227,7 +2235,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "273e61b7-9cc3-464e-a7b8-27c71aca4014", + "id" : "711adf58-692f-4f22-ae20-0ba01d8d667c", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2249,7 +2257,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "00f41bfc-8513-466d-8c6a-366b7f2f36ca", + "id" : "dd53182d-ca4a-4096-b1fc-60237af977c4", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2271,7 +2279,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "980ebf01-fe0a-4cfa-880e-dd86ce8e190e", + "id" : "23c368c2-dce4-4ca8-8096-b6c726fa0e32", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2294,7 +2302,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5e6a7a10-4be8-4038-8fc5-0588b452328d", + "id" : "37ff6b93-bdfe-4245-9247-009061fdfc7b", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2316,7 +2324,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "024e07f8-f975-41ef-b755-d2b089b5567c", + "id" : "c1f58e18-5d41-40b1-aa73-4a4e4a970430", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2352,7 +2360,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "324da9be-755e-4556-a1d3-58569b9df47c", + "id" : "9229472e-78c8-4e83-aa20-7a2e22c28f59", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2388,7 +2396,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "bced47d4-5d04-4bb9-8605-94041185c0f3", + "id" : "d841dca1-b9ca-47bc-8f9a-dcd5896678dd", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2417,7 +2425,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6b301d9d-68c0-44c3-9a57-92669d08b2f3", + "id" : "42e0301c-d81c-4127-9e17-064811566f9a", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2432,7 +2440,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "9c9ddfeb-37a2-4186-a58f-cf90dca8e191", + "id" : "4809629a-0e3c-4894-8cd7-60d99abeb2e8", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2455,7 +2463,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a9ef5094-93bf-49fc-9d0f-dcfc551cac5a", + "id" : "7ce37ac0-9aba-412d-98fb-78745e6df1ff", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2477,7 +2485,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "fae6e2e4-a071-458b-ac03-41dda3456f5a", + "id" : "9fa4ee30-9ab4-40c3-bb9f-b56b8738d1c0", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2499,7 +2507,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ae5bcac5-8867-42e1-887f-fc67418b0c4b", + "id" : "bba37884-4bd0-4597-9f26-e8b8c7d60dc6", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2515,7 +2523,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "72524b5d-1cfc-41b0-b29b-6f6890d2dc7f", + "id" : "9e3b3ba5-e37e-4f6d-a7a7-fd37558f6e2d", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2551,7 +2559,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "834c96b8-790d-4869-8c66-d42cd35e4873", + "id" : "e38d574a-2171-408b-9f9d-1ebe60791110", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2587,7 +2595,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7f131501-e3ff-48f2-98e6-e34e4c5d6f9e", + "id" : "5560dfff-822c-43fb-a910-db38b4470268", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2603,13 +2611,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "638341f1-94ba-4042-a3ee-41a0f41718f6", + "id" : "201f18f6-b170-4fcc-bcc2-2ca05b1558aa", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "3c355b8c-8a51-4346-88f2-1ff81856b55c", + "id" : "f6e84d09-4994-452a-be1a-fe896289ae9d", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql index 2981117119..f3de67233f 100644 --- a/dbrepo-metadata-db/setup-schema.sql +++ b/dbrepo-metadata-db/setup-schema.sql @@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS `mdb_databases` description text, engine character varying(20), is_public boolean NOT NULL DEFAULT TRUE, + image longblob, created_by character varying(36), owned_by character varying(36), contact_person character varying(36), diff --git a/dbrepo-metadata-service/Dockerfile b/dbrepo-metadata-service/Dockerfile index 14f3dbe95f..4c9ae73e54 100644 --- a/dbrepo-metadata-service/Dockerfile +++ b/dbrepo-metadata-service/Dockerfile @@ -75,6 +75,7 @@ ENV DATACITE_PASSWORD="" ENV S3_STORAGE_ENDPOINT="http://storage-service:9000" ENV S3_ACCESS_KEY_ID="seaweedfsadmin" ENV S3_SECRET_ACCESS_KEY="seaweedfsadmin" +ENV DELETE_STALE_FILES_RATE=60 ENV MIRROR_RATE=60 ENV OBTAIN_METADATA_RATE=60 ENV DELETE_STALE_QUERIES_RATE=60 diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java index e6db0c9736..9ecf26e386 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java @@ -97,6 +97,9 @@ public class DatabaseDto { @Field(name = "owner", type = FieldType.Object) private UserDto owner; + @ToString.Exclude + private byte[] image; + @NotNull @Schema(example = "2021-03-12T15:26:21Z") @Field(name = "created", type = FieldType.Date) diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java new file mode 100644 index 0000000000..627714f6cb --- /dev/null +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java @@ -0,0 +1,17 @@ +package at.tuwien.api.database; + +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class DatabaseModifyImageDto { + + private String key; + +} diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java index 8a1f869a0e..19b7eae6c9 100644 --- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java +++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java @@ -17,6 +17,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.io.Serializable; +import java.sql.Blob; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -131,6 +132,11 @@ public class Database implements Serializable { @Column(nullable = false) private Boolean isPublic; + @Lob + @Basic(fetch = FetchType.LAZY) + @Column(columnDefinition = "LONGBLOB") + private byte[] image; + @CreatedDate @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP default NOW()") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java index 87e68ef389..6e89e98494 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java @@ -1,28 +1,10 @@ package at.tuwien.mapper; -import io.minio.GetObjectArgs; import org.mapstruct.Mapper; -import org.springframework.http.MediaType; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; - -import java.util.Collections; @Mapper(componentModel = "spring") public interface S3Mapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S3Mapper.class); - default GetObjectArgs s3ArgsToObjectArgs(String bucketName, String key) { - return s3ArgsToObjectArgs(bucketName, key, null); - } - - default GetObjectArgs s3ArgsToObjectArgs(String bucketName, String key, Long length) { - final GetObjectArgs.Builder builder = GetObjectArgs.builder() - .bucket(bucketName) - .object(key); - if (length != null) { - builder.length(length); - } - return builder.build(); - } } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index 5f6aad756d..435e31feeb 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -4,6 +4,7 @@ import at.tuwien.api.amqp.ExchangeDto; import at.tuwien.api.database.*; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.config.RabbitConfig; +import at.tuwien.config.S3Config; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.user.User; @@ -13,6 +14,10 @@ import at.tuwien.service.*; import at.tuwien.utils.PrincipalUtil; import at.tuwien.utils.UserUtil; import io.micrometer.observation.annotation.Observed; +import io.minio.GetObjectArgs; +import io.minio.GetObjectResponse; +import io.minio.MinioClient; +import io.minio.errors.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -31,6 +36,9 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.List; import java.util.stream.Collectors; @@ -45,16 +53,19 @@ public class DatabaseEndpoint { private final RabbitConfig rabbitConfig; private final AccessService accessService; private final DatabaseMapper databaseMapper; + private final StorageService storageService; private final DatabaseService databaseService; private final QueryStoreService queryStoreService; private final MessageQueueService messageQueueService; @Autowired public DatabaseEndpoint(DatabaseMapper databaseMapper, UserService userService, RabbitConfig rabbitConfig, - DatabaseService databaseService, QueryStoreService queryStoreService, - AccessService accessService, MessageQueueService messageQueueService) { + StorageService storageService, DatabaseService databaseService, + QueryStoreService queryStoreService, AccessService accessService, + MessageQueueService messageQueueService) { this.userService = userService; this.rabbitConfig = rabbitConfig; + this.storageService = storageService; this.accessService = accessService; this.databaseMapper = databaseMapper; this.databaseService = databaseService; @@ -218,11 +229,11 @@ public class DatabaseEndpoint { @Valid @RequestBody DatabaseModifyVisibilityDto data, @NotNull Principal principal) throws DatabaseNotFoundException, NotAllowedException { - log.debug("endpoint update database, id={}, data={}, {}", id, data, PrincipalUtil.formatForDebug(principal)); + log.debug("endpoint modify database visibility, id={}, data={}, {}", id, data, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.findById(id); if (!database.getOwnedBy().equals(UserUtil.getId(principal))) { - log.error("Failed to create database: not owner"); - throw new NotAllowedException(("Failed to create database: not owner")); + log.error("Failed to modify database visibility: not owner"); + throw new NotAllowedException("Failed to modify database visibility: not owner"); } final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(databaseService.visibility(id, data)); log.trace("update database resulted in database {}", dto); @@ -256,12 +267,12 @@ public class DatabaseEndpoint { @Valid @RequestBody DatabaseTransferDto transferDto, @NotNull Principal principal) throws DatabaseNotFoundException, UserNotFoundException, NotAllowedException { - log.debug("endpoint update database, id={}, transferDto={}, {}", id, transferDto, PrincipalUtil.formatForDebug(principal)); + log.debug("endpoint transfer database, id={}, transferDto={}, {}", id, transferDto, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.findById(id); final User user = userService.findByUsername(principal.getName()); if (!database.getOwnedBy().equals(user.getId())) { - log.error("Failed to create database: not owner"); - throw new NotAllowedException(("Failed to create database: not owner")); + log.error("Failed to transfer database: not owner"); + throw new NotAllowedException("Failed to transfer database: not owner"); } final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(databaseService.transfer(id, transferDto)); log.trace("update database resulted in database {}", dto); @@ -269,6 +280,55 @@ public class DatabaseEndpoint { .body(dto); } + @PutMapping("/{id}/image") + @Transactional + @PreAuthorize("hasAuthority('modify-database-image')") + @Observed(name = "dbr_database_image") + @Operation(summary = "Modify database image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Modify of image was successful", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = DatabaseDto.class))}), + @ApiResponse(responseCode = "404", + description = "Database or user could not be found", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "403", + description = "Modify of image is not permitted", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "410", + description = "File was not found in the Storage Service", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + }) + public ResponseEntity<DatabaseDto> modifyImage(@NotNull @PathVariable Long id, + @Valid @RequestBody DatabaseModifyImageDto imageDto, + @NotNull Principal principal) throws DatabaseNotFoundException, + UserNotFoundException, NotAllowedException, FileStorageException { + log.debug("endpoint modify database image, id={}, imageDto={}, {}", id, imageDto, PrincipalUtil.formatForDebug(principal)); + final Database database = databaseService.findById(id); + final User user = userService.findByUsername(principal.getName()); + if (!database.getOwnedBy().equals(user.getId())) { + log.error("Failed to update database image: not owner"); + throw new NotAllowedException("Failed to update database image: not owner"); + } + final DatabaseDto dto; + byte[] image = null; + if (imageDto.getKey() != null) { + image = storageService.getBytes(imageDto.getKey()); + } + dto = databaseMapper.databaseToDatabaseDto(databaseService.modifyImage(id, image)); + log.trace("update database resulted in database {}", dto); + return ResponseEntity.accepted() + .body(dto); + } + @GetMapping("/{id}") @Transactional(readOnly = true) @Observed(name = "dbr_database_find") diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java index e0702cd8a5..dd003742bd 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; @@ -106,7 +107,7 @@ public class MaintenanceEndpoint { @PostMapping("/message") @Observed(name = "dbr_maintenance_create") - @Operation(summary = "Create maintenance message") + @Operation(summary = "Create maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('create-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "201", @@ -125,7 +126,7 @@ public class MaintenanceEndpoint { @PutMapping("/message/{id}") @Observed(name = "dbr_maintenance_update") - @Operation(summary = "Update maintenance message") + @Operation(summary = "Update maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('update-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -151,7 +152,7 @@ public class MaintenanceEndpoint { @DeleteMapping("/message/{id}") @Observed(name = "dbr_maintenance_delete") - @Operation(summary = "Delete maintenance message") + @Operation(summary = "Delete maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('delete-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "202", diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml index ed462f3bc4..1ae0d498b2 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml @@ -59,6 +59,8 @@ fda: secretAccessKey: seaweedfsadmin importBucket: dbrepo-upload exportBucket: dbrepo-download + deleteStaleFilesRate: 60 + staleSeconds: 60 jwt: issuer: http://localhost/api/auth/realms/dbrepo public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml index 26bcbeb843..994ce611d0 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml @@ -72,6 +72,8 @@ fda: secretAccessKey: "${S3_SECRET_ACCESS_KEY}" importBucket: "${S3_IMPORT_BUCKET}" exportBucket: "${S3_EXPORT_BUCKET}" + deleteStaleFilesRate: "${DELETE_STALE_FILES_RATE}" + staleSeconds: 3600 jwt: issuer: "${JWT_ISSUER}" public_key: "${JWT_PUBKEY}" diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java index e5b7427ad0..14fb3972ef 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java @@ -2,6 +2,7 @@ package at.tuwien.annotations; import at.tuwien.listener.DatabaseListener; import at.tuwien.listener.MirrorListener; +import at.tuwien.listener.StorageListener; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBeans; @@ -12,6 +13,6 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@MockBeans({@MockBean(DatabaseListener.class), @MockBean(MirrorListener.class)}) +@MockBeans({@MockBean(DatabaseListener.class), @MockBean(MirrorListener.class), @MockBean(StorageListener.class)}) public @interface MockListeners { } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java index 98ed8d5a21..7ecd9496e4 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java @@ -1,20 +1,17 @@ package at.tuwien.config; -import io.minio.BucketExistsArgs; -import io.minio.MakeBucketArgs; -import io.minio.MinioClient; -import io.minio.UploadObjectArgs; +import io.minio.*; +import io.minio.errors.*; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; @Slf4j @Getter @@ -36,6 +33,9 @@ public class S3Config { @Value("${fda.s3.exportBucket}") private String s3ExportBucket; + @Value("${fda.s3.staleSeconds}") + private Integer staleSeconds; + @Bean public MinioClient minioClient() { return MinioClient.builder() @@ -74,6 +74,18 @@ public class S3Config { } } + public boolean objectExists(String bucket, String key) { + try { + final StatObjectResponse response = minioClient().statObject(StatObjectArgs.builder() + .object(key) + .bucket(bucket) + .build()); + return true; + } catch (Exception e) { + return false; + } + } + public void uploadFile(String bucket, String filepath, String filename) throws IOException { final File file = new File(filepath); if (!file.exists()) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java index 2b6b480b59..1fc625b8dc 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java @@ -16,7 +16,12 @@ import at.tuwien.service.ContainerService; import at.tuwien.service.MessageQueueService; import at.tuwien.service.QueryStoreService; import at.tuwien.service.impl.MariaDbServiceImpl; +import io.minio.GetObjectArgs; +import io.minio.GetObjectResponse; +import io.minio.MinioClient; +import io.minio.errors.*; import lombok.extern.log4j.Log4j2; +import okhttp3.Headers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +33,10 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.List; import java.util.Optional; @@ -71,6 +80,9 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @MockBean private UserRepository userRepository; + @MockBean + private MinioClient minioClient; + @Autowired private DatabaseEndpoint databaseEndpoint; @@ -275,6 +287,41 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { }); } + @Test + @WithMockUser(username = USER_4_USERNAME) + public void modifyImage_noRole_fails() { + final DatabaseModifyImageDto request = DatabaseModifyImageDto.builder() + .key("s3key_here") + .build(); + + /* test */ + assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> { + databaseEndpoint.modifyImage(DATABASE_3_ID, request, USER_4_PRINCIPAL); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-database-image"}) + public void modifyImage_hasRole_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + NotAllowedException, IOException, FileStorageException, ServerException, InsufficientDataException, + ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, + XmlParserException, InternalException { + final DatabaseModifyImageDto request = DatabaseModifyImageDto.builder() + .key("s3key_here") + .build(); + + /* mock */ + when(databaseService.findById(DATABASE_1_ID)) + .thenReturn(DATABASE_1); + when(minioClient.getObject(any(GetObjectArgs.class))) + .thenReturn(new GetObjectResponse(Headers.of(), "dbrepo-upload", "default", "object", InputStream.nullInputStream())); + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + databaseEndpoint.modifyImage(DATABASE_1_ID, request, USER_1_PRINCIPAL); + } + @Test @WithMockUser(username = USER_4_USERNAME) public void transfer_noRole_fails() { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index 680754fd3f..94a97d3643 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -197,7 +197,7 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database", "modify-database-visibility", "modify-database-owner", "delete-database"}) + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database", "modify-database-visibility", "modify-database-owner", "delete-database", "modify-database-image"}) public void prometheusDatabaseEndpoint_succeeds() { /* mock */ @@ -231,9 +231,14 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } catch (Exception e) { /* ignore */ } + try { + databaseEndpoint.modifyImage(DATABASE_1_ID, DatabaseModifyImageDto.builder().build(), USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } /* test */ - for (String metric : List.of("dbr_database_findall", "dbr_database_count", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find")) { + for (String metric : List.of("dbr_database_findall", "dbr_database_count", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find", "dbr_database_image")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java new file mode 100644 index 0000000000..b058a69867 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java @@ -0,0 +1,79 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.config.S3Config; +import at.tuwien.exception.*; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@Testcontainers +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockListeners +@MockOpensearch +public class StorageServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private S3Config s3Config; + + @Autowired + private StorageService storageService; + + @Container + private static MinIOContainer minIOContainer = new MinIOContainer("minio/minio") + .withUserName("seaweedfsadmin") + .withPassword("seaweedfsadmin"); + + @DynamicPropertySource + static void openSearchProperties(DynamicPropertyRegistry registry) { + registry.add("fda.s3.endpoint", () -> minIOContainer.getS3URL()); + } + + @BeforeEach + public void beforeEach() throws IOException { + s3Config.makeBuckets(s3Config.getS3ImportBucket()); + s3Config.uploadFile(s3Config.getS3ImportBucket(), "./src/test/resources/csv/testdata.csv", "s3_filekey"); + } + + @Test + public void deleteStaleFiles_succeeds() throws FileStorageException, InterruptedException { + + /* test */ + Thread.sleep(5000); + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + assertFalse(s3Config.objectExists(s3Config.getS3ImportBucket(), "s3_filekey")); + } + + @Test + public void deleteStaleFiles_fails() throws FileStorageException { + + /* test */ + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + assertTrue(s3Config.objectExists(s3Config.getS3ImportBucket(), "s3_filekey")); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/resources/application.properties b/dbrepo-metadata-service/rest-service/src/test/resources/application.properties index 70e4aa0fa0..81fe783d17 100644 --- a/dbrepo-metadata-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-metadata-service/rest-service/src/test/resources/application.properties @@ -16,8 +16,7 @@ spring.jpa.hibernate.ddl-auto=create # logging logging.level.root=error -logging.level.at.tuwien.=debug -logging.level.at.tuwien.validation.=trace +logging.level.at.tuwien.=trace # rabbitmq spring.rabbitmq.host=localhost @@ -34,5 +33,8 @@ fda.datacite.password: test-password # keycloak fda.keycloak.endpoint: http://localhost:8080/ +# s3 +fda.s3.staleSeconds=1 + # consumers fda.consumers=2 \ No newline at end of file diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java index 6e99fc0ee2..3bbf37d2cf 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java @@ -27,6 +27,9 @@ public class S3Config { @Value("${fda.s3.exportBucket}") private String s3ExportBucket; + @Value("${fda.s3.staleSeconds}") + private Integer staleSeconds; + @Bean public MinioClient minioClient() { return MinioClient.builder() diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java new file mode 100644 index 0000000000..88a5260387 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java @@ -0,0 +1,15 @@ +package at.tuwien.listener; + +import at.tuwien.exception.FileStorageException; +import org.springframework.scheduling.annotation.Scheduled; + +public interface StorageListener { + + /** + * Deletes old files from the buckets used by the system in regular intervals. + * + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + @Scheduled + void deleteStaleFiles() throws FileStorageException; +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java new file mode 100644 index 0000000000..73c4c9913a --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java @@ -0,0 +1,34 @@ +package at.tuwien.listener.impl; + +import at.tuwien.config.S3Config; +import at.tuwien.exception.FileStorageException; +import at.tuwien.listener.StorageListener; +import at.tuwien.service.StorageService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Log4j2 +@Component +public class StorageListenerImpl implements StorageListener { + + final S3Config s3Config; + final StorageService storageService; + + @Autowired + public StorageListenerImpl(S3Config s3Config, StorageService storageService) { + this.s3Config = s3Config; + this.storageService = storageService; + } + + @Override + @Scheduled(fixedRateString = "${fda.s3.deleteStaleFilesRate}", timeUnit = TimeUnit.SECONDS) + public void deleteStaleFiles() throws FileStorageException { + storageService.deleteStaleFiles(s3Config.getS3ExportBucket()); + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + } + +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java index e013d98274..59c0743a2e 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java @@ -1,12 +1,14 @@ package at.tuwien.service; import at.tuwien.api.database.DatabaseCreateDto; +import at.tuwien.api.database.DatabaseModifyImageDto; import at.tuwien.api.database.DatabaseModifyVisibilityDto; import at.tuwien.api.database.DatabaseTransferDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.user.User; import at.tuwien.exception.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.security.Principal; import java.util.List; @@ -101,6 +103,16 @@ public interface DatabaseService { Database transfer(Long databaseId, DatabaseTransferDto transferDto) throws DatabaseNotFoundException, UserNotFoundException; + /** + * Modify image of database with given id. + * + * @param databaseId The database id. + * @param image The image. + * @return The database, if successful. + * @throws DatabaseNotFoundException The database was not found in the metadata database. + */ + Database modifyImage(Long databaseId, byte[] image) throws DatabaseNotFoundException; + /** * Obtain metadata from database with given id to read table and view information (schema) and write it to the metadata database for management by DBRepo. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java new file mode 100644 index 0000000000..52a32bd563 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java @@ -0,0 +1,65 @@ +package at.tuwien.service; + +import at.tuwien.ExportResource; +import at.tuwien.exception.FileStorageException; + +import java.io.InputStream; + +public interface StorageService { + + /** + * Loads an object of a bucket from the Storage Service into an input stream. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The input stream, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + InputStream getObject(String bucket, String key) throws FileStorageException; + + /** + * Loads an object of the default upload bucket from the Storage Service into a byte array. + * + * @param key The object key. + * @return The byte array. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + byte[] getBytes(String key) throws FileStorageException; + + /** + * Loads an object of a bucket from the Storage Service into a byte array. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The byte array. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + byte[] getBytes(String bucket, String key) throws FileStorageException; + + /** + * Loads an object of the default export bucket from the Storage Service into an export resource. + * + * @param key The object key. + * @return The export resource, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + ExportResource getResource(String key) throws FileStorageException; + + /** + * Loads an object of a bucket from the Storage Service into an export resource. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The export resource, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + ExportResource getResource(String bucket, String key) throws FileStorageException; + + /** + * Deletes files older than an hour from the bucket. + * + * @param bucketName The bucket name. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + void deleteStaleFiles(String bucketName) throws FileStorageException; +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java index 626cbc38d3..1b675b0fac 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java @@ -1,6 +1,7 @@ package at.tuwien.service.impl; import at.tuwien.api.database.DatabaseCreateDto; +import at.tuwien.api.database.DatabaseModifyImageDto; import at.tuwien.api.database.DatabaseModifyVisibilityDto; import at.tuwien.api.database.DatabaseTransferDto; import at.tuwien.config.QueryConfig; @@ -36,7 +37,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; -import java.util.function.UnaryOperator; @Log4j2 @Service @@ -214,6 +214,20 @@ public class MariaDbServiceImpl extends HibernateConnector implements DatabaseSe return entity; } + @Override + @Transactional + public Database modifyImage(Long databaseId, byte[] image) throws DatabaseNotFoundException { + /* check */ + final Database database = findById(databaseId); + /* update in metadata database */ + database.setImage(image); + final Database entity = databaseRepository.save(database); + /* save in open search database */ + databaseIdxRepository.save(databaseMapper.databaseToDatabaseDto(entity)); + log.info("Updated database owner of database with id {} in metadata database & search database", entity.getId()); + return entity; + } + @Override @Transactional public Database obtainMetadata(Long databaseId) throws DatabaseNotFoundException, QueryMalformedException, diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index 94b66158f2..775df9e82d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -7,7 +7,6 @@ import at.tuwien.api.database.query.ImportDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; -import at.tuwien.config.S3Config; import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.View; @@ -18,26 +17,15 @@ import at.tuwien.gateway.DataDbSidecarGateway; import at.tuwien.mapper.QueryMapper; import at.tuwien.mapper.ViewMapper; import at.tuwien.querystore.Query; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.QueryService; -import at.tuwien.service.StoreService; -import at.tuwien.service.TableService; +import at.tuwien.service.*; import com.mchange.v2.c3p0.ComboPooledDataSource; -import io.minio.GetObjectArgs; -import io.minio.MinioClient; -import io.minio.errors.*; import lombok.extern.log4j.Log4j2; import net.sf.jsqlparser.JSQLParserException; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; -import java.io.InputStream; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.sql.Connection; import java.sql.PreparedStatement; @@ -51,24 +39,22 @@ import java.util.List; @Service public class QueryServiceImpl extends HibernateConnector implements QueryService { - private final S3Config s3Config; private final ViewMapper viewMapper; - private final MinioClient minioClient; private final QueryMapper queryMapper; private final StoreService storeService; private final TableService tableService; + private final StorageService storageService; private final DatabaseService databaseService; private final DataDbSidecarGateway dataDbSidecarGateway; @Autowired - public QueryServiceImpl(S3Config s3Config, ViewMapper viewMapper, MinioClient minioClient, QueryMapper queryMapper, - TableService tableService, DatabaseService databaseService, StoreService storeService, + public QueryServiceImpl(ViewMapper viewMapper, QueryMapper queryMapper, TableService tableService, + StorageService storageService, DatabaseService databaseService, StoreService storeService, DataDbSidecarGateway dataDbSidecarGateway) { - this.s3Config = s3Config; this.viewMapper = viewMapper; - this.minioClient = minioClient; this.queryMapper = queryMapper; this.tableService = tableService; + this.storageService = storageService; this.storeService = storeService; this.databaseService = databaseService; this.dataDbSidecarGateway = dataDbSidecarGateway; @@ -268,19 +254,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService /* upload from sidecar into blob storage */ dataDbSidecarGateway.exportFile(container.getSidecarHost(), container.getSidecarPort(), filename); /* export file from blob storage */ - try { - final InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(s3Config.getS3ExportBucket()).object(filename).build()); - log.debug("found object with key {} in bucket {}", filename, s3Config.getS3ExportBucket()); - return ExportResource.builder() - .resource(new InputStreamResource(stream)) - .filename(filename) - .build(); - } catch (ServerException | InsufficientDataException | ErrorResponseException | IOException | - NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | XmlParserException | - InternalException e) { - log.error("Failed to find object {} in bucket {}: {}", filename, s3Config.getS3ExportBucket(), e.getMessage()); - throw new FileStorageException("Failed to find object " + filename + " in bucket " + s3Config.getS3ExportBucket() + ": " + e.getMessage()); - } + return storageService.getResource(filename); } @Override diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java new file mode 100644 index 0000000000..41b9de1d44 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java @@ -0,0 +1,123 @@ +package at.tuwien.service.impl; + +import at.tuwien.ExportResource; +import at.tuwien.config.S3Config; +import at.tuwien.exception.FileStorageException; +import at.tuwien.service.StorageService; +import io.minio.*; +import io.minio.errors.*; +import io.minio.messages.DeleteError; +import io.minio.messages.DeleteObject; +import io.minio.messages.Item; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.LinkedList; +import java.util.List; + +@Log4j2 +@Service +public class SeaweedServiceImpl implements StorageService { + + private final S3Config s3Config; + private final MinioClient minioClient; + + @Autowired + public SeaweedServiceImpl(S3Config s3Config, MinioClient minioClient) { + this.s3Config = s3Config; + this.minioClient = minioClient; + } + + @Override + public InputStream getObject(String bucket, String key) throws FileStorageException { + try { + return minioClient.getObject(GetObjectArgs.builder() + .bucket(bucket) + .object(key) + .build()); + } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | + InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | + XmlParserException e) { + log.error("Failed to find object {} in bucket {}: {}", key, bucket, e.getMessage()); + throw new FileStorageException("Failed to find object " + key + " in bucket " + bucket + ": " + e.getMessage(), e); + } + } + + @Override + public byte[] getBytes(String key) throws FileStorageException { + return getBytes(s3Config.getS3ImportBucket(), key); + } + + @Override + public byte[] getBytes(String bucket, String key) throws FileStorageException { + try { + return getObject(bucket, key) + .readAllBytes(); + } catch (IOException e) { + log.error("Failed to read bytes from input stream: {}", e.getMessage()); + throw new FileStorageException("Failed to read bytes from input stream: " + e.getMessage(), e); + } + } + + @Override + public ExportResource getResource(String key) throws FileStorageException { + return getResource(s3Config.getS3ExportBucket(), key); + } + + @Override + public ExportResource getResource(String bucket, String key) throws FileStorageException { + final InputStream stream = getObject(bucket, key); + return ExportResource.builder() + .resource(new InputStreamResource(stream)) + .filename(key) + .build(); + } + + @Override + public void deleteStaleFiles(String bucketName) throws FileStorageException { + final List<Item> objects = new LinkedList<>(); + for (Result<Item> result : minioClient.listObjects(ListObjectsArgs.builder() + .bucket(bucketName) + .build())) { + try { + final Item item = result.get(); + final long diff = item.lastModified().toEpochSecond() - ZonedDateTime.now().minusSeconds(s3Config.getStaleSeconds()).toEpochSecond(); + if (diff <= 0) { + log.trace("file {} of bucket {} is due {} second(s)", item.objectName(), bucketName, diff * -1); + objects.add(item); + } else { + log.trace("file {} of bucket {} is not yet due for {} second(s)", item.objectName(), bucketName, diff); + } + } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | + InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | + XmlParserException e) { + log.error("Failed to retrieve file infos from bucket {}: {}", bucketName, e.getMessage()); + throw new FileStorageException("Failed to retrieve file infos from bucket " + bucketName + ": " + e.getMessage(), e); + } + } + log.debug("deleting files {}", objects.stream().map(Item::objectName).toList()); + final Iterable<Result<DeleteError>> response = minioClient.removeObjects(RemoveObjectsArgs.builder() + .bucket(bucketName) + .objects(objects.stream().map(o -> new DeleteObject(o.objectName())).toList()) + .build()); + for (Result<DeleteError> result : response) { + try { + result.get(); + } catch (ServerException | InsufficientDataException | ErrorResponseException | IOException | + NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | XmlParserException | + InternalException e) { + log.error("Failed to delete file from bucket {}: {}", bucketName, e.getMessage()); + throw new FileStorageException("Failed to delete file from bucket " + bucketName + ": " + e.getMessage(), e); + } + } + log.info("Deleted {} files", objects.size()); + } + +} diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java index 3342e7939d..67a2711a98 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java @@ -143,7 +143,7 @@ public abstract class BaseTest { public final static String[] DEFAULT_DATABASE_HANDLING = new String[]{"default-database-handling", "update-database-access", "modify-database-visibility", "create-database", "modify-database-owner", - "delete-database-access", "check-database-access", "list-databases", + "delete-database-access", "check-database-access", "list-databases", "modify-database-image", "create-database-access", "find-database", "import-database-data"}; public final static String[] ESCALATED_DATABASE_HANDLING = new String[]{"escalated-database-handling", diff --git a/dbrepo-ui/api/database.service.js b/dbrepo-ui/api/database.service.js index 9c4fb14f22..c112b6982f 100644 --- a/dbrepo-ui/api/database.service.js +++ b/dbrepo-ui/api/database.service.js @@ -99,6 +99,20 @@ class DatabaseService { }) } + modifyImage (databaseId, payload) { + return new Promise((resolve, reject) => { + api.put(`/api/database/${databaseId}/image`, payload, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + displayError(error, 'Failed to modify database visibility') + reject(error) + }) + }) + } + modifyOwner (databaseId, username) { return new Promise((resolve, reject) => { api.put(`/api/database/${databaseId}/transfer`, { username }, { headers: { Accept: 'application/json' } }) diff --git a/dbrepo-ui/pages/database/_database_id/info.vue b/dbrepo-ui/pages/database/_database_id/info.vue index bfacf06b0a..d2a619b876 100644 --- a/dbrepo-ui/pages/database/_database_id/info.vue +++ b/dbrepo-ui/pages/database/_database_id/info.vue @@ -16,6 +16,12 @@ <v-list dense> <v-list-item> <v-list-item-content> + <v-list-item-title v-if="databaseImage" class="mt-2"> + Database Image + </v-list-item-title> + <v-list-item-content v-if="databaseImage"> + <v-img :src="databaseImage" alt="database image" max-width="200" max-height="200" /> + </v-list-item-content> <v-list-item-title class="mt-2"> Database Name </v-list-item-title> @@ -237,6 +243,12 @@ export default { let sum = 0 this.database.tables.forEach((t) => { sum += t.data_length }) return sizeToHumanLabel(sum) + }, + databaseImage () { + if (!this.database || !this.database.image) { + return null + } + return `data:image/webp;base64,${this.database.image}` } }, methods: { diff --git a/dbrepo-ui/pages/database/_database_id/settings.vue b/dbrepo-ui/pages/database/_database_id/settings.vue index 8734d197c9..77c5cccb82 100644 --- a/dbrepo-ui/pages/database/_database_id/settings.vue +++ b/dbrepo-ui/pages/database/_database_id/settings.vue @@ -4,6 +4,48 @@ <v-progress-linear v-if="loading" /> <v-tabs-items v-model="tab"> <v-tab-item> + <v-card v-if="canModifyImage" flat tile> + <v-card-title>Image</v-card-title> + <v-card-text> + <v-row dense> + <v-col> + The image will be displayed in a box with maximum dimensions 200x200 pixels. + </v-col> + </v-row> + <v-row dense> + <v-col sm="6"> + <v-file-input + v-model="fileModel" + accept="image/*" + hint="max. 1MB file size" + persistent-hint + clearable + :loading="loadingUpload" + :show-size="1000" + counter + label="Teaser Image" + @change="uploadFile" /> + </v-col> + </v-row> + <v-btn + small + class="black--text mt-4" + :loading="loadingImage" + @click="updateDatabaseImage"> + Modify Image + </v-btn> + <v-btn + v-if="database.image" + small + color="warning" + class="black--text mt-4" + :loading="loadingDeleteImage" + @click="removeDatabaseImage"> + Remove Image + </v-btn> + </v-card-text> + </v-card> + <v-divider /> <v-card v-if="isOwner" flat tile> <v-card-title>Access</v-card-title> <v-data-table @@ -100,6 +142,7 @@ import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' import EditAccess from '@/components/dialogs/EditAccess.vue' import DatabaseService from '@/api/database.service' import UserService from '@/api/user.service' +import UploadService from '@/api/upload.service' export default { components: { @@ -114,6 +157,10 @@ export default { accessType: null, users: [], loading: false, + loadingUpload: false, + loadingImage: false, + loadingDeleteImage: false, + fileModel: null, loadingUsers: false, editAccessDialog: false, editVisibilityDialog: false, @@ -123,6 +170,9 @@ export default { modifyOwner: { id: null }, + modifyImage: { + key: null + }, visibility: [ { text: 'Public', value: true }, { text: 'Private', value: false } @@ -214,6 +264,12 @@ export default { return false } return this.roles.includes('create-database-access') + }, + canModifyImage () { + if (!this.isOwner) { + return false + } + return this.roles.includes('modify-database-image') } }, watch: { @@ -254,6 +310,50 @@ export default { this.loading = false }) }, + uploadFile () { + this.loadingUpload = true + UploadService.upload(this.fileModel) + .then((metadata) => { + console.debug('uploaded image', metadata) + this.modifyImage.key = metadata.s3key + this.loadingUpload = false + }) + .finally(() => { + this.loadingUpload = false + }) + }, + updateDatabaseImage () { + this.loadingImage = true + DatabaseService.modifyImage(this.$route.params.database_id, this.modifyImage) + .then(() => { + this.$toast.success('Updated image successfully') + this.$store.dispatch('reloadDatabase') + this.loadingImage = false + }) + .catch(() => { + this.$toast.error('Failed to modify image') + this.loadingImage = false + }) + .finally(() => { + this.loadingImage = false + }) + }, + removeDatabaseImage () { + this.loadingDeleteImage = true + DatabaseService.modifyImage(this.$route.params.database_id, { key: null }) + .then(() => { + this.$toast.success('Removed image successfully') + this.$store.dispatch('reloadDatabase') + this.loadingDeleteImage = false + }) + .catch(() => { + this.$toast.error('Failed to delete image') + this.loadingDeleteImage = false + }) + .finally(() => { + this.loadingDeleteImage = false + }) + }, updateDatabaseOwner () { this.loading = true DatabaseService.modifyOwner(this.$route.params.database_id, this.modifyOwner.username) diff --git a/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue index a074108ef3..bff92a1fb5 100644 --- a/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue +++ b/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue @@ -18,11 +18,11 @@ Table ID </v-list-item-title> <v-list-item-content v-if="table && table.id" v-text="table.id" /> - <v-list-item-title> + <v-list-item-title v-if="table && table.data_length"> Table Size </v-list-item-title> <v-list-item-content v-if="table && table.data_length" v-text="sizeToHumanLabel(table.data_length)" /> - <v-list-item-title> + <v-list-item-title v-if="table && table.num_rows"> Table Rows </v-list-item-title> <v-list-item-content v-if="table && table.num_rows" v-text="table.num_rows" /> diff --git a/docker-compose.yml b/docker-compose.yml index 9eeafdeeb6..1fb934e775 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,6 +160,7 @@ services: S3_SECRET_ACCESS_KEY: "${STORAGE_PASSWORD:-seaweedfsadmin}" S3_IMPORT_BUCKET: "${STORAGE_IMPORT_BUCKET:-dbrepo-upload}" S3_EXPORT_BUCKET: "${STORAGE_EXPORT_BUCKET:-dbrepo-download}" + DELETE_STALE_FILES_RATE: "${DELETE_STALE_FILES_RATE:-60}" MIRROR_RATE: ${METADATA_SERVICE_MIRROR_RATE:-60} OBTAIN_METADATA_RATE: ${METADATA_SERVICE_OBTAIN_METADATA_RATE:-60} DELETE_STALE_QUERIES_RATE: ${METADATA_SERVICE_DELETE_STALE_QUERIES_RATE:-60} -- GitLab