/* * Copyright 2016 JBoss by Red Hat. * * 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 org.jboss.as.repository; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.CopyOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import org.jboss.as.repository.logging.DeploymentRepositoryLogger; /** * Utility class for manipulating Path. * @author Emmanuel Hugonnet (c) 2016 Red Hat, inc. */ public class PathUtil { /** * Copy a path recursively. * @param source a Path pointing to a file or a directory that must exist * @param target a Path pointing to a directory where the contents will be copied. * @param overwrite overwrite existing files - if set to false fails if the target file already exists. * @throws IOException */ public static void copyRecursively(final Path source, final Path target, boolean overwrite) throws IOException { final CopyOption[] options; if (overwrite) { options = new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING}; } else { options = new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES}; } Files.walkFileTree(source, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Files.copy(dir, target.resolve(source.relativize(dir)), options); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.copy(file, target.resolve(source.relativize(file)), options); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { DeploymentRepositoryLogger.ROOT_LOGGER.cannotCopyFile(exc, file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } }); } /** * Delete a path recursively, not throwing Exception if it fails or if the path is null. * @param path a Path pointing to a file or a directory that may not exists anymore. */ public static void deleteSilentlyRecursively(final Path path) { if (path != null) { try { deleteRecursively(path); } catch (IOException ioex) { DeploymentRepositoryLogger.ROOT_LOGGER.cannotDeleteFile(ioex, path); } } } /** * Delete a path recursively. * @param path a Path pointing to a file or a directory that may not exists anymore. * @throws IOException */ public static void deleteRecursively(final Path path) throws IOException { DeploymentRepositoryLogger.ROOT_LOGGER.debugf("Deleting %s recursively", path); if (Files.exists(path)) { Files.walkFileTree(path, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { DeploymentRepositoryLogger.ROOT_LOGGER.cannotDeleteFile(exc, path); throw exc; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } } /** * Resolve a path from the rootPath checking that it doesn't go out of the rootPath. * @param rootPath the starting point for resolution. * @param path the path we want to resolve. * @return the resolved path. * @throws IllegalArgumentException if the resolved path is out of the rootPath or if the resolution failed. */ public static final Path resolveSecurely(Path rootPath, String path) { Path resolvedPath; if(path == null || path.isEmpty()) { resolvedPath = rootPath.normalize(); } else { String relativePath = removeSuperflousSlashes(path); resolvedPath = rootPath.resolve(relativePath).normalize(); } if(!resolvedPath.startsWith(rootPath)) { throw DeploymentRepositoryLogger.ROOT_LOGGER.forbiddenPath(path); } return resolvedPath; } private static String removeSuperflousSlashes(String path) { if (path.startsWith("/")) { return removeSuperflousSlashes(path.substring(1)); } return path; } /** * Test if the target path is an archive. * @param path path to the file. * @return true if the path points to a zip file - false otherwise. * @throws IOException */ public static final boolean isArchive(Path path) throws IOException { if (Files.exists(path) && Files.isRegularFile(path)) { try (ZipFile zip = new ZipFile(path.toFile())){ return true; } catch (ZipException e) { return false; } } return false; } /** * Test if the target path is an archive. * @param path path to the file. * @return true if the path points to a zip file - false otherwise. * @throws IOException */ public static final boolean isArchive(InputStream in) throws IOException { if (in != null) { try (ZipInputStream zip = new ZipInputStream(in)){ return zip.getNextEntry() != null; } catch (ZipException e) { return false; } } return false; } /** * List files in a path according to the specified filter. * @param rootPath the path from which we are listing the files. * @param filter the filter to be applied. * @return the list of files / directory. * @throws IOException */ public static List<ContentRepositoryElement> listFiles(final Path rootPath, Path tempDir, final ContentFilter filter) throws IOException { List<ContentRepositoryElement> result = new ArrayList<>(); if (Files.exists(rootPath)) { if(isArchive(rootPath)) { return listZipContent(rootPath, filter); } Files.walkFileTree(rootPath, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (filter.acceptFile(rootPath, file)) { result.add(ContentRepositoryElement.createFile(formatPath(rootPath.relativize(file)), Files.size(file))); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (filter.acceptDirectory(rootPath, dir)) { String directoryPath = formatDirectoryPath(rootPath.relativize(dir)); if(! "/".equals(directoryPath)) { result.add(ContentRepositoryElement.createFolder(directoryPath)); } } return FileVisitResult.CONTINUE; } private String formatDirectoryPath(Path path) { return formatPath(path) + '/'; } private String formatPath(Path path) { return path.toString().replace(File.separatorChar, '/'); } }); } else { Path file = getFile(rootPath); if(isArchive(file)) { Path relativePath = file.relativize(rootPath); Path target = createTempDirectory(tempDir, "unarchive"); unzip(file, target); return listFiles(target.resolve(relativePath), tempDir, filter); } else { throw new FileNotFoundException(rootPath.toString()); } } return result; } private static List<ContentRepositoryElement> listZipContent(final Path zipFilePath, final ContentFilter filter) throws IOException { List<ContentRepositoryElement> result = new ArrayList<>(); try (final ZipFile zip = new ZipFile(zipFilePath.toFile())) { final Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { final ZipEntry entry = entries.nextElement(); final String name = entry.getName(); Path entryPath = zipFilePath.resolve(name); if (entry.isDirectory()) { if(filter.acceptDirectory(zipFilePath, entryPath)) { result.add(ContentRepositoryElement.createFolder(name)); } } else { try (InputStream in = zip.getInputStream(entry)) { if (filter.acceptFile(zipFilePath, entryPath, in)) { result.add(ContentRepositoryElement.createFile(name, entry.getSize())); } } } } } return result; } private static List<FileAttribute<Set<PosixFilePermission>>> getPosixAttributes(Path file) throws IOException { if (Files.exists(file) && Files.getFileStore(file).supportsFileAttributeView(PosixFileAttributeView.class)) { PosixFileAttributeView posixView = Files.getFileAttributeView(file, PosixFileAttributeView.class); if (posixView != null) { return Collections.singletonList(PosixFilePermissions.asFileAttribute(posixView.readAttributes().permissions())); } } return Collections.emptyList(); } private static List<FileAttribute<List<AclEntry>>> getAclAttributes(Path file) throws IOException { if (Files.exists(file) && Files.getFileStore(file).supportsFileAttributeView(AclFileAttributeView.class)) { AclFileAttributeView aclView = Files.getFileAttributeView(file, AclFileAttributeView.class); if (aclView != null) { final List<AclEntry> entries = aclView.getAcl(); return Collections.singletonList(new FileAttribute<List<AclEntry>>() { @Override public List<AclEntry> value() { return entries; } @Override public String name() { return "acl:acl"; } }); } } return Collections.emptyList(); } /** * Create a temporary directory with the same attributes as its parent directory. * @param dir * the path to directory in which to create the directory * @param prefix * the prefix string to be used in generating the directory's name; * may be {@code null} * @return the path to the newly created directory that did not exist before * this method was invoked * @throws IOException */ public static Path createTempDirectory(Path dir, String prefix) throws IOException { try { return Files.createTempDirectory(dir, prefix); } catch (UnsupportedOperationException ex) { } return Files.createTempDirectory(dir, prefix); } /** * Unzip a file to a target directory. * @param zip the path to the zip file. * @param target the path to the target directory into which the zip file will be unzipped. * @throws IOException */ public static void unzip(Path zip, Path target) throws IOException { try (final ZipFile zipFile = new ZipFile(zip.toFile())){ unzip(zipFile, target); } } private static void unzip(final ZipFile zip, final Path targetDir) throws IOException { final Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { final ZipEntry entry = entries.nextElement(); final String name = entry.getName(); final Path current = targetDir.resolve(name); if (entry.isDirectory()) { if (!Files.exists(current)) { Files.createDirectories(current); } } else { if (Files.notExists(current.getParent())) { Files.createDirectories(current.getParent()); } try (final InputStream eis = zip.getInputStream(entry)) { Files.copy(eis, current); } } try { Files.getFileAttributeView(current, BasicFileAttributeView.class).setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); } catch (IOException e) { //ignore, if we cannot set it, world will not end } } } public static String getFileExtension(Path path) { String fileName = path.getFileName().toString(); int separator = fileName.lastIndexOf('.'); if(separator > 0) { return fileName.substring(separator); } return ""; } public static Path readFile(Path src, Path tempDir) throws IOException { if(isFile(src)) { return src; } else { Path file = getFile(src); if(isArchive(file)) { Path relativePath = file.relativize(src); Path target = createTempDirectory(tempDir, "unarchive"); unzip(file, target); return readFile(target.resolve(relativePath), tempDir); } else { throw new FileNotFoundException(src.toString()); } } } private static Path getFile(Path src) throws FileNotFoundException { if (src.getNameCount() > 1) { Path parent = src.getParent(); if (isFile(parent)) { return parent; } return getFile(parent); } throw new FileNotFoundException(src.toString()); } private static boolean isFile(Path src) { return Files.exists(src) && Files.isRegularFile(src); } }