From 3906df18d3dbf0451a2bc1bded1732d3951411e5 Mon Sep 17 00:00:00 2001 From: sharonl <6546457+sharonluong@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:59:28 -0400 Subject: [PATCH 01/17] Bxc 4673 zip file controller (#1786) * BXC-4673 start bulk download controller * BXC-4673 returning a file in the controller and a test * BXC-4673 DRY up to share logic between download controller tests * BXC-4673 finish bulk download controller test * BXC-4673 cleanup * BXC-4673 update directory name * BXC-4673 whoops the __files naming was necessary: * BXC-4673 adding object type mismatch handling --- .../impl/download/DownloadBulkRequest.java | 16 +- .../impl/download/DownloadBulkService.java | 3 +- .../download/DownloadBulkServiceTest.java | 3 +- .../services/rest/DownloadBulkController.java | 53 +++++++ .../RestResponseEntityExceptionHandler.java | 7 + .../main/webapp/WEB-INF/service-context.xml | 6 + .../rest/DownloadBulkControllerIT.java | 141 ++++++++++++++++++ .../rest/DownloadImageControllerIT.java | 19 +-- .../services/utils/DownloadTestHelper.java | 19 +++ 9 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkController.java create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkControllerIT.java create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/utils/DownloadTestHelper.java diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkRequest.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkRequest.java index ef17fc35ac..2de499b396 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkRequest.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkRequest.java @@ -1,17 +1,17 @@ package edu.unc.lib.boxc.operations.impl.download; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; public class DownloadBulkRequest { private String workPidString; - @JsonDeserialize(as = AgentPrincipalsImpl.class) - private AgentPrincipals agent; + private AccessGroupSet principals; - public DownloadBulkRequest(String workPidString, AgentPrincipals agent) { + public DownloadBulkRequest(String workPidString, AccessGroupSet principals) { this.workPidString = workPidString; - this.agent = agent; + this.principals = principals; } public String getWorkPidString() { @@ -22,11 +22,11 @@ public void setWorkPidString(String workPidString) { this.workPidString = workPidString; } - public AgentPrincipals getAgent() { - return agent; + public AccessGroupSet getPrincipals() { + return principals; } - public void setAgent(AgentPrincipals agent) { - this.agent = agent; + public void setPrincipals(AccessGroupSet principals) { + this.principals = principals; } } diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java index df47e33b32..d317d5314d 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java @@ -4,7 +4,6 @@ import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.services.AccessControlService; import edu.unc.lib.boxc.fcrepo.exceptions.ServiceException; -import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.objects.ContentObject; import edu.unc.lib.boxc.model.api.objects.FileObject; import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; @@ -33,7 +32,7 @@ public class DownloadBulkService { public Path downloadBulk(DownloadBulkRequest request) { var pidString = request.getWorkPidString(); var workPid = PIDs.get(pidString); - var agentPrincipals = request.getAgent().getPrincipals(); + var agentPrincipals = request.getPrincipals(); aclService.assertHasAccess( "User does not have permissions to view the Work for download", workPid, agentPrincipals, Permission.viewOriginal); diff --git a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java index 23b6a024a0..db0d19e3d0 100644 --- a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java +++ b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java @@ -5,7 +5,6 @@ import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; import edu.unc.lib.boxc.auth.api.services.AccessControlService; -import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.exceptions.ObjectTypeMismatchException; import edu.unc.lib.boxc.model.api.ids.PID; import edu.unc.lib.boxc.model.api.objects.BinaryObject; @@ -78,7 +77,7 @@ public void init() throws Exception { parentPid = PIDs.get(PARENT_UUID); fileObject1Pid = PIDs.get(CHILD1_UUID); fileObject2Pid = PIDs.get(CHILD2_UUID); - request = new DownloadBulkRequest(PARENT_UUID, mockAgent); + request = new DownloadBulkRequest(PARENT_UUID, mockAccessSet); when(mockAgent.getUsername()).thenReturn("user"); when(mockAgent.getPrincipals()).thenReturn(mockAccessSet); diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkController.java new file mode 100644 index 0000000000..27ec7d2fe9 --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkController.java @@ -0,0 +1,53 @@ +package edu.unc.lib.boxc.web.services.rest; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.operations.impl.download.DownloadBulkRequest; +import edu.unc.lib.boxc.operations.impl.download.DownloadBulkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import static edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore.getAgentPrincipals; +import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; + +/** + * Controller for downloading FileObject original files from a WorkObject into a zip file + */ +@Controller +public class DownloadBulkController { + @Autowired + private AccessControlService aclService; + @Autowired + private DownloadBulkService downloadBulkService; + + @RequestMapping("/bulkDownload/{id}") + @ResponseBody + public ResponseEntity getZip(@PathVariable("id") String pidString) { + PID pid = PIDs.get(pidString); + + AccessGroupSet principals = getAgentPrincipals().getPrincipals(); + aclService.assertHasAccess("Insufficient permissions to bulk download for " + pidString, + pid, principals, Permission.viewOriginal); + var request = new DownloadBulkRequest(pidString, principals); + var path = downloadBulkService.downloadBulk(request); + var filename = DownloadBulkService.getZipFilename(pidString); + + return ResponseEntity.ok() + .header(CONTENT_DISPOSITION,"attachment;filename=\"" + filename + "\"") + .contentType(MediaType.valueOf("application/zip")) + .body(new FileSystemResource(path)); + } + + public void setDownloadBulkService(DownloadBulkService downloadBulkService) { + this.downloadBulkService = downloadBulkService; + } +} diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java index ed9f39ab6e..2934e156da 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java @@ -3,6 +3,7 @@ import edu.unc.lib.boxc.model.api.exceptions.InvalidOperationForObjectType; import java.io.EOFException; +import edu.unc.lib.boxc.model.api.exceptions.ObjectTypeMismatchException; import edu.unc.lib.boxc.search.api.exceptions.SolrRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,6 +90,12 @@ protected ResponseEntity handleUnavailable(Exception ex, WebRequest requ } } + @ExceptionHandler(value = { ObjectTypeMismatchException.class }) + protected ResponseEntity handleObjectTypeMismatch(RuntimeException ex, WebRequest request) { + String bodyOfResponse = "Object type mismatch"; + return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request); + } + @ExceptionHandler(value = { Exception.class }) protected ResponseEntity handleUncaught(Exception ex, WebRequest request) { try { diff --git a/web-services-app/src/main/webapp/WEB-INF/service-context.xml b/web-services-app/src/main/webapp/WEB-INF/service-context.xml index e810b7bb02..77831c8649 100644 --- a/web-services-app/src/main/webapp/WEB-INF/service-context.xml +++ b/web-services-app/src/main/webapp/WEB-INF/service-context.xml @@ -492,6 +492,12 @@ + + + + + + diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkControllerIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkControllerIT.java new file mode 100644 index 0000000000..19f149f9fd --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadBulkControllerIT.java @@ -0,0 +1,141 @@ +package edu.unc.lib.boxc.web.services.rest; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.api.objects.BinaryObject; +import edu.unc.lib.boxc.model.api.objects.FileObject; +import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.api.objects.WorkObject; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.model.fcrepo.test.TestHelper; +import edu.unc.lib.boxc.operations.impl.download.DownloadBulkService; +import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; +import edu.unc.lib.boxc.web.services.utils.DownloadTestHelper; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static edu.unc.lib.boxc.auth.api.Permission.viewOriginal; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class DownloadBulkControllerIT { + private static final String WORK_ID = "f277bb38-272c-471c-a28a-9887a1328a1f"; + private static final String FILE_ID = "83c2d7f8-2e6b-4f0b-ab7e-7397969c0682"; + private static final PID FILE_PID = PIDs.get(FILE_ID); + private final static String USERNAME = "test_user"; + private final static AccessGroupSet GROUPS = new AccessGroupSetImpl("adminGroup"); + @Mock + private AccessControlService aclService; + @Mock + private RepositoryObjectLoader repositoryObjectLoader; + @Mock + private WorkObject workObject; + @Mock + private FileObject fileObject; + @TempDir + public Path tmpFolder; + @InjectMocks + private DownloadBulkController controller; + private MockMvc mvc; + private FileInputStream fileInputStream; + private AutoCloseable closeable; + + @BeforeEach + public void init() throws FileNotFoundException { + closeable = openMocks(this); + DownloadBulkService downloadBulkService = new DownloadBulkService(); + downloadBulkService.setAclService(aclService); + downloadBulkService.setBasePath(tmpFolder); + downloadBulkService.setRepoObjLoader(repositoryObjectLoader); + controller.setDownloadBulkService(downloadBulkService); + fileInputStream = new FileInputStream("src/test/resources/__files/bunny.jpg"); + mvc = MockMvcBuilders.standaloneSetup(controller) + .setControllerAdvice(new RestResponseEntityExceptionHandler()) + .build(); + TestHelper.setContentBase("http://localhost:48085/rest"); + GroupsThreadStore.storeUsername(USERNAME); + GroupsThreadStore.storeGroups(GROUPS); + } + + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + + @Test + public void noAccessTest() throws Exception { + doThrow(new AccessRestrictionException()).when(aclService).assertHasAccess( + anyString(), any(), any(AccessGroupSetImpl.class), eq(viewOriginal)); + + mvc.perform(get("/bulkDownload/" + WORK_ID)) + .andExpect(status().isForbidden()) + .andReturn(); + } + + @Test + public void successTest() throws Exception { + when(repositoryObjectLoader.getWorkObject(any())).thenReturn(workObject); + when(workObject.getMembers()).thenReturn(List.of(fileObject)); + when(fileObject.getPid()).thenReturn(FILE_PID); + var binObj = mock(BinaryObject.class); + when(fileObject.getOriginalFile()).thenReturn(binObj); + when(binObj.getBinaryStream()).thenReturn(fileInputStream); + when(binObj.getFilename()).thenReturn("bunny.jpg"); + when(aclService.hasAccess(eq(FILE_PID), any(), + eq(Permission.viewOriginal))).thenReturn(true); + + var result = mvc.perform(get("/bulkDownload/" + WORK_ID)) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + var response = result.getResponse(); + var bytes = response.getContentAsByteArray(); + var zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes)); + + assertZipFiles(zipInputStream, List.of("bunny.jpg")); + assertEquals("application/zip", response.getHeader("Content-Type")); + } + + private void assertZipFiles(ZipInputStream stream, List filenames) throws IOException { + var actualFilenames = new ArrayList(); + try (stream) { + ZipEntry entry; + while ((entry = stream.getNextEntry()) != null) { + actualFilenames.add(entry.getName()); + var actualContent = IOUtils.toByteArray(stream); + DownloadTestHelper.assertCorrectImageReturned(actualContent); + } + } + assertEquals(filenames, actualFilenames); + } +} diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java index 6e9c75f987..86d1763da8 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java @@ -18,6 +18,7 @@ import edu.unc.lib.boxc.web.services.processing.DownloadImageService; import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; import edu.unc.lib.boxc.operations.api.images.ImageServerUtil; +import edu.unc.lib.boxc.web.services.utils.DownloadTestHelper; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -149,7 +150,8 @@ public void testGetImageAtPixelSizeSmallerThanFull() throws Exception { var response = result.getResponse(); assertEquals("attachment; filename=bunny_800px.jpg", response.getHeader(CONTENT_DISPOSITION)); - assertCorrectImageReturned(response); + assertEquals("image/jpeg", response.getContentType()); + DownloadTestHelper.assertCorrectImageReturned(response.getContentAsByteArray()); } @Test @@ -182,7 +184,8 @@ public void testGetImageAtPixelSizeBiggerThanFull() throws Exception { var response = result.getResponse(); assertEquals("attachment; filename=bunny_max.jpg", response.getHeader(CONTENT_DISPOSITION)); - assertCorrectImageReturned(response); + assertEquals("image/jpeg", response.getContentType()); + DownloadTestHelper.assertCorrectImageReturned(response.getContentAsByteArray()); } @Test @@ -255,7 +258,8 @@ public void testGetImageFromJp2AtPixelSizeBiggerThanFull() throws Exception { var response = result.getResponse(); assertEquals("attachment; filename=bunny_max.jpg", response.getHeader(CONTENT_DISPOSITION)); - assertCorrectImageReturned(response); + assertEquals("image/jpeg", response.getContentType()); + DownloadTestHelper.assertCorrectImageReturned(response.getContentAsByteArray()); } @Test @@ -390,13 +394,4 @@ public void testGetAccessImageSolrUnavailable() throws Exception { .andExpect(status().isServiceUnavailable()) .andReturn(); } - - private void assertCorrectImageReturned(MockHttpServletResponse response) throws IOException { - assertEquals("image/jpeg", response.getContentType()); - - var responseContent = response.getContentAsByteArray(); - byte[] imageContent = FileUtils.readFileToByteArray(new File("src/test/resources/__files/bunny.jpg")); - - assertArrayEquals(responseContent, imageContent); - } } diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/utils/DownloadTestHelper.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/utils/DownloadTestHelper.java new file mode 100644 index 0000000000..279ce1a658 --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/utils/DownloadTestHelper.java @@ -0,0 +1,19 @@ +package edu.unc.lib.boxc.web.services.utils; + +import org.apache.commons.io.FileUtils; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class DownloadTestHelper { + public static void assertCorrectImageReturned(byte[] actualContent) throws IOException { + byte[] imageContent = FileUtils.readFileToByteArray(new File("src/test/resources/__files/bunny.jpg")); + + assertArrayEquals(imageContent, actualContent); + } + + +} From 1b6d998f27e36395fb86d94f4391bba0f084e026 Mon Sep 17 00:00:00 2001 From: sharonl <6546457+sharonluong@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:15:12 -0400 Subject: [PATCH 02/17] BXC-4684 add download zip button (#1788) * BXC-4684 add download zip button * BXC-4684 add tests --- .../components/full_record/restrictedContent.vue | 3 +++ .../js/vue-cdr-access/src/mixins/fullRecordUtils.js | 4 ++++ static/js/vue-cdr-access/src/translations.js | 1 + .../tests/unit/restrictedContent.spec.js | 13 +++++++++++++ 4 files changed, 21 insertions(+) diff --git a/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue b/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue index c0f2b5f018..cc6042a28b 100644 --- a/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue +++ b/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue @@ -9,6 +9,9 @@ + diff --git a/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js b/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js index 91289a0623..ea0d875311 100644 --- a/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js +++ b/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js @@ -95,6 +95,10 @@ export default { return `https://${window.location.host}/admin/describe/${id}`; }, + downloadBulkUrl(id) { + return `https://${window.location.host}/services/api/bulkDownload/${id}`; + }, + hasMoreExhibits(current_exhibit, exhibits) { return current_exhibit < Object.keys(exhibits).length - 1; } diff --git a/static/js/vue-cdr-access/src/translations.js b/static/js/vue-cdr-access/src/translations.js index 7250ac6c63..f5a39e578f 100644 --- a/static/js/vue-cdr-access/src/translations.js +++ b/static/js/vue-cdr-access/src/translations.js @@ -40,6 +40,7 @@ export default { full_record: { additional_metadata: "View Additional Metadata", available_date: "Available after {available_date}", + bulk_download: "Download ZIP file", collection: "Collection", contains: "Contains", copied_link: "Download URL, {text}, copied to clipboard", diff --git a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js index e133c268e1..884f967810 100644 --- a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js +++ b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js @@ -167,6 +167,19 @@ describe('restrictedContent.vue', () => { expect(wrapper.find('a.edit').exists()).toBe(false); }); + it('shows a bulk download option if user has viewOriginal permissions', async () => { + logUserIn(); + await setRecordPermissions(record, ['viewMetadata', 'viewAccessCopies', 'viewReducedResImages', + 'viewOriginal', 'viewHidden', 'editDescription']); + expect(wrapper.find('a.bulk-download').exists()).toBe(true); + }); + + it('does not show a bulk download option if user does not have viewOriginal permissions', async () => { + await setRecordPermissions(record, []); + + expect(wrapper.find('a.bulk-download').exists()).toBe(false); + }); + it('does not show embargo info if there is no dataFileUrl', async () => { const updated_data = cloneDeep(record); updated_data.dataFileUrl = ""; From e63dbefa266336ea3447b38f1037af9984d7b788 Mon Sep 17 00:00:00 2001 From: sharonl <6546457+sharonluong@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:28:24 -0400 Subject: [PATCH 03/17] BXC-4736 handle non file member objects (#1819) --- .../impl/download/DownloadBulkService.java | 3 +++ .../impl/download/DownloadBulkServiceTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java index d317d5314d..bbb081e99d 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkService.java @@ -61,6 +61,9 @@ private void zipFiles(WorkObject workObject, AccessGroupSet agentPrincipals, Pat Map duplicates = new HashMap<>(); for (ContentObject memberObject : memberObjects ) { + if (!(memberObject instanceof FileObject)) { + continue; + } var fileObject = (FileObject) memberObject; if (aclService.hasAccess(memberObject.getPid(), agentPrincipals, Permission.viewOriginal)) { var binObj = fileObject.getOriginalFile(); diff --git a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java index db0d19e3d0..ffea60c4b7 100644 --- a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java +++ b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/download/DownloadBulkServiceTest.java @@ -10,6 +10,7 @@ import edu.unc.lib.boxc.model.api.objects.BinaryObject; import edu.unc.lib.boxc.model.api.objects.FileObject; import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.api.objects.Tombstone; import edu.unc.lib.boxc.model.api.objects.WorkObject; import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; import org.apache.commons.io.IOUtils; @@ -63,6 +64,8 @@ public class DownloadBulkServiceTest { private WorkObject parentWork; @Mock private FileObject fileObject1, fileObject2; + @Mock + private Tombstone tombstone; @TempDir public Path zipStorageBasePath; @@ -153,6 +156,15 @@ public void noOriginalFilesTest() throws IOException { assertZipFiles(List.of(), List.of()); } + @Test + public void tombstoneTest() throws IOException { + when(repoObjLoader.getWorkObject(eq(parentPid))).thenReturn(parentWork); + when(parentWork.getMembers()).thenReturn(List.of(tombstone)); + service.downloadBulk(request); + // the zip file should be empty + assertZipFiles(List.of(), List.of()); + } + @Test public void successTest() throws IOException { when(repoObjLoader.getWorkObject(eq(parentPid))).thenReturn(parentWork); From c6410d7a1d241b239f541ba6ee490fb8202e311b Mon Sep 17 00:00:00 2001 From: Dean Farrell Date: Mon, 21 Oct 2024 11:53:24 -0400 Subject: [PATCH 04/17] Move zip download button (#1816) * Move bulk download above the file list * Bump bulma version * Use bulma without a dark mode * Align search box with zip download button * Use bulma's button layout * Fix issues with hompage hover links * Change button text, per RCMT --- static/css/sass/cdr_ui_styles.scss | 22 +++++++--- static/js/vue-cdr-access/index.html | 2 +- .../full_record/aggregateRecord.vue | 1 + .../src/components/full_record/fileList.vue | 40 +++++++++++++++++-- .../full_record/restrictedContent.vue | 3 -- static/js/vue-cdr-access/src/translations.js | 2 +- .../tests/unit/fileList.spec.js | 13 +++++- .../tests/unit/restrictedContent.spec.js | 7 ---- static/service-unavailable.html | 2 +- .../webapp/WEB-INF/html/headElements.html | 2 +- 10 files changed, 70 insertions(+), 24 deletions(-) diff --git a/static/css/sass/cdr_ui_styles.scss b/static/css/sass/cdr_ui_styles.scss index 37b01f44bf..2e9e47fd39 100644 --- a/static/css/sass/cdr_ui_styles.scss +++ b/static/css/sass/cdr_ui_styles.scss @@ -420,11 +420,6 @@ p.no-search-results { #child-files_filter { margin-bottom: 25px; - - label { - float: right; - width: 250px; - } } .input::placeholder, .input { @@ -719,7 +714,7 @@ table.dataTable { .actionlink { a.action { font-size: 16px; - padding: 15px 15px 30px 15px; + padding: 15px; } } } @@ -936,6 +931,21 @@ table.dataTable { display: inline-flex; } +.button.is-link.is-hovered, +.button.is-link:hover { + background-color: #276cda; + border-color: transparent; + color: #fff; +} + +.navbar-link.is-active, +.navbar-link:hover, +a.navbar-item.is-active, +a.navbar-item:hover { + background-color: #fafafa; + color: #3273dc; +} + /*** End of Bulma overrides ***/ /* Following Bulma breakpoints */ diff --git a/static/js/vue-cdr-access/index.html b/static/js/vue-cdr-access/index.html index 6cdd5eeac7..85d9821af3 100644 --- a/static/js/vue-cdr-access/index.html +++ b/static/js/vue-cdr-access/index.html @@ -2,7 +2,7 @@ - + diff --git a/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue b/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue index 75154fda89..d042c3d52f 100644 --- a/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue +++ b/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue @@ -73,6 +73,7 @@ :child-count="childCount" :work-id="recordData.briefObject.id" :download-access="hasDownloadAccess(recordData)" + :view-original-access="hasPermission(recordData, 'viewOriginal')" :edit-access="hasPermission(recordData,'editDescription')">