Commit a08aa147 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '11-ftp-folders-navigation' into 'master'

Improved session-bound folder navigation

Closes #11

See merge request !7
parents 4b689a4b 234637f8
......@@ -64,7 +64,7 @@ public interface RepositoryFilePersistence extends JpaRepository<RepositoryFile,
* @param prefix the prefix
* @return the list of paths
*/
@Query("select distinct(rf.path) from RepositoryFile rf where rf.path like ?1%")
@Query("select distinct(rf.path) from RepositoryFile rf where rf.path like ?1% and locate('/', trim(both '/' from substring(rf.path, length(?1), 200))) = 0")
List<String> listDistinctPaths(String prefix);
/**
......
......@@ -101,9 +101,7 @@ public abstract class CanBeAnythingFile implements FtpFile {
public abstract boolean mkdir();
@Override
public boolean delete() {
return false;
}
public abstract boolean delete();
@Override
public boolean move(FtpFile destination) {
......
......@@ -52,7 +52,7 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
private TemporaryBytesManager bytesManager;
private RepositoryFtpFile file(RepositoryFile repositoryFile) {
LOG.debug("Making RepositoryFtpFile repositoryFile={}", repositoryFile);
LOG.trace("Making RepositoryFtpFile repositoryFile={}", repositoryFile);
RepositoryFtpFile rff = new RepositoryFtpFile(repositoryFile) {
......@@ -110,7 +110,7 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
}
private RepositoryFtpDirectory directory(String path, RepositoryFileSystemView session) {
LOG.debug("Making RepositoryFtpDirectory path={}", path);
LOG.trace("Making RepositoryFtpDirectory path={}", path);
RepositoryFtpDirectory rfd = new RepositoryFtpDirectory(path) {
@Override
......@@ -129,9 +129,24 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
@Override
public List<? extends FtpFile> listFiles() {
LOG.info("Listing files in path={} service={}", this.getAbsolutePath(), repositoryService);
LOG.debug("Listing files in path={}", this.getAbsolutePath());
ArrayList<FtpFile> all = new ArrayList<>();
all.addAll(session.temporaryDirs.stream().map(path -> directory(path, session)).collect(Collectors.toList()));
final Path root = Paths.get(getAbsolutePath());
all.addAll(session.temporaryDirs.stream().filter(path -> path.startsWith(getAbsolutePath()) && !path.equals(getAbsolutePath()))
// we have full paths as string, filter the ones that are direct children
.map(path -> {
Path relativized = root.relativize(Paths.get(path));
LOG.trace("Rel={} rel[0]={} root.resolve={}", relativized.toString(), relativized.getName(0), root.resolve(relativized.getName(0)).normalize().toString());
return root.resolve(relativized.getName(0)).normalize().toString();
})
// unique
.distinct().peek(p -> {
LOG.debug("Temporary folder={}", p);
})
// map to RepositoryFtpDirectory
.map(path -> directory(path, session)).collect(Collectors.toList()));
all.addAll(repositoryService.getFiles(this.getAbsolutePath()).stream().map(rf -> file(rf)).collect(Collectors.toList()));
all.addAll(repositoryService.listPaths(getAbsolutePath()).stream().map(path -> directory(path, session)).collect(Collectors.toList()));
all.sort((a, b) -> {
......@@ -155,8 +170,8 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
@Override
public boolean delete() {
LOG.info("Delete this={}", getAbsolutePath());
if (session.temporaryDirs.contains(getAbsolutePath())) {
session.temporaryDirs.remove(getAbsolutePath());
if (session.hasTempDir(getAbsolutePath())) {
session.removeTempDir(getAbsolutePath());
return true;
}
LOG.warn("Not deleting repository folder={}", getAbsolutePath());
......@@ -165,8 +180,11 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
@Override
public boolean changeWorkingDirectory(String dir) {
LOG.info("CWD this={} dir={}", getAbsolutePath(), dir);
return false;
String normalized = Paths.get(getAbsolutePath()).resolve(dir).normalize().toAbsolutePath().toString();
LOG.info("CWD this={} dir={} normalized={}", getAbsolutePath(), dir, normalized);
this.cwd(normalized);
// TODO Check if such path exists?
return true;
}
};
......@@ -184,17 +202,18 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
public FtpFile getFile(String file) throws FtpException {
LOG.debug("getFile file={} for user={}", file, username);
Path path = Paths.get(cwd.getAbsolutePath(), file).normalize();
LOG.info("Resolved normalized={}", path.toString());
LOG.trace("Resolved normalized={}", path.toString());
if (temporaryDirs.contains(path.toString())) {
LOG.debug("dir={} is a temporary session-bound directory", path);
if (temporaryDirs.stream().filter(longpath -> longpath.startsWith(path.toString())).findFirst().isPresent()) {
LOG.trace("dir={} is a temporary session-bound directory", path);
return directory(path.toString(), this);
}
try {
return path.endsWith("/") ? directory(path.toString(), this) : file(repositoryService.getFile(path.getParent().toString(), path.getFileName().toString()));
} catch (NoSuchRepositoryFileException e) {
LOG.info("Making new CanBeAnythingFile path={} name={}", path.getParent().toString(), path.getFileName().toString());
LOG.debug("Making new CanBeAnythingFile path={} name={}", path.getParent().toString(), path.getFileName().toString());
return new CanBeAnythingFile(path.getParent().toString(), path.getFileName().toString()) {
@Override
......@@ -204,6 +223,13 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
return true;
}
public boolean delete() {
Set<String> matches = temporaryDirs.stream().filter(longpath -> longpath.startsWith(getAbsolutePath())).collect(Collectors.toSet());
LOG.debug("Removing session-bound directories {}", matches);
temporaryDirs.removeAll(matches);
return true;
};
@Override
public OutputStream createOutputStream(long offset) throws IOException {
LOG.info("Creating output stream for new file={} at offset={}", getAbsolutePath(), offset);
......@@ -235,6 +261,12 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
this.user = user;
}
public void removeTempDir(String absolutePath) {
Set<String> matches = temporaryDirs.stream().filter(longpath -> longpath.startsWith(absolutePath)).collect(Collectors.toSet());
LOG.debug("Removing session-bound directories {}", matches);
temporaryDirs.removeAll(matches);
}
@Override
public boolean isRandomAccessible() throws FtpException {
// TODO Auto-generated method stub
......@@ -263,5 +295,9 @@ public class RepositoryFileSystemFactory implements FileSystemFactory, Initializ
LOG.debug("CWD dir={} for user={}", dir, username);
return this.cwd.changeWorkingDirectory(dir);
}
public boolean hasTempDir(String absolutePath) {
return temporaryDirs.stream().filter(longpath -> longpath.startsWith(absolutePath)).findFirst().isPresent();
}
}
}
......@@ -18,6 +18,8 @@ package org.genesys.filerepository.service.ftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.apache.ftpserver.ftplet.FtpFile;
......@@ -30,28 +32,26 @@ import org.apache.ftpserver.ftplet.FtpFile;
*/
public abstract class RepositoryFtpDirectory implements FtpFile {
private String parent;
private String name;
private Path path;
public RepositoryFtpDirectory() {
public RepositoryFtpDirectory(String path) {
assert (path != null);
cwd(path);
}
public RepositoryFtpDirectory(String path) {
final String name = path.substring(path.lastIndexOf('/'));
// System.err.println("RFD path=" + path + " name=" + name);
this.name = name;
this.parent = path.substring(0, path.length() - name.length());
// System.err.println("RFD path=" + path + " name=" + name + " parent=" + parent);
protected void cwd(String path) {
this.path = Paths.get(path).normalize().toAbsolutePath();
}
@Override
public String getAbsolutePath() {
return this.parent.length() == 0 || this.parent == null ? this.name : this.parent.concat("/").concat(this.name);
return path.normalize().toAbsolutePath().toString();
}
@Override
public String getName() {
return this.name;
return path.getFileName().toString();
}
@Override
......
......@@ -49,13 +49,13 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
private int ftpPort;
private int maxThreads;
private int maxThreads = 20;
// The maximum number of simultaneous users
private int maxLogins;
private int maxLogins = 10;
// Idle timeout
private int idleTimeout;
private int idleTimeout = 60;
private String keystorePath;
......@@ -66,6 +66,12 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
@Autowired
private FileSystemFactory repositoryFileSystemFactory;
/**
* Set the path to Java keystore
*
* <pre>keytool -genkey -alias testdomain -keyalg RSA -keystore ftpserver.jks -keysize 4096</pre>
* @param keystorePath
*/
public void setKeystorePath(final String keystorePath) {
this.keystorePath = keystorePath;
}
......@@ -86,9 +92,21 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
this.ftpPort = ftpPort;
}
public void setMaxLogins(int maxLogins) {
this.maxLogins = maxLogins;
}
public void setIdleTimeout(int idleTimeout) {
this.idleTimeout = idleTimeout;
}
public void setMaxThreads(int maxThreads) {
this.maxThreads = maxThreads;
}
public void afterPropertiesSet() throws FtpException {
FtpServerFactory serverFactory = new FtpServerFactory();
serverFactory.setUserManager(userManager);
if (messageResource != null) {
serverFactory.setMessageResource(messageResource);
......@@ -121,7 +139,7 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
final ConnectionConfig ftpConnectionConfig = new ConnectionConfig() {
@Override
public boolean isAnonymousLoginEnabled() {
return true;
return false;
}
@Override
......@@ -142,7 +160,7 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
@Override
public int getMaxAnonymousLogins() {
return 3;
return 0;
}
// The number of milliseconds that the connection is delayed after a failed login attempt.
......@@ -152,7 +170,7 @@ public class RepositoryFtpServer implements InitializingBean, DisposableBean {
}
};
serverFactory.setConnectionConfig(ftpConnectionConfig);
Map<String, Ftplet> ftplets = new HashMap<>();
ftplets.put("default", repositoryFtplet());
serverFactory.setFtplets(ftplets);
......
......@@ -70,7 +70,7 @@ public class ApplicationConfig {
user.setPassword(username + "1!");
user.setEnabled(true);
List<Authority> authorities = new ArrayList<>();
authorities.add(new ConcurrentLoginPermission(2, 0));
authorities.add(new ConcurrentLoginPermission(10, 0));
authorities.add(new WritePermission());
user.setAuthorities(authorities);
user.setHomeDirectory("/");
......
......@@ -34,6 +34,7 @@ import org.genesys.filerepository.config.DatabaseConfig;
import org.genesys.filerepository.config.ServiceBeanConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
......@@ -70,6 +71,19 @@ public class FtpServerTest {
return ftp;
}
/**
* Uncomment the @Ignore and run this test to connect with other clients for testing. Press enter in the console to
* stop the server.
*
* @throws IOException
*/
@Ignore
@Test
public void waitForKeypress() throws IOException {
System.err.println("Press enter to stop server!");
System.in.read();
}
@Test
public void serverListening() throws SocketException, IOException {
final FTPClient ftp = createFtpClient();
......@@ -81,7 +95,7 @@ public class FtpServerTest {
ftp.disconnect();
}
}
@Test
public void serverListeningFTPS() throws SocketException, IOException {
final FTPSClient ftp = (FTPSClient) createFtpClient();
......@@ -126,13 +140,13 @@ public class FtpServerTest {
ftp.makeDirectory("test1");
FTPFile[] files = ftp.listFiles();
assertThat(files, notNullValue());
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("/test1"));
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("test1"));
assertThat(Arrays.stream(files).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true));
ftp.makeDirectory("test2");
files = ftp.listFiles();
assertThat(files, notNullValue());
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("/test1", "/test2"));
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("test1", "test2"));
assertThat(Arrays.stream(files).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true));
debugListFiles(ftp);
......@@ -164,18 +178,22 @@ public class FtpServerTest {
ftp.makeDirectory("test1");
ftp.makeDirectory("test2");
ftp.makeDirectory("test3/a/b/c/d");
debugListFiles(ftp);
FTPFile[] files = ftp.listFiles();
assertThat(files, notNullValue());
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("/test1", "/test2"));
assertThat(Arrays.stream(files).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true));
assertThat(Arrays.stream(files).map(file -> file.getName()).collect(Collectors.toList()), hasItems("test1", "test2", "test3"));
assertThat(Arrays.stream(files).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true, true));
debugListFiles(ftp);
assertThat(ftp.removeDirectory("/test1"), is(true));
assertThat(ftp.removeDirectory("/test2"), is(true));
assertThat(Arrays.stream(ftp.listFiles()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("test3"));
assertThat(ftp.removeDirectory("test3"), is(true));
assertThat(ftp.listFiles().length, is(0));
} finally {
ftp.disconnect();
......@@ -345,4 +363,64 @@ public class FtpServerTest {
}
}
@Test
public void folderNavigationTest() throws SocketException, IOException {
final FTPClient ftp = createFtpClient();
try {
ftp.connect("localhost", 8021);
assertThat("FTP server refused connection", FTPReply.isPositiveCompletion(ftp.getReplyCode()), is(true));
ftp.login(username, password);
assertThat("Login failed", FTPReply.isPositiveCompletion(ftp.getReplyCode()), is(true));
assertThat(ftp.printWorkingDirectory(), is("/"));
assertThat(ftp.makeDirectory("folder1"), is(true));
assertThat(ftp.makeDirectory("folder2"), is(true));
assertThat(ftp.makeDirectory("folder3/"), is(true));
assertThat(Arrays.stream(ftp.listDirectories()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("folder1", "folder2", "folder3"));
assertThat(Arrays.stream(ftp.listDirectories()).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true, true));
assertThat(ftp.makeDirectory("/a/b1/c"), is(true));
assertThat(ftp.makeDirectory("/a/b2/c/d"), is(true));
assertThat(ftp.makeDirectory("/a/b3/c1/d"), is(true));
assertThat(ftp.makeDirectory("/a/b3/c2/d"), is(true));
assertThat(ftp.makeDirectory("/a/b3/c3/d"), is(true));
assertThat(ftp.makeDirectory("/a/b3/c4/d"), is(true));
assertThat(ftp.makeDirectory("1/2/3"), is(true));
assertThat(ftp.makeDirectory("/4/5/6"), is(true));
debugListFiles(ftp);
assertThat(ftp.changeWorkingDirectory("a"), is(true));
debugListFiles(ftp);
assertThat(Arrays.stream(ftp.listDirectories()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("b1", "b2", "b3"));
assertThat(Arrays.stream(ftp.listFiles()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("b1", "b2", "b3"));
assertThat(Arrays.stream(ftp.listFiles()).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true, true));
assertThat(ftp.changeWorkingDirectory("/a/b3"), is(true));
debugListFiles(ftp);
assertThat(Arrays.stream(ftp.listDirectories()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("c1", "c2", "c3", "c4"));
assertThat(Arrays.stream(ftp.listFiles()).map(file -> file.getName()).collect(Collectors.toList()), hasItems("c1", "c2", "c3", "c4"));
assertThat(Arrays.stream(ftp.listFiles()).map(file -> file.isDirectory()).collect(Collectors.toList()), hasItems(true, true, true, true));
assertThat(ftp.changeWorkingDirectory("/a/b3/c4"), is(true));
assertThat(ftp.changeWorkingDirectory(".."), is(true));
assertThat(ftp.printWorkingDirectory(), is("/a/b3"));
assertThat(ftp.changeWorkingDirectory("/a/b1/c"), is(true));
assertThat(ftp.listFiles().length, is(0));
assertThat(ftp.changeWorkingDirectory("/"), is(true));
Arrays.stream(ftp.listDirectories()).forEach(dir -> {
try {
ftp.removeDirectory(dir.getName());
} catch (IOException e) {
}
});
assertThat(ftp.listFiles().length, is(0));
} finally {
ftp.disconnect();
}
}
}
......@@ -25,6 +25,6 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %t %5p %c{1}:%L - %m
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=error, stdout
log4j.category.org.genesys.filerepository=trace
log4j.category.org.apache.ftpserver=trace
log4j.category.org.apache.commons.net=trace
log4j.category.org.genesys.filerepository=debug
log4j.category.org.apache.ftpserver=debug
log4j.category.org.apache.commons.net=info
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment