add files API and provders
This commit is contained in:
parent
6eca3c6d2b
commit
2a724f1b37
22 changed files with 1374 additions and 2 deletions
8
files-api/src/main/java/module-info.java
Normal file
8
files-api/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import org.xbib.files.DefaultFilesProvider;
|
||||||
|
import org.xbib.files.Provider;
|
||||||
|
|
||||||
|
module org.xbib.files {
|
||||||
|
exports org.xbib.files;
|
||||||
|
uses Provider;
|
||||||
|
provides Provider with DefaultFilesProvider;
|
||||||
|
}
|
332
files-api/src/main/java/org/xbib/files/DefaultFiles.java
Normal file
332
files-api/src/main/java/org/xbib/files/DefaultFiles.java
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
package org.xbib.files;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.nio.file.CopyOption;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.OpenOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.nio.file.attribute.UserPrincipal;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class DefaultFiles implements org.xbib.files.Files {
|
||||||
|
|
||||||
|
private static final int READ_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final Set<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 DefaultFiles(URI uri, Map<String, ?> env) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean exists(String path) {
|
||||||
|
return Files.exists(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isExecutable(String path) {
|
||||||
|
return Files.isExecutable(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isDirectory(String path) {
|
||||||
|
return Files.isDirectory(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isRegularFile(String path) {
|
||||||
|
return Files.isRegularFile(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isHidden(String path) throws IOException {
|
||||||
|
return Files.isHidden(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isSameFile(String path1, String path2) throws IOException {
|
||||||
|
return Files.isSameFile(Paths.get(path1), Paths.get(path2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isSymbolicLink(String path) {
|
||||||
|
return Files.isSymbolicLink(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isReadable(String path) {
|
||||||
|
return Files.isReadable(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isWritable(String path) {
|
||||||
|
return Files.isWritable(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAttribute(String path, String attribute, Object value) throws IOException {
|
||||||
|
Files.setAttribute(Paths.get(path), attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getAttribute(String path, String attribute) throws IOException {
|
||||||
|
return Files.getAttribute(Paths.get(path), attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermissions(String path, Set<PosixFilePermission> permissions) throws IOException {
|
||||||
|
Files.setPosixFilePermissions(Paths.get(path), permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<PosixFilePermission> getPermissions(String path) throws IOException {
|
||||||
|
return Files.getPosixFilePermissions(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
|
||||||
|
Files.setOwner(Paths.get(path), userPrincipal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPrincipal getOwner(String path) throws IOException {
|
||||||
|
return Files.getOwner(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLastModifiedTime(String path, FileTime fileTime) throws IOException {
|
||||||
|
Files.setLastModifiedTime(Paths.get(path), fileTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileTime getLastModifiedTime(String path) throws IOException {
|
||||||
|
return Files.getLastModifiedTime(Paths.get(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createFile(String path, FileAttribute<?>... attributes) throws IOException {
|
||||||
|
Files.createFile(Paths.get(path), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createDirectory(String path, FileAttribute<?>... attributes) throws IOException {
|
||||||
|
Files.createDirectory(Paths.get(path), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createDirectories(String path, FileAttribute<?>... attributes) throws IOException {
|
||||||
|
Files.createDirectories(Paths.get(path), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
download(source, target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
download(Paths.get(source), target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, OutputStream target) throws IOException {
|
||||||
|
download(source, target, READ_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, OutputStream target) throws IOException {
|
||||||
|
download(Paths.get(source), target, READ_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
Files.move(Paths.get(source), Paths.get(target), copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(String source) throws IOException {
|
||||||
|
Files.deleteIfExists(Paths.get(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
Files.copy(Paths.get(source), Paths.get(target), copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
|
||||||
|
return Files.newDirectoryStream(Paths.get(path), glob);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> filter(String path, DirectoryStream.Filter<Path> filter) throws IOException {
|
||||||
|
return Files.newDirectoryStream(Paths.get(path), filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(Path source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(InputStream source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Channels.newChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(ReadableByteChannel source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForWrite(target, dirPerms, filePerms);
|
||||||
|
transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(Path source,
|
||||||
|
OutputStream outputStream,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
download(source, Channels.newChannel(outputStream), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(Path source,
|
||||||
|
WritableByteChannel writableByteChannel,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel,
|
||||||
|
bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(Path source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForWrite(target);
|
||||||
|
transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)),
|
||||||
|
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForWrite(Path path) throws IOException {
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path parent = path.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
if (!Files.exists(parent)) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
Files.createFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForWrite(Path path,
|
||||||
|
Set<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,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
|
||||||
|
int read;
|
||||||
|
while ((read = readableByteChannel.read(buffer)) > 0) {
|
||||||
|
buffer.flip();
|
||||||
|
while (read > 0) {
|
||||||
|
read -= writableByteChannel.write(buffer);
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.xbib.files;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class DefaultFilesProvider implements Provider {
|
||||||
|
@Override
|
||||||
|
public Files provide(URI uri, Map<String, ?> env) {
|
||||||
|
return !uri.isAbsolute() || uri.getScheme().equals("file") ? new DefaultFiles(uri, env) : null;
|
||||||
|
}
|
||||||
|
}
|
105
files-api/src/main/java/org/xbib/files/Files.java
Normal file
105
files-api/src/main/java/org/xbib/files/Files.java
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package org.xbib.files;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.CopyOption;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.UserPrincipal;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface Files {
|
||||||
|
|
||||||
|
class Holder {
|
||||||
|
|
||||||
|
private static Files createFiles(URI uri, Map<String, ?> env) {
|
||||||
|
ServiceLoader<Provider> serviceLoader = ServiceLoader.load(Provider.class);
|
||||||
|
Optional<Files> first = serviceLoader.stream()
|
||||||
|
.map(ServiceLoader.Provider::get)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(p -> p.provide(uri, env))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst();
|
||||||
|
return first.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default Files newInstance(URI uri, Map<String, ?> env) {
|
||||||
|
return Holder.createFiles(uri, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean exists(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isExecutable(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isDirectory(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isRegularFile(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isHidden(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isSameFile(String path1, String path2) throws IOException;
|
||||||
|
|
||||||
|
Boolean isSymbolicLink(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isReadable(String path) throws IOException;
|
||||||
|
|
||||||
|
Boolean isWritable(String path) throws IOException;
|
||||||
|
|
||||||
|
void setAttribute(String path, String attribute, Object value) throws IOException;
|
||||||
|
|
||||||
|
Object getAttribute(String path, String attribute) throws IOException;
|
||||||
|
|
||||||
|
void setPermissions(String path, Set<PosixFilePermission> permissions) throws IOException;
|
||||||
|
|
||||||
|
Set<PosixFilePermission> getPermissions(String path) throws IOException;
|
||||||
|
|
||||||
|
void setOwner(String path, UserPrincipal userPrincipal) throws IOException;
|
||||||
|
|
||||||
|
UserPrincipal getOwner(String path) throws IOException;
|
||||||
|
|
||||||
|
void setLastModifiedTime(String path, FileTime fileTime) throws IOException;
|
||||||
|
|
||||||
|
FileTime getLastModifiedTime(String path) throws IOException;
|
||||||
|
|
||||||
|
void createFile(String path, FileAttribute<?>... attributes) throws IOException;
|
||||||
|
|
||||||
|
void createDirectory(String path, FileAttribute<?>... attributes) throws IOException;
|
||||||
|
|
||||||
|
void createDirectories(String path, FileAttribute<?>... attributes) throws IOException;
|
||||||
|
|
||||||
|
void upload(Path source, Path target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void upload(Path source, String target, CopyOption... copyOptions) throws Exception;
|
||||||
|
|
||||||
|
void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(Path source, Path target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(String source, Path target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(Path source, OutputStream target) throws IOException;
|
||||||
|
|
||||||
|
void download(String source, OutputStream target) throws IOException;
|
||||||
|
|
||||||
|
void rename(String source, String target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void remove(String source) throws IOException;
|
||||||
|
|
||||||
|
void copy(String source, String target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
DirectoryStream<Path> stream(String path, String glob) throws IOException;
|
||||||
|
|
||||||
|
DirectoryStream<Path> filter(String path, DirectoryStream.Filter<Path> filter) throws IOException;
|
||||||
|
}
|
9
files-api/src/main/java/org/xbib/files/Provider.java
Normal file
9
files-api/src/main/java/org/xbib/files/Provider.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package org.xbib.files;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface Provider {
|
||||||
|
|
||||||
|
Files provide(URI uri, Map<String, ?> env);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.xbib.files.DefaultFilesProvider
|
|
@ -1,4 +1,5 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api project(':files-api')
|
||||||
api project(':files-ftp')
|
api project(':files-ftp')
|
||||||
testImplementation "org.mockftpserver:MockFtpServer:${project.property('mockftpserver.version')}"
|
testImplementation "org.mockftpserver:MockFtpServer:${project.property('mockftpserver.version')}"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-params:${project.property('junit.version')}"
|
testImplementation "org.junit.jupiter:junit-jupiter-params:${project.property('junit.version')}"
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
|
import java.nio.file.spi.FileSystemProvider;
|
||||||
|
import org.xbib.files.Provider;
|
||||||
|
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
||||||
|
import org.xbib.io.ftp.fs.spi.FTPFilesProvider;
|
||||||
|
|
||||||
module org.xbib.files.ftp.fs {
|
module org.xbib.files.ftp.fs {
|
||||||
|
requires org.xbib.files;
|
||||||
requires org.xbib.files.ftp;
|
requires org.xbib.files.ftp;
|
||||||
provides java.nio.file.spi.FileSystemProvider
|
provides FileSystemProvider with FTPFileSystemProvider;
|
||||||
with org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
provides Provider with FTPFilesProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.xbib.io.ftp.fs.spi;
|
||||||
|
|
||||||
|
import org.xbib.io.ftp.fs.FTPEnvironment;
|
||||||
|
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class FTPContext {
|
||||||
|
|
||||||
|
final FTPFileSystemProvider provider;
|
||||||
|
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
|
FTPContext(URI uri, Map<String, ?> env) throws IOException {
|
||||||
|
this.provider = new FTPFileSystemProvider();
|
||||||
|
this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
fileSystem.close();
|
||||||
|
}
|
||||||
|
}
|
391
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java
Normal file
391
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
package org.xbib.io.ftp.fs.spi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.nio.file.CopyOption;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.OpenOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.nio.file.attribute.UserPrincipal;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class FTPFiles implements org.xbib.files.Files {
|
||||||
|
|
||||||
|
private static final int READ_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final Set<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 FTPFiles(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, FileTime fileTime) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileTime getLastModifiedTime(String path) throws IOException{
|
||||||
|
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPrincipal getOwner(String path) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> filter(String path, DirectoryStream.Filter<Path> filter) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(Path source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(Path source, String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void download(Path source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target, READ_BUFFER_SIZE);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void download(String source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
Files.copy(ctx.fileSystem.getPath(source), target);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(String source) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
Files.deleteIfExists(ctx.fileSystem.getPath(source));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(FTPContext ctx,
|
||||||
|
ReadableByteChannel source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForWrite(target, dirPerms, filePerms);
|
||||||
|
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(FTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
OutputStream outputStream,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(FTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
WritableByteChannel writableByteChannel,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
|
||||||
|
bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(FTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForWrite(target);
|
||||||
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
|
||||||
|
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForWrite(Path path) throws IOException {
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path parent = path.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
if (!Files.exists(parent)) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
Files.createFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForWrite(Path path,
|
||||||
|
Set<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,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
|
||||||
|
int read;
|
||||||
|
while ((read = readableByteChannel.read(buffer)) > 0) {
|
||||||
|
buffer.flip();
|
||||||
|
while (read > 0) {
|
||||||
|
read -= writableByteChannel.write(buffer);
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T performWithContext(WithContext<T> action) throws IOException {
|
||||||
|
FTPContext ctx = null;
|
||||||
|
try {
|
||||||
|
if (uri != null) {
|
||||||
|
ctx = new FTPContext(uri, env);
|
||||||
|
return action.perform(ctx);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (ctx != null) {
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.io.ftp.fs.spi;
|
||||||
|
|
||||||
|
import org.xbib.files.Files;
|
||||||
|
import org.xbib.files.Provider;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class FTPFilesProvider implements Provider {
|
||||||
|
@Override
|
||||||
|
public Files provide(URI uri, Map<String, ?> env) {
|
||||||
|
return !uri.isAbsolute() || uri.getScheme().equals("file") ? new FTPFiles(uri, env) : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.xbib.io.ftp.fs.spi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
interface WithContext<T> {
|
||||||
|
T perform(FTPContext ctx) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.xbib.io.ftp.fs.spi.FTPFilesProvider
|
0
files-ftp/build.gradle
Normal file
0
files-ftp/build.gradle
Normal file
|
@ -1,4 +1,5 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api project(':files-api')
|
||||||
api project(':files-sftp')
|
api project(':files-sftp')
|
||||||
testImplementation "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}"
|
testImplementation "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import org.apache.sshd.fs.SftpFileSystemProvider;
|
import org.apache.sshd.fs.SftpFileSystemProvider;
|
||||||
import java.nio.file.spi.FileSystemProvider;
|
import java.nio.file.spi.FileSystemProvider;
|
||||||
|
import org.xbib.files.Provider;
|
||||||
|
import org.apache.sshd.fs.spi.SFTPFilesProvider;
|
||||||
|
|
||||||
module org.xbib.files.sftp.fs {
|
module org.xbib.files.sftp.fs {
|
||||||
exports org.apache.sshd.fs;
|
exports org.apache.sshd.fs;
|
||||||
|
exports org.apache.sshd.fs.spi;
|
||||||
|
requires org.xbib.files;
|
||||||
requires transitive org.xbib.files.sftp;
|
requires transitive org.xbib.files.sftp;
|
||||||
provides FileSystemProvider with SftpFileSystemProvider;
|
provides FileSystemProvider with SftpFileSystemProvider;
|
||||||
|
provides Provider with SFTPFilesProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.apache.sshd.fs.spi;
|
||||||
|
|
||||||
|
import org.apache.sshd.client.ClientBuilder;
|
||||||
|
import org.apache.sshd.client.SshClient;
|
||||||
|
import org.apache.sshd.fs.SftpFileSystem;
|
||||||
|
import org.apache.sshd.fs.SftpFileSystemProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class SFTPContext {
|
||||||
|
|
||||||
|
private final SshClient sshClient;
|
||||||
|
|
||||||
|
final SftpFileSystemProvider provider;
|
||||||
|
|
||||||
|
final SftpFileSystem fileSystem;
|
||||||
|
|
||||||
|
SFTPContext(URI uri, Map<String, ?> env) throws IOException {
|
||||||
|
this.sshClient = ClientBuilder.builder().build();
|
||||||
|
Object object = env.get("workers");
|
||||||
|
if (object instanceof Integer) {
|
||||||
|
sshClient.setNioWorkers((Integer) object);
|
||||||
|
} else if (object instanceof String) {
|
||||||
|
sshClient.setNioWorkers(Integer.parseInt((String) object));
|
||||||
|
} else {
|
||||||
|
// we do not require a vast pool of threads
|
||||||
|
sshClient.setNioWorkers(1);
|
||||||
|
}
|
||||||
|
sshClient.start();
|
||||||
|
this.provider = new SftpFileSystemProvider(sshClient);
|
||||||
|
this.fileSystem = provider.newFileSystem(uri, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
sshClient.stop();
|
||||||
|
fileSystem.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,392 @@
|
||||||
|
package org.apache.sshd.fs.spi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.nio.file.CopyOption;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.OpenOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.nio.file.attribute.UserPrincipal;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SFTPFiles implements org.xbib.files.Files {
|
||||||
|
|
||||||
|
private static final int READ_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
|
private static final Set<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 SFTPFiles(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, FileTime fileTime) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileTime getLastModifiedTime(String path) throws IOException{
|
||||||
|
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPrincipal getOwner(String path) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectoryStream<Path> filter(String path, DirectoryStream.Filter<Path> filter) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target, READ_BUFFER_SIZE);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(String source) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.deleteIfExists(ctx.fileSystem.getPath(source)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(Path source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
|
||||||
|
dirPermissions, filePermissions, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(Path source, String target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
||||||
|
dirPermissions, filePermissions, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, Path target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
|
||||||
|
dirPermissions, filePermissions, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(InputStream source, String target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
||||||
|
dirPermissions, filePermissions, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(SFTPContext ctx,
|
||||||
|
ReadableByteChannel source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForWrite(target, dirPerms, filePerms);
|
||||||
|
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(SFTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
OutputStream outputStream,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(SFTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
WritableByteChannel writableByteChannel,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
|
||||||
|
bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void download(SFTPContext ctx,
|
||||||
|
Path source,
|
||||||
|
Path target,
|
||||||
|
int bufferSize,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
prepareForRead(target);
|
||||||
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
|
||||||
|
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForRead(Path path) throws IOException {
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path parent = path.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
if (!Files.exists(parent)) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
Files.createFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForWrite(Path path,
|
||||||
|
Set<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,
|
||||||
|
int bufferSize) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
|
||||||
|
int read;
|
||||||
|
while ((read = readableByteChannel.read(buffer)) > 0) {
|
||||||
|
buffer.flip();
|
||||||
|
while (read > 0) {
|
||||||
|
read -= writableByteChannel.write(buffer);
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T performWithContext(WithContext<T> action) throws IOException {
|
||||||
|
SFTPContext ctx = null;
|
||||||
|
try {
|
||||||
|
if (uri != null) {
|
||||||
|
ctx = new SFTPContext(uri, env);
|
||||||
|
return action.perform(ctx);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (ctx != null) {
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.apache.sshd.fs.spi;
|
||||||
|
|
||||||
|
import org.xbib.files.Files;
|
||||||
|
import org.xbib.files.Provider;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SFTPFilesProvider implements Provider {
|
||||||
|
@Override
|
||||||
|
public Files provide(URI uri, Map<String, ?> env) {
|
||||||
|
return !uri.isAbsolute() || uri.getScheme().equals("file") ? new SFTPFiles(uri, env) : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.apache.sshd.fs.spi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
interface WithContext<T> {
|
||||||
|
T perform(SFTPContext ctx) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.apache.sshd.fs.spi.SFTPFilesProvider
|
|
@ -1,3 +1,4 @@
|
||||||
|
include 'files-api'
|
||||||
include 'files-eddsa'
|
include 'files-eddsa'
|
||||||
include 'files-zlib'
|
include 'files-zlib'
|
||||||
include 'files-ftp'
|
include 'files-ftp'
|
||||||
|
|
Loading…
Reference in a new issue