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