add FTPS file system

This commit is contained in:
Jörg Prante 2024-07-23 16:05:39 +02:00
parent 4c66383a05
commit a905b0559a
20 changed files with 2208 additions and 80 deletions

View file

@ -4,7 +4,6 @@ dependencies {
testImplementation testLibs.junit.jupiter.params testImplementation testLibs.junit.jupiter.params
testImplementation testLibs.mockito.core testImplementation testLibs.mockito.core
testImplementation testLibs.mockito.junit.jupiter testImplementation testLibs.mockito.junit.jupiter
testImplementation testLibs.slf4j
testImplementation project(':files-ftp-mock') testImplementation project(':files-ftp-mock')
} }

View file

@ -1,13 +1,15 @@
import java.nio.file.spi.FileSystemProvider; import java.nio.file.spi.FileSystemProvider;
import org.xbib.files.FileServiceProvider; import org.xbib.files.FileServiceProvider;
import org.xbib.files.ftp.fs.FTPFileSystemProvider; 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.FTPFileServiceProvider;
import org.xbib.files.ftp.fs.spi.FTPSFileServiceProvider;
module org.xbib.files.ftp.fs { module org.xbib.files.ftp.fs {
requires org.xbib.files; requires org.xbib.files;
requires org.xbib.files.ftp; requires org.xbib.files.ftp;
exports org.xbib.files.ftp.fs; exports org.xbib.files.ftp.fs;
exports org.xbib.files.ftp.fs.spi; exports org.xbib.files.ftp.fs.spi;
provides FileSystemProvider with FTPFileSystemProvider; provides FileSystemProvider with FTPFileSystemProvider, FTPSFileSystemProvider;
provides FileServiceProvider with FTPFileServiceProvider; provides FileServiceProvider with FTPFileServiceProvider, FTPSFileServiceProvider;
} }

View file

