From 558c4b0ae12ead23fc67abdf281e7a33fa5751ae Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Mon, 1 Jan 2024 22:51:47 +0100 Subject: [PATCH] allow relative paths for SFTP --- .../src/test/java/FilesListTest.java | 3 + .../java/no/maddin/niofs/sftp/SFTPHost.java | 10 +++ .../java/no/maddin/niofs/sftp/SFTPPath.java | 89 +++++++++++++------ .../java/no/maddin/niofs/sftp/PartsTest.java | 21 +++-- .../niofs/testutil/SftpgoContainer.java | 4 +- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/integration-tests/src/test/java/FilesListTest.java b/integration-tests/src/test/java/FilesListTest.java index 692d8de..a3a298c 100644 --- a/integration-tests/src/test/java/FilesListTest.java +++ b/integration-tests/src/test/java/FilesListTest.java @@ -1,6 +1,8 @@ +import no.maddin.niofs.sftp.SFTPPath; import no.maddin.niofs.testutil.BasicTestContainer; import no.maddin.niofs.testutil.FileUtils; import no.maddin.niofs.testutil.SftpgoContainer; +import no.maddin.niofs.webdav.WebdavPath; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -12,6 +14,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPHost.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPHost.java index fcebc9b..cbd0ad0 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPHost.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPHost.java @@ -25,6 +25,8 @@ public class SFTPHost extends FileSystem { private final AtomicBoolean open = new AtomicBoolean(); + static final SFTPHost SFTP_TEST_HOST = new SFTPHost(); + SFTPHost(SFTPFileSystemProvider provider, URI serverUri) { this.serverUri = serverUri; this.provider = provider; @@ -34,6 +36,14 @@ public class SFTPHost extends FileSystem { open.set(true); } + private SFTPHost() { + this.provider = null; + this.host = null; + this.port = 0; + this.userInfo = null; + this.serverUri = null; + } + private static UserInfo userInfo(String userInfo) { String un = null; String pw = null; diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPPath.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPPath.java index 9e9d107..043f2ec 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPPath.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPPath.java @@ -19,21 +19,23 @@ public class SFTPPath implements Path { static final String PATH_SEP = "/"; private final String path; private final SFTPHost host; - private final java.util.List parts; + private final List parts; - SFTPPath(SFTPHost sftpHost, String path) { + SFTPPath(@NotNull SFTPHost sftpHost, String path) { this.host = sftpHost; - parts = splitParts(path); - this.path = "/" + String.join(PATH_SEP, parts); + if (path == null) { + this.path = ""; + this.parts = Collections.emptyList(); + } else { + this.path = path; + this.parts = splitParts(path); + } } - private static List splitParts(String path) { - if (path == null || !path.startsWith(PATH_SEP)) { - throw new IllegalArgumentException("Path must start with " + PATH_SEP); - } + private static List splitParts(@NotNull String path) { Deque parts = new ArrayDeque<>(); try { - for (String p : path.substring(1).split(PATH_SEP, -1)) { + for (String p : path.split(PATH_SEP, -1)) { if (p.isEmpty() || p.equals(".")) { // ignore } else if (p.equals("..")) { @@ -48,63 +50,73 @@ private static List splitParts(String path) { return new ArrayList<>(parts); } - private String combineParts(int startIdx, int endIdx) { - StringBuilder sb = new StringBuilder(PATH_SEP); - for (String part : parts.subList(startIdx, endIdx)) { - if (sb.length() > 0) { + private String combineParts(int endIdx) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (path.startsWith(PATH_SEP)) { + sb.append(PATH_SEP); + } + for (String part : parts.subList(0, endIdx)) { + if (!first) { sb.append(PATH_SEP); } sb.append(part); + first = false; } return sb.toString(); } @Override - public FileSystem getFileSystem() { + public @NotNull FileSystem getFileSystem() { return this.host; } @Override public boolean isAbsolute() { - return path.startsWith(PATH_SEP); + return path.startsWith(PATH_SEP) && host != null; } @Override public Path getRoot() { - if (path == null) { - return this; + if (host == null) { + return null; } - return new SFTPPath(this.host, null); + return new SFTPPath(this.host, PATH_SEP); } @Override public Path getFileName() { - throw new UnsupportedOperationException("Not Implemented"); + if (path.isEmpty()) { + return null; + } + return new SFTPPath(this.host, parts.get(parts.size() - 1)); } @Override public Path getParent() { - if (path == null) { + if (!path.startsWith(PATH_SEP)) { return null; } - return new SFTPPath(this.host, combineParts(0, getNameCount() - 1)); + return new SFTPPath(this.host, combineParts(getNameCount() - 1)); } @Override public int getNameCount() { - return parts.size(); } @Override public @NotNull Path getName(int index) { - throw new UnsupportedOperationException("Not Implemented"); + if (index < 0 || index >= parts.size()) { + throw new IllegalArgumentException("index"); + } + return new SFTPPath(this.host, parts.get(index - 1)); } @Override public @NotNull Path subpath(int beginIndex, int endIndex) { - return new SFTPPath(beginIndex == 0 ? host : null, combineParts(0, endIndex)); + return new SFTPPath(beginIndex == 0 ? host : null, combineParts(endIndex)); } @Override @@ -134,12 +146,9 @@ public boolean endsWith(@NotNull String other) { return false; } - /** - * SFTPPAths are normalized at creation time. This just returns itself. - */ @Override public @NotNull Path normalize() { - return this; + return new SFTPPath(this.host, combineParts(getNameCount())); } @Override @@ -228,4 +237,28 @@ public String getPathString() { public List getParts() { return Collections.unmodifiableList(parts); } + + @Override + @NotNull + public String toString() { + StringBuilder sb = new StringBuilder(); + if (host != null && path.startsWith(PATH_SEP)) { + sb.append(host); + } + sb.append(path); + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SFTPPath) { + SFTPPath other = (SFTPPath) obj; + return Objects.equals(this.host, other.host) && Objects.equals(this.path, other.path); + } + return false; + } + + public int hashCode() { + return Objects.hash(this.host, this.path); + } } \ No newline at end of file diff --git a/sftp/src/test/java/no/maddin/niofs/sftp/PartsTest.java b/sftp/src/test/java/no/maddin/niofs/sftp/PartsTest.java index 7155f67..b7a075f 100644 --- a/sftp/src/test/java/no/maddin/niofs/sftp/PartsTest.java +++ b/sftp/src/test/java/no/maddin/niofs/sftp/PartsTest.java @@ -27,17 +27,22 @@ static Stream validPathsData() { Arguments.of("/aa/bb", Arrays.asList("aa", "bb")), Arguments.of("/aa/../bb", Collections.singletonList("bb")), Arguments.of("/aa/../bb", Collections.singletonList("bb")), - Arguments.of("/aa/../bb/cc/../d.txt", Arrays.asList("bb", "d.txt")) + Arguments.of("/aa/../bb/cc/../d.txt", Arrays.asList("bb", "d.txt")), + Arguments.of(".", Collections.emptyList()), + Arguments.of("./", Collections.emptyList()), + Arguments.of("", Collections.emptyList()), + Arguments.of("aa", Collections.singletonList("aa")), + Arguments.of(null, Collections.emptyList()) ); } static Stream invalidPathsData() { return Stream.of( - Arguments.of(".", instanceOf(IllegalArgumentException.class)), - Arguments.of("", instanceOf(IllegalArgumentException.class)), - Arguments.of(null, instanceOf(IllegalArgumentException.class)), - Arguments.of("./", instanceOf(IllegalArgumentException.class)), - Arguments.of("aa", instanceOf(IllegalArgumentException.class)), +// Arguments.of(".", instanceOf(IllegalArgumentException.class)), +// Arguments.of("", instanceOf(IllegalArgumentException.class)), +// Arguments.of(null, instanceOf(IllegalArgumentException.class)), +// Arguments.of("./", instanceOf(IllegalArgumentException.class)), +// Arguments.of("aa", instanceOf(IllegalArgumentException.class)), Arguments.of("/aa/../../bb.txt", instanceOf(IllegalArgumentException.class)), Arguments.of("../", instanceOf(IllegalArgumentException.class)) ); @@ -46,7 +51,7 @@ static Stream invalidPathsData() { @ParameterizedTest @MethodSource({"validPathsData"}) void validPaths(String input, List result) { - SFTPPath path = new SFTPPath(null, input); + SFTPPath path = new SFTPPath(SFTPHost.SFTP_TEST_HOST, input); assertThat(path, hasProperty("parts", is(equalTo(result)))); } @@ -55,7 +60,7 @@ void validPaths(String input, List result) { @MethodSource({"invalidPathsData"}) void invalidPaths(String input, Matcher expectedException) { try { - SFTPPath path = new SFTPPath(null, input); + SFTPPath path = new SFTPPath(SFTPHost.SFTP_TEST_HOST, input); fail("Call with '" + input + "' should have failed."); } catch (Exception ex) { diff --git a/test-util/src/main/java/no/maddin/niofs/testutil/SftpgoContainer.java b/test-util/src/main/java/no/maddin/niofs/testutil/SftpgoContainer.java index 9ea863d..629e37d 100644 --- a/test-util/src/main/java/no/maddin/niofs/testutil/SftpgoContainer.java +++ b/test-util/src/main/java/no/maddin/niofs/testutil/SftpgoContainer.java @@ -46,7 +46,9 @@ public SftpgoContainer(String testDataResource) { .withClasspathResourceMapping(testDataResource, "/srv/sftpgo", BindMode.READ_WRITE) .withClasspathResourceMapping("/sftpgo-config", "/var/lib/sftpgo", BindMode.READ_WRITE) .withExposedPorts(WEBDAV_PORT, SFTP_PORT) - .waitingFor(Wait.forHttp("/").forPort(WEBDAV_PORT).withBasicCredentials(USERNAME, PASSWORD).forStatusCode(207)); + .waitingFor(Wait.forHttp("/").forPort(WEBDAV_PORT).withBasicCredentials(USERNAME, PASSWORD).forStatusCode(207)) + .waitingFor(Wait.forListeningPorts(SFTP_PORT)) + ; } @SuppressWarnings("java:S112")