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

personthecat.catlib.io.FileIO Maven / Gradle / Ivy

Go to download

Utilities for serialization, commands, noise generation, IO, and some new data types.

The newest version!
package personthecat.catlib.io;

import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import personthecat.catlib.exception.DirectoryNotCreatedException;
import personthecat.catlib.exception.ResourceException;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import personthecat.fresult.OptionalResult;
import personthecat.fresult.PartialResult;
import personthecat.fresult.Result;
import personthecat.fresult.Void;
import personthecat.fresult.functions.ThrowingConsumer;

import javax.annotation.CheckReturnValue;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import static java.util.Optional.empty;
import static personthecat.catlib.exception.Exceptions.directoryNotCreated;
import static personthecat.catlib.exception.Exceptions.runEx;
import static personthecat.catlib.exception.Exceptions.resourceEx;
import static personthecat.catlib.util.Shorthand.f;
import static personthecat.catlib.util.Shorthand.full;
import static personthecat.catlib.util.Shorthand.nullable;

@Log4j2
@UtilityClass
@ParametersAreNonnullByDefault
@SuppressWarnings({"unused", "UnusedReturnValue"})
public class FileIO {

    /** For outputting the correct new line type. */
    private static final String NEW_LINE = System.lineSeparator();

    /** The size of the array used for copy operations. */
    private static final int BUFFER_SIZE = 1024;

    /**
     * Creates the input directory file and parent files, as needed. Unlike {@link File#mkdirs},
     * this method never throws a {@link SecurityException} and instead returns false
     * if any error occurs.
     *
     * @param f The directory being created.
     * @return Whether the file exists or was created. False if err.
     */
    @CheckReturnValue
    public static boolean mkdirs(final File f) {
        return Result.suppress(() -> f.exists() || f.mkdirs()).orElse(false);
    }

    /**
     * Variant of {@link #mkdirs(File)} which accepts multiple files. Note that this method does
     * not return whether the file exists or has been created and instead will throw an exception
     * if the operation fails.
     *
     * @param files A series of directories being created.
     * @throws DirectoryNotCreatedException If any error occurs when creating the directory.
     */
    public static void mkdirsOrThrow(final File... files) {
        for (final File f : files) {
            Result.suppress(() -> f.exists() || f.mkdirs())
                .mapErr(e -> directoryNotCreated(f, e))
                .filter(b -> b, () -> directoryNotCreated(f))
                .throwIfErr();
        }
    }

    /**
     * Determines whether the given file currently exists. Unlike {@link File#exists}, this method
     * never throws a {@link SecurityException} and instead returns false if any
     * error occurs.
     *
     * @param f The file being operated on.
     * @return Whether the file exists. False if err.
     */
    @CheckReturnValue
    public static boolean fileExists(final File f) {
        return Result.suppress(f::exists).orElse(false);
    }

