/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community 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://opensource.org/licenses/ecl2.txt * * 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.opencastproject.fileupload.rest; import org.opencastproject.fileupload.api.FileUploadService; import org.opencastproject.fileupload.api.exception.FileUploadException; import org.opencastproject.fileupload.api.job.FileUploadJob; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.util.doc.rest.RestParameter; import org.opencastproject.util.doc.rest.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.util.doc.rest.RestService; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; import org.apache.commons.lang3.StringUtils; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** REST endpoint for large file uploads. * */ @Path("/") @RestService(name = "fileupload", title = "Big File Upload Service", abstractText = "This service provides a facility to upload files that exceed the 2 GB boundry imposed by most " + "browsers through chunked uploads via HTTP.", notes = { "All paths above are relative to the REST endpoint base (something like http://your.server/files)", "If the service is down or not working it will return a status 503, this means the the underlying service is " + "not working and is either restarting or has failed", "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In " + "other words, there is a bug! You should file an error report with your server logs from the time when the " + "error occurred: <a href=\"https://opencast.jira.com\">Opencast Issue Tracker</a>" }) public class FileUploadRestService { // message field names static final String REQUESTFIELD_FILENAME = "filename"; static final String REQUESTFIELD_FILESIZE = "filesize"; static final String REQUESTFIELD_DATA = "filedata"; static final String REQUESTFIELD_CHUNKSIZE = "chunksize"; static final String REQUESTFIELD_CHUNKNUM = "chunknumber"; static final String REQUESTFIELD_MEDIAPACKAGE = "mediapackage"; static final String REQUESTFIELD_FLAVOR = "flavor"; private static final Logger log = LoggerFactory.getLogger(FileUploadRestService.class); private FileUploadService uploadService; private MediaPackageBuilderFactory factory = null; public FileUploadRestService() { factory = MediaPackageBuilderFactory.newInstance(); } // <editor-fold defaultstate="collapsed" desc="OSGi Service Stuff" > protected void setFileUploadService(FileUploadService service) { uploadService = service; } protected void unsetFileUploadService(FileUploadService service) { uploadService = null; } protected void activate(ComponentContext cc) { log.info("File Upload REST Endpoint activated"); } protected void deactivate(ComponentContext cc) { log.info("File Upload REST Endpoint deactivated"); } // </editor-fold> @POST @Produces(MediaType.TEXT_PLAIN) @Path("newjob") @RestQuery(name = "newjob", description = "Creates a new upload job and returns the jobs ID.", restParameters = { @RestParameter(description = "The name of the file that will be uploaded", isRequired = false, name = REQUESTFIELD_FILENAME, type = RestParameter.Type.STRING), @RestParameter(description = "The size of the file that will be uploaded", isRequired = false, name = REQUESTFIELD_FILESIZE, type = RestParameter.Type.STRING), @RestParameter(description = "The size of the chunks that will be uploaded", isRequired = false, name = REQUESTFIELD_CHUNKSIZE, type = RestParameter.Type.STRING), @RestParameter(description = "The flavor of this track", isRequired = false, name = REQUESTFIELD_FLAVOR, type = RestParameter.Type.STRING), @RestParameter(description = "The mediapackage the file should belong to", isRequired = false, name = REQUESTFIELD_MEDIAPACKAGE, type = RestParameter.Type.TEXT)}, reponses = { @RestResponse(description = "job was successfully created", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "upload service gave an error", responseCode = HttpServletResponse.SC_NO_CONTENT) }, returnDescription = "The ID of the newly created upload job") public Response getNewJob( @FormParam(REQUESTFIELD_FILENAME) String filename, @FormParam(REQUESTFIELD_FILESIZE) long filesize, @FormParam(REQUESTFIELD_CHUNKSIZE) int chunksize, @FormParam(REQUESTFIELD_MEDIAPACKAGE) String mediapackage, @FormParam(REQUESTFIELD_FLAVOR) String flav) { try { if (StringUtils.isBlank(filename)) { filename = "john.doe"; } if (filesize < 1) { filesize = -1; } if (chunksize < 1) { chunksize = -1; } MediaPackage mp = null; if (StringUtils.isNotBlank(mediapackage)) { mp = factory.newMediaPackageBuilder().loadFromXml(mediapackage); } MediaPackageElementFlavor flavor = null; if (StringUtils.isNotBlank(flav)) { flavor = new MediaPackageElementFlavor(flav.split("/")[0], flav.split("/")[1]); } FileUploadJob job = uploadService.createJob(filename, filesize, chunksize, mp, flavor); return Response.ok(job.getId()).build(); } catch (FileUploadException e) { log.error(e.getMessage(), e); return Response.status(Response.Status.NO_CONTENT).entity(e.getMessage()).build(); } catch (Exception e) { log.error(e.getMessage(), e); return Response.serverError().entity(buildUnexpectedErrorMessage(e)).build(); } } @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Path("job/{jobID}.{format:xml|json}") @RestQuery(name = "job", description = "Returns the XML or the JSON representation of an upload job.", pathParameters = { @RestParameter(description = "The ID of the upload job", isRequired = false, name = "jobID", type = RestParameter.Type.STRING), @RestParameter(description = "The output format (json or xml) of the response body.", isRequired = true, name = "format", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "the job was successfully retrieved.", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "the job was not found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "The XML representation of the requested upload job.") public Response getJob(@PathParam("jobID") String id, @PathParam("format") String format) { try { if (uploadService.hasJob(id)) { FileUploadJob job = uploadService.getJob(id); // Return the results using the requested format final String type = "json".equals(format) ? MediaType.APPLICATION_JSON : MediaType.APPLICATION_XML; return Response.ok().entity(job).type(type).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } } catch (Exception e) { log.error(e.getMessage(), e); return Response.serverError().entity(buildUnexpectedErrorMessage(e)).build(); } } @POST @Produces(MediaType.APPLICATION_XML) @Path("job/{jobID}") @RestQuery(name = "newjob", description = "Appends the next chunk of data to the file on the server.", pathParameters = { @RestParameter(description = "The ID of the upload job", isRequired = false, name = "jobID", type = RestParameter.Type.STRING) }, restParameters = { @RestParameter(description = "The number of the current chunk", isRequired = false, name = "chunknumber", type = RestParameter.Type.STRING), @RestParameter(description = "The payload", isRequired = false, name = "filedata", type = RestParameter.Type.FILE)}, reponses = { @RestResponse(description = "the chunk data was successfully appended to file on server", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "the upload job was not found", responseCode = HttpServletResponse.SC_NOT_FOUND), @RestResponse(description = "the request was malformed", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "The XML representation of the updated upload job") public Response postPayload(@PathParam("jobID") String jobId, @Context HttpServletRequest request) { try { if (!ServletFileUpload.isMultipartContent(request)) { // make sure request is "multipart/form-data" throw new FileUploadException("Request is not of type multipart/form-data"); } if (uploadService.hasJob(jobId)) { // testing for existence of job here already so we can generate a 404 early long chunkNum = 0; FileUploadJob job = uploadService.getJob(jobId); ServletFileUpload upload = new ServletFileUpload(); for (FileItemIterator iter = upload.getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); if (item.isFormField()) { String name = item.getFieldName(); if (REQUESTFIELD_CHUNKNUM.equalsIgnoreCase(name)) { chunkNum = Long.parseLong(Streams.asString(item.openStream())); } } else if (REQUESTFIELD_DATA.equalsIgnoreCase(item.getFieldName())) { uploadService.acceptChunk(job, chunkNum, item.openStream()); return Response.ok(job).build(); } } throw new FileUploadException("No payload!"); } else { log.warn("Upload job not found: " + jobId); return Response.status(Response.Status.NOT_FOUND).build(); } } catch (FileUploadException e) { log.error(e.getMessage(), e); return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); } catch (Exception e) { log.error(e.getMessage(), e); return Response.serverError().entity(buildUnexpectedErrorMessage(e)).build(); } } @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("job/{jobID}/{filename}") @RestQuery(name = "payload", description = "Returns the payload of the upload job.", pathParameters = { @RestParameter(description = "The ID of the upload job to retrieve the file from", isRequired = false, name = "jobID", type = RestParameter.Type.STRING), @RestParameter(description = "The name of the payload file", isRequired = false, name = "filename", type = RestParameter.Type.STRING)}, reponses = { @RestResponse(description = "the job and file have been found.", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "the job or file were not found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "The payload of the upload job") public Response getPayload(@PathParam("jobID") String id, @PathParam("filename") String filename) { try { if (uploadService.hasJob(id)) { FileUploadJob job = uploadService.getJob(id); InputStream payload = uploadService.getPayload(job); return Response.ok(payload).build(); // TODO use AutoDetectParser to guess Content-Type header } else { return Response.status(Response.Status.NOT_FOUND).build(); } } catch (Exception e) { log.error(e.getMessage(), e); return Response.serverError().entity(buildUnexpectedErrorMessage(e)).build(); } } @DELETE @Produces(MediaType.TEXT_PLAIN) @Path("job/{jobID}") @RestQuery(name = "job", description = "Deletes an upload job on the server.", pathParameters = { @RestParameter(description = "The ID of the upload job to be deleted", isRequired = false, name = "jobID", type = RestParameter.Type.STRING)}, reponses = { @RestResponse(description = "the job was successfully deleted.", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "the job was not found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "A success message that starts with OK") public Response deleteJob(@PathParam("jobID") String id) { try { if (uploadService.hasJob(id)) { uploadService.deleteJob(id); StringBuilder okMessage = new StringBuilder().append("OK: Deleted job ").append(id); return Response.ok(okMessage.toString()).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } } catch (Exception e) { log.error(e.getMessage(), e); return Response.serverError().entity(buildUnexpectedErrorMessage(e)).build(); } } /** Builds an error message in case of an unexpected error in an endpoint method, * includes the exception type and message if existing. * * TODO append stack trace * * @param e Exception that was thrown * @return error message */ private String buildUnexpectedErrorMessage(Exception e) { StringBuilder sb = new StringBuilder(); sb.append("Unexpected error (").append(e.getClass().getName()).append(")"); String message = e.getMessage(); if (StringUtils.isNotBlank(message)) { sb.append(": ").append(message); } return sb.toString(); } }