View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.staticfiles.endpoint;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
25  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
26  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
27  
28  import org.opencastproject.security.api.SecurityService;
29  import org.opencastproject.staticfiles.api.StaticFileService;
30  import org.opencastproject.systems.OpencastConstants;
31  import org.opencastproject.util.MimeTypes;
32  import org.opencastproject.util.NotFoundException;
33  import org.opencastproject.util.OsgiUtil;
34  import org.opencastproject.util.ProgressInputStream;
35  import org.opencastproject.util.RestUtil;
36  import org.opencastproject.util.RestUtil.R;
37  import org.opencastproject.util.UrlSupport;
38  import org.opencastproject.util.data.Option;
39  import org.opencastproject.util.doc.rest.RestParameter;
40  import org.opencastproject.util.doc.rest.RestQuery;
41  import org.opencastproject.util.doc.rest.RestResponse;
42  import org.opencastproject.util.doc.rest.RestService;
43  
44  import org.apache.commons.fileupload.FileItemIterator;
45  import org.apache.commons.fileupload.FileItemStream;
46  import org.apache.commons.fileupload.servlet.ServletFileUpload;
47  import org.apache.commons.io.IOUtils;
48  import org.apache.commons.lang3.BooleanUtils;
49  import org.osgi.service.cm.ConfigurationException;
50  import org.osgi.service.component.ComponentContext;
51  import org.osgi.service.component.annotations.Component;
52  import org.osgi.service.component.annotations.Reference;
53  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import java.beans.PropertyChangeEvent;
58  import java.beans.PropertyChangeListener;
59  import java.io.IOException;
60  import java.io.InputStream;
61  import java.net.URI;
62  
63  import javax.servlet.http.HttpServletRequest;
64  import javax.servlet.http.HttpServletResponse;
65  import javax.ws.rs.Consumes;
66  import javax.ws.rs.DELETE;
67  import javax.ws.rs.GET;
68  import javax.ws.rs.POST;
69  import javax.ws.rs.Path;
70  import javax.ws.rs.PathParam;
71  import javax.ws.rs.Produces;
72  import javax.ws.rs.WebApplicationException;
73  import javax.ws.rs.core.Context;
74  import javax.ws.rs.core.MediaType;
75  import javax.ws.rs.core.Response;
76  import javax.ws.rs.core.Response.Status;
77  
78  /**
79   * Stores and serves static files via HTTP.
80   */
81  @Path("/staticfiles")
82  @RestService(
83      name = "StaticResourceService",
84      title = "Static Resources Service",
85      abstractText = "This service allows the uploading of static resources such as videos and images.",
86      notes = {
87          "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
88          "If the service is down or not working it will return a status 503, this means the the "
89              + "underlying service is not working and is either restarting or has failed",
90          "A status code 500 means a general failure has occurred which is not recoverable and was "
91              + "not anticipated. In other words, there is a bug! You should file an error report "
92              + "with your server logs from the time when the error occurred: "
93              + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
94      }
95  )
96  @Component(
97      immediate = true,
98      service = StaticFileRestService.class,
99      property = {
100         "service.description=Static File Service REST Endpoint",
101         "opencast.service.type=org.opencastproject.staticfiles",
102         "opencast.service.path=/staticfiles",
103         "opencast.service.jobproducer=false"
104     }
105 )
106 @JaxrsResource
107 public class StaticFileRestService {
108 
109   /** The logging facility */
110   private static final Logger logger = LoggerFactory.getLogger(StaticFileRestService.class);
111 
112   /** The default URL path of the static files */
113   public static final String STATICFILES_URL_PATH = "staticfiles";
114 
115   /** The key to find whether the static file webserver is enabled or not. */
116   public static final String STATICFILES_WEBSERVER_ENABLED_KEY = "org.opencastproject.staticfiles.webserver.enabled";
117 
118   /** The key to find the URL for where a webserver is hosting the static files. */
119   public static final String STATICFILES_WEBSERVER_URL_KEY = "org.opencastproject.staticfiles.webserver.url";
120 
121   /** The key to find the maximum sized file to accept as an upload. */
122   public static final String STATICFILES_UPLOAD_MAX_SIZE_KEY = "org.opencastproject.staticfiles.upload.max.size";
123 
124   /** The security service */
125   private SecurityService securityService = null;
126 
127   /** The static file service */
128   private StaticFileService staticFileService;
129 
130   /** The URL of the current server */
131   private String serverUrl;
132 
133   /** The URL to serve static files from a webserver instead of Opencast. */
134   private Option<String> webserverURL = Option.none();
135 
136   /** The maximum file size to allow to be uploaded in bytes, default 1GB */
137   private long maxUploadSize = 1000000000;
138 
139   /**
140    * Whether to provide urls to download the static files from a webserver
141    * without organization and security protection, or use Opencast to provide
142    * the files.
143    */
144   protected boolean useWebserver = false;
145 
146   /** OSGi callback to bind service instance. */
147   @Reference
148   public void setStaticFileService(StaticFileService staticFileService) {
149     this.staticFileService = staticFileService;
150   }
151 
152   /** OSGi callback to bind service instance. */
153   @Reference
154   public void setSecurityService(SecurityService securityService) {
155     this.securityService = securityService;
156   }
157 
158   /**
159    * OSGI callback for activating this component
160    *
161    * @param cc
162    *          the osgi component context
163    */
164   public void activate(ComponentContext cc) throws ConfigurationException {
165     logger.info("Static File REST Service started.");
166     serverUrl = OsgiUtil.getContextProperty(cc, OpencastConstants.SERVER_URL_PROPERTY);
167     useWebserver = BooleanUtils.toBoolean(OsgiUtil.getOptCfg(cc.getProperties(), STATICFILES_WEBSERVER_ENABLED_KEY)
168             .getOrElse("false"));
169     webserverURL = OsgiUtil.getOptCfg(cc.getProperties(), STATICFILES_WEBSERVER_URL_KEY);
170 
171     Option<String> cfgMaxUploadSize = OsgiUtil.getOptContextProperty(cc, STATICFILES_UPLOAD_MAX_SIZE_KEY);
172     if (cfgMaxUploadSize.isSome()) {
173       maxUploadSize = Long.parseLong(cfgMaxUploadSize.get());
174     }
175   }
176 
177   @GET
178   @Path("{uuid}")
179   @RestQuery(
180       name = "getStaticFile",
181       description = "Returns a static file resource",
182       pathParameters = {
183           @RestParameter(
184               name = "uuid",
185               description = "Static File Universal Unique Id",
186               isRequired = true,
187               type = RestParameter.Type.STRING
188           )
189       },
190       responses = {
191           @RestResponse(
192               description = "Returns a static file resource",
193               responseCode = HttpServletResponse.SC_OK
194           ),
195           @RestResponse(
196               description = "No file by the given uuid found",
197               responseCode = HttpServletResponse.SC_NOT_FOUND
198           )
199       },
200       returnDescription = ""
201   )
202   public Response getStaticFile(@PathParam("uuid") String uuid) throws NotFoundException {
203     try {
204       final InputStream file = staticFileService.getFile(uuid);
205       final String filename = staticFileService.getFileName(uuid);
206       final Long length = staticFileService.getContentLength(uuid);
207       // It is safe to pass the InputStream without closing it, JAX-RS takes care of that
208       return RestUtil.R.ok(file, getMimeType(filename), Option.some(length), Option.some(filename));
209     } catch (NotFoundException | IOException e) {
210       return RestUtil.R.notFound();
211     }
212   }
213 
214   @POST
215   @Consumes(MediaType.MULTIPART_FORM_DATA)
216   @Produces(MediaType.TEXT_PLAIN)
217   @Path("")
218   @RestQuery(
219       name = "postStaticFile",
220       description = "Post a new static resource",
221       bodyParameter = @RestParameter(
222           description = "The static resource file",
223           isRequired = true,
224           name = "BODY",
225           type = RestParameter.Type.FILE
226       ),
227       responses = {
228           @RestResponse(
229               description = "Returns the id of the uploaded static resource",
230               responseCode = HttpServletResponse.SC_CREATED
231           ),
232           @RestResponse(
233               description = "No filename or file to upload found",
234               responseCode = HttpServletResponse.SC_BAD_REQUEST
235           ),
236           @RestResponse(
237               description = "The upload size is too big",
238               responseCode = HttpServletResponse.SC_BAD_REQUEST
239           )
240       },
241       returnDescription = ""
242   )
243   public Response postStaticFile(@Context HttpServletRequest request) {
244     if (maxUploadSize > 0 && request.getContentLength() > maxUploadSize) {
245       logger.warn("Preventing upload of static file as its size {} is larger than the max size allowed {}",
246               request.getContentLength(), maxUploadSize);
247       return Response.status(Status.BAD_REQUEST).build();
248     }
249     ProgressInputStream inputStream = null;
250     try {
251       String filename = null;
252       if (ServletFileUpload.isMultipartContent(request)) {
253         boolean isDone = false;
254         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
255           FileItemStream item = iter.next();
256           if (item.isFormField()) {
257             continue;
258           } else {
259             logger.debug("Processing file item");
260             filename = item.getName();
261             inputStream = new ProgressInputStream(item.openStream());
262             inputStream.addPropertyChangeListener(new PropertyChangeListener() {
263               @Override
264               public void propertyChange(PropertyChangeEvent evt) {
265                 long totalNumBytesRead = (Long) evt.getNewValue();
266                 if (totalNumBytesRead > maxUploadSize) {
267                   logger.warn("Upload limit of {} bytes reached, returning a bad request.", maxUploadSize);
268                   throw new WebApplicationException(Status.BAD_REQUEST);
269                 }
270               }
271             });
272             isDone = true;
273           }
274           if (isDone) {
275             break;
276           }
277         }
278       } else {
279         logger.warn("Request is not multi part request, returning a bad request.");
280         return Response.status(Status.BAD_REQUEST).build();
281       }
282 
283       if (filename == null) {
284         logger.warn("Request was missing the filename, returning a bad request.");
285         return Response.status(Status.BAD_REQUEST).build();
286       }
287 
288       if (inputStream == null) {
289         logger.warn("Request was missing the file, returning a bad request.");
290         return Response.status(Status.BAD_REQUEST).build();
291       }
292 
293       String uuid = staticFileService.storeFile(filename, inputStream);
294       try {
295         return Response.created(getStaticFileURL(uuid)).entity(uuid).build();
296       } catch (NotFoundException e) {
297         logger.error("Previous stored file with uuid {} couldn't beren found:", uuid, e);
298         return Response.serverError().build();
299       }
300     } catch (WebApplicationException e) {
301       return e.getResponse();
302     } catch (Exception e) {
303       logger.error("Unable to store file", e);
304       return Response.serverError().build();
305     } finally {
306       IOUtils.closeQuietly(inputStream);
307     }
308   }
309 
310   @POST
311   @Path("{uuid}/persist")
312   @RestQuery(
313       name = "persistFile",
314       description = "Persists a recently uploaded file to the permanent storage",
315       pathParameters = {
316           @RestParameter(description = "File UUID", isRequired = true, name = "uuid", type = RestParameter.Type.STRING)
317       },
318       responses = {
319           @RestResponse(
320               description = "The file has been persisted",
321               responseCode = HttpServletResponse.SC_OK
322           ),
323           @RestResponse(
324               description = "No file by the given UUID found",
325               responseCode = HttpServletResponse.SC_NOT_FOUND
326           )
327       },
328       returnDescription = ""
329   )
330   public Response persistFile(@PathParam("uuid") String uuid) throws NotFoundException {
331     try {
332       staticFileService.persistFile(uuid);
333       return R.ok();
334     } catch (IOException e) {
335       logger.error("Unable to persist file '{}':", uuid, e);
336       return R.serverError();
337     }
338   }
339 
340   @GET
341   @Produces(MediaType.TEXT_PLAIN)
342   @Path("{uuid}/url")
343   @RestQuery(
344       name = "getStaticFileUrl",
345       description = "Returns a static file resource's URL",
346       pathParameters = {
347           @RestParameter(
348               name = "uuid",
349               description = "Static File Universal Unique Id",
350               isRequired = true,
351               type = RestParameter.Type.STRING
352           )
353       },
354       responses = {
355           @RestResponse(
356               description = "Returns a static file resource's URL",
357               responseCode = HttpServletResponse.SC_OK
358           ),
359           @RestResponse(
360               description = "No file by the given uuid found",
361               responseCode = HttpServletResponse.SC_NOT_FOUND
362           )
363       },
364       returnDescription = ""
365   )
366   public Response getStaticFileUrl(@PathParam("uuid") String uuid) throws NotFoundException {
367     try {
368       return Response.ok(getStaticFileURL(uuid).toString()).build();
369     } catch (NotFoundException e) {
370       throw e;
371     } catch (Exception e) {
372       logger.error("Unable to retrieve static file URL from {}", uuid, e);
373       return Response.serverError().build();
374     }
375   }
376 
377   @DELETE
378   @Path("{uuid}")
379   @RestQuery(
380       name = "deleteStaticFile",
381       description = "Remove the static file",
382       returnDescription = "No content",
383       pathParameters = {
384           @RestParameter(
385               name = "uuid",
386               description = "Static File Universal Unique Id",
387               isRequired = true,
388               type = STRING
389           )
390       },
391       responses = {
392           @RestResponse(responseCode = SC_NO_CONTENT, description = "File deleted"),
393           @RestResponse(responseCode = SC_NOT_FOUND, description = "No file by the given uuid found")
394       }
395   )
396   public Response deleteStaticFile(@PathParam("uuid") String uuid) throws NotFoundException {
397     try {
398       staticFileService.deleteFile(uuid);
399       return Response.noContent().build();
400     } catch (NotFoundException e) {
401       throw e;
402     } catch (Exception e) {
403       logger.error("Unable to delete static file {}", uuid, e);
404       return Response.serverError().build();
405     }
406   }
407 
408   /**
409    * Get the URI for a static file resource depending on whether to get it
410    * direct from Opencast or from a webserver.
411    *
412    * @param uuid
413    *          The unique identifier for the static file.
414    * @return The URL for the static file resource.
415    * @throws NotFoundException
416    *           if the resource couldn't been found
417    */
418   public URI getStaticFileURL(String uuid) throws NotFoundException {
419     if (useWebserver && webserverURL.isSome()) {
420       return URI.create(UrlSupport.concat(webserverURL.get(), securityService.getOrganization().getId(), uuid,
421               staticFileService.getFileName(uuid)));
422     } else {
423       return URI.create(UrlSupport.concat(serverUrl, STATICFILES_URL_PATH, uuid));
424     }
425   }
426 
427   private Option<String> getMimeType(final String filename) {
428     Option<String> mimeType;
429     try {
430       mimeType = Option.some(MimeTypes.fromString(filename).toString());
431     } catch (Exception e) {
432       logger.warn("Unable to detect the mime type of file {}", filename);
433       mimeType = Option.<String> none();
434     }
435     return mimeType;
436   }
437 
438 }