Skip to content

Commit

Permalink
Video streaming working
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosjepard committed Dec 3, 2024
1 parent 5dce0f5 commit a779d46
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package com.databasepreservation.common.api.common;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;

import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RequestNotValidException;

public interface ConsumesSkipableOutputStream extends ConsumesOutputStream {

void consumeOutputStream(OutputStream out, long from, long end);

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@

import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.roda.core.data.common.RodaConstants;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import com.databasepreservation.common.server.storage.BinaryConsumesOutputStream;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.core.StreamingOutput;

Expand Down Expand Up @@ -85,6 +90,38 @@ public static String getMediaType(final String acceptFormat, final String accept
return mediaType;
}

public static ResponseEntity<StreamingResponseBody> rangeResponse(HttpHeaders headers, BinaryConsumesOutputStream streamResponse) {

// BinaryConsumesOutputStream responseStream = outputStream ->
// streamResponse.getStream().consumeOutputStream(outputStream);
final HttpHeaders responseHeaders = new HttpHeaders();

HttpRange range = headers.getRange().get(0);
long start = range.getRangeStart(streamResponse.getSize());
long end = range.getRangeEnd(streamResponse.getSize());

String contentLength = String.valueOf((end - start) + 1);
responseHeaders.add(HttpHeaders.CONTENT_TYPE, streamResponse.getMediaType());
responseHeaders.add(HttpHeaders.CONTENT_LENGTH, contentLength);
responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + streamResponse.getFileName() + "\"");
responseHeaders.add(HttpHeaders.ACCEPT_RANGES, "bytes");
responseHeaders.add(HttpHeaders.CONTENT_RANGE,
"bytes" + " " + start + "-" + end + "/" + streamResponse.getSize());

StreamingResponseBody responseStream = os -> streamResponse.consumeOutputStream(os, start, end);

Date lastModifiedDate = streamResponse.getLastModified();
if (lastModifiedDate != null) {
CacheControl cacheControl = CacheControl.empty().cachePrivate().sMaxAge(Duration.ofSeconds(60));
responseHeaders.add(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue());
responseHeaders.setETag(Long.toString(lastModifiedDate.getTime()));
responseHeaders.add(HttpHeaders.LAST_MODIFIED, streamResponse.getLastModified().toString());
}

return new ResponseEntity<>(responseStream, responseHeaders, HttpStatus.PARTIAL_CONTENT);
}

