starting work on jadaptive ssh client

This commit is contained in:
Jörg Prante 2024-05-17 17:24:26 +02:00
parent 0f0f6bfa09
commit f39c974e74
9 changed files with 625 additions and 0 deletions

View file

@ -0,0 +1,5 @@
dependencies {
api project(':files-api')
api libs.maverick.synergy.client
testImplementation libs.net.security
}

View file

@ -0,0 +1,3 @@
module org.xbib.files.sftp.jadaptive.fs {
requires org.xbib.files;
}

View file

@ -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<Path> getRootDirectories() {
return null;
}
@Override
public Iterable<FileStore> getFileStores() {
return null;
}
@Override
public Set<String> 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;
}
}

View file

@ -0,0 +1,4 @@
package org.xbib.files.sftp.jadaptive.fs;
public class SftpFileSystemProvider {
}

View file

@ -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<String, ?> 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();
}
}

View file

@ -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<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
PosixFilePermissions.fromString("rw-r--r--");
private final URI uri;
private final Map<String, ?> env;
public SFTPFileService(URI uri, Map<String, ?> 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<PosixFilePermission> permissions) throws IOException {
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
}
@Override
public Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> 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<Path> 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<Path> stream(String path, DirectoryStream.Filter<Path> filter) throws IOException {
SFTPContext ctx = new SFTPContext(uri, env);
return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter));
}
@Override
public Stream<Path> 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<Path> 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<Path> 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<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
// ignore user copy options
return EnumSet.of(StandardOpenOption.READ);
}
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
Set<? extends OpenOption> 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> T performWithContext(WithContext<T> 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();
}
}
}
}

View file

@ -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<String, ?> env) {
return uri.isAbsolute() && uri.getScheme().equals("sftp") ? new SFTPFileService(uri, env) : null;
}
}

View file

@ -0,0 +1,7 @@
package org.xbib.files.sftp.jadaptive.fs.spi;
import java.io.IOException;
interface WithContext<T> {
T perform(SFTPContext ctx) throws IOException;
}

View file

@ -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'