Skip to content

Commit

Permalink
Merge pull request #1794 from UNC-Libraries/bxc-4709
Browse files Browse the repository at this point in the history
BXC-4709 - Handle range end >= file size
  • Loading branch information
sharonluong committed Sep 9, 2024
2 parents f66fb33 + 4b5c529 commit 87f58e1
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ public void testGetDatastreamWithRange() throws Exception {
assertEquals("inline; filename=\"file.txt\"", response.getHeader(CONTENT_DISPOSITION));
}

@Test
public void testGetDatastreamWithRangeEndingSameAsSize() throws Exception {
PID filePid = makePid();

FileObject fileObj = repositoryObjectFactory.createFileObject(filePid, null);
fileObj.addOriginalFile(makeContentUri(originalPid(fileObj), BINARY_CONTENT), "file.txt", "text/plain", null, null);

MvcResult result = mvc.perform(get("/content/" + filePid.getId())
.header(RANGE,"bytes=0-14"))
.andExpect(status().is2xxSuccessful())
.andReturn();

// Verify content was retrieved
MockHttpServletResponse response = result.getResponse();
assertEquals(BINARY_CONTENT, response.getContentAsString());

assertEquals(BINARY_CONTENT.length(), response.getContentAsString().length());
assertEquals("text/plain", response.getContentType());
assertEquals("inline; filename=\"file.txt\"", response.getHeader(CONTENT_DISPOSITION));
}

@Test
public void testGetDatastreamDownload() throws Exception {
PID filePid = makePid();
Expand Down Expand Up @@ -284,7 +305,7 @@ public void testRangeExceedsFileLength() throws Exception {
fileObj.addOriginalFile(makeContentUri(originalPid(fileObj), BINARY_CONTENT), null, "text/plain", null, null);

mvc.perform(get("/indexablecontent/" + filePid.getId())
.header(RANGE,"bytes=0-900000"))
.header(RANGE,"bytes=900000-900000"))
.andExpect(status().isRequestedRangeNotSatisfiable())
.andReturn();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public void streamData(PID pid, String dsName, boolean asAttachment,
binObj = repositoryObjectLoader.getBinaryObject(dsPid);
}

// Make sure the range is valid or will produce a reasonable response from fedora
range = correctRangeValue(range, binObj);

response.setHeader(CONTENT_TYPE, binObj.getMimetype());
String binaryName = binObj.getFilename();
String filename = binaryName == null ? pid.getId() : binaryName;
Expand All @@ -116,6 +119,25 @@ public void streamData(PID pid, String dsName, boolean asAttachment,
}
}

private String correctRangeValue(String range, BinaryObject binObj) {
if (range == null) {
return null;
}
var rangeParts = range.split("-");
if (rangeParts.length == 2) {
var endingRange = rangeParts[1];
try {
var endingValue = Long.parseLong(endingRange);
if (endingValue >= binObj.getFilesize()) {
return rangeParts[0] + "-";
}
} catch (NumberFormatException e) {
LOG.debug("Invalid range ending provided: {}", e.getMessage());
}
}
return range;
}

public void streamEventLog(PID pid, AccessGroupSet principals, boolean asAttachment,
HttpServletResponse response) throws IOException {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package edu.unc.lib.boxc.web.common.services;

import edu.unc.lib.boxc.auth.api.services.AccessControlService;
import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl;
import edu.unc.lib.boxc.fcrepo.exceptions.RangeNotSatisfiableException;
import edu.unc.lib.boxc.model.api.DatastreamType;
import edu.unc.lib.boxc.model.api.event.PremisLog;
import edu.unc.lib.boxc.model.api.exceptions.NotFoundException;
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.rdf.DcElements;
import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids;
import org.apache.jena.rdf.model.ModelFactory;
import org.fcrepo.client.FcrepoClient;
import org.fcrepo.client.FcrepoOperationFailedException;
import org.fcrepo.client.FcrepoResponse;
Expand All @@ -19,19 +27,22 @@
import javax.servlet.http.HttpServletResponse;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE;
import static edu.unc.lib.boxc.model.fcrepo.test.TestHelper.makePid;
import static org.apache.http.HttpHeaders.CONTENT_LENGTH;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.apache.http.HttpHeaders.CONTENT_RANGE;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;
import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION;

public class FedoraContentServiceTest {
private AutoCloseable closeable;
Expand All @@ -54,14 +65,18 @@ public class FedoraContentServiceTest {
private FcrepoResponse fcrepoResponse;
@Mock
private ServletOutputStream outputStream;
@Mock
private PremisLog premisLog;

@BeforeEach
public void setup() {
public void setup() throws Exception {
closeable = openMocks(this);
fedoraContentService = new FedoraContentService();
fedoraContentService.setClient(fcrepoClient);
fedoraContentService.setAccessControlService(accessControlService);
fedoraContentService.setRepositoryObjectLoader(repositoryObjectLoader);
when(response.getOutputStream()).thenReturn(outputStream);
when(fileObject.getPremisLog()).thenReturn(premisLog);
}

@AfterEach
Expand Down Expand Up @@ -90,18 +105,151 @@ public void streamDataWithExternalDatastream() {
@Test
public void streamDataSuccess() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject);
when(fileObject.getOriginalFile()).thenReturn(binaryObject);
when(binaryObject.getFilename()).thenReturn("Best Name");
when(binaryObject.getPid()).thenReturn(pid);
when(fcrepoClient.get(any())).thenReturn(builder);
when(builder.perform()).thenReturn(fcrepoResponse);
mockWithOriginalFile(pid);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8)));
when(response.getOutputStream()).thenReturn(outputStream);
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5");

fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, null);

