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", title = "Working File Repository", abstractText = "Stores and retrieves files for use during media processing.", notes = {
84          "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
85          "If the service is down or not working it will return a status 503, this means the the underlying service is "
86                  + "not working and is either restarting or has failed",
87          "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
88                  + "other words, there is a bug! You should file an error report with your server logs from the time when the "
89                  + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
90  @Component(
91    name = "org.opencastproject.workingfilerepository.impl.WorkingFileRepository",
92    property = {
93      "service.description=Working File Repository REST Endpoint",
94      "opencast.service.type=org.opencastproject.files",
95      "opencast.service.path=/files"
96    },
97    immediate = true,
98    service = { WorkingFileRepositoryRestEndpoint.class, WorkingFileRepository.class, PathMappable.class }
99  )
100 @JaxrsResource
101 public class WorkingFileRepositoryRestEndpoint extends WorkingFileRepositoryImpl {
102 
103   private static final Logger logger = LoggerFactory.getLogger(WorkingFileRepositoryRestEndpoint.class);
104 
105   /**
106    * Callback from OSGi that is called when this service is activated.
107    *
108    * @param cc
109    *          OSGi component context
110    */
111   @Override
112   @Activate
113   public void activate(ComponentContext cc) throws IOException {
114     super.activate(cc);
115   }
116 
117   /**
118    * Callback from OSGi that is called when this service is deactivated.
119    */
120   @Override
121   @Deactivate
122   public void deactivate() {
123     super.deactivate();
124   }
125 
126   /**
127    * Sets the remote service manager.
128    *
129    * @param remoteServiceManager
130    */
131   @Override
132   @Reference
133   public void setRemoteServiceManager(ServiceRegistry remoteServiceManager) {
134     super.setRemoteServiceManager(remoteServiceManager);
135   }
136 
137   @Override
138   @Reference
139   public void setSecurityService(SecurityService securityService) {
140     super.setSecurityService(securityService);
141   }
142 
143   @POST
144   @Produces(MediaType.TEXT_HTML)
145   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
146   @RestQuery(name = "put", description = "Store a file in working repository under ./mediaPackageID/mediaPackageElementID", returnDescription = "The URL to access the stored file", pathParameters = {
147           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true, type = STRING),
148           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier", isRequired = true, type = STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "OK, file stored") }, restParameters = { @RestParameter(name = "file", description = "the filename", isRequired = true, type = FILE) })
149   public Response restPut(@PathParam("mediaPackageID") String mediaPackageID,
150           @PathParam("mediaPackageElementID") String mediaPackageElementID, @Context HttpServletRequest request)
151           throws Exception {
152     if (ServletFileUpload.isMultipartContent(request)) {
153       for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
154         FileItemStream item = iter.next();
155         if (item.isFormField()) {
156           continue;
157 
158         }
159         URI url = this.put(mediaPackageID, mediaPackageElementID, item.getName(), item.openStream());
160         return Response.ok(url.toString()).build();
161       }
162     }
163     return Response.serverError().status(400).build();
164   }
165 
166   @POST
167   @Produces(MediaType.TEXT_HTML)
168   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}/{filename}")
169   @RestQuery(name = "putWithFilename", description = "Store a file in working repository under ./mediaPackageID/mediaPackageElementID/filename", returnDescription = "The URL to access the stored file", pathParameters = {
170           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true, type = STRING),
171           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier", isRequired = true, type = STRING),
172           @RestParameter(name = "filename", description = "the filename", isRequired = true, type = FILE) }, responses = { @RestResponse(responseCode = SC_OK, description = "OK, file stored") })
173   public Response restPutURLEncoded(@Context HttpServletRequest request,
174           @PathParam("mediaPackageID") String mediaPackageID,
175           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("filename") String filename,
176           @FormParam("content") String content) throws Exception {
177     String encoding = request.getCharacterEncoding();
178     if (encoding == null)
179       encoding = "utf-8";
180 
181     URI url = this.put(mediaPackageID, mediaPackageElementID, filename, IOUtils.toInputStream(content, encoding));
182     return Response.ok(url.toString()).build();
183   }
184 
185   @POST
186   @Produces(MediaType.TEXT_HTML)
187   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}")
188   @RestQuery(name = "putInCollection", description = "Store a file in working repository under ./collectionId/filename", returnDescription = "The URL to access the stored file", pathParameters = { @RestParameter(name = "collectionId", description = "the colection identifier", isRequired = true, type = STRING) }, restParameters = { @RestParameter(name = "file", description = "the filename", isRequired = true, type = FILE) }, responses = { @RestResponse(responseCode = SC_OK, description = "OK, file stored") })
189   public Response restPutInCollection(@PathParam("collectionId") String collectionId,
190           @Context HttpServletRequest request) throws Exception {
191     if (ServletFileUpload.isMultipartContent(request)) {
192       for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
193         FileItemStream item = iter.next();
194         if (item.isFormField()) {
195           continue;
196 
197         }
198         URI url = this.putInCollection(collectionId, item.getName(), item.openStream());
199         return Response.ok(url.toString()).build();
200       }
201     }
202     return Response.serverError().status(400).build();
203   }
204 
205   @DELETE
206   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
207   @RestQuery(name = "delete", description = "Remove the file from the working repository under /mediaPackageID/mediaPackageElementID", returnDescription = "No content", pathParameters = {
208           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true, type = STRING),
209           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier", isRequired = true, type = STRING) }, responses = {
210           @RestResponse(responseCode = SC_OK, description = "File deleted"),
211           @RestResponse(responseCode = SC_NOT_FOUND, description = "File did not exist") })
212   public Response restDelete(@PathParam("mediaPackageID") String mediaPackageID,
213           @PathParam("mediaPackageElementID") String mediaPackageElementID) {
214     try {
215       if (delete(mediaPackageID, mediaPackageElementID))
216         return Response.ok().build();
217       else
218         return Response.status(HttpStatus.SC_NOT_FOUND).build();
219     } catch (Exception e) {
220       logger.error("Unable to delete element '{}' from mediapackage '{}'", mediaPackageElementID,
221               mediaPackageID, e);
222       return Response.serverError().entity(e.getMessage()).build();
223     }
224   }
225 
226   @DELETE
227   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}/{fileName}")
228   @RestQuery(name = "deleteFromCollection", description = "Remove the file from the working repository under /collectionId/filename", returnDescription = "No content", pathParameters = {
229           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true, type = STRING),
230           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING) }, responses = {
231           @RestResponse(responseCode = SC_NO_CONTENT, description = "File deleted"),
232           @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection or file not found") })
233   public Response restDeleteFromCollection(@PathParam("collectionId") String collectionId,
234           @PathParam("fileName") String fileName) {
235     try {
236       if (this.deleteFromCollection(collectionId, fileName))
237         return Response.noContent().build();
238       else
239         return Response.status(SC_NOT_FOUND).build();
240     } catch (Exception e) {
241       logger.error("Unable to delete element '{}' from collection '{}'", fileName, collectionId, e);
242       return Response.serverError().entity(e.getMessage()).build();
243     }
244   }
245 
246   @DELETE
247   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "cleanup/{collectionId}/{numberOfDays}")
248   @RestQuery(name = "cleanupOldFilesFromCollection", description = "Remove the files from the working repository under /collectionId that are older than N days", returnDescription = "No content", pathParameters = {
249           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true, type = STRING),
250           @RestParameter(name = "numberOfDays", description = "files older than this number of days will be deleted", isRequired = true, type = STRING) }, responses = {
251                   @RestResponse(responseCode = SC_NO_CONTENT, description = "Files deleted"),
252                   @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection not found") })
253   public Response restCleanupOldFilesFromCollection(@PathParam("collectionId") String collectionId,
254           @PathParam("numberOfDays") long days) {
255     try {
256       if (this.cleanupOldFilesFromCollection(collectionId, days))
257         return Response.noContent().build();
258       else
259         return Response.status(SC_NOT_FOUND).build();
260     } catch (Exception e) {
261       logger.error("Unable to delete files older than '{}' days from collection '{}'",
262               days, collectionId, e);
263       return Response.serverError().entity(e.getMessage()).build();
264     }
265   }
266 
267   @DELETE
268   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "cleanup/mediapackage/{numberOfDays}")
269   @RestQuery(name = "cleanupOldFilesFromMediaPackage", description = "Remove files and directories from the working file repository under /mediapackage that are older than N days", returnDescription = "No content", pathParameters = {
270           @RestParameter(name = "numberOfDays", description = "files older than this number of days will be deleted", isRequired = true, type = STRING) }, responses = {
271           @RestResponse(responseCode = SC_NO_CONTENT, description = "Files deleted")})
272   public Response restCleanupOldFilesFromMediaPackage(@PathParam("numberOfDays") long days) {
273     try {
274       this.cleanupOldFilesFromMediaPackage(days);
275       return Response.noContent().build();
276     } catch (Exception e) {
277       return Response.serverError().entity(e.getMessage()).build();
278     }
279   }
280 
281   @GET
282   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}")
283   @RestQuery(name = "get", description = "Gets the file from the working repository under /mediaPackageID/mediaPackageElementID", returnDescription = "The file", pathParameters = {
284           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true, type = STRING),
285           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier", isRequired = true, type = STRING) }, responses = {
286           @RestResponse(responseCode = SC_OK, description = "File returned"),
287           @RestResponse(responseCode = SC_NOT_MODIFIED, description = "If file not modified"),
288           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found") })
289   public Response restGet(@PathParam("mediaPackageID") final String mediaPackageID,
290           @PathParam("mediaPackageElementID") final String mediaPackageElementID,
291           @HeaderParam("If-None-Match") String ifNoneMatch) throws NotFoundException {
292     // Check the If-None-Match header first
293     String md5 = null;
294     try {
295       md5 = getMediaPackageElementDigest(mediaPackageID, mediaPackageElementID);
296       if (StringUtils.isNotBlank(ifNoneMatch) && md5.equals(ifNoneMatch)) {
297         return Response.notModified(md5).build();
298       }
299     } catch (IOException e) {
300       logger.warn("Error reading digest of {}/{}", mediaPackageElementID, mediaPackageElementID);
301     }
302     try {
303       String contentType;
304       File file = getFile(mediaPackageID, mediaPackageElementID);
305       try {
306         contentType = MimeTypes.fromString(file.getPath()).toString();
307       } catch (UnknownFileTypeException e) {
308         contentType = "application/octet-stream";
309       }
310       try {
311         return ok(get(mediaPackageID, mediaPackageElementID), contentType, Optional.of(file.length()), Optional.empty());
312       } catch (IOException e) {
313         throw new NotFoundException();
314       }
315     } catch (IllegalStateException e) {
316       logger.error("Unable to provide element '{}' from mediapackage '{}'", mediaPackageElementID,
317               mediaPackageID, e);
318       return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
319     }
320   }
321 
322   @GET
323   @Path(WorkingFileRepository.MEDIAPACKAGE_PATH_PREFIX + "{mediaPackageID}/{mediaPackageElementID}/{fileName}")
324   @RestQuery(name = "getWithFilename", description = "Gets the file from the working repository under /mediaPackageID/mediaPackageElementID/filename", returnDescription = "The file", pathParameters = {
325           @RestParameter(name = "mediaPackageID", description = "the mediapackage identifier", isRequired = true, type = STRING),
326           @RestParameter(name = "mediaPackageElementID", description = "the mediapackage element identifier", isRequired = true, type = STRING),
327           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING) }, responses = {
328           @RestResponse(responseCode = SC_OK, description = "File returned"),
329           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found") })
330   public Response restGet(@PathParam("mediaPackageID") String mediaPackageID,
331           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("fileName") String fileName,
332           @HeaderParam("If-None-Match") String ifNoneMatch, @HeaderParam("Range") String range)
333           throws NotFoundException {
334     String md5 = null;
335     // Check the If-None-Match header first
336     try {
337       md5 = getMediaPackageElementDigest(mediaPackageID, mediaPackageElementID);
338       if (StringUtils.isNotBlank(ifNoneMatch) && md5.equals(ifNoneMatch)) {
339         return Response.notModified(md5).build();
340       }
341     } catch (IOException e) {
342       logger.warn("Error reading digest of {}/{}/{}", mediaPackageElementID, mediaPackageElementID,
343               fileName);
344     }
345 
346     try {
347       if (StringUtils.isNotBlank(range)) {
348         logger.debug("trying to retrieve range: {}", range);
349         return partialFileResponse(getFile(mediaPackageID, mediaPackageElementID), getMimeType(fileName),
350                 Optional.of(fileName), range).tag(md5).build();
351 
352       } else {
353         // No If-Non-Match header provided, or the file changed in the meantime
354         return fileResponse(getFile(mediaPackageID, mediaPackageElementID), getMimeType(fileName),
355                 Optional.of(fileName)).tag(md5).build();
356       }
357     } catch (Exception e) {
358       logger.error("Unable to provide element '{}' from mediapackage '{}'", mediaPackageElementID,
359               mediaPackageID, e);
360       return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
361     }
362   }
363 
364   @GET
365   @Path(WorkingFileRepository.COLLECTION_PATH_PREFIX + "{collectionId}/{fileName}")
366   @RestQuery(name = "getFromCollection", description = "Gets the file from the working repository under /collectionId/filename", returnDescription = "The file", pathParameters = {
367           @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true, type = STRING),
368           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING) }, responses = {
369           @RestResponse(responseCode = SC_OK, description = "File returned"),
370           @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found") })
371   public Response restGetFromCollection(@PathParam("collectionId") String collectionId,
372           @PathParam("fileName") String fileName) throws NotFoundException {
373     return fileResponse(getFileFromCollection(collectionId, fileName), getMimeType(fileName), Optional.of(fileName))
374             .build();
375   }
376 
377   @GET
378   @Path("/collectionuri/{collectionID}/{fileName}")
379   @RestQuery(name = "getUriFromCollection", description = "Gets the URL for a file to be stored in the working repository under /collectionId/filename", returnDescription = "The url to this file", pathParameters = {
380           @RestParameter(name = "collectionID", description = "the collection identifier", isRequired = true, type = STRING),
381           @RestParameter(name = "fileName", description = "the file name", isRequired = true, type = STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "URL returned") })
382   public Response restGetCollectionUri(@PathParam("collectionID") String collectionId,
383           @PathParam("fileName") String fileName) {
384     URI uri = this.getCollectionURI(collectionId, fileName);
385     return Response.ok(uri.toString()).build();
386   }
387 
388   @GET
389   @Path("/uri/{mediaPackageID}/{mediaPackageElementID}")
390   @RestQuery(name = "getUri", description = "Gets the URL for a file to be stored in the working repository under /mediaPackageID", returnDescription = "The url to this file", pathParameters = {
391           @RestParameter(name = "mediaPackageID", description = "the mediaPackage identifier", isRequired = true, type = STRING),
392           @RestParameter(name = "mediaPackageElementID", description = "the mediaPackage element identifier", isRequired = true, type = STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "URL returned") })
393   public Response restGetUri(@PathParam("mediaPackageID") String mediaPackageID,
394           @PathParam("mediaPackageElementID") String mediaPackageElementID) {
395     URI uri = this.getURI(mediaPackageID, mediaPackageElementID);
396     return Response.ok(uri.toString()).build();
397   }
398 
399   @GET
400   @Path("/uri/{mediaPackageID}/{mediaPackageElementID}/{fileName}")
401   @RestQuery(name = "getUriWithFilename", description = "Gets the URL for a file to be stored in the working repository under /mediaPackageID", returnDescription = "The url to this file", pathParameters = {
402           @RestParameter(name = "mediaPackageID", description = "the mediaPackage identifier", isRequired = true, type = STRING),
403           @RestParameter(name = "mediaPackageElementID", description = "the mediaPackage element identifier", isRequired = true, type = STRING),
404           @RestParameter(name = "fileName", description = "the filename", isRequired = true, type = STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "URL returned") })
405   public Response restGetUri(@PathParam("mediaPackageID") String mediaPackageID,
406           @PathParam("mediaPackageElementID") String mediaPackageElementID, @PathParam("fileName") String fileName) {
407     URI uri = this.getURI(mediaPackageID, mediaPackageElementID, fileName);
408     return Response.ok(uri.toString()).build();
409   }
410 
411   @SuppressWarnings("unchecked")
412   @GET
413   @Produces(MediaType.APPLICATION_JSON)
414   @Path("/list/{collectionId}.json")
415   @RestQuery(name = "filesInCollection", description = "Lists files in a collection", returnDescription = "Links to the URLs in a collection", pathParameters = { @RestParameter(name = "collectionId", description = "the collection identifier", isRequired = true, type = STRING) }, responses = {
416           @RestResponse(responseCode = SC_OK, description = "URLs returned"),
417           @RestResponse(responseCode = SC_NOT_FOUND, description = "Collection not found") })
418   public Response restGetCollectionContents(@PathParam("collectionId") String collectionId) throws NotFoundException {
419     URI[] uris = super.getCollectionContents(collectionId);
420     JSONArray jsonArray = new JSONArray();
421     for (URI uri : uris) {
422       jsonArray.add(uri.toString());
423     }
424     return Response.ok(jsonArray.toJSONString()).build();
425   }
426 
427   @POST
428   @Path("/copy/{fromCollection}/{fromFileName}/{toMediaPackage}/{toMediaPackageElement}/{toFileName}")
429   @RestQuery(name = "copy", description = "Copies a file from a collection to a mediapackage", returnDescription = "A URL to the copied file", pathParameters = {
430           @RestParameter(name = "fromCollection", description = "the collection identifier hosting the source", isRequired = true, type = STRING),
431           @RestParameter(name = "fromFileName", description = "the source file name", isRequired = true, type = STRING),
432           @RestParameter(name = "toMediaPackage", description = "the destination mediapackage identifier", isRequired = true, type = STRING),
433           @RestParameter(name = "toMediaPackageElement", description = "the destination mediapackage element identifier", isRequired = true, type = STRING),
434           @RestParameter(name = "toFileName", description = "the destination file name", isRequired = true, type = STRING) }, responses = {
435           @RestResponse(responseCode = SC_OK, description = "URL returned"),
436           @RestResponse(responseCode = SC_NOT_FOUND, description = "File to copy not found") })
437   public Response restCopyTo(@PathParam("fromCollection") String fromCollection,
438           @PathParam("fromFileName") String fromFileName, @PathParam("toMediaPackage") String toMediaPackage,
439           @PathParam("toMediaPackageElement") String toMediaPackageElement, @PathParam("toFileName") String toFileName)
440           throws NotFoundException {
441     try {
442       URI uri = super.copyTo(fromCollection, fromFileName, toMediaPackage, toMediaPackageElement, toFileName);
443       return Response.ok().entity(uri.toString()).build();
444     } catch (IOException e) {
445       logger.error("Unable to copy file '{}' from collection '{}' to mediapackage {}/{}",
446               fromFileName, fromCollection, toMediaPackage, toMediaPackageElement, e);
447       return Response.serverError().entity(e.getMessage()).build();
448     }
449   }
450 
451   @POST
452   @Path("/move/{fromCollection}/{fromFileName}/{toMediaPackage}/{toMediaPackageElement}/{toFileName}")
453   @RestQuery(name = "move", description = "Moves a file from a collection to a mediapackage", returnDescription = "A URL to the moved file", pathParameters = {
454           @RestParameter(name = "fromCollection", description = "the collection identifier hosting the source", isRequired = true, type = STRING),
455           @RestParameter(name = "fromFileName", description = "the source file name", isRequired = true, type = STRING),
456           @RestParameter(name = "toMediaPackage", description = "the destination mediapackage identifier", isRequired = true, type = STRING),
457           @RestParameter(name = "toMediaPackageElement", description = "the destination mediapackage element identifier", isRequired = true, type = STRING),
458           @RestParameter(name = "toFileName", description = "the destination file name", isRequired = true, type = STRING) }, responses = {
459           @RestResponse(responseCode = SC_OK, description = "URL returned"),
460           @RestResponse(responseCode = SC_NOT_FOUND, description = "File to move not found") })
461   public Response restMoveTo(@PathParam("fromCollection") String fromCollection,
462           @PathParam("fromFileName") String fromFileName, @PathParam("toMediaPackage") String toMediaPackage,
463           @PathParam("toMediaPackageElement") String toMediaPackageElement, @PathParam("toFileName") String toFileName)
464           throws NotFoundException {
465     try {
466       URI uri = super.moveTo(fromCollection, fromFileName, toMediaPackage, toMediaPackageElement, toFileName);
467       return Response.ok().entity(uri.toString()).build();
468     } catch (IOException e) {
469       logger.error("Unable to move file '{}' from collection '{}' to mediapackage {}/{}",
470               fromFileName, fromCollection, toMediaPackage, toMediaPackageElement, e);
471       return Response.serverError().entity(e.getMessage()).build();
472     }
473   }
474 
475   @SuppressWarnings("unchecked")
476   @GET
477   @Produces(MediaType.APPLICATION_JSON)
478   @Path("storage")
479   @RestQuery(name = "storage", description = "Returns a report on the disk usage and availability", returnDescription = "Plain text containing the report", responses = { @RestResponse(responseCode = SC_OK, description = "Report returned") })
480   public Response restGetTotalStorage() {
481     long total = this.getTotalSpace().get();
482     long usable = this.getUsableSpace().get();
483     long used = this.getUsedSpace().get();
484     String summary = this.getDiskSpace();
485     JSONObject json = new JSONObject();
486     json.put("size", total);
487     json.put("usable", usable);
488     json.put("used", used);
489     json.put("summary", summary);
490     return Response.ok(json.toJSONString()).build();
491   }
492 
493   @GET
494   @Produces(MediaType.TEXT_PLAIN)
495   @Path("/baseUri")
496   @RestQuery(name = "baseuri", description = "Returns a base URI for this repository", returnDescription = "Plain text containing the base URI", responses = { @RestResponse(responseCode = SC_OK, description = "Base URI returned") })
497   public Response restGetBaseUri() {
498     return Response.ok(super.getBaseUri().toString()).build();
499   }
500 }