I'm creating a command line application that needs to output some files (more than one) into either a ZIP file or a plain folder depending on the paramters given.
My approach is to encapsulate the target (plain folder/ZIP file) with a FileSystem
.
My problem is that I cannot sucessfully create a FileSystem
object for a directory other than the current working directory denoting an absolute path on my hard disk:
public class FileSystemWriteTest {
public static void main(String[] args) throws IOException {
Path absolutePath = Paths.get("target", "testpath").toAbsolutePath();
System.out.println(String.format("user.dir before change:\n %s", System.getProperty("user.dir")));
System.setProperty("user.dir", absolutePath.toString());
System.out.println(String.format("changed user.dir:\n %s", System.getProperty("user.dir")));
FileSystem defaultSystem = FileSystems.getDefault();
Path testFilePath = defaultSystem.getPath("test.file");
System.out.println(String.format("expected to be in changed user.dir:\n %s", testFilePath.toAbsolutePath()));
URI uri = absolutePath.toUri();
System.out.println(String.format("URI: %s", uri));
FileSystem localFileSystem =
FileSystems.newFileSystem(uri, Collections.emptyMap());
Path file = localFileSystem.getPath("test.txt");
System.out.println(file.toAbsolutePath());
}
}
The output is:
user.dir before change:
D:\data\scm-workspace\anderes\Test
changed user.dir:
D:\data\scm-workspace\anderes\Test\target\testpath
expected to be in changed user.dir:
D:\data\scm-workspace\anderes\Test\test.file
URI: file:///D:/data/scm-workspace/anderes/Test/target/testpath/
Exception in thread "main" java.lang.IllegalArgumentException: Path component should be '/'
at sun.nio.fs.WindowsFileSystemProvider.checkUri(Unknown Source)
at sun.nio.fs.WindowsFileSystemProvider.newFileSystem(Unknown Source)
at java.nio.file.FileSystems.newFileSystem(Unknown Source)
at java.nio.file.FileSystems.newFileSystem(Unknown Source)
at com.oc.test.filesystem.FileSystemWriteTest.main(FileSystemWriteTest.java:27)
If I change to FileSystems.newFileSystem(Path, Classloader)
the Exception changes to:
Exception in thread "main" java.nio.file.ProviderNotFoundException: Provider not found
at java.nio.file.FileSystems.newFileSystem(Unknown Source)
at com.oc.test.filesystem.FileSystemWriteTest.main(FileSystemWriteTest.java:27)
Looks like this only works with regular files, not with directories.
So how can I create a FileSystem
object for a directory other than pwd?
There is no built facility for creating a FileSystem
with chroot
like semantics. The default file system only supports file:///
as URI and doesn’t allow more than one instantiation.
In this regard, FileSystems.getDefault().getPath("test.file")
creates a relative path, just like Paths.get("test.file")
. The difference between relative paths created for the default file system and other file systems lies in the resolve behavior when no other base path has been specified (e.g. when calling toAbsolutePath()
or just trying to open them). But being resolved against the current working directory does not make them a root path.
The best solution for implementing file system agnostic operations, is to let the code receive base Path
objects, to resolve relative paths against.
E.g. a simple tree copy routine may look like:
static void copyTree(Path sourceBase, Path targetBase) throws IOException {
try {
Files.walk(sourceBase).forEach(path -> {
if(Files.isRegularFile(path)) try {
Path target = targetBase.resolve(sourceBase.relativize(path).toString());
if(!Files.isDirectory(target.getParent()))
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.COPY_ATTRIBUTES);
} catch(IOException ex) {
throw new UncheckedIOException(ex);
}
});
} catch(UncheckedIOException ex) {
throw ex.getCause();
}
}
For this method, it doesn’t matter whether you’re copying from a harddrive directory to another harddrive directory or to a zip filesystem, or from a zip filesystem to the harddrive, or from a zip file to another zip file, etc.
The most interesting part is the invocation of sourceBase.relativize(path)
, to get the relative path from the source base path to the sub-path of the actual file. Since relative Path
instances still are tied to a particular filesystem, the code invokes toString()
, before passing it to targetBase.resolve(…)
, to ensure that it will work across different filesystems. Note that path.resolve(string)
is equivalent to path.resolve(path.getFileSystem().getPath(string))
. It would be legitimate, if the method first checks whether both Path
instances belong to the same filesystem, to skip the String
detour in that case.