com.swirlds.common.io.utility.FileUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swirlds-common Show documentation
Show all versions of swirlds-common Show documentation
Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.
/*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* 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 com.swirlds.common.io.utility;
import static com.swirlds.common.io.utility.LegacyTemporaryFileBuilder.buildTemporaryDirectory;
import static com.swirlds.logging.legacy.LogMarker.EXCEPTION;
import static com.swirlds.logging.legacy.LogMarker.STATE_TO_DISK;
import static java.nio.file.Files.exists;
import com.swirlds.common.io.streams.MerkleDataOutputStream;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Utility methods for file operations.
*/
public final class FileUtils {
private static final Logger logger = LogManager.getLogger(FileUtils.class);
private FileUtils() {}
/**
* Syntactic sugar. Runs an operation and rethrows any {@link IOException}s as {@link UncheckedIOException}s.
*
* @param runnable an operation to run
*/
public static void rethrowIO(@NonNull final IORunnable runnable) {
try {
runnable.run();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Syntactic sugar. Runs an operation and rethrows any {@link IOException}s as {@link UncheckedIOException}s.
*
* @param supplier an operation that supplies a value
*/
public static @NonNull T rethrowIO(@NonNull final IOSupplier supplier) {
try {
return supplier.get();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Get an absolute path to the current working directory, i.e. ".".
*/
public static @NonNull Path getAbsolutePath() {
return getAbsolutePath(".");
}
/**
* Get an absolute path to a particular location described by a string, starting in the current working directory.
* For example, if the current execution directory is "/user/home" and this method is invoked with "foo", then a
* {@link Path} at "/user/home/foo" is returned. Resolves "~".
*
* @param pathDescription a description of the path, e.g. "foo", "/foobar", "foo/bar"
* @return an absolute Path to the requested location
*/
public static @NonNull Path getAbsolutePath(@NonNull final String pathDescription) {
final String expandedPath = pathDescription.replaceFirst("^~", System.getProperty("user.home"));
return FileSystems.getDefault().getPath(expandedPath).toAbsolutePath().normalize();
}
/**
* Get an absolute path to a particular location described by a string, starting in the current working directory.
* For example, if the current execution directory is "/user/home" and this method is invoked with "foo", then a
* {@link Path} at "/user/home/foo" is returned. Resolves "~".
*
* @param path a non-absolute path
* @return an absolute Path to the requested location
*/
public static @NonNull Path getAbsolutePath(@NonNull final Path path) {
return getAbsolutePath(path.toString());
}
/**
* Delete a directory and all files contained in this directory. If there is a problem deleting a file
* or subdirectory, no more files or directories are attempted to be deleted.
*
* @param directoryToBeDeleted the directory to be deleted
*/
public static void deleteDirectory(@NonNull final Path directoryToBeDeleted) throws IOException {
if (Files.isDirectory(directoryToBeDeleted)) {
try (final Stream list = Files.list(directoryToBeDeleted)) {
final Iterator children = list.iterator();
while (children.hasNext()) {
deleteDirectory(children.next());
}
}
Files.delete(directoryToBeDeleted);
} else {
Files.deleteIfExists(directoryToBeDeleted);
}
}
/**
* Similar to {@link #deleteDirectory(Path)} but with additional logging. If there is a problem deleting a file or
* subdirectory, no more files or directories are attempted to be deleted.
*
* @param directoryToBeDeleted the directory to be deleted
*/
public static void deleteDirectoryAndLog(@NonNull final Path directoryToBeDeleted) throws IOException {
logger.info(STATE_TO_DISK.getMarker(), "deleting directory {}", directoryToBeDeleted);
try {
deleteDirectory(directoryToBeDeleted);
} catch (final Exception e) {
logger.error(EXCEPTION.getMarker(), "failed to delete directory {}", directoryToBeDeleted);
throw e;
}
}
/**
* Create a shallow copy of a directory structure using hard links. Resulting directory structure will contain the
* same files. Modifying files in the new directory will also modify the corresponding files in the original
* directory. Deleting files in the new directory has no effect on the old directory. Adding files to the new
* directory has no effect on the old directory.
*
* @param source the directory structure to copy, is legal for this argument to be a regular file
* @param destination the location where the directory structure will be placed, assumed that no file or directory
* already exists in this location. Note that the root of the copied directory structure will be
* placed at this destination, and not in this destination.
* @throws UncheckedIOException if there is a problem hard linking the tree
*/
public static void hardLinkTree(@NonNull final Path source, @NonNull final Path destination) throws IOException {
if (!exists(source)) {
throw new IOException(source + " does not exist or can not be accessed");
}
if (exists(destination)) {
throw new IOException(destination + " already exists");
}
// Recreate the directory tree
try (final Stream files = Files.walk(source)) {
files.filter(Files::isDirectory).forEach((final Path originalDirectoryPath) -> {
final Path relativeDirectoryPath = source.relativize(originalDirectoryPath);
final Path newDirectoryPath = destination.resolve(relativeDirectoryPath);
try {
Files.createDirectories(newDirectoryPath);
} catch (final IOException ex) {
throw new UncheckedIOException(ex);
}
});
}
// Hard link files
try (final Stream files = Files.walk(source)) {
files.filter(Files::isRegularFile).forEach((final Path originalFilePath) -> {
final Path relativeFilePath = source.relativize(originalFilePath);
final Path newFilePath = destination.resolve(relativeFilePath);
try {
Files.createLink(newFilePath, originalFilePath);
} catch (final IOException ex) {
throw new UncheckedIOException(ex);
}
});
}
}
/**
* Throw an exception if any of an array of files already exists.
*
* @param files an array of files
* @throws IOException if any of the files exist
*/
public static void throwIfFileExists(@Nullable final Path... files) throws IOException {
if (files == null) {
return;
}
for (final Path file : files) {
if (exists(file)) {
throw new FileAlreadyExistsException("File " + file + " already exists");
}
}
}
/**
* Execute an operation that writes to a directory. When the operation is complete, rename the directory. Useful for
* file operations that need to be atomic.
*
* @param directory the name of directory after it is renamed
* @param operation an operation that writes to a directory
*/
public static void executeAndRename(@NonNull final Path directory, @NonNull final IOConsumer operation)
throws IOException {
executeAndRename(directory, buildTemporaryDirectory(), operation);
}
/**
* Execute an operation that writes to a directory. When the operation is complete, rename the directory. Useful for
* file operations that need to be atomic.
*
* @param directory the name of directory after it is renamed
* @param tmpDirectory the name of the temporary directory, if it does not exist then it is created
* @param operation an operation that writes to a directory
*/
public static void executeAndRename(
@NonNull final Path directory, @NonNull final Path tmpDirectory, @NonNull final IOConsumer operation)
throws IOException {
try {
throwIfFileExists(directory);
if (!exists(tmpDirectory)) {
Files.createDirectories(tmpDirectory);
}
if (!Files.exists(directory.getParent())) {
Files.createDirectories(directory.getParent());
}
operation.accept(tmpDirectory);
// Move needs to be atomic to guarantee that the folder only exists when its contents are complete.
// Otherwise, it's possible another thread will see a half-completed directory.
Files.move(tmpDirectory, directory, StandardCopyOption.ATOMIC_MOVE);
} catch (final Throwable ex) {
logger.info(STATE_TO_DISK.getMarker(), "deleting temporary file due to exception");
throw ex;
} finally {
try {
deleteDirectory(tmpDirectory);
} catch (final Throwable t) {
logger.warn(
STATE_TO_DISK.getMarker(),
"unable to delete temporary directory {} due to {}",
tmpDirectory,
t);
}
}
}
/**
* Write to a new file, and make sure it's flushed to disk before returning.
*
* @param file the file to be written to, should not exist prior to this method being called
* @param writeMethod the method that writes
*/
public static void writeAndFlush(
@NonNull final Path file, @NonNull final IOConsumer writeMethod)
throws IOException {
throwIfFileExists(file);
try (final FileOutputStream fileOut = new FileOutputStream(file.toFile());
final BufferedOutputStream bufOut = new BufferedOutputStream(fileOut);
final MerkleDataOutputStream out = new MerkleDataOutputStream(bufOut)) {
writeMethod.accept(out);
// flush all the data to the file stream
out.flush();
// make sure the data is actually written to disk
fileOut.getFD().sync();
}
}
/**
* Returns the user directory path specified by the {@code user.dir} system property.
*
* @return the user directory path
*/
public static @NonNull String getUserDir() {
return System.getProperty("user.dir");
}
/**
* Find files in the given directory that end with the given suffix.
* @param dir the directory to search
* @param suffix the suffix to match
* @return a list of paths to files that match the given suffix
* @throws IOException if there is a problem walking the directory
*/
public static @NonNull List findFiles(@NonNull final Path dir, @NonNull final String suffix)
throws IOException {
try (Stream files = Files.walk(dir)) {
return files.filter(Files::isRegularFile)
.filter(f -> f.toString().endsWith(suffix))
.toList();
}
}
/**
* Delete files in the given directory that end with the given suffix.
* @param dir the directory to search
* @param suffix the suffix to match
* @throws IOException if there is a problem walking the directory or deleting
*/
public static void deleteFiles(@NonNull final Path dir, @NonNull final String suffix) throws IOException {
final List files = findFiles(dir, suffix);
for (final Path file : files) {
Files.delete(file);
}
}
}