public static ResponseEntity<StreamingResponseBody> okResponse(StreamResponse streamResponse) {
return okResponse(streamResponse, false);
}
Expand Down Expand Up @@ -131,7 +168,8 @@ public static ResponseEntity<StreamingResponseBody> okResponse(StreamResponse st
responseHeaders.add("Content-Type", streamResponse.getStream().getMediaType());
responseHeaders.add(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + streamResponse.getStream().getFileName() + "\"");
responseHeaders.add("Content-Length", String.valueOf(streamResponse.getStream().getSize()));
// responseHeaders.add("Content-Length",
// String.valueOf(streamResponse.getStream().getSize()));

Date lastModifiedDate = streamResponse.getStream().getLastModified();

Expand All @@ -144,4 +182,4 @@ public static ResponseEntity<StreamingResponseBody> okResponse(StreamResponse st

return ResponseEntity.ok().headers(responseHeaders).body(responseStream);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;

Expand Down Expand Up @@ -99,7 +100,7 @@ public Date getLastModified() {
}

@Override
public long getSize() {
public long getSize() {
return -1;
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import com.databasepreservation.common.api.utils.ExtraMediaType;
import com.databasepreservation.model.exception.ModuleException;
import com.databasepreservation.common.api.exceptions.RESTException;
import com.databasepreservation.common.exceptions.AuthorizationException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.roda.core.data.exceptions.*;
import org.roda.core.data.exceptions.AlreadyExistsException;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.utils.JsonUtils;
import org.roda.core.data.v2.index.sublist.Sublist;
import org.springframework.batch.core.BatchStatus;
Expand All @@ -42,7 +42,6 @@
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.JobOperator;
Expand All @@ -53,17 +52,21 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import com.databasepreservation.common.api.exceptions.RESTException;
import com.databasepreservation.common.api.utils.ApiUtils;
import com.databasepreservation.common.api.utils.DownloadUtils;
import com.databasepreservation.common.api.utils.ExtraMediaType;
import com.databasepreservation.common.api.utils.HandlebarsUtils;
import com.databasepreservation.common.api.utils.StreamResponse;
import com.databasepreservation.common.api.utils.ViewerStreamingOutput;
Expand All @@ -75,7 +78,6 @@
import com.databasepreservation.common.client.ViewerConstants;
import com.databasepreservation.common.client.common.search.SavedSearch;
import com.databasepreservation.common.client.common.search.SearchInfo;
import com.databasepreservation.common.exceptions.SavedSearchException;
import com.databasepreservation.common.client.index.FindRequest;
import com.databasepreservation.common.client.index.IndexResult;
import com.databasepreservation.common.client.index.filter.Filter;
Expand All @@ -95,6 +97,8 @@
import com.databasepreservation.common.client.models.user.User;
import com.databasepreservation.common.client.services.CollectionService;
import com.databasepreservation.common.client.tools.ViewerStringUtils;
import com.databasepreservation.common.exceptions.AuthorizationException;
import com.databasepreservation.common.exceptions.SavedSearchException;
import com.databasepreservation.common.exceptions.ViewerException;
import com.databasepreservation.common.server.ViewerConfiguration;
import com.databasepreservation.common.server.ViewerFactory;
Expand All @@ -107,6 +111,7 @@
import com.databasepreservation.common.server.index.utils.IterableIndexResult;
import com.databasepreservation.common.server.index.utils.JsonTransformer;
import com.databasepreservation.common.server.index.utils.SolrUtils;
import com.databasepreservation.common.server.storage.BinaryConsumesOutputStream;
import com.databasepreservation.common.utils.ControllerAssistant;
import com.databasepreservation.common.utils.LobManagerUtils;
import com.databasepreservation.common.utils.UserUtility;
Expand Down Expand Up @@ -164,8 +169,7 @@ public ResponseEntity<Resource> getReport(@PathVariable(name = "databaseUUID") S
InputStreamResource resource = new InputStreamResource(new FileInputStream(reportPath.toFile()));
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"" + reportPath.toFile().getName() + "\"")
.contentLength(reportPath.toFile().length())
.contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
.contentLength(reportPath.toFile().length()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
} catch (NotFoundException | IOException | AuthorizationException e) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
Expand Down Expand Up @@ -503,7 +507,8 @@ public ResponseEntity<StreamingResponseBody> exportLOB(
@PathVariable(name = ViewerConstants.API_PATH_PARAM_DATABASE_UUID) String databaseUUID,
@PathVariable(name = ViewerConstants.API_PATH_PARAM_COLLECTION_UUID) String collectionUUID,
@PathVariable(name = "schema") String schema, @PathVariable(name = "table") String table,
@PathVariable(name = "rowIndex") String rowIndex, @PathVariable(name = "columnIndex") Integer columnIndex) {
@PathVariable(name = "rowIndex") String rowIndex, @PathVariable(name = "columnIndex") Integer columnIndex,
@RequestHeader HttpHeaders headers) {

ControllerAssistant controllerAssistant = new ControllerAssistant() {};

Expand Down Expand Up @@ -532,7 +537,7 @@ public ResponseEntity<StreamingResponseBody> exportLOB(
return handleExternalLobDownload(configTable, row, columnIndex);
} else {
String version = ViewerFactory.getSolrManager().retrieve(ViewerDatabase.class, databaseUUID).getVersion();
return handleInternalLobDownload(database.getPath(), configTable, row, columnIndex, version);
return handleInternalLobDownload(database.getPath(), configTable, row, columnIndex, version, headers);
}
}
} catch (NotFoundException | GenericException | IOException | AuthorizationException e) {
Expand Down Expand Up @@ -601,7 +606,7 @@ private ResponseEntity<StreamingResponseBody> handleExternalLobDownload(TableSta
}

private ResponseEntity<StreamingResponseBody> handleInternalLobDownload(String databasePath,
TableStatus tableConfiguration, ViewerRow row, int columnIndex, String version)
TableStatus tableConfiguration, ViewerRow row, int columnIndex, String version, HttpHeaders headers)
throws IOException, GenericException {
String handlebarsFilename = HandlebarsUtils.applyExportTemplate(row, tableConfiguration, columnIndex);

Expand Down Expand Up @@ -636,6 +641,13 @@ private ResponseEntity<StreamingResponseBody> handleInternalLobDownload(String d
return ApiUtils.okResponse(new StreamResponse(handlebarsFilename, handlebarsMimeType,
DownloadUtils.stream(new BufferedInputStream(new FileInputStream(tempZipFile.toFile())))));
} else {

if (!headers.getRange().isEmpty()) {
return ApiUtils.rangeResponse(headers,
new BinaryConsumesOutputStream(Path.of(filePath), Path.of(filePath).toFile().length(), handlebarsFilename,
handlebarsMimeType));
}

return ApiUtils.okResponse(new StreamResponse(handlebarsFilename, handlebarsMimeType,
DownloadUtils.stream(new BufferedInputStream(new FileInputStream(filePath)))));
}
Expand Down Expand Up @@ -963,4 +975,4 @@ public void deleteSavedSearch(String databaseUUID, String collectionUUID, String
databaseUUID, ViewerConstants.CONTROLLER_SAVED_SEARCH_UUID_PARAM, savedSearchUUID);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package com.databasepreservation.common.server.storage;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.util.Date;

import com.databasepreservation.common.api.common.ConsumesSkipableOutputStream;

public class BinaryConsumesOutputStream implements ConsumesSkipableOutputStream {
private final Path path;
private final long size;
private final String filename;
private final String mediaType;

public BinaryConsumesOutputStream(Path path, long size, String filename, String mediaType) {
this.path = path;
this.size = size;
this.filename = filename;
this.mediaType = mediaType;
}

@Override
public void consumeOutputStream(OutputStream output) throws IOException {
// TODO document why this method is empty
}

@Override
public Date getLastModified() {
return null;
}


@Override
public long getSize() {
return this.size;
}

@Override
public void consumeOutputStream(OutputStream out, long from, long end) {
try {
File file = path.toFile();
byte[] buffer = new byte[1024];
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
long pos = from;
randomAccessFile.seek(pos);
while (pos < end) {
randomAccessFile.read(buffer);
out.write(buffer);
pos += buffer.length;
}
out.flush();
}
} catch (IOException e) {
// ignore
}

}

@Override
public String getFileName() {
return this.filename;
}

@Override
public String getMediaType() {
return this.mediaType;
}
}

0 comments on commit a779d46

Please sign in to comment.