All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.gc.aafc.dina.file.FileCleaner Maven / Gradle / Ivy

The newest version!
package ca.gc.aafc.dina.file;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.TemporalUnit;
import java.util.EnumSet;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;

import lombok.SneakyThrows;

/**
 * Deletes file, recursively, from a root path if the provided predicate returns true.
 * By default, only files will be checked (no folders/no symlinks).
 *
 * Some checks are also included to avoid file system root or non-existing folder.
 */
public final class FileCleaner {

  public enum Options { ALLOW_NON_TMP }

  private static final String TMP_DIR_PROPERTY = "java.io.tmpdir";

  private final Path rootPath;
  private final Predicate predicate;

  /**
   * Creates a default instance where the provided predicate will be combined with the buildFileOnlyPredicate.
   * Folders and symlinks will be ignored.
   * @param rootPath
   * @param predicate
   * @return
   */
  public static FileCleaner newInstance(Path rootPath, Predicate predicate) {
    return new FileCleaner(rootPath, buildFileOnlyPredicate().and(predicate), null);
  }

  /**
   * Creates an instance with specific options.
   * Use carefully, options gives more flexibility but requires the caller to do more checks to avoid
   * unwanted destructive (file delete) operations.
   * @param rootPath
   * @param predicate
   * @param options
   * @return
   */
  public static FileCleaner newInstance(Path rootPath, Predicate predicate, EnumSet options) {
    Objects.requireNonNull(options);
    return new FileCleaner(rootPath, buildFileOnlyPredicate().and(predicate), options);
  }

  /**
   * Private constructor to avoid misuse of always true predicate.
   * @param rootPath
   * @param predicate
   */
  private FileCleaner(Path rootPath, Predicate predicate, EnumSet options) {
    // sanity checks
    Objects.requireNonNull(rootPath);
    Objects.requireNonNull(predicate);

    Path normalizedRootPath = rootPath.normalize();
    if (!normalizedRootPath.toFile().isDirectory() || !normalizedRootPath.toFile().exists()) {
      throw new IllegalArgumentException(
        "FileCleaner can only be initialized on an existing directory");
    }

    // by default (no options provided) we restrict to tmp directory
    boolean restrictToTmpDirectory = options == null || !options.contains(Options.ALLOW_NON_TMP);

    if (restrictToTmpDirectory && !normalizedRootPath.startsWith(System.getProperty(TMP_DIR_PROPERTY))) {
      throw new IllegalArgumentException(
        "FileCleaner can only be initialized on a directory under " +
          System.getProperty(TMP_DIR_PROPERTY));
    }

    if (StreamSupport.stream(normalizedRootPath.getFileSystem().getRootDirectories().spliterator(), false)
      .anyMatch(p -> p.equals(normalizedRootPath))) {
      throw new IllegalArgumentException("can't initialize FileCleaner on a root directory");
    }

    this.rootPath = normalizedRootPath;
    this.predicate = predicate;
  }

  /**
   * Build a predicate that is checking for the maximum age of a file based on its lastModifiedTime.
   * @param unit
   * @param maxAge
   * @return
   */
  public static Predicate buildMaxAgePredicate(TemporalUnit unit, long maxAge) {
    return path -> {
      Duration interval = Duration.between(getLastModifiedTime(path).toInstant(), Instant.now());
      return interval.get(unit) > maxAge;
    };
  }

  /**
   * Build a predicate for checking for a specific file extension of a file.
   * 
   * The check for the extension is case-insensitive. 
   * 
   * @param extension the extension without the leading "." (eg. "txt", "md").
   * @return predicate checking the extension based on the file extension
   *         provided.
   */
  public static Predicate buildFileExtensionPredicate(String extension) {
    if (StringUtils.isBlank(extension)) {
      throw new IllegalArgumentException("Extension must not be null or empty.");
    }

    return path -> FilenameUtils.isExtension(path.getFileName().toString().toLowerCase(), extension.toLowerCase());
  }

  /**
   * Excludes folder and symlinks
   * @return
   */
  public static Predicate buildFileOnlyPredicate() {
    return path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
  }

  /**
   * Clean folder recursively by deleting all files that are matching the predicate.
   * @throws IOException
   */
  public void clean() throws IOException {
    try (Stream p = Files.walk(rootPath)) {
      p.filter(predicate)
        .forEach(FileCleaner::delete);
    }
  }

  @SneakyThrows
  public static FileTime getLastModifiedTime(Path path) {
    return Files.getLastModifiedTime(path);
  }

  @SneakyThrows
  public static void delete(Path path) {
    Files.delete(path);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy