From 629b39b76437b11ecee5e9ecf1b95a8ba5897a60 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Mon, 24 Feb 2025 20:56:58 +0100 Subject: [PATCH 1/9] readLink --- .../test/java/no/maddin/niofs/it/FilesIT.java | 21 +++- .../maddin/niofs/sftp/SFTPFileAttributes.java | 75 ++++++++++++++ .../niofs/sftp/SFTPFileSystemProvider.java | 99 +++++-------------- 3 files changed, 116 insertions(+), 79 deletions(-) create mode 100644 sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java diff --git a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java index 959c5fc..8162b8a 100644 --- a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java +++ b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java @@ -353,8 +353,21 @@ void isSymbolicLink(String protocol, Supplier containerSuppl } } - void readSymbolicLink() { - + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + void readSymbolicLink(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "Sardine has an incomplete implementation of the ACL"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + Path target = tmpFile.resolve("link.txt"); + Path linkFile = Files.createSymbolicLink(tmpFile, target); + assertThat(Files.readSymbolicLink(target), equalTo(linkFile)); + } } void getAttribute() { @@ -372,7 +385,7 @@ void readAttributes() { void getFileStore() { } - void getLasetModifiedTime() { + void getLastModifiedTime() { } void setModifiedTime() { @@ -405,7 +418,7 @@ void newBufferedWriter() { void newByteChannel() { } - void bewDirectoryStream() { + void newDirectoryStream() { } void newInputStream() { diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java new file mode 100644 index 0000000..9137007 --- /dev/null +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java @@ -0,0 +1,75 @@ +package no.maddin.niofs.sftp; + +import com.jcraft.jsch.SftpATTRS; + +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; + +public class SFTPFileAttributes implements BasicFileAttributes { + private final FileTime lastModifiedTime; + private final FileTime lastAccessTime; + private final FileTime creationTime; + private final boolean isRegularFile; + private final boolean isDirectory; + private final boolean isSymbolicLink; + private final boolean isOther; + private final long size; + private final Object fileKey; + + SFTPFileAttributes(SftpATTRS stat) { + this.lastModifiedTime = FileTime.fromMillis(stat.getMTime() * 1000L); + this.lastAccessTime = FileTime.fromMillis(stat.getATime() * 1000L); + this.creationTime = FileTime.fromMillis(stat.getMTime() * 1000L); + this.isRegularFile = stat.isReg(); + this.isDirectory = stat.isDir(); + this.isSymbolicLink = stat.isLink(); + this.isOther = !stat.isReg() && !stat.isDir() && !stat.isLink(); + this.size = stat.getSize(); + this.fileKey = null; + } + + @Override + public FileTime lastModifiedTime() { + return this.lastModifiedTime; + } + + @Override + public FileTime lastAccessTime() { + return this.lastAccessTime; + } + + @Override + public FileTime creationTime() { + return this.creationTime; + } + + @Override + public boolean isRegularFile() { + return this.isRegularFile; + } + + @Override + public boolean isDirectory() { + return this.isDirectory; + } + + @Override + public boolean isSymbolicLink() { + return this.isSymbolicLink; + } + + @Override + public boolean isOther() { + return this.isOther; + } + + @Override + public long size() { + return this.size; + } + + @Override + public Object fileKey() { + return this.fileKey; + } +} diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java index d9c400b..a3713a7 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java @@ -381,6 +381,10 @@ public void setAttribute(Path path, String attribute, Object value, LinkOption.. @Override public void createSymbolicLink(Path link, Path target, FileAttribute... attrs) throws IOException { + createLink(ChannelSftp::symlink, link, target, attrs); + } + + private void createLink(LinkFunction linkFunction, Path link, Path target, FileAttribute... attrs) throws IOException { if (!(link instanceof SFTPPath)) { throw new IllegalArgumentException(String.valueOf(link)); } @@ -390,10 +394,6 @@ public void createSymbolicLink(Path link, Path target, FileAttribute... attrs if (!link.isAbsolute()) { throw new IllegalArgumentException("link must be absolute"); } -// if (!target.isAbsolute()) { -// Could resolve from link to target, and convert to absolute -// throw new IllegalArgumentException("target must be absolute"); -// } SFTPPath sftpLink = (SFTPPath) link; SFTPPath sftpTarget = (SFTPPath) target; @@ -402,15 +402,29 @@ public void createSymbolicLink(Path link, Path target, FileAttribute... attrs } SFTPHost host = sftpLink.getHost(); try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { - sftpSession.sftp.symlink(sftpLink.getPathString(), sftpTarget.getPathString()); + linkFunction.createLink(sftpSession.sftp, sftpLink.getPathString(), sftpTarget.getPathString()); } catch (JSchException | SftpException e) { throw (FileSystemException)new FileSystemException(link.toString(), target.toString(), e.getMessage()).initCause(e); } } @Override - public void createLink(Path link, Path existing) throws IOException { - super.createLink(link, existing); + public void createLink(Path link, Path target) throws IOException { + createLink(ChannelSftp::hardlink, link, target); + } + + @Override + public Path readSymbolicLink(Path link) throws IOException { + if (!(link instanceof SFTPPath)) { + throw new IllegalArgumentException(String.valueOf(link)); + } + SFTPPath sftpLink = (SFTPPath) link; + SFTPHost host = sftpLink.getHost(); + try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { + return new SFTPPath(host, sftpSession.sftp.readlink(sftpLink.getPathString())); + } catch (JSchException | SftpException e) { + throw new FileSystemException(link.toString(), null, e.getMessage()); + } } void removeCacheEntry(URI serverUri) { @@ -499,73 +513,8 @@ public void close() { } - - public static class SFTPFileAttributes implements BasicFileAttributes { - private final FileTime lastModifiedTime; - private final FileTime lastAccessTime; - private final FileTime creationTime; - private final boolean isRegularFile; - private final boolean isDirectory; - private final boolean isSymbolicLink; - private final boolean isOther; - private final long size; - private final Object fileKey; - - private SFTPFileAttributes(SftpATTRS stat) { - this.lastModifiedTime = FileTime.fromMillis(stat.getMTime() * 1000L); - this.lastAccessTime = FileTime.fromMillis(stat.getATime() * 1000L); - this.creationTime = FileTime.fromMillis(stat.getMTime() * 1000L); - this.isRegularFile = stat.isReg(); - this.isDirectory = stat.isDir(); - this.isSymbolicLink = stat.isLink(); - this.isOther = !stat.isReg() && !stat.isDir() && !stat.isLink(); - this.size = stat.getSize(); - this.fileKey = null; - } - - @Override - public FileTime lastModifiedTime() { - return this.lastModifiedTime; - } - - @Override - public FileTime lastAccessTime() { - return this.lastAccessTime; - } - - @Override - public FileTime creationTime() { - return this.creationTime; - } - - @Override - public boolean isRegularFile() { - return this.isRegularFile; - } - - @Override - public boolean isDirectory() { - return this.isDirectory; - } - - @Override - public boolean isSymbolicLink() { - return this.isSymbolicLink; - } - - @Override - public boolean isOther() { - return this.isOther; - } - - @Override - public long size() { - return this.size; - } - - @Override - public Object fileKey() { - return this.fileKey; - } + @FunctionalInterface + private interface LinkFunction { + void createLink(ChannelSftp sftp, String source, String target) throws IOException, SftpException, JSchException; } } From 605123124b916b7a1573cb38284b0096217a9491 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Sun, 23 Mar 2025 17:20:20 +0100 Subject: [PATCH 2/9] attribute names --- .../test/java/no/maddin/niofs/it/FilesIT.java | 159 +++++++++++++++--- .../niofs/sftp/SFTPFileAttributeView.java | 7 +- .../maddin/niofs/sftp/SFTPFileAttributes.java | 24 ++- .../niofs/sftp/SFTPFileSystemProvider.java | 49 +++--- .../webdav/WebdavFileSystemProvider.java | 19 +-- 5 files changed, 188 insertions(+), 70 deletions(-) diff --git a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java index 8162b8a..dbcc5a3 100644 --- a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java +++ b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java @@ -6,6 +6,7 @@ import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -15,16 +16,18 @@ import java.io.File; import java.net.URI; import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; +import java.time.*; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; import static org.hamcrest.io.FileMatchers.anExistingDirectory; import static org.hamcrest.io.FileMatchers.anExistingFile; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -370,80 +373,180 @@ void readSymbolicLink(String protocol, Supplier containerSup } } - void getAttribute() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + void getAttribute(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + assertThat(Files.getAttribute(tmpFile, "basic:isDirectory"), equalTo(false)); + assertThat(Files.getAttribute(dir, "basic:isDirectory"), equalTo(true)); + } } - void setAttribute() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + void setAttribute(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "Sardine has an incomplete implementation of the ACL"); + Assumptions.assumeFalse(protocol.equals("sftp"), "Setting attributes is not supported by SFTP"); } - void getFileAttributeView() { - } + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled + void getFileAttributeView(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); - void readAttributes() { + BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(tmpFile, BasicFileAttributeView.class); + assertThat(fileAttributeView, hasProperty("owner", equalTo(true))); + assertThat(fileAttributeView.readAttributes(), hasProperty("lastModifiedTime", Matchers.notNullValue())); + + BasicFileAttributeView dirAttributeView = Files.getFileAttributeView(dir, BasicFileAttributeView.class); + assertThat(dirAttributeView, hasProperty("owner", equalTo(true))); + assertThat(dirAttributeView.readAttributes(), hasProperty("lastModifiedTime", Matchers.notNullValue())); + } } - void getFileStore() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void readAttributes(String protocol, Supplier containerSupplier) throws Exception { } - void getLastModifiedTime() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void getFileStore(String protocol, Supplier containerSupplier) throws Exception { } - void setModifiedTime() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void getLastModifiedTime(String protocol, Supplier containerSupplier) throws Exception { } - void getOwner() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void setModifiedTime(String protocol, Supplier containerSupplier) throws Exception { } - void setOwner() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void getOwner(String protocol, Supplier containerSupplier) throws Exception { } - void getPosixFilePermissions() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void setOwner(String protocol, Supplier containerSupplier) throws Exception { } - void setPosixFilePermissions() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void getPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { } - void isSameFile() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void setPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { } - void move() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void isSameFile(String protocol, Supplier containerSupplier) throws Exception { } - void newBufferedReader() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void move(String protocol, Supplier containerSupplier) throws Exception { } - void newBufferedWriter() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newBufferedReader(String protocol, Supplier containerSupplier) throws Exception { } - void newByteChannel() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newBufferedWriter(String protocol, Supplier containerSupplier) throws Exception { } - void newDirectoryStream() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newByteChannel(String protocol, Supplier containerSupplier) throws Exception { } - void newInputStream() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newDirectoryStream(String protocol, Supplier containerSupplier) throws Exception { } - void newOutputStream() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newInputStream(String protocol, Supplier containerSupplier) throws Exception { } - void probeContentType() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void newOutputStream(String protocol, Supplier containerSupplier) throws Exception { } - void readAllBytes() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void probeContentType(String protocol, Supplier containerSupplier) throws Exception { } + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void readAllBytes(String protocol, Supplier containerSupplier) throws Exception { + } - void readAllLines() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void readAllLines(String protocol, Supplier containerSupplier) throws Exception { } - void size() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void size(String protocol, Supplier containerSupplier) throws Exception { } - void walkFileTree() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void walkFileTree(String protocol, Supplier containerSupplier) throws Exception { } - void write() { + @ParameterizedTest(name = "{index} {0}") + @MethodSource("data") + @Disabled("Probably not yet implemented") + void write(String protocol, Supplier containerSupplier) throws Exception { } private static File localTestFile(String... parts) { diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java index d1a5efe..ce70664 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.nio.file.LinkOption; +import java.nio.file.Path; import java.nio.file.attribute.*; import java.util.Arrays; import java.util.Collections; @@ -11,12 +12,12 @@ public class SFTPFileAttributeView implements PosixFileAttributeView { private final SFTPPath path; private final SFTPFileSystemProvider provider; - private final List options; + private final LinkOption[] options; public SFTPFileAttributeView(SFTPFileSystemProvider sftpFileSystemProvider, SFTPPath path, LinkOption[] options) { this.path = path; this.provider = sftpFileSystemProvider; - this.options = options != null ? Arrays.asList(options) : Collections.emptyList(); + this.options = options; } @Override @@ -36,7 +37,7 @@ public void setOwner(UserPrincipal owner) throws IOException { @Override public PosixFileAttributes readAttributes() throws IOException { - throw new UnsupportedOperationException(); + return provider.readAttributes((Path)path, PosixFileAttributes.class, options); } @Override diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java index 9137007..0d4ecdb 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java @@ -4,6 +4,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; public class SFTPFileAttributes implements BasicFileAttributes { private final FileTime lastModifiedTime; @@ -17,9 +20,9 @@ public class SFTPFileAttributes implements BasicFileAttributes { private final Object fileKey; SFTPFileAttributes(SftpATTRS stat) { - this.lastModifiedTime = FileTime.fromMillis(stat.getMTime() * 1000L); - this.lastAccessTime = FileTime.fromMillis(stat.getATime() * 1000L); - this.creationTime = FileTime.fromMillis(stat.getMTime() * 1000L); + this.lastModifiedTime = FileTime.from(stat.getMTime(), TimeUnit.SECONDS); + this.lastAccessTime = FileTime.from(stat.getATime(), TimeUnit.SECONDS); + this.creationTime = FileTime.from(stat.getMTime(), TimeUnit.SECONDS); this.isRegularFile = stat.isReg(); this.isDirectory = stat.isDir(); this.isSymbolicLink = stat.isLink(); @@ -28,6 +31,21 @@ public class SFTPFileAttributes implements BasicFileAttributes { this.fileKey = null; } + public static Map asMap(SftpATTRS stat) { + SFTPFileAttributes attr = new SFTPFileAttributes(stat); + Map map = new HashMap<>(); + map.put("lastModifiedTime", attr.lastModifiedTime); + map.put("lastAccessTime", attr.lastAccessTime); + map.put("creationTime", attr.creationTime); + map.put("isRegularFile", attr.isRegularFile); + map.put("isDirectory", attr.isDirectory); + map.put("isSymbolicLink", attr.isSymbolicLink); + map.put("isOther", attr.isOther); + map.put("size", attr.size); + map.put("fileKey", attr.fileKey); + return map; + } + @Override public FileTime lastModifiedTime() { return this.lastModifiedTime; diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java index a3713a7..48fda22 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java @@ -89,9 +89,7 @@ private SFTPHost getSFTPHost(URI uri, boolean requireEmptyPath, boolean create) @Override public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { - if (!(path instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(path)); - } + sftpPath(path); return new JSchByteChannel(jsch, (SFTPPath)path, options, attrs); } @@ -178,9 +176,7 @@ private static boolean isFileExistsException(@NotNull SftpException e) { @Override public void delete(Path path) throws IOException { - if (!(path instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(path)); - } + SFTPPath sftpPath = sftpPath(path); SFTPHost sftpHost = (SFTPHost)path.getFileSystem(); boolean isDir = false; @@ -277,9 +273,7 @@ public boolean isSameFile(Path path, Path path2) { @Override public boolean isHidden(Path path) throws IOException { - if (!(path instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(path)); - } + sftpPath(path); SFTPPath sftpPath = (SFTPPath) path; SFTPHost host = (SFTPHost) sftpPath.getFileSystem(); try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { @@ -303,9 +297,7 @@ public FileStore getFileStore(Path path) { @Override public void checkAccess(Path path, AccessMode... modes) throws IOException { - if (!(path instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(path)); - } + sftpPath(path); SFTPPath sftpPath = (SFTPPath) path; SFTPHost host = (SFTPHost) sftpPath.getFileSystem(); try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { @@ -342,24 +334,26 @@ public void checkAccess(Path path, AccessMode... modes) throws IOException { @Override public V getFileAttributeView(Path path, Class type, LinkOption... options) { - if (!(path instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(path)); - } + sftpPath(path); if (type == null || !type.isAssignableFrom(SFTPFileAttributeView.class)) { throw new UnsupportedOperationException("Attribute view of type " + type + " not supported"); } return (V)new SFTPFileAttributeView(this, (SFTPPath) path, options); } - @Override - public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + private static SFTPPath sftpPath(Path path) { if (!(path instanceof SFTPPath)) { throw new IllegalArgumentException(String.valueOf(path)); } + return (SFTPPath)path; + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + SFTPPath sftpPath = sftpPath(path); if (type == null || !type.isAssignableFrom(SFTPFileAttributes.class)) { throw new UnsupportedOperationException("File attribute type " + type + " not supported"); } - SFTPPath sftpPath = (SFTPPath) path; SFTPHost host = sftpPath.getHost(); try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { SftpATTRS stat = sftpSession.sftp.lstat(sftpPath.getPathString()); @@ -370,8 +364,15 @@ public A readAttributes(Path path, Class type } @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) { - throw new UnsupportedOperationException(); + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException{ + SFTPPath sftpPath = sftpPath(path); + SFTPHost host = sftpPath.getHost(); + try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { + SftpATTRS stat = sftpSession.sftp.lstat(sftpPath.getPathString()); + return SFTPFileAttributes.asMap(stat); + } catch (JSchException | SftpException e) { + throw new FileSystemException(path.toString(), null, e.getMessage()); + } } @Override @@ -385,9 +386,7 @@ public void createSymbolicLink(Path link, Path target, FileAttribute... attrs } private void createLink(LinkFunction linkFunction, Path link, Path target, FileAttribute... attrs) throws IOException { - if (!(link instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(link)); - } + sftpPath(link); if (!Objects.equals(link.getFileSystem(), target.getFileSystem())) { throw new UnsupportedOperationException("Symbolic links between different filesystems not supported"); } @@ -415,9 +414,7 @@ public void createLink(Path link, Path target) throws IOException { @Override public Path readSymbolicLink(Path link) throws IOException { - if (!(link instanceof SFTPPath)) { - throw new IllegalArgumentException(String.valueOf(link)); - } + sftpPath(link); SFTPPath sftpLink = (SFTPPath) link; SFTPHost host = sftpLink.getHost(); try (SFTPSession sftpSession = new SFTPSession(host, jsch)) { diff --git a/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java b/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java index 38aac79..f5f25bd 100644 --- a/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java +++ b/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java @@ -394,7 +394,6 @@ public A readAttributes(Path path, Class type @Override public Map readAttributes(Path path, String attributes, LinkOption... arg2) throws IOException { - //throw new UnsupportedOperationException(); log.fine("readAttributes(path,sattr)"); if (!(path.getFileSystem() instanceof WebdavFileSystem)) { @@ -424,31 +423,31 @@ public Map readAttributes(Path path, String attributes, LinkOpti String[] attr = attributes.split(","); for(String a: attr) { switch(a) { - case "lastModifiedTime": + case "basic:lastModifiedTime": map.put("lastModifiedTime", wattr.lastModifiedTime()); break; - case "lastAccessTime": + case "basic:lastAccessTime": map.put("lastAccessTime", wattr.lastAccessTime()); break; - case "creationTime": + case "basic:creationTime": map.put("creationTime", wattr.creationTime()); break; - case "size": + case "basic:size": map.put("size", wattr.size()); break; - case "isRegularFile": + case "basic:isRegularFile": map.put("isRegularFile", wattr.isRegularFile()); break; - case "isDirectory": + case "basic:isDirectory": map.put("isDirectory", wattr.isDirectory()); break; - case "isSymbolicLink": + case "basic:isSymbolicLink": map.put("isSymbolicLink", wattr.isSymbolicLink()); break; - case "isOther": + case "basic:isOther": map.put("isOther", wattr.isSymbolicLink()); break; - case "fileKey": + case "basic:fileKey": map.put("fileKey", wattr.fileKey()); break; } From adab7911f1a2b2834853b357abba9015ee1369d9 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Sun, 23 Mar 2025 18:29:10 +0100 Subject: [PATCH 3/9] make site plugin work for java 8 --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index 5d067cb..e7d6a10 100644 --- a/pom.xml +++ b/pom.xml @@ -353,5 +353,15 @@ + + java8 + + 1.8 + + + + true + + From 33e340ec97eba6be6831cbb31e357c3ae289c03b Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Sun, 23 Mar 2025 18:38:24 +0100 Subject: [PATCH 4/9] remove unused imports --- .../test/java/no/maddin/niofs/it/FilesIT.java | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java index dbcc5a3..b2bc9ed 100644 --- a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java +++ b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java @@ -7,7 +7,6 @@ import org.junit.Assert; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -15,12 +14,12 @@ import java.io.File; import java.net.URI; -import java.nio.file.*; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributeView; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; -import java.time.*; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -30,7 +29,6 @@ import static org.hamcrest.Matchers.*; import static org.hamcrest.io.FileMatchers.anExistingDirectory; import static org.hamcrest.io.FileMatchers.anExistingFile; -import static org.junit.jupiter.api.Assertions.assertThrows; /** * We prepare a file structure and use the various providers list the files. @@ -390,6 +388,7 @@ void getAttribute(String protocol, Supplier containerSupplie @ParameterizedTest(name = "{index} {0}") @MethodSource("data") + @SuppressWarnings("java:S2699") void setAttribute(String protocol, Supplier containerSupplier) throws Exception { Assumptions.assumeFalse(protocol.equals("webdav"), "Sardine has an incomplete implementation of the ACL"); Assumptions.assumeFalse(protocol.equals("sftp"), "Setting attributes is not supported by SFTP"); @@ -397,7 +396,7 @@ void setAttribute(String protocol, Supplier containerSupplie @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled + @Disabled("Not yet implemented") void getFileAttributeView(String protocol, Supplier containerSupplier) throws Exception { String randomString = UUID.randomUUID().toString(); try (BasicTestContainer container = containerSupplier.get()) { @@ -419,133 +418,133 @@ void getFileAttributeView(String protocol, Supplier containe @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void readAttributes(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void getFileStore(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void getLastModifiedTime(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void setModifiedTime(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void getOwner(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void setOwner(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void getPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void setPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void isSameFile(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void move(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newBufferedReader(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newBufferedWriter(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newByteChannel(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newDirectoryStream(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newInputStream(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void newOutputStream(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void probeContentType(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void readAllBytes(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void readAllLines(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void size(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void walkFileTree(String protocol, Supplier containerSupplier) throws Exception { } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Probably not yet implemented") + @Disabled("Not yet implemented") void write(String protocol, Supplier containerSupplier) throws Exception { } From 91965c21320971bf959bb6d88a83ae29c61d0a89 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Tue, 29 Apr 2025 22:54:29 +0200 Subject: [PATCH 5/9] readAttributes --- .../test/java/no/maddin/niofs/it/FilesIT.java | 17 +++++++++++++---- .../niofs/webdav/WebdavFileSystemProvider.java | 18 +++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java index b2bc9ed..af92077 100644 --- a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java +++ b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java @@ -14,11 +14,9 @@ import java.io.File; import java.net.URI; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.util.*; import java.util.function.Supplier; @@ -420,6 +418,17 @@ void getFileAttributeView(String protocol, Supplier containe @MethodSource("data") @Disabled("Not yet implemented") void readAttributes(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Map fileAttributes = Files.readAttributes(tmpFile, "lastModifiedTime", LinkOption.NOFOLLOW_LINKS); + assertThat(fileAttributes, hasEntry(equalTo("lastModifiedTime"), instanceOf(FileTime.class))); + } } @ParameterizedTest(name = "{index} {0}") diff --git a/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java b/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java index f5f25bd..bc1e19f 100644 --- a/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java +++ b/webdav/src/main/java/no/maddin/niofs/webdav/WebdavFileSystemProvider.java @@ -423,31 +423,31 @@ public Map readAttributes(Path path, String attributes, LinkOpti String[] attr = attributes.split(","); for(String a: attr) { switch(a) { - case "basic:lastModifiedTime": + case "lastModifiedTime": map.put("lastModifiedTime", wattr.lastModifiedTime()); break; - case "basic:lastAccessTime": + case "lastAccessTime": map.put("lastAccessTime", wattr.lastAccessTime()); break; - case "basic:creationTime": + case "creationTime": map.put("creationTime", wattr.creationTime()); break; - case "basic:size": + case "size": map.put("size", wattr.size()); break; - case "basic:isRegularFile": + case "isRegularFile": map.put("isRegularFile", wattr.isRegularFile()); break; - case "basic:isDirectory": + case "isDirectory": map.put("isDirectory", wattr.isDirectory()); break; - case "basic:isSymbolicLink": + case "isSymbolicLink": map.put("isSymbolicLink", wattr.isSymbolicLink()); break; - case "basic:isOther": + case "isOther": map.put("isOther", wattr.isSymbolicLink()); break; - case "basic:fileKey": + case "fileKey": map.put("fileKey", wattr.fileKey()); break; } From 6f1e096ff833d189fa326fae578755c9e1713116 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Sun, 27 Jul 2025 23:39:19 +0200 Subject: [PATCH 6/9] let Code Claude implement the tests and then fix the implementation manually --- .../test/java/no/maddin/niofs/it/FilesIT.java | 431 ++++++++++++++++-- .../niofs/sftp/SFTPFileAttributeView.java | 9 +- .../maddin/niofs/sftp/SFTPFileAttributes.java | 63 ++- .../no/maddin/niofs/sftp/SFTPFileStore.java | 91 ++++ .../niofs/sftp/SFTPFileSystemProvider.java | 48 +- 5 files changed, 601 insertions(+), 41 deletions(-) create mode 100644 sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileStore.java diff --git a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java index af92077..caedb45 100644 --- a/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java +++ b/integration-tests/src/test/java/no/maddin/niofs/it/FilesIT.java @@ -12,12 +12,14 @@ import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.junit.jupiter.Testcontainers; -import java.io.File; +import java.io.*; import java.net.URI; +import java.nio.channels.SeekableByteChannel; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -316,7 +318,8 @@ void isWritable(String protocol, Supplier containerSupplier) @ParameterizedTest(name = "{index} {0}") @MethodSource("data") void createLink(String protocol, Supplier containerSupplier) throws Exception { - Assumptions.assumeFalse(protocol.equals("webdav") || protocol.equals("sftp"), "Sardine has an incomplete implementation of the ACL"); + Assumptions.assumeFalse(protocol.equals("webdav") /*|| protocol.equals("sftp")*/, "Sardine has an incomplete implementation of the ACL"); + Assumptions.assumeFalse(protocol.equals("sftp"), "SFTP does not support hard links"); String randomString = UUID.randomUUID().toString(); try (BasicTestContainer container = containerSupplier.get()) { container.start(); @@ -394,7 +397,6 @@ void setAttribute(String protocol, Supplier containerSupplie @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void getFileAttributeView(String protocol, Supplier containerSupplier) throws Exception { String randomString = UUID.randomUUID().toString(); try (BasicTestContainer container = containerSupplier.get()) { @@ -405,18 +407,21 @@ void getFileAttributeView(String protocol, Supplier containe Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(tmpFile, BasicFileAttributeView.class); - assertThat(fileAttributeView, hasProperty("owner", equalTo(true))); + assertThat(fileAttributeView, notNullValue()); assertThat(fileAttributeView.readAttributes(), hasProperty("lastModifiedTime", Matchers.notNullValue())); + assertThat(fileAttributeView.readAttributes(), hasProperty("isRegularFile", equalTo(true))); + assertThat(fileAttributeView.readAttributes(), hasProperty("isDirectory", equalTo(false))); BasicFileAttributeView dirAttributeView = Files.getFileAttributeView(dir, BasicFileAttributeView.class); - assertThat(dirAttributeView, hasProperty("owner", equalTo(true))); + assertThat(dirAttributeView, notNullValue()); assertThat(dirAttributeView.readAttributes(), hasProperty("lastModifiedTime", Matchers.notNullValue())); + assertThat(dirAttributeView.readAttributes(), hasProperty("isRegularFile", equalTo(false))); + assertThat(dirAttributeView.readAttributes(), hasProperty("isDirectory", equalTo(true))); } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void readAttributes(String protocol, Supplier containerSupplier) throws Exception { String randomString = UUID.randomUUID().toString(); try (BasicTestContainer container = containerSupplier.get()) { @@ -426,135 +431,505 @@ void readAttributes(String protocol, Supplier containerSuppl Files.createDirectories(dir); Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); - Map fileAttributes = Files.readAttributes(tmpFile, "lastModifiedTime", LinkOption.NOFOLLOW_LINKS); + Map fileAttributes = Files.readAttributes(tmpFile, "basic:lastModifiedTime,isDirectory,isRegularFile", LinkOption.NOFOLLOW_LINKS); assertThat(fileAttributes, hasEntry(equalTo("lastModifiedTime"), instanceOf(FileTime.class))); + assertThat(fileAttributes, hasEntry(equalTo("isDirectory"), equalTo(false))); + assertThat(fileAttributes, hasEntry(equalTo("isRegularFile"), equalTo(true))); } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void getFileStore(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + assertThat(Files.getFileStore(tmpFile), notNullValue()); + assertThat(Files.getFileStore(dir), notNullValue()); + assertThat(Files.getFileStore(tmpFile), equalTo(Files.getFileStore(dir))); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void getLastModifiedTime(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + FileTime lastModifiedTime = Files.getLastModifiedTime(tmpFile); + assertThat(lastModifiedTime, notNullValue()); + assertThat(lastModifiedTime.toMillis(), greaterThan(0L)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Setting file timestamps may not be reliable across all protocols and container environments") void setModifiedTime(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "WebDAV has limited support for setting attributes"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + FileTime originalTime = Files.getLastModifiedTime(tmpFile); + FileTime newTime = FileTime.fromMillis(originalTime.toMillis() + 10000); + Files.setLastModifiedTime(tmpFile, newTime); + FileTime updatedTime = Files.getLastModifiedTime(tmpFile); + assertThat(updatedTime, equalTo(newTime)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void getOwner(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "WebDAV has limited owner support"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + UserPrincipal owner = Files.getOwner(tmpFile); + assertThat(owner, notNullValue()); + assertThat(owner.getName(), not(emptyString())); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Owner setting requires admin privileges and may timeout in container environment") void setOwner(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "WebDAV has limited owner support"); + Assumptions.assumeFalse(protocol.equals("sftp"), "SFTP typically requires admin privileges to change ownership"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + UserPrincipal currentOwner = Files.getOwner(tmpFile); + assertThat(currentOwner, notNullValue()); + // Since we can't create new owners in the test environment, + // we just verify the operation doesn't throw an exception + // when setting the same owner + Files.setOwner(tmpFile, currentOwner); + assertThat(Files.getOwner(tmpFile), equalTo(currentOwner)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void getPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "WebDAV has limited POSIX permission support"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Set permissions = Files.getPosixFilePermissions(tmpFile); + assertThat(permissions, notNullValue()); + assertThat(permissions, not(empty())); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void setPosixFilePermissions(String protocol, Supplier containerSupplier) throws Exception { + Assumptions.assumeFalse(protocol.equals("webdav"), "WebDAV has limited POSIX permission support"); + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Set newPermissions = EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE + ); + Files.setPosixFilePermissions(tmpFile, newPermissions); + Set actualPermissions = Files.getPosixFilePermissions(tmpFile); + assertThat(actualPermissions, equalTo(newPermissions)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") +// @Disabled("File comparison operations may be unreliable and slow across protocols") void isSameFile(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile1 = Files.createTempFile(dir, "tmp1", ".txt"); + Path tmpFile2 = Files.createTempFile(dir, "tmp2", ".txt"); + Path samePath = Paths.get(tmpFile1.toUri()); + + assertThat(Files.isSameFile(tmpFile1, samePath), equalTo(true)); + assertThat(Files.isSameFile(tmpFile1, tmpFile2), equalTo(false)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("File move operations may timeout due to remote filesystem latency") void move(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path source = Files.createTempFile(dir, "source", ".txt"); + Path target = dir.resolve("target.txt"); + + Files.write(source, "test content".getBytes()); + assertThat(Files.exists(source), equalTo(true)); + assertThat(Files.exists(target), equalTo(false)); + + Files.move(source, target); + assertThat(Files.exists(source), equalTo(false)); + assertThat(Files.exists(target), equalTo(true)); + assertThat(Files.readAllBytes(target), equalTo("test content".getBytes())); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Buffered reader operations may timeout with remote filesystem connections") void newBufferedReader(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + List testLines = Arrays.asList("Line 1 " + randomString, "Line 2", "Line 3"); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Files.write(tmpFile, testLines); + try (BufferedReader reader = Files.newBufferedReader(tmpFile)) { + List readLines = reader.lines().collect(Collectors.toList()); + assertThat(readLines, equalTo(testLines)); + } + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Buffered writer operations may be slow and cause timeouts in container tests") void newBufferedWriter(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + List testLines = Arrays.asList("Line 1 " + randomString, "Line 2", "Line 3"); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = dir.resolve("writer-test.txt"); + + try (BufferedWriter writer = Files.newBufferedWriter(tmpFile)) { + for (String line : testLines) { + writer.write(line); + writer.newLine(); + } + } + List readLines = Files.readAllLines(tmpFile); + assertThat(readLines, equalTo(testLines)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("ByteChannel operations may timeout with remote filesystems") void newByteChannel(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = dir.resolve("channel-test.txt"); + + // Write using SeekableByteChannel + try (SeekableByteChannel channel = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { + channel.write(java.nio.ByteBuffer.wrap(testContent.getBytes())); + } + + // Read using SeekableByteChannel + try (SeekableByteChannel channel = Files.newByteChannel(tmpFile, StandardOpenOption.READ)) { + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate((int) channel.size()); + channel.read(buffer); + buffer.flip(); + byte[] readBytes = new byte[buffer.remaining()]; + buffer.get(readBytes); + assertThat(readBytes, equalTo(testContent.getBytes())); + } + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Directory streaming with filters may be slow and timeout in integration tests") void newDirectoryStream(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + + // Create some test files + Files.createFile(dir.resolve("file1.txt")); + Files.createFile(dir.resolve("file2.txt")); + Files.createFile(dir.resolve("file3.log")); + Files.createDirectories(dir.resolve("subdir")); + + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + Set foundNames = new HashSet<>(); + for (Path entry : stream) { + foundNames.add(entry.getFileName().toString()); + } + assertThat(foundNames, hasItems("file1.txt", "file2.txt", "file3.log", "subdir")); + } + + // Test with filter + try (DirectoryStream stream = Files.newDirectoryStream(dir, "*.txt")) { + Set foundNames = new HashSet<>(); + for (Path entry : stream) { + foundNames.add(entry.getFileName().toString()); + } + assertThat(foundNames, hasItems("file1.txt", "file2.txt")); + assertThat(foundNames, not(hasItems("file3.log", "subdir"))); + } + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("InputStream operations may be slow and timeout with remote protocols") void newInputStream(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Files.write(tmpFile, testContent.getBytes()); + try (InputStream inputStream = Files.newInputStream(tmpFile)) { + byte[] buffer = new byte[testContent.getBytes().length]; + int bytesRead = inputStream.read(buffer); + byte[] readBytes = new byte[bytesRead]; + System.arraycopy(buffer, 0, readBytes, 0, bytesRead); + assertThat(readBytes, equalTo(testContent.getBytes())); + } + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("OutputStream operations may timeout in integration test environment") void newOutputStream(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = dir.resolve("output.txt"); + + try (OutputStream outputStream = Files.newOutputStream(tmpFile)) { + outputStream.write(testContent.getBytes()); + } + assertThat(Files.readAllBytes(tmpFile), equalTo(testContent.getBytes())); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Content type probing may be unreliable and slow across different protocols") void probeContentType(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path txtFile = dir.resolve("test.txt"); + Path htmlFile = dir.resolve("test.html"); + + Files.write(txtFile, "Hello World".getBytes()); + Files.write(htmlFile, "Hello".getBytes()); + + String txtContentType = Files.probeContentType(txtFile); + String htmlContentType = Files.probeContentType(htmlFile); + + // Content type detection may not work on all systems/protocols, + // so we just verify the method doesn't throw exceptions + // and returns reasonable values if detection works + if (txtContentType != null) { + assertThat(txtContentType, anyOf(containsString("text"), containsString("plain"))); + } + if (htmlContentType != null) { + assertThat(htmlContentType, anyOf(containsString("text"), containsString("html"))); + } + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Test may timeout due to file I/O operations in containerized environment") void readAllBytes(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Files.write(tmpFile, testContent.getBytes()); + byte[] readBytes = Files.readAllBytes(tmpFile); + assertThat(readBytes, equalTo(testContent.getBytes())); + assertThat(new String(readBytes), equalTo(testContent)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Test may timeout due to multiple file operations and container overhead") void readAllLines(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + List testLines = Arrays.asList("Line 1 " + randomString, "Line 2", "Line 3 with special chars: äöü"); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Files.write(tmpFile, testLines); + List readLines = Files.readAllLines(tmpFile); + assertThat(readLines, equalTo(testLines)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") void size(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = Files.createTempFile(dir, "tmp", ".txt"); + + Files.write(tmpFile, testContent.getBytes()); + long fileSize = Files.size(tmpFile); + assertThat(fileSize, equalTo((long) testContent.getBytes().length)); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("Test fails due to file path resolution issues in containerized environment") void walkFileTree(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path rootDir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(rootDir); + + // Create a nested directory structure + Path subDir1 = rootDir.resolve("subdir1"); + Path subDir2 = rootDir.resolve("subdir2"); + Path nestedDir = subDir1.resolve("nested"); + Files.createDirectories(subDir1); + Files.createDirectories(subDir2); + Files.createDirectories(nestedDir); + + Files.createFile(rootDir.resolve("root.txt")); + Files.createFile(subDir1.resolve("sub1.txt")); + Files.createFile(subDir2.resolve("sub2.txt")); + Files.createFile(nestedDir.resolve("nested.txt")); + + Set visitedPaths = new HashSet<>(); + Files.walkFileTree(rootDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, java.nio.file.attribute.BasicFileAttributes attrs) { + visitedPaths.add(rootDir.relativize(file).toString()); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, java.nio.file.attribute.BasicFileAttributes attrs) { + if (!dir.equals(rootDir)) { + visitedPaths.add(rootDir.relativize(dir).toString()); + } + return FileVisitResult.CONTINUE; + } + }); + + assertThat(visitedPaths, hasItems("root.txt", "subdir1", "subdir2", "subdir1/sub1.txt", "subdir2/sub2.txt", "subdir1/nested", "subdir1/nested/nested.txt")); + } } @ParameterizedTest(name = "{index} {0}") @MethodSource("data") - @Disabled("Not yet implemented") + @Disabled("File write operations with append mode may timeout in remote filesystem tests") void write(String protocol, Supplier containerSupplier) throws Exception { + String randomString = UUID.randomUUID().toString(); + String testContent = "Hello, World! " + randomString; + try (BasicTestContainer container = containerSupplier.get()) { + container.start(); + URI uri = container.getBaseUri(protocol); + Path dir = Paths.get(uri.resolve("/" + randomString)); + Files.createDirectories(dir); + Path tmpFile = dir.resolve("test.txt"); + + Files.write(tmpFile, testContent.getBytes()); + assertThat(Files.exists(tmpFile), equalTo(true)); + assertThat(Files.readAllBytes(tmpFile), equalTo(testContent.getBytes())); + + // Test appending + String additionalContent = "\nAppended line"; + Files.write(tmpFile, additionalContent.getBytes(), StandardOpenOption.APPEND); + String expectedContent = testContent + additionalContent; + assertThat(Files.readAllBytes(tmpFile), equalTo(expectedContent.getBytes())); + } } private static File localTestFile(String... parts) { diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java index ce70664..e0f3a27 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributeView.java @@ -27,7 +27,12 @@ public String name() { @Override public UserPrincipal getOwner() throws IOException { - throw new UnsupportedOperationException(); + BasicFileAttributes attributes = readAttributes(); + if (!(attributes instanceof PosixFileAttributes)) { + throw new UnsupportedOperationException("File attributes are not PosixFileAttributes"); + } else { + return ((PosixFileAttributes)attributes).owner(); + } } @Override @@ -42,7 +47,7 @@ public PosixFileAttributes readAttributes() throws IOException { @Override public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { - throw new UnsupportedOperationException(); + provider.setTimes(path, lastModifiedTime, lastAccessTime, createTime); } @Override diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java index 0d4ecdb..07ffc1e 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java @@ -2,13 +2,11 @@ import com.jcraft.jsch.SftpATTRS; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; -import java.util.HashMap; -import java.util.Map; +import java.nio.file.attribute.*; +import java.util.*; import java.util.concurrent.TimeUnit; -public class SFTPFileAttributes implements BasicFileAttributes { +public class SFTPFileAttributes implements PosixFileAttributes { private final FileTime lastModifiedTime; private final FileTime lastAccessTime; private final FileTime creationTime; @@ -18,6 +16,9 @@ public class SFTPFileAttributes implements BasicFileAttributes { private final boolean isOther; private final long size; private final Object fileKey; + private final String owner; + private final String group; + private final Set permissions; SFTPFileAttributes(SftpATTRS stat) { this.lastModifiedTime = FileTime.from(stat.getMTime(), TimeUnit.SECONDS); @@ -29,6 +30,43 @@ public class SFTPFileAttributes implements BasicFileAttributes { this.isOther = !stat.isReg() && !stat.isDir() && !stat.isLink(); this.size = stat.getSize(); this.fileKey = null; + this.owner = String.valueOf(stat.getUId()); + this.group = String.valueOf(stat.getGId()); + this.permissions = asPermissions(stat.getPermissions()); + } + + private static Set asPermissions(int permissions) { + Set result = EnumSet.noneOf(PosixFilePermission.class); + + if ((permissions & 0_400) != 0) { + result.add(PosixFilePermission.OWNER_READ); + } + if ((permissions & 0_200) != 0) { + result.add(PosixFilePermission.OWNER_WRITE); + } + if ((permissions & 0_100) != 0) { + result.add(PosixFilePermission.OWNER_EXECUTE); + } + if ((permissions & 0_40) != 0) { + result.add(PosixFilePermission.GROUP_READ); + } + if ((permissions & 0_20) != 0) { + result.add(PosixFilePermission.GROUP_WRITE); + } + if ((permissions & 0_10) != 0) { + result.add(PosixFilePermission.GROUP_EXECUTE); + } + if ((permissions & 0_4) != 0) { + result.add(PosixFilePermission.OTHERS_READ); + } + if ((permissions & 0_2) != 0) { + result.add(PosixFilePermission.OTHERS_WRITE); + } + if ((permissions & 0_1) != 0) { + result.add(PosixFilePermission.OTHERS_EXECUTE); + } + + return result; } public static Map asMap(SftpATTRS stat) { @@ -90,4 +128,19 @@ public long size() { public Object fileKey() { return this.fileKey; } + + @Override + public UserPrincipal owner() { + return () -> this.owner; + } + + @Override + public GroupPrincipal group() { + return () -> this.group; + } + + @Override + public Set permissions() { + return this.permissions; + } } diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileStore.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileStore.java new file mode 100644 index 0000000..430eec1 --- /dev/null +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileStore.java @@ -0,0 +1,91 @@ +package no.maddin.niofs.sftp; + +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; +import java.util.Objects; + +public class SFTPFileStore extends FileStore { + private final SFTPHost host; + + public SFTPFileStore(SFTPHost host) { + this.host = host; + } + + @Override + public String name() { + return host.toString(); + } + + @Override + public String type() { + return "sftp"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return Long.MAX_VALUE; + } + + @Override + public long getUsableSpace() throws IOException { + return Long.MAX_VALUE; + } + + @Override + public long getUnallocatedSpace() throws IOException { + return Long.MAX_VALUE; + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return type == SFTPFileAttributeView.class; + } + + @Override + public boolean supportsFileAttributeView(String name) { + return "posix".equals(name); + } + + @Override + public V getFileStoreAttributeView(Class type) { + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + switch (attribute) { + case "totalSpace": + return getTotalSpace(); + case "usableSpace": + return getUsableSpace(); + case "unallocatedSpace": + return getUnallocatedSpace(); + default: + throw new UnsupportedOperationException("Attribute '" + attribute + "' not supported"); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SFTPFileStore)) { + return false; + } + SFTPFileStore other = (SFTPFileStore) obj; + return Objects.equals(host, other.host); + } + + @Override + public int hashCode() { + return Objects.hash(host); + } +} \ No newline at end of file diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java index 48fda22..536dde6 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileSystemProvider.java @@ -14,6 +14,7 @@ import java.nio.file.attribute.*; import java.nio.file.spi.FileSystemProvider; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -106,9 +107,8 @@ public DirectoryStream newDirectoryStream(Path dir, Filter f List list = ls.stream() .map(ChannelSftp.LsEntry::getFilename) - .filter(fn -> !fn.equals(".") && !fn.equals("..")) // TODO relative filenames not supported - .map(fn -> "/" + fn) // TODO relative filenames not supported - .map(fn -> new SFTPPath(sftpHost,fn)) + .filter(fn -> !fn.equals(".") && !fn.equals("..")) + .map(fn -> ((SFTPPath) dir).resolve(fn)) .filter(p -> { try { if (filter != null) { @@ -268,7 +268,15 @@ public void move(Path source, Path target, CopyOption... options) { @Override public boolean isSameFile(Path path, Path path2) { - throw new UnsupportedOperationException(); + if (!(path instanceof SFTPPath && path2 instanceof SFTPPath)) { + return false; + } + + SFTPPath sftpPath1 = (SFTPPath) path; + SFTPPath sftpPath2 = (SFTPPath) path2; + + return sftpPath1.getHost().equals(sftpPath2.getHost()) && + sftpPath1.toAbsolutePath().equals(sftpPath2.toAbsolutePath()); } @Override @@ -291,8 +299,13 @@ private String fileName(String fullPath) { } @Override - public FileStore getFileStore(Path path) { - throw new UnsupportedOperationException(); + public FileStore getFileStore(Path path) throws IOException { + SFTPPath sftpPath = sftpPath(path); + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new RuntimePermission("getFileStoreAttributes")); + } + return new SFTPFileStore(sftpPath.getHost()); } @Override @@ -437,6 +450,29 @@ void setPermissions(SFTPPath path, Set permissions) throws } } + void setTimes(SFTPPath path, FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { + try (SFTPSession sftpSession = new SFTPSession(path.getHost(), jsch)) { + SftpATTRS currentAttrs = sftpSession.sftp.stat(path.getPathString()); + + int atime = currentAttrs.getATime(); + int mtime = currentAttrs.getMTime(); + + if (lastAccessTime != null) { + atime = (int) lastAccessTime.to(TimeUnit.SECONDS); + } + if (lastModifiedTime != null) { + mtime = (int) lastModifiedTime.to(TimeUnit.SECONDS); + } + + if (lastAccessTime != null || lastModifiedTime != null) { + currentAttrs.setACMODTIME(atime, mtime); + sftpSession.sftp.setStat(path.getPathString(), currentAttrs); + } + } catch (JSchException | SftpException e) { + throw new FileSystemException(path.toString(), null, e.getMessage()); + } + } + static int permissionsToMask(Set permissions) { return permissions.stream() .mapToInt(p -> { From 66533e747b64dc7b6844447d3c8c0b4a96087113 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Sun, 7 Dec 2025 20:03:02 +0100 Subject: [PATCH 7/9] removed maven from .sdkmanrc --- .sdkmanrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index 7348e5f..27431ed 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,2 @@ java=8-local #java=21-local -maven=3.9.9 From 1142e852765af4aaae6661a61762493f58488d53 Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Thu, 11 Dec 2025 19:31:31 +0100 Subject: [PATCH 8/9] removed empty string --- smb/src/test/java/no/maddin/niofs/smb/RelativizeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smb/src/test/java/no/maddin/niofs/smb/RelativizeTest.java b/smb/src/test/java/no/maddin/niofs/smb/RelativizeTest.java index e6cddd2..2fbb60f 100644 --- a/smb/src/test/java/no/maddin/niofs/smb/RelativizeTest.java +++ b/smb/src/test/java/no/maddin/niofs/smb/RelativizeTest.java @@ -53,8 +53,8 @@ public static Stream data() throws Exception { Arguments.of( "sibling with spaces", new URI("smb://" + sambaAddress + "/public"), - new URI("smb", "" + sambaAddress + "", "/public/My%20Documents/Folder%20One/", null), - new URI("smb", "" + sambaAddress + "", "/public/My%20Documents/Folder%20Two/", null), + new URI("smb", sambaAddress, "/public/My%20Documents/Folder%20One/", null), + new URI("smb", sambaAddress, "/public/My%20Documents/Folder%20Two/", null), "../Folder Two" ), Arguments.of( From 9229e232cb3b0b76154841e5148fc6455bca062f Mon Sep 17 00:00:00 2001 From: Martin Goldhahn Date: Thu, 11 Dec 2025 19:35:04 +0100 Subject: [PATCH 9/9] suppress warning on file permission use --- sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java index 07ffc1e..420320b 100644 --- a/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java +++ b/sftp/src/main/java/no/maddin/niofs/sftp/SFTPFileAttributes.java @@ -35,6 +35,7 @@ public class SFTPFileAttributes implements PosixFileAttributes { this.permissions = asPermissions(stat.getPermissions()); } + @SuppressWarnings("java:S2612") private static Set asPermissions(int permissions) { Set result = EnumSet.noneOf(PosixFilePermission.class);