/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Lachlan Dowding
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package permafrost.tundra.io;
import permafrost.tundra.time.DateTimeHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.xml.datatype.Duration;
/**
* A collection of convenience methods for working with file system directories.
*/
public final class DirectoryHelper {
/**
* Disallow instantiation of this class;
*/
private DirectoryHelper() {}
/**
* Creates a new directory.
*
* @param directory The directory to be created.
* @throws IOException If the directory already exists or otherwise cannot be created.
*/
public static void create(File directory) throws IOException {
create(directory, true);
}
/**
* Creates a new directory.
*
* @param directory The directory to be created.
* @throws IOException If the directory already exists or otherwise cannot be created.
*/
public static void create(String directory) throws IOException {
create(directory, true);
}
/**
* Creates a new directory.
*
* @param directory The directory to be created.
* @param raise If true, will throw an exception if the creation of the directory fails.
* @throws IOException If raise is true and the directory already exists or otherwise cannot be created.
*/
public static void create(File directory, boolean raise) throws IOException {
if (directory != null) {
if (raise || !exists(directory)) {
if (!directory.mkdirs()) {
throw new IOException("Unable to create directory: " + FileHelper.normalize(directory));
}
}
}
}
/**
* Creates a new directory.
*
* @param directory The directory to be created.
* @param raise If true, will throw an exception if the creation of the directory fails.
* @throws IOException If raise is true and the directory already exists or otherwise cannot be created.
*/
public static void create(String directory, boolean raise) throws IOException {
create(FileHelper.construct(directory), raise);
}
/**
* Returns true if the given directory exists and is a directory.
*
* @param directory The directory to check existence of.
* @return True if the directory exists and is a directory.
*/
public static boolean exists(File directory) {
if (directory == null) return false;
return directory.exists() && directory.isDirectory();
}
/**
* Returns true if the given directory exists and is a directory.
*
* @param directory The directory to check existence of.
* @return True if the directory exists and is a directory.
*/
public static boolean exists(String directory) {
return exists(FileHelper.construct(directory));
}
/**
* Deletes the given directory.
*
* @param directory The directory to be deleted.
* @param recurse If true, all child directories and files will also be recursively deleted.
* @throws IOException If the directory cannot be deleted.
*/
public static void remove(File directory, boolean recurse) throws IOException {
if (exists(directory)) {
if (recurse) {
for (File item : directory.listFiles()) {
if (item.isFile()) {
FileHelper.remove(item);
} else {
remove(item, recurse);
}
}
}
if (!directory.delete()) {
throw new IOException("Unable to remove directory: " + FileHelper.normalize(directory));
}
}
}
/**
* Deletes the given directory.
*
* @param directory The directory to be deleted.
* @param recurse If true, all child directories and files will also be recursively deleted.
* @throws IOException If the directory cannot be deleted.
*/
public static void remove(String directory, boolean recurse) throws IOException {
remove(FileHelper.construct(directory), recurse);
}
/**
* Renames a directory.
*
* @param source The directory to be renamed.
* @param target The new name for the directory.
* @throws IOException If the directory cannot be renamed.
*/
public static void rename(File source, File target) throws IOException {
if (source != null && target != null) {
if (!exists(source) || exists(target) || !source.renameTo(target)) {
throw new IOException("Unable to rename directory '" + FileHelper.normalize(source) + "' to '" + FileHelper.normalize(target) + "'");
}
}
}
/**
* Renames a directory.
*
* @param source The directory to be renamed.
* @param target The new name for the directory.
* @throws IOException If the directory cannot be renamed.
*/
public static void rename(String source, String target) throws IOException {
rename(FileHelper.construct(source), FileHelper.construct(target));
}
/**
* Returns a raw directory listing with no additional processing: useful for when performance takes priority over
* ease of use; for example, when the directory contains hundreds of thousands or more files.
*
* @param directory The directory to list.
* @return The list of item names in the given directory.
* @throws FileNotFoundException If the directory does not exist.
*/
public static String[] list(String directory) throws FileNotFoundException {
return list(FileHelper.construct(directory));
}
/**
* Returns a raw directory listing with no additional processing: useful for when performance takes priority over
* ease of use; for example, when the directory contains hundreds of thousands or more files.
*
* @param directory The directory to list.
* @return The list of item names in the given directory.
* @throws FileNotFoundException If the directory does not exist.
*/
public static String[] list(File directory) throws FileNotFoundException {
String[] listing;
if (!exists(directory) || (listing = directory.list()) == null) {
throw new FileNotFoundException("Unable to list directory as it either does not exist, access is denied, or an IO error occurred: " + FileHelper.normalize(directory));
}
return listing;
}
/**
* Deletes all files in the given directory, and child directories if recurse is true, older than the given
* duration.
*
* @param directory The directory to be purged.
* @param duration The age files must be before they are deleted.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, then child files and directories will also be recursively purged.
* @return The number of files deleted.
* @throws FileNotFoundException If the directory does not exist.
*/
public static long purge(File directory, Duration duration, FilenameFilter filter, boolean recurse) throws FileNotFoundException {
return purge(directory, duration == null ? null : DateTimeHelper.earlier(duration), filter, recurse);
}
/**
* Deletes all files in the given directory, and child directories if recurse is true, older than the given
* duration.
*
* @param directory The directory to be purged.
* @param duration The age files must be before they are deleted.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, then child files and directories will also be recursively purged.
* @return The number of files deleted.
* @throws FileNotFoundException If the directory does not exist.
*/
public static long purge(String directory, Duration duration, FilenameFilter filter, boolean recurse) throws FileNotFoundException {
return purge(FileHelper.construct(directory), duration, filter, recurse);
}
/**
* Deletes all files in the given directory, and child directories if recurse is true, older than the given
* duration.
*
* @param directory The directory to be purged.
* @param olderThan Only files modified prior to this datetime will be deleted.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, then child files and directories will also be recursively purged.
* @return The number of files deleted.
* @throws FileNotFoundException If the directory does not exist.
*/
public static long purge(File directory, Calendar olderThan, FilenameFilter filter, boolean recurse) throws FileNotFoundException {
long count = 0;
for (String item : list(directory)) {
File child = new File(directory, item);
if (child.exists()) {
if (child.isFile() && (filter == null || filter.accept(directory, item))) {
boolean shouldPurge = true;
if (olderThan != null) {
Calendar modified = Calendar.getInstance();
modified.setTime(new Date(child.lastModified()));
shouldPurge = modified.compareTo(olderThan) <= 0;
}
if (shouldPurge && child.delete()) count += 1;
} else if (recurse && child.isDirectory()) {
count += purge(child, olderThan, filter, recurse);
}
}
}
return count;
}
/**
* Deletes all files in the given directory, and child directories if recurse is true, older than the given
* duration.
*
* @param directory The directory to be purged.
* @param olderThan Only files modified prior to this datetime will be deleted.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, then child files and directories will also be recursively purged.
* @return The number of files deleted.
* @throws FileNotFoundException If the directory does not exist.
*/
public static long purge(String directory, Calendar olderThan, FilenameFilter filter, boolean recurse) throws FileNotFoundException {
return purge(FileHelper.construct(directory), olderThan, filter, recurse);
}
/**
* Creates a new path given a parent directory and children.
*
* @param parent The parent directory.
* @param children The child path items.
* @return A new path.
*/
public static File join(File parent, String... children) {
File path = null;
if (parent != null || (children != null && children.length > 0)) {
if (parent != null) path = parent;
if (children != null) {
for (String child : children) {
if (path == null) {
path = FileHelper.construct(child);
} else {
path = new File(path, child);
}
}
}
}
return path;
}
/**
* Creates a new path given a list of path items.
*
* @param path The path items.
* @return A new path.
*/
public static String join(String... path) {
return FileHelper.normalize(join(null, path));
}
/**
* Deletes all empty child directories in the given directory, and
* optionally deletes the given directory itself if empty.
*
* @param directory The directory to be compacted.
* @param deleteSelf If true the given directory will also be deleted if empty
* @throws IOException If the directory could not be deleted.
*/
public static void compact(File directory, boolean deleteSelf) throws IOException {
if (directory == null || !directory.isDirectory()) return;
File[] children = directory.listFiles();
Throwable cause = null;
if (children != null) {
// compact all child directories recursively
for (File child : children) {
if (child.isDirectory()) {
try {
compact(child, true);
} catch (IOException ex) {
cause = ex;
}
}
}
// after compacting children, delete this directory if required
if (deleteSelf) {
// re-list files after compacting children
if (children.length > 0) children = directory.listFiles();
// delete this directory if it is empty
if (children != null && children.length == 0 && !directory.delete()) {
throw new IOException("Directory could not be deleted: " + FileHelper.normalize(directory));
} else if (cause != null) {
throw new IOException("Directory could not be deleted: " + FileHelper.normalize(directory), cause);
}
}
}
}
/**
* Returns the total size in bytes of all files in the given directory.
*
* @param directory The directory to calculate the size of.
* @param recurse If true, will recursively calculate the size of the entire directory tree including all
* child directories.
* @return The total size in bytes of all files in the given directory.
* @throws IOException If the given directory does not exist or is a file.
*/
public static BigInteger size(String directory, boolean recurse) throws IOException {
return size(FileHelper.construct(directory), recurse);
}
/**
* Returns the total size in bytes of all files in the given directory.
*
* @param directory The directory to calculate the size of.
* @param recurse If true, will recursively calculate the size of the entire directory tree including all
* child directories.
* @return The total size in bytes of all files in the given directory.
* @throws IOException If the given directory does not exist or is a file.
*/
public static BigInteger size(File directory, boolean recurse) throws IOException {
if (!exists(directory)) throw new FileNotFoundException("Unable to calculate size of directory as it does not exist: " + FileHelper.normalize(directory));
BigInteger totalSize = BigInteger.ZERO;
File[] children = directory.listFiles();
for (File child : children) {
if (FileHelper.exists(child)) {
totalSize = totalSize.add(BigInteger.valueOf(child.length()));
} else if (recurse && exists(child)) {
totalSize = totalSize.add(size(child, recurse));
}
}
return totalSize;
}
/**
* Reduces the size in bytes of a directory to an allowable size by deleting the least recently used
* files.
*
* @param directory The directory to be squeezed.
* @param allowedSize The allowable size of the directory in bytes.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, child directories will be included in the total size and their files
* may be deleted when reducing the total size of the parent.
* @return The resulting total size in bytes of all files in the given directory.
* @throws IOException If the given directory does not exist or is not a file.
*/
public static BigInteger squeeze(String directory, BigInteger allowedSize, FilenameFilter filter, boolean recurse) throws IOException {
return squeeze(FileHelper.construct(directory), allowedSize, filter, recurse);
}
/**
* Reduces the size in bytes of a directory to an allowable size by deleting the least recently used
* files.
*
* @param directory The directory to be squeezed.
* @param allowedSize The allowable size of the directory in bytes.
* @param filter An optional FilenameFilter used to filter which files are deleted.
* @param recurse If true, child directories will be included in the total size and their files
* may be deleted when reducing the total size of the parent.
* @return The resulting total size in bytes of all files in the given directory.
* @throws IOException If the given directory does not exist or is not a file.
*/
public static BigInteger squeeze(File directory, BigInteger allowedSize, FilenameFilter filter, boolean recurse) throws IOException {
BigInteger totalSize = size(directory, recurse);
if (allowedSize != null && totalSize.compareTo(allowedSize) > 0) {
BigInteger requiredReductionSize = totalSize.subtract(allowedSize);
DirectoryLister directoryLister = new DirectoryLister(directory, filter, recurse);
DirectoryListing directoryListing = directoryLister.list();
List<File> files = directoryListing.listFiles();
Collections.sort(files, new FileModificationComparator(true));
BigInteger totalReductionSize = BigInteger.ZERO;
for (File file : files) {
if (FileHelper.exists(file)) {
long fileSize = file.length();
if (file.delete()) totalReductionSize = totalReductionSize.add(BigInteger.valueOf(fileSize));
}
if (totalReductionSize.compareTo(requiredReductionSize) > 0) {
break;
}
}
}
return size(directory, recurse);
}
/**
* Compresses files in the given directory, and child directories if recurse is true, older than the given
* duration using gzip.
*
* @param directory The directory whose files are to be compressed.
* @param olderThan Only files modified prior to this datetime will be compressed.
* @param filter An optional FilenameFilter used to filter which files are compressed.
* @param recurse If true, then child files and directories will also be recursively compressed.
* @param replace Whether the original file should be deleted once compressed.
* @return The number of files compressed.
* @throws IOException If an IO error occurs.
*/
public static long gzip(File directory, Calendar olderThan, FilenameFilter filter, boolean recurse, boolean replace) throws IOException {
long count = 0;
for (String item : list(directory)) {
File child = new File(directory, item);
if (child.exists()) {
if (child.isFile() && (filter == null || filter.accept(directory, item))) {
boolean shouldCompress = true;
if (olderThan != null) {
Calendar modified = Calendar.getInstance();
modified.setTime(new Date(child.lastModified()));
shouldCompress = modified.compareTo(olderThan) <= 0;
}
if (shouldCompress) {
FileHelper.gzip(child, replace);
count += 1;
}
} else if (recurse && child.isDirectory()) {
count += gzip(child, olderThan, filter, recurse, replace);
}
}
}
return count;
}
/**
* Compresses files in the given directory, and child directories if recurse is true, older than the given
* duration using gzip.
*
* @param directory The directory whose files are to be compressed.
* @param duration The age files must be before they are deleted.
* @param filter An optional FilenameFilter used to filter which files are compressed.
* @param recurse If true, then child files and directories will also be recursively compressed.
* @param replace Whether the original file should be deleted once compressed.
* @return The number of files compressed.
* @throws IOException If an IO error occurs.
*/
public static long gzip(File directory, Duration duration, FilenameFilter filter, boolean recurse, boolean replace) throws IOException {
return gzip(directory, duration == null ? null : DateTimeHelper.earlier(duration), filter, recurse, replace);
}
}