diff --git a/files-ftp-fs/build.gradle b/files-ftp-fs/build.gradle index 84bbd00..3e55da1 100644 --- a/files-ftp-fs/build.gradle +++ b/files-ftp-fs/build.gradle @@ -4,7 +4,6 @@ dependencies { testImplementation testLibs.junit.jupiter.params testImplementation testLibs.mockito.core testImplementation testLibs.mockito.junit.jupiter - testImplementation testLibs.slf4j testImplementation project(':files-ftp-mock') } diff --git a/files-ftp-fs/src/main/java/module-info.java b/files-ftp-fs/src/main/java/module-info.java index 887eb36..852df74 100644 --- a/files-ftp-fs/src/main/java/module-info.java +++ b/files-ftp-fs/src/main/java/module-info.java @@ -1,13 +1,15 @@ import java.nio.file.spi.FileSystemProvider; import org.xbib.files.FileServiceProvider; import org.xbib.files.ftp.fs.FTPFileSystemProvider; +import org.xbib.files.ftp.fs.FTPSFileSystemProvider; import org.xbib.files.ftp.fs.spi.FTPFileServiceProvider; +import org.xbib.files.ftp.fs.spi.FTPSFileServiceProvider; module org.xbib.files.ftp.fs { requires org.xbib.files; requires org.xbib.files.ftp; exports org.xbib.files.ftp.fs; exports org.xbib.files.ftp.fs.spi; - provides FileSystemProvider with FTPFileSystemProvider; - provides FileServiceProvider with FTPFileServiceProvider; + provides FileSystemProvider with FTPFileSystemProvider, FTPSFileSystemProvider; + provides FileServiceProvider with FTPFileServiceProvider, FTPSFileServiceProvider; } diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPClientPool.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPClientPool.java index c6120a6..cb2051f 100644 --- a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPClientPool.java +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPClientPool.java @@ -20,24 +20,27 @@ import java.util.concurrent.BlockingQueue; /** * A pool of FTP clients, allowing multiple commands to be executed concurrently. */ -final class FTPClientPool { +class FTPClientPool { - private final String hostname; - private final int port; + protected final String hostname; + protected final int port; + protected final FTPEnvironment env; - private final FTPEnvironment env; - private final FileSystemExceptionFactory exceptionFactory; + private FileSystemExceptionFactory exceptionFactory; - private final BlockingQueue pool; + private BlockingQueue pool; FTPClientPool(String hostname, int port, FTPEnvironment env) throws IOException { this.hostname = hostname; this.port = port; this.env = env.clone(); + init(); + } + + protected void init() throws IOException { this.exceptionFactory = env.getExceptionFactory(); final int poolSize = env.getClientConnectionCount(); this.pool = new ArrayBlockingQueue<>(poolSize); - try { for (int i = 0; i < poolSize; i++) { pool.add(new Client(true)); @@ -148,21 +151,23 @@ final class FTPClientPool { pool.add(client); } - final class Client implements Closeable { - - private final FTPClient client; - private final boolean pooled; + class Client implements Closeable { + protected final boolean pooled; + private FTPClient client; private FileType fileType; private FileStructure fileStructure; private FileTransferMode fileTransferMode; private int refCount = 0; - private Client(boolean pooled) throws IOException { - this.client = env.createClient(hostname, port); + Client(boolean pooled) throws IOException { this.pooled = pooled; + init(); + } + protected void init() throws IOException { + this.client = env.createClient(hostname, port); this.fileType = env.getDefaultFileType(); this.fileStructure = env.getDefaultFileStructure(); this.fileTransferMode = env.getDefaultFileTransferMode(); diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPEnvironment.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPEnvironment.java index aef80eb..3ef9b60 100644 --- a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPEnvironment.java +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPEnvironment.java @@ -4,6 +4,7 @@ import org.xbib.files.ftp.FTP; import org.xbib.files.ftp.FTPClient; import org.xbib.files.ftp.FTPClientConfig; import org.xbib.files.ftp.FTPFileEntryParser; +import org.xbib.files.ftp.FTPSClient; import org.xbib.files.ftp.parser.FTPFileEntryParserFactory; import javax.net.ServerSocketFactory; @@ -320,8 +321,6 @@ public class FTPEnvironment implements Map, Cloneable { return this; } - // FTPClient - /** * Stores the timeout in milliseconds to use when reading from data connections. * @@ -566,12 +565,10 @@ public class FTPEnvironment implements Map, Cloneable { } FileStructure getDefaultFileStructure() { - // as specified by FTPClient return FileStructure.FILE; } FileTransferMode getDefaultFileTransferMode() { - // as specified by FTPClient return FileTransferMode.STREAM; } @@ -594,6 +591,15 @@ public class FTPEnvironment implements Map, Cloneable { return client; } + FTPSClient createSecureClient(String hostname, int port) throws IOException { + FTPSClient client = new FTPSClient(); + initializePreConnect(client); + connect(client, hostname, port); + initializePostConnect(client); + verifyConnection(client); + return client; + } + void initializePreConnect(FTPClient client) throws IOException { client.setListHiddenFiles(true); diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileStrategy.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileStrategy.java index 8d18851..65a40c3 100644 --- a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileStrategy.java +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileStrategy.java @@ -1,7 +1,6 @@ package org.xbib.files.ftp.fs; import org.xbib.files.ftp.FTPFile; -import org.xbib.files.ftp.FTPFileFilter; import java.io.IOException; import java.nio.file.NoSuchFileException; @@ -17,12 +16,9 @@ import java.util.List; abstract class FTPFileStrategy { static FTPFileStrategy getInstance(FTPClientPool.Client client) throws IOException { - FTPFile[] ftpFiles = client.listFiles("/", new FTPFileFilter() { - @Override - public boolean accept(FTPFile ftpFile) { - String fileName = FTPFileSystem.getFileName(ftpFile); - return FTPFileSystem.CURRENT_DIR.equals(fileName); - } + FTPFile[] ftpFiles = client.listFiles("/", ftpFile -> { + String fileName = FTPFileSystem.getFileName(ftpFile); + return FTPFileSystem.CURRENT_DIR.equals(fileName); }); return ftpFiles.length == 0 ? NonUnix.INSTANCE : Unix.INSTANCE; } @@ -67,12 +63,9 @@ abstract class FTPFileStrategy { FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException { final String name = path.fileName(); - FTPFile[] ftpFiles = client.listFiles(path.path(), new FTPFileFilter() { - @Override - public boolean accept(FTPFile ftpFile) { - String fileName = FTPFileSystem.getFileName(ftpFile); - return FTPFileSystem.CURRENT_DIR.equals(fileName) || (name != null && name.equals(fileName)); - } + FTPFile[] ftpFiles = client.listFiles(path.path(), ftpFile -> { + String fileName = FTPFileSystem.getFileName(ftpFile); + return FTPFileSystem.CURRENT_DIR.equals(fileName) || (name != null && name.equals(fileName)); }); client.throwIfEmpty(path.path(), ftpFiles); if (ftpFiles.length == 1) { @@ -94,21 +87,13 @@ abstract class FTPFileStrategy { if (ftpFile.isDirectory() && FTPFileSystem.CURRENT_DIR.equals(FTPFileSystem.getFileName(ftpFile))) { // The file is returned using getFTPFile, which returns the . (current directory) entry for directories. // List the parent (if any) instead. - final String parentPath = path.toAbsolutePath().parentPath(); final String name = path.fileName(); - if (parentPath == null) { // path is /, there is no link return null; } - - FTPFile[] ftpFiles = client.listFiles(parentPath, new FTPFileFilter() { - @Override - public boolean accept(FTPFile ftpFile) { - return (ftpFile.isDirectory() || ftpFile.isSymbolicLink()) && name.equals(FTPFileSystem.getFileName(ftpFile)); - } - }); + FTPFile[] ftpFiles = client.listFiles(parentPath, ftpFile1 -> (ftpFile1.isDirectory() || ftpFile1.isSymbolicLink()) && name.equals(FTPFileSystem.getFileName(ftpFile1))); client.throwIfEmpty(path.path(), ftpFiles); return ftpFiles[0].getLink() == null ? null : ftpFiles[0]; } @@ -122,9 +107,7 @@ abstract class FTPFileStrategy { @Override List getChildren(FTPClientPool.Client client, FTPPath path) throws IOException { - FTPFile[] ftpFiles = client.listFiles(path.path()); - boolean isDirectory = false; List children = new ArrayList<>(ftpFiles.length); for (FTPFile ftpFile : ftpFiles) { @@ -135,7 +118,6 @@ abstract class FTPFileStrategy { children.add(ftpFile); } } - if (!isDirectory && children.size() <= 1) { // either zero or one, check the parent to see if the path exists and is a directory FTPPath currentPath = path; @@ -148,7 +130,6 @@ abstract class FTPFileStrategy { throw new NotDirectoryException(path.path()); } } - return children; } diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileSystem.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileSystem.java index a36acdb..da572b9 100644 --- a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileSystem.java +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPFileSystem.java @@ -53,43 +53,51 @@ public class FTPFileSystem extends FileSystem { static final String CURRENT_DIR = "."; static final String PARENT_DIR = ".."; - private static final Set SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections + protected static final Set SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections .unmodifiableSet(new HashSet<>(Arrays.asList("basic", "owner", "posix"))); - private static final Set BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + protected static final Set BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "basic:lastModifiedTime", "basic:lastAccessTime", "basic:creationTime", "basic:size", "basic:isRegularFile", "basic:isDirectory", "basic:isSymbolicLink", "basic:isOther", "basic:fileKey"))); - private static final Set OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Collections.singletonList( + protected static final Set OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Collections.singletonList( "owner:owner"))); - private static final Set POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + protected static final Set POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "posix:lastModifiedTime", "posix:lastAccessTime", "posix:creationTime", "posix:size", "posix:isRegularFile", "posix:isDirectory", "posix:isSymbolicLink", "posix:isOther", "posix:fileKey", "posix:owner", "posix:group", "posix:permissions"))); - private final FTPFileSystemProvider provider; - private final Iterable rootDirectories; - private final FileStore fileStore; - private final Iterable fileStores; - private final FTPClientPool clientPool; - private final URI uri; - private final String defaultDirectory; - private final FTPFileStrategy ftpFileStrategy; + + protected final FTPFileSystemProvider provider; + protected final URI uri; + protected final FTPEnvironment env; + protected Iterable rootDirectories; + protected FileStore fileStore; + protected Iterable fileStores; + private FTPClientPool clientPool; + protected String defaultDirectory; + FTPFileStrategy ftpFileStrategy; private final AtomicBoolean open = new AtomicBoolean(true); public FTPFileSystem(FTPFileSystemProvider provider, URI uri, FTPEnvironment env) throws IOException { this.provider = Objects.requireNonNull(provider); + this.uri = Objects.requireNonNull(uri); + this.env = Objects.requireNonNull(env); + init(); + } + + protected void init() throws IOException { this.rootDirectories = Collections.singleton(new FTPPath(this, "/")); this.fileStore = new FTPFileStore(this); this.fileStores = Collections.singleton(fileStore); - this.clientPool = new FTPClientPool(uri.getHost(), uri.getPort(), env); - this.uri = Objects.requireNonNull(uri); - try (FTPClientPool.Client client = clientPool.get()) { this.defaultDirectory = client.pwd(); - this.ftpFileStrategy = FTPFileStrategy.getInstance(client); } } + FTPClientPool getClientPool() { + return clientPool; + } + public static String getFileName(FTPFile ftpFile) { String fileName = ftpFile.getName(); if (fileName == null) { @@ -469,7 +477,8 @@ public class FTPFileSystem extends FileSystem { getFTPFile(client, path); } String fileName = path.fileName(); - return !CURRENT_DIR.equals(fileName) && !PARENT_DIR.equals(fileName) && fileName.startsWith("."); + return !CURRENT_DIR.equals(fileName) && !PARENT_DIR.equals(fileName) && + fileName != null && fileName.startsWith("."); } public FileStore getFileStore(FTPPath path) throws IOException { @@ -492,16 +501,11 @@ public class FTPFileSystem extends FileSystem { } private boolean hasAccess(FTPFile ftpFile, AccessMode mode) { - switch (mode) { - case READ: - return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION); - case WRITE: - return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION); - case EXECUTE: - return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION); - default: - return false; - } + return switch (mode) { + case READ -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION); + case WRITE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION); + case EXECUTE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION); + }; } public PosixFileAttributes readAttributes(FTPPath path, LinkOption... options) throws IOException { diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSClientPool.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSClientPool.java new file mode 100644 index 0000000..648507b --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSClientPool.java @@ -0,0 +1,442 @@ +package org.xbib.files.ftp.fs; + +import org.xbib.files.ftp.FTPSClient; +import org.xbib.files.ftp.FTPFile; +import org.xbib.files.ftp.FTPFileFilter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.nio.file.OpenOption; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** + * A pool of FTPS clients, allowing multiple commands to be executed concurrently. + */ +class FTPSClientPool extends FTPClientPool { + + private FileSystemExceptionFactory exceptionFactory; + + private BlockingQueue pool; + + FTPSClientPool(String hostname, int port, FTPEnvironment env) throws IOException { + super(hostname, port, env); + } + + @Override + protected void init() throws IOException { + this.exceptionFactory = env.getExceptionFactory(); + final int poolSize = env.getClientConnectionCount(); + this.pool = new ArrayBlockingQueue<>(poolSize); + try { + for (int i = 0; i < poolSize; i++) { + pool.add(new Client(true)); + } + } catch (IOException e) { + // creating the pool failed, disconnect all clients + for (Client client : pool) { + try { + client.disconnect(); + } catch (IOException e2) { + e.addSuppressed(e2); + } + } + throw e; + } + } + + Client get() throws IOException { + try { + Client client = pool.take(); + try { + if (!client.isConnected()) { + client = new Client(true); + } + } catch (final Exception e) { + // could not create a new client; re-add the broken client to the pool to prevent pool starvation + pool.add(client); + throw e; + } + client.increaseRefCount(); + return client; + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + InterruptedIOException iioe = new InterruptedIOException(e.getMessage()); + iioe.initCause(e); + throw iioe; + } + } + + Client getOrCreate() throws IOException { + Client client = pool.poll(); + if (client == null) { + // nothing was taken from the pool, so no risk of pool starvation if creating the client fails + return new Client(false); + } + try { + if (!client.isConnected()) { + client = new Client(true); + } + } catch (final Exception e) { + // could not create a new client; re-add the broken client to the pool to prevent pool starvation + pool.add(client); + throw e; + } + client.increaseRefCount(); + return client; + } + + void keepAlive() throws IOException { + List clients = new ArrayList<>(); + pool.drainTo(clients); + + IOException exception = null; + for (Client client : clients) { + try { + client.keepAlive(); + } catch (IOException e) { + exception = add(exception, e); + } finally { + returnToPool(client); + } + } + if (exception != null) { + throw exception; + } + } + + void close() throws IOException { + List clients = new ArrayList<>(); + pool.drainTo(clients); + + IOException exception = null; + for (Client client : clients) { + try { + client.disconnect(); + } catch (IOException e) { + exception = add(exception, e); + } + } + if (exception != null) { + throw exception; + } + } + + private IOException add(IOException existing, IOException e) { + if (existing == null) { + return e; + } + existing.addSuppressed(e); + return existing; + } + + private void returnToPool(Client client) { + assert client.refCount == 0; + + pool.add(client); + } + + class Client extends FTPClientPool.Client { + + private FTPSClient client; + private FileType fileType; + private FileStructure fileStructure; + private FileTransferMode fileTransferMode; + + private int refCount = 0; + + private Client(boolean pooled) throws IOException { + super(pooled); + } + + @Override + protected void init() throws IOException { + this.client = env.createSecureClient(hostname, port); + this.fileType = env.getDefaultFileType(); + this.fileStructure = env.getDefaultFileStructure(); + this.fileTransferMode = env.getDefaultFileTransferMode(); + } + + private void increaseRefCount() { + refCount++; + } + + private int decreaseRefCount() { + if (refCount > 0) { + refCount--; + } + return refCount; + } + + private void keepAlive() throws IOException { + client.sendNoOp(); + } + + private boolean isConnected() { + if (client.isConnected()) { + try { + keepAlive(); + return true; + } catch (IOException e) { + // the keep alive failed - treat as not connected, and actually disconnect quietly + disconnectQuietly(); + } + } + return false; + } + + private void disconnect() throws IOException { + client.disconnect(); + } + + private void disconnectQuietly() { + try { + client.disconnect(); + } catch (IOException e) { + // ignore + } + } + + @Override + public void close() throws IOException { + if (decreaseRefCount() == 0) { + if (pooled) { + returnToPool(this); + } else { + disconnect(); + } + } + } + + String pwd() throws IOException { + String pwd = client.printWorkingDirectory(); + if (pwd == null) { + throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString()); + } + return pwd; + } + + private void applyTransferOptions(TransferOptions options) throws IOException { + if (options.fileType != null && options.fileType != fileType) { + options.fileType.apply(client); + fileType = options.fileType; + } + if (options.fileStructure != null && options.fileStructure != fileStructure) { + options.fileStructure.apply(client); + fileStructure = options.fileStructure; + } + if (options.fileTransferMode != null && options.fileTransferMode != fileTransferMode) { + options.fileTransferMode.apply(client); + fileTransferMode = options.fileTransferMode; + } + } + + InputStream newInputStream(String path, OpenOptions options) throws IOException { + assert options.read; + + applyTransferOptions(options); + + InputStream in = client.retrieveFileStream(path); + if (in == null) { + throw exceptionFactory.createNewInputStreamException(path, client.getReplyCode(), client.getReplyString()); + } + refCount++; + return new FTPInputStream(path, in, options.deleteOnClose); + } + + OutputStream newOutputStream(String path, OpenOptions options) throws IOException { + assert options.write; + + applyTransferOptions(options); + + OutputStream out = options.append ? client.appendFileStream(path) : client.storeFileStream(path); + if (out == null) { + throw exceptionFactory.createNewOutputStreamException(path, client.getReplyCode(), client.getReplyString(), options.options); + } + refCount++; + return new FTPOutputStream(path, out, options.deleteOnClose); + } + + private void finalizeStream() throws IOException { + assert refCount > 0; + + if (!client.completePendingCommand()) { + throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString()); + } + if (decreaseRefCount() == 0) { + if (pooled) { + returnToPool(Client.this); + } else { + disconnect(); + } + } + } + + void storeFile(String path, InputStream local, TransferOptions options, Collection openOptions) throws IOException { + applyTransferOptions(options); + + if (!client.storeFile(path, local)) { + throw exceptionFactory.createNewOutputStreamException(path, client.getReplyCode(), client.getReplyString(), openOptions); + } + } + + FTPFile[] listFiles(String path) throws IOException { + return client.listFiles(path); + } + + FTPFile[] listFiles(String path, FTPFileFilter filter) throws IOException { + return client.listFiles(path, filter); + } + + void throwIfEmpty(String path, FTPFile[] ftpFiles) throws IOException { + if (ftpFiles.length == 0) { + throw exceptionFactory.createGetFileException(path, client.getReplyCode(), client.getReplyString()); + } + } + + void mkdir(String path) throws IOException { + if (!client.makeDirectory(path)) { + throw exceptionFactory.createCreateDirectoryException(path, client.getReplyCode(), client.getReplyString()); + } + } + + void delete(String path, boolean isDirectory) throws IOException { + boolean success = isDirectory ? client.removeDirectory(path) : client.deleteFile(path); + if (!success) { + throw exceptionFactory.createDeleteException(path, client.getReplyCode(), client.getReplyString(), isDirectory); + } + } + + void rename(String source, String target) throws IOException { + if (!client.rename(source, target)) { + throw exceptionFactory.createMoveException(source, target, client.getReplyCode(), client.getReplyString()); + } + } + + ZonedDateTime mdtm(String path) throws IOException { + FTPFile file = client.mdtmFile(path); + return file == null ? null : file.getTimestamp(); + } + + private final class FTPInputStream extends InputStream { + + private final String path; + private final InputStream in; + private final boolean deleteOnClose; + + private boolean open = true; + + private FTPInputStream(String path, InputStream in, boolean deleteOnClose) { + this.path = path; + this.in = in; + this.deleteOnClose = deleteOnClose; + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return in.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return in.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return in.skip(n); + } + + @Override + public int available() throws IOException { + return in.available(); + } + + @Override + public void close() throws IOException { + if (open) { + in.close(); + open = false; + finalizeStream(); + if (deleteOnClose) { + delete(path, false); + } + } + } + + @Override + public synchronized void mark(int readlimit) { + in.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + in.reset(); + } + + @Override + public boolean markSupported() { + return in.markSupported(); + } + } + + private final class FTPOutputStream extends OutputStream { + + private final String path; + private final OutputStream out; + private final boolean deleteOnClose; + + private boolean open = true; + + private FTPOutputStream(String path, OutputStream out, boolean deleteOnClose) { + this.path = path; + this.out = out; + this.deleteOnClose = deleteOnClose; + } + + @Override + public void write(int b) throws IOException { + out.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + if (open) { + out.close(); + open = false; + finalizeStream(); + if (deleteOnClose) { + delete(path, false); + } + } + } + } + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystem.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystem.java new file mode 100644 index 0000000..366e822 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystem.java @@ -0,0 +1,653 @@ +package org.xbib.files.ftp.fs; + +import org.xbib.files.ftp.FTPFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessDeniedException; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotLinkException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An FTPS file system. + */ +public class FTPSFileSystem extends FTPFileSystem { + + private FTPSClientPool clientPool; + + public FTPSFileSystem(FTPSFileSystemProvider provider, URI uri, FTPEnvironment env) throws IOException { + super(provider, uri, env); + } + + protected void init() throws IOException { + this.rootDirectories = Collections.singleton(new FTPPath(this, "/")); + this.fileStore = new FTPFileStore(this); + this.fileStores = Collections.singleton(fileStore); + this.clientPool = new FTPSClientPool(uri.getHost(), uri.getPort(), env); + try (FTPSClientPool.Client client = clientPool.get()) { + this.defaultDirectory = client.pwd(); + this.ftpFileStrategy = FTPFileStrategy.getInstance(client); + } + } + + public static String getFileName(FTPFile ftpFile) { + String fileName = ftpFile.getName(); + if (fileName == null) { + return null; + } + int index = fileName.lastIndexOf('/'); + return index == -1 || index == fileName.length() - 1 ? fileName : fileName.substring(index + 1); + } + + @Override + public void keepAlive() throws IOException { + clientPool.keepAlive(); + } + + @Override + public FTPPath toRealPath(FTPPath path, LinkOption... options) throws IOException { + boolean followLinks = LinkOptionSupport.followLinks(options); + try (FTPClientPool.Client client = clientPool.get()) { + return toRealPath(client, path, followLinks).ftpPath; + } + } + + private FTPPathAndFilePair toRealPath(FTPClientPool.Client client, FTPPath path, boolean followLinks) throws IOException { + FTPPath absPath = toAbsolutePath(path).normalize(); + // call getFTPFile to verify the file exists + FTPFile ftpFile = getFTPFile(client, absPath); + + if (followLinks && isPossibleSymbolicLink(ftpFile)) { + FTPFile link = getLink(client, ftpFile, absPath); + if (link != null) { + return toRealPath(client, new FTPPath(this, link.getLink()), followLinks); + } + } + return new FTPPathAndFilePair(absPath, ftpFile); + } + + private boolean isPossibleSymbolicLink(FTPFile ftpFile) { + return ftpFile.isSymbolicLink() || (ftpFile.isDirectory() && CURRENT_DIR.equals(getFileName(ftpFile))); + } + + public InputStream newInputStream(FTPPath path, OpenOption... options) throws IOException { + OpenOptions openOptions = OpenOptions.forNewInputStream(options); + try (FTPClientPool.Client client = clientPool.get()) { + return newInputStream(client, path, openOptions); + } + } + + private InputStream newInputStream(FTPClientPool.Client client, FTPPath path, OpenOptions options) throws IOException { + assert options.read; + + return client.newInputStream(path.path(), options); + } + + public OutputStream newOutputStream(FTPPath path, OpenOption... options) throws IOException { + OpenOptions openOptions = OpenOptions.forNewOutputStream(options); + + try (FTPClientPool.Client client = clientPool.get()) { + return newOutputStream(client, path, false, openOptions).out; + } + } + + private FTPFileAndOutputStreamPair newOutputStream(FTPClientPool.Client client, FTPPath path, boolean requireFTPFile, OpenOptions options) throws IOException { + + // retrieve the file unless create is true and createNew is false, because then the file can be created + FTPFile ftpFile = null; + if (!options.create || options.createNew) { + ftpFile = findFTPFile(client, path); + if (ftpFile != null && ftpFile.isDirectory()) { + throw Messages.fileSystemProvider().isDirectory(path.path()); + } + if (!options.createNew && ftpFile == null) { + throw new NoSuchFileException(path.path()); + } else if (options.createNew && ftpFile != null) { + throw new FileAlreadyExistsException(path.path()); + } + } + // else the file can be created if necessary + + if (ftpFile == null && requireFTPFile) { + ftpFile = findFTPFile(client, path); + } + + OutputStream out = client.newOutputStream(path.path(), options); + return new FTPFileAndOutputStreamPair(ftpFile, out); + } + + public SeekableByteChannel newByteChannel(FTPPath path, Set options, FileAttribute... attrs) throws IOException { + if (attrs.length > 0) { + throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name()); + } + + OpenOptions openOptions = OpenOptions.forNewByteChannel(options); + + try (FTPClientPool.Client client = clientPool.get()) { + if (openOptions.read) { + // use findFTPFile instead of getFTPFile, to let the opening of the stream provide the correct error message + FTPFile ftpFile = findFTPFile(client, path); + InputStream in = newInputStream(client, path, openOptions); + long size = ftpFile == null ? 0 : ftpFile.getSize(); + return FileSystemProviderSupport.createSeekableByteChannel(in, size); + } + + // if append then we need the FTP file, to find the initial position of the channel + boolean requireFTPFile = openOptions.append; + FTPFileAndOutputStreamPair outPair = newOutputStream(client, path, requireFTPFile, openOptions); + long initialPosition = outPair.ftpFile == null ? 0 : outPair.ftpFile.getSize(); + return FileSystemProviderSupport.createSeekableByteChannel(outPair.out, initialPosition); + } + } + + public DirectoryStream newDirectoryStream(final FTPPath path, Filter filter) throws IOException { + List children; + try (FTPClientPool.Client client = clientPool.get()) { + children = ftpFileStrategy.getChildren(client, path); + } + return new FTPPathDirectoryStream(path, children, filter); + } + + public void createDirectory(FTPPath path, FileAttribute... attrs) throws IOException { + if (attrs.length > 0) { + throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name()); + } + try (FTPClientPool.Client client = clientPool.get()) { + client.mkdir(path.path()); + } + } + + public void delete(FTPPath path) throws IOException { + try (FTPClientPool.Client client = clientPool.get()) { + FTPFile ftpFile = getFTPFile(client, path); + boolean isDirectory = ftpFile.isDirectory(); + client.delete(path.path(), isDirectory); + } + } + + public FTPPath readSymbolicLink(FTPPath path) throws IOException { + try (FTPClientPool.Client client = clientPool.get()) { + FTPFile ftpFile = getFTPFile(client, path); + FTPFile link = getLink(client, ftpFile, path); + if (link == null) { + throw new NotLinkException(path.path()); + } + return path.resolveSibling(link.getLink()); + } + } + + public void copy(FTPPath source, FTPPath target, CopyOption... options) throws IOException { + boolean sameFileSystem = source.getFileSystem() == target.getFileSystem(); + CopyOptions copyOptions = CopyOptions.forCopy(options); + + try (FTPClientPool.Client client = clientPool.get()) { + // get the FTP file to determine whether a directory needs to be created or a file needs to be copied + // Files.copy specifies that for links, the final target must be copied + FTPPathAndFilePair sourcePair = toRealPath(client, source, true); + + if (!sameFileSystem) { + copyAcrossFileSystems(client, source, sourcePair.ftpFile, target, copyOptions); + return; + } + + try { + if (sourcePair.ftpPath.path().equals(toRealPath(client, target, true).ftpPath.path())) { + // non-op, don't do a thing as specified by Files.copy + return; + } + } catch (NoSuchFileException e) { + // the target does not exist or either path is an invalid link, ignore the error and continue + } + + FTPFile targetFtpFile = findFTPFile(client, target); + + if (targetFtpFile != null) { + if (copyOptions.replaceExisting) { + client.delete(target.path(), targetFtpFile.isDirectory()); + } else { + throw new FileAlreadyExistsException(target.path()); + } + } + + if (sourcePair.ftpFile.isDirectory()) { + client.mkdir(target.path()); + } else { + try (FTPClientPool.Client client2 = clientPool.getOrCreate()) { + copyFile(client, source, client2, target, copyOptions); + } + } + } + } + + private void copyAcrossFileSystems(FTPClientPool.Client sourceClient, FTPPath source, FTPFile sourceFtpFile, FTPPath target, CopyOptions options) + throws IOException { + try (FTPClientPool.Client targetClient = target.getFileSystem().getClientPool().getOrCreate()) { + FTPFile targetFtpFile = findFTPFile(targetClient, target); + if (targetFtpFile != null) { + if (options.replaceExisting) { + targetClient.delete(target.path(), targetFtpFile.isDirectory()); + } else { + throw new FileAlreadyExistsException(target.path()); + } + } + if (sourceFtpFile.isDirectory()) { + sourceClient.mkdir(target.path()); + } else { + copyFile(sourceClient, source, targetClient, target, options); + } + } + } + + private void copyFile(FTPClientPool.Client sourceClient, FTPPath source, FTPClientPool.Client targetClient, FTPPath target, CopyOptions options) throws IOException { + OpenOptions inOptions = OpenOptions.forNewInputStream(options.toOpenOptions(StandardOpenOption.READ)); + OpenOptions outOptions = OpenOptions + .forNewOutputStream(options.toOpenOptions(StandardOpenOption.WRITE, StandardOpenOption.CREATE)); + try (InputStream in = sourceClient.newInputStream(source.path(), inOptions)) { + targetClient.storeFile(target.path(), in, outOptions, outOptions.options); + } + } + + public void move(FTPPath source, FTPPath target, CopyOption... options) throws IOException { + boolean sameFileSystem = source.getFileSystem() == target.getFileSystem(); + CopyOptions copyOptions = CopyOptions.forMove(sameFileSystem, options); + + try (FTPClientPool.Client client = clientPool.get()) { + if (!sameFileSystem) { + FTPFile ftpFile = getFTPFile(client, source); + if (getLink(client, ftpFile, source) != null) { + throw new IOException(FTPMessages.copyOfSymbolicLinksAcrossFileSystemsNotSupported()); + } + copyAcrossFileSystems(client, source, ftpFile, target, copyOptions); + client.delete(source.path(), ftpFile.isDirectory()); + return; + } + + try { + if (isSameFile(client, source, target)) { + // non-op, don't do a thing as specified by Files.move + return; + } + } catch (NoSuchFileException e) { + // the source or target does not exist or either path is an invalid link + // call getFTPFile to ensure the source file exists + // ignore any error to target or if the source link is invalid + getFTPFile(client, source); + } + + if (toAbsolutePath(source).parentPath() == null) { + // cannot move or rename the root + throw new DirectoryNotEmptyException(source.path()); + } + + FTPFile targetFTPFile = findFTPFile(client, target); + if (copyOptions.replaceExisting && targetFTPFile != null) { + client.delete(target.path(), targetFTPFile.isDirectory()); + } + + client.rename(source.path(), target.path()); + } + } + + public boolean isSameFile(FTPPath path, FTPPath path2) throws IOException { + if (path.getFileSystem() != path2.getFileSystem()) { + return false; + } + if (path.equals(path2)) { + return true; + } + try (FTPClientPool.Client client = clientPool.get()) { + return isSameFile(client, path, path2); + } + } + + private boolean isSameFile(FTPClientPool.Client client, FTPPath path, FTPPath path2) throws IOException { + if (path.equals(path2)) { + return true; + } + return toRealPath(client, path, true).ftpPath.path().equals(toRealPath(client, path2, true).ftpPath.path()); + } + + public boolean isHidden(FTPPath path) throws IOException { + // call getFTPFile to check for existence + try (FTPClientPool.Client client = clientPool.get()) { + getFTPFile(client, path); + } + String fileName = path.fileName(); + return !CURRENT_DIR.equals(fileName) && !PARENT_DIR.equals(fileName) && + fileName != null && fileName.startsWith("."); + } + + public FileStore getFileStore(FTPPath path) throws IOException { + // call getFTPFile to check existence of the path + try (FTPClientPool.Client client = clientPool.get()) { + getFTPFile(client, path); + } + return fileStore; + } + + public void checkAccess(FTPPath path, AccessMode... modes) throws IOException { + try (FTPClientPool.Client client = clientPool.get()) { + FTPFile ftpFile = getFTPFile(client, path); + for (AccessMode mode : modes) { + if (!hasAccess(ftpFile, mode)) { + throw new AccessDeniedException(path.path()); + } + } + } + } + + private boolean hasAccess(FTPFile ftpFile, AccessMode mode) { + return switch (mode) { + case READ -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION); + case WRITE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION); + case EXECUTE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION); + }; + } + + public PosixFileAttributes readAttributes(FTPPath path, LinkOption... options) throws IOException { + boolean followLinks = LinkOptionSupport.followLinks(options); + try (FTPClientPool.Client client = clientPool.get()) { + FTPPathAndFilePair pair = toRealPath(client, path, followLinks); + ZonedDateTime lastModified = client.mdtm(pair.ftpPath.path()); + FTPFile link = followLinks ? null : getLink(client, pair.ftpFile, path); + FTPFile ftpFile = link == null ? pair.ftpFile : link; + return new FTPPathFileAttributes(ftpFile, lastModified); + } + } + + public Map readAttributes(FTPPath path, String attributes, LinkOption... options) throws IOException { + String view; + int pos = attributes.indexOf(':'); + if (pos == -1) { + view = "basic"; + attributes = "basic:" + attributes; + } else { + view = attributes.substring(0, pos); + } + if (!SUPPORTED_FILE_ATTRIBUTE_VIEWS.contains(view)) { + throw Messages.fileSystemProvider().unsupportedFileAttributeView(view); + } + Set allowedAttributes; + if (attributes.startsWith("basic:")) { + allowedAttributes = BASIC_ATTRIBUTES; + } else if (attributes.startsWith("owner:")) { + allowedAttributes = OWNER_ATTRIBUTES; + } else if (attributes.startsWith("posix:")) { + allowedAttributes = POSIX_ATTRIBUTES; + } else { + // should not occur + throw Messages.fileSystemProvider().unsupportedFileAttributeView(attributes.substring(0, attributes.indexOf(':'))); + } + Map result = getAttributeMap(attributes, allowedAttributes); + PosixFileAttributes posixAttributes = readAttributes(path, options); + for (Map.Entry entry : result.entrySet()) { + switch (entry.getKey()) { + case "basic:lastModifiedTime": + case "posix:lastModifiedTime": + entry.setValue(posixAttributes.lastModifiedTime()); + break; + case "basic:lastAccessTime": + case "posix:lastAccessTime": + entry.setValue(posixAttributes.lastAccessTime()); + break; + case "basic:creationTime": + case "posix:creationTime": + entry.setValue(posixAttributes.creationTime()); + break; + case "basic:size": + case "posix:size": + entry.setValue(posixAttributes.size()); + break; + case "basic:isRegularFile": + case "posix:isRegularFile": + entry.setValue(posixAttributes.isRegularFile()); + break; + case "basic:isDirectory": + case "posix:isDirectory": + entry.setValue(posixAttributes.isDirectory()); + break; + case "basic:isSymbolicLink": + case "posix:isSymbolicLink": + entry.setValue(posixAttributes.isSymbolicLink()); + break; + case "basic:isOther": + case "posix:isOther": + entry.setValue(posixAttributes.isOther()); + break; + case "basic:fileKey": + case "posix:fileKey": + entry.setValue(posixAttributes.fileKey()); + break; + case "owner:owner": + case "posix:owner": + entry.setValue(posixAttributes.owner()); + break; + case "posix:group": + entry.setValue(posixAttributes.group()); + break; + case "posix:permissions": + entry.setValue(posixAttributes.permissions()); + break; + default: + // should not occur + throw new IllegalStateException("unexpected attribute name: " + entry.getKey()); + } + } + return result; + } + + private Map getAttributeMap(String attributes, Set allowedAttributes) { + int indexOfColon = attributes.indexOf(':'); + String prefix = attributes.substring(0, indexOfColon + 1); + attributes = attributes.substring(indexOfColon + 1); + + String[] attributeList = attributes.split(","); + Map result = new HashMap<>(allowedAttributes.size()); + + for (String attribute : attributeList) { + String prefixedAttribute = prefix + attribute; + if (allowedAttributes.contains(prefixedAttribute)) { + result.put(prefixedAttribute, null); + } else if ("*".equals(attribute)) { + for (String s : allowedAttributes) { + result.put(s, null); + } + } else { + throw Messages.fileSystemProvider().unsupportedFileAttribute(attribute); + } + } + return result; + } + + public FTPFile getFTPFile(FTPPath path) throws IOException { + try (FTPClientPool.Client client = clientPool.get()) { + return getFTPFile(client, path); + } + } + + private FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException { + return ftpFileStrategy.getFTPFile(client, path); + } + + private FTPFile findFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException { + try { + return getFTPFile(client, path); + } catch (NoSuchFileException e) { + return null; + } + } + + private FTPFile getLink(FTPClientPool.Client client, FTPFile ftpFile, FTPPath path) throws IOException { + return ftpFileStrategy.getLink(client, ftpFile, path); + } + + private static final class FTPPathAndFilePair { + private final FTPPath ftpPath; + private final FTPFile ftpFile; + + private FTPPathAndFilePair(FTPPath ftpPath, FTPFile ftpFile) { + this.ftpPath = ftpPath; + this.ftpFile = ftpFile; + } + } + + private static final class FTPFileAndOutputStreamPair { + + private final FTPFile ftpFile; + private final OutputStream out; + + private FTPFileAndOutputStreamPair(FTPFile ftpFile, OutputStream out) { + this.ftpFile = ftpFile; + this.out = out; + } + } + + private static final class FTPPathDirectoryStream extends AbstractDirectoryStream { + + private final FTPPath path; + private final List files; + private Iterator iterator; + + private FTPPathDirectoryStream(FTPPath path, List files, Filter filter) { + super(filter); + this.path = path; + this.files = files; + } + + @Override + protected void setupIteration() { + iterator = files.iterator(); + } + + @Override + protected Path getNext() throws IOException { + return iterator.hasNext() ? path.resolve(getFileName(iterator.next())) : null; + } + } + + private static final class FTPPathFileAttributes implements PosixFileAttributes { + + private static final FileTime EPOCH = FileTime.fromMillis(0L); + + private final FTPFile ftpFile; + private final FileTime lastModified; + + private FTPPathFileAttributes(FTPFile ftpFile, ZonedDateTime lastModified) { + this.ftpFile = ftpFile; + if (lastModified == null) { + ZonedDateTime timestamp = ftpFile.getTimestamp(); + this.lastModified = timestamp == null ? EPOCH : FileTime.from(timestamp.toInstant()); + } else { + this.lastModified = FileTime.from(lastModified.toInstant()); + } + } + + @Override + public UserPrincipal owner() { + String user = ftpFile.getUser(); + return user == null ? null : new SimpleUserPrincipal(user); + } + + @Override + public GroupPrincipal group() { + String group = ftpFile.getGroup(); + return group == null ? null : new SimpleGroupPrincipal(group); + } + + @Override + public Set permissions() { + Set permissions = EnumSet.noneOf(PosixFilePermission.class); + addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.OWNER_READ, permissions); + addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.OWNER_WRITE, permissions); + addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.OWNER_EXECUTE, permissions); + addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.GROUP_READ, permissions); + addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.GROUP_WRITE, permissions); + addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.GROUP_EXECUTE, permissions); + addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.OTHERS_READ, permissions); + addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.OTHERS_WRITE, permissions); + addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.OTHERS_EXECUTE, permissions); + return permissions; + } + + private void addPermissionIfSet(FTPFile ftpFile, int access, int permission, PosixFilePermission value, + Set permissions) { + + if (ftpFile.hasPermission(access, permission)) { + permissions.add(value); + } + } + + @Override + public FileTime lastModifiedTime() { + return lastModified; + } + + @Override + public FileTime lastAccessTime() { + return lastModifiedTime(); + } + + @Override + public FileTime creationTime() { + return lastModifiedTime(); + } + + @Override + public boolean isRegularFile() { + return ftpFile.isFile(); + } + + @Override + public boolean isDirectory() { + return ftpFile.isDirectory(); + } + + @Override + public boolean isSymbolicLink() { + return ftpFile.isSymbolicLink(); + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public long size() { + return ftpFile.getSize(); + } + + @Override + public Object fileKey() { + return null; + } + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystemProvider.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystemProvider.java new file mode 100644 index 0000000..dfe4e45 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/FTPSFileSystemProvider.java @@ -0,0 +1,512 @@ +package org.xbib.files.ftp.fs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.ProviderMismatchException; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileOwnerAttributeView; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A provider for FTPS file systems. + */ +public class FTPSFileSystemProvider extends FTPFileSystemProvider { + + private final Map fileSystems = new HashMap<>(); + + private static FTPPath toFTPPath(Path path) { + Objects.requireNonNull(path); + if (path instanceof FTPPath) { + return (FTPPath) path; + } + throw new ProviderMismatchException(); + } + + /** + * Send a keep-alive signal for an FTP file system. + * + * @param fs The FTP file system to send a keep-alive signal for. + * @throws ProviderMismatchException If the given file system is not an FTP file system + * (not created by an {@code FTPFileSystemProvider}). + * @throws IOException If an I/O error occurred. + */ + public static void keepAlive(FileSystem fs) throws IOException { + if (fs instanceof FTPSFileSystem) { + ((FTPSFileSystem) fs).keepAlive(); + } + throw new ProviderMismatchException(); + } + + /** + * Returns the URI scheme that identifies this provider: {@code ftps}. + */ + @Override + public String getScheme() { + return "ftps"; + } + + /** + * Constructs a new {@code FileSystem} object identified by a URI. + *

+ * The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()}, + * and no {@link URI#getUserInfo() user information}, + * {@link URI#getPath() path}, {@link URI#getQuery() query} or {@link URI#getFragment() fragment}. + * Authentication credentials must be set through + * the given environment map, preferably through {@link FTPEnvironment}. + *

+ * This provider allows multiple file systems per host, but only one file system per user on a host. + * Once a file system is {@link FileSystem#close() closed}, this provider allows a new file system + * to be created with the same URI and credentials + * as the closed file system. + */ + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + // user info must come from the environment map + checkURI(uri, false, false); + FTPEnvironment environment = wrapEnvironment(env); + String username = environment.getUsername(); + URI normalizedURI = normalizeWithUsername(uri, username); + synchronized (fileSystems) { + if (fileSystems.containsKey(normalizedURI)) { + throw new FileSystemAlreadyExistsException(normalizedURI.toString()); + } + FTPSFileSystem fs = new FTPSFileSystem(this, normalizedURI, environment); + fileSystems.put(normalizedURI, fs); + return fs; + } + } + + FTPEnvironment wrapEnvironment(Map env) { + return FTPEnvironment.wrap(env); + } + + /** + * Returns an existing {@code FileSystem} created by this provider. + * The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()}, + * and no {@link URI#getPath() path}, + * {@link URI#getQuery() query} or {@link URI#getFragment() fragment}. + * Because the original credentials were provided through an environment map, + * the URI can contain {@link URI#getUserInfo() user information}, although this should not + * contain a password for security reasons. + * Once a file system is {@link FileSystem#close() closed}, + * this provided will throw a {@link FileSystemNotFoundException}. + */ + @Override + public FileSystem getFileSystem(URI uri) { + checkURI(uri, true, false); + return getExistingFileSystem(uri); + } + + /** + * Return a {@code Path} object by converting the given {@link URI}. The resulting {@code Path} + * is associated with a {@link FileSystem} that + * already exists. This method does not support constructing {@code FileSystem}s automatically. + *

+ * The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()}, + * and no {@link URI#getQuery() query} or + * {@link URI#getFragment() fragment}. Because the original credentials were provided through an environment map, + * the URI can contain {@link URI#getUserInfo() user information}, + * although this should not contain a password for security reasons. + */ + @Override + public Path getPath(URI uri) { + checkURI(uri, true, true); + FTPSFileSystem fs = getExistingFileSystem(uri); + return fs.getPath(uri.getPath()); + } + + private FTPSFileSystem getExistingFileSystem(URI uri) { + URI normalizedURI = normalizeWithoutPassword(uri); + synchronized (fileSystems) { + FTPSFileSystem fs = fileSystems.get(normalizedURI); + if (fs == null) { + throw new FileSystemNotFoundException(uri.toString()); + } + return fs; + } + } + + private void checkURI(URI uri, boolean allowUserInfo, boolean allowPath) { + if (!uri.isAbsolute()) { + throw Messages.uri().notAbsolute(uri); + } + if (!getScheme().equalsIgnoreCase(uri.getScheme())) { + throw Messages.uri().invalidScheme(uri, getScheme()); + } + if (!allowUserInfo && uri.getUserInfo() != null && !uri.getUserInfo().isEmpty()) { + throw Messages.uri().hasUserInfo(uri); + } + if (uri.isOpaque()) { + throw Messages.uri().notHierarchical(uri); + } + if (!allowPath && uri.getPath() != null && !uri.getPath().isEmpty()) { + throw Messages.uri().hasPath(uri); + } + if (uri.getQuery() != null && !uri.getQuery().isEmpty()) { + throw Messages.uri().hasQuery(uri); + } + if (uri.getFragment() != null && !uri.getFragment().isEmpty()) { + throw Messages.uri().hasFragment(uri); + } + } + + void removeFileSystem(URI uri) { + URI normalizedURI = normalizeWithoutPassword(uri); + synchronized (fileSystems) { + fileSystems.remove(normalizedURI); + } + } + + private URI normalizeWithoutPassword(URI uri) { + String userInfo = uri.getUserInfo(); + if (userInfo == null && uri.getPath() == null && uri.getQuery() == null && uri.getFragment() == null) { + // nothing to normalize, return the URI + return uri; + } + String username = null; + if (userInfo != null) { + int index = userInfo.indexOf(':'); + username = index == -1 ? userInfo : userInfo.substring(0, index); + } + // no path, query or fragment + return URISupport.create(uri.getScheme(), username, uri.getHost(), uri.getPort(), null, null, null); + } + + private URI normalizeWithUsername(URI uri, String username) { + if (username == null && uri.getUserInfo() == null && uri.getPath() == null && uri.getQuery() == null && uri.getFragment() == null) { + // nothing to normalize or add, return the URI + return uri; + } + // no path, query or fragment + return URISupport.create(uri.getScheme(), username, uri.getHost(), uri.getPort(), null, null, null); + } + + /** + * Opens a file, returning an input stream to read from the file. + * This method works in exactly the manner specified by the {@link Files#newInputStream(Path, OpenOption...)} method. + *

+ * In addition to the standard open options, this method also supports single occurrences of each of + * {@link FileType}, {@link FileStructure} and + * {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()}, + * {@link FileStructure#FILE} and + * {@link FileTransferMode#STREAM}, persist for all calls that support file transfers: + *

    + *
  • {@link #newInputStream(Path, OpenOption...)}
  • + *
  • {@link #newOutputStream(Path, OpenOption...)}
  • + *
  • {@link #newByteChannel(Path, Set, FileAttribute...)}
  • + *
  • {@link #copy(Path, Path, CopyOption...)}
  • + *
  • {@link #move(Path, Path, CopyOption...)}
  • + *
+ *

+ * Note: while the returned input stream is not closed, the path's file system will have + * one available connection fewer. + * It is therefore essential that the input stream is closed as soon as possible. + */ + @Override + public InputStream newInputStream(Path path, OpenOption... options) throws IOException { + return toFTPPath(path).newInputStream(options); + } + + /** + * Opens or creates a file, returning an output stream that may be used to write bytes to the file. + * This method works in exactly the manner specified by the {@link Files#newOutputStream(Path, OpenOption...)} method. + *

+ * In addition to the standard open options, this method also supports single occurrences of each of + * {@link FileType}, {@link FileStructure} and + * {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()}, + * {@link FileStructure#FILE} and + * {@link FileTransferMode#STREAM}, persist for all calls that support file transfers: + *

    + *
  • {@link #newInputStream(Path, OpenOption...)}
  • + *
  • {@link #newOutputStream(Path, OpenOption...)}
  • + *
  • {@link #newByteChannel(Path, Set, FileAttribute...)}
  • + *
  • {@link #copy(Path, Path, CopyOption...)}
  • + *
  • {@link #move(Path, Path, CopyOption...)}
  • + *
+ *

+ * Note: while the returned output stream is not closed, the path's file system will have one available + * connection fewer. + * It is therefore essential that the output stream is closed as soon as possible. + */ + @Override + public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + return toFTPPath(path).newOutputStream(options); + } + + /** + * Opens or creates a file, returning a seekable byte channel to access the file. + * This method works in exactly the manner specified by the + * {@link Files#newByteChannel(Path, Set, FileAttribute...)} method. + *

+ * In addition to the standard open options, this method also supports single occurrences of + * each of {@link FileType}, {@link FileStructure} and + * {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()}, + * {@link FileStructure#FILE} and + * {@link FileTransferMode#STREAM}, persist for all calls that support file transfers: + *

    + *
  • {@link #newInputStream(Path, OpenOption...)}
  • + *
  • {@link #newOutputStream(Path, OpenOption...)}
  • + *
  • {@link #newByteChannel(Path, Set, FileAttribute...)}
  • + *
  • {@link #copy(Path, Path, CopyOption...)}
  • + *
  • {@link #move(Path, Path, CopyOption...)}
  • + *
+ *

+ * This method does not support any file attributes to be set. If any file attributes are given, + * an {@link UnsupportedOperationException} will be + * thrown. + *

+ * Note: while the returned channel is not closed, the path's file system will have one available connection fewer. + * It is therefore essential that the channel is closed as soon as possible. + */ + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, + FileAttribute... attrs) throws IOException { + return toFTPPath(path).newByteChannel(options, attrs); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { + return toFTPPath(dir).newDirectoryStream(filter); + } + + /** + * Creates a new directory. + * This method works in exactly the manner specified by the + * {@link Files#createDirectory(Path, FileAttribute...)} method. + *

+ * This method does not support any file attributes to be set. + * If any file attributes are given, an {@link UnsupportedOperationException} will be + * thrown. + */ + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + toFTPPath(dir).createDirectory(attrs); + } + + @Override + public void delete(Path path) throws IOException { + toFTPPath(path).delete(); + } + + @Override + public Path readSymbolicLink(Path link) throws IOException { + return toFTPPath(link).readSymbolicLink(); + } + + /** + * Copy a file to a target file. + * This method works in exactly the manner specified by the {@link Files#copy(Path, Path, CopyOption...)} + * method except that both the source and + * target paths must be associated with this provider. + *

+ * In addition to the standard copy options, this method also supports single occurrences of each of + * {@link FileType}, {@link FileStructure} and + * {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()}, + * {@link FileStructure#FILE} and + * {@link FileTransferMode#STREAM}, persist for all calls that support file transfers: + *

    + *
  • {@link #newInputStream(Path, OpenOption...)}
  • + *
  • {@link #newOutputStream(Path, OpenOption...)}
  • + *
  • {@link #newByteChannel(Path, Set, FileAttribute...)}
  • + *
  • {@link #copy(Path, Path, CopyOption...)}
  • + *
  • {@link #move(Path, Path, CopyOption...)}
  • + *
+ *

+ * {@link StandardCopyOption#COPY_ATTRIBUTES} and {@link StandardCopyOption#ATOMIC_MOVE} are not supported though. + */ + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + toFTPPath(source).copy(toFTPPath(target), options); + } + + /** + * Move or rename a file to a target file. + * This method works in exactly the manner specified by the {@link Files#move(Path, Path, CopyOption...)} + * method except that both the source and + * target paths must be associated with this provider. + *

+ * In addition to the standard copy options, this method also supports single occurrences of each of + * {@link FileType}, {@link FileStructure} and + * {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()}, + * {@link FileStructure#FILE} and + * {@link FileTransferMode#STREAM}, persist for all calls that support file transfers: + *

    + *
  • {@link #newInputStream(Path, OpenOption...)}
  • + *
  • {@link #newOutputStream(Path, OpenOption...)}
  • + *
  • {@link #newByteChannel(Path, Set, FileAttribute...)}
  • + *
  • {@link #copy(Path, Path, CopyOption...)}
  • + *
  • {@link #move(Path, Path, CopyOption...)}
  • + *
+ *

+ * {@link StandardCopyOption#COPY_ATTRIBUTES} is not supported though. + * {@link StandardCopyOption#ATOMIC_MOVE} is only supported if the paths have + * the same file system. + */ + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + toFTPPath(source).move(toFTPPath(target), options); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return toFTPPath(path).isSameFile(path2); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return toFTPPath(path).isHidden(); + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + return toFTPPath(path).getFileStore(); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + toFTPPath(path).checkAccess(modes); + } + + /** + * Returns a file attribute view of a given type. + * This method works in exactly the manner specified by the + * {@link Files#getFileAttributeView(Path, Class, LinkOption...)} method. + *

+ * This provider supports {@link BasicFileAttributeView}, {@link FileOwnerAttributeView} and + * {@link PosixFileAttributeView}. + * All other classes will result in a {@code null} return value. + *

+ * Note that the returned {@link FileAttributeView} is read-only; any attempt to change any attributes + * through the view will result in an + * {@link UnsupportedOperationException} to be thrown. + */ + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + Objects.requireNonNull(type); + if (type == BasicFileAttributeView.class) { + return type.cast(new AttributeView("basic", toFTPPath(path))); + } + if (type == FileOwnerAttributeView.class) { + return type.cast(new AttributeView("owner", toFTPPath(path))); + } + if (type == PosixFileAttributeView.class) { + return type.cast(new AttributeView("posix", toFTPPath(path))); + } + return null; + } + + /** + * Reads a file's attributes as a bulk operation. + * This method works in exactly the manner specified by the + * {@link Files#readAttributes(Path, Class, LinkOption...)} method. + * This provider supports {@link BasicFileAttributes} and {@link PosixFileAttributes} + * (there is no {@code FileOwnerFileAttributes}). + * All other classes will result in an {@link UnsupportedOperationException} to be thrown. + */ + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + if (type == BasicFileAttributes.class || type == PosixFileAttributes.class) { + return type.cast(toFTPPath(path).readAttributes(options)); + } + throw Messages.fileSystemProvider().unsupportedFileAttributesType(type); + } + + /** + * Reads a set of file attributes as a bulk operation. + * This method works in exactly the manner specified by the {@link Files#readAttributes(Path, String, LinkOption...)} method. + *

+ * This provider supports views {@code basic}, {@code owner} and {code posix}, where {@code basic} will be used if no view is given. + * All other views will result in an {@link UnsupportedOperationException} to be thrown. + */ + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + return toFTPPath(path).readAttributes(attributes, options); + } + + /** + * Sets the value of a file attribute. + * This method works in exactly the manner specified by the {@link Files#setAttribute(Path, String, Object, LinkOption...)} method. + *

+ * This provider does not support attributes for paths to be set. This method will always throw an {@link UnsupportedOperationException}. + */ + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw Messages.unsupportedOperation(FileSystemProvider.class, "setAttribute"); + } + + private static final class AttributeView implements PosixFileAttributeView { + + private final String name; + private final FTPPath path; + + private AttributeView(String name, FTPPath path) { + this.name = Objects.requireNonNull(name); + this.path = Objects.requireNonNull(path); + } + + @Override + public String name() { + return name; + } + + @Override + public UserPrincipal getOwner() throws IOException { + return readAttributes().owner(); + } + + @Override + public void setOwner(UserPrincipal owner) throws IOException { + throw Messages.unsupportedOperation(FileOwnerAttributeView.class, "setOwner"); + } + + @Override + public PosixFileAttributes readAttributes() throws IOException { + return path.readAttributes(); + } + + @Override + public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { + throw Messages.unsupportedOperation(BasicFileAttributeView.class, "setTimes"); + } + + @Override + public void setGroup(GroupPrincipal group) throws IOException { + throw Messages.unsupportedOperation(PosixFileAttributeView.class, "setGroup"); + } + + @Override + public void setPermissions(Set perms) throws IOException { + throw Messages.unsupportedOperation(PosixFileAttributeView.class, "setPermissions"); + } + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSContext.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSContext.java new file mode 100644 index 0000000..0ad6316 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSContext.java @@ -0,0 +1,27 @@ +package org.xbib.files.ftp.fs.spi; + +import org.xbib.files.ftp.fs.FTPEnvironment; +import org.xbib.files.ftp.fs.FTPSFileSystemProvider; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.util.Map; + +class FTPSContext implements Closeable { + + final FTPSFileSystemProvider provider; + + final FileSystem fileSystem; + + FTPSContext(URI uri, Map env) throws IOException { + this.provider = new FTPSFileSystemProvider(); + this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment()); + } + + @Override + public void close() throws IOException { + fileSystem.close(); + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSFileService.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSFileService.java new file mode 100644 index 0000000..a42d5e4 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSFileService.java @@ -0,0 +1,476 @@ +package org.xbib.files.ftp.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 FTPSFileService 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 FTPSFileService(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, + 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 dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, dirPerms, filePerms, copyOptions); + return null; + }); + } + + @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 dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), + dirPerms, filePerms, copyOptions); + return null; + }); + } + + @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, + Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, dirPerms, filePerms, 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, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws IOException { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), + dirPerms, filePerms, copyOptions); + return null; + }); + } + + @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 -> { + Files.copy(ctx.fileSystem.getPath(source), target); + return null; + }); + } + + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + FTPSContext ctx = new FTPSContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException { + FTPSContext ctx = new FTPSContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @Override + public Stream list(String path) throws IOException { + FTPSContext ctx = new FTPSContext(uri, env); + return FileWalker.list(new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path)))); + } + + @Override + public Stream walk(String path, FileVisitOption... options) throws IOException { + FTPSContext ctx = new FTPSContext(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 { + FTPSContext ctx = new FTPSContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options); + } + + @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); + return null; + }); + } + + @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); + return null; + }); + } + + @Override + public void remove(String source) throws IOException { + performWithContext(ctx -> { + Files.deleteIfExists(ctx.fileSystem.getPath(source)); + return null; + }); + } + + private void upload(FTPSContext 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(FTPSContext ctx, + Path source, + OutputStream outputStream) throws IOException { + download(ctx, source, Channels.newChannel(outputStream)); + } + + private void download(FTPSContext ctx, + Path source, + WritableByteChannel writableByteChannel) throws IOException { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel); + } + + private void download(FTPSContext ctx, + Path source, + Path target, + CopyOption... copyOptions) throws IOException { + prepareForWrite(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions))); + } + + 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) 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(WithSecureContext action) throws IOException { + FTPSContext ctx = null; + try { + if (uri != null) { + ctx = new FTPSContext(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/files/ftp/fs/spi/FTPSFileServiceProvider.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSFileServiceProvider.java new file mode 100644 index 0000000..7d71836 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/FTPSFileServiceProvider.java @@ -0,0 +1,15 @@ +package org.xbib.files.ftp.fs.spi; + +import org.xbib.files.FileService; +import org.xbib.files.FileServiceProvider; + +import java.net.URI; +import java.util.Map; + +public class FTPSFileServiceProvider implements FileServiceProvider { + + @Override + public FileService provide(URI uri, Map env) { + return uri.isAbsolute() && uri.getScheme().equals("ftps") ? new FTPSFileService(uri, env) : null; + } +} diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithContext.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithContext.java index 7ecc576..a1ac356 100644 --- a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithContext.java +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithContext.java @@ -2,6 +2,7 @@ package org.xbib.files.ftp.fs.spi; import java.io.IOException; +@FunctionalInterface interface WithContext { T perform(FTPContext ctx) throws IOException; } diff --git a/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithSecureContext.java b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithSecureContext.java new file mode 100644 index 0000000..13c7492 --- /dev/null +++ b/files-ftp-fs/src/main/java/org/xbib/files/ftp/fs/spi/WithSecureContext.java @@ -0,0 +1,8 @@ +package org.xbib.files.ftp.fs.spi; + +import java.io.IOException; + +@FunctionalInterface +interface WithSecureContext { + T perform(FTPSContext ctx) throws IOException; +} diff --git a/files-ftp-fs/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/files-ftp-fs/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider index 66ac81d..19e2fa1 100644 --- a/files-ftp-fs/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider +++ b/files-ftp-fs/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -1 +1,2 @@ org.xbib.files.ftp.fs.FTPFileSystemProvider +org.xbib.files.ftp.fs.FTPSFileSystemProvider \ No newline at end of file diff --git a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider index ca550b0..3b243b7 100644 --- a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider +++ b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider @@ -1 +1,2 @@ -org.xbib.files.ftp.fs.spi.FTPFileServiceProvider \ No newline at end of file +org.xbib.files.ftp.fs.spi.FTPFileServiceProvider +org.xbib.files.ftp.fs.spi.FTPSFileServiceProvider \ No newline at end of file diff --git a/files-ftp-fs/src/test/java/module-info.java b/files-ftp-fs/src/test/java/module-info.java index c7237b7..667ff85 100644 --- a/files-ftp-fs/src/test/java/module-info.java +++ b/files-ftp-fs/src/test/java/module-info.java @@ -3,7 +3,6 @@ module org.xbib.files.ftp.fs.test { requires org.junit.jupiter.api; requires org.junit.jupiter.params; requires org.mockito; - requires org.slf4j; requires org.xbib.files.ftp; requires org.xbib.files.ftp.fs; requires org.xbib.files.ftp.mock; diff --git a/files-ftp-fs/src/test/java/org/xbib/files/ftp/fs/test/FTPFileSystemTest.java b/files-ftp-fs/src/test/java/org/xbib/files/ftp/fs/test/FTPFileSystemTest.java index 79b7e70..8ce70c3 100644 --- a/files-ftp-fs/src/test/java/org/xbib/files/ftp/fs/test/FTPFileSystemTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/files/ftp/fs/test/FTPFileSystemTest.java @@ -65,9 +65,6 @@ import static org.mockito.Mockito.verify; public class FTPFileSystemTest extends AbstractFTPFileSystemTest { - //@Rule - //public ExpectedException thrown = ExpectedException.none(); - @Test public void testGetPath() { testGetPath("/", "/"); diff --git a/gradle.properties b/gradle.properties index a66ad21..ef53a0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group = org.xbib name = files -version = 4.7.0 +version = 4.8.0 diff --git a/settings.gradle b/settings.gradle index de99828..cd1d673 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,7 +29,6 @@ dependencyResolutionManagement { library('junit4', 'junit', 'junit').version('4.13.2') library('mockito-core', 'org.mockito', 'mockito-core').version('5.11.0') library('mockito-junit-jupiter', 'org.mockito', 'mockito-junit-jupiter').version('5.11.0') - library('slf4j', 'org.slf4j', 'slf4j-api').version('2.0.13') } } }