/* * Copyright 2013 Brian Rosenberger (Brutex Network) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.brutex.xservices.ws.rs; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.core.UriInfo; import net.brutex.xservices.security.DirectoryPermission; import net.brutex.xservices.types.FileInfoType; import net.brutex.xservices.util.FileWalker; import org.apache.jcs.JCS; import org.apache.jcs.access.exception.CacheException; import org.apache.log4j.Logger; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.UnauthorizedException; /** * The Class FileInfoImpl. * * @author Brian Rosenberger, bru(at)brutex.de */ public class FileInfoImpl implements FileInfo { Logger logger = Logger.getLogger(FileInfoImpl.class); /* (non-Javadoc) * @see net.brutex.xservices.ws.rs.FileInfo#getFiles(javax.ws.rs.core.HttpHeaders, java.lang.String, boolean, boolean, int, java.lang.String, int, int) */ public Response getFiles(HttpHeaders h, UriInfo uriInfo, String dir, boolean withDir, boolean withFiles, int level, String search, int count, int page, boolean useCache) { if(dir==null) { dir = "c:/"; logger.warn("No directory specified. Default is 'c:/'."); } isPermitted(dir); URI baseuri = URI.create(uriInfo.getBaseUri()+FileInfo.BASE_PATH+"getFile?file="); logger.info(String.format("Listing directory '%s'.", dir)); if (level <= 0) level = 1; if ((!withDir) && (!withFiles)) withFiles = true; String cachekey = level + "||" + withFiles + "||" + withDir + "||" + search + "||" + dir; try { logger.debug(String.format("Hitting cache with cachekey '%s'", cachekey)); JCS jcs = JCS.getInstance("FileCache"); /*Try to retrieve the file list from the cache*/ List list = (List)jcs.get(cachekey); if (list == null || !useCache) { list = setDirectory(baseuri, dir, withDir, withFiles, level, search); jcs.put(cachekey, list); logger.debug("Stored in Cache: " + list.toString()); } else { logger.debug("Got from Cache: " + list.toString()); } int fromIndex = 0; int toIndex = 0; fromIndex = (page - 1) * count; toIndex = page * count; if (toIndex > list.size()) toIndex = list.size(); if (fromIndex > toIndex) fromIndex = toIndex; GenericEntity> sublist = new GenericEntity>(list.subList(fromIndex, toIndex)) {}; logger.info(String.format("Returning items %s to %s from total of %s items in the list.", fromIndex, toIndex, list.size())); return Response.ok(sublist).build(); } catch (CacheException e) { return Response.serverError().build(); } } /** * Sets the directory. * * @param list the list * @param dir the dir * @param withDirectories the with directories * @param withFiles the with files * @param depth the depth * @param search the search */ private void setDirectory(final URI baseuri, final List list, File dir, boolean withDirectories, boolean withFiles, final int depth, String search) { if (depth <= 0) return; if(search==null || search.equals("") ) { search = "*"; logger.info("No search pattern supplied, using default '*'."); } FileWalker finder = new FileWalker(search); try { Files.walkFileTree(dir.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), depth, finder); logger.info("FileWalker returned '"+finder.getCount()+"' hits. '" + finder.getTotal() + "' files have been scanned."); List result = finder.getResult(); for(Path f : result) { if(! withDirectories) { if(f.toFile().isDirectory()) continue; } if(! withFiles) { if(f.toFile().isFile()) continue; } list.add(new FileInfoType(f, baseuri)); } } catch (IOException e2) { logger.error(e2.getMessage(), e2);; } } /** * Sets the directory. * * @param dir the dir * @param withDirectories the with directories * @param withFiles the with files * @param depth the depth * @param search the search * @return the list */ private List setDirectory(URI baseuri, String dir, boolean withDirectories, boolean withFiles, int depth, String search) { List list = new ArrayList(); setDirectory(baseuri, list, new File(dir), withDirectories, withFiles, depth, search); return list; } @Override public Response getFile(HttpHeaders paramHttpHeaders, String file) { isPermitted(file); try { Path path = FileSystems.getDefault().getPath(file); BasicFileAttributeView basicView = Files.getFileAttributeView(path, BasicFileAttributeView.class); BasicFileAttributes basic; basic = basicView.readAttributes(); //In case this is a directory //we zip it and return the zip stream if(basic.isDirectory()) return getDirectoryAsZip(path); MediaType mime = MediaType.APPLICATION_OCTET_STREAM_TYPE; try { mime = MediaType.valueOf(Files.probeContentType(path)); } catch (IllegalArgumentException | IOException e) { //In case we can not find the media type for some reason //the default assignment is taken, so we can //ignore this error. logger.debug(String.format("Could not probe media type for file '%s'. Default is '%s'", path.toString(), mime.getType()), e); } Response r = Response.ok(path.toFile(), mime).build(); String fileName = path.getFileName().toString(); if(mime == MediaType.APPLICATION_OCTET_STREAM_TYPE) r.getHeaders().add("Content-Disposition", "attachment; filename=\"" + fileName + "\""); return r; } catch (IOException e1) { // TODO Auto-generated catch block logger.error(e1.getMessage(), e1); return Response.serverError().build(); } } private Response getDirectoryAsZip(final Path path) { StreamingOutput output = new StreamingOutput() { @Override public void write(OutputStream os) throws IOException, WebApplicationException { ZipOutputStream zos = new ZipOutputStream(os); //read directory content (files only) try (DirectoryStream stream = Files.newDirectoryStream(path)) { for (Path file: stream) { //skip anything not being a file if(! file.toFile().isFile()) continue; //ZipEntry String filename = file.getFileName().toString(); ZipEntry ze = new ZipEntry(filename); zos.putNextEntry( ze ); //read a file and put it into the output stream FileInputStream fis = new FileInputStream(file.toFile()); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) > 0) { zos.write(buffer, 0, len); } zos.flush(); fis.close(); } zos.close(); } } }; Response r = Response.ok(output, MediaType.APPLICATION_OCTET_STREAM_TYPE).build(); String zipname = (path.getFileName()==null) ? "null.zip" : path.getFileName().toString()+".zip"; r.getHeaders().add("Content-Disposition", "attachment; filename=\"" + zipname + "\""); return r; } private boolean isPermitted(String dir) { if(! SecurityUtils.getSubject().isPermitted( new DirectoryPermission(dir))) { logger.warn(String.format("User '%s' does not have permission to access '%s'.",SecurityUtils.getSubject().getPrincipal(), dir )); throw new NotAuthorizedException(new UnauthorizedException("User does not have permission to access "+ dir)); } return true; } //http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java private static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } }