package de.is24.infrastructure.gridfs.http.web.controller;
import de.is24.infrastructure.gridfs.http.exception.BadRangeRequestException;
import de.is24.infrastructure.gridfs.http.exception.GridFSFileNotFoundException;
import de.is24.infrastructure.gridfs.http.gridfs.BoundedGridFsResource;
import de.is24.infrastructure.gridfs.http.gridfs.StorageService;
import de.is24.infrastructure.gridfs.http.storage.FileDescriptor;
import de.is24.infrastructure.gridfs.http.storage.FileStorageService;
import de.is24.util.monitoring.InApplicationMonitor;
import de.is24.util.monitoring.spring.TimeMeasurement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.ResponseStatus;
import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static de.is24.infrastructure.gridfs.http.web.MediaTypes.APPLICATION_X_RPM;
import static java.lang.Long.parseLong;
import static java.lang.String.format;
import static java.util.regex.Pattern.compile;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.HttpStatus.PARTIAL_CONTENT;
import static org.springframework.http.MediaType.valueOf;
import static org.springframework.web.bind.annotation.RequestMethod.DELETE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
@Controller
@RequestMapping(FileController.PREFIX)
@TimeMeasurement
public class FileController {
private static final Logger LOGGER = LoggerFactory.getLogger(FileController.class);
public static final String PREFIX = "/repo";
public static final String RANGE_PATTERN_REGEXP = "^bytes=(0|[1-9]\\d*)-((0|[1-9]\\d*)?)$";
private static final Pattern RANGE_PATTERN = compile(RANGE_PATTERN_REGEXP);
public static final String RPM_EXTENSION = ".rpm";
private final FileStorageService fileStorageService;
private final StorageService storageService;
// just for cglib
protected FileController() {
this.storageService = null;
this.fileStorageService = null;
}
@Autowired
public FileController(StorageService storageService, FileStorageService fileStorageService) {
this.storageService = storageService;
this.fileStorageService = fileStorageService;
}
@RequestMapping(value = "/{repo}/{arch}/{filename:.+}", method = GET)
public ResponseEntity<InputStreamResource> deliverFile(@PathVariable("repo") String repo,
@PathVariable("arch") String arch,
@PathVariable("filename") String filename) throws IOException {
BoundedGridFsResource resource = fileStorageService.getResource(new FileDescriptor(repo, arch, filename));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(resource.contentLength());
InApplicationMonitor.getInstance().incrementCounter(getClass().getName() + ".get.rpm");
return new ResponseEntity<>(resource, withContentHeaders(httpHeaders, resource), OK);
}
@RequestMapping(value = "/{repo}/{arch}/{filename:.+}", method = GET, headers = { "Range" })
public ResponseEntity<InputStreamResource> deliverRangeOfFile(@PathVariable("repo") String repo,
@PathVariable("arch") String arch,
@PathVariable("filename") String filename,
@RequestHeader("Range") String rangeHeader)
throws IOException {
FileDescriptor descriptor = new FileDescriptor(repo, arch, filename);
Matcher matcher = getMatcher(rangeHeader);
String intervalStartString = matcher.group(1);
String intervalEndString = matcher.group(2);
long intervalStart = parseRangeLong(intervalStartString, rangeHeader);
BoundedGridFsResource resource;
if (isEmpty(intervalEndString)) {
resource = fileStorageService.getResource(descriptor, intervalStart);
} else {
long intervalEnd = parseRangeLong(intervalEndString, rangeHeader);
if (intervalEnd < intervalStart) {
throw new BadRangeRequestException(
format("Range end is before range start for path [%s]", descriptor.getPath()),
rangeHeader);
}
resource = fileStorageService.getResource(descriptor, intervalStart, intervalEnd - intervalStart + 1);
}
InApplicationMonitor.getInstance().incrementCounter(getClass().getName() + ".get.rpm-range");
return new ResponseEntity<>(resource, withContentHeaders(rangeHeaders(resource), resource),
PARTIAL_CONTENT);
}
private long parseRangeLong(String value, String rangeHeader) {
try {
return parseLong(value);
} catch (NumberFormatException e) {
throw new BadRangeRequestException("Could not parse range element '" + value + "' to long.", rangeHeader);
}
}
private HttpHeaders withContentHeaders(HttpHeaders httpHeaders, BoundedGridFsResource resource) {
if (isNotBlank(resource.getContentType())) {
httpHeaders.setContentType(valueOf(resource.getContentType()));
}
String disposition = APPLICATION_X_RPM.equals(httpHeaders.getContentType()) ? "attachment" : "inline";
httpHeaders.set("Content-Disposition", disposition + "; filename=" + new File(resource.getFilename()).getName());
return httpHeaders;
}
private Matcher getMatcher(String rangeHeader) {
Matcher matcher = RANGE_PATTERN.matcher(rangeHeader);
if (!matcher.matches()) {
throw new BadRangeRequestException("Byte range header does not match " + RANGE_PATTERN_REGEXP, rangeHeader);
}
return matcher;
}
@RequestMapping(value = "/{repoName}/{arch}/{filename}" + RPM_EXTENSION, method = DELETE)
@ResponseStatus(NO_CONTENT)
public void deleteFile(@PathVariable("repoName") String repoName,
@PathVariable("arch") String arch,
@PathVariable("filename") String filename) {
FileDescriptor descriptor = new FileDescriptor(repoName, arch, filename + RPM_EXTENSION);
try {
storageService.delete(descriptor);
LOGGER.info("Deleted file {}.rpm", filename);
InApplicationMonitor.getInstance().incrementCounter(getClass().getName() + ".delete.rpm");
} catch (GridFSFileNotFoundException ex) {
LOGGER.info("ignoring delete of none existing resource '{}'", descriptor.getPath());
InApplicationMonitor.getInstance().incrementCounter(getClass().getName() + ".delete.nonExistentRPM");
}
}
private HttpHeaders rangeHeaders(BoundedGridFsResource resource) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.set("Accept-Ranges", "bytes");
headers.set("Content-Range",
"bytes 0-" + (resource.contentLength() - 1) + "/" + resource.getFileLength());
headers.setContentLength(resource.contentLength());
return headers;
}
}