diff --git a/files-api/src/main/java/module-info.java b/files-api/src/main/java/module-info.java index b41f910..1d0afc8 100644 --- a/files-api/src/main/java/module-info.java +++ b/files-api/src/main/java/module-info.java @@ -1,8 +1,8 @@ -import org.xbib.files.DefaultFilesProvider; -import org.xbib.files.Provider; +import org.xbib.files.DefaultFileServiceProvider; +import org.xbib.files.FileServiceProvider; module org.xbib.files { exports org.xbib.files; - uses Provider; - provides Provider with DefaultFilesProvider; + uses FileServiceProvider; + provides FileServiceProvider with DefaultFileServiceProvider; } diff --git a/files-api/src/main/java/org/xbib/files/DefaultFiles.java b/files-api/src/main/java/org/xbib/files/DefaultFileService.java similarity index 94% rename from files-api/src/main/java/org/xbib/files/DefaultFiles.java rename to files-api/src/main/java/org/xbib/files/DefaultFileService.java index 1f3f157..4d55a74 100644 --- a/files-api/src/main/java/org/xbib/files/DefaultFiles.java +++ b/files-api/src/main/java/org/xbib/files/DefaultFileService.java @@ -10,6 +10,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; @@ -26,8 +27,9 @@ import java.util.Map; import java.util.Set; import java.nio.file.Files; +import java.util.stream.Stream; -public class DefaultFiles implements org.xbib.files.Files { +public class DefaultFileService implements FileService { private static final int READ_BUFFER_SIZE = 128 * 1024; @@ -39,13 +41,7 @@ public class DefaultFiles implements org.xbib.files.Files { private static final Set DEFAULT_FILE_PERMISSIONS = PosixFilePermissions.fromString("rw-r--r--"); - private final URI uri; - - private final Map env; - - public DefaultFiles(URI uri, Map env) { - this.uri = uri; - this.env = env; + public DefaultFileService(URI uri, Map env) { } @Override @@ -209,10 +205,25 @@ public class DefaultFiles implements org.xbib.files.Files { } @Override - public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { + public DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException { return Files.newDirectoryStream(Paths.get(path), filter); } + @Override + public Stream list(String path) throws IOException { + return Files.list(Paths.get(path)); + } + + @Override + public Stream walk(String path, FileVisitOption... options) throws IOException { + return Files.walk(Paths.get(path), options); + } + + @Override + public Stream walk(String path, int maxdepth, FileVisitOption... options) throws IOException { + return Files.walk(Paths.get(path), maxdepth, options); + } + private void upload(Path source, Path target, Set dirPerms, Set filePerms, diff --git a/files-api/src/main/java/org/xbib/files/DefaultFileServiceProvider.java b/files-api/src/main/java/org/xbib/files/DefaultFileServiceProvider.java new file mode 100644 index 0000000..47da2e8 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/DefaultFileServiceProvider.java @@ -0,0 +1,11 @@ +package org.xbib.files; + +import java.net.URI; +import java.util.Map; + +public class DefaultFileServiceProvider implements FileServiceProvider { + @Override + public FileService provide(URI uri, Map env) { + return !uri.isAbsolute() || uri.getScheme().equals("file") ? new DefaultFileService(uri, env) : null; + } +} diff --git a/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java b/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java deleted file mode 100644 index 13f97bd..0000000 --- a/files-api/src/main/java/org/xbib/files/DefaultFilesProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.xbib.files; - -import java.net.URI; -import java.util.Map; - -public class DefaultFilesProvider implements Provider { - @Override - public Files provide(URI uri, Map env) { - return !uri.isAbsolute() || uri.getScheme().equals("file") ? new DefaultFiles(uri, env) : null; - } -} diff --git a/files-api/src/main/java/org/xbib/files/Files.java b/files-api/src/main/java/org/xbib/files/FileService.java similarity index 79% rename from files-api/src/main/java/org/xbib/files/Files.java rename to files-api/src/main/java/org/xbib/files/FileService.java index b10afff..6ef7dfa 100644 --- a/files-api/src/main/java/org/xbib/files/Files.java +++ b/files-api/src/main/java/org/xbib/files/FileService.java @@ -6,6 +6,8 @@ import java.io.OutputStream; import java.net.URI; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; @@ -16,14 +18,15 @@ import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; +import java.util.stream.Stream; -public interface Files { +public interface FileService { class Holder { - private static Files createFiles(URI uri, Map env) { - ServiceLoader serviceLoader = ServiceLoader.load(Provider.class); - Optional first = serviceLoader.stream() + private static FileService createFiles(URI uri, Map env) { + ServiceLoader serviceLoader = ServiceLoader.load(FileServiceProvider.class); + Optional first = serviceLoader.stream() .map(ServiceLoader.Provider::get) .filter(Objects::nonNull) .map(p -> p.provide(uri, env)) @@ -33,7 +36,11 @@ public interface Files { } } - default Files newInstance(URI uri, Map env) { + static FileService newInstance(String uri, Map env) { + return Holder.createFiles(URI.create(uri), env); + } + + static FileService newInstance(URI uri, Map env) { return Holder.createFiles(uri, env); } @@ -101,5 +108,11 @@ public interface Files { DirectoryStream stream(String path, String glob) throws IOException; - DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException; + DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException; + + Stream list(String path) throws IOException; + + Stream walk(String path, FileVisitOption... options) throws IOException; + + Stream walk(String path, int maxdepth, FileVisitOption... options) throws IOException; } diff --git a/files-api/src/main/java/org/xbib/files/FileServiceProvider.java b/files-api/src/main/java/org/xbib/files/FileServiceProvider.java new file mode 100644 index 0000000..2db7b4e --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/FileServiceProvider.java @@ -0,0 +1,9 @@ +package org.xbib.files; + +import java.net.URI; +import java.util.Map; + +public interface FileServiceProvider { + + FileService provide(URI uri, Map env); +} diff --git a/files-api/src/main/java/org/xbib/files/FileTreeIterator.java b/files-api/src/main/java/org/xbib/files/FileTreeIterator.java new file mode 100644 index 0000000..2691a96 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/FileTreeIterator.java @@ -0,0 +1,102 @@ +package org.xbib.files; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import org.xbib.files.FileTreeWalker.Event; + +/** + * This source is a copy taken from java.nio.file.FileTreeIterator which is inaccessible. + * + * An {@code Iterator} to iterate over the nodes of a file tree. + * + *
{@code
+ *     try (FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options)) {
+ *         while (iterator.hasNext()) {
+ *             Event ev = iterator.next();
+ *             Path path = ev.file();
+ *             BasicFileAttributes attrs = ev.attributes();
+ *         }
+ *     }
+ * }
+ */ + +class FileTreeIterator implements Iterator, Closeable { + private final FileTreeWalker walker; + private Event next; + + /** + * Creates a new iterator to walk the file tree starting at the given file. + * + * @throws IllegalArgumentException + * if {@code maxDepth} is negative + * @throws IOException + * if an I/O errors occurs opening the starting file + * @throws SecurityException + * if the security manager denies access to the starting file + * @throws NullPointerException + * if {@code start} or {@code options} is {@code null} or + * the options array contains a {@code null} element + */ + FileTreeIterator(Path start, int maxDepth, FileVisitOption... options) + throws IOException + { + this.walker = new FileTreeWalker(Arrays.asList(options), maxDepth); + this.next = walker.walk(start); + assert next.type() == FileTreeWalker.EventType.ENTRY || + next.type() == FileTreeWalker.EventType.START_DIRECTORY; + + // IOException if there a problem accessing the starting file + IOException ioe = next.ioeException(); + if (ioe != null) + throw ioe; + } + + private void fetchNextIfNeeded() { + if (next == null) { + FileTreeWalker.Event ev = walker.next(); + while (ev != null) { + IOException ioe = ev.ioeException(); + if (ioe != null) + throw new UncheckedIOException(ioe); + + // END_DIRECTORY events are ignored + if (ev.type() != FileTreeWalker.EventType.END_DIRECTORY) { + next = ev; + return; + } + ev = walker.next(); + } + } + } + + @Override + public boolean hasNext() { + if (!walker.isOpen()) + throw new IllegalStateException(); + fetchNextIfNeeded(); + return next != null; + } + + @Override + public Event next() { + if (!walker.isOpen()) + throw new IllegalStateException(); + fetchNextIfNeeded(); + if (next == null) + throw new NoSuchElementException(); + Event result = next; + next = null; + return result; + } + + @Override + public void close() { + walker.close(); + } +} diff --git a/files-api/src/main/java/org/xbib/files/FileTreeWalker.java b/files-api/src/main/java/org/xbib/files/FileTreeWalker.java new file mode 100644 index 0000000..02844e6 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/FileTreeWalker.java @@ -0,0 +1,398 @@ +package org.xbib.files; + +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystemLoopException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Iterator; + +/** + * This source is a copy taken from java.nio.file.FileTreeWalker which is inaccessible. + * + * Walks a file tree, generating a sequence of events corresponding to the files + * in the tree. + * + *
{@code
+ *     Path top = ...
+ *     Set options = ...
+ *     int maxDepth = ...
+ *
+ *     try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
+ *         FileTreeWalker.Event ev = walker.walk(top);
+ *         do {
+ *             process(ev);
+ *             ev = walker.next();
+ *         } while (ev != null);
+ *     }
+ * }
+ * + * @see Files#walkFileTree + */ + +class FileTreeWalker implements Closeable { + private final boolean followLinks; + private final LinkOption[] linkOptions; + private final int maxDepth; + private final ArrayDeque stack = new ArrayDeque<>(); + private boolean closed; + + /** + * The element on the walking stack corresponding to a directory node. + */ + private static class DirectoryNode { + private final Path dir; + private final Object key; + private final DirectoryStream stream; + private final Iterator iterator; + private boolean skipped; + + DirectoryNode(Path dir, Object key, DirectoryStream stream) { + this.dir = dir; + this.key = key; + this.stream = stream; + this.iterator = stream.iterator(); + } + + Path directory() { + return dir; + } + + Object key() { + return key; + } + + DirectoryStream stream() { + return stream; + } + + Iterator iterator() { + return iterator; + } + + void skip() { + skipped = true; + } + + boolean skipped() { + return skipped; + } + } + + /** + * The event types. + */ + static enum EventType { + /** + * Start of a directory + */ + START_DIRECTORY, + /** + * End of a directory + */ + END_DIRECTORY, + /** + * An entry in a directory + */ + ENTRY; + } + + /** + * Events returned by the {@link #walk} and {@link #next} methods. + */ + static class Event { + private final EventType type; + private final Path file; + private final BasicFileAttributes attrs; + private final IOException ioe; + + private Event(EventType type, Path file, BasicFileAttributes attrs, IOException ioe) { + this.type = type; + this.file = file; + this.attrs = attrs; + this.ioe = ioe; + } + + Event(EventType type, Path file, BasicFileAttributes attrs) { + this(type, file, attrs, null); + } + + Event(EventType type, Path file, IOException ioe) { + this(type, file, null, ioe); + } + + EventType type() { + return type; + } + + Path file() { + return file; + } + + BasicFileAttributes attributes() { + return attrs; + } + + IOException ioeException() { + return ioe; + } + } + + /** + * Creates a {@code FileTreeWalker}. + * + * @throws IllegalArgumentException + * if {@code maxDepth} is negative + * @throws ClassCastException + * if {@code options} contains an element that is not a + * {@code FileVisitOption} + * @throws NullPointerException + * if {@code options} is {@code null} or the options + * array contains a {@code null} element + */ + FileTreeWalker(Collection options, int maxDepth) { + boolean fl = false; + for (FileVisitOption option: options) { + // will throw NPE if options contains null + switch (option) { + case FOLLOW_LINKS : fl = true; break; + default: + throw new AssertionError("Should not get here"); + } + } + if (maxDepth < 0) + throw new IllegalArgumentException("'maxDepth' is negative"); + + this.followLinks = fl; + this.linkOptions = (fl) ? new LinkOption[0] : + new LinkOption[] { LinkOption.NOFOLLOW_LINKS }; + this.maxDepth = maxDepth; + } + + /** + * Returns the attributes of the given file, taking into account whether + * the walk is following sym links is not. The {@code canUseCached} + * argument determines whether this method can use cached attributes. + */ + private BasicFileAttributes getAttributes(Path file, boolean canUseCached) + throws IOException + { + // attempt to get attributes of file. If fails and we are following + // links then a link target might not exist so get attributes of link + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions); + } catch (IOException ioe) { + if (!followLinks) + throw ioe; + + // attempt to get attrmptes without following links + attrs = Files.readAttributes(file, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } + return attrs; + } + + /** + * Returns true if walking into the given directory would result in a + * file system loop/cycle. + */ + private boolean wouldLoop(Path dir, Object key) { + // if this directory and ancestor has a file key then we compare + // them; otherwise we use less efficient isSameFile test. + for (DirectoryNode ancestor: stack) { + Object ancestorKey = ancestor.key(); + if (key != null && ancestorKey != null) { + if (key.equals(ancestorKey)) { + // cycle detected + return true; + } + } else { + try { + if (Files.isSameFile(dir, ancestor.directory())) { + // cycle detected + return true; + } + } catch (IOException | SecurityException x) { + // ignore + } + } + } + return false; + } + + /** + * Visits the given file, returning the {@code Event} corresponding to that + * visit. + * + * The {@code ignoreSecurityException} parameter determines whether + * any SecurityException should be ignored or not. If a SecurityException + * is thrown, and is ignored, then this method returns {@code null} to + * mean that there is no event corresponding to a visit to the file. + * + * The {@code canUseCached} parameter determines whether cached attributes + * for the file can be used or not. + */ + private Event visit(Path entry, boolean ignoreSecurityException, boolean canUseCached) { + // need the file attributes + BasicFileAttributes attrs; + try { + attrs = getAttributes(entry, canUseCached); + } catch (IOException ioe) { + return new Event(EventType.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; + } + + // at maximum depth or file is not a directory + int depth = stack.size(); + if (depth >= maxDepth || !attrs.isDirectory()) { + return new Event(EventType.ENTRY, entry, attrs); + } + + // check for cycles when following links + if (followLinks && wouldLoop(entry, attrs.fileKey())) { + return new Event(EventType.ENTRY, entry, + new FileSystemLoopException(entry.toString())); + } + + // file is a directory, attempt to open it + DirectoryStream stream = null; + try { + stream = Files.newDirectoryStream(entry); + } catch (IOException ioe) { + return new Event(EventType.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; + } + + // push a directory node to the stack and return an event + stack.push(new DirectoryNode(entry, attrs.fileKey(), stream)); + return new Event(EventType.START_DIRECTORY, entry, attrs); + } + + + /** + * Start walking from the given file. + */ + Event walk(Path file) { + if (closed) + throw new IllegalStateException("Closed"); + + Event ev = visit(file, + false, // ignoreSecurityException + false); // canUseCached + assert ev != null; + return ev; + } + + /** + * Returns the next Event or {@code null} if there are no more events or + * the walker is closed. + */ + Event next() { + DirectoryNode top = stack.peek(); + if (top == null) + return null; // stack is empty, we are done + + // continue iteration of the directory at the top of the stack + Event ev; + do { + Path entry = null; + IOException ioe = null; + + // get next entry in the directory + if (!top.skipped()) { + Iterator iterator = top.iterator(); + try { + if (iterator.hasNext()) { + entry = iterator.next(); + } + } catch (DirectoryIteratorException x) { + ioe = x.getCause(); + } + } + + // no next entry so close and pop directory, + // creating corresponding event + if (entry == null) { + try { + top.stream().close(); + } catch (IOException e) { + if (ioe == null) { + ioe = e; + } else { + ioe.addSuppressed(e); + } + } + stack.pop(); + return new Event(EventType.END_DIRECTORY, top.directory(), ioe); + } + + // visit the entry + ev = visit(entry, + true, // ignoreSecurityException + true); // canUseCached + + } while (ev == null); + + return ev; + } + + /** + * Pops the directory node that is the current top of the stack so that + * there are no more events for the directory (including no END_DIRECTORY) + * event. This method is a no-op if the stack is empty or the walker is + * closed. + */ + void pop() { + if (!stack.isEmpty()) { + DirectoryNode node = stack.pop(); + try { + node.stream().close(); + } catch (IOException ignore) { } + } + } + + /** + * Skips the remaining entries in the directory at the top of the stack. + * This method is a no-op if the stack is empty or the walker is closed. + */ + void skipRemainingSiblings() { + if (!stack.isEmpty()) { + stack.peek().skip(); + } + } + + /** + * Returns {@code true} if the walker is open. + */ + boolean isOpen() { + return !closed; + } + + /** + * Closes/pops all directories on the stack. + */ + @Override + public void close() { + if (!closed) { + while (!stack.isEmpty()) { + pop(); + } + closed = true; + } + } +} diff --git a/files-api/src/main/java/org/xbib/files/FileWalker.java b/files-api/src/main/java/org/xbib/files/FileWalker.java new file mode 100644 index 0000000..b94e858 --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/FileWalker.java @@ -0,0 +1,228 @@ +package org.xbib.files; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystemLoopException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * This source is a copy taken from java.nio.file.Files#walk and modified to process a java.io.Closeable after the walk. + */ +public class FileWalker { + + /** + * Return a lazily populated {@code Stream}, the elements of + * which are the entries in the directory. The listing is not recursive. + * + *

The elements of the stream are {@link Path} objects that are + * obtained as if by {@link Path#resolve(Path) resolving} the name of the + * directory entry against {@code dir}. Some file systems maintain special + * links to the directory itself and the directory's parent directory. + * Entries representing these links are not included. + * + *

The stream is weakly consistent. It is thread safe but does + * not freeze the directory while iterating, so it may (or may not) + * reflect updates to the directory that occur after returning from this + * method. + * + *

The returned stream contains a reference to an open directory. + * The directory is closed by closing the stream. + * + *

Operating on a closed stream behaves as if the end of stream + * has been reached. Due to read-ahead, one or more elements may be + * returned after the stream has been closed. + * + *

If an {@link IOException} is thrown when accessing the directory + * after this method has returned, it is wrapped in an {@link + * UncheckedIOException} which will be thrown from the method that caused + * the access to take place. + * + * @apiNote + * This method must be used within a try-with-resources statement or similar + * control structure to ensure that the stream's open directory is closed + * promptly after the stream's operations have completed. + * + * @param ds The directory stream + * + * @return The {@code Stream} describing the content of the + * directory + * + * @throws NotDirectoryException + * if the file could not otherwise be opened because it is not + * a directory (optional specific exception) + * @throws IOException + * if an I/O error occurs when opening the directory + * @throws SecurityException + * In the case of the default provider, and a security manager is + * installed, the {@link SecurityManager#checkRead(String) checkRead} + * method is invoked to check read access to the directory. + */ + public static Stream list(DirectoryStream ds) throws IOException { + try { + final Iterator delegate = ds.iterator(); + Iterator iterator = new Iterator<>() { + @Override + public boolean hasNext() { + try { + return delegate.hasNext(); + } catch (DirectoryIteratorException e) { + throw new UncheckedIOException(e.getCause()); + } + } + @Override + public Path next() { + try { + return delegate.next(); + } catch (DirectoryIteratorException e) { + throw new UncheckedIOException(e.getCause()); + } + } + }; + Spliterator spliterator = + Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT); + return StreamSupport.stream(spliterator, false) + .onClose(asUncheckedRunnable(ds)); + } catch (Error|RuntimeException e) { + try { + ds.close(); + } catch (IOException ex) { + try { + e.addSuppressed(ex); + } catch (Throwable ignore) {} + } + throw e; + } + } + + /** + * Return a {@code Stream} that is lazily populated with {@code + * Path} by walking the file tree rooted at a given starting file. The + * file tree is traversed depth-first, the elements in the stream + * are {@link Path} objects that are obtained as if by {@link + * Path#resolve(Path) resolving} the relative path against {@code start}. + * + *

The {@code stream} walks the file tree as elements are consumed. + * The {@code Stream} returned is guaranteed to have at least one + * element, the starting file itself. For each file visited, the stream + * attempts to read its {@link BasicFileAttributes}. If the file is a + * directory and can be opened successfully, entries in the directory, and + * their descendants will follow the directory in the stream as + * they are encountered. When all entries have been visited, then the + * directory is closed. The file tree walk then continues at the next + * sibling of the directory. + * + *

The stream is weakly consistent. It does not freeze the + * file tree while iterating, so it may (or may not) reflect updates to + * the file tree that occur after returned from this method. + * + *

By default, symbolic links are not automatically followed by this + * method. If the {@code options} parameter contains the {@link + * FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then symbolic links are + * followed. When following links, and the attributes of the target cannot + * be read, then this method attempts to get the {@code BasicFileAttributes} + * of the link. + * + *

If the {@code options} parameter contains the {@link + * FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then the stream keeps + * track of directories visited so that cycles can be detected. A cycle + * arises when there is an entry in a directory that is an ancestor of the + * directory. Cycle detection is done by recording the {@link + * java.nio.file.attribute.BasicFileAttributes#fileKey file-key} of directories, + * or if file keys are not available, by invoking the {@link java.nio.file.Files#isSameFile + * isSameFile} method to test if a directory is the same file as an + * ancestor. When a cycle is detected it is treated as an I/O error with + * an instance of {@link FileSystemLoopException}. + * + *

The {@code maxDepth} parameter is the maximum number of levels of + * directories to visit. A value of {@code 0} means that only the starting + * file is visited, unless denied by the security manager. A value of + * {@link Integer#MAX_VALUE MAX_VALUE} may be used to indicate that all + * levels should be visited. + * + *

When a security manager is installed and it denies access to a file + * (or directory), then it is ignored and not included in the stream. + * + *

The returned stream contains references to one or more open directories. + * The directories are closed by closing the stream. + * + *

If an {@link IOException} is thrown when accessing the directory + * after this method has returned, it is wrapped in an {@link + * UncheckedIOException} which will be thrown from the method that caused + * the access to take place. + * + * @apiNote + * This method must be used within a try-with-resources statement or similar + * control structure to ensure that the stream's open directories are closed + * promptly after the stream's operations have completed. + * + * @param closeable a closeable which must be closed after walking + * @param start + * the starting file + * @param maxDepth + * the maximum number of directory levels to visit + * @param options + * options to configure the traversal + * + * @return the {@link Stream} of {@link Path} + * + * @throws IllegalArgumentException + * if the {@code maxDepth} parameter is negative + * @throws SecurityException + * If the security manager denies access to the starting file. + * In the case of the default provider, the {@link + * SecurityManager#checkRead(String) checkRead} method is invoked + * to check read access to the directory. + * @throws IOException + * if an I/O error is thrown when accessing the starting file. + */ + public static Stream walk(Closeable closeable, + Path start, + int maxDepth, + FileVisitOption... options) throws IOException { + FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options); + try { + Spliterator spliterator = + Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT); + return StreamSupport.stream(spliterator, false) + .onClose(() -> { + iterator.close(); + try { + closeable.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .map(FileTreeWalker.Event::file); + } catch (Error|RuntimeException e) { + iterator.close(); + closeable.close(); + throw e; + } + } + + /** + * Convert a Closeable to a Runnable by converting checked IOException + * to UncheckedIOException + */ + private static Runnable asUncheckedRunnable(Closeable c) { + return () -> { + try { + c.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } +} diff --git a/files-api/src/main/java/org/xbib/files/Provider.java b/files-api/src/main/java/org/xbib/files/Provider.java deleted file mode 100644 index 7cb8e83..0000000 --- a/files-api/src/main/java/org/xbib/files/Provider.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.xbib.files; - -import java.net.URI; -import java.util.Map; - -public interface Provider { - - Files provide(URI uri, Map env); -} diff --git a/files-api/src/main/java/org/xbib/files/WrappedDirectoryStream.java b/files-api/src/main/java/org/xbib/files/WrappedDirectoryStream.java new file mode 100644 index 0000000..be2af6b --- /dev/null +++ b/files-api/src/main/java/org/xbib/files/WrappedDirectoryStream.java @@ -0,0 +1,29 @@ +package org.xbib.files; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.util.Iterator; + +public class WrappedDirectoryStream implements DirectoryStream { + + private final Closeable closeable; + + private final DirectoryStream directoryStream; + + public WrappedDirectoryStream(Closeable closeable, DirectoryStream directoryStream) { + this.closeable = closeable; + this.directoryStream = directoryStream; + } + + @Override + public Iterator iterator() { + return directoryStream.iterator(); + } + + @Override + public void close() throws IOException { + directoryStream.close(); + closeable.close(); + } +} diff --git a/files-api/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider b/files-api/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider new file mode 100644 index 0000000..fcd0892 --- /dev/null +++ b/files-api/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider @@ -0,0 +1 @@ +org.xbib.files.DefaultFileServiceProvider \ No newline at end of file diff --git a/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider deleted file mode 100644 index 4c9449a..0000000 --- a/files-api/src/main/resources/META-INF/services/org.xbib.files.Provider +++ /dev/null @@ -1 +0,0 @@ -org.xbib.files.DefaultFilesProvider \ No newline at end of file diff --git a/files-ftp-fs/src/main/java/module-info.java b/files-ftp-fs/src/main/java/module-info.java index ebcf219..eef23ea 100644 --- a/files-ftp-fs/src/main/java/module-info.java +++ b/files-ftp-fs/src/main/java/module-info.java @@ -1,5 +1,5 @@ import java.nio.file.spi.FileSystemProvider; -import org.xbib.files.Provider; +import org.xbib.files.FileServiceProvider; import org.xbib.io.ftp.fs.FTPFileSystemProvider; import org.xbib.io.ftp.fs.spi.FTPFilesProvider; @@ -7,5 +7,5 @@ module org.xbib.files.ftp.fs { requires org.xbib.files; requires org.xbib.files.ftp; provides FileSystemProvider with FTPFileSystemProvider; - provides Provider with FTPFilesProvider; + provides FileServiceProvider with FTPFilesProvider; } diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java index 734eab8..1855e0a 100644 --- a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPContext.java @@ -3,12 +3,13 @@ package org.xbib.io.ftp.fs.spi; import org.xbib.io.ftp.fs.FTPEnvironment; import org.xbib.io.ftp.fs.FTPFileSystemProvider; +import java.io.Closeable; import java.io.IOException; import java.net.URI; import java.nio.file.FileSystem; import java.util.Map; -class FTPContext { +class FTPContext implements Closeable { final FTPFileSystemProvider provider; @@ -19,7 +20,8 @@ class FTPContext { this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment()); } - void close() throws IOException { + @Override + public void close() throws IOException { fileSystem.close(); } } diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java index 795602f..10fb54e 100644 --- a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java @@ -1,5 +1,9 @@ package org.xbib.io.ftp.fs.spi; +import org.xbib.files.FileService; +import org.xbib.files.FileWalker; +import org.xbib.files.WrappedDirectoryStream; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,6 +14,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -24,8 +29,9 @@ import java.nio.file.attribute.UserPrincipal; import java.util.EnumSet; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; -public class FTPFiles implements org.xbib.files.Files { +public class FTPFiles implements FileService { private static final int READ_BUFFER_SIZE = 128 * 1024; @@ -146,21 +152,41 @@ public class FTPFiles implements org.xbib.files.Files { return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); } - @Override - public DirectoryStream stream(String path, String glob) throws IOException { - return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); - } - - @Override - public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { - return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); - } - @Override public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException { upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); } + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + FTPContext ctx = new FTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException { + FTPContext ctx = new FTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @Override + public Stream list(String path) throws IOException { + FTPContext ctx = new FTPContext(uri, env); + return FileWalker.list(new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path)))); + } + + @Override + public Stream walk(String path, FileVisitOption... options) throws IOException { + FTPContext ctx = new FTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), Integer.MAX_VALUE, options); + } + + @Override + public Stream walk(String path, int maxdepth, FileVisitOption... options) throws IOException { + FTPContext ctx = new FTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options); + } + public void upload(Path source, Path target, Set dirPerms, Set filePerms, diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java index 994c41d..7dac0a4 100644 --- a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java @@ -1,14 +1,14 @@ package org.xbib.io.ftp.fs.spi; -import org.xbib.files.Files; -import org.xbib.files.Provider; +import org.xbib.files.FileService; +import org.xbib.files.FileServiceProvider; import java.net.URI; import java.util.Map; -public class FTPFilesProvider implements Provider { +public class FTPFilesProvider implements FileServiceProvider { @Override - public Files provide(URI uri, Map env) { - return !uri.isAbsolute() || uri.getScheme().equals("file") ? new FTPFiles(uri, env) : null; + public FileService provide(URI uri, Map env) { + return uri.isAbsolute() && uri.getScheme().equals("ftp") ? new FTPFiles(uri, env) : null; } } diff --git a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider similarity index 100% rename from files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider rename to files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider diff --git a/files-sftp-fs/src/main/java/module-info.java b/files-sftp-fs/src/main/java/module-info.java index d82facf..f0d578f 100644 --- a/files-sftp-fs/src/main/java/module-info.java +++ b/files-sftp-fs/src/main/java/module-info.java @@ -1,7 +1,7 @@ import org.apache.sshd.fs.SftpFileSystemProvider; import java.nio.file.spi.FileSystemProvider; -import org.xbib.files.Provider; -import org.apache.sshd.fs.spi.SFTPFilesProvider; +import org.xbib.files.FileServiceProvider; +import org.apache.sshd.fs.spi.SFTPFileServiceProvider; module org.xbib.files.sftp.fs { exports org.apache.sshd.fs; @@ -9,5 +9,6 @@ module org.xbib.files.sftp.fs { requires org.xbib.files; requires transitive org.xbib.files.sftp; provides FileSystemProvider with SftpFileSystemProvider; - provides Provider with SFTPFilesProvider; + provides FileServiceProvider with SFTPFileServiceProvider; + requires java.logging; } diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/SftpFileSystem.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/SftpFileSystem.java index 2e9e805..2698457 100644 --- a/files-sftp-fs/src/main/java/org/apache/sshd/fs/SftpFileSystem.java +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/SftpFileSystem.java @@ -71,7 +71,7 @@ public class SftpFileSystem private final ThreadLocal wrappers = new ThreadLocal<>(); private final int version; private final Set supportedViews; - private SftpPath defaultDir; + private final SftpPath defaultDir; private int readBufferSize; private int writeBufferSize; private final List stores; diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java index d03f1c5..c7d1733 100644 --- a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPContext.java @@ -5,11 +5,12 @@ import org.apache.sshd.client.SshClient; import org.apache.sshd.fs.SftpFileSystem; import org.apache.sshd.fs.SftpFileSystemProvider; +import java.io.Closeable; import java.io.IOException; import java.net.URI; import java.util.Map; -class SFTPContext { +public class SFTPContext implements Closeable { private final SshClient sshClient; @@ -17,7 +18,7 @@ class SFTPContext { final SftpFileSystem fileSystem; - SFTPContext(URI uri, Map env) throws IOException { + public SFTPContext(URI uri, Map env) throws IOException { this.sshClient = ClientBuilder.builder().build(); Object object = env.get("workers"); if (object instanceof Integer) { @@ -33,7 +34,8 @@ class SFTPContext { this.fileSystem = provider.newFileSystem(uri, env); } - void close() throws IOException { + @Override + public void close() throws IOException { sshClient.stop(); fileSystem.close(); } diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java similarity index 90% rename from files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java rename to files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java index a4af113..da93d8d 100644 --- a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFiles.java +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java @@ -1,5 +1,9 @@ package org.apache.sshd.fs.spi; +import org.xbib.files.FileService; +import org.xbib.files.FileWalker; +import org.xbib.files.WrappedDirectoryStream; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,6 +14,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -24,8 +29,9 @@ import java.nio.file.attribute.UserPrincipal; import java.util.EnumSet; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; -public class SFTPFiles implements org.xbib.files.Files { +public class SFTPFileService implements FileService { private static final int READ_BUFFER_SIZE = 128 * 1024; @@ -41,7 +47,7 @@ public class SFTPFiles implements org.xbib.files.Files { private final Map env; - public SFTPFiles(URI uri, Map env) { + public SFTPFileService(URI uri, Map env) { this.uri = uri; this.env = env; } @@ -146,16 +152,6 @@ public class SFTPFiles implements org.xbib.files.Files { return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); } - @Override - public DirectoryStream stream(String path, String glob) throws IOException { - return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); - } - - @Override - public DirectoryStream filter(String path, DirectoryStream.Filter filter) throws IOException { - return performWithContext(ctx -> Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); - } - @Override public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException { upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); @@ -223,6 +219,36 @@ public class SFTPFiles implements org.xbib.files.Files { performWithContext(ctx -> Files.deleteIfExists(ctx.fileSystem.getPath(source))); } + @Override + public DirectoryStream stream(String path, String glob) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob)); + } + + @Override + public DirectoryStream stream(String path, DirectoryStream.Filter filter) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)); + } + + @Override + public Stream list(String path) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.list(new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path)))); + } + + @Override + public Stream walk(String path, FileVisitOption... options) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), Integer.MAX_VALUE, options); + } + + @Override + public Stream walk(String path, int maxdepth, FileVisitOption... options) throws IOException { + SFTPContext ctx = new SFTPContext(uri, env); + return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options); + } + public void upload(Path source, Path target, Set dirPermissions, Set filePermissions, @@ -389,4 +415,8 @@ public class SFTPFiles implements org.xbib.files.Files { } } } + + private T performWithoutClose(SFTPContext ctx, WithContext action) throws IOException { + return action.perform(ctx); + } } diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileServiceProvider.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileServiceProvider.java new file mode 100644 index 0000000..d70cb88 --- /dev/null +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileServiceProvider.java @@ -0,0 +1,14 @@ +package org.apache.sshd.fs.spi; + +import org.xbib.files.FileService; +import org.xbib.files.FileServiceProvider; + +import java.net.URI; +import java.util.Map; + +public class SFTPFileServiceProvider implements FileServiceProvider { + @Override + public FileService provide(URI uri, Map env) { + return uri.isAbsolute() && uri.getScheme().equals("sftp") ? new SFTPFileService(uri, env) : null; + } +} diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java deleted file mode 100644 index 4210243..0000000 --- a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFilesProvider.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.apache.sshd.fs.spi; - -import org.xbib.files.Files; -import org.xbib.files.Provider; - -import java.net.URI; -import java.util.Map; - -public class SFTPFilesProvider implements Provider { - @Override - public Files provide(URI uri, Map env) { - return !uri.isAbsolute() || uri.getScheme().equals("file") ? new SFTPFiles(uri, env) : null; - } -} diff --git a/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider b/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider new file mode 100644 index 0000000..0e075d7 --- /dev/null +++ b/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider @@ -0,0 +1 @@ +org.apache.sshd.fs.spi.SFTPFileServiceProvider \ No newline at end of file diff --git a/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider b/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider deleted file mode 100644 index 74789f6..0000000 --- a/files-sftp-fs/src/main/resources/META-INF/services/org.xbib.files.Provider +++ /dev/null @@ -1 +0,0 @@ -org.apache.sshd.fs.spi.SFTPFilesProvider \ No newline at end of file diff --git a/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/FileServiceProviderTest.java b/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/FileServiceProviderTest.java new file mode 100644 index 0000000..442b665 --- /dev/null +++ b/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/FileServiceProviderTest.java @@ -0,0 +1,30 @@ +package org.apache.sshd.fs.test; + +import org.junit.jupiter.api.Test; +import org.xbib.files.FileService; + +import java.io.IOException; +import java.security.Provider; +import java.security.Security; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.logging.Logger; + +public class FileServiceProviderTest { + + static { + // load bouncy castle provider (and other security providers) + for (Provider p : ServiceLoader.load(Provider.class)) { + if (Security.getProvider(p.getName()) == null) { + Security.addProvider(p); + } + } + } + + @Test + public void testSFTP() throws IOException { + Map env = Map.of("username", "joerg"); + FileService fs = FileService.newInstance("sftp://alkmene:22", env); + fs.list(".").forEach(p -> Logger.getAnonymousLogger().info(p.toString())); + } +} diff --git a/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/SFTPFileSystemTest.java b/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/SFTPFileSystemTest.java index 8522073..70e09fc 100644 --- a/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/SFTPFileSystemTest.java +++ b/files-sftp-fs/src/test/java/org/apache/sshd/fs/test/SFTPFileSystemTest.java @@ -13,9 +13,11 @@ import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyPair; +import java.security.Provider; import java.security.Security; import java.util.HashMap; import java.util.Map; +import java.util.ServiceLoader; import java.util.logging.Logger; public class SFTPFileSystemTest { @@ -23,8 +25,12 @@ public class SFTPFileSystemTest { private static final Logger logger = Logger.getLogger(SFTPFileSystemTest.class.getName()); static { - Security.addProvider(new EdDSASecurityProvider()); - Security.addProvider(new BouncyCastleProvider()); + // load bouncy castle provider (and other security providers) + for (Provider p : ServiceLoader.load(Provider.class)) { + if (Security.getProvider(p.getName()) == null) { + Security.addProvider(p); + } + } } @Test @@ -44,6 +50,7 @@ public class SFTPFileSystemTest { sshClient.setNioWorkers(1); sshClient.start(); SftpFileSystem fileSystem = new SftpFileSystemProvider(sshClient).newFileSystem(uri, env); + // ... sshClient.stop(); } } diff --git a/files-sftp/src/main/java/org/apache/sshd/client/impl/AbstractSftpClient.java b/files-sftp/src/main/java/org/apache/sshd/client/impl/AbstractSftpClient.java index c371afe..090dbf6 100644 --- a/files-sftp/src/main/java/org/apache/sshd/client/impl/AbstractSftpClient.java +++ b/files-sftp/src/main/java/org/apache/sshd/client/impl/AbstractSftpClient.java @@ -675,8 +675,6 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme if (!isOpen()) { throw new IOException("close(" + handle + ") client is closed"); } - - byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false); buffer.putBytes(id); @@ -688,8 +686,6 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme if (!isOpen()) { throw new IOException("remove(" + path + ") client is closed"); } - - Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer = putReferencedName(SftpConstants.SSH_FXP_REMOVE, buffer, path, 0); checkCommandStatus(SftpConstants.SSH_FXP_REMOVE, buffer);