From f39c974e747d5d546c3c611eb00896aa4d4b90c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Fri, 17 May 2024 17:24:26 +0200 Subject: [PATCH] starting work on jadaptive ssh client --- files-sftp-jadaptive-fs/build.gradle | 5 + .../src/main/java/module-info.java | 3 + .../sftp/jadaptive/fs/SftpFileSystem.java | 74 +++ .../jadaptive/fs/SftpFileSystemProvider.java | 4 + .../sftp/jadaptive/fs/spi/SFTPContext.java | 39 ++ .../jadaptive/fs/spi/SFTPFileService.java | 476 ++++++++++++++++++ .../fs/spi/SFTPFileServiceProvider.java | 15 + .../sftp/jadaptive/fs/spi/WithContext.java | 7 + settings.gradle | 2 + 9 files changed, 625 insertions(+) create mode 100644 files-sftp-jadaptive-fs/build.gradle create mode 100644 files-sftp-jadaptive-fs/src/main/java/module-info.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystem.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystemProvider.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPContext.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileService.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileServiceProvider.java create mode 100644 files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/WithContext.java diff --git a/files-sftp-jadaptive-fs/build.gradle b/files-sftp-jadaptive-fs/build.gradle new file mode 100644 index 0000000..3fa236c --- /dev/null +++ b/files-sftp-jadaptive-fs/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':files-api') + api libs.maverick.synergy.client + testImplementation libs.net.security +} diff --git a/files-sftp-jadaptive-fs/src/main/java/module-info.java b/files-sftp-jadaptive-fs/src/main/java/module-info.java new file mode 100644 index 0000000..70abebd --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.xbib.files.sftp.jadaptive.fs { + requires org.xbib.files; +} \ No newline at end of file diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystem.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystem.java new file mode 100644 index 0000000..fd8b5a1 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystem.java @@ -0,0 +1,74 @@ +package org.xbib.files.sftp.jadaptive.fs; + +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.Set; + +public class SftpFileSystem extends FileSystem { + + @Override + public FileSystemProvider provider() { + return null; + } + + @Override + public void close() throws IOException { + + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public String getSeparator() { + return ""; + } + + @Override + public Iterable getRootDirectories() { + return null; + } + + @Override + public Iterable getFileStores() { + return null; + } + + @Override + public Set supportedFileAttributeViews() { + return Set.of(); + } + + @Override + public Path getPath(String s, String... strings) { + return null; + } + + @Override + public PathMatcher getPathMatcher(String s) { + return null; + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + return null; + } + + @Override + public WatchService newWatchService() throws IOException { + return null; + } +} diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystemProvider.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystemProvider.java new file mode 100644 index 0000000..9bf25b7 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/SftpFileSystemProvider.java @@ -0,0 +1,4 @@ +package org.xbib.files.sftp.jadaptive.fs; + +public class SftpFileSystemProvider { +} diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPContext.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPContext.java new file mode 100644 index 0000000..c29bb28 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPContext.java @@ -0,0 +1,39 @@ +package org.xbib.files.sftp.jadaptive.fs.spi; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +import org.xbib.files.sftp.jadaptive.fs.SftpFileSystem; +import org.xbib.files.sftp.jadaptive.fs.SftpFileSystemProvider; + +public class SFTPContext implements Closeable { + + final SftpFileSystemProvider provider; + + final SftpFileSystem fileSystem; + + public SFTPContext(URI uri, Map env) throws IOException { + this.sshClient = ClientBuilder.builder().build(); + Object object = env.get("workers"); + if (object instanceof Integer) { + sshClient.setNioWorkers((Integer) object); + } else if (object instanceof String) { + sshClient.setNioWorkers(Integer.parseInt((String) object)); + } else { + // we do not require a vast pool of threads + sshClient.setNioWorkers(1); + } + sshClient.start(); + this.provider = new SftpFileSystemProvider(sshClient); + this.fileSystem = provider.newFileSystem(uri, env); + } + + @Override + public void close() throws IOException { + sshClient.stop(); + sshClient.close(); + fileSystem.close(); + } +} diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileService.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileService.java new file mode 100644 index 0000000..cb24e03 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileService.java @@ -0,0 +1,476 @@ +package org.xbib.files.sftp.jadaptive.fs.spi; + +import org.xbib.files.FileService; +import org.xbib.files.FileWalker; +import org.xbib.files.WrappedDirectoryStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Instant; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class SFTPFileService implements FileService { + + private static final int BUFFER_SIZE = 128 * 1024; + + private static final Set DEFAULT_DIR_PERMISSIONS = + PosixFilePermissions.fromString("rwxr-xr-x"); + + private static final Set DEFAULT_FILE_PERMISSIONS = + PosixFilePermissions.fromString("rw-r--r--"); + + private final URI uri; + + private final Map env; + + public SFTPFileService(URI uri, Map env) { + this.uri = uri; + this.env = env; + } + + @Override + public Boolean exists(String path) throws IOException { + return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isExecutable(String path) throws IOException { + return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isDirectory(String path) throws IOException { + return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isRegularFile(String path) throws IOException { + return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isHidden(String path) throws IOException { + return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isSameFile(String path1, String path2) throws IOException { + return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2))); + } + + @Override + public Boolean isSymbolicLink(String path) throws IOException { + return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isReadable(String path) throws IOException { + return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path))); + } + + @Override + public Boolean isWritable(String path) throws IOException { + return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path))); + } + + @Override + public void createFile(String path, FileAttribute... attributes) throws IOException { + performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes)); + } + + @Override + public void createDirectory(String path, FileAttribute... attributes) throws IOException { + performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes)); + } + + @Override + public void createDirectories(String path, FileAttribute... attributes) throws IOException { + performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes)); + } + + @Override + public void setAttribute(String path, String attribute, Object value) throws IOException { + performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value)); + } + + @Override + public Object getAttribute(String path, String attribute) throws IOException { + return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute)); + } + + @Override + public void setPermissions(String path, Set permissions) throws IOException { + performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions)); + } + + @Override + public Set getPermissions(String path) throws IOException { + return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path))); + } + + @Override + public void setLastModifiedTime(String path, Instant lastModified) throws IOException { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), FileTime.from(lastModified))); + } + + @Override + public Instant getLastModifiedTime(String path) throws IOException{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)).toInstant()); + } + + @Override + public void setPosixFileAttributes(String path, + String owner, + String group, + Instant lastModifiedTime, + Instant lastAccessTime, + Instant createTime) throws IOException { + performWithContext(ctx -> { + PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + view.setOwner(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner)); + view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group)); + view.setTimes(FileTime.from(lastModifiedTime), + FileTime.from(lastAccessTime), + FileTime.from(createTime)); + return null; + }); + } + + @Override + public PosixFileAttributes getPosixFileAttributes(String path) throws IOException { + return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes()); + } + + @Override + public void setOwner(String path, String owner) throws IOException { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), + ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner))); + } + + @Override + public String getOwner(String path) throws IOException { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)).getName()); + } + + @Override + public void setGroup(String path, String group) throws IOException { + performWithContext(ctx -> { + PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group)); + return null; + }); + } + + @Override + public String getGroup(String path) throws IOException { + return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes().group().getName()); + } + + @Override + public void upload(Path source, + String target, + CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + @Override + public void upload(Path source, + String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + doUpload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + @Override + public void upload(Path source, + Path target, + CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + @Override + public void upload(Path source, + Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + doUpload(ctx, Files.newByteChannel(source), target, dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + @Override + public void upload(InputStream source, + String target, + CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + @Override + public void upload(InputStream source, + Path target, + CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + + @Override + public void download(Path source, + Path target, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + download(ctx, source, target, copyOptions); + return null; + }); + } + + @Override + public void download(String source, + Path target, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, copyOptions); + return null; + }); + } + + @Override + public void download(Path source, + OutputStream target) throws IOException { + performWithContext(ctx -> { + download(ctx, source, target); + return null; + }); + } + + @Override + public void download(String source, + OutputStream target) throws IOException { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target); + return null; + }); + } + + @Override + public void copy(String source, String target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions)); + } + + @Override + public void rename(String source, String target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions)); + } + + @Override + public void remove(String source) throws IOException { + performWithContext(ctx -> Files.deleteIfExists(ctx.fileSystem.getPath(source))); + } + + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @Override + public Stream list(String path) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.list(new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path)))); + } + + @Override + public Stream walk(String path, FileVisitOption... options) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), Integer.MAX_VALUE, options); + } + + @Override + public Stream walk(String path, int maxdepth, FileVisitOption... options) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options); + } + + @Override + public void upload(InputStream source, + Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + doUpload(ctx, Channels.newChannel(source), target, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + @Override + public void upload(InputStream source, + String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + doUpload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + private void doUpload(SFTPContext ctx, + ReadableByteChannel source, + Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions))); + } + + private void download(SFTPContext ctx, + Path source, + OutputStream outputStream) throws IOException { + download(ctx, source, Channels.newChannel(outputStream)); + } + + private void download(SFTPContext ctx, + Path source, + WritableByteChannel writableByteChannel) throws IOException { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel); + } + + private void download(SFTPContext ctx, + Path source, + Path target, + CopyOption... copyOptions) throws IOException { + prepareForRead(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions))); + } + + private void prepareForRead(Path path) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + } + if (!Files.exists(path)) { + Files.createFile(path); + } + } + + private void prepareForWrite(Path path, + Set dirPerms, + Set filePerms) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(parent, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(dirPerms); + } + if (!Files.exists(path)) { + Files.createFile(path); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(path, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(filePerms); + } + + private Set prepareReadOptions(CopyOption... copyOptions) { + // ignore user copy options + return EnumSet.of(StandardOpenOption.READ); + } + + private Set prepareWriteOptions(CopyOption... copyOptions) { + Set options = null; + for (CopyOption copyOption : copyOptions) { + if (copyOption == StandardCopyOption.REPLACE_EXISTING) { + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + if (options == null) { + // we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile() + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } + return options; + } + + private void transfer(ReadableByteChannel readableByteChannel, + WritableByteChannel writableByteChannel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + int read; + while ((read = readableByteChannel.read(buffer)) > 0) { + buffer.flip(); + while (read > 0) { + read -= writableByteChannel.write(buffer); + } + buffer.clear(); + } + } + + private T performWithContext(WithContext action) throws IOException { + SFTPContext ctx = null; + try { + if (uri != null) { + ctx = new SFTPContext(uri, env); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileServiceProvider.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileServiceProvider.java new file mode 100644 index 0000000..4f4bb78 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/SFTPFileServiceProvider.java @@ -0,0 +1,15 @@ +package org.xbib.files.sftp.jadaptive.fs.spi; + +import org.xbib.files.FileService; +import org.xbib.files.FileServiceProvider; + +import java.net.URI; +import java.util.Map; + +public class SFTPFileServiceProvider implements FileServiceProvider { + + @Override + public FileService provide(URI uri, Map env) { + return uri.isAbsolute() && uri.getScheme().equals("sftp") ? new SFTPFileService(uri, env) : null; + } +} diff --git a/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/WithContext.java b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/WithContext.java new file mode 100644 index 0000000..26efd15 --- /dev/null +++ b/files-sftp-jadaptive-fs/src/main/java/org/xbib/files/sftp/jadaptive/fs/spi/WithContext.java @@ -0,0 +1,7 @@ +package org.xbib.files.sftp.jadaptive.fs.spi; + +import java.io.IOException; + +interface WithContext { + T perform(SFTPContext ctx) throws IOException; +} diff --git a/settings.gradle b/settings.gradle index cd68f03..1ef6ac5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ dependencyResolutionManagement { version('gradle', '8.7') version('net', '4.4.0') library('net-security', 'org.xbib', 'net-security').versionRef('net') + library('maverick-synergy-client', 'com.sshtools', 'maverick-synergy-client').version('3.1.1') } testLibs { version('junit', '5.10.2') @@ -41,5 +42,6 @@ include 'files-ftp-fs' include 'files-ftp-mock' include 'files-sftp' include 'files-sftp-fs' +include 'files-sftp-jadaptive-fs' include 'files-webdav' include 'files-webdav-fs'