/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.core.util.vfs.restapi; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.Normalizer; import java.util.Collections; import java.util.Date; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.WebappHelper; import org.olat.core.util.vfs.VFSConstants; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSStatus; import org.olat.core.util.vfs.filters.SystemItemFilter; import org.olat.restapi.support.MultipartReader; import org.olat.restapi.support.vo.File64VO; import org.olat.restapi.support.vo.FileMetadataVO; import org.olat.restapi.support.vo.FileVO; public class VFSWebservice { private static final String VERSION = "1.0"; private static final OLog log = Tracing.createLoggerFor(VFSWebservice.class); public static CacheControl cc = new CacheControl(); static { cc.setMaxAge(-1); } private final VFSContainer container; public VFSWebservice(VFSContainer container) { this.container = container; } /** * Retrieves the version of the Folder Course Node Web Service. * @response.representation.200.mediaType text/plain * @response.representation.200.doc The version of this specific Web Service * @response.representation.200.example 1.0 * @return */ @GET @Path("version") @Produces(MediaType.TEXT_PLAIN) public Response getVersion() { return Response.ok(VERSION).build(); } /** * This retrieves the files or a specific file in the root folder * @response.representation.200.doc The list of files * @response.representation.200.qname {http://www.example.com}linkVOes * @param uriInfo The uri infos * @param request The REST request * @return */ @GET @Produces({"*/*", MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_HTML, MediaType.APPLICATION_OCTET_STREAM}) public Response listFiles(@Context UriInfo uriInfo, @Context Request request) { return get(Collections.<PathSegment>emptyList(), uriInfo, request); } /** * This retrieves the files or a specific file in a folder * @response.representation.200.doc The list of files or the file * @response.representation.200.qname {http://www.example.com}linkVOes * @param path the path to the folder * @param uriInfo The uri infos * @param request The REST request * @return */ @GET @Path("{path:.*}") @Produces({"*/*", MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_HTML, MediaType.APPLICATION_OCTET_STREAM}) public Response listFiles(@PathParam("path") List<PathSegment> path, @Context UriInfo uriInfo, @Context Request request) { return get(path, uriInfo, request); } /** * This retrieves some metadata of a specific file in a folder * The metadata are: filename, size, date of last modification, MIME-type and file href for downloading via REST * @response.representation.200.doc The metadata of the file * @response.representation.200.qname {http://www.example.com}fileMetadataVO * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_FILE_METADATA} * @param path the path to the file * @param uriInfo The uri infos * @return */ @GET @Path("metadata/{path:.*}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response getFileMetadata(@PathParam("path") List<PathSegment> path, @Context UriInfo uriInfo) { return getFMetadata(path, uriInfo); } /** * Upload a file to the root folder or create a new folder. One of the two sets * of parameters must be set: foldername to create * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @return The link to the created file */ @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response postFileToRoot(@Context UriInfo uriInfo, @Context HttpServletRequest request) { return addFileToFolder(uriInfo, Collections.<PathSegment>emptyList(), request); } /** * Upload a file to the root folder or create a new folder. One of the two sets * of parameters must be set: foldername to create * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (encoded with Base64) * @param uriInfo The uri infos * @return The link to the created file */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response postFile64ToRoot(@FormParam("foldername") String foldername, @FormParam("filename") String filename, @FormParam("file") String file, @Context UriInfo uriInfo) { byte[] fileAsBytes = Base64.decodeBase64(file); InputStream in = new ByteArrayInputStream(fileAsBytes); return putFile(foldername, filename, in, uriInfo, Collections.<PathSegment>emptyList()); } /** * Upload a file to the specified folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @param path The path to the folder * @return The link to the created file */ @POST @Path("{path:.*}") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces({"*/*", MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response postFileToFolder(@Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path, @Context HttpServletRequest request) { return addFileToFolder(uriInfo, path, request); } /** * Upload a file to the specified folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (encoded with Base64) * @param uriInfo The uri infos * @param path The path to the folder * @return The link to the created file */ @POST @Path("{path:.*}") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces({"*/*", MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response postFile64ToFolder(@FormParam("foldername") String foldername, @FormParam("filename") String filename, @FormParam("file") String file, @Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path) { byte[] fileAsBytes = Base64.decodeBase64(file); InputStream in = new ByteArrayInputStream(fileAsBytes); return putFile(foldername, filename, in, uriInfo, path); } /** * Upload a file to the root folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @return The link to the created file */ @PUT @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFileToRoot(@Context UriInfo uriInfo, @Context HttpServletRequest request) { return addFileToFolder(uriInfo, Collections.<PathSegment>emptyList(), request); } /** * Upload a file to the root folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @return The link to the created file */ @PUT @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFile64VOToRoot(File64VO file, @Context UriInfo uriInfo) { byte[] fileAsBytes = Base64.decodeBase64(file.getFile()); InputStream in = new ByteArrayInputStream(fileAsBytes); return putFile(null, file.getFilename(), in, uriInfo, Collections.<PathSegment>emptyList()); } /** * Upload a file to the specified folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @param path The path to the folder * @return The link to the created file */ @PUT @Path("{path:.*}") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces({"*/*", MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFileToFolder(@Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path, @Context HttpServletRequest request) { return addFileToFolder(uriInfo, path, request); } private Response addFileToFolder(UriInfo uriInfo, List<PathSegment> path, HttpServletRequest request) { InputStream in = null; MultipartReader partsReader = null; try { partsReader = new MultipartReader(request); File tmpFile = partsReader.getFile(); if(tmpFile != null) { in = new FileInputStream(tmpFile); } String filename = partsReader.getValue("filename"); String foldername = partsReader.getValue("foldername"); return putFile(foldername, filename, in, uriInfo, path); } catch (FileNotFoundException e) { log.error("", e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { MultipartReader.closeQuietly(partsReader); IOUtils.closeQuietly(in); } } /** * Upload a file to the specified folder or create a new folder * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param foldername The name of the new folder (optional) * @param filename The name of the file (optional) * @param file The content of the file (optional) * @param uriInfo The uri infos * @param path The path to the folder * @return The link to the created file */ @PUT @Path("{path:.*}") @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFile64ToFolder(File64VO file, @Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path) { byte[] fileAsBytes = Base64.decodeBase64(file.getFile()); InputStream in = new ByteArrayInputStream(fileAsBytes); return putFile(null, file.getFilename(), in, uriInfo, path); } /** * Create folders * @response.representation.200.doc The link to the created file * @response.representation.200.qname {http://www.example.com}linkVO * @param uriInfo The uri infos * @param path The path to the folder * @return The link to the created file */ @PUT @Path("{path:.*}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFolders(@Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path) { return createFolders(uriInfo, path); } @DELETE @Path("{path:.*}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response deleteItem(@PathParam("path") List<PathSegment> path) { if(container.getLocalSecurityCallback() != null && !container.getLocalSecurityCallback().canDelete()) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } VFSItem item = resolveFile(path); if(item == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } VFSStatus status = item.delete(); if(status == VFSConstants.YES) { return Response.ok().build(); } //need something better return Response.ok().build(); } protected Response createFolders(UriInfo uriInfo, List<PathSegment> path) { if(container.getLocalSecurityCallback() != null && !container.getLocalSecurityCallback().canWrite()) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } VFSContainer directory = resolveContainer(path, true); if(directory == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } return Response.ok(createFileVO(directory, uriInfo)).build(); } protected Response putFile(String foldername, String filename, InputStream file, UriInfo uriInfo, List<PathSegment> path) { if(container.getLocalSecurityCallback() != null && !container.getLocalSecurityCallback().canWrite()) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } VFSContainer directory = resolveContainer(path, true); if(directory == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } if(filename == null && file == null) { //only create folders if(foldername != null) { VFSItem newFolder = directory.resolve(foldername); if(newFolder instanceof VFSContainer) { return Response.ok(createFileVO(newFolder, uriInfo)).build(); } else if (newFolder == null) { newFolder = directory.createChildContainer(foldername); if(newFolder != null) { return Response.ok(createFileVO(newFolder, uriInfo)).build(); } } } return Response.serverError().status(Status.CONFLICT).build(); } VFSItem newItem = directory.resolve(filename); VFSLeaf newFile; if(newItem == null) { newFile = directory.createChildLeaf(filename); } else if (newItem instanceof VFSLeaf) { newFile = (VFSLeaf)newItem; } else { return Response.serverError().status(Status.FORBIDDEN).build(); } try { OutputStream out = newFile.getOutputStream(false); IOUtils.copy(file, out); IOUtils.closeQuietly(out); IOUtils.closeQuietly(file); return Response.ok(createFileVO(newFile, uriInfo)).build(); } catch (IOException e) { return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } protected Response get(List<PathSegment> path, UriInfo uriInfo, Request request) { VFSItem vItem = resolveFile(path); if(vItem == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (vItem instanceof VFSContainer) { VFSContainer directory = (VFSContainer)vItem; List<VFSItem> items = directory.getItems(new SystemItemFilter()); int count=0; FileVO[] links = new FileVO[items.size()]; for(VFSItem item:items) { UriBuilder builder = uriInfo.getAbsolutePathBuilder(); String uri = builder.path(normalize(item.getName())).build().toString(); if(item instanceof VFSLeaf) { links[count++] = new FileVO("self", uri, item.getName(), ((VFSLeaf)item).getSize()); } else { links[count++] = new FileVO("self", uri, item.getName()); } } return Response.ok(links).build(); } else if (vItem instanceof VFSLeaf) { VFSLeaf leaf = (VFSLeaf)vItem; Date lastModified = new Date(leaf.getLastModified()); Response.ResponseBuilder response = request.evaluatePreconditions(lastModified); if(response == null) { String mimeType = WebappHelper.getMimeType(leaf.getName()); if (mimeType == null) { mimeType = MediaType.APPLICATION_OCTET_STREAM; } response = Response.ok(leaf.getInputStream(), mimeType).lastModified(lastModified).cacheControl(cc); } return response.build(); } return Response.serverError().status(Status.BAD_REQUEST).build(); } protected Response getFMetadata(List<PathSegment> path, UriInfo uriInfo) { VFSItem vItem = resolveFile(path); if(vItem == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (vItem instanceof VFSContainer) { return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); } else if (vItem instanceof VFSLeaf) { VFSLeaf leaf = (VFSLeaf)vItem; UriBuilder builder = uriInfo.getAbsolutePathBuilder(); String uri = builder.build().toString(); String[] uriArray = uri.split("metadata/"); uri = uriArray[0] + uriArray[1]; FileMetadataVO metaVo = new FileMetadataVO(uri, leaf); return Response.ok(metaVo).build(); } return Response.serverError().status(Status.BAD_REQUEST).build(); } protected VFSContainer resolveContainer(List<PathSegment> path, boolean create) { VFSContainer directory = container; boolean notFound = false; //remove trailing segment if a trailing / is used if(path.size() > 0 && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { path = path.subList(0, path.size() -1); } a_a: for(PathSegment seg:path) { String segPath = seg.getPath(); for(VFSItem item : directory.getItems(new SystemItemFilter())) { if(item instanceof VFSLeaf) { // } else if (item instanceof VFSContainer && normalize(item.getName()).equals(segPath)) { directory = (VFSContainer)item; continue a_a; } } if(create) { directory = directory.createChildContainer(segPath); } else if(path.get(path.size() - 1) == seg) { break a_a; } else { notFound = true; } } if(notFound) { return null; } return directory; } protected VFSItem resolveFile(List<PathSegment> path) { VFSContainer directory = container; VFSItem resolvedItem = directory; boolean notFound = false; //remove trailing segment if a trailing / is used if(path.size() > 0 && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { path = path.subList(0, path.size() -1); } a_a: for(PathSegment seg:path) { String segPath = seg.getPath(); for(VFSItem item : directory.getItems(new SystemItemFilter())) { if(normalize(item.getName()).equals(segPath)) { if(item instanceof VFSLeaf) { if(path.get(path.size() - 1) == seg) { resolvedItem = item; break a_a; } } else if (item instanceof VFSContainer) { resolvedItem = directory = (VFSContainer)item; continue a_a; } } } notFound = true; } if(notFound) { return null; } return resolvedItem; } public static FileVO createFileVO(VFSItem item, UriInfo uriInfo) { UriBuilder builder = uriInfo.getAbsolutePathBuilder(); String uri = builder.path(normalize(item.getName())).build().toString(); FileVO link = new FileVO("self", uri, item.getName()); if(item instanceof VFSLeaf) { link.setSize(((VFSLeaf)item).getSize()); } return link; } public static String normalize(String segment) { segment = segment.replace(" ", "_"); segment = Normalizer.normalize(segment, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""); return segment; } }