All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.fileupload.rest.FileUploadRestService Maven / Gradle / Ivy

/**
 * 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.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
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: "
            + "Opencast Issue Tracker"
    }
)
@Component(
    immediate = true,
    service = FileUploadRestService.class,
    property = {
        "service.description=File Upload REST Endpoint",
        "opencast.service.type=org.opencastproject.fileupload",
        "opencast.service.path=/upload"
    }
)
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();
  }

  // 
  @Reference(
      unbind = "unsetFileUploadService"
  )
  protected void setFileUploadService(FileUploadService service) {
    uploadService = service;
  }

  protected void unsetFileUploadService(FileUploadService service) {
    uploadService = null;
  }

  @Activate
  protected void activate(ComponentContext cc) {
    log.info("File Upload REST Endpoint activated");
  }

  protected void deactivate(ComponentContext cc) {
    log.info("File Upload REST Endpoint deactivated");
  }
  // 

  @POST
  @Produces(MediaType.TEXT_PLAIN)
  @Path("newjob")
  @RestQuery(
      name = "newjob",
      description = "Creates a new upload job and returns the jobs ID.",
      restParameters = {
          @RestParameter(
              name = REQUESTFIELD_FILENAME,
              description = "The name of the file that will be uploaded",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = REQUESTFIELD_FILESIZE,
              description = "The size of the file that will be uploaded",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = REQUESTFIELD_CHUNKSIZE,
              description = "The size of the chunks that will be uploaded",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = REQUESTFIELD_FLAVOR,
              description = "The flavor of this track",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = REQUESTFIELD_MEDIAPACKAGE,
              description = "The mediapackage the file should belong to",
              isRequired = false,
              type = RestParameter.Type.TEXT
          ),
      },
      responses = {
          @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(
              name = "jobID",
              description = "The ID of the upload job",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = "format",
              description = "The output format (json or xml) of the response body.",
              isRequired = true,
              type = RestParameter.Type.STRING
          ),
      },
      responses = {
          @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)) {
        // Return the results using the requested format
        FileUploadJob job = uploadService.getJob(id);
        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(
              name = "jobID",
              description = "The ID of the upload job",
              isRequired = false,
              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
          ),
      },
      responses = {
          @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 {
      // make sure request is "multipart/form-data"
      if (!ServletFileUpload.isMultipartContent(request)) {
        throw new FileUploadException("Request is not of type multipart/form-data");
      }

      // testing for existence of job here already so we can generate a 404 early
      if (uploadService.hasJob(jobId)) {
        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(
              name = "jobID",
              description = "The ID of the upload job to retrieve the file from",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
          @RestParameter(
              name = "filename",
              description = "The name of the payload file",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
      },
      responses = {
        @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);
        // TODO use AutoDetectParser to guess Content-Type header
        return Response.ok(payload).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();
    }
  }

  @DELETE
  @Produces(MediaType.TEXT_PLAIN)
  @Path("job/{jobID}")
  @RestQuery(
      name = "job",
      description = "Deletes an upload job on the server.",
      pathParameters = {
          @RestParameter(
              name = "jobID",
              description = "The ID of the upload job to be deleted",
              isRequired = false,
              type = RestParameter.Type.STRING
          ),
      },
      responses = {
          @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();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy