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.workingfilerepository.impl;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
25  import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
26  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
27  import static javax.servlet.http.HttpServletResponse.SC_OK;
28  import static org.opencastproject.util.MimeTypes.getMimeType;
29  import static org.opencastproject.util.RestUtil.R.ok;
30  import static org.opencastproject.util.RestUtil.fileResponse;
31  import static org.opencastproject.util.RestUtil.partialFileResponse;
32  import static org.opencastproject.util.data.Option.none;
33  import static org.opencastproject.util.data.Option.some;
34  import static org.opencastproject.util.doc.rest.RestParameter.Type.FILE;
35  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36  
37  import org.opencastproject.security.api.SecurityService;
38  import org.opencastproject.serviceregistry.api.ServiceRegistry;
39  import org.opencastproject.util.MimeTypes;
40  import org.opencastproject.util.NotFoundException;
41  import org.opencastproject.util.UnknownFileTypeException;
42  import org.opencastproject.util.doc.rest.RestParameter;
43  import org.opencastproject.util.doc.rest.RestQuery;
44  import org.opencastproject.util.doc.rest.RestResponse;
45  import org.opencastproject.util.doc.rest.RestService;
46  import org.opencastproject.workingfilerepository.api.PathMappable;
47  import org.opencastproject.workingfilerepository.api.WorkingFileRepository;
48  
49  import org.apache.commons.fileupload.FileItemIterator;
50  import org.apache.commons.fileupload.FileItemStream;
51  import org.apache.commons.fileupload.servlet.ServletFileUpload;
52  import org.apache.commons.io.IOUtils;
53  import org.apache.commons.lang3.StringUtils;
54  import org.apache.http.HttpStatus;
55  import org.json.simple.JSONArray;
56  import org.json.simple.JSONObject;
57  import org.osgi.service.component.ComponentContext;
58  import org.osgi.service.component.annotations.Activate;
59  import org.osgi.service.component.annotations.Component;
60  import org.osgi.service.component.annotations.Deactivate;
61  import org.osgi.service.component.annotations.Reference;
62  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  import java.io.File;
67  import java.io.IOException;
68  import java.net.URI;
69  
70  import javax.servlet.http.HttpServletRequest;
71  import javax.ws.rs.DELETE;
72  import javax.ws.rs.FormParam;
73  import javax.ws.rs.GET;
74  import javax.ws.rs.HeaderParam;
75  import javax.ws.rs.POST;
76  import javax.ws.rs.Path;
77  import javax.ws.rs.PathParam;
78  import javax.ws.rs.Produces;
79  import javax.ws.rs.core.Context;
80  import javax.ws.rs.core.MediaType;
81  import javax.ws.rs.core.Response;
82  
83  @Path("/files")
84  @RestService(name = "filerepo",
85      title = "Working File Repository",
86      abstractText = "Stores and retrieves files for use during media processing.",
87      notes = {
88          "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
89          "If the service is down or not working it will return a status 503, this means the the underlying service is "
90                  + "not working and is either restarting or has failed",
91          "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
92                  + "other words, there is a bug! You should file an error report with your server logs from the time "
93                  + "when the error occurred: "
94                  + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
95      }
96  )
97  @Component(
98      name = "org.opencastproject.workingfilerepository.impl.WorkingFileRepository",
99      property = {
100       "service.description=Working File Repository REST Endpoint",
101       "opencast.service.type=org.opencastproject.files",
102       "opencast.service.path=/files"
103     },
104     immediate = true,
105     service = { WorkingFileRepositoryRestEndpoint.class, WorkingFileRepository.class, PathMappable.class }
106 )
107 @JaxrsResource
108 public class WorkingFileRepositoryRestEndpoint extends WorkingFileRepositoryImpl {
109 
110   private static final Logger logger = LoggerFactory.getLogger(WorkingFileRepositoryRestEndpoint.class);
111 
112   /**
113    * Callback from OSGi that is called when this service is activated.
114    *
115    * @param cc
116    *          OSGi component context
117    */
118   @Override
119   @Activate
120   public void activate(ComponentContext cc) throws IOException {
121     super.activate(cc);
122   }
123 
124   /**
125    * Callback from OSGi that is called when this service is deactivated.
126    */
127   @Override
128   @Deactivate
129   public void deactivate() {
130     super.deactivate();
131   }
132 
133   /**
134    * Sets the remote service manager.
135    *
136    * @param remoteServiceManager
137    */
138   @Override
139   @Reference
140   public void setRemoteServiceManager(ServiceRegistry remoteServiceManager) {
141     super.setRemoteServiceManager(remoteServiceManager);
142   }
143 
144   @Override
145   @Reference
146   public void setSecurityService(SecurityService securityService) {
147     super.setSecurityService(securityService);
148   }
149 
150   @POST
151   @Produces(MediaType.TEXT_HTML)
152   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
153   @RestQuery(name = "put",
154       description = "Store a file in working repository under ./mediaPackageID/mediaPackageElementID",
155       returnDescription = "The URL to access the stored file",
156       pathParameters = {
157           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true,
158               type = STRING),
159           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier",
160               isRequired = true, type = STRING)
161       },
162       responses = {
163           @RestResponse(responseCode = SC_OK, description = "OK, file stored")
164       },
165       restParameters = {
166           @RestParameter(name = "file", description = "the filename", isRequired = true, type = FILE)
167       }
168   )
169   public Response restPut(@PathParam("mediaPackageID") String mediaPackageID,
170           @PathParam("mediaPackageElementID") String mediaPackageElementID, @Context HttpServletRequest request)
171           throws Exception {
172     if (ServletFileUpload.isMultipartContent(request)) {
173       for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
174         FileItemStream item = iter.next();
175         if (item.isFormField()) {
176           continue;
177 
178         }
179         URI url = this.put(mediaPackageID, mediaPackageElementID, item.getName(), item.openStream());
180         return Response.ok(url.toString()).build();
181       }
182     }
183     return Response.serverError().status(400).build();
184   }
185 
186   @POST
187   @Produces(MediaType.TEXT_HTML)
188   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}/{filename}")
189   @RestQuery(name = "putWithFilename",
190       description = "Store a file in working repository under ./mediaPackageID/mediaPackageElementID/filename",
191       returnDescription = "The URL to access the stored file",
192       pathParameters = {
193           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true,
194               type = STRING),
195           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier",
196               isRequired = true, type = STRING),
197           @RestParameter(name = "filename", description = "the filename", isRequired = true, type = FILE)
198       },
199       responses = {
200           @RestResponse(responseCode = SC_OK, description = "OK, file stored")
201       }
202   )
203   public Response restPutURLEncoded(@Context HttpServletRequest request,
204           @PathParam("mediaPackageID") String mediaPackageID,
205           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("filename") String filename,
206           @FormParam("content") String content) throws Exception {
207     String encoding = request.getCharacterEncoding();
208     if (encoding == null) {
209       encoding = "utf-8";
210     }
211 
212     URI url = this.put(mediaPackageID, mediaPackageElementID, filename, IOUtils.toInputStream(content, encoding));
213     return Response.ok(url.toString()).build();
214   }
215 
216   @POST
217   @Produces(MediaType.TEXT_HTML)
218   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}")
219   @RestQuery(name = "putInCollection",
220       description = "Store a file in working repository under ./collectionId/filename",
221       returnDescription = "The URL to access the stored file",
222       pathParameters = {
223           @RestParameter(name = "collectionId", description = "the colection identifier", isRequired = true,
224               type = STRING)
225       },
226       restParameters = {
227           @RestParameter(name = "file", description = "the filename", isRequired = true, type = FILE)
228       },
229       responses = {
230           @RestResponse(responseCode = SC_OK, description = "OK, file stored")
231       }
232   )
233   public Response restPutInCollection(@PathParam("collectionId") String collectionId,
234           @Context HttpServletRequest request) throws Exception {
235     if (ServletFileUpload.isMultipartContent(request)) {
236       for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
237         FileItemStream item = iter.next();
238         if (item.isFormField()) {
239           continue;
240 
241         }
242         URI url = this.putInCollection(collectionId, item.getName(), item.openStream());
243         return Response.ok(url.toString()).build();
244       }
245     }
246     return Response.serverError().status(400).build();
247   }
248 
249   @DELETE
250   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
251   @RestQuery(name = "delete",
252       description = "Remove the file from the working repository under /mediaPackageID/mediaPackageElementID",
253       returnDescription = "No content",
254       pathParameters = {
255           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true,
256               type = STRING),
257           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier",
258               isRequired = true, type = STRING)
259       },
260       responses = {
261           @RestResponse(responseCode = SC_OK, description = "File deleted"),
262           @RestResponse(responseCode = SC_NOT_FOUND, description = "File did not exist")
263       }
264   )
265   public Response restDelete(@PathParam("mediaPackageID") String mediaPackageID,
266           @PathParam("mediaPackageElementID") String mediaPackageElementID) {
267     try {
268       if (delete(mediaPackageID, mediaPackageElementID)) {
269         return Response.ok().build();
270       } else {
271         return Response.status(HttpStatus.SC_NOT_FOUND).build();
272       }
273     } catch (Exception e) {
274       logger.error("Unable to delete element '{}' from mediapackage '{}'", mediaPackageElementID,
275               mediaPackageID, e);
276       return Response.serverError().entity(e.getMessage()).build();
277     }
278   }
279 
280   @DELETE
281   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}/{fileName}")
282   @RestQuery(name = "deleteFromCollection",
283       description = "Remove the file from the working repository under /collectionId/filename",
284       returnDescription = "No content",
285       pathParameters = {
286           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true,
287               type = STRING),
288           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING)
289       },
290       responses = {
291           @RestResponse(responseCode = SC_NO_CONTENT, description = "File deleted"),
292           @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection or file not found")
293       }
294   )
295   public Response restDeleteFromCollection(@PathParam("collectionId") String collectionId,
296           @PathParam("fileName") String fileName) {
297     try {
298       if (this.deleteFromCollection(collectionId, fileName)) {
299         return Response.noContent().build();
300       } else {
301         return Response.status(SC_NOT_FOUND).build();
302       }
303     } catch (Exception e) {
304       logger.error("Unable to delete element '{}' from collection '{}'", fileName, collectionId, e);
305       return Response.serverError().entity(e.getMessage()).build();
306     }
307   }
308 
309   @DELETE
310   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "cleanup/{collectionId}/{numberOfDays}")
311   @RestQuery(name = "cleanupOldFilesFromCollection",
312       description = "Remove the files from the working repository under /collectionId that are older than N days",
313       returnDescription = "No content",
314       pathParameters = {
315           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true,
316               type = STRING),
317           @RestParameter(name = "numberOfDays", description = "files older than this number of days will be deleted",
318               isRequired = true, type = STRING)
319       },
320       responses = {
321                   @RestResponse(responseCode = SC_NO_CONTENT, description = "Files deleted"),
322                   @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection not found")
323       }
324   )
325   public Response restCleanupOldFilesFromCollection(@PathParam("collectionId") String collectionId,
326           @PathParam("numberOfDays") long days) {
327     try {
328       if (this.cleanupOldFilesFromCollection(collectionId, days)) {
329         return Response.noContent().build();
330       } else {
331         return Response.status(SC_NOT_FOUND).build();
332       }
333     } catch (Exception e) {
334       logger.error("Unable to delete files older than '{}' days from collection '{}'",
335               days, collectionId, e);
336       return Response.serverError().entity(e.getMessage()).build();
337     }
338   }
339 
340   @DELETE
341   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "cleanup/mediapackage/{numberOfDays}")
342   @RestQuery(name = "cleanupOldFilesFromMediaPackage",
343       description = "Remove files and directories from the working file repository under /mediapackage that are "
344           + "older than N days",
345       returnDescription = "No content",
346       pathParameters = {
347           @RestParameter(name = "numberOfDays", description = "files older than this number of days will be deleted",
348               isRequired = true, type = STRING)
349       },
350       responses = {
351           @RestResponse(responseCode = SC_NO_CONTENT, description = "Files deleted")
352       }
353   )
354   public Response restCleanupOldFilesFromMediaPackage(@PathParam("numberOfDays") long days) {
355     try {
356       this.cleanupOldFilesFromMediaPackage(days);
357       return Response.noContent().build();
358     } catch (Exception e) {
359       return Response.serverError().entity(e.getMessage()).build();
360     }
361   }
362 
363   @GET
364   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
365   @RestQuery(name = "get",
366       description = "Gets the file from the working repository under /mediaPackageID/mediaPackageElementID",
367       returnDescription = "The file",
368       pathParameters = {
369           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true,
370               type = STRING),
371           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier",
372               isRequired = true, type = STRING)
373       },
374       responses = {
375           @RestResponse(responseCode = SC_OK, description = "File returned"),
376           @RestResponse(responseCode = SC_NOT_MODIFIED, description = "If file not modified"),
377           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found")
378       }
379   )
380   public Response restGet(@PathParam("mediaPackageID") final String mediaPackageID,
381           @PathParam("mediaPackageElementID") final String mediaPackageElementID,
382           @HeaderParam("If-None-Match") String ifNoneMatch) throws NotFoundException {
383     // Check the If-None-Match header first
384     String md5 = null;
385     try {
386       md5 = getMediaPackageElementDigest(mediaPackageID, mediaPackageElementID);
387       if (StringUtils.isNotBlank(ifNoneMatch) && md5.equals(ifNoneMatch)) {
388         return Response.notModified(md5).build();
389       }
390     } catch (IOException e) {
391       logger.warn("Error reading digest of {}/{}", mediaPackageElementID, mediaPackageElementID);
392     }
393     try {
394       String contentType;
395       File file = getFile(mediaPackageID, mediaPackageElementID);
396       try {
397         contentType = MimeTypes.fromString(file.getPath()).toString();
398       } catch (UnknownFileTypeException e) {
399         contentType = "application/octet-stream";
400       }
401       try {
402         return ok(get(mediaPackageID, mediaPackageElementID), contentType, some(file.length()), none(""));
403       } catch (IOException e) {
404         throw new NotFoundException();
405       }
406     } catch (IllegalStateException e) {
407       logger.error("Unable to provide element '{}' from mediapackage '{}'", mediaPackageElementID,
408               mediaPackageID, e);
409       return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
410     }
411   }
412 
413   @GET
414   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}/{fileName}")
415   @RestQuery(name = "getWithFilename",
416       description = "Gets the file from the working repository under /mediaPackageID/mediaPackageElementID/filename",
417       returnDescription = "The file",
418       pathParameters = {
419           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true,
420               type = STRING),
421           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier",
422               isRequired = true, type = STRING),
423           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING)
424       },
425       responses = {
426           @RestResponse(responseCode = SC_OK, description = "File returned"),
427           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found")
428       }
429   )
430   public Response restGet(@PathParam("mediaPackageID") String mediaPackageID,
431           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("fileName") String fileName,
432           @HeaderParam("If-None-Match") String ifNoneMatch, @HeaderParam("Range") String range)
433           throws NotFoundException {
434     String md5 = null;
435     // Check the If-None-Match header first
436     try {
437       md5 = getMediaPackageElementDigest(mediaPackageID, mediaPackageElementID);
438       if (StringUtils.isNotBlank(ifNoneMatch) && md5.equals(ifNoneMatch)) {
439         return Response.notModified(md5).build();
440       }
441     } catch (IOException e) {
442       logger.warn("Error reading digest of {}/{}/{}", mediaPackageElementID, mediaPackageElementID,
443               fileName);
444     }
445 
446     try {
447       if (StringUtils.isNotBlank(range)) {
448         logger.debug("trying to retrieve range: {}", range);
449         return partialFileResponse(getFile(mediaPackageID, mediaPackageElementID), getMimeType(fileName),
450                 some(fileName), range).tag(md5).build();
451 
452       } else {
453         // No If-Non-Match header provided, or the file changed in the meantime
454         return fileResponse(getFile(mediaPackageID, mediaPackageElementID), getMimeType(fileName),
455                 some(fileName)).tag(md5).build();
456       }
457     } catch (Exception e) {
458       logger.error("Unable to provide element '{}' from mediapackage '{}'", mediaPackageElementID,
459               mediaPackageID, e);
460       return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
461     }
462   }
463 
464   @GET
465   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}/{fileName}")
466   @RestQuery(name = "getFromCollection",
467       description = "Gets the file from the working repository under /collectionId/filename",
468       returnDescription = "The file",
469       pathParameters = {
470           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true,
471               type = STRING),
472           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING)
473       },
474       responses = {
475           @RestResponse(responseCode = SC_OK, description = "File returned"),
476           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found")
477       }
478   )
479   public Response restGetFromCollection(@PathParam("collectionId") String collectionId,
480           @PathParam("fileName") String fileName) throws NotFoundException {
481     return fileResponse(getFileFromCollection(collectionId, fileName), getMimeType(fileName), some(fileName))
482             .build();
483   }
484 
485   @GET
486   @Path("/collectionuri/{collectionID}/{fileName}")
487   @RestQuery(name = "getUriFromCollection",
488       description = "Gets the URL for a file to be stored in the working repository under /collectionId/filename",
489       returnDescription = "The url to this file",
490       pathParameters = {
491           @RestParameter(name = "collectionID", description = "the collection identifier", isRequired = true,
492               type = STRING),
493           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING)
494       },
495       responses = {
496           @RestResponse(responseCode = SC_OK, description = "URL returned")
497       }
498   )
499   public Response restGetCollectionUri(@PathParam("collectionID") String collectionId,
500           @PathParam("fileName") String fileName) {
501     URI uri = this.getCollectionURI(collectionId, fileName);
502     return Response.ok(uri.toString()).build();
503   }
504 
505   @GET
506   @Path("/uri/{mediaPackageID}/{mediaPackageElementID}")
507   @RestQuery(name = "getUri",
508       description = "Gets the URL for a file to be stored in the working repository under /mediaPackageID",
509       returnDescription = "The url to this file",
510       pathParameters = {
511           @RestParameter(name = "mediaPackageID", description = "the mediaPackage identifier", isRequired = true,
512               type = STRING),
513           @RestParameter(name = "mediaPackageElementID", description = "the mediaPackage element identifier",
514               isRequired = true, type = STRING)
515       },
516       responses = {
517       @RestResponse(responseCode = SC_OK, description = "URL returned")
518       }
519   )
520   public Response restGetUri(@PathParam("mediaPackageID") String mediaPackageID,
521           @PathParam("mediaPackageElementID") String mediaPackageElementID) {
522     URI uri = this.getURI(mediaPackageID, mediaPackageElementID);
523     return Response.ok(uri.toString()).build();
524   }
525 
526   @GET
527   @Path("/uri/{mediaPackageID}/{mediaPackageElementID}/{fileName}")
528   @RestQuery(name = "getUriWithFilename",
529       description = "Gets the URL for a file to be stored in the working repository under /mediaPackageID",
530       returnDescription = "The url to this file",
531       pathParameters = {
532           @RestParameter(name = "mediaPackageID", description = "the mediaPackage identifier", isRequired = true,
533               type = STRING),
534           @RestParameter(name = "mediaPackageElementID", description = "the mediaPackage element identifier",
535               isRequired = true, type = STRING),
536           @RestParameter(name = "fileName", description = "the filename", isRequired = true, type = STRING)
537       },
538       responses = {
539           @RestResponse(responseCode = SC_OK, description = "URL returned")
540       }
541   )
542   public Response restGetUri(@PathParam("mediaPackageID") String mediaPackageID,
543           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("fileName") String fileName) {
544     URI uri = this.getURI(mediaPackageID, mediaPackageElementID, fileName);
545     return Response.ok(uri.toString()).build();
546   }
547 
548   @SuppressWarnings("unchecked")
549   @GET
550   @Produces(MediaType.APPLICATION_JSON)
551   @Path("/list/{collectionId}.json")
552   @RestQuery(name = "filesInCollection",
553       description = "Lists files in a collection",
554       returnDescription = "Links to the URLs in a collection",
555       pathParameters = {
556           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true,
557               type = STRING)
558       },
559       responses = {
560           @RestResponse(responseCode = SC_OK, description = "URLs returned"),
561           @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection not found")
562       }
563   )
564   public Response restGetCollectionContents(@PathParam("collectionId") String collectionId) throws NotFoundException {
565     URI[] uris = super.getCollectionContents(collectionId);
566     JSONArray jsonArray = new JSONArray();
567     for (URI uri : uris) {
568       jsonArray.add(uri.toString());
569     }
570     return Response.ok(jsonArray.toJSONString()).build();
571   }
572 
573   @POST
574   @Path("/copy/{fromCollection}/{fromFileName}/{toMediaPackage}/{toMediaPackageElement}/{toFileName}")
575   @RestQuery(name = "copy",
576       description = "Copies a file from a collection to a mediapackage",
577       returnDescription = "A URL to the copied file",
578       pathParameters = {
579           @RestParameter(name = "fromCollection", description = "the collection identifier hosting the source",
580               isRequired = true, type = STRING),
581           @RestParameter(name = "fromFileName", description = "the source file name", isRequired = true, type = STRING),
582           @RestParameter(name = "toMediaPackage", description = "the destination mediapackage identifier",
583               isRequired = true, type = STRING),
584           @RestParameter(name = "toMediaPackageElement", description = "the destination mediapackage element "
585               + "identifier", isRequired = true, type = STRING),
586           @RestParameter(name = "toFileName", description = "the destination file name", isRequired = true,
587               type = STRING)
588       },
589       responses = {
590           @RestResponse(responseCode = SC_OK, description = "URL returned"),
591           @RestResponse(responseCode = SC_NOT_FOUND, description = "File to copy not found")
592       }
593   )
594   public Response restCopyTo(@PathParam("fromCollection") String fromCollection,
595           @PathParam("fromFileName") String fromFileName, @PathParam("toMediaPackage") String toMediaPackage,
596           @PathParam("toMediaPackageElement") String toMediaPackageElement, @PathParam("toFileName") String toFileName)
597           throws NotFoundException {
598     try {
599       URI uri = super.copyTo(fromCollection, fromFileName, toMediaPackage, toMediaPackageElement, toFileName);
600       return Response.ok().entity(uri.toString()).build();
601     } catch (IOException e) {
602       logger.error("Unable to copy file '{}' from collection '{}' to mediapackage {}/{}",
603               fromFileName, fromCollection, toMediaPackage, toMediaPackageElement, e);
604       return Response.serverError().entity(e.getMessage()).build();
605     }
606   }
607 
608   @POST
609   @Path("/move/{fromCollection}/{fromFileName}/{toMediaPackage}/{toMediaPackageElement}/{toFileName}")
610   @RestQuery(name = "move",
611       description = "Moves a file from a collection to a mediapackage",
612       returnDescription = "A URL to the moved file",
613       pathParameters = {
614           @RestParameter(name = "fromCollection", description = "the collection identifier hosting the source",
615               isRequired = true, type = STRING),
616           @RestParameter(name = "fromFileName", description = "the source file name", isRequired = true, type = STRING),
617           @RestParameter(name = "toMediaPackage", description = "the destination mediapackage identifier",
618               isRequired = true, type = STRING),
619           @RestParameter(name = "toMediaPackageElement", description = "the destination mediapackage element "
620               + "identifier", isRequired = true, type = STRING),
621           @RestParameter(name = "toFileName", description = "the destination file name", isRequired = true,
622               type = STRING)
623       },
624       responses = {
625           @RestResponse(responseCode = SC_OK, description = "URL returned"),
626           @RestResponse(responseCode = SC_NOT_FOUND, description = "File to move not found")
627       }
628   )
629   public Response restMoveTo(@PathParam("fromCollection") String fromCollection,
630           @PathParam("fromFileName") String fromFileName, @PathParam("toMediaPackage") String toMediaPackage,
631           @PathParam("toMediaPackageElement") String toMediaPackageElement, @PathParam("toFileName") String toFileName)
632           throws NotFoundException {
633     try {
634       URI uri = super.moveTo(fromCollection, fromFileName, toMediaPackage, toMediaPackageElement, toFileName);
635       return Response.ok().entity(uri.toString()).build();
636     } catch (IOException e) {
637       logger.error("Unable to move file '{}' from collection '{}' to mediapackage {}/{}",
638               fromFileName, fromCollection, toMediaPackage, toMediaPackageElement, e);
639       return Response.serverError().entity(e.getMessage()).build();
640     }
641   }
642 
643   @SuppressWarnings("unchecked")
644   @GET
645   @Produces(MediaType.APPLICATION_JSON)
646   @Path("storage")
647   @RestQuery(name = "storage",
648       description = "Returns a report on the disk usage and availability",
649       returnDescription = "Plain text containing the report",
650       responses = {
651           @RestResponse(responseCode = SC_OK, description = "Report returned")
652       }
653   )
654   public Response restGetTotalStorage() {
655     long total = this.getTotalSpace().get();
656     long usable = this.getUsableSpace().get();
657     long used = this.getUsedSpace().get();
658     String summary = this.getDiskSpace();
659     JSONObject json = new JSONObject();
660     json.put("size", total);
661     json.put("usable", usable);
662     json.put("used", used);
663     json.put("summary", summary);
664     return Response.ok(json.toJSONString()).build();
665   }
666 
667   @GET
668   @Produces(MediaType.TEXT_PLAIN)
669   @Path("/baseUri")
670   @RestQuery(name = "baseuri",
671       description = "Returns a base URI for this repository",
672       returnDescription = "Plain text containing the base URI",
673       responses = {
674           @RestResponse(responseCode = SC_OK, description = "Base URI returned")
675       }
676   )
677   public Response restGetBaseUri() {
678     return Response.ok(super.getBaseUri().toString()).build();
679   }
680 }