    /**
     * Copies a file to the given location with no explicit options. The output is allowed
     * to be specified either as a directory which will contain the new file or as the actual
     * file being written.
     *
     * 

In the event where the file being copied is a directory, its contents will also be * copied and the output directory will always be to. * * @param f The file being copied. * @param to The directory or file being copied into. * @return The result of this operation, which you may wish to rethrow. */ public static Result copy(final File f, final File to) { final PartialResult partial; if (f.isDirectory()) { partial = Result.of(() -> { FileUtils.copyDirectory(f, to); return to.toPath(); }); } else { final File output = to.isDirectory() ? new File(to, f.getName()) : to; partial = Result.of(() -> Files.copy(f.toPath(), output.toPath())); } return partial.ifErr(e -> log.error("Copying file", e)); } /** * Moves a file to the given location with no explicit options. The output is allowed * to be specified either as a directory which will contain the new file or as the actual * file being written. * * @param f The file being moved. * @param to The directory or file being moved into. * @return The result of this operation, which you may wish to rethrow. */ public static Result move(final File f, final File to) { final File output = to.isDirectory() ? new File(to, f.getName()) : to; return Result.of(() -> Files.move(f.toPath(), output.toPath())) .ifErr(e -> log.error("moving file", e)); } /** * Returns a list of all files in the given directory. Unlike {@link File#listFiles}, this * method does not return null. * * @param dir The directory being operated on. * @return An array of all files in the given directory. */ @NotNull @CheckReturnValue public static File[] listFiles(final File dir) { return nullable(dir.listFiles()).orElseGet(() -> new File[0]); } /** * Returns a list of all files in the given directory when applying the given filter. Unlike * {@link File#listFiles(FileFilter)}, this method never returns null. * * @param dir The directory being operated on. * @param filter A predicate determining whether a given file should be included. * @return An array containing the requested files. */ @NotNull @CheckReturnValue public static File[] listFiles(final File dir, final FileFilter filter) { return nullable(dir.listFiles(filter)).orElse(new File[0]); } /** * Recursive variant of {@link #listFiles(File)}. Includes the contents of each file in every * subdirectory for the given folder. * * @param dir The directory being operated on. * @return A {@link List} of all files in the given directory. */ @NotNull @CheckReturnValue public static List listFilesRecursive(final File dir) { if (dir.isFile()) { return Collections.emptyList(); } final List files = new ArrayList<>(); listFilesInto(files, dir); return files; } /** * Recursively stores a reference to each file in the given directory in the provided array. * * @param files The array storing the final list of files. * @param dir The directory being operated on. */ private static void listFilesInto(final List files, final File dir) { final File[] inDir = dir.listFiles(); if (inDir != null) { for (final File f : inDir) { if (f.isDirectory()) { listFilesInto(files, f); } else { files.add(f); } } } } /** * Variant of {@link #listFilesRecursive(File)} which filters based on a predicate. * * @param dir The root directory which may contain the expected files. * @param filter A predicate used to match the expected files. * @return Every matching file nested within this directory. */ public static List listFilesRecursive(final File dir, final FileFilter filter) { if (dir.isFile()) { return Collections.emptyList(); } final List files = new ArrayList<>(); listFilesInto(files, dir, filter); return files; } /** * Variant of {@link #listFilesInto(List, File)} which filters based on a predicate. * * @param files The array storing the final list of files. * @param dir The directory being operated on. * @param filter A filter used for matching files. */ private static void listFilesInto(final List files, final File dir, final FileFilter filter) { final File[] inDir = dir.listFiles(); if (inDir != null) { for (final File f : inDir) { if (f.isDirectory()) { listFilesInto(files, f); } else if (filter.accept(f)) { files.add(f); } } } } /** * Recursively searches through the given directory until the first matching file is found. * * @param dir The root directory which may contain the expected file. * @param filter A predicate used to match the expected file. * @return The requested file, or else {@link Optional#empty}. */ @CheckReturnValue public static Optional locateFileRecursive(final File dir, final FileFilter filter) { final File[] inDir = dir.listFiles(); if (inDir != null) { for (final File f : inDir) { if (f.isDirectory()) { final Optional found = locateFileRecursive(f, filter); if (found.isPresent()) { return found; } } else if (filter.accept(f)) { return full(f); } } } return empty(); } /** * Variant of {@link #locateFileRecursive(File, FileFilter)} which first looks in a * preferred directory. * * @param dir The root directory which may contain the expected file. * @param preferred The first directory to search through. * @param filter A predicate used to match the expected file. * @return The requested file, or else {@link Optional#empty}. */ @CheckReturnValue public static Optional locateFileRecursive(final File dir, @Nullable final File preferred, final FileFilter filter) { if (preferred == null) return locateFileRecursive(dir, filter); final Optional inPreferred = locateFileRecursive(preferred, filter); return inPreferred.isPresent() ? inPreferred : locateFileRecursiveInternal(dir, preferred, filter); } /** * Internal variant of {@link #locateFileRecursive(File, File, FileFilter)} which has * already searched through the preferred directory. * * @param dir The root directory which may contain the expected file. * @param preferred The first directory to search through. * @param filter A predicate used to match the expected file. * @return The requested file, or else {@link Optional#empty}. */ @CheckReturnValue private static Optional locateFileRecursiveInternal(final File dir, final File preferred, final FileFilter filter) { final File[] inDir = dir.listFiles(f -> !f.equals(preferred)); if (inDir != null) { for (final File f : inDir) { if (f.isDirectory()) { final Optional found = locateFileRecursiveInternal(f, preferred, filter); if (found.isPresent()) { return found; } } else if (filter.accept(f)) { return full(f); } } } return empty(); } /** * Attempts to read the contents of a file. Returns an error if any exception occurs. * * @param f The file being read. * @return The contents of the file, or else {@link Result#err}. */ public static Result readFile(final File f) { try { return Result.ok(FileUtils.readFileToString(f, Charset.defaultCharset())); } catch (final IOException e) { return Result.err(e); } } /** * Attempts to retrieve the contents of the input file, or else {@link Optional#empty}. * * @param f The file being read from. * @return A list of each line in the given file, or else {@link Optional#empty}. */ @CheckReturnValue public static Optional> readLines(final File f) { return Result.of(() -> Files.readAllLines(f.toPath())).get(Result::IGNORE); } /** * Returns the raw string contents of the given file, or else {@link Optional#empty}. * * @param f The file being read from. * @return A string representation of the given file's contents, or else {@link Optional#empty}. */ @CheckReturnValue public Optional contents(final File f) { return readFile(f).get(); } /** * Copies a file to the given backup directory. * * @param dir The directory where this backup will be stored. * @param f The file being backed up. * @return The number of backups of this file that now exist. */ public static int backup(final File dir, final File f) { return backup(dir, f, true); } /** * Variant of {@link #backup(File, File, boolean)} which never throws an exception. * * @param dir The directory where this backup will be stored. * @param f The file being backed up. * @param copy Whether to additionally copy the file instead of just moving it. * @return A result indicating either the number of backups or the error. */ public static Result tryBackup(final File dir, final File f, final boolean copy) { try { return Result.ok(backup(dir, f, copy)); } catch (final ResourceException e) { return Result.err(e); } } /** * Copies (or moves) a file to the given backup directory. * * @throws ResourceException If any IO exception occurs. * @param dir The directory where this backup will be stored. * @param f The file being backed up. * @param copy Whether to additionally copy the file instead of just moving it. * @return The number of backups of this file that now exist. */ public static int backup(final File dir, final File f, final boolean copy) { if (f.getAbsolutePath().startsWith(dir.getAbsolutePath())) { throw resourceEx("Cannot create backup inside backups directory: {}", f.getName()); } if (!mkdirs(dir)) { throw resourceEx("Error creating backup directory: {}", dir); } final File backup = new File(dir, f.getName()); final BackupHelper helper = new BackupHelper(f); final int count = helper.cycle(dir); if (fileExists(backup)) { throw resourceEx("Could not rename backups: {}", f.getName()); } if (copy) { copy(f, dir).ifErr(Result::THROW); } else { move(f, backup).ifErr(e -> { throw resourceEx(f("Error moving {} to backups", f.getName()), e); }); } return count + 1; } /** * Variant of {@link #truncateBackups(File, File, int)} which never throws an exception. * * @param dir The directory containing the files being truncated. * @param f The being moved into the backup folder. * @param count The maximum number of matching backups to keep in this directory. * @return A result wrapping the error, if applicable. */ public static Result tryTruncateBackups(final File dir, final File f, final int count) { try { truncateBackups(dir, f, count); return Result.ok(); } catch (final ResourceException e) { return Result.err(e); } } /** * Deletes any matching file in the backup folder, keeping count files. * * @throws ResourceException If any IO exception occurs in the process. * @param dir The directory containing the files being truncated. * @param f The being moved into the backup folder. * @param count The maximum number of matching backups to keep in this directory. * @return true, if any backups were deleted. */ public static boolean truncateBackups(final File dir, final File f, final int count) { if (!mkdirs(dir)) { throw resourceEx("Error creating backup directory: {}", dir); } return new BackupHelper(f).truncate(dir, count); } /** * Deletes the given file. If this file is a directory, its contents will be * deleted recursively. * * @param f The file or directory being deleted. */ public static void delete(final File f) { if (f.isFile()) { if (!f.delete()) { throw resourceEx("Error deleting file {}.", f.getName()); } } else { try { Files.walk(f.toPath()).sorted(Comparator.reverseOrder()).forEach(p -> { if (!p.toFile().delete()) { log.error("Error deleting {}", p.toFile().getName()); } }); } catch (final IOException e) { throw resourceEx("Error deleting directory", e); } } } /** * Renames a file when given a top-level name only. * * @param f The file being renamed. * @param name The new name for this file. * @return A result for handling errors, if any. */ public static Result rename(final File f, final String name) { final File path = new File(f.getParentFile(), name); if (!f.renameTo(path)) { return Result.err(runEx("Cannot rename: {}", path)); } return Result.ok(); } /** * Standard stream copy process. Returns an exception, instead of throwing it. *

* Note that the given streams will not be closed.. *

* @param is The input stream being copied out of. * @param os The output stream being copied into. * @return The result of the operation. Any errors should not be ignored. */ public static Result copyStream(final InputStream is, final OutputStream os) { return Result.of(() -> { final byte[] b = new byte[BUFFER_SIZE]; int length; while ((length = is.read(b)) > 0) { os.write(b, 0, length); } }).ifErr(e -> log.error("Error copying stream", e)); } /** * Variant of {@link #copyStream(InputStream, OutputStream)} which accepts a file path * parameter instead of an {@link OutputStream} stream directly. *

* Note that the given input stream will not be closed.. *

* @param is The input stream being copied out of. * @param path The raw file path, as a string. * @return The result of the operation. Any errors should not be ignored. */ @CheckReturnValue public static Result copyStream(final InputStream is, final String path) { return Result.with(() -> new FileOutputStream(path)) .of((ThrowingConsumer) os -> copyStream(is, os).throwIfErr()) .ifErr(e -> log.error("Error creating output stream", e)); } /** * Determines whether an asset is present in the jar. * * @param path The path to the file, with or without the beginning /. * @return Whether a resource exists at this location. */ @CheckReturnValue public static boolean resourceExists(final String path) { final Optional is = getResource(path); is.ifPresent(s -> Result.suppress(s::close).ifErr(e -> log.error("Closing resource", e))); return is.isPresent(); } /** * Retrieves an asset from the jar file or resources directory. * * @param path The path to the file, with or without the beginning /. * @return The expected resource, or else {@link Optional#empty}. */ @CheckReturnValue public static Optional getResource(final String path) { final String file = path.startsWith("/") ? path : "/" + path; return nullable(FileIO.class.getClassLoader().getResourceAsStream(file)); } /** * Retrieves an asset from the jar file or resources directory. * * @throws ResourceException If the file is not found. * @param path The path to the file, with or without the beginning /. * @return The resource as an {@link InputStream}. */ @CheckReturnValue public static InputStream getRequiredResource(final String path) { return getResource(path).orElseThrow(() -> resourceEx("The required file \"{}\" does not exist.", path)); } /** * Returns a resource from the jar as a string. * * @param path The path to the resource being read. * @return The resource as a string, or else {@link Optional#empty}. */ @CheckReturnValue public static OptionalResult getResourceAsString(final String path) { return Result.nullable(getResource(path)).flatMap(FileIO::readString); } /** * Parses an input stream as a regular string. * * @param is The stream being copied out of. * @return The file as a string, or else {@link Optional#empty}, if err. */ @CheckReturnValue private static OptionalResult readString(final InputStream is) { return Result.with(() -> is) .with(() -> new BufferedReader(new InputStreamReader(is))) .nullable(FileIO::read) .ifErr(e -> log.error("Reading string", e)); } /** * Parses all lines out of the given reader. * * @throws IOException Declared by {@link BufferedReader#readLine()} * @param reader The buffered reader providing string data. * @return A parsed string. */ private static String read(final BufferedReader reader) throws IOException { final StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); sb.append(NEW_LINE); } return sb.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy