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.
*/
public final class FileUtil {
public static final String CURRENT_DIR = ".";
public static final String PARENT_DIR = "..";
/**
* Read and parse the path to its absolute canonical path.
* <p>
* To circumvent the problem that java cached file metadata, including symlink
* targets, we need to read canonical paths directly. This includes resolving
* symlinks and relative path resolution (../..).<br>
* <p>
* This will read all the file meta each time and not use any of the java file
* meta caching, so will probably be a little slower. So should not be used
* repeatedly or too often.
*
* @param path The path to make canonical path of.
* @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;
}
/**
* Similar to {@link Files#createSymbolicLink(Path, Path, FileAttribute[])}, but
* will replace the link if it already exists, and will try to write / replace
* it as an atomic operation.
*
* @param link The path of the symbolic link. The parent directory of the
* link file must already exist, and must be writable.
* See {@link Files#createDirectories(Path, FileAttribute[])}
* @param newTarget The new target path.
* @throws IOException If unable to create 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 set to true, will not list sub-dirs, but will recursively
* enter the sub-dir and add the files
* @return List of files in 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 the file or directory recursively.
*
* @param path The file or directory path.
* @throws IOException If delete failed at some point.
*/
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() {}
}