add FTPS file system
This commit is contained in:
parent
4c66383a05
commit
a905b0559a
20 changed files with 2208 additions and 80 deletions
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
org.xbib.files.ftp.fs.FTPFileSystemProvider
|
org.xbib.files.ftp.fs.FTPFileSystemProvider
|
||||||
|
org.xbib.files.ftp.fs.FTPSFileSystemProvider
|
|
@ -1 +1,2 @@
|
||||||
org.xbib.files.ftp.fs.spi.FTPFileServiceProvider
|
org.xbib.files.ftp.fs.spi.FTPFileServiceProvider
|
||||||
|
org.xbib.files.ftp.fs.spi.FTPSFileServiceProvider
|
|
@ -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;
|
||||||
|
|
|
@ -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("/", "/");
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
group = org.xbib
|
group = org.xbib
|
||||||
name = files
|
name = files
|
||||||
version = 4.7.0
|
version = 4.8.0
|
||||||
|
|
|
@ -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')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue