RepositoryDownloadController.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package org.genesys.server.api.v1;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.exception.NotFoundElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.HandlerMapping;
/**
* This controller servers thumbnails and files.
*
* @author Matija Obreza
*/
@RestController("repositoryDownload1")
public class RepositoryDownloadController {
public static final Logger LOG = LoggerFactory.getLogger(RepositoryDownloadController.class);
@Autowired
private RepositoryService repositoryService;
private void downloadFile(final Path path, final String name, final String ext, final HttpServletResponse response, HttpServletRequest request) throws IOException {
boolean noCache = "no-cache".equalsIgnoreCase(request.getHeader(HttpHeaders.CACHE_CONTROL))
|| "no-cache".equalsIgnoreCase(request.getHeader(HttpHeaders.PRAGMA));
String extension = StringUtils.removeStartIgnoreCase(ext, ".");
if (path.startsWith(RepositoryService.THUMB_PATH) && (extension.equals(RepositoryService.THUMB_EXT_JPG) || extension.equals(RepositoryService.THUMB_EXT_WEBP))) {
final String filename = name + ext;
if (LOG.isDebugEnabled()) {
LOG.debug("_thumb path={} filename={}", path, filename);
}
try {
final RepositoryFile repositoryFile = this.repositoryService.getFile(UUID.fromString(path.getFileName().toString()));
// check Request Cache headers (Modified-Since, ETag)
if (! noCache && clientCacheValid(repositoryFile, request, response)) {
LOG.debug("Client cache is valid.");
return;
}
byte[] data;
try {
data = repositoryService.getThumbnail(path, name, extension, repositoryFile);
} catch (Exception e) {
response.reset();
throw new NotFoundElement("Thumbnail cannot be fetched", e);
}
response.setDateHeader(HttpHeaders.LAST_MODIFIED, repositoryFile.getLastModifiedDate().toEpochMilli());
response.setHeader(HttpHeaders.ETAG, repositoryFile.getSha1Sum());
if (extension.equals(RepositoryService.THUMB_EXT_JPG)) {
response.setContentType(RepositoryService.THUMB_CONTENT_TYPE_JPG);
} else {
response.setContentType(RepositoryService.THUMB_CONTENT_TYPE_WEBP);
}
if (SecurityContextUtil.anyoneHasPermission(repositoryFile, "READ")) {
// Cache for 30days
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=2592000, s-maxage=2592000, public, no-transform");
} else {
// Cache for 24hrs
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=86400, s-maxage=86400, private, no-transform");
}
// We're writing bytes directly
response.setContentLength(data.length);
response.getOutputStream().write(data);
response.getOutputStream().flush();
} catch (NoSuchRepositoryFileException e) {
throw new NotFoundElement("No file for thumb " + name);
}
} else {
// Regular repository file
try {
UUID uuid = null;
try {
uuid = UUID.fromString(name);
} catch (final IllegalArgumentException e) {
LOG.debug("404 - UUID in wrong format.");
// throw new NotFoundElement("No such thing", e);
}
final RepositoryFile repositoryFile = uuid != null
? this.repositoryService.getFile(uuid)
: this.repositoryService.getFile(path, name + ext);
sanityCheck(path, ext, repositoryFile);
// check Request Cache headers (Modified-Since, ETag)
if (! noCache && clientCacheValid(repositoryFile, request, response)) {
LOG.debug("Client cache is valid.");
return;
}
if (SecurityContextUtil.anyoneHasPermission(repositoryFile, "READ")) {
// Cache for 30days
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=2592000, s-maxage=2592000, public, no-transform");
} else {
// Cache for 24hrs
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=86400, s-maxage=86400, private, no-transform");
}
response.setHeader(HttpHeaders.PRAGMA, "");
response.setDateHeader(HttpHeaders.LAST_MODIFIED, repositoryFile.getLastModifiedDate().toEpochMilli());
response.setHeader(HttpHeaders.ETAG, repositoryFile.getSha1Sum());
response.setContentType(repositoryFile.getContentType());
response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", repositoryFile.getOriginalFilename()));
response.setContentLength(repositoryFile.getSize());
try {
repositoryService.streamFileBytes(repositoryFile, response.getOutputStream());
} catch (Exception e) {
LOG.warn("Error streaming bytes: {}", e.getMessage());
response.reset();
throw new NotFoundElement(e.getMessage());
}
response.getOutputStream().flush();
} catch (final NoSuchRepositoryFileException | InvalidRepositoryPathException e) {
if (LOG.isDebugEnabled()) {
LOG.warn("404 - No such repository file: {}", e.getMessage(), e);
} else {
LOG.warn("404 - No such repository file: {}", e.getMessage());
}
throw new NotFoundElement("No such thing", e);
}
}
}
private boolean clientCacheValid(RepositoryFile repositoryFile, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (repositoryFile.getSha1Sum().equals(request.getHeader(HttpHeaders.IF_NONE_MATCH))) {
LOG.debug("ETag matches");
response.setStatus(HttpStatus.NOT_MODIFIED.value());
response.flushBuffer();
return true;
}
long sinceDate = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
if (sinceDate >= -1 && repositoryFile.getLastModifiedDate().toEpochMilli() < sinceDate) {
LOG.debug("Not modified since: {} < {}", repositoryFile.getLastModifiedBy(), Instant.ofEpochMilli(sinceDate));
response.setStatus(HttpStatus.NOT_MODIFIED.value());
response.flushBuffer();
return true;
}
return false;
}
private void sanityCheck(final Path path, final String ext, final RepositoryFile repositoryFile) {
if (repositoryFile == null) {
throw new NotFoundElement("No such thing");
}
if (!repositoryFile.getExtension().equals(ext) || (!repositoryFile.getStorageFolder().equals(path.toString()) && !repositoryFile.getFolder().getFolderPath().equals(path))) {
LOG.warn("{}!={}", repositoryFile.getStorageFolder(), path);
LOG.warn("{}!={}", repositoryFile.getExtension(), ext);
throw new NotFoundElement("No such thing");
}
}
/**
* Serve the bytes of the repository object
*/
@GetMapping(value = RepositoryController.CONTROLLER_URL + "/download/d/**")
public void downloadFile(final HttpServletRequest request, final HttpServletResponse response) throws IOException, NotFoundElement, NoSuchRepositoryFileException {
final String fullpath = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((RepositoryController.CONTROLLER_URL + "/download/d").length());
if (LOG.isTraceEnabled()) {
LOG.trace("Fullname: {}", fullpath);
}
final String ext = fullpath.substring(fullpath.lastIndexOf("."));
final String name = fullpath.substring(fullpath.lastIndexOf("/") + 1, fullpath.lastIndexOf("."));
final String path = fullpath.substring(0, fullpath.lastIndexOf('/'));
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", path, name + ext);
// Enumeration<String> headerNames = request.getHeaderNames();
// while (headerNames.hasMoreElements()) {
// String headerName = headerNames.nextElement();
// LOG.debug(">> {}: {}", headerName, request.getHeader(headerName));
// }
}
downloadFile(Paths.get(path), name, ext, response, request);
}
/**
* Return repository object metadata
*/
@GetMapping(value = RepositoryController.CONTROLLER_URL + "/download/d/**", params = { "metadata" }, produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody RepositoryFile getMetadata(final HttpServletRequest request) throws IOException, NotFoundElement, NoSuchRepositoryFileException {
final String fullpath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
LOG.debug("Fullname: {}", fullpath);
String path;
String uuid;
String ext;
try {
ext = fullpath.substring(fullpath.lastIndexOf("."));
uuid = fullpath.substring(fullpath.lastIndexOf("/") + 1, fullpath.lastIndexOf("."));
path = fullpath.substring((RepositoryController.CONTROLLER_URL + "/download/d").length(), fullpath.lastIndexOf("/"));
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", path, uuid + ext);
}
} catch (ArrayIndexOutOfBoundsException e) {
// fullpath.lastIndexOf may return -1, causing AIOBE
throw new NotFoundElement("No such resource " + fullpath);
}
final RepositoryFile repositoryFile = this.repositoryService.getFile(UUID.fromString(uuid));
sanityCheck(Paths.get(path), ext, repositoryFile);
return repositoryFile;
}
}