Quiero enumerar todos los archivos en un directorio y subdirectorios dentro de ese directorio que coincidan con una máscara de archivo .
Por ejemplo, "M:\SOURCE\*.doc", mientras que SOURCE puede verse así:
|-- SOURCE | |-- Folder1 | | |-- File1.doc | | |-- File1.txt | |-- File2.doc | |-- File3.xmlDebería devolver File1.doc y File2.doc.
Inicialmente, uso un DirectoryStream , porque eso ya hace algunas comprobaciones de la sintaxis de máscara/glob, además de poder usarlo para filtrar, ya que NO ES solo una expresión regular, sino una máscara de archivo real que un usuario normal encuentra más fácil de entender.
Files.newDirectoryStream(path, mask);El problema es que DirectoryStream solo verifica el directorio de ruta inmediata que proporciona y no sus subdirectorios
ENTONCES viene un método de "aplanamiento" con Files.walk que de hecho es capaz de mirar a través de todos los subdirectorios, el problema es que NO brinda la posibilidad de "filtrar" por una máscara de archivo de la misma manera que lo hace un DirectoryStream
Files.walk(path, Integer.MAX_VALUE);Así que estoy atascado, incapaz de combinar lo mejor de ambos métodos aquí...
También puede usar FileVisitor personalizado [1], con una combinación de PathMatcher [2], que funciona perfectamente con GLOB.
El código podría verse así:
public static void main(String[] args) throws IOException { System.out.println(getFiles(Paths.get("/tmp/SOURCE"), "*.doc")); } public static List<Path> getFiles(final Path directory, final String glob) throws IOException { final var docFileVisitor = new GlobFileVisitor(glob); Files.walkFileTree(directory, docFileVisitor); return docFileVisitor.getMatchedFiles(); } public static class GlobFileVisitor extends SimpleFileVisitor<Path> { private final PathMatcher pathMatcher; private List<Path> matchedFiles = new ArrayList<>(); public GlobFileVisitor(final String glob) { this.pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + glob); } @Override public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException { if (pathMatcher.matches(path.getFileName())) { matchedFiles.add(path); } return FileVisitResult.CONTINUE; } public List<Path> getMatchedFiles() { return matchedFiles; } }[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileVisitor.html
[2] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/PathMatcher.html
Creo que podría haber resuelto mi propia pregunta con la información recibida aquí y otras preguntas que mencionan el objeto PathMatcher
final PathMatcher maskMatcher = FileSystems.getDefault() .getPathMatcher("glob:" + mask); final List<Path> matchedFiles = Files.walk(path) .collect(Collectors.toList()); final List<Path> filesToRemove = new ArrayList<>(matchedFiles.size()); matchedFiles.forEach(foundPath -> { if (!maskMatcher.matches(foundPath.getFileName()) || Files.isDirectory(foundPath)) { filesToRemove.add(foundPath); } }); matchedFiles.removeAll(filesToRemove); Así que básicamente .getPathMatcher("glob:" + mask); es lo mismo que estaba haciendo DirectoryStream para filtrar los archivos
Todo lo que tengo que hacer ahora es filtrar la lista de rutas que obtengo con Files.walk eliminando los elementos que no coinciden con mi PathMatcher y que no son del tipo File
Es posible usar el filter de flujo común para recuperar los nombres de archivo filtrados de Files.walk usando String::matches con la expresión regular adecuada:
final String SOURCE_DIR = "test"; Files.walk(Paths.get(SOURCE_DIR)); .filter(p -> p.getFileName().toString().matches(".*\\.docx?")) .forEach(System.out::println);Producción
test\level01\level11\test.doc test\level02\test-level2.doc test\t1.doc test\t3.docxEstructura del directorio de entrada:
│ t1.doc │ t2.txt │ t3.docx │ t4.bin │ ├───level01 │ │ test.do │ │ │ └───level11 │ test.doc │ └───level02 test-level2.docActualizar
Es posible una solución recursiva utilizando newDirectoryStream , sin embargo, debe convertirse en Stream:
static Stream<Path> readFilesByMaskRecursively(Path start, String mask) { List<Stream<Path>> sub = new ArrayList<>(); try { sub.add(StreamSupport.stream( // read files by mask in current dir Files.newDirectoryStream(start, mask).spliterator(), false)); Files.newDirectoryStream(start, (path) -> path.toFile().isDirectory()) .forEach(path -> sub.add(recursive(path, mask))); } catch (IOException ioex) { ioex.printStackTrace(); } return sub.stream().flatMap(s -> s); // convert to Stream<Path> } // test readFilesByMaskRecursively(Paths.get(SOURCE_DIR), "*.doc*") .forEach(System.out::println);Producción:
test\t1.doc test\t3.docx test\level01\level11\test.doc test\level02\test-level2.docActualización 2
Se puede agregar un prefijo **/ al PathMatcher para cruzar los límites del directorio, luego la solución basada en Files.walk puede usar un filtro simplificado sin la necesidad de eliminar entradas específicas:
String mask = "*.doc*"; PathMatcher maskMatcher = FileSystems.getDefault().getPathMatcher("glob:**/" + mask); Files.walk(Paths.get(SOURCE_DIR)) .filter(path -> maskMatcher.matches(path)) .forEach(System.out::println);Salida (igual que en la solución recursiva):
test\level01\level11\test.doc test\level02\test-level2.doc test\t1.doc test\t3.docx