planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/util/FileUtils.java

258 wiersze
8.3 KiB
Java
Czysty Zwykły widok Historia

package com.onthegomap.planetiler.util;
2021-05-01 20:40:44 +00:00
import java.io.IOException;
2022-07-31 11:17:42 +00:00
import java.io.InputStream;
2021-09-10 00:46:20 +00:00
import java.io.UncheckedIOException;
import java.nio.file.FileStore;
2021-05-01 20:40:44 +00:00
import java.nio.file.FileSystem;
import java.nio.file.Files;
2021-05-04 12:02:22 +00:00
import java.nio.file.NoSuchFileException;
2021-05-01 20:40:44 +00:00
import java.nio.file.Path;
2022-07-31 11:17:42 +00:00
import java.nio.file.StandardOpenOption;
2021-05-04 10:17:10 +00:00
import java.util.Comparator;
2022-07-31 11:17:42 +00:00
import java.util.Objects;
2021-05-01 20:40:44 +00:00
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
2022-07-31 11:17:42 +00:00
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
2021-09-10 00:46:20 +00:00
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
2021-05-01 20:40:44 +00:00
2021-09-10 00:46:20 +00:00
/**
* Convenience methods for working with files on disk.
*/
2021-05-01 20:40:44 +00:00
public class FileUtils {
2022-07-31 11:17:42 +00:00
private static final Format FORMAT = Format.defaultInstance();
// Prevent zip-bomb attack, see https://rules.sonarsource.com/java/RSPEC-5042
private static final int ZIP_THRESHOLD_ENTRIES = 10_000;
private static final int ZIP_THRESHOLD_SIZE = 1_000_000_000;
private static final double ZIP_THRESHOLD_RATIO = 1_000;
2021-05-01 20:40:44 +00:00
2021-09-10 00:46:20 +00:00
private static final Logger LOGGER = LoggerFactory.getLogger(FileUtils.class);
private FileUtils() {}
2021-05-01 20:40:44 +00:00
2021-09-10 00:46:20 +00:00
/** Returns a stream that lists all files in {@code fileSystem}. */
2021-05-01 20:40:44 +00:00
public static Stream<Path> walkFileSystem(FileSystem fileSystem) {
return StreamSupport.stream(fileSystem.getRootDirectories().spliterator(), false)
.flatMap(rootDirectory -> {
try {
return Files.walk(rootDirectory);
} catch (IOException e) {
2021-09-10 00:46:20 +00:00
LOGGER.error("Unable to walk " + rootDirectory + " in " + fileSystem, e);
return Stream.empty();
2021-05-01 20:40:44 +00:00
}
});
}
2021-09-10 00:46:20 +00:00
/** Returns true if {@code path} ends with ".extension" (case-insensitive). */
2021-05-01 20:40:44 +00:00
public static boolean hasExtension(Path path, String extension) {
return path.toString().toLowerCase().endsWith("." + extension.toLowerCase());
}
2021-05-04 10:17:10 +00:00
2021-09-10 00:46:20 +00:00
/** Returns the size of {@code path} as a file, or 0 if missing/inaccessible. */
2021-05-04 10:17:10 +00:00
public static long fileSize(Path path) {
try {
return Files.size(path);
} catch (IOException e) {
return 0;
}
}
2021-09-10 00:46:20 +00:00
/** Returns the directory usage of all files until {@code path} or 0 if missing/inaccessible. */
2021-05-04 10:17:10 +00:00
public static long directorySize(Path path) {
2022-04-23 09:58:49 +00:00
try (var walker = Files.walk(path)) {
return walker
2021-05-04 10:17:10 +00:00
.filter(Files::isRegularFile)
.mapToLong(FileUtils::fileSize)
.sum();
} catch (IOException e) {
return 0;
}
}
2021-09-10 00:46:20 +00:00
/** Returns the size of a directory or file at {@code path} or 0 if missing/inaccessible. */
2021-05-04 10:17:10 +00:00
public static long size(Path path) {
return Files.isDirectory(path) ? directorySize(path) : fileSize(path);
}
2021-09-10 00:46:20 +00:00
/** Deletes a file if it exists or fails silently if it doesn't. */
2021-05-04 10:17:10 +00:00
public static void deleteFile(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
2021-09-10 00:46:20 +00:00
LOGGER.error("Unable to delete " + path, e);
2021-05-04 10:17:10 +00:00
}
}
2021-09-10 00:46:20 +00:00
/** Deletes all files under a directory and fails silently if it doesn't exist. */
2021-05-04 10:17:10 +00:00
public static void deleteDirectory(Path path) {
2022-04-23 09:58:49 +00:00
try (var walker = Files.walk(path)) {
walker
2021-05-04 10:17:10 +00:00
.sorted(Comparator.reverseOrder())
.forEach(FileUtils::deleteFile);
2021-05-04 12:02:22 +00:00
} catch (NoSuchFileException e) {
// this is OK, file doesn't exist, so can't walk
2021-05-04 10:17:10 +00:00
} catch (IOException e) {
2021-09-10 00:46:20 +00:00
LOGGER.error("Unable to delete " + path, e);
2021-05-04 10:17:10 +00:00
}
}
/** Deletes files or directories recursively, failing silently if missing. */
public static void delete(Path... paths) {
for (Path path : paths) {
if (Files.isDirectory(path)) {
deleteDirectory(path);
} else {
deleteFile(path);
}
2021-05-04 10:17:10 +00:00
}
}
2021-08-05 01:22:20 +00:00
/** Returns the {@link FileStore} for {@code path}, or its nearest parent directory if it does not exist yet. */
public static FileStore getFileStore(final Path path) {
Path absolutePath = path.toAbsolutePath();
IOException exception = null;
while (absolutePath != null) {
try {
return Files.getFileStore(absolutePath);
} catch (IOException e) {
exception = e;
absolutePath = absolutePath.getParent();
}
}
throw new UncheckedIOException("Cannot get file store for " + path, exception);
}
2021-09-10 00:46:20 +00:00
/**
* Moves a file.
*
* @throws UncheckedIOException if an error occurs
*/
public static void move(Path from, Path to) {
try {
Files.move(from, to);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Ensures a directory and all parent directories exists.
*
* @throws IllegalStateException if an error occurs
*/
public static void createDirectory(Path path) {
try {
Files.createDirectories(path);
} catch (IOException e) {
throw new IllegalStateException("Unable to create directories " + path, e);
}
}
2021-09-10 00:46:20 +00:00
/**
* Ensures all parent directories of each path in {@code paths} exist.
2021-09-10 00:46:20 +00:00
*
* @throws IllegalStateException if an error occurs
*/
public static void createParentDirectories(Path... paths) {
for (var path : paths) {
try {
if (Files.isDirectory(path) && !Files.exists(path)) {
Files.createDirectories(path);
} else {
Path parent = path.getParent();
if (parent != null && !Files.exists(parent)) {
Files.createDirectories(parent);
}
}
} catch (IOException e) {
throw new IllegalStateException("Unable to create parent directories " + path, e);
2021-08-05 01:22:20 +00:00
}
}
}
2021-09-10 00:46:20 +00:00
/**
* Attempts to delete the file located at {@code path} on normal JVM exit.
*/
public static void deleteOnExit(Path path) {
path.toFile().deleteOnExit();
}
2022-07-31 11:17:42 +00:00
/**
* Unzips a zip file on the classpath to {@code destDir}.
*
* @throws UncheckedIOException if an IO exception occurs
*/
public static void unzipResource(String resource, Path dest) {
try (var is = FileUtils.class.getResourceAsStream(resource)) {
Objects.requireNonNull(is, "Resource not found on classpath: " + resource);
unzip(is, dest);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Unzips a zip file from an input stream to {@code destDir}.
*
* @throws UncheckedIOException if an IO exception occurs
*/
public static void unzip(InputStream input, Path destDir) {
int totalSizeArchive = 0;
int totalEntryArchive = 0;
try (var zip = new ZipInputStream(input)) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
Path targetDirResolved = destDir.resolve(entry.getName());
Path destination = targetDirResolved.normalize();
if (!destination.startsWith(destDir)) {
throw new IOException("Bad zip entry: " + entry.getName());
}
if (entry.isDirectory()) {
FileUtils.createDirectory(destDir);
} else {
createParentDirectories(destination);
// Instead of Files.copy, read 2kB at a time to prevent zip bomb attack, see https://rules.sonarsource.com/java/RSPEC-5042
int nBytes;
byte[] buffer = new byte[2048];
int totalSizeEntry = 0;
try (
var out = Files.newOutputStream(destination, StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE)
) {
totalEntryArchive++;
while ((nBytes = zip.read(buffer)) > 0) {
out.write(buffer, 0, nBytes);
totalSizeEntry += nBytes;
totalSizeArchive += nBytes;
double compressionRatio = totalSizeEntry * 1d / entry.getCompressedSize();
if (compressionRatio > ZIP_THRESHOLD_RATIO) {
throw new IOException(
"Ratio between compressed and uncompressed data is highly suspicious " +
FORMAT.numeric(compressionRatio) +
"x, looks like a Zip Bomb Attack");
}
}
if (totalSizeArchive > ZIP_THRESHOLD_SIZE) {
throw new IOException("The uncompressed data size " + FORMAT.storage(totalSizeArchive) +
"B is too much for the application resource capacity");
}
if (totalEntryArchive > ZIP_THRESHOLD_ENTRIES) {
throw new IOException("Too much entries in this archive " + FORMAT.integer(totalEntryArchive) +
", can lead to inodes exhaustion of the system");
}
}
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
2021-05-01 20:40:44 +00:00
}