verify(response).setHeader(CONTENT_LENGTH, "5");
verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"Best Name\"");
}

@Test
public void streamDataSuccessAsAttachment() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
mockWithOriginalFile(pid);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8)));
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5");

fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), true, response, null);

verify(response).setHeader(CONTENT_LENGTH, "5");
verify(response).setHeader(CONTENT_DISPOSITION, "attachment; filename=\"Best Name\"");
}

@Test
public void streamDataSuccessOtherDatastream() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
var modsPid = DatastreamPids.getMdDescriptivePid(pid);
mockWithOriginalFile(pid);
var modsBinary = mock(BinaryObject.class);
when(modsBinary.getFilename()).thenReturn("mods.xml");
when(modsBinary.getPid()).thenReturn(modsPid);
when(modsBinary.getFilesize()).thenReturn(4L);
when(repositoryObjectLoader.getBinaryObject(eq(modsPid))).thenReturn(modsBinary);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("desc".getBytes(StandardCharsets.UTF_8)));
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4");

fedoraContentService.streamData(pid, DatastreamType.MD_DESCRIPTIVE.getId(), false, response, null);

verify(response).setHeader(CONTENT_LENGTH, "4");
verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"mods.xml\"");
}

@Test
public void streamDataWithValidRange() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
mockWithOriginalFile(pid);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("imag".getBytes(StandardCharsets.UTF_8)));
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4");
var contentRange = "bytes 0-3/5";
when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange);

fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=0-3");

verify(response).setHeader(CONTENT_LENGTH, "4");
verify(response).setHeader(CONTENT_RANGE, contentRange);
verify(builder).addHeader("Range", "bytes=0-3");
}

@Test
public void streamDataWithEndRangeSameAsSize() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
mockWithOriginalFile(pid);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8)));
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5");
var contentRange = "bytes 0-4/5";
when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange);

fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=0-5");

verify(response).setHeader(CONTENT_LENGTH, "5");
verify(response).setHeader(CONTENT_RANGE, contentRange);
verify(builder).addHeader("Range", "bytes=0-");
}

@Test
public void streamDataWithEndRangeGreaterThanSize() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
mockWithOriginalFile(pid);
when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("mage".getBytes(StandardCharsets.UTF_8)));
when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4");
var contentRange = "bytes 1-4/5";
when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange);

fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=1-8");

verify(response).setHeader(CONTENT_LENGTH, "4");
verify(response).setHeader(CONTENT_RANGE, contentRange);
verify(builder).addHeader("Range", "bytes=1-");
}

@Test
public void streamDataWithEndRangeInvalid() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
mockWithOriginalFile(pid);
when(builder.perform()).thenThrow(new FcrepoOperationFailedException(null, 416, "Bad Range"));
when(fcrepoResponse.getStatusCode()).thenReturn(416);

assertThrows(RangeNotSatisfiableException.class, () -> {
fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=1-bad");
});
}

@Test
public void streamEventLogTest() throws IOException, FcrepoOperationFailedException {
var pid = makePid();

when(repositoryObjectLoader.getRepositoryObject(eq(pid))).thenReturn(fileObject);
var model = ModelFactory.createDefaultModel();
when(premisLog.getEventsModel()).thenReturn(model);
var logResource = model.getResource(pid.getRepositoryPath());
logResource.addProperty(DcElements.title, "test title");

var principals = new AccessGroupSetImpl("group");

fedoraContentService.streamEventLog(pid, principals, false, response);

verify(response).setHeader(CONTENT_TYPE, "text/turtle");
verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"" + pid.getId() + "_event_log.ttl\"");
}

@Test
public void streamEventLogAttachmentTest() throws IOException, FcrepoOperationFailedException {
var pid = makePid();
when(repositoryObjectLoader.getRepositoryObject(eq(pid))).thenReturn(fileObject);

var model = ModelFactory.createDefaultModel();
when(premisLog.getEventsModel()).thenReturn(model);
var logResource = model.getResource(pid.getRepositoryPath());
logResource.addProperty(DcElements.title, "test title");

var principals = new AccessGroupSetImpl("group");

fedoraContentService.streamEventLog(pid, principals, true, response);

verify(response).setHeader(CONTENT_TYPE, "text/turtle");
verify(response).setHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + pid.getId() + "_event_log.ttl\"");
}

private void mockWithOriginalFile(PID pid) throws FcrepoOperationFailedException {
when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject);
when(fileObject.getOriginalFile()).thenReturn(binaryObject);
when(binaryObject.getFilename()).thenReturn("Best Name");
when(binaryObject.getPid()).thenReturn(pid);
when(binaryObject.getFilesize()).thenReturn(5L);
when(fcrepoClient.get(any())).thenReturn(builder);
when(builder.perform()).thenReturn(fcrepoResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ public void testRangeExceedsFileLength() throws Exception {
fileObj.addOriginalFile(contentPath.toUri(), "file.txt", "text/plain", null, null);

mvc.perform(get("/file/" + filePid.getId())
.header(RANGE,"bytes=0-900000"))
.header(RANGE,"bytes=900000-900001"))
.andExpect(status().isRequestedRangeNotSatisfiable())
.andReturn();
}
Expand Down

0 comments on commit 87f58e1

Please sign in to comment.