add Files walk alternative without AccessDeniedException

This commit is contained in:
Jörg Prante 2023-11-20 16:59:34 +01:00
parent 720640f8aa
commit 94cea9ae79
6 changed files with 101 additions and 147 deletions

View file

@ -5,4 +5,5 @@ module org.xbib.files {
exports org.xbib.files; exports org.xbib.files;
uses FileServiceProvider; uses FileServiceProvider;
provides FileServiceProvider with DefaultFileServiceProvider; provides FileServiceProvider with DefaultFileServiceProvider;
requires java.logging;
} }

View file

@ -18,6 +18,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream; import java.util.stream.Stream;
public interface FileService { public interface FileService {
@ -115,4 +117,29 @@ public interface FileService {
Stream<Path> walk(String path, FileVisitOption... options) throws IOException; Stream<Path> walk(String path, FileVisitOption... options) throws IOException;
Stream<Path> walk(String path, int maxdepth, FileVisitOption... options) throws IOException; Stream<Path> walk(String path, int maxdepth, FileVisitOption... options) throws IOException;
/**
* Replacement for Files.walk() without AccessDeniedException.
* @param p the path
* @return a stream of readable files under the path
*/
static Stream<Path> walk(Path p) {
if (Files.isReadable(p)) {
if (Files.isDirectory(p)) {
try (Stream<Path> stream = Files.list(p)) {
return stream.flatMap(FileService::walk);
} catch (IOException e) {
Logger.getLogger(FileService.class.getName())
.log(Level.FINE,"skipped " + p + " because of " + e.getMessage());
return Stream.empty();
}
} else {
return Stream.of(p);
}
} else {
Logger.getLogger(FileService.class.getName())
.log(Level.WARNING, "unreadable: " + p);
}
return Stream.empty();
}
} }

View file

@ -12,7 +12,6 @@ import org.xbib.files.FileTreeWalker.Event;
/** /**
* This source is a copy taken from java.nio.file.FileTreeIterator which is inaccessible. * 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. * An {@code Iterator} to iterate over the nodes of a file tree.
* *
* <pre>{@code * <pre>{@code
@ -27,7 +26,9 @@ import org.xbib.files.FileTreeWalker.Event;
*/ */
class FileTreeIterator implements Iterator<Event>, Closeable { class FileTreeIterator implements Iterator<Event>, Closeable {
private final FileTreeWalker walker; private final FileTreeWalker walker;
private Event next; private Event next;
/** /**
@ -37,21 +38,15 @@ class FileTreeIterator implements Iterator<Event>, Closeable {
* if {@code maxDepth} is negative * if {@code maxDepth} is negative
* @throws IOException * @throws IOException
* if an I/O errors occurs opening the starting file * if an I/O errors occurs opening the starting file
* @throws SecurityException
* if the security manager denies access to the starting file
* @throws NullPointerException * @throws NullPointerException
* if {@code start} or {@code options} is {@code null} or * if {@code start} or {@code options} is {@code null} or
* the options array contains a {@code null} element * the options array contains a {@code null} element
*/ */
FileTreeIterator(Path start, int maxDepth, FileVisitOption... options) FileTreeIterator(Path start, int maxDepth, FileVisitOption... options) throws IOException {
throws IOException
{
this.walker = new FileTreeWalker(Arrays.asList(options), maxDepth); this.walker = new FileTreeWalker(Arrays.asList(options), maxDepth);
this.next = walker.walk(start); this.next = walker.walk(start);
assert next.type() == FileTreeWalker.EventType.ENTRY || assert next.type() == FileTreeWalker.EventType.ENTRY ||
next.type() == FileTreeWalker.EventType.START_DIRECTORY; next.type() == FileTreeWalker.EventType.START_DIRECTORY;
// IOException if there a problem accessing the starting file
IOException ioe = next.ioeException(); IOException ioe = next.ioeException();
if (ioe != null) if (ioe != null)
throw ioe; throw ioe;
@ -62,10 +57,9 @@ class FileTreeIterator implements Iterator<Event>, Closeable {
FileTreeWalker.Event ev = walker.next(); FileTreeWalker.Event ev = walker.next();
while (ev != null) { while (ev != null) {
IOException ioe = ev.ioeException(); IOException ioe = ev.ioeException();
if (ioe != null) if (ioe != null) {
throw new UncheckedIOException(ioe); throw new UncheckedIOException(ioe);
}
// END_DIRECTORY events are ignored
if (ev.type() != FileTreeWalker.EventType.END_DIRECTORY) { if (ev.type() != FileTreeWalker.EventType.END_DIRECTORY) {
next = ev; next = ev;
return; return;
@ -77,19 +71,22 @@ class FileTreeIterator implements Iterator<Event>, Closeable {
@Override @Override
public boolean hasNext() { public boolean hasNext() {
if (!walker.isOpen()) if (!walker.isOpen()) {
throw new IllegalStateException(); throw new IllegalStateException();
}
fetchNextIfNeeded(); fetchNextIfNeeded();
return next != null; return next != null;
} }
@Override @Override
public Event next() { public Event next() {
if (!walker.isOpen()) if (!walker.isOpen()) {
throw new IllegalStateException(); throw new IllegalStateException();
}
fetchNextIfNeeded(); fetchNextIfNeeded();
if (next == null) if (next == null) {
throw new NoSuchElementException(); throw new NoSuchElementException();
}
Event result = next; Event result = next;
next = null; next = null;
return result; return result;

View file

@ -12,11 +12,12 @@ import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Collection; import java.util.Collection;
import java.util.Deque;
import java.util.Iterator; import java.util.Iterator;
import java.util.Objects;
/** /**
* This source is a copy taken from java.nio.file.FileTreeWalker which is inaccessible. * 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 * Walks a file tree, generating a sequence of events corresponding to the files
* in the tree. * in the tree.
* *
@ -38,10 +39,11 @@ import java.util.Iterator;
*/ */
class FileTreeWalker implements Closeable { class FileTreeWalker implements Closeable {
private final boolean followLinks; private final boolean followLinks;
private final LinkOption[] linkOptions; private final LinkOption[] linkOptions;
private final int maxDepth; private final int maxDepth;
private final ArrayDeque<DirectoryNode> stack = new ArrayDeque<>(); private final Deque<DirectoryNode> stack = new ArrayDeque<>();
private boolean closed; private boolean closed;
/** /**
@ -89,7 +91,7 @@ class FileTreeWalker implements Closeable {
/** /**
* The event types. * The event types.
*/ */
static enum EventType { enum EventType {
/** /**
* Start of a directory * Start of a directory
*/ */
@ -109,13 +111,13 @@ class FileTreeWalker implements Closeable {
*/ */
static class Event { static class Event {
private final EventType type; private final EventType type;
private final Path file; private final Path path;
private final BasicFileAttributes attrs; private final BasicFileAttributes attrs;
private final IOException ioe; private final IOException ioe;
private Event(EventType type, Path file, BasicFileAttributes attrs, IOException ioe) { private Event(EventType type, Path path, BasicFileAttributes attrs, IOException ioe) {
this.type = type; this.type = type;
this.file = file; this.path = path;
this.attrs = attrs; this.attrs = attrs;
this.ioe = ioe; this.ioe = ioe;
} }
@ -132,8 +134,8 @@ class FileTreeWalker implements Closeable {
return type; return type;
} }
Path file() { Path path() {
return file; return path;
} }
BasicFileAttributes attributes() { BasicFileAttributes attributes() {
@ -161,9 +163,9 @@ class FileTreeWalker implements Closeable {
boolean fl = false; boolean fl = false;
for (FileVisitOption option: options) { for (FileVisitOption option: options) {
// will throw NPE if options contains null // will throw NPE if options contains null
switch (option) { if (Objects.requireNonNull(option) == FileVisitOption.FOLLOW_LINKS) {
case FOLLOW_LINKS : fl = true; break; fl = true;
default: } else {
throw new AssertionError("Should not get here"); throw new AssertionError("Should not get here");
} }
} }
@ -171,8 +173,7 @@ class FileTreeWalker implements Closeable {
throw new IllegalArgumentException("'maxDepth' is negative"); throw new IllegalArgumentException("'maxDepth' is negative");
this.followLinks = fl; this.followLinks = fl;
this.linkOptions = (fl) ? new LinkOption[0] : this.linkOptions = (fl) ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
this.maxDepth = maxDepth; this.maxDepth = maxDepth;
} }
@ -181,22 +182,18 @@ class FileTreeWalker implements Closeable {
* the walk is following sym links is not. The {@code canUseCached} * the walk is following sym links is not. The {@code canUseCached}
* argument determines whether this method can use cached attributes. * argument determines whether this method can use cached attributes.
*/ */
private BasicFileAttributes getAttributes(Path file, boolean canUseCached) private BasicFileAttributes getAttributes(Path file)
throws IOException throws IOException {
{
// attempt to get attributes of file. If fails and we are following // 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 // links then a link target might not exist so get attributes of link
BasicFileAttributes attrs; BasicFileAttributes attrs;
try { try {
attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions); attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions);
} catch (IOException ioe) { } catch (IOException ioe) {
if (!followLinks) if (!followLinks) {
throw ioe; throw ioe;
}
// attempt to get attrmptes without following links attrs = Files.readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
attrs = Files.readAttributes(file,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
} }
return attrs; return attrs;
} }
@ -232,70 +229,39 @@ class FileTreeWalker implements Closeable {
/** /**
* Visits the given file, returning the {@code Event} corresponding to that * Visits the given file, returning the {@code Event} corresponding to that
* visit. * 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) { private Event visit(Path entry) {
// need the file attributes
BasicFileAttributes attrs; BasicFileAttributes attrs;
try { try {
attrs = getAttributes(entry, canUseCached); attrs = getAttributes(entry);
} catch (IOException ioe) { } catch (IOException ioe) {
return new Event(EventType.ENTRY, entry, 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(); int depth = stack.size();
if (depth >= maxDepth || !attrs.isDirectory()) { if (depth >= maxDepth || !attrs.isDirectory()) {
return new Event(EventType.ENTRY, entry, attrs); return new Event(EventType.ENTRY, entry, attrs);
} }
// check for cycles when following links
if (followLinks && wouldLoop(entry, attrs.fileKey())) { if (followLinks && wouldLoop(entry, attrs.fileKey())) {
return new Event(EventType.ENTRY, entry, return new Event(EventType.ENTRY, entry, new FileSystemLoopException(entry.toString()));
new FileSystemLoopException(entry.toString()));
} }
DirectoryStream<Path> stream;
// file is a directory, attempt to open it
DirectoryStream<Path> stream = null;
try { try {
stream = Files.newDirectoryStream(entry); stream = Files.newDirectoryStream(entry);
} catch (IOException ioe) { } catch (IOException ioe) {
return new Event(EventType.ENTRY, entry, 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)); stack.push(new DirectoryNode(entry, attrs.fileKey(), stream));
return new Event(EventType.START_DIRECTORY, entry, attrs); return new Event(EventType.START_DIRECTORY, entry, attrs);
} }
/** /**
* Start walking from the given file. * Start walking from the given file.
*/ */
Event walk(Path file) { Event walk(Path file) {
if (closed) if (closed) {
throw new IllegalStateException("Closed"); throw new IllegalStateException("Closed");
}
Event ev = visit(file, return visit(file);
false, // ignoreSecurityException
false); // canUseCached
assert ev != null;
return ev;
} }
/** /**
@ -304,16 +270,11 @@ class FileTreeWalker implements Closeable {
*/ */
Event next() { Event next() {
DirectoryNode top = stack.peek(); DirectoryNode top = stack.peek();
if (top == null) if (top == null) {
return null; // stack is empty, we are done return null;
}
// continue iteration of the directory at the top of the stack
Event ev;
do {
Path entry = null; Path entry = null;
IOException ioe = null; IOException ioe = null;
// get next entry in the directory
if (!top.skipped()) { if (!top.skipped()) {
Iterator<Path> iterator = top.iterator(); Iterator<Path> iterator = top.iterator();
try { try {
@ -324,9 +285,6 @@ class FileTreeWalker implements Closeable {
ioe = x.getCause(); ioe = x.getCause();
} }
} }
// no next entry so close and pop directory,
// creating corresponding event
if (entry == null) { if (entry == null) {
try { try {
top.stream().close(); top.stream().close();
@ -340,15 +298,7 @@ class FileTreeWalker implements Closeable {
stack.pop(); stack.pop();
return new Event(EventType.END_DIRECTORY, top.directory(), ioe); return new Event(EventType.END_DIRECTORY, top.directory(), ioe);
} }
return visit(entry);
// visit the entry
ev = visit(entry,
true, // ignoreSecurityException
true); // canUseCached
} while (ev == null);
return ev;
} }
/** /**
@ -362,18 +312,10 @@ class FileTreeWalker implements Closeable {
DirectoryNode node = stack.pop(); DirectoryNode node = stack.pop();
try { try {
node.stream().close(); node.stream().close();
} catch (IOException ignore) { } } catch (IOException ignore) {
// 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();
}
} }
/** /**

View file

@ -7,7 +7,6 @@ import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemLoopException; import java.nio.file.FileSystemLoopException;
import java.nio.file.FileVisitOption; import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.NotDirectoryException; import java.nio.file.NotDirectoryException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
@ -64,10 +63,6 @@ public class FileWalker {
* a directory <i>(optional specific exception)</i> * a directory <i>(optional specific exception)</i>
* @throws IOException * @throws IOException
* if an I/O error occurs when opening the directory * 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<Path> list(DirectoryStream<Path> ds) throws IOException { public static Stream<Path> list(DirectoryStream<Path> ds) throws IOException {
try { try {
@ -90,10 +85,8 @@ public class FileWalker {
} }
} }
}; };
Spliterator<Path> spliterator = Spliterator<Path> spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT);
Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT); return StreamSupport.stream(spliterator, false).onClose(asUncheckedRunnable(ds));
return StreamSupport.stream(spliterator, false)
.onClose(asUncheckedRunnable(ds));
} catch (Error|RuntimeException e) { } catch (Error|RuntimeException e) {
try { try {
ds.close(); ds.close();
@ -179,11 +172,6 @@ public class FileWalker {
* *
* @throws IllegalArgumentException * @throws IllegalArgumentException
* if the {@code maxDepth} parameter is negative * 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 * @throws IOException
* if an I/O error is thrown when accessing the starting file. * if an I/O error is thrown when accessing the starting file.
*/ */
@ -204,7 +192,7 @@ public class FileWalker {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
}) })
.map(FileTreeWalker.Event::file); .map(FileTreeWalker.Event::path);
} catch (Error|RuntimeException e) { } catch (Error|RuntimeException e) {
iterator.close(); iterator.close();
closeable.close(); closeable.close();
@ -213,8 +201,7 @@ public class FileWalker {
} }
/** /**
* Convert a Closeable to a Runnable by converting checked IOException * Convert a Closeable to a Runnable by converting checked IOException to UncheckedIOException.
* to UncheckedIOException
*/ */
private static Runnable asUncheckedRunnable(Closeable c) { private static Runnable asUncheckedRunnable(Closeable c) {
return () -> { return () -> {

View file

@ -1,5 +1,5 @@
group = org.xbib group = org.xbib
name = files name = files
version = 4.1.0 version = 4.2.0
org.gradle.warning.mode = ALL org.gradle.warning.mode = ALL