1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
110 private static final Logger logger = LoggerFactory.getLogger(StaticFileRestService.class);
111
112
113 public static final String STATICFILES_URL_PATH = "staticfiles";
114
115
116 public static final String STATICFILES_WEBSERVER_ENABLED_KEY = "org.opencastproject.staticfiles.webserver.enabled";
117
118
119 public static final String STATICFILES_WEBSERVER_URL_KEY = "org.opencastproject.staticfiles.webserver.url";
120
121
122 public static final String STATICFILES_UPLOAD_MAX_SIZE_KEY = "org.opencastproject.staticfiles.upload.max.size";
123
124
125 private SecurityService securityService = null;
126
127
128 private StaticFileService staticFileService;
129
130
131 private String serverUrl;
132
133
134 private Option<String> webserverURL = Option.none();
135
136
137 private long maxUploadSize = 1000000000;
138
139
140
141
142
143
144 protected boolean useWebserver = false;
145
146
147 @Reference
148 public void setStaticFileService(StaticFileService staticFileService) {
149 this.staticFileService = staticFileService;
150 }
151
152
153 @Reference
154 public void setSecurityService(SecurityService securityService) {
155 this.securityService = securityService;
156 }
157
158
159
160
161
162
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
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
410
411
412
413
414
415
416
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 }