/*
* Copyright 2013-2017 the original author or authors.
*
* 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.cloudfoundry.util;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFilePermission;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Utilities for files
*/
public final class FileUtils {
private static final Integer DEFAULT_PERMISSIONS = 0744;
private static final Map<PosixFilePermission, Integer> PERMISSION_MODES = FluentMap.<PosixFilePermission, Integer>builder()
.entry(PosixFilePermission.OWNER_READ, 0400)
.entry(PosixFilePermission.OWNER_WRITE, 0200)
.entry(PosixFilePermission.OWNER_EXECUTE, 0100)
.entry(PosixFilePermission.GROUP_READ, 0040)
.entry(PosixFilePermission.GROUP_WRITE, 0020)
.entry(PosixFilePermission.GROUP_EXECUTE, 0010)
.entry(PosixFilePermission.OTHERS_READ, 0004)
.entry(PosixFilePermission.OTHERS_WRITE, 0002)
.entry(PosixFilePermission.OTHERS_EXECUTE, 0001)
.build();
private FileUtils() {
}
/**
* Compresses a candidate {@link Path} if it is a directory. Otherwise returns the original {@link Path}.
*
* @param candidate the candidate {@link Path} to compress
* @return the {@link Path} for a compressed artifact
*/
public static Mono<Path> compress(Path candidate) {
return compress(candidate, path -> true);
}
/**
* Compresses a candidate {@link Path} filtering out entries
*
* @param candidate the candidate {@link Path} to compress
* @param filter a filter applied to each path
* @return the {@link Path} for a compressed artifact
*/
public static Mono<Path> compress(Path candidate, Predicate<String> filter) {
return Mono
.defer(() -> {
try {
Path staging = Files.createTempFile(String.format("resource-matched-%s-", candidate.getFileName()), ".zip");
try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(staging.toFile())) {
if (Files.isDirectory(candidate)) {
compressFromDirectory(candidate, filter, out);
} else {
compressFromZip(candidate, filter, out);
}
}
return Mono.just(staging);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
})
.subscribeOn(Schedulers.elastic());
}
/**
* Get the relative path of an application
*
* @param root the root to relativize against
* @param path the path to relativize
* @return the relative path
*/
public static String getRelativePathName(Path root, Path path) {
Path relative = root.relativize(path);
return Files.isDirectory(path) && !relative.toString().endsWith("/") ? String.format("%s/", relative.toString()) : relative.toString();
}
/**
* Calculates the SHA-1 hash for a {@link Path}
*
* @param path the {@link Path} to calculate the hash for
* @return a {@link String} representation of the hash
*/
public static String hash(Path path) {
try (InputStream in = Files.newInputStream(path)) {
return hash(in);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
/**
* Calculates the SHA-1 hash for an {@link InputStream}
*
* @param in the {@link InputStream} to calculate the hash for
* @return {@link String} representation of the hash
*/
public static String hash(InputStream in) {
try {
MessageDigest digest = MessageDigest.getInstance("sha1");
ByteArrayPool.withByteArray(buffer -> {
try {
int length;
while ((length = in.read(buffer)) != -1) {
digest.update(buffer, 0, length);
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
});
return String.format("%040x", new BigInteger(1, digest.digest()));
} catch (NoSuchAlgorithmException e) {
throw Exceptions.propagate(e);
}
}
/**
* Calculates permissions for a {@link Path}
*
* @param path the {@link Path} to calculate the permissions for
* @return a {@link String} representation of the permissions
*/
public static String permissions(Path path) {
try {
return permissions(getUnixMode(path));
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
/**
* Calculates permissions for a UNIX mode
*
* @param mode the UNIX mode to calculate the permissions for
* @return a {@link String} representation of the permissions
*/
public static String permissions(int mode) {
return Integer.toOctalString(mode == 0 ? DEFAULT_PERMISSIONS : mode);
}
/**
* Calculates the size of a {@link Path}
*
* @param path the {@link Path} to calculate the size for
* @return the size
*/
public static int size(Path path) {
try {
return (int) Files.size(path);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
private static void compressFromDirectory(Path candidate, Predicate<String> filter, ZipArchiveOutputStream out) {
try (Stream<Path> contents = Files.walk(candidate)) {
contents
.filter(path -> {
try {
return !Files.isSameFile(candidate, path);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
})
.filter(path -> filter.test(getRelativePathName(candidate, path)))
.forEach(path -> {
try (InputStream in = Files.isDirectory(path) ? null : Files.newInputStream(path)) {
write(in, Files.getLastModifiedTime(path), getUnixMode(path), out, getRelativePathName(candidate, path));
} catch (IOException e) {
throw Exceptions.propagate(e);
}
});
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
private static void compressFromZip(Path candidate, Predicate<String> filter, ZipArchiveOutputStream out) {
try (ZipFile zipFile = new ZipFile(candidate.toFile())) {
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
if (filter.test(entry.getName())) {
try (InputStream in = zipFile.getInputStream(entry)) {
int mode = entry.getUnixMode();
write(in, entry.getLastModifiedTime(), mode == 0 ? DEFAULT_PERMISSIONS : mode, out, entry.getName());
}
}
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
private static int getUnixMode(Path path) throws IOException {
if (!isPosixFile(path)) {
return DEFAULT_PERMISSIONS;
}
return Files.getPosixFilePermissions(path).stream()
.mapToInt(PERMISSION_MODES::get)
.sum();
}
private static boolean isPosixFile(Path path) {
return path.getFileSystem().supportedFileAttributeViews().contains("posix");
}
private static void write(InputStream in, FileTime lastModifiedTime, int mode, ZipArchiveOutputStream out, String path) {
try {
ZipArchiveEntry entry = new ZipArchiveEntry(path);
entry.setUnixMode(mode);
entry.setLastModifiedTime(lastModifiedTime);
out.putArchiveEntry(entry);
if (in != null) {
ByteArrayPool.withByteArray(buffer -> {
try {
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
});
}
out.closeArchiveEntry();
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
}