/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.restupload;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.StringUtils;
import org.geoserver.catalog.Catalog;
import org.geoserver.rest.util.RESTUtils;
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.restlet.Context;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import org.restlet.resource.StringRepresentation;
import org.restlet.util.Series;
/**
* The main feature of the following module is the ability to resume the upload process of a file via REST.
* <p>
* An upload URL is generated at the first REST POST request and saved in order to execute the other upload steps.</br>
* Successive PUT request on the URL created before allow partial or full upload of binary file.</br>
* RANGE parameter in PUT request/response allow handshake of the number of bytes currently uploaded.</br>
* GET request can be used to retrieve informations about upload status.
* <p>
* The uploaded resource is stored to temporary folder until the upload is not completed or the
* {@link ResumableUploadResourceCleaner#expirationDelay} time is elapsed.</br>
* When the upload is terminated the file is moved to REST main folder by
* {@link ResumableUploadPathMapper}
*
*
* @author Nicola Lagomarsini
*
*/
public class ResumableUploadCatalogResource extends Resource {
private static final Logger LOGGER = Logging.getLogger(ResumableUploadCatalogResource.class);
/** Manager for the Resumable REST upload */
private ResumableUploadResourceManager resumableUploadResourceManager;
/**
* If the server has successfully received all bytes from the operation, it responds with a final status code;
* otherwise it responds with a 308
* (Resume Incomplete), indicating which bytes of the operation it has successfully received.
*/
public static final Status RESUME_INCOMPLETE = new Status(308);
public ResumableUploadCatalogResource(Context context, Request request, Response response,
Catalog catalog, ResumableUploadResourceManager resumableUploadResourceManager) {
super(context, request, response);
this.resumableUploadResourceManager = resumableUploadResourceManager;
}
@Override
public boolean allowPost() {
return true;
}
/**
* PUT request is allow only if at least one upload is in progress
*/
@Override
public boolean allowPut() {
return resumableUploadResourceManager.hasAnyResource();
}
@Override
public boolean allowGet() {
return true;
}
/**
* POST request returns upload URL with uploadId to call with successive PUT request.</br>
* The body of POST request must contains the desired final file path,
* it can be relative path with subfolder.
*/
@Override
public void handlePost() {
try {
String filePath = getRequest().getEntity().getText();
if (filePath == null || filePath.isEmpty()) {
getResponse().setStatus(
new Status(Status.CLIENT_ERROR_BAD_REQUEST,
"POST data must contains upload file path"));
return;
}
Reference ref = getRequest().getResourceRef();
String baseURL = ref.getIdentifier();
String uploadId = resumableUploadResourceManager.createUploadResource(filePath);
Representation output = new StringRepresentation("-----TO USE IN PUT-----\n"+baseURL+"/"+uploadId+"\n-----------------------\n", MediaType.TEXT_PLAIN);
Response response = getResponse();
Series<Parameter> headers = new Form();
headers.add("Location", baseURL+uploadId);
getResponse().getAttributes().put("org.restlet.http.headers", headers);
response.setEntity(output);
response.setStatus(Status.SUCCESS_CREATED);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
getResponse().setStatus(new Status(Status.SERVER_ERROR_INTERNAL, e.getMessage()));
return;
}
}
/**
* PUT request is used to uploads file. </br>
* The request must contains the uploadId attribute with the value returned by previous POST request. </br>
* If the PUT request is the first, it must contains the header parameters "Content-Length: {total file size in bytes}"
* Successive resume PUT request must contains the header parameters:</br>
* <ul>
* <li>Content-Length:{total size of bytes which must be uploaded}
* <li>Content-Range:{resume byte start byte index}-{file end byte index}/{total file size in bytes}
* </ul>
* If the upload is incomplete, the PUT return the RANGE header attribute:</br>
* Range: 0-{uploded end byte index}.
* If the upload is complete, the uploaded file is moved to REST root folder and the PUT return
* the relative path of the file.</br>
* Sidecar file is created in temporary folder to mark the upload as ended and provide information to
* successive GET requests.
*/
@Override
public void handlePut() {
/*
* Check required parameters: - uploadId - Content-Length
*/
String uploadId = RESTUtils.getAttribute(getRequest(), "uploadId");
if (uploadId == null || uploadId.isEmpty()) {
getResponse().setStatus(
new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Missing upload ID"));
return;
}
if (!resumableUploadResourceManager.resourceExists(uploadId)) {
getResponse()
.setStatus(new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Unknow upload ID"));
return;
}
Long totalByteToUpload = getContentLength();
Long startPosition = 0L;
Long endPosition = (totalByteToUpload - 1);
Long totalFileSize = totalByteToUpload;
if (totalByteToUpload == 0) {
getResponse().setStatus(
new Status(Status.CLIENT_ERROR_LENGTH_REQUIRED,
"Not zero Content-Length header must be specified"));
return;
}
HeaderRange headerRange = getHeaderRange();
if (headerRange != null) {
try {
if (headerRange.getMinimum() > headerRange.getMaximum()
|| (headerRange.getRange().longValue() != totalByteToUpload)) {
getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST,
"Range parameter values are not valid");
return;
}
startPosition = headerRange.getMinimum().longValue();
endPosition = headerRange.getMaximum().longValue();
totalFileSize = headerRange.getTotalFileSize();
/*
* Validate resume request If resume is requested existing file must contains the
* number of bytes matching startPosition
*/
Boolean validated = resumableUploadResourceManager.validateUpload(uploadId,
totalByteToUpload, startPosition, endPosition, totalFileSize);
if (!validated) {
getResponse().setStatus(Status.CLIENT_ERROR_REQUESTED_RANGE_NOT_SATISFIABLE,
"Range parameter values not meets partial uploaded files size");
return;
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage());
return;
}
} else {
// Clear previous file if exists
resumableUploadResourceManager.clearUpload(uploadId);
}
/*
* Start upload
*/
Long writedBytes = resumableUploadResourceManager.handleUpload(uploadId, getRequest()
.getEntity(), startPosition);
if (writedBytes < totalFileSize) {
getResponse().setStatus(new Status(RESUME_INCOMPLETE.getCode()));
Series<Parameter> headers = new Form();
headers.add("Content-Length", "0");
headers.add("Range", "0-" + (writedBytes - 1));
getResponse().getAttributes().put("org.restlet.http.headers", headers);
} else {
String mappedPath;
try {
mappedPath = resumableUploadResourceManager.uploadDone(uploadId);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage());
return;
}
Representation output = new StringRepresentation(mappedPath, MediaType.TEXT_PLAIN);
Response response = getResponse();
response.setEntity(output);
response.setStatus(Status.SUCCESS_OK);
}
}
/**
* GET request with uploadId is used to get the status of upload
* If the upload is incomplete, the GET return the RANGE header attribute:</br>
* Range: 0-{uploded end byte index}.
*/
@Override
public void handleGet() {
String uploadId = RESTUtils.getAttribute(getRequest(), "uploadId");
if (uploadId == null || uploadId.isEmpty()) {
getResponse().setStatus(
new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Missing upload ID"));
return;
}
try {
if (!resumableUploadResourceManager.isUploadDone(uploadId)) {
Long writedBytes = resumableUploadResourceManager.getWrittenBytes(uploadId);
getResponse().setStatus(new Status(RESUME_INCOMPLETE.getCode()));
Series<Parameter> headers = new Form();
headers.add("Content-Length", "0");
headers.add("Range", "0-" + (writedBytes - 1));
getResponse().getAttributes().put("org.restlet.http.headers", headers);
} else {
Response response = getResponse();
response.setStatus(Status.SUCCESS_OK);
}
} catch (IllegalStateException e) {
getResponse().setStatus(new Status(Status.CLIENT_ERROR_NOT_FOUND, e.getMessage()));
return;
} catch (IOException e) {
getResponse().setStatus(new Status(Status.SERVER_ERROR_INTERNAL, e.getMessage()));
return;
}
}
private Long getContentLength() {
Long contentLength = 0L;
Object oHeaders = getRequest().getAttributes().get("org.restlet.http.headers");
if (oHeaders != null) {
Series<Parameter> headers = (Series<Parameter>) oHeaders;
Parameter contentLengthParam = headers.getFirst("Content-Length", true);
if (contentLengthParam != null) {
String contentLengthStr = contentLengthParam.getValue();
if (!contentLengthStr.isEmpty() && StringUtils.isNumeric(contentLengthStr)) {
contentLength = Long.parseLong(contentLengthStr);
}
}
}
return contentLength;
}
private HeaderRange getHeaderRange() {
HeaderRange headerRange = null;
Object oHeaders = getRequest().getAttributes().get("org.restlet.http.headers");
if (oHeaders != null) {
Series<Parameter> headers = (Series<Parameter>) oHeaders;
Parameter contentRangeParam = headers.getFirst("Content-Range", true);
if (contentRangeParam != null) {
String contentRangeStr = contentRangeParam.getValue();
String range = contentRangeStr.substring(6);
String[] rangeParts = range.split("/");
Long startPosition = Long.parseLong(rangeParts[0].split("-")[0]);
Long endPosition = Long.parseLong(rangeParts[0].split("-")[1]);
Long totalFileSize = Long.parseLong(rangeParts[1]);
headerRange = new HeaderRange(startPosition, endPosition, totalFileSize);
}
}
return headerRange;
}
private class HeaderRange {
public final NumberRange<Long> contentRange;
public final Long totalFileSize;
public HeaderRange(Long startPosition, Long endPosition, Long totalFileSize) {
super();
this.contentRange = new NumberRange<Long>(Long.class, startPosition, endPosition);
this.totalFileSize = totalFileSize;
}
public Double getMinimum() {
return contentRange.getMinimum();
}
public Double getMaximum() {
return contentRange.getMaximum();
}
public Long getTotalFileSize() {
return totalFileSize;
}
public Double getRange() {
return (contentRange.getMaximum() - contentRange.getMinimum());
}
}
}