FileUtil.java
/*
* Copyright (c) 2017, Stein Eldar Johnsen
*
* 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 net.morimekta.file;
import net.morimekta.file.internal.DeleteFilesRecursivelyVisitor;
import net.morimekta.file.internal.ListFilesVisitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NotLinkException;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* NIO file utility extensions for working with paths, symbolic links,
* listing directory contents, and recursive deletion.
*/
public final class FileUtil {
public static final String CURRENT_DIR = ".";
public static final String PARENT_DIR = "..";
/**
* Resolve the given path to its absolute canonical form by reading symlink
* targets and resolving relative segments ({@code .} and {@code ..}) directly
* from the file system.
* <p>
* Unlike {@link java.io.File#getCanonicalPath()} or
* {@link Path#toRealPath(java.nio.file.LinkOption...)}, this method never
* uses the JVM's cached symlink targets. The JVM aggressively caches
* resolved paths, which effectively makes symlinks static within a running
* process. This method bypasses that cache and always reads the current
* symlink target, making it suitable for long-running processes that need
* to detect symlink changes.
* <p>
* Because every call reads file metadata from disk, it is slower than
* cached alternatives and should not be called in tight loops.
*
* @param path The path to resolve.
* @return The resolved canonical path.
* @throws IOException If unable to read the path.
*/
public static Path readCanonicalPath(Path path) throws IOException {
if (!path.isAbsolute()) {
path = path.toAbsolutePath();
}
if (path.toString().equals(File.separator)) {
return path;
}
String fileName = path.getFileName().toString();
if (CURRENT_DIR.equals(fileName)) {
path = path.getParent();
fileName = path.getFileName().toString();
}
// resolve ".." relative to the top of the path.
int parents = 0;
while (PARENT_DIR.equals(fileName)) {
path = path.getParent();
if (path == null || path.getFileName() == null) {
throw new IOException("Parent of root does not exist!");
}
fileName = path.getFileName().toString();
++parents;
}
while (parents-- > 0) {
path = path.getParent();
if (path == null || path.getFileName() == null) {
throw new IOException("Parent of root does not exist!");
}
fileName = path.getFileName().toString();
}
if (path.getParent() != null) {
Path parent = readCanonicalPath(path.getParent());
path = parent.resolve(fileName);
if (Files.isSymbolicLink(path)) {
path = Files.readSymbolicLink(path);
if (!path.isAbsolute()) {
path = readCanonicalPath(parent.resolve(path));
}
}
}
return path;
}
/**
* Create or atomically replace a symbolic link. If the link already exists
* it is replaced in a single atomic move, producing only one file event
* instead of the two that a delete-then-create sequence would generate.
* If the link does not exist, it is simply created.
*
* @param link The path of the symbolic link. The parent directory must
* already exist and be writable.
* @param newTarget The new target path for the symbolic link.
* @throws NotLinkException If the path exists but is not a symbolic link.
* @throws IOException If unable to create or replace the symbolic link.
*/
public static void replaceSymbolicLink(Path link, Path newTarget) throws IOException {
link = link.toAbsolutePath();
if (Files.exists(link, NOFOLLOW_LINKS)) {
if (!Files.isSymbolicLink(link)) {
throw new NotLinkException(String.format("%s is not a symbolic link", link));
}
// This operation will follow the link. And we want it to.
Path parent = readCanonicalPath(link.getParent());
Path temp = parent.resolve(String.format("..tmp.%x", ThreadLocalRandom.current().nextLong()));
try {
Files.createSymbolicLink(temp, newTarget);
Files.move(temp, link, ATOMIC_MOVE, REPLACE_EXISTING, NOFOLLOW_LINKS);
} finally {
Files.deleteIfExists(temp);
}
} else {
Files.createSymbolicLink(link, newTarget);
}
}
/**
* List all files in the given directory.
*
* @param path Directory to list.
* @return List of files, symlinks and sub-directories in given directory.
* @throws IOException When unable to list files.
*/
public static Collection<Path> list(Path path) throws IOException {
return list(path, false);
}
/**
* List all files in the given directory.
*
* @param pathToList Directory to list.
* @param recursive If true, recursively enters sub-directories and lists
* their contents instead of listing the sub-directories
* themselves. Symbolic links to directories are listed as
* entries but not followed.
* @return List of files in the directory.
* @throws IOException If listing failed.
*/
public static Collection<Path> list(Path pathToList, boolean recursive) throws IOException {
if (!Files.isDirectory(pathToList)) {
return Collections.emptyList();
}
List<Path> all = new ArrayList<>();
Files.walkFileTree(pathToList, new ListFilesVisitor(pathToList, recursive, all::add));
return all;
}
/**
* Delete a file or directory recursively. If the path is a directory, all
* of its contents are deleted before the directory itself. Symbolic links
* to directories are deleted without following them.
*
* @param path The file or directory path to delete.
* @throws IOException If deletion failed.
*/
public static void deleteRecursively(Path path) throws IOException {
if (Files.isDirectory(path)) {
AtomicReference<IOException> exception = new AtomicReference<>();
Files.walkFileTree(path, new DeleteFilesRecursivelyVisitor(exception));
if (exception.get() != null) {
throw exception.get();
}
} else {
Files.deleteIfExists(path);
}
}
private FileUtil() {}
}