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