diff --git a/files-api/src/main/java/module-info.java b/files-api/src/main/java/module-info.java new file mode 100644 index 0000000..b41f910 --- /dev/null +++ b/files-api/src/main/java/module-info.java @@ -0,0 +1,8 @@ +import org.xbib.files.DefaultFilesProvider; +import org.xbib.files.Provider; + +module org.xbib.files { + exports org.xbib.files; + uses Provider; + provides Provider with DefaultFilesProvider; +} diff --git a/files-api/src/main/java/org/xbib/files/DefaultFiles.java b/files-api/src/main/java/org/xbib/files/DefaultFiles.java new file mode 100644 index 0000000..1f3f157 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/DefaultFiles.java @@ -0,0 +1,332 @@ +package org.xbib.files; + +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.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +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.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import java.nio.file.Files; + +public class DefaultFiles implements org.xbib.files.Files { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_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 DefaultFiles(URI uri, Map env) { + this.uri = uri; + this.env = env; + } + + @Override + public Boolean exists(String path) { + return Files.exists(Paths.get(path)); + } + + @Override + public Boolean isExecutable(String path) { + return Files.isExecutable(Paths.get(path)); + } + + @Override + public Boolean isDirectory(String path) { + return Files.isDirectory(Paths.get(path)); + } + + @Override + public Boolean isRegularFile(String path) { + return Files.isRegularFile(Paths.get(path)); + } + + @Override + public Boolean isHidden(String path) throws IOException { + return Files.isHidden(Paths.get(path)); + } + + @Override + public Boolean isSameFile(String path1, String path2) throws IOException { + return Files.isSameFile(Paths.get(path1), Paths.get(path2)); + } + + @Override + public Boolean isSymbolicLink(String path) { + return Files.isSymbolicLink(Paths.get(path)); + } + + @Override + public Boolean isReadable(String path) { + return Files.isReadable(Paths.get(path)); + } + + @Override + public Boolean isWritable(String path) { + return Files.isWritable(Paths.get(path)); + } + + @Override + public void setAttribute(String path, String attribute, Object value) throws IOException { + Files.setAttribute(Paths.get(path), attribute, value); + } + + @Override + public Object getAttribute(String path, String attribute) throws IOException { + return Files.getAttribute(Paths.get(path), attribute); + } + + @Override + public void setPermissions(String path, Set permissions) throws IOException { + Files.setPosixFilePermissions(Paths.get(path), permissions); + } + + @Override + public Set getPermissions(String path) throws IOException { + return Files.getPosixFilePermissions(Paths.get(path)); + } + + @Override + public void setOwner(String path, UserPrincipal userPrincipal) throws IOException { + Files.setOwner(Paths.get(path), userPrincipal); + } + + @Override + public UserPrincipal getOwner(String path) throws IOException { + return Files.getOwner(Paths.get(path)); + } + + @Override + public void setLastModifiedTime(String path, FileTime fileTime) throws IOException { + Files.setLastModifiedTime(Paths.get(path), fileTime); + } + + @Override + public FileTime getLastModifiedTime(String path) throws IOException { + return Files.getLastModifiedTime(Paths.get(path)); + } + + @Override + public void createFile(String path, FileAttribute... attributes) throws IOException { + Files.createFile(Paths.get(path), attributes); + } + + @Override + public void createDirectory(String path, FileAttribute... attributes) throws IOException { + Files.createDirectory(Paths.get(path), attributes); + } + + @Override + public void createDirectories(String path, FileAttribute... attributes) throws IOException { + Files.createDirectories(Paths.get(path), attributes); + } + + @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, String target, CopyOption... copyOptions) throws IOException { + upload(source, Paths.get(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 upload(InputStream source, String target, CopyOption... copyOptions) throws IOException { + upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + @Override + public void download(Path source, Path target, CopyOption... copyOptions) throws IOException { + download(source, target, READ_BUFFER_SIZE, copyOptions); + } + + @Override + public void download(String source, Path target, CopyOption... copyOptions) throws IOException { + download(Paths.get(source), target, READ_BUFFER_SIZE, copyOptions); + } + + @Override + public void download(Path source, OutputStream target) throws IOException { + download(source, target, READ_BUFFER_SIZE); + } + + @Override + public void download(String source, OutputStream target) throws IOException { + download(Paths.get(source), target, READ_BUFFER_SIZE); + } + + @Override + public void rename(String source, String target, CopyOption... copyOptions) throws IOException { + Files.move(Paths.get(source), Paths.get(target), copyOptions); + } + + @Override + public void remove(String source) throws IOException { + Files.deleteIfExists(Paths.get(source)); + } + + @Override + public void copy(String source, String target, CopyOption... copyOptions) throws IOException { + Files.copy(Paths.get(source), Paths.get(target), copyOptions); + } + + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + return Files.newDirectoryStream(Paths.get(path), glob); + } + + @Override + public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { + return Files.newDirectoryStream(Paths.get(path), filter); + } + + private void upload(Path source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + upload(Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions); + } + + private void upload(InputStream source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + upload(Channels.newChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions); + } + + private void upload(ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(Path source, + OutputStream outputStream, + int bufferSize) throws IOException { + download(source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target); + transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void prepareForWrite(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, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + int read; + while ((read = readableByteChannel.read(buffer)) > 0) { + buffer.flip(); + while (read > 0) { + read -= writableByteChannel.write(buffer); + } + buffer.clear(); + } + } +} diff --git a/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java b/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java new file mode 100644 index 0000000..13f97bd --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java @@ -0,0 +1,11 @@ +package org.xbib.files; + +import java.net.URI; +import java.util.Map; + +public class DefaultFilesProvider implements Provider { + @Override + public Files provide(URI uri, Map env) { + return !uri.isAbsolute() || uri.getScheme().equals("file") ? new DefaultFiles(uri, env) : null; + } +} diff --git a/files-api/src/main/java/org/xbib/files/Files.java b/files-api/src/main/java/org/xbib/files/Files.java new file mode 100644 index 0000000..b10afff --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/Files.java @@ -0,0 +1,105 @@ +package org.xbib.files; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; + +public interface Files { + + class Holder { + + private static Files createFiles(URI uri, Map env) { + ServiceLoader serviceLoader = ServiceLoader.load(Provider.class); + Optional first = serviceLoader.stream() + .map(ServiceLoader.Provider::get) + .filter(Objects::nonNull) + .map(p -> p.provide(uri, env)) + .filter(Objects::nonNull) + .findFirst(); + return first.orElse(null); + } + } + + default Files newInstance(URI uri, Map env) { + return Holder.createFiles(uri, env); + } + + Boolean exists(String path) throws IOException; + + Boolean isExecutable(String path) throws IOException; + + Boolean isDirectory(String path) throws IOException; + + Boolean isRegularFile(String path) throws IOException; + + Boolean isHidden(String path) throws IOException; + + Boolean isSameFile(String path1, String path2) throws IOException; + + Boolean isSymbolicLink(String path) throws IOException; + + Boolean isReadable(String path) throws IOException; + + Boolean isWritable(String path) throws IOException; + + void setAttribute(String path, String attribute, Object value) throws IOException; + + Object getAttribute(String path, String attribute) throws IOException; + + void setPermissions(String path, Set permissions) throws IOException; + + Set getPermissions(String path) throws IOException; + + void setOwner(String path, UserPrincipal userPrincipal) throws IOException; + + UserPrincipal getOwner(String path) throws IOException; + + void setLastModifiedTime(String path, FileTime fileTime) throws IOException; + + FileTime getLastModifiedTime(String path) throws IOException; + + void createFile(String path, FileAttribute... attributes) throws IOException; + + void createDirectory(String path, FileAttribute... attributes) throws IOException; + + void createDirectories(String path, FileAttribute... attributes) throws IOException; + + void upload(Path source, Path target, CopyOption... copyOptions) throws IOException; + + void upload(Path source, String target, CopyOption... copyOptions) throws Exception; + + void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException; + + void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException; + + void download(Path source, Path target, CopyOption... copyOptions) throws IOException; + + void download(String source, Path target, CopyOption... copyOptions) throws IOException; + + void download(Path source, OutputStream target) throws IOException; + + void download(String source, OutputStream target) throws IOException; + + void rename(String source, String target, CopyOption... copyOptions) throws IOException; + + void remove(String source) throws IOException; + + void copy(String source, String target, CopyOption... copyOptions) throws IOException; + + DirectoryStream stream(String path, String glob) throws IOException; + + DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException; +} diff --git a/files-api/src/main/java/org/xbib/files/Provider.java b/files-api/src/main/java/org/xbib/files/Provider.java new file mode 100644 index 0000000..7cb8e83 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/Provider.java @@ -0,0 +1,9 @@ +package org.xbib.files; + +import java.net.URI; +import java.util.Map; + +public interface Provider { + + Files provide(URI uri, Map env); +} diff --git a/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider new file mode 100644 index 0000000..4c9449a --- /dev/null +++ b/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider @@ -0,0 +1 @@ +org.xbib.files.DefaultFilesProvider \ No newline at end of file diff --git a/files-ftp-fs/build.gradle b/files-ftp-fs/build.gradle index 23eaa6a..4be1784 100644 --- a/files-ftp-fs/build.gradle +++ b/files-ftp-fs/build.gradle @@ -1,4 +1,5 @@ dependencies { + api project(':files-api') api project(':files-ftp') testImplementation "org.mockftpserver:MockFtpServer:${project.property('mockftpserver.version')}" testImplementation "org.junit.jupiter:junit-jupiter-params:${project.property('junit.version')}" diff --git a/files-ftp-fs/src/main/java/module-info.java b/files-ftp-fs/src/main/java/module-info.java index 15186f8..ebcf219 100644 --- a/files-ftp-fs/src/main/java/module-info.java +++ b/files-ftp-fs/src/main/java/module-info.java @@ -1,5 +1,11 @@ +import java.nio.file.spi.FileSystemProvider; +import org.xbib.files.Provider; +import org.xbib.io.ftp.fs.FTPFileSystemProvider; +import org.xbib.io.ftp.fs.spi.FTPFilesProvider; + module org.xbib.files.ftp.fs { + requires org.xbib.files; requires org.xbib.files.ftp; - provides java.nio.file.spi.FileSystemProvider - with org.xbib.io.ftp.fs.FTPFileSystemProvider; + provides FileSystemProvider with FTPFileSystemProvider; + provides Provider with FTPFilesProvider; } diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java new file mode 100644 index 0000000..734eab8 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java @@ -0,0 +1,25 @@ +package org.xbib.io.ftp.fs.spi; + +import org.xbib.io.ftp.fs.FTPEnvironment; +import org.xbib.io.ftp.fs.FTPFileSystemProvider; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.util.Map; + +class FTPContext { + + final FTPFileSystemProvider provider; + + final FileSystem fileSystem; + + FTPContext(URI uri, Map env) throws IOException { + this.provider = new FTPFileSystemProvider(); + this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment()); + } + + void close() throws IOException { + fileSystem.close(); + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java new file mode 100644 index 0000000..795602f --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java @@ -0,0 +1,391 @@ +package org.xbib.io.ftp.fs.spi; + +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.Files; +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.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +public class FTPFiles implements org.xbib.files.Files { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_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 FTPFiles(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, FileTime fileTime) throws IOException { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime)); + } + + @Override + public FileTime getLastModifiedTime(String path) throws IOException{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path))); + } + + @Override + public void setOwner(String path, UserPrincipal userPrincipal) throws IOException { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal)); + } + + @Override + public UserPrincipal getOwner(String path) throws IOException { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); + } + + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { + return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @Override + public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(Path source, String target, CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void download(Path source, Path target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(String source, Path target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(Path source, OutputStream target) throws IOException { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE); + return null; + }); + } + + public void download(String source, OutputStream target) throws IOException { + performWithContext(ctx -> { + Files.copy(ctx.fileSystem.getPath(source), target); + return null; + }); + } + + public void copy(String source, String target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void rename(String source, String target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void remove(String source) throws IOException { + performWithContext(ctx -> { + Files.deleteIfExists(ctx.fileSystem.getPath(source)); + return null; + }); + } + + private void upload(FTPContext ctx, + ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(FTPContext ctx, + Path source, + OutputStream outputStream, + int bufferSize) throws IOException { + download(ctx, source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(FTPContext ctx, + Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(FTPContext ctx, + Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void prepareForWrite(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, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + 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 { + FTPContext ctx = null; + try { + if (uri != null) { + ctx = new FTPContext(uri, env); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java new file mode 100644 index 0000000..994c41d --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java @@ -0,0 +1,14 @@ +package org.xbib.io.ftp.fs.spi; + +import org.xbib.files.Files; +import org.xbib.files.Provider; + +import java.net.URI; +import java.util.Map; + +public class FTPFilesProvider implements Provider { + @Override + public Files provide(URI uri, Map env) { + return !uri.isAbsolute() || uri.getScheme().equals("file") ? new FTPFiles(uri, env) : null; + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/WithContext.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/WithContext.java new file mode 100644 index 0000000..b39eb16 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/WithContext.java @@ -0,0 +1,7 @@ +package org.xbib.io.ftp.fs.spi; + +import java.io.IOException; + +interface WithContext { + T perform(FTPContext ctx) throws IOException; +} diff --git a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider new file mode 100644 index 0000000..32d37d1 --- /dev/null +++ b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider @@ -0,0 +1 @@ +org.xbib.io.ftp.fs.spi.FTPFilesProvider \ No newline at end of file diff --git a/files-ftp/build.gradle b/files-ftp/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/files-sftp-fs/build.gradle b/files-sftp-fs/build.gradle index e05b722..6971ef4 100644 --- a/files-sftp-fs/build.gradle +++ b/files-sftp-fs/build.gradle @@ -1,4 +1,5 @@ dependencies { + api project(':files-api') api project(':files-sftp') testImplementation "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}" } diff --git a/files-sftp-fs/src/main/java/module-info.java b/files-sftp-fs/src/main/java/module-info.java index 9ce7d9e..d82facf 100644 --- a/files-sftp-fs/src/main/java/module-info.java +++ b/files-sftp-fs/src/main/java/module-info.java @@ -1,8 +1,13 @@ import org.apache.sshd.fs.SftpFileSystemProvider; import java.nio.file.spi.FileSystemProvider; +import org.xbib.files.Provider; +import org.apache.sshd.fs.spi.SFTPFilesProvider; module org.xbib.files.sftp.fs { exports org.apache.sshd.fs; + exports org.apache.sshd.fs.spi; + requires org.xbib.files; requires transitive org.xbib.files.sftp; provides FileSystemProvider with SftpFileSystemProvider; + provides Provider with SFTPFilesProvider; } diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java new file mode 100644 index 0000000..d03f1c5 --- /dev/null +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java @@ -0,0 +1,40 @@ +package org.apache.sshd.fs.spi; + +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.fs.SftpFileSystem; +import org.apache.sshd.fs.SftpFileSystemProvider; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +class SFTPContext { + + private final SshClient sshClient; + + final SftpFileSystemProvider provider; + + final SftpFileSystem fileSystem; + + 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); + } + + void close() throws IOException { + sshClient.stop(); + fileSystem.close(); + } +} diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java new file mode 100644 index 0000000..a4af113 --- /dev/null +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java @@ -0,0 +1,392 @@ +package org.apache.sshd.fs.spi; + +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.Files; +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.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +public class SFTPFiles implements org.xbib.files.Files { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_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 SFTPFiles(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, FileTime fileTime) throws IOException { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime)); + } + + @Override + public FileTime getLastModifiedTime(String path) throws IOException{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path))); + } + + @Override + public void setOwner(String path, UserPrincipal userPrincipal) throws IOException { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal)); + } + + @Override + public UserPrincipal getOwner(String path) throws IOException { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); + } + + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { + return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @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, 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 upload(InputStream source, String 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, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + @Override + public void download(String source, Path target, CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + @Override + public void download(Path source, OutputStream target) throws IOException { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE); + return null; + }); + } + + @Override + public void download(String source, OutputStream target) throws IOException { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE); + 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))); + } + + public void upload(Path source, Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(Path source, String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(InputStream source, Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(InputStream source, String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + private void upload(SFTPContext ctx, + ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + OutputStream outputStream, + int bufferSize) throws IOException { + download(ctx, source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws IOException { + prepareForRead(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + 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, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + 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-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java new file mode 100644 index 0000000..4210243 --- /dev/null +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java @@ -0,0 +1,14 @@ +package org.apache.sshd.fs.spi; + +import org.xbib.files.Files; +import org.xbib.files.Provider; + +import java.net.URI; +import java.util.Map; + +public class SFTPFilesProvider implements Provider { + @Override + public Files provide(URI uri, Map env) { + return !uri.isAbsolute() || uri.getScheme().equals("file") ? new SFTPFiles(uri, env) : null; + } +} diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/WithContext.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/WithContext.java new file mode 100644 index 0000000..c11eeba --- /dev/null +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/WithContext.java @@ -0,0 +1,7 @@ +package org.apache.sshd.fs.spi; + +import java.io.IOException; + +interface WithContext { + T perform(SFTPContext ctx) throws IOException; +} diff --git a/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider new file mode 100644 index 0000000..74789f6 --- /dev/null +++ b/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider @@ -0,0 +1 @@ +org.apache.sshd.fs.spi.SFTPFilesProvider \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 03e1221..24290ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ +include 'files-api' include 'files-eddsa' include 'files-zlib' include 'files-ftp'