blob: 1f48707910f18f0be3ab3d1abb3b048540381118 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.io.util;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.nio.file.*; // checkstyle: permit this import
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.RateLimiter;
import net.openhft.chronicle.core.util.ThrowingFunction;
import org.apache.cassandra.io.FSWriteError;
import static org.apache.cassandra.io.util.PathUtils.filename;
import static org.apache.cassandra.utils.Throwables.maybeFail;
/**
* A thin wrapper around java.nio.file.Path to provide more ergonomic functionality.
*
* TODO codebase probably should not use tryList, as unexpected exceptions are hidden;
* probably want to introduce e.g. listIfExists
*/
public class File implements Comparable<File>
{
private static FileSystem filesystem = FileSystems.getDefault();
public enum WriteMode { OVERWRITE, APPEND }
public static String pathSeparator()
{
return filesystem.getSeparator();
}
@Nullable final Path path; // nullable to support concept of empty path, that resolves to the working directory if converted to an absolute path
/**
* Construct a File representing the child {@code child} of {@code parent}
*/
public File(String parent, String child)
{
this(parent.isEmpty() ? null : filesystem.getPath(parent), child);
}
/**
* Construct a File representing the child {@code child} of {@code parent}
*/
public File(File parent, String child)
{
this(parent.path, child);
}
/**
* Construct a File representing the child {@code child} of {@code parent}
*/
public File(Path parent, String child)
{
// if "empty abstract path" (a la java.io.File) is provided, we should behave as though resolving relative path
if (child.startsWith(pathSeparator()))
child = child.substring(pathSeparator().length());
this.path = parent == null ? filesystem.getPath(child) : parent.resolve(child);
}
/**
* Construct a File representing the provided {@code path}
*/
public File(String path)
{
this(path.isEmpty() ? null : filesystem.getPath(path));
}
/**
* Create a File equivalent to the java.io.File provided
*/
public File(java.io.File file)
{
this(file.getPath().isEmpty() ? null : file.toPath());
}
/**
* Construct a File representing the child {@code child} of {@code parent}
*/
public File(java.io.File parent, String child)
{
this(new File(parent), child);
}
/**
* Convenience constructor equivalent to {@code new File(Paths.get(path))}
*/
public File(URI path)
{
this(Paths.get(path)); //TODO unsafe if uri is file:// as it uses default file system and not File.filesystem
if (!path.isAbsolute() || path.isOpaque()) throw new IllegalArgumentException();
}
/**
* @param path the path to wrap
*/
public File(Path path)
{
if (path != null && path.getFileSystem() != filesystem)
throw new IllegalArgumentException("Incompatible file system");
this.path = path;
}
public static Path getPath(String first, String... more)
{
return filesystem.getPath(first, more);
}
/**
* Try to delete the file, returning true iff it was deleted by us. Does not ordinarily throw exceptions.
*/
public boolean tryDelete()
{
return path != null && PathUtils.tryDelete(path);
}
/**
* This file will be deleted, and any exceptions encountered merged with {@code accumulate} to the return value
*/
public Throwable delete(Throwable accumulate)
{
return delete(accumulate, null);
}
/**
* This file will be deleted, obeying the provided rate limiter.
* Any exceptions encountered will be merged with {@code accumulate} to the return value
*/
public Throwable delete(Throwable accumulate, RateLimiter rateLimiter)
{
return PathUtils.delete(toPathForWrite(), accumulate, rateLimiter);
}
/**
* This file will be deleted, with any failures being reported with an FSError
* @throws FSWriteError if cannot be deleted
*/
public void delete()
{
maybeFail(delete(null, null));
}
/**
* This file will be deleted, with any failures being reported with an FSError
* @throws FSWriteError if cannot be deleted
*/
public void deleteIfExists()
{
if (path != null)
PathUtils.deleteIfExists(path);
}
/**
* This file will be deleted, obeying the provided rate limiter.
* @throws FSWriteError if cannot be deleted
*/
public void delete(RateLimiter rateLimiter)
{
maybeFail(delete(null, rateLimiter));
}
/**
* Deletes all files and subdirectories under "dir".
* @throws FSWriteError if any part of the tree cannot be deleted
*/
public void deleteRecursive(RateLimiter rateLimiter)
{
PathUtils.deleteRecursive(toPathForWrite(), rateLimiter);
}
/**
* Deletes all files and subdirectories under "dir".
* @throws FSWriteError if any part of the tree cannot be deleted
*/
public void deleteRecursive()
{
PathUtils.deleteRecursive(toPathForWrite());
}
/**
* Try to delete the file on process exit.
*/
public void deleteOnExit()
{
if (path != null) PathUtils.deleteOnExit(path);
}
/**
* This file will be deleted on clean shutdown; if it is a directory, its entire contents
* <i>at the time of shutdown</i> will be deleted
*/
public void deleteRecursiveOnExit()
{
if (path != null)
PathUtils.deleteRecursiveOnExit(path);
}
/**
* Try to rename the file atomically, if the system supports it.
* @return true iff successful, false if it fails for any reason.
*/
public boolean tryMove(File to)
{
return path != null && PathUtils.tryRename(path, to.path);
}
/**
* Atomically (if supported) rename/move this file to {@code to}
* @throws FSWriteError if any part of the tree cannot be deleted
*/
public void move(File to)
{
PathUtils.rename(toPathForRead(), to.toPathForWrite());
}
/**
* @return the length of the file if it exists and if we can read it; 0 otherwise.
*/
public long length()
{
return path == null ? 0L : PathUtils.tryGetLength(path);
}
/**
* @return the last modified time in millis of the path if it exists and we can read it; 0 otherwise.
*/
public long lastModified()
{
return path == null ? 0L : PathUtils.tryGetLastModified(path);
}
/**
* Try to set the last modified time in millis of the path
* @return true if it exists and we can write it; return false otherwise.
*/
public boolean trySetLastModified(long value)
{
return path != null && PathUtils.trySetLastModified(path, value);
}
/**
* Try to set if the path is readable by its owner
* @return true if it exists and we can write it; return false otherwise.
*/
public boolean trySetReadable(boolean value)
{
return path != null && PathUtils.trySetReadable(path, value);
}
/**
* Try to set if the path is writable by its owner
* @return true if it exists and we can write it; return false otherwise.
*/
public boolean trySetWritable(boolean value)
{
return path != null && PathUtils.trySetWritable(path, value);
}
/**
* Try to set if the path is executable by its owner
* @return true if it exists and we can write it; return false otherwise.
*/
public boolean trySetExecutable(boolean value)
{
return path != null && PathUtils.trySetExecutable(path, value);
}
/**
* @return true if the path exists, false if it does not, or we cannot determine due to some exception
*/
public boolean exists()
{
return path != null && PathUtils.exists(path);
}
/**
* @return true if the path refers to a directory
*/
public boolean isDirectory()
{
return path != null && PathUtils.isDirectory(path);
}
/**
* @return true if the path refers to a regular file
*/
public boolean isFile()
{
return path != null && PathUtils.isFile(path);
}
/**
* @return true if the path can be read by us
*/
public boolean isReadable()
{
return path != null && Files.isReadable(path);
}
/**
* @return true if the path can be written by us
*/
public boolean isWritable()
{
return path != null && Files.isWritable(path);
}
/**
* @return true if the path can be executed by us
*/
public boolean isExecutable()
{
return path != null && Files.isExecutable(path);
}
/**
* Try to create a new regular file at this path.
* @return true if successful, false if it already exists
*/
public boolean createFileIfNotExists()
{
return PathUtils.createFileIfNotExists(toPathForWrite());
}
public boolean createDirectoriesIfNotExists()
{
return PathUtils.createDirectoriesIfNotExists(toPathForWrite());
}
/**
* Try to create a directory at this path.
* Return true if a new directory was created at this path, and false otherwise.
*/
public boolean tryCreateDirectory()
{
return path != null && PathUtils.tryCreateDirectory(path);
}
/**
* Try to create a directory at this path, creating any parent directories as necessary.
* @return true if a new directory was created at this path, and false otherwise.
*/
public boolean tryCreateDirectories()
{
return path != null && PathUtils.tryCreateDirectories(path);
}
/**
* @return the parent file, or null if none
*/
public File parent()
{
if (path == null) return null;
Path parent = path.getParent();
if (parent == null) return null;
return new File(parent);
}
/**
* @return the parent file's path, or null if none
*/
public String parentPath()
{
File parent = parent();
return parent == null ? null : parent.toString();
}
/**
* @return true if the path has no relative path elements
*/
public boolean isAbsolute()
{
return path != null && path.isAbsolute();
}
public boolean isAncestorOf(File child)
{
return PathUtils.isContained(toPath(), child.toPath());
}
/**
* @return a File that represents the same path as this File with any relative path elements resolved.
* If this is the empty File, returns the working directory.
*/
public File toAbsolute()
{
return new File(toPath().toAbsolutePath());
}
/** {@link #toAbsolute} */
public String absolutePath()
{
return toPath().toAbsolutePath().toString();
}
/**
* @return a File that represents the same path as this File with any relative path elements and links resolved.
* If this is the empty File, returns the working directory.
*/
public File toCanonical()
{
Path canonical = PathUtils.toCanonicalPath(toPath());
return canonical == path ? this : new File(canonical);
}
/** {@link #toCanonical} */
public String canonicalPath()
{
return toCanonical().toString();
}
/**
* @return the last path element for this file
*/
public String name()
{
return path == null ? "" : filename(path);
}
public void forEach(Consumer<File> forEach)
{
PathUtils.forEach(path, path -> forEach.accept(new File(path)));
}
public void forEachRecursive(Consumer<File> forEach)
{
PathUtils.forEachRecursive(path, path -> forEach.accept(new File(path)));
}
private static <V> ThrowingFunction<IOException, V, RuntimeException> nulls() { return ignore -> null; }
private static <V> ThrowingFunction<IOException, V, IOException> rethrow()
{
return fail -> {
if (fail == null) throw new FileNotFoundException();
throw fail;
};
}
private static <V> ThrowingFunction<IOException, V, UncheckedIOException> unchecked()
{
return fail -> {
if (fail == null) fail = new FileNotFoundException();
throw new UncheckedIOException(fail);
};
}
/**
* @return if a directory, the names of the files within; null otherwise
*/
public String[] tryListNames()
{
return tryListNames(nulls());
}
/**
* @return if a directory, the names of the files within, filtered by the provided predicate; null otherwise
*/
public String[] tryListNames(BiPredicate<File, String> filter)
{
return tryListNames(filter, nulls());
}
/**
* @return if a directory, the files within; null otherwise
*/
public File[] tryList()
{
return tryList(nulls());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
public File[] tryList(Predicate<File> filter)
{
return tryList(filter, nulls());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
public File[] tryList(BiPredicate<File, String> filter)
{
return tryList(filter, nulls());
}
/**
* @return if a directory, the names of the files within; null otherwise
*/
public String[] listNames() throws IOException
{
return tryListNames(rethrow());
}
/**
* @return if a directory, the names of the files within, filtered by the provided predicate; null otherwise
*/
public String[] listNames(BiPredicate<File, String> filter) throws IOException
{
return tryListNames(filter, rethrow());
}
/**
* @return if a directory, the files within; null otherwise
*/
public File[] list() throws IOException
{
return tryList(rethrow());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
public File[] list(Predicate<File> filter) throws IOException
{
return tryList(filter, rethrow());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
public File[] list(BiPredicate<File, String> filter) throws IOException
{
return tryList(filter, rethrow());
}
/**
* @return if a directory, the names of the files within; null otherwise
*/
public String[] listNamesUnchecked() throws UncheckedIOException
{
return tryListNames(unchecked());
}
/**
* @return if a directory, the names of the files within, filtered by the provided predicate; null otherwise
*/
public String[] listNamesUnchecked(BiPredicate<File, String> filter) throws UncheckedIOException
{
return tryListNames(filter, unchecked());
}
/**
* @return if a directory, the files within; null otherwise
*/
public File[] listUnchecked() throws UncheckedIOException
{
return tryList(unchecked());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
public File[] listUnchecked(Predicate<File> filter) throws UncheckedIOException
{
return tryList(filter, unchecked());
}
/**
* @return if a directory, the files within, filtered by the provided predicate; throw an UncheckedIO exception otherwise
*/
public File[] listUnchecked(BiPredicate<File, String> filter) throws UncheckedIOException
{
return tryList(filter, unchecked());
}
/**
* @return if a directory, the names of the files within; null otherwise
*/
public <T extends Throwable> String[] tryListNames(ThrowingFunction<IOException, String[], T> orElse) throws T
{
return tryListNames(path, Function.identity(), orElse);
}
/**
* @return if a directory, the names of the files within, filtered by the provided predicate; null otherwise
*/
public <T extends Throwable> String[] tryListNames(BiPredicate<File, String> filter, ThrowingFunction<IOException, String[], T> orElse) throws T
{
return tryList(path, stream -> stream.map(PathUtils::filename).filter(filename -> filter.test(this, filename)), String[]::new, orElse);
}
/**
* @return if a directory, the files within; null otherwise
*/
private <T extends Throwable> File[] tryList(ThrowingFunction<IOException, File[], T> orElse) throws T
{
return tryList(path, Function.identity(), orElse);
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
private <T extends Throwable> File[] tryList(Predicate<File> filter, ThrowingFunction<IOException, File[], T> orElse) throws T
{
return tryList(path, stream -> stream.filter(filter), orElse);
}
/**
* @return if a directory, the files within, filtered by the provided predicate; null otherwise
*/
private <T extends Throwable> File[] tryList(BiPredicate<File, String> filter, ThrowingFunction<IOException, File[], T> orElse) throws T
{
return tryList(path, stream -> stream.filter(file -> filter.test(this, file.name())), orElse);
}
private static <T extends Throwable> String[] tryListNames(Path path, Function<Stream<File>, Stream<File>> toFiles, ThrowingFunction<IOException, String[], T> orElse) throws T
{
if (path == null)
return orElse.apply(null);
return PathUtils.tryList(path, stream -> toFiles.apply(stream.map(File::new)).map(File::name), String[]::new, orElse);
}
private static <T extends Throwable, V> V[] tryList(Path path, Function<Stream<Path>, Stream<V>> transformation, IntFunction<V[]> constructor, ThrowingFunction<IOException, V[], T> orElse) throws T
{
if (path == null)
return orElse.apply(null);
return PathUtils.tryList(path, transformation, constructor, orElse);
}
private static <T extends Throwable> File[] tryList(Path path, Function<Stream<File>, Stream<File>> toFiles, ThrowingFunction<IOException, File[], T> orElse) throws T
{
if (path == null)
return orElse.apply(null);
return PathUtils.tryList(path, stream -> toFiles.apply(stream.map(File::new)), File[]::new, orElse);
}
/**
* @return the path of this file
*/
public String path()
{
return toString();
}
/**
* @return the {@link Path} of this file
*/
public Path toPath()
{
return path == null ? filesystem.getPath("") : path;
}
/**
* @return the path of this file
*/
@Override
public String toString()
{
return path == null ? "" : path.toString();
}
@Override
public int hashCode()
{
return path == null ? 0 : path.hashCode();
}
@Override
public boolean equals(Object obj)
{
return obj instanceof File && Objects.equals(path, ((File) obj).path);
}
@Override
public int compareTo(File that)
{
if (this.path == null || that.path == null)
return this.path == null && that.path == null ? 0 : this.path == null ? -1 : 1;
return this.path.compareTo(that.path);
}
public java.io.File toJavaIOFile()
{
return path == null ? new java.io.File("") // checkstyle: permit this instantiation
: path.toFile(); // checkstyle: permit this invocation
}
/**
* @return a new {@link FileChannel} for reading
*/
public FileChannel newReadChannel() throws NoSuchFileException
{
return PathUtils.newReadChannel(toPathForRead());
}
/**
* @return a new {@link FileChannel} for reading or writing; file will be created if it doesn't exist
*/
public FileChannel newReadWriteChannel() throws NoSuchFileException
{
return PathUtils.newReadWriteChannel(toPathForRead());
}
/**
* @param mode whether or not the channel appends to the underlying file
* @return a new {@link FileChannel} for writing; file will be created if it doesn't exist
*/
public FileChannel newWriteChannel(WriteMode mode) throws NoSuchFileException
{
switch (mode)
{
default: throw new AssertionError();
case APPEND: return PathUtils.newWriteAppendChannel(toPathForWrite());
case OVERWRITE: return PathUtils.newWriteOverwriteChannel(toPathForWrite());
}
}
public FileWriter newWriter(WriteMode mode) throws IOException
{
return new FileWriter(this, mode);
}
public FileOutputStreamPlus newOutputStream(WriteMode mode) throws NoSuchFileException
{
return new FileOutputStreamPlus(this, mode);
}
public FileInputStreamPlus newInputStream() throws NoSuchFileException
{
return new FileInputStreamPlus(this);
}
private Path toPathForWrite()
{
if (path == null)
throw new IllegalStateException("Cannot write to an empty path");
return path;
}
private Path toPathForRead()
{
if (path == null)
throw new IllegalStateException("Cannot read from an empty path");
return path;
}
public static void unsafeSetFilesystem(FileSystem fs)
{
filesystem = fs;
}
}