FileUtils.java
package com.renomad.minum.utils;
import com.renomad.minum.state.Constants;
import com.renomad.minum.logging.ILogger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Helper functions for working with files.
* <br>
* In all these functions, note that it is disallowed to request a path
* having certain characters - see {@link #badFilePathPatterns}
*/
public final class FileUtils {
/**
* These patterns can be used in path strings to access files higher in
* the directory structure. We disallow this, as a security precaution.
* <ul>
* <li>1st Alternative {@code //} - This prevents going to the root directory
* <li>2nd Alternative {@code ..} - prevents going up a directory
* <li>3rd Alternative {@code :} - prevents certain special paths, like "C:" or "file://"
* </ul>
*/
public static final Pattern badFilePathPatterns = Pattern.compile("//|\\.\\.|:");
private final ILogger logger;
private final IFileReader fileReader;
public FileUtils(ILogger logger, Constants constants) {
this(
logger,
new FileReader(
LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles),
constants.useCacheForStaticFiles,
logger));
}
/**
* This version of the constructor is mainly for testing
*/
FileUtils(ILogger logger, IFileReader fileReader) {
this.logger = logger;
this.fileReader = fileReader;
}
/**
* Write a string to a path on disk.
* <p>
* Parent directories are made unavailable by searching the path for
* bad characters. See {@link #badFilePathPatterns}
* </p>
*/
public void writeString(Path path, String content) {
if (path.toString().isEmpty()) {
logger.logDebug(() -> "an empty path was provided to writeString");
return;
}
if (badFilePathPatterns.matcher(path.toString()).find()) {
logger.logDebug(() -> String.format("Bad path requested at writeString: %s", path));
return;
}
try {
Files.writeString(path, content);
} catch (IOException e) {
throw new UtilsException(e);
}
}
/**
* Deletes a directory, deleting everything inside it
* recursively afterwards. A more dangerous method than
* many others, take care.
* <p>
* Parent directories are made unavailable by searching the path for
* bad characters. See {@link #badFilePathPatterns}
* </p>
*/
public void deleteDirectoryRecursivelyIfExists(Path myPath) {
if (badFilePathPatterns.matcher(myPath.toString()).find()) {
logger.logDebug(() -> String.format("Bad path requested at deleteDirectoryRecursivelyIfExists: %s", myPath));
return;
}
if (!Files.exists(myPath)) {
logger.logDebug(() -> "system was requested to delete directory: "+myPath+", but it did not exist");
} else {
walkPathDeleting(myPath);
}
}
void walkPathDeleting(Path myPath) {
try (Stream<Path> walk = Files.walk(myPath)) {
final var files = walk.sorted(Comparator.reverseOrder())
.map(Path::toFile).toList();
for(var file: files) {
logger.logDebug(() -> "deleting " + file);
Files.delete(file.toPath());
}
} catch (IOException ex) {
throw new UtilsException("Error during deleteDirectoryRecursivelyIfExists: " + ex);
}
}
/**
* Creates a directory if it doesn't already exist.
* <p>
* Parent directories are made unavailable by searching the path for
* bad characters. See {@link #badFilePathPatterns}
* </p>
* <p>
* If the directory does exist, the program will simply skip
* building it, and mention it in the logs.
* </p>
*/
public void makeDirectory(Path directory) {
if (badFilePathPatterns.matcher(directory.toString()).find()) {
logger.logDebug(() -> String.format("Bad path requested at makeDirectory: %s", directory));
return;
}
logger.logDebug(() -> "Creating a directory " + directory);
boolean directoryExists = Files.exists(directory);
logger.logDebug(() -> "Directory: " + directory + ". Already exists: " + directory);
if (!directoryExists) {
logger.logDebug(() -> "Creating directory, since it does not already exist: " + directory);
innerCreateDirectory(directory);
logger.logDebug(() -> "Directory: " + directory + " created");
}
}
static void innerCreateDirectory(Path directory) {
try {
Files.createDirectories(directory);
} catch (Exception e) {
throw new UtilsException(e);
}
}
/**
* Read a binary file, return as a byte array
* <p>
* If there is an error, this will return an empty byte array.
* </p>
*/
public byte[] readBinaryFile(String path) {
try {
return fileReader.readFile(path);
} catch (IOException e) {
logger.logDebug(() -> String.format("Error while reading file %s, returning empty byte array. %s", path, e));
return new byte[0];
}
}
/**
* Read a text file from the given path, return as a string.
*
* <p>
* Access is prevented to data in parent directories or using alternate
* drives. If the data is read, it will be added to a cache, if
* the property {@link Constants#useCacheForStaticFiles} is set to true. The maximum
* size of the cache is controlled by
* </p>
* <p>
* If there is an error, this will return an empty string.
* </p>
*/
public String readTextFile(String path) {
try {
return new String(fileReader.readFile(path), StandardCharsets.UTF_8);
} catch (IOException e) {
logger.logDebug(() -> String.format("Error while reading file %s, returning empty string. %s", path, e));
return "";
}
}
}