diff --git a/NEWS.md b/NEWS.md index 169645b9..5f63434b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +## 2024-04-16 v3.2.1 + +[Full Changelog](https://github.com/folio-org/mod-data-export-spring/compare/v3.2.0...v3.2.1) + +### Bug fixes +* [MODEXPS-261](https://folio-org.atlassian.net/browse/MODEXPS-261) "Request has expired" or "Expired token" errors + ## 2024-03-19 v3.2.0 [Full Changelog](https://github.com/folio-org/mod-data-export-spring/compare/v3.1.0...v3.2.0) diff --git a/pom.xml b/pom.xml index b50c102a..a41108d4 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.folio mod-data-export-spring Data Export Spring module - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT jar diff --git a/src/main/java/org/folio/des/controller/JobsController.java b/src/main/java/org/folio/des/controller/JobsController.java index c70e5789..de7e22c3 100644 --- a/src/main/java/org/folio/des/controller/JobsController.java +++ b/src/main/java/org/folio/des/controller/JobsController.java @@ -66,9 +66,9 @@ public ResponseEntity resendExportedFile(UUID jobId) { } @Override - public ResponseEntity downloadExportedFileByJobId(UUID id) { - log.info("downloadExportedFileByJobId:: with id={}.", id); - return ResponseEntity.ok(new InputStreamResource(service.downloadExportedFile(id))); + public ResponseEntity downloadExportedFileByJobId(UUID id, String key) { + log.info("downloadExportedFileByJobId:: with id={}, key={}.", id, key); + return ResponseEntity.ok(new InputStreamResource(service.downloadExportedFile(id, key))); } @Override diff --git a/src/main/java/org/folio/des/service/JobService.java b/src/main/java/org/folio/des/service/JobService.java index 8759d8ba..29330e32 100644 --- a/src/main/java/org/folio/des/service/JobService.java +++ b/src/main/java/org/folio/des/service/JobService.java @@ -59,8 +59,9 @@ public interface JobService { /** * Downloading the exported file. A job can have only one exported file. * @param jobId the job id + * @param key the key of the file in the storage * @return Input stream of exported file. */ - InputStream downloadExportedFile(UUID jobId); + InputStream downloadExportedFile(UUID jobId, String key); } diff --git a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java index 4fab1cd8..19642b5c 100644 --- a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java +++ b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java @@ -1,5 +1,6 @@ package org.folio.des.service.impl; +import static java.util.Objects.nonNull; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_IDENTIFIERS; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_QUERY; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_UPDATE; @@ -247,7 +248,7 @@ public void deleteJobs(List jobs) { } @Override - public InputStream downloadExportedFile(UUID jobId) { + public InputStream downloadExportedFile(UUID jobId, String key) { log.debug("downloadExportedFile:: download exported files for jobId={}.", jobId); Job job = getJobEntity(jobId); if (CollectionUtils.isEmpty(job.getFileNames())) { @@ -256,14 +257,14 @@ public InputStream downloadExportedFile(UUID jobId) { } log.debug("Refreshing download url for jobId: {}", job.getId()); try { - PresignedUrl presignedUrl = exportWorkerClient.getRefreshedPresignedUrl(job.getFiles().get(0)); + PresignedUrl presignedUrl = getPresignedUrl(job, key); URL url = new URL(presignedUrl.getUrl()); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(CONNECTION_TIMEOUT); return conn.getInputStream(); } catch (Exception e) { - log.error("Error downloading a file: {} for jobId: {}", e.getMessage(), job.getId()); + log.error("Error downloading a file: {} for jobId: {} and key: {}", e.getMessage(), job.getId(), key); throw new FileDownloadException(String.format("Error downloading a file: %s", e)); } } @@ -311,4 +312,11 @@ private String getUserName(FolioExecutionContext context) { Optional userInfo = StringUtils.isBlank(jwt) ? Optional.empty() : JWTokenUtils.parseToken(jwt); return StringUtils.substring(userInfo.map(JWTokenUtils.UserInfo::getUserName).orElse(null), 0, 50); } + + private PresignedUrl getPresignedUrl(Job job, String key) { + if (nonNull(key)) { + return exportWorkerClient.getRefreshedPresignedUrl(key); + } + return exportWorkerClient.getRefreshedPresignedUrl(job.getFiles().get(0)); + } } diff --git a/src/main/resources/swagger.api/jobs.yaml b/src/main/resources/swagger.api/jobs.yaml index 280b70b9..d6479e34 100644 --- a/src/main/resources/swagger.api/jobs.yaml +++ b/src/main/resources/swagger.api/jobs.yaml @@ -177,6 +177,12 @@ paths: description: UUID of the job schema: $ref: "#/components/schemas/UUID" + - name: key + in: query + required: false + description: Key of the file in storage to be downloaded + schema: + type: string responses: "200": description: Export file successfully retrieved diff --git a/src/test/java/org/folio/des/controller/JobsControllerTest.java b/src/test/java/org/folio/des/controller/JobsControllerTest.java index 4fa54e2a..3e13db7b 100644 --- a/src/test/java/org/folio/des/controller/JobsControllerTest.java +++ b/src/test/java/org/folio/des/controller/JobsControllerTest.java @@ -99,8 +99,8 @@ void getJobs() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(8)), - jsonPath("$.jobRecords", hasSize(8)))); + jsonPath("$.totalRecords", is(9)), + jsonPath("$.jobRecords", hasSize(9)))); } @Test @@ -115,7 +115,7 @@ void findSortedJobs() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(8)), + jsonPath("$.totalRecords", is(9)), jsonPath("$.jobRecords", hasSize(3)))); } @@ -131,7 +131,7 @@ void findSortedJobsByExportMethodName() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(8)), + jsonPath("$.totalRecords", is(9)), jsonPath("$.jobRecords", hasSize(3)))); } @@ -177,8 +177,8 @@ void excludeJobById() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(7)), - jsonPath("$.jobRecords", hasSize(7)))); + jsonPath("$.totalRecords", is(8)), + jsonPath("$.jobRecords", hasSize(8)))); } @Test @@ -193,8 +193,8 @@ void findJobsBySourceOrDesc() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(7)), - jsonPath("$.jobRecords", hasSize(7)))); + jsonPath("$.totalRecords", is(8)), + jsonPath("$.jobRecords", hasSize(8)))); } @Test @@ -239,8 +239,8 @@ void findJobsByQuery() throws Exception { matchAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.totalRecords", is(3)), - jsonPath("$.jobRecords", hasSize(3)))); + jsonPath("$.totalRecords", is(4)), + jsonPath("$.jobRecords", hasSize(4)))); } @Test @@ -289,6 +289,23 @@ void shouldFailedDownloadWithBadRequest() throws Exception { status().is5xxServerError())); } + @Test + @DisplayName("Should succeed download file with key to storage") + void shouldSucceedDownloadWithKey() throws Exception { + PresignedUrl presignedUrl = new PresignedUrl(); + presignedUrl.setUrl("http://localhost:" + WIRE_MOCK_PORT + "/TestFile.csv"); + when(exportWorkerClient.getRefreshedPresignedUrl(anyString())).thenReturn(presignedUrl); + mockMvc + .perform( + get("/data-export-spring/jobs/22ae5d0f-6425-82a1-a361-1bc9b88e8172/download?key=TestFile.csv") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .headers(defaultHeaders())) + .andExpect( + matchAll( + status().is2xxSuccessful(), + content().bytes("Test file content".getBytes()))); + } + @Test @DisplayName("Can not fetch job with wrong id") void notFoundJob() throws Exception { diff --git a/src/test/resources/job.sql b/src/test/resources/job.sql index b7069048..30c8d156 100644 --- a/src/test/resources/job.sql +++ b/src/test/resources/job.sql @@ -48,6 +48,20 @@ VALUES ('42ae5d0f-6425-82a1-a361-1bc9b88e8172', '000101', 'test-desc', 'data-exp 'data-export-system-user', 'Fees & Fines Bursar Report', null, 'COMPLETED', '{ "exitCode": "COMPLETED" }'); +INSERT INTO diku_mod_data_export_spring.job (id, name, description, source, is_system_source, type, + export_type_specific_parameters, status, files, file_names, + start_time, end_time, created_date, created_by_user_id, + created_by_username, updated_date, updated_by_user_id, + updated_by_username, output_format, error_details, + batch_status, exit_status) +VALUES ('22ae5d0f-6425-82a1-a361-1bc9b88e8172', '000102', 'test-desc', 'data-export-system-user', true, + 'BULK_EDIT_IDENTIFIERS', + '{}', + 'SUCCESSFUL', '["http://localhost/test-url/"]', '["TestFile.csv"]', '2021-03-16 09:29:54.250000', '2021-03-16 09:30:09.831000', '2021-03-16 09:29:54.170000', + null, 'data-export-system-user', '2021-03-16 09:30:09.968000', null, + 'data-export-system-user', 'Bulk Edit report', null, 'COMPLETED', '{ + "exitCode": "COMPLETED" + }'); INSERT INTO diku_mod_data_export_spring.job (id, name, description, source, is_system_source, type, export_type_specific_parameters, status, files, start_time, end_time, created_date, created_by_user_id, diff --git a/src/test/resources/mappings/files.json b/src/test/resources/mappings/files.json new file mode 100644 index 00000000..49a83606 --- /dev/null +++ b/src/test/resources/mappings/files.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "url": "/TestFile.csv" + }, + "response": { + "status": 200, + "body": "Test file content", + "headers": { + "Content-Type": "text/plain" + } + } + } + ] +}