@ -20,24 +20,27 @@ import java.util.concurrent.BlockingQueue;
/** /**
* A pool of FTP clients, allowing multiple commands to be executed concurrently. * A pool of FTP clients, allowing multiple commands to be executed concurrently.
*/ */
final class FTPClientPool { class FTPClientPool {
private final String hostname; protected final String hostname;
private final int port; protected final int port;
protected final FTPEnvironment env;
private final FTPEnvironment env; private FileSystemExceptionFactory exceptionFactory;
private final FileSystemExceptionFactory exceptionFactory;
private final BlockingQueue<Client> pool; private BlockingQueue<Client> pool;
FTPClientPool(String hostname, int port, FTPEnvironment env) throws IOException { FTPClientPool(String hostname, int port, FTPEnvironment env) throws IOException {
this.hostname = hostname; this.hostname = hostname;
this.port = port; this.port = port;
this.env = env.clone(); this.env = env.clone();
init();
}
protected void init() throws IOException {
this.exceptionFactory = env.getExceptionFactory(); this.exceptionFactory = env.getExceptionFactory();
final int poolSize = env.getClientConnectionCount(); final int poolSize = env.getClientConnectionCount();
this.pool = new ArrayBlockingQueue<>(poolSize); this.pool = new ArrayBlockingQueue<>(poolSize);
try { try {
for (int i = 0; i < poolSize; i++) { for (int i = 0; i < poolSize; i++) {
pool.add(new Client(true)); pool.add(new Client(true));
@ -148,21 +151,23 @@ final class FTPClientPool {
pool.add(client); pool.add(client);
} }
final class Client implements Closeable { class Client implements Closeable {
private final FTPClient client;
private final boolean pooled;
protected final boolean pooled;
private FTPClient client;
private FileType fileType; private FileType fileType;
private FileStructure fileStructure; private FileStructure fileStructure;
private FileTransferMode fileTransferMode; private FileTransferMode fileTransferMode;
private int refCount = 0; private int refCount = 0;
private Client(boolean pooled) throws IOException { Client(boolean pooled) throws IOException {
this.client = env.createClient(hostname, port);
this.pooled = pooled; this.pooled = pooled;
init();
}
protected void init() throws IOException {
this.client = env.createClient(hostname, port);
this.fileType = env.getDefaultFileType(); this.fileType = env.getDefaultFileType();
this.fileStructure = env.getDefaultFileStructure(); this.fileStructure = env.getDefaultFileStructure();
this.fileTransferMode = env.getDefaultFileTransferMode(); this.fileTransferMode = env.getDefaultFileTransferMode();

View file

@ -4,6 +4,7 @@ import org.xbib.files.ftp.FTP;
import org.xbib.files.ftp.FTPClient; import org.xbib.files.ftp.FTPClient;
import org.xbib.files.ftp.FTPClientConfig; import org.xbib.files.ftp.FTPClientConfig;
import org.xbib.files.ftp.FTPFileEntryParser; import org.xbib.files.ftp.FTPFileEntryParser;
import org.xbib.files.ftp.FTPSClient;
import org.xbib.files.ftp.parser.FTPFileEntryParserFactory; import org.xbib.files.ftp.parser.FTPFileEntryParserFactory;
import javax.net.ServerSocketFactory; import javax.net.ServerSocketFactory;
@ -320,8 +321,6 @@ public class FTPEnvironment implements Map<String, Object>, Cloneable {
return this; return this;
} }
// FTPClient
/** /**
* Stores the timeout in milliseconds to use when reading from data connections. * Stores the timeout in milliseconds to use when reading from data connections.
* *
@ -566,12 +565,10 @@ public class FTPEnvironment implements Map<String, Object>, Cloneable {
} }
FileStructure getDefaultFileStructure() { FileStructure getDefaultFileStructure() {
// as specified by FTPClient
return FileStructure.FILE; return FileStructure.FILE;
} }
FileTransferMode getDefaultFileTransferMode() { FileTransferMode getDefaultFileTransferMode() {
// as specified by FTPClient
return FileTransferMode.STREAM; return FileTransferMode.STREAM;
} }
@ -594,6 +591,15 @@ public class FTPEnvironment implements Map<String, Object>, Cloneable {
return client; 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 { void initializePreConnect(FTPClient client) throws IOException {
client.setListHiddenFiles(true); client.setListHiddenFiles(true);

View file

@ -1,7 +1,6 @@
package org.xbib.files.ftp.fs; package org.xbib.files.ftp.fs;
import org.xbib.files.ftp.FTPFile; import org.xbib.files.ftp.FTPFile;
import org.xbib.files.ftp.FTPFileFilter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
@ -17,12 +16,9 @@ import java.util.List;
abstract class FTPFileStrategy { abstract class FTPFileStrategy {
static FTPFileStrategy getInstance(FTPClientPool.Client client) throws IOException { static FTPFileStrategy getInstance(FTPClientPool.Client client) throws IOException {
FTPFile[] ftpFiles = client.listFiles("/", new FTPFileFilter() { FTPFile[] ftpFiles = client.listFiles("/", ftpFile -> {
@Override
public boolean accept(FTPFile ftpFile) {
String fileName = FTPFileSystem.getFileName(ftpFile); String fileName = FTPFileSystem.getFileName(ftpFile);
return FTPFileSystem.CURRENT_DIR.equals(fileName); return FTPFileSystem.CURRENT_DIR.equals(fileName);
}
}); });
return ftpFiles.length == 0 ? NonUnix.INSTANCE : Unix.INSTANCE; return ftpFiles.length == 0 ? NonUnix.INSTANCE : Unix.INSTANCE;
} }
@ -67,12 +63,9 @@ abstract class FTPFileStrategy {
FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException { FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException {
final String name = path.fileName(); final String name = path.fileName();
FTPFile[] ftpFiles = client.listFiles(path.path(), new FTPFileFilter() { FTPFile[] ftpFiles = client.listFiles(path.path(), ftpFile -> {
@Override
public boolean accept(FTPFile ftpFile) {
String fileName = FTPFileSystem.getFileName(ftpFile); String fileName = FTPFileSystem.getFileName(ftpFile);
return FTPFileSystem.CURRENT_DIR.equals(fileName) || (name != null && name.equals(fileName)); return FTPFileSystem.CURRENT_DIR.equals(fileName) || (name != null && name.equals(fileName));
}
}); });
client.throwIfEmpty(path.path(), ftpFiles); client.throwIfEmpty(path.path(), ftpFiles);
if (ftpFiles.length == 1) { if (ftpFiles.length == 1) {
@ -94,21 +87,13 @@ abstract class FTPFileStrategy {
if (ftpFile.isDirectory() && FTPFileSystem.CURRENT_DIR.equals(FTPFileSystem.getFileName(ftpFile))) { if (ftpFile.isDirectory() && FTPFileSystem.CURRENT_DIR.equals(FTPFileSystem.getFileName(ftpFile))) {
// The file is returned using getFTPFile, which returns the . (current directory) entry for directories. // The file is returned using getFTPFile, which returns the . (current directory) entry for directories.
// List the parent (if any) instead. // List the parent (if any) instead.
final String parentPath = path.toAbsolutePath().parentPath(); final String parentPath = path.toAbsolutePath().parentPath();
final String name = path.fileName(); final String name = path.fileName();
if (parentPath == null) { if (parentPath == null) {
// path is /, there is no link // path is /, there is no link
return null; return null;
} }
FTPFile[] ftpFiles = client.listFiles(parentPath, ftpFile1 -> (ftpFile1.isDirectory() || ftpFile1.isSymbolicLink()) && name.equals(FTPFileSystem.getFileName(ftpFile1)));
FTPFile[] ftpFiles = client.listFiles(parentPath, new FTPFileFilter() {
@Override
public boolean accept(FTPFile ftpFile) {
return (ftpFile.isDirectory() || ftpFile.isSymbolicLink()) && name.equals(FTPFileSystem.getFileName(ftpFile));
}
});
client.throwIfEmpty(path.path(), ftpFiles); client.throwIfEmpty(path.path(), ftpFiles);
return ftpFiles[0].getLink() == null ? null : ftpFiles[0]; return ftpFiles[0].getLink() == null ? null : ftpFiles[0];
} }
@ -122,9 +107,7 @@ abstract class FTPFileStrategy {
@Override @Override
List<FTPFile> getChildren(FTPClientPool.Client client, FTPPath path) throws IOException { List<FTPFile> getChildren(FTPClientPool.Client client, FTPPath path) throws IOException {
FTPFile[] ftpFiles = client.listFiles(path.path()); FTPFile[] ftpFiles = client.listFiles(path.path());
boolean isDirectory = false; boolean isDirectory = false;
List<FTPFile> children = new ArrayList<>(ftpFiles.length); List<FTPFile> children = new ArrayList<>(ftpFiles.length);
for (FTPFile ftpFile : ftpFiles) { for (FTPFile ftpFile : ftpFiles) {
@ -135,7 +118,6 @@ abstract class FTPFileStrategy {
children.add(ftpFile); children.add(ftpFile);
} }
} }
if (!isDirectory && children.size() <= 1) { if (!isDirectory && children.size() <= 1) {
// either zero or one, check the parent to see if the path exists and is a directory // either zero or one, check the parent to see if the path exists and is a directory
FTPPath currentPath = path; FTPPath currentPath = path;
@ -148,7 +130,6 @@ abstract class FTPFileStrategy {
throw new NotDirectoryException(path.path()); throw new NotDirectoryException(path.path());
} }
} }
return children; return children;
} }

View file

@ -53,43 +53,51 @@ public class FTPFileSystem extends FileSystem {
static final String CURRENT_DIR = "."; static final String CURRENT_DIR = ".";
static final String PARENT_DIR = ".."; static final String PARENT_DIR = "..";
private static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections protected static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList("basic", "owner", "posix"))); .unmodifiableSet(new HashSet<>(Arrays.asList("basic", "owner", "posix")));
private static final Set<String> BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( protected static final Set<String> BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"basic:lastModifiedTime", "basic:lastAccessTime", "basic:creationTime", "basic:size", "basic:lastModifiedTime", "basic:lastAccessTime", "basic:creationTime", "basic:size",
"basic:isRegularFile", "basic:isDirectory", "basic:isSymbolicLink", "basic:isOther", "basic:fileKey"))); "basic:isRegularFile", "basic:isDirectory", "basic:isSymbolicLink", "basic:isOther", "basic:fileKey")));
private static final Set<String> OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Collections.singletonList( protected static final Set<String> OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Collections.singletonList(
"owner:owner"))); "owner:owner")));
private static final Set<String> POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( protected static final Set<String> POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"posix:lastModifiedTime", "posix:lastAccessTime", "posix:creationTime", "posix:size", "posix:lastModifiedTime", "posix:lastAccessTime", "posix:creationTime", "posix:size",
"posix:isRegularFile", "posix:isDirectory", "posix:isSymbolicLink", "posix:isOther", "posix:fileKey", "posix:isRegularFile", "posix:isDirectory", "posix:isSymbolicLink", "posix:isOther", "posix:fileKey",
"posix:owner", "posix:group", "posix:permissions"))); "posix:owner", "posix:group", "posix:permissions")));
private final FTPFileSystemProvider provider;
private final Iterable<Path> rootDirectories; protected final FTPFileSystemProvider provider;
private final FileStore fileStore; protected final URI uri;
private final Iterable<FileStore> fileStores; protected final FTPEnvironment env;
private final FTPClientPool clientPool; protected Iterable<Path> rootDirectories;
private final URI uri; protected FileStore fileStore;
private final String defaultDirectory; protected Iterable<FileStore> fileStores;
private final FTPFileStrategy ftpFileStrategy; private FTPClientPool clientPool;
protected String defaultDirectory;
FTPFileStrategy ftpFileStrategy;
private final AtomicBoolean open = new AtomicBoolean(true); private final AtomicBoolean open = new AtomicBoolean(true);
public FTPFileSystem(FTPFileSystemProvider provider, URI uri, FTPEnvironment env) throws IOException { public FTPFileSystem(FTPFileSystemProvider provider, URI uri, FTPEnvironment env) throws IOException {
this.provider = Objects.requireNonNull(provider); this.provider = Objects.requireNonNull(provider);
this.uri = Objects.requireNonNull(uri);
this.env = Objects.requireNonNull(env);
init();
}
protected void init() throws IOException {
this.rootDirectories = Collections.<Path>singleton(new FTPPath(this, "/")); this.rootDirectories = Collections.<Path>singleton(new FTPPath(this, "/"));
this.fileStore = new FTPFileStore(this); this.fileStore = new FTPFileStore(this);
this.fileStores = Collections.<FileStore>singleton(fileStore); this.fileStores = Collections.<FileStore>singleton(fileStore);
this.clientPool = new FTPClientPool(uri.getHost(), uri.getPort(), env); this.clientPool = new FTPClientPool(uri.getHost(), uri.getPort(), env);
this.uri = Objects.requireNonNull(uri);
try (FTPClientPool.Client client = clientPool.get()) { try (FTPClientPool.Client client = clientPool.get()) {
this.defaultDirectory = client.pwd(); this.defaultDirectory = client.pwd();
this.ftpFileStrategy = FTPFileStrategy.getInstance(client); this.ftpFileStrategy = FTPFileStrategy.getInstance(client);
} }
} }
FTPClientPool getClientPool() {
return clientPool;
}
public static String getFileName(FTPFile ftpFile) { public static String getFileName(FTPFile ftpFile) {
String fileName = ftpFile.getName(); String fileName = ftpFile.getName();
if (fileName == null) { if (fileName == null) {
@ -469,7 +477,8 @@ public class FTPFileSystem extends FileSystem {
getFTPFile(client, path); getFTPFile(client, path);
} }
String fileName = path.fileName(); 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 { public FileStore getFileStore(FTPPath path) throws IOException {
@ -492,16 +501,11 @@ public class FTPFileSystem extends FileSystem {
} }
private boolean hasAccess(FTPFile ftpFile, AccessMode mode) { private boolean hasAccess(FTPFile ftpFile, AccessMode mode) {
switch (mode) { return switch (mode) {
case READ: case READ -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION);
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION); case WRITE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION);
case WRITE: case EXECUTE -> ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION);
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION); };
case EXECUTE:
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION);
default:
return false;
}
} }
public PosixFileAttributes readAttributes(FTPPath path, LinkOption... options) throws IOException { public PosixFileAttributes readAttributes(FTPPath path, LinkOption... options) throws IOException {

View file

@ -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<Client> 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<Client> 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<Client> 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<? extends OpenOption> 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);
}
}
}
}
}
}

View file

@ -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.<Path>singleton(new FTPPath(this, "/"));
this.fileStore = new FTPFileStore(this);
this.fileStores = Collections.<FileStore>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<? extends OpenOption> 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<Path> newDirectoryStream(final FTPPath path, Filter<? super Path> filter) throws IOException {
List<FTPFile> 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<String, Object> 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<String> 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<String, Object> result = getAttributeMap(attributes, allowedAttributes);
PosixFileAttributes posixAttributes = readAttributes(path, options);
for (Map.Entry<String, Object> 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<String, Object> getAttributeMap(String attributes, Set<String> allowedAttributes) {
int indexOfColon = attributes.indexOf(':');
String prefix = attributes.substring(0, indexOfColon + 1);
attributes = attributes.substring(indexOfColon + 1);
String[] attributeList = attributes.split(",");
Map<String, Object> 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<Path> {
private final FTPPath path;
private final List<FTPFile> files;
private Iterator<FTPFile> iterator;
private FTPPathDirectoryStream(FTPPath path, List<FTPFile> files, Filter<? super Path> 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<PosixFilePermission> permissions() {
Set<PosixFilePermission> 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<PosixFilePermission> 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;
}
}
}

View file

@ -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<URI, FTPSFileSystem> 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.
* <p>
* 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}.
* <p>
* 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<String, ?> 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<String, ?> 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.
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* This method does not support any file attributes to be set. If any file attributes are given,
* an {@link UnsupportedOperationException} will be
* thrown.
* <p>
* 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<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException {
return toFTPPath(path).newByteChannel(options, attrs);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> 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.
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* {@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.
* <p>
* 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:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* {@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.
* <p>
* This provider supports {@link BasicFileAttributeView}, {@link FileOwnerAttributeView} and
* {@link PosixFileAttributeView}.
* All other classes will result in a {@code null} return value.
* <p>
* 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 extends FileAttributeView> V getFileAttributeView(Path path, Class<V> 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 extends BasicFileAttributes> A readAttributes(Path path, Class<A> 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.
* <p>
* 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<String, Object> 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.
* <p>
* 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<PosixFilePermission> perms) throws IOException {
throw Messages.unsupportedOperation(PosixFileAttributeView.class, "setPermissions");
}
}
}

View file

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

View file

@ -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<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 FTPSFileService(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,
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> dirPerms,
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<Path> 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<Path> stream(String path, DirectoryStream.Filter<Path> filter) throws IOException {
FTPSContext ctx = new FTPSContext(uri, env);
return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter));
}
@Override
public Stream<Path> 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<Path> 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<Path> 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<PosixFilePermission> dirPerms,
Set<PosixFilePermission> 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<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(WithSecureContext<T> 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();
}
}
}
}

View file

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

View file

@ -2,6 +2,7 @@ package org.xbib.files.ftp.fs.spi;
import java.io.IOException; import java.io.IOException;
@FunctionalInterface
interface WithContext<T> { interface WithContext<T> {
T perform(FTPContext ctx) throws IOException; T perform(FTPContext ctx) throws IOException;
} }

View file

@ -0,0 +1,8 @@
package org.xbib.files.ftp.fs.spi;
import java.io.IOException;
@FunctionalInterface
interface WithSecureContext<T> {
T perform(FTPSContext ctx) throws IOException;
}

View file

@ -1 +1,2 @@
org.xbib.files.ftp.fs.FTPFileSystemProvider org.xbib.files.ftp.fs.FTPFileSystemProvider
org.xbib.files.ftp.fs.FTPSFileSystemProvider

View file

@ -1 +1,2 @@
org.xbib.files.ftp.fs.spi.FTPFileServiceProvider org.xbib.files.ftp.fs.spi.FTPFileServiceProvider
org.xbib.files.ftp.fs.spi.FTPSFileServiceProvider

View file

@ -3,7 +3,6 @@ module org.xbib.files.ftp.fs.test {
requires org.junit.jupiter.api; requires org.junit.jupiter.api;
requires org.junit.jupiter.params; requires org.junit.jupiter.params;
requires org.mockito; requires org.mockito;
requires org.slf4j;
requires org.xbib.files.ftp; requires org.xbib.files.ftp;
requires org.xbib.files.ftp.fs; requires org.xbib.files.ftp.fs;
requires org.xbib.files.ftp.mock; requires org.xbib.files.ftp.mock;

View file

@ -65,9 +65,6 @@ import static org.mockito.Mockito.verify;
public class FTPFileSystemTest extends AbstractFTPFileSystemTest { public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
//@Rule
//public ExpectedException thrown = ExpectedException.none();
@Test @Test
public void testGetPath() { public void testGetPath() {
testGetPath("/", "/"); testGetPath("/", "/");

View file

@ -1,3 +1,3 @@
group = org.xbib group = org.xbib
name = files name = files
version = 4.7.0 version = 4.8.0

View file

@ -29,7 +29,6 @@ dependencyResolutionManagement {
library('junit4', 'junit', 'junit').version('4.13.2') library('junit4', 'junit', 'junit').version('4.13.2')
library('mockito-core', 'org.mockito', 'mockito-core').version('5.11.0') 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('mockito-junit-jupiter', 'org.mockito', 'mockito-junit-jupiter').version('5.11.0')
library('slf4j', 'org.slf4j', 'slf4j-api').version('2.0.13')
} }
} }
} }