package com.github.marschall.memoryfilesystem;
import static java.nio.file.FileVisitResult.CONTINUE;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.CopyOption;
import java.nio.file.FileSystemException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.DosFileAttributes;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.attribute.UserPrincipalNotFoundException;
import java.util.HashSet;
import java.util.Set;
/**
* Implements recursive copy missing in {@link Files}.
*/
public final class Directories {
private static final LinkOption[] NO_LINK_OPTIONS = new LinkOption[0];
private static final LinkOption[] NOFOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
private Directories() {
throw new AssertionError("not instantiable");
}
/**
* Copy a directory to a target directory recursively.
*
* <p>This method performs a copy much like
* {@link Files#copy(Path, Path, CopyOption...)}. Unlike
* {@link Files#copy(Path, Path, CopyOption...)} is can also copy non-empty
* directories.</p>
*
* <p>This method makes a best effort to copy attributes across
* different file system providers.</p>
*
* <h2>Known Issues:</h2>
* <ul>
* <li>hard links will not be handled correctly</li>
* </ul>
*
* @see Files#copy(Path, Path, CopyOption...)
*
* @param source the path to the file to copy
* @param target the path to the target file (may be associated with a different
* provider to the source path)
* @param copyOptions options specifying how the copy should be done
* @throws IOException if an I/O error occurs
*/
public static void copyRecursive(Path source, Path target, CopyOption... copyOptions) throws IOException {
boolean sameFileSystem = source.getFileSystem() == target.getFileSystem();
LinkOption[] linkOptions = linkOptions(copyOptions);
boolean targetExists = Files.exists(target, linkOptions);
boolean copyAttribues = Options.isCopyAttribues(copyOptions);
Set<Class<? extends FileAttributeView>> supportedAttribueViews = supportedAttribueViews(source, target, sameFileSystem);
if (!targetExists) {
Files.createDirectories(target);
}
FileVisitor<Path> copier = new DirectoryCopier(source, target, copyOptions, linkOptions, supportedAttribueViews, sameFileSystem, copyAttribues);
Files.walkFileTree(source, copier);
if (!targetExists && copyAttribues) {
copyAttributes(source, target, sameFileSystem, supportedAttribueViews, linkOptions);
}
}
private static LinkOption[] linkOptions(CopyOption[] copyOptions) {
return Options.isFollowSymLinks(copyOptions) ? NO_LINK_OPTIONS : NOFOLLOW_LINKS;
}
private static Set<Class<? extends FileAttributeView>> supportedAttribueViews(Path source, Path target, boolean sameFileSystem) {
Set<String> viewNames = source.getFileSystem().supportedFileAttributeViews();
if (!sameFileSystem) {
viewNames = new HashSet<>(viewNames); // can be unmodifyable
viewNames.retainAll(target.getFileSystem().supportedFileAttributeViews());
}
Set<Class<? extends FileAttributeView>> supportedAttribueViews = new HashSet<>(viewNames.size());
for (String string : viewNames) {
supportedAttribueViews.add(FileAttributeViews.mapAttributeViewName(string));
}
return supportedAttribueViews;
}
private static void copyAttributes(Path source, Path target, boolean sameFileSystem, Set<Class<? extends FileAttributeView>> attribueViews, LinkOption[] linkOptions) throws IOException {
BasicFileAttributes basicAttributes = Files.readAttributes(target, BasicFileAttributes.class, linkOptions);
BasicFileAttributeView basicView = Files.getFileAttributeView(target, BasicFileAttributeView.class, linkOptions);
basicView.setTimes(basicAttributes.lastModifiedTime(), basicAttributes.lastAccessTime(), basicAttributes.creationTime());
if (attribueViews.contains(PosixFileAttributeView.class)) {
PosixFileAttributes posixAttributes = Files.readAttributes(target, PosixFileAttributes.class, linkOptions);
PosixFileAttributeView posixView = Files.getFileAttributeView(target, PosixFileAttributeView.class, linkOptions);
posixView.setPermissions(posixAttributes.permissions());
copyOwner(source, target, sameFileSystem, posixAttributes, posixView, linkOptions);
copyGroup(source, target, sameFileSystem, posixAttributes, posixView, linkOptions);
}
if (attribueViews.contains(UserDefinedFileAttributeView.class)) {
UserDefinedFileAttributeView sourceAttributes = Files.getFileAttributeView(source, UserDefinedFileAttributeView.class, linkOptions);
UserDefinedFileAttributeView targeAttributes = Files.getFileAttributeView(target, UserDefinedFileAttributeView.class, linkOptions);
// try to reuse the buffer
// TODO reuse buffer across files
ByteBuffer buffer = null;
for (String each : sourceAttributes.list()) {
int size = sourceAttributes.size(each);
if (buffer == null) {
buffer = ByteBuffer.allocate(size);
} else {
buffer.reset();
if (buffer.capacity() < size) {
buffer = ByteBuffer.allocate(size);
}
}
int read = sourceAttributes.read(each, buffer);
if (read != size) {
throw new FileSystemException(source.toString(), null,"could not read attribute: " + each);
}
buffer.flip();
int written = targeAttributes.write(each, buffer);
if (written != size) {
throw new FileSystemException(target.toString(), null, "could not read attribute: " + each);
}
}
}
if (attribueViews.contains(DosFileAttributeView.class)) {
DosFileAttributes dosAttributes = Files.readAttributes(target, DosFileAttributes.class, linkOptions);
DosFileAttributeView dosView = Files.getFileAttributeView(target, DosFileAttributeView.class, linkOptions);
dosView.setArchive(dosAttributes.isArchive());
dosView.setHidden(dosAttributes.isHidden());
dosView.setSystem(dosAttributes.isSystem());
dosView.setReadOnly(dosAttributes.isReadOnly());
}
}
private static void copyOwner(Path source, Path target, boolean sameFileSystem, PosixFileAttributes posixAttributes, PosixFileAttributeView posixView, LinkOption[] linkOptions) throws IOException {
UserPrincipal owner = posixAttributes.owner();
if (!sameFileSystem) {
UserPrincipalLookupService userPrincipalLookupService = source.getFileSystem().getUserPrincipalLookupService();
try {
owner = userPrincipalLookupService.lookupPrincipalByName(owner.getName());
} catch (UserPrincipalNotFoundException e) {
// user doesn't exist in target file system, ignore
return;
}
}
posixView.setOwner(owner);
}
private static void copyGroup(Path source, Path target, boolean sameFileSystem, PosixFileAttributes posixAttributes, PosixFileAttributeView posixView, LinkOption[] linkOptions) throws IOException {
GroupPrincipal group = posixAttributes.group();
if (!sameFileSystem) {
UserPrincipalLookupService userPrincipalLookupService = source.getFileSystem().getUserPrincipalLookupService();
try {
group = userPrincipalLookupService.lookupPrincipalByGroupName(group.getName());
} catch (UserPrincipalNotFoundException e) {
// user doesn't exist in target file system, ignore
return;
}
}
posixView.setGroup(group);
}
static final class DirectoryCopier extends SimpleFileVisitor<Path> {
private final Path source;
private final Path target;
private final LinkOption[] linkOptions;
private final CopyOption[] copyOptions;
private final boolean sameFileSystem;
private final boolean copyAttribues;
private final Set<Class<? extends FileAttributeView>> supportedAttribueViews;
DirectoryCopier(Path source, Path target, CopyOption[] copyOptions, LinkOption[] linkOptions, Set<Class<? extends FileAttributeView>> supportedAttribueViews, boolean sameFileSystem, boolean copyAttribues) {
this.source = source;
this.target = target;
this.copyOptions = copyOptions;
this.linkOptions = linkOptions;
this.supportedAttribueViews = supportedAttribueViews;
this.sameFileSystem = sameFileSystem;
this.copyAttribues = copyAttribues;
}
private Path relativize(Path path) {
Path relativized = this.source.relativize(path);
if (this.sameFileSystem) {
// TODO would same provider be enough?
return this.target.resolve(relativized);
} else {
return this.target.resolve(relativized.toString());
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, this.relativize(file), this.copyOptions);
return CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!dir.equals(dir.getRoot())) {
// skip creating root on target file system
Files.createDirectory(this.relativize(dir));
}
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (this.copyAttribues) {
copyAttributes(this.source, this.relativize(dir), this.sameFileSystem, this.supportedAttribueViews, this.linkOptions);
}
return CONTINUE;
}
}
}