org.conqat.lib.commons.filesystem.FileSystemUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* 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.conqat.lib.commons.filesystem;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Collections.reverseOrder;
import static java.util.stream.Collectors.toList;
import static org.conqat.lib.commons.date.DateTimeUtils.now;
import static org.conqat.lib.commons.string.StringUtils.bytesToString;
import static org.conqat.lib.commons.string.StringUtils.concat;
import static org.conqat.lib.commons.string.StringUtils.splitLinesAsList;
import static org.conqat.lib.commons.string.StringUtils.stripSuffix;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.CopyOption;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.IllegalFormatException;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.CaseInsensitiveStringSet;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.collections.PairList;
import org.conqat.lib.commons.concurrent.ThreadUtils;
import org.conqat.lib.commons.function.SupplierWithException;
import org.conqat.lib.commons.string.StringUtils;
/** File system utilities. */
public class FileSystemUtils {
/** Encoding for UTF-8. */
public static final String UTF8_ENCODING = StandardCharsets.UTF_8.name();
/**
* The {@link Charset} used by the underlying operating system.
*
* Starting with Java 18, this value may differ from {@link Charset#defaultCharset()}, which will
* default to {@link StandardCharsets#UTF_8 UTF-8} instead of the OS charset.
*/
public static final Charset SYSTEM_CHARSET = Charset
.forName(System.getProperty("native.encoding", Charset.defaultCharset().name()));
/** The path to the directory used by Java to store temporary files */
public static final String TEMP_DIR_PATH = System.getProperty("java.io.tmpdir");
/** Unix file path separator */
public static final char UNIX_SEPARATOR = '/';
/** Windows file path separator */
private static final char WINDOWS_SEPARATOR = '\\';
/**
* String containing the unit letters for units in the metric system (K for kilo, M for mega, ...).
* Ordered by their power value (the order is important).
*/
private static final String METRIC_SYSTEM_UNITS = "KMGTPEZY";
/**
* Pattern matching the start of the data-size unit in a data-size string (the first non-space char
* not belonging to the numeric value).
*/
private static final Pattern DATA_SIZE_UNIT_START_PATTERN = Pattern.compile("[^\\d\\s.,]");
/**
* Names of path segments that are reserved in certain operating systems and hence may not be used.
*
* @see Why
* can't I create a folder with name ...
*/
private static final Set RESERVED_PATH_SEGMENT_NAMES = new CaseInsensitiveStringSet(
Arrays.asList("CON", "PRN", "AUX", "CLOCK$", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
"COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"));
/**
* Copy an input stream to an output stream. This does not close the streams.
*
* @param input
* input stream
* @param output
* output stream
* @return number of bytes copied
* @throws IOException
* if an IO exception occurs.
*/
public static int copy(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[1024];
int size = 0;
int len;
while ((len = input.read(buffer)) > 0) {
output.write(buffer, 0, len);
size += len;
}
return size;
}
/**
* Copy a file. This creates all necessary directories.
*
* @deprecated Use {@link #copyFile(Path, Path)} instead.
*/
@Deprecated
public static void copyFile(File sourceFile, File targetFile) throws IOException {
copyFile(sourceFile.toPath(), targetFile.toPath());
}
/** Copy a file. This creates all necessary directories. */
public static void copyFile(Path sourceFile, Path targetFile) throws IOException {
if (sourceFile.toAbsolutePath().equals(targetFile.toAbsolutePath())) {
throw new IOException("Can not copy file onto itself: " + sourceFile);
}
ensureParentDirectoryExists(targetFile);
Files.copy(sourceFile, targetFile);
}
/**
* Copy all files specified by a file filter from one directory to another. This automatically
* creates all necessary directories.
*
* @param fileFilter
* filter to specify file types. If all files should be copied, use
* {@link FileOnlyFilter}.
* @return number of files copied
*
* @deprecated Use {@link Files#walk} with {@link Stream#filter} and {@link #copyFile(Path, Path)}
* instead.
*/
@Deprecated
public static int copyFiles(File sourceDirectory, File targetDirectory, FileFilter fileFilter) throws IOException {
List files = FileSystemUtils.listFilesRecursively(sourceDirectory, fileFilter);
int fileCount = 0;
for (File sourceFile : files) {
if (sourceFile.isFile()) {
String path = sourceFile.getAbsolutePath();
int index = sourceDirectory.getAbsolutePath().length();
String newPath = path.substring(index);
File targetFile = new File(targetDirectory, newPath);
copyFile(sourceFile, targetFile);
fileCount++;
}
}
return fileCount;
}
/**
* Recursively delete directories and files.
*
* @param directory
* Directory to delete.
* @throws IllegalArgumentException
* if {@code directory == null}.
*
* @deprecated Use {@link #deleteRecursively(Path)} instead.
*/
@Deprecated
public static void deleteRecursively(File directory) throws IOException {
checkArgument(directory != null, String.format("Parameter %s is null!", "directory"));
deleteRecursively(directory.toPath());
}
/**
* Recursively delete directories and files.
*
* @param directory
* Directory to delete. Does nothing if {@code directory == null}.
*
* @throws IOException
* when a {@link Files#delete} operation fails. This will immediately abort deletion,
* hence the operation might leave the partial contents of a directory behind if this
* exception occurred.
*/
public static void deleteRecursively(@Nullable Path directory) throws IOException {
if (directory == null) {
return;
}
if (!Files.exists(directory)) {
return;
}
try (Stream walk = Files.walk(directory)) {
for (Iterator it = walk.sorted(reverseOrder()).iterator(); it.hasNext();) {
Files.delete(it.next());
}
}
}
/**
* Deletes the given file and throws an exception if this fails. Does nothing if the given file
* doesn't exist.
*
* @see Files#delete(Path)
*
* @deprecated Use {@link Files#delete(Path)} instead.
*/
@Deprecated
public static void deleteFile(File file) throws IOException {
if (!file.exists()) {
return;
}
try {
Files.delete(file.toPath());
} catch (IOException e) {
throw new IOException("Could not delete " + file, e);
}
}
/**
* Renames the given file and throws an exception if this fails.
*
* @deprecated Use {@link Files#move(Path, Path, CopyOption...)} instead.
*/
@Deprecated
public static void renameFileTo(File file, File dest) throws IOException {
try {
Files.move(file.toPath(), dest.toPath());
} catch (Exception e) {
// Catching all exceptions here since we want to convert any exception to an
// IOException. Listing all possible exceptions is too cumbersome and
// error-prone.
throw new IOException("Could not rename " + file + " to " + dest, e);
}
}
/**
* Creates a directory and throws an exception if this fails.
*
* @see File#mkdir()
*
* @deprecated Use {@link Files#createDirectory(Path, FileAttribute[])} instead.
*/
@Deprecated
public static void mkdir(File dir) throws IOException {
if (!dir.mkdir()) {
throw new IOException("Could not create directory " + dir);
}
}
/**
* Creates a directory and all required parent directories. Throws an exception if this fails.
*
* @see File#mkdirs()
*
* @deprecated Use {@link Files#createDirectories(Path, FileAttribute[])} instead.
*/
@Deprecated
public static void mkdirs(File dir) throws IOException {
if (dir.exists() && dir.isDirectory()) {
// mkdirs will return false if the directory already exists, but in
// that case we don't want to throw an exception
return;
}
if (!dir.mkdirs()) {
throw new IOException("Could not create directory " + dir);
}
}
/**
* Checks if a directory exists and is writable. If not it creates the directory and all necessary
* parent directories.
*
* @throws IOException
* if directories couldn't be created.
*
* @deprecated Use {@link #ensureDirectoryExists(Path)} instead.
*/
@Deprecated
public static void ensureDirectoryExists(File directory) throws IOException {
ensureDirectoryExists(directory.toPath());
}
/**
* Checks if a directory exists and is writable. If not it creates the directory and all necessary
* parent directories.
*
* @throws IOException
* if directories couldn't be created.
*/
public static void ensureDirectoryExists(Path directory) throws IOException {
if (!Files.exists(directory)) {
Files.createDirectories(directory);
}
if (Files.exists(directory) && Files.isWritable(directory)) {
return;
}
// Something is wrong. Either the directory does not exist yet, or it is not
// writable (yet?). We had a case on a Windows OS where the directory was not
// writable in a very small fraction of the calls. We assume this was because
// the directory was not "ready" yet although createDirectories returned.
Instant start = now();
while ((!Files.exists(directory) || !Files.isWritable(directory))
&& start.until(now(), ChronoUnit.MILLIS) < 100) {
ThreadUtils.sleep(10);
}
if (!Files.exists(directory)) {
throw new IOException("Directory '" + directory + "' could not be created.");
}
if (!Files.isWritable(directory)) {
throw new IOException("Directory '" + directory + "' exists, but is not writable.");
}
}
/**
* Checks if the parent directory of a file exists. If not it creates the directory and all
* necessary parent directories.
*
* @throws IOException
* if directories couldn't be created.
*
* @deprecated Use {@link #ensureParentDirectoryExists(Path)} instead.
*/
@Deprecated
public static void ensureParentDirectoryExists(File file) throws IOException {
ensureParentDirectoryExists(file.toPath());
}
/**
* Checks if the parent directory of a file exists. If not it creates the directory and all
* necessary parent directories.
*
* @throws IOException
* if directories couldn't be created.
*/
public static void ensureParentDirectoryExists(Path file) throws IOException {
checkArgument(file != null, String.format("Parameter %s must not be null.", "file"));
ensureDirectoryExists(file.toAbsolutePath().getParent());
}
/**
* Returns a list of all files and directories contained in the given directory and all
* subdirectories. The given directory itself is not included in the result.
*
* This method knows nothing about (symbolic and hard) links, so care should be taken when
* traversing directories containing recursive links.
*
* @param directory
* the directory to start the search from.
* @return the list of files found (the order is determined by the file system).
*
* @deprecated Use {@link Files#walk(Path, FileVisitOption...)} instead.
*/
@Deprecated
public static List listFilesRecursively(File directory) {
return listFilesRecursively(directory, null);
}
/**
* Returns a list of all files and directories contained in the given directory and all
* subdirectories matching the filter provided. The given directory itself is not included in the
* result.
*
* The file filter may or may not exclude directories.
*
* This method knows nothing about (symbolic and hard) links, so care should be taken when
* traversing directories containing recursive links.
*
* @param directory
* the directory to start the search from. If this is null or the directory does not
* exist, an empty list is returned.
* @param filter
* the filter used to determine whether the result should be included. If the filter is
* null, all files and directories are included.
* @return the list of files found (the order is determined by the file system). Whether the paths
* are relative or absolute is determined by the given directory path {@link File#listFiles}
*
* @deprecated Use {@link Files#walk(Path, FileVisitOption...)} with {@link Stream#filter} instead.
*/
@Deprecated
public static List listFilesRecursively(File directory, FileFilter filter) {
if (directory == null || !directory.isDirectory()) {
return CollectionUtils.emptyList();
}
List result = new ArrayList<>();
listFilesRecursively(directory, result, filter);
return result;
}
/**
* Finds all files and directories contained in the given directory and all subdirectories matching
* the filter provided and put them into the result collection. The given directory itself is not
* included in the result.
*
* This method knows nothing about (symbolic and hard) links, so care should be taken when
* traversing directories containing recursive links.
*
* @param directory
* the directory to start the search from.
* @param result
* the collection to add to all files found.
* @param filter
* the filter used to determine whether the result should be included. If the filter is
* null, all files and directories are included.
*/
private static void listFilesRecursively(File directory, Collection result, FileFilter filter) {
File[] files = directory.listFiles();
if (files == null) {
// From the docs of `listFiles`: "If this abstract pathname does not denote a
// directory, then this method returns null."
// It seems to be ok to just return here without throwing an exception.
return;
}
for (File file : files) {
if (file.isDirectory()) {
listFilesRecursively(file, result, filter);
}
if (filter == null || filter.accept(file)) {
result.add(file);
}
}
}
/** @see #listFilesInSameLocationForURL(URL, boolean) */
public static List listFilesInSameLocationForURL(URL baseUrl) throws IOException {
return listFilesInSameLocationForURL(baseUrl, false);
}
/**
* Lists the names of all simple files (i.e. no directories) next to an URL. For example for a file,
* this would return the names of all files in the same directory (including the file itself).
* Currently, this supports both file and jar URLs. The intended use-case is to list a set of files
* in a package via the class loader.
*/
public static List listFilesInSameLocationForURL(URL baseUrl, boolean includeSubfolders)
throws IOException {
String protocol = baseUrl.getProtocol();
if ("file".equals(protocol)) {
return listFilesForFileURL(baseUrl, includeSubfolders);
}
if ("jar".equals(protocol)) {
return listFilesForJarURL(baseUrl, includeSubfolders);
}
throw new IOException("Unsupported protocol: " + protocol);
}
/**
* Returns the parent path within the jar for a class file url. E.g. for the URL
* "jar:file:/path/to/file.jar!/sub/folder/File.class" the method returns "sub/folder/". If the url
* does already point to a directory it returns the path of this directory.
*/
private static String getJarUrlParentDirectoryPrefix(URL baseUrl) {
// in JAR URLs we can rely on the separator being a slash
String parentPath = StringUtils.getLastPart(baseUrl.getPath(), '!');
parentPath = StringUtils.stripPrefix(parentPath, "/");
if (parentPath.endsWith(".class")) {
parentPath = stripSuffix(parentPath, StringUtils.getLastPart(parentPath, UNIX_SEPARATOR));
} else {
parentPath = StringUtils.ensureEndsWith(parentPath, String.valueOf(UNIX_SEPARATOR));
}
return parentPath;
}
/**
* Lists the names of files for a JAR URL. If the recursive flag is set, all files within the same
* jar directory hierarchy will be listed, otherwise only the ones in the same directory.
*/
private static List listFilesForJarURL(URL baseUrl, boolean recursive) throws IOException {
try (JarFile jarFile = new JarFile(FileSystemUtils.extractJarFileFromJarURL(baseUrl))) {
String parentPath = getJarUrlParentDirectoryPrefix(baseUrl);
return jarFile.stream().filter(entry -> shouldBeContainedInResult(entry, parentPath, recursive))
.map(entry -> StringUtils.stripPrefix(entry.getName(), parentPath)).collect(toList());
}
}
/**
* @return whether the jar entry should be returned when searching for files contained in the given
* path.
*/
private static boolean shouldBeContainedInResult(JarEntry entry, String path, boolean recursive) {
if (entry.isDirectory()) {
return false;
}
String simpleName = StringUtils.getLastPart(entry.getName(), UNIX_SEPARATOR);
String entryPath = stripSuffix(entry.getName(), simpleName);
return !recursive && entryPath.equals(path) || (recursive && entryPath.startsWith(path));
}
/**
* Lists the names of files (not including directories) within the given file URL. This will only
* include subfolder (recursively) if the includeSubfolders flag is set.
*
* @return a list of relative, separator-normalized file paths without slash prefix, e.g.
* ['foo.java', 'subfolder/bar.java']
*/
private static List listFilesForFileURL(URL baseUrl, boolean includeSubfolders) throws IOException {
try {
File directory = new File(baseUrl.toURI());
if (!directory.isDirectory()) {
directory = directory.getParentFile();
}
if (directory == null || !directory.isDirectory()) {
throw new IOException("Parent directory does not exist or is not readable for " + baseUrl);
}
if (includeSubfolders) {
File finalDirectory = directory;
return CollectionUtils.filterAndMap(listFilesRecursively(directory), File::isFile, file -> {
String relativeFilePath = StringUtils.stripPrefix(file.getAbsolutePath(),
finalDirectory.getAbsolutePath());
relativeFilePath = FileSystemUtils.normalizeSeparators(relativeFilePath);
return StringUtils.stripPrefix(relativeFilePath, String.valueOf(UNIX_SEPARATOR));
});
}
File[] files = directory.listFiles();
if (files == null) {
throw new IOException("Failed to list files for directory '" + directory.toPath()
+ "', even though it was supposed to be a valid directory.");
}
List names = new ArrayList<>();
for (File file : files) {
if (file.isFile()) {
names.add(file.getName());
}
}
return names;
} catch (URISyntaxException e) {
throw new IOException("Could not convert URL to valid file: " + baseUrl, e);
}
}
/**
* Extract all top-level classes in the given JAR and returns a list of their fully qualified class
* names. Inner classes are ignored.
*/
public static List listTopLevelClassesInJarFile(File jarFile) throws IOException {
List result = new ArrayList<>();
try (PathBasedContentProviderBase provider = PathBasedContentProviderBase.createProvider(jarFile)) {
Collection paths = provider.getPaths();
for (String path : paths) {
if (path.endsWith(ClassPathUtils.CLASS_FILE_SUFFIX) && !path.contains("$")) {
String fqn = StringUtils.removeLastPart(path, '.');
fqn = fqn.replace(UNIX_SEPARATOR, '.');
result.add(fqn);
}
}
return result;
}
}
/**
* Returns the extension of the file.
*
* @return File extension, i.e. "java" for "FileSystemUtils.java", or {@code null}, if the file has
* no extension (i.e. if a filename contains no '.'), returns the empty string if the '.' is
* the filename's last character.
*/
public static @Nullable String getFileExtension(Path file) {
return getFileExtension(file.toString());
}
/**
* Returns the extension of the file.
*
* @return File extension, i.e. "java" for "FileSystemUtils.java", or {@code null}, if the file has
* no extension (i.e. if a filename contains no '.'), returns the empty string if the '.' is
* the filename's last character.
*
* @deprecated Use {@link #getFileExtension(Path)} instead.
*/
@Deprecated
public static @Nullable String getFileExtension(File file) {
return getFileExtension(file.getName());
}
/** Returns the extension of the file at the given path. */
public static String getFileExtension(String path) {
int posLastDot = path.lastIndexOf('.');
if (posLastDot < 0) {
return null;
}
return path.substring(posLastDot + 1);
}
/**
* @return the name of the given file without extension. Example: '/home/joe/data.dat' -> 'data'.
*
* @deprecated Use {@link #getFilenameWithoutExtension(Path)} instead.
*/
@Deprecated
public static String getFilenameWithoutExtension(File file) {
return getFilenameWithoutExtension(file.getName());
}
/**
* @return the name of the given file without extension. Example: '/home/joe/data.dat' -> 'data'.
*/
public static String getFilenameWithoutExtension(Path file) {
return getFilenameWithoutExtension(file.getFileName().toString());
}
/**
* @return the name of the given file without extension. Example: 'data.dat' -> 'data'.
*/
public static String getFilenameWithoutExtension(String filename) {
return StringUtils.removeLastPart(filename, '.');
}
/**
* @return the last path segment (i.e. file name or folder name) of a unix file path (assumes unix
* separators).
*/
public static String getLastPathSegment(String filePath) {
String[] split = getPathSegments(filePath);
return split[split.length - 1];
}
/** @return the segments of a path (assuming unix separators). */
public static String[] getPathSegments(String filePath) {
return FileSystemUtils.normalizeSeparators(filePath).split(String.valueOf(UNIX_SEPARATOR));
}
/**
* @return whether a string is a valid path. It will return false when the path is invalid on the
* current platform e.g. because of any non-allowed characters or because the path schema is
* for Windows (D:\test) but runs under Linux.
*/
public static boolean isValidPath(String path) {
try {
Paths.get(path);
} catch (InvalidPathException ex) {
return false;
}
// Split at the default platform separators and check whether there remain any
// separator characters in the path segments
return Arrays.stream(path.split(Pattern.quote(File.separator)))
.noneMatch(pathSegment -> pathSegment.contains(String.valueOf(FileSystemUtils.WINDOWS_SEPARATOR))
|| pathSegment.contains(String.valueOf(FileSystemUtils.UNIX_SEPARATOR)));
}
/**
* Checks whether the given folder can be written to, based on {@link Files#isWritable(Path)}. If
* the based File object is a directory, the check will be performed on the object itself. If a file
* is passed instead, its first parent that is a directory will be used for the
* {@link Files#isWritable} check.
*/
// Inverting this method would make it less intuitive to read.
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isPathWriteable(Path file) {
Path folderToCheck = file;
if (!Files.isDirectory(file)) {
folderToCheck = file.toAbsolutePath().getParent();
}
while (folderToCheck != null && !Files.isDirectory(folderToCheck)) {
folderToCheck = folderToCheck.getParent();
}
return folderToCheck != null && Files.isWritable(folderToCheck);
}
/**
* Read file content into a string using the default encoding for the platform. If the file starts
* with a UTF byte order mark (BOM), the encoding is ignored and the correct encoding based on this
* BOM is used for reading the file.
*
* @see EByteOrderMark
*/
public static String readFile(Path file) throws IOException {
return readFile(file, Charset.defaultCharset());
}
/**
* Read file content into a string using the default encoding for the platform. If the file starts
* with a UTF byte order mark (BOM), the encoding is ignored and the correct encoding based on this
* BOM is used for reading the file.
*
* @see EByteOrderMark
*
* @deprecated Use {@link #readFile(Path)} instead.
*/
@Deprecated
public static String readFile(File file) throws IOException {
return readFile(file, Charset.defaultCharset());
}
/**
* Read file content into a string using UTF-8 encoding. If the file starts with a UTF byte order
* mark (BOM), the encoding is ignored and the correct encoding based on this BOM is used for
* reading the file.
*
* @see EByteOrderMark
*/
public static String readFileUTF8(Path file) throws IOException {
return readFile(file, StandardCharsets.UTF_8);
}
/**
* Read file content into a string using UTF-8 encoding. If the file starts with a UTF byte order
* mark (BOM), the encoding is ignored and the correct encoding based on this BOM is used for
* reading the file.
*
* @see EByteOrderMark
*
* @deprecated Use {@link #readFileUTF8(Path)} instead.
*/
@Deprecated
public static String readFileUTF8(File file) throws IOException {
return readFile(file, StandardCharsets.UTF_8);
}
/**
* Read file content into a string using the given encoding. If the file starts with a UTF byte
* order mark (BOM), the encoding is ignored and the correct encoding based on this BOM is used for
* reading the file.
*
* @see EByteOrderMark
*
* @deprecated Use {@link #readFile(Path)} instead.
*/
@Deprecated
public static String readFile(File file, Charset encoding) throws IOException {
byte[] buffer = readFileBinary(file);
return bytesToString(buffer, encoding);
}
/**
* Read file content into a string using the given encoding. If the file starts with a UTF byte
* order mark (BOM), the encoding is ignored and the correct encoding based on this BOM is used for
* reading the file.
*
* @see EByteOrderMark
*/
public static String readFile(Path file, Charset encoding) throws IOException {
byte[] buffer = Files.readAllBytes(file);
return bytesToString(buffer, encoding);
}
/**
* Read file content into a list of lines (strings) using the given encoding. This uses automatic
* BOM handling, just as {@link #readFile(Path)}.
*/
public static List readLines(Path file, Charset encoding) throws IOException {
return splitLinesAsList(readFile(file, encoding));
}
/**
* Read file content into a list of lines (strings) using the given encoding. This uses automatic
* BOM handling, just as {@link #readFile(Path)}.
*
* @deprecated Use {@link #readLines(Path, Charset)} instead.
*/
@Deprecated
public static List readLines(File file, Charset encoding) throws IOException {
return readLines(file.toPath(), encoding);
}
/**
* Read file content into a list of lines (strings) using UTF-8 encoding. This uses automatic BOM
* handling, just as {@link #readFile(Path)}.
*/
public static List readLinesUTF8(Path file) throws IOException {
return readLines(file, StandardCharsets.UTF_8);
}
/**
* Read file content into a list of lines (strings) using UTF-8 encoding. This uses automatic BOM
* handling, just as {@link #readFile(File)}.
*
* @deprecated Use {@link #readLinesUTF8(Path)} instead.
*/
@Deprecated
public static List readLinesUTF8(File file) throws IOException {
return readLinesUTF8(file.toPath());
}
/** Read file content into a byte array. */
public static byte[] readFileBinary(String filePath) throws IOException {
return Files.readAllBytes(Paths.get(filePath));
}
/**
* Read file content into a byte array.
*
* @deprecated Use {@link Files#readAllBytes(Path)} or {@link #readFileBinary(String)} instead.
*/
@Deprecated
public static byte[] readFileBinary(File file) throws IOException {
FileInputStream in = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
FileChannel channel = in.getChannel();
try {
int readSum = 0;
while (readSum < buffer.length) {
int read = channel.read(byteBuffer);
if (read < 0) {
throw new IOException("Reached EOF before entire file could be read!");
}
readSum += read;
}
} finally {
close(channel);
close(in);
}
return buffer;
}
/** Extract a JAR file to a directory. */
public static void unjar(File jarFile, File targetDirectory) throws IOException {
unzip(jarFile, targetDirectory);
}
/** Extract a ZIP file to a directory. */
public static void unzip(File zipFile, File targetDirectory) throws IOException {
unzip(zipFile, targetDirectory, null, null);
}
/**
* Extract the entries of ZIP file to a directory.
*
* @param prefix
* Sets a prefix for the entry names (paths) which should be extracted. Only entries
* which start with the given prefix are extracted. If prefix is null
or
* empty all entries are extracted. The prefix will be stripped form the extracted
* entries.
* @param charset
* defines the {@link Charset} of the ZIP file. If null
, the standard of
* {@link ZipFile} is used (which is UTF-8).
* @return list of the extracted paths
*/
public static List unzip(File zipFile, File targetDirectory, String prefix, Charset charset)
throws IOException {
ZipFile zip = null;
try {
if (charset == null) {
zip = new ZipFile(zipFile);
} else {
zip = new ZipFile(zipFile, charset);
}
return unzip(zip, targetDirectory, prefix);
} finally {
close(zip);
}
}
/**
* Extract entries of a ZipFile to a directory when the ZipFile is created externally. Note that
* this does not close the ZipFile, so the caller has to take care of this.
*/
public static List unzip(ZipFile zip, File targetDirectory, String prefix) throws IOException {
Enumeration entries = zip.getEntries();
List extractedPaths = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String filename = entry.getName();
if (!StringUtils.isEmpty(prefix)) {
if (!filename.startsWith(prefix)) {
continue;
}
filename = StringUtils.stripPrefix(filename, prefix);
}
try (InputStream entryStream = zip.getInputStream(entry)) {
File file = new File(targetDirectory, filename);
ensureParentDirectoryExists(file);
try (FileOutputStream outputStream = new FileOutputStream(file)) {
copy(entryStream, outputStream);
}
}
extractedPaths.add(filename);
}
return extractedPaths;
}
/**
* Extract entries of a zip file input stream to a directory. The input stream is automatically
* closed by this method.
*/
public static List unzip(InputStream inputStream, File targetDirectory) throws IOException {
List extractedPaths = new ArrayList<>();
try (ZipInputStream zipStream = new ZipInputStream(inputStream)) {
while (true) {
ZipEntry entry = zipStream.getNextEntry();
if (entry == null) {
break;
} else if (entry.isDirectory()) {
continue;
}
String filename = entry.getName();
File file = new File(targetDirectory, filename);
ensureParentDirectoryExists(file);
try (OutputStream targetStream = Files.newOutputStream(file.toPath())) {
copy(zipStream, targetStream);
}
extractedPaths.add(filename);
}
}
return extractedPaths;
}
/**
* Write string to a file with the default encoding. This ensures all directories exist.
*/
public static void writeFile(Path file, String content) throws IOException {
writeFile(file, content, Charset.defaultCharset().name());
}
/**
* Write string to a file with the default encoding. This ensures all directories exist.
*
* @deprecated Use {@link #writeFile(Path, String)} instead.
*/
@Deprecated
public static void writeFile(File file, String content) throws IOException {
writeFile(file, content, Charset.defaultCharset().name());
}
/**
* Writes the given collection of String as lines into the specified file. This method uses \n as a
* line separator.
*/
public static void writeLines(Path file, Collection lines) throws IOException {
writeFile(file, concat(lines, "\n"));
}
/**
* Writes the given collection of String as lines into the specified file. This method uses \n as a
* line separator.
*
* @deprecated Use {@link #writeLines(Path, Collection)} instead.
*/
@Deprecated
public static void writeLines(File file, Collection lines) throws IOException {
writeLines(file.toPath(), lines);
}
/**
* Write string to a file with UTF8 encoding. This ensures all directories exist.
*
* @deprecated Use {@link #writeFileUTF8(Path, String)} instead.
*/
@Deprecated
public static void writeFileUTF8(File file, String content) throws IOException {
writeFile(file, content, UTF8_ENCODING);
}
/**
* Write string to a file with UTF8 encoding. This ensures all directories exist.
*/
public static void writeFileUTF8(Path file, String content) throws IOException {
writeFile(file, content, UTF8_ENCODING);
}
/** Write string to a file. This ensures all directories exist. */
public static void writeFile(Path file, String content, String encoding) throws IOException {
ensureParentDirectoryExists(file);
try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(file), encoding)) {
writer.write(content);
}
}
/**
* Write string to a file. This ensures all directories exist.
*
* @deprecated Use {@link #writeFile(Path, String, String)} instead.
*/
@Deprecated
public static void writeFile(File file, String content, String encoding) throws IOException {
writeFile(file.toPath(), content, encoding);
}
/**
* Writes the given bytes to the given file. Directories are created as needed. The file is closed
* after writing.
*/
public static void writeFileBinary(Path file, byte[] bytes) throws IOException {
ensureParentDirectoryExists(file);
Files.write(file, bytes);
}
/**
* Writes the given bytes to the given file. Directories are created as needed. The file is closed
* after writing.
*
* @deprecated Use {@link #writeFileBinary(Path, byte[])} instead.
*/
@Deprecated
public static void writeFileBinary(File file, byte[] bytes) throws IOException {
writeFileBinary(file.toPath(), bytes);
}
/**
* Loads template file with a
* Format
* string, formats it and writes result to file.
*
* @param templateFile
* the template file with the format string
* @param outFile
* the target file, parent directories are created automatically.
* @param arguments
* the formatting arguments.
* @throws IOException
* if an IO exception occurs or the template file defines an illegal format.
*/
public static void mergeTemplate(File templateFile, File outFile, Object... arguments) throws IOException {
String template = readFile(templateFile);
String output;
try {
output = String.format(template, arguments);
} catch (IllegalFormatException e) {
throw new IOException("Illegal format: " + e.getMessage(), e);
}
writeFile(outFile, output);
}
/**
* Loads template file with a
* Format
* string, formats it and provides result as stream. No streams are closed by this method.
*
* @param inStream
* stream that provides the template format string
* @param arguments
* the formatting arguments.
* @throws IOException
* if an IOException occurs or the template file defines an illegal format.
*/
public static InputStream mergeTemplate(InputStream inStream, Object... arguments) throws IOException {
String template = readStream(inStream);
String output;
try {
output = String.format(template, arguments);
} catch (IllegalFormatException e) {
throw new IOException("Illegal format: " + e.getMessage(), e);
}
return new ByteArrayInputStream(output.getBytes());
}
/** Read input stream into string. */
public static String readStream(InputStream input) throws IOException {
return readStream(input, Charset.defaultCharset());
}
/** Read input stream into string. */
public static String readStreamUTF8(InputStream input) throws IOException {
return readStream(input, StandardCharsets.UTF_8);
}
/**
* Read input stream into string. This method is BOM aware, i.e. deals with the UTF-BOM.
*/
public static String readStream(InputStream input, Charset encoding) throws IOException {
StringBuilder out = new StringBuilder();
Reader r = streamReader(input, encoding);
char[] b = new char[4096];
int n;
while ((n = r.read(b)) != -1) {
out.append(b, 0, n);
}
return out.toString();
}
/** Read input stream into raw byte array. */
public static byte[] readStreamBinary(InputStream input) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(input, out);
return out.toByteArray();
}
/**
* Returns a reader that wraps the given input stream. This method handles the BOM transparently. As
* the normal reader constructors can not deal with this, direct construction of readers is
* discouraged.
*/
public static Reader streamReader(InputStream in, Charset encoding) throws IOException {
// we need marking to read the BOM mark
if (!in.markSupported()) {
in = new BufferedInputStream(in);
}
in.mark(EByteOrderMark.MAX_BOM_LENGTH);
byte[] prefix = new byte[EByteOrderMark.MAX_BOM_LENGTH];
EByteOrderMark bom = null;
try {
safeRead(in, prefix);
bom = EByteOrderMark.determineBOM(prefix).orElse(null);
} catch (IOException e) {
// just use provided encoding; keep BOM as null
}
in.reset();
if (bom != null) {
encoding = bom.getEncoding();
// consume BOM
for (int i = 0; i < bom.getBOMLength(); ++i) {
int ignored = in.read();
}
}
return new InputStreamReader(in, encoding);
}
/** Reads properties from a properties file. */
public static Properties readProperties(File propertiesFile) throws IOException {
return readProperties(() -> Files.newInputStream(propertiesFile.toPath()));
}
/** Reads properties from a properties stream. */
private static Properties readProperties(SupplierWithException streamSupplier)
throws IOException {
try (InputStream stream = streamSupplier.get()) {
Properties props = new Properties();
props.load(stream);
return props;
}
}
/**
* Determines the root directory from a collection of files. The root directory is the lowest common
* ancestor directory of the files in the directory tree.
*
* This method does not require the input files to exist.
*
* @param files
* Collection of files for which root directory gets determined. This collection is
* required to contain at least 2 files. If it does not, an AssertionError is thrown.
* @return Root directory, or null, if the files do not have a common root directory.
* @throws AssertionError
* If less than two different files are provided whereas fully qualified canonical names
* are used for comparison.
* @throws IOException
* Since canonical paths are used for determination of the common root, and
* {@link File#getCanonicalPath()} can throw {@link IOException}s.
*/
public static File commonRoot(Iterable files) throws IOException {
// determine longest common prefix on canonical absolute paths
Set absolutePaths = new HashSet<>();
for (File file : files) {
absolutePaths.add(file.getCanonicalPath());
}
CCSMAssert.isTrue(absolutePaths.size() >= 2, "Expected are at least 2 files");
String longestCommonPrefix = StringUtils.longestCommonPrefix(absolutePaths);
// trim to name of root directory (remove possible equal filename
// prefixes.)
int lastSeparator = longestCommonPrefix.lastIndexOf(File.separator);
if (lastSeparator > -1) {
longestCommonPrefix = longestCommonPrefix.substring(0, lastSeparator);
}
if (StringUtils.isEmpty(longestCommonPrefix)) {
return null;
}
return new File(longestCommonPrefix);
}
/**
* Transparently creates a stream for decompression if the provided stream is compressed. Otherwise,
* the stream is just handed through. Currently, only GZIP via {@link GZIPInputStream} is supported.
*/
public static InputStream autoDecompressStream(InputStream in) throws IOException {
if (!in.markSupported()) {
in = new BufferedInputStream(in);
}
in.mark(2);
// check first two bytes for GZIP header
boolean isGZIP = (in.read() & 0xff | (in.read() & 0xff) << 8) == GZIPInputStream.GZIP_MAGIC;
in.reset();
if (isGZIP) {
return new GZIPInputStream(in);
}
return in;
}
/**
* Closes the given ZIP file quietly, i.e. ignoring a potential IOException. Additionally, it is
* {@code null} safe.
*/
public static void close(ZipFile zipFile) {
if (zipFile == null) {
return;
}
try {
zipFile.close();
} catch (IOException e) {
// ignore
}
}
/**
* This method can be used to simplify the typical {@code finally}-block of code dealing with
* streams and readers/writers. It checks if the provided closeable is {@code null}. If not it
* closes it. If an exception is thrown during the close operation it will be ignored.
*/
public static void close(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}
/** Compares files based on the lexical order of their fully qualified names. */
public static void sort(List files) {
files.sort(new FilenameComparator());
}
/**
* Replace platform dependent separator char (of the operating system running the Teamscale
* instance) with forward slashes to create system-independent paths.
*
* Be careful when using this method on strings coming from project source code. This will be wrong
* if the source code contains (for example) windows paths like {@code "..\foo.h"} and the Teamscale
* instance runs on a unix machine.
*/
public static String normalizeSeparators(String path) {
return path.replace(File.separatorChar, UNIX_SEPARATOR);
}
/**
* @return a path normalized by replacing all occurrences of Windows back-slash separators (if
* present) with unix forward-slash separators. This is in contrast to
* {@link #normalizeSeparators(String)} that replaces all platform-dependent separators.
*/
public static String normalizeSeparatorsPlatformIndependently(String path) {
if (path.contains(String.valueOf(WINDOWS_SEPARATOR)) && !path.contains(String.valueOf(UNIX_SEPARATOR))) {
return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
}
return path;
}
/**
* Returns the JAR file for a URL with protocol 'jar'. If the protocol is not 'jar' an assertion
* error will be caused! An assertion error is also thrown if URL does not point to a file.
*/
public static File extractJarFileFromJarURL(URL url) {
CCSMAssert.isTrue("jar".equals(url.getProtocol()), "May only be used with 'jar' URLs!");
String path = url.getPath();
CCSMAssert.isTrue(path.startsWith("file:"), "May only be used for URLs pointing to files");
// the exclamation mark is the separator between jar file and path
// within the file
int index = path.indexOf('!');
CCSMAssert.isTrue(index >= 0, "Unknown format for jar URLs");
path = path.substring(0, index);
return fromURL(path);
}
/**
* Often file URLs are created the wrong way, i.e. without proper escaping characters invalid in
* URLs. Unfortunately, the URL class allows this and the Eclipse framework does it. See
* How to
* convert java.net.url to java.io.file for details.
*
* This method attempts to fix this problem and create a file from it.
*
* @throws AssertionError
* if cleaning up fails.
*
* @implNote We cannot simply encode the URL this also encodes slashes and other stuff. As a result,
* the file constructor throws an exception. As a simple heuristic, we only fix the
* spaces. The other route to go would be manually stripping of "file:" and simply
* creating a file. However, this does not work if the URL was created properly and
* contains URL escapes.
*/
private static File fromURL(String url) {
url = url.replace(StringUtils.SPACE, "%20");
try {
return new File(new URI(url));
} catch (URISyntaxException e) {
throw new AssertionError("The assumption is that this method is capable of "
+ "working with non-standard-compliant URLs, too. " + "Apparently it is not. Invalid URL: " + url
+ ". Ex: " + e.getMessage(), e);
}
}
/**
* Returns whether a filename represents an absolute path.
*
* This method returns the same result, independent on which operating system it gets executed. In
* contrast, the behavior of {@link File#isAbsolute()} is operating system specific.
*/
public static boolean isAbsolutePath(String filename) {
// Unix and macOS: absolute path starts with slash or user home
if (filename.startsWith("/") || filename.startsWith("~")) {
return true;
}
// Windows and OS/2: absolute path start with letter and colon
if (filename.length() > 2 && Character.isLetter(filename.charAt(0)) && filename.charAt(1) == ':') {
return true;
}
// UNC paths (aka network shares): start with double backslash
return filename.startsWith("\\\\");
}
/**
* Reads bytes of data from the input stream into an array of bytes until the array is full. This
* method blocks until input data is available, end of file is detected, or an exception is thrown.
*
* The reason for this method is that {@link InputStream#read(byte[])} may read less than the
* requested number of bytes, while this method ensures the data is complete.
*
* @param in
* the stream to read from.
* @param data
* the stream to read from.
* @throws IOException
* if reading the underlying stream causes an exception.
* @throws EOFException
* if the end of file was reached before the requested data was read.
*/
public static void safeRead(InputStream in, byte[] data) throws IOException {
int offset = 0;
int length = data.length;
while (length > 0) {
int read = in.read(data, offset, length);
if (read < 0) {
throw new EOFException("Reached end of file before completing read.");
}
offset += read;
length -= read;
}
}
/** Obtains the system's temporary directory */
public static File getTmpDir() {
return new File(TEMP_DIR_PATH);
}
/** Obtains the current user's home directory */
public static File getUserHomeDir() {
return new File(System.getProperty("user.home"));
}
/**
* Obtains the current working directory. This is usually the directory in which the current Java
* process was started. In dev-mode (including unit tests), the temp directory is used.
*/
public static File getJvmWorkingDirOrTempForDevMode() {
if (isDevModeOrJunitTest()) {
return getTmpDir();
}
return new File(System.getProperty("user.dir"));
}
/** Returns if dev-mode is active or a JUnit test is executed. */
public static boolean isDevModeOrJunitTest() {
return Boolean.getBoolean("com.teamscale.dev-mode") || isJUnitTest();
}
private static boolean isJUnitTest() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTrace) {
if (element.getClassName().startsWith("org.junit.")) {
return true;
}
}
return false;
}
/**
* @return whether the given {@code files} are non-null, plain files, and readable.
*/
// Inverting this method would make it less intuitive to read.
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isReadableFile(File... files) {
return Arrays.stream(files)
.noneMatch(file -> file == null || !file.exists() || !file.isFile() || !file.canRead());
}
/**
* Concatenates all path parts into a single path with normalized separators.
*/
public static String concatenatePaths(String firstParent, String... paths) {
return normalizeSeparators(Paths.get(firstParent, paths).toString());
}
/**
* Removes the given path if it is an empty directory and recursively parent directories if the only
* child was deleted.
*
* If the given path points to file which does not exist, the parent directory of that file is
* deleted.
*
* @throws IOException
* if an I/O error during deletion occurs.
*/
public static void recursivelyRemoveDirectoryIfEmpty(File path) throws IOException {
String[] children = path.list();
if (children == null) {
// path either points to a plain file or to a non-existent
// path. In the first case, nothing should be done otherwise
// deletion should continue with the parent path.
if (path.exists()) {
return;
}
} else if (children.length == 0) {
deleteFile(path);
} else {
return;
}
recursivelyRemoveDirectoryIfEmpty(path.getParentFile());
}
/**
* Converts the given human-readable data size to the corresponding number of bytes. For example "1
* KB" is converted to 1024. Also supports Si units ("1 KiB" is converted to 1000).
*
* Commas are ignored and can be used as thousands separator. A dot is the decimal separator.
* ("1.2KiB" is converted to 1200).
*
* Method implementation based on this stackoverflow
* answer
*/
public static long parseDataSize(String dataSize) {
String dataSizeWithoutComma = dataSize.replaceAll(",", "");
int unitBeginIndex = StringUtils.indexOfMatch(dataSizeWithoutComma, DATA_SIZE_UNIT_START_PATTERN);
if (unitBeginIndex == -1) {
return Long.parseLong(dataSizeWithoutComma);
}
double rawDataSize = Double.parseDouble(dataSizeWithoutComma.substring(0, unitBeginIndex));
String unitString = dataSizeWithoutComma.substring(unitBeginIndex);
int unitChar = unitString.charAt(0);
int power = METRIC_SYSTEM_UNITS.indexOf(unitChar) + 1;
boolean isSi = unitString.length() >= 2 && unitString.charAt(1) == 'i';
int factor = 1024;
if (isSi) {
factor = 1000;
if (stripSuffix(unitString, "B").length() != 2) {
throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma);
}
} else if (power == 0) {
if (!stripSuffix(unitString, "B").isEmpty()) {
throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma);
}
} else {
if (stripSuffix(unitString, "B").length() != 1) {
throw new NumberFormatException("Malformed data size: " + dataSizeWithoutComma);
}
}
return (long) (rawDataSize * Math.pow(factor, power));
}
/** Determines the last modified timestamp in a platform-agnostic way. */
public static long getLastModifiedTimestamp(File file) throws IOException {
return Files.getLastModifiedTime(Paths.get(file.toURI())).toMillis();
}
/**
* Returns a safe filename that can be used for downloads. Replaces everything that is not a letter
* or number with "-".
*
* Attention: This replaces dots, including the file-end-separator.
* {@code toSafeFilename("a.c")=="a-c"}
*/
public static String toSafeFilename(String name) {
name = name.replaceAll("\\W+", "-");
name = name.replaceAll("[-_]+", "-");
return name;
}
/**
* Returns a filename that replaces the Windows-specific illegal characters with "-". Less strict
* than {@link #toSafeFilename} as the goal is to ensure that it is possible to create description
* files for checks with illegal characters in their names.
*/
public static String toValidFileName(String name) {
return name.replaceAll("[:\\\\/*\"?|<>']", "-");
}
/**
* @return a new file with all file path segments that are reserved (windows) path names escaped.
*/
public static File escapeReservedFileNames(File file) {
String[] parts = file.getPath().split(Pattern.quote(File.separator));
for (int i = 0; i < parts.length; ++i) {
if (RESERVED_PATH_SEGMENT_NAMES.contains(parts[i])) {
parts[i] = "_" + parts[i];
}
}
return new File(concat(parts, File.separator));
}
/**
* Reads a file using UTF-8 encoding and normalizes line breaks (replacing "\n\r" and "\r" with
* "\n"). This generates an OS-independent view on a file. To ensure OS-independent test results,
* this method should be used in all tests to read files.
*/
public static String readFileSystemIndependent(File file) throws IOException {
return StringUtils.normalizeLineSeparatorsPlatformIndependent(readFileUTF8(file));
}
/**
* Replaces the file name of the given path with the given new extension. Returns the newFileName if
* the file denoted by the uniform path does not contain a '/'. This method assumes that folders are
* separated by '/' (uniform paths).
*
* Examples:
*
* - {@code replaceFilePathFilenameWith("xx", "yy")} returns {@code "yy"}
* - {@code replaceFilePathFilenameWith("xx/zz", "yy")} returns {@code "xx/yy"}
* - {@code replaceFilePathFilenameWith("xx/zz/", "yy")} returns {@code "xx/zz/yy"}
* - {@code replaceFilePathFilenameWith("", "yy")} returns {@code "yy"}
*
*/
public static String replaceFilePathFilenameWith(String uniformPath, String newFileName) {
int folderSepIndex = uniformPath.lastIndexOf('/');
if (uniformPath.endsWith("/")) {
return uniformPath + newFileName;
} else if (folderSepIndex == -1) {
return newFileName;
}
return uniformPath.substring(0, folderSepIndex) + "/" + newFileName;
}
/**
* Calculates the directory size of the provided folder (in bytes).
*
* The size is calculated by traversing each file in the directory and summing their size. Because
* of this, the result may be incorrect when the directory is modified concurrently (e.g. when files
* are deleted or moved).
*
* @return Computed directory size in bytes.
*
* @see #calculateDirectorySize(Path, Consumer)
*/
public static long calculateDirectorySize(@NonNull Path folder) {
return calculateDirectorySize(folder, null);
}
/**
* Calculates the directory size of the provided folder (in bytes).
*
* The size is calculated by traversing each file in the directory and summing their size. Because
* of this, the result may be incorrect when the directory is modified concurrently (e.g. when files
* are deleted or moved).
*
* Any file, for which the size could not be determined (e.g. because it was deleted mid-traversal)
* will be provided to the {@code failedFilesConsumer} together with the corresponding
* {@link IOException}.
*
* @return Computed directory size in bytes.
*
* @see #calculateDirectorySize(Path)
*/
public static long calculateDirectorySize(@NonNull Path folder,
@Nullable Consumer> failedFilesConsumer) {
CCSMAssert.isNotNull(folder, () -> String.format("Expected \"%s\" to be not null", "folder"));
if (!Files.isDirectory(folder)) {
throw new IllegalArgumentException(
String.format("Provided file \"%s\" is not a directory", folder.toAbsolutePath()));
}
LongAdder directorySize = new LongAdder();
NonThrowingFileVisitor visitor = new NonThrowingFileVisitor<>(file -> {
BasicFileAttributes fileAttributes = Files.readAttributes(file, BasicFileAttributes.class);
if (fileAttributes.isRegularFile()) {
directorySize.add(fileAttributes.size());
}
});
try {
Files.walkFileTree(folder, visitor);
} catch (IOException e) {
// Should never happen as we are using the NonThrowingFileVisitor
CCSMAssert.fail(String.format("Unexpected IOException occurred while calculating directory size of: %s",
folder.toAbsolutePath()), e);
}
if (failedFilesConsumer != null && !visitor.getFailedFiles().isEmpty()) {
failedFilesConsumer.accept(visitor.getFailedFiles());
}
return directorySize.longValue();
}
/**
* Checks whether a file name belongs to a file/folder that may have been creating when
* (re-)zipping.
*/
public static boolean isSystemFileName(String entryName) {
return entryName.startsWith("__MACOSX") || entryName.endsWith(".DS_Store") || entryName.endsWith("~");
}
/**
* Creates a {@link TemporaryDirectory} with the given {@code prefix}.
*
* @see Files#createTempDirectory(String, FileAttribute[])
* @see Runtime#addShutdownHook(Thread)
*/
public static TemporaryDirectory getTemporaryDirectory(String prefix) throws IOException {
return new TemporaryDirectory(Files.createTempDirectory(prefix), false);
}
/**
* Creates a {@link TemporaryDirectory} which will not be deleted on close, but on shutdown.
*
* @see Files#createTempDirectory(String, FileAttribute[])
* @see Runtime#addShutdownHook(Thread)
*/
public static TemporaryDirectory getTemporaryDirectoryDeletedOnShutdown(String prefix) throws IOException {
return new TemporaryDirectory(Files.createTempDirectory(prefix), true);
}
}