Przejdź do treści

Listowanie i przeszukiwanie katalogów przez walkFileTree

  • Pliki i katalogi
  • poziom: średni
  • aktualizacja: 16 sierpnia 2026

Files.walkFileTree odwiedza drzewo katalogów przez kontrolowany FileVisitor. W odróżnieniu od krótkiego potoku Files.walk pozwala przed wejściem do podkatalogu zdecydować, że całe jego poddrzewo ma zostać pominięte. To ważne przy danych niezaufanych: dowiązanie do pliku mogłoby ujawnić treść spoza katalogu, a windowsowe złączenie katalogów prowadzić poza drzewo lub utworzyć pętlę. Przykład pomija oba rodzaje odwołań, ogranicza głębokość i czyta znalezione pliki w UTF-8 bez ładowania ich w całości do pamięci.

JAVA PrzeszukiwanieKatalogow.java 147 linii Pobierz .java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

/**
 * Listowanie i przeszukiwanie katalogów przez walkFileTree — skryptyJAVA.pl
 * Zapisz jako: PrzeszukiwanieKatalogow.java
 */
public class PrzeszukiwanieKatalogow {

    /** Znajduje zwykłe pliki o rozszerzeniu, schodząc najwyżej na podaną głębokość. */
    public static List<Path> znajdzPoRozszerzeniu(Path katalog, String rozszerzenie,
                                                  int maksymalnaGlebokosc) throws IOException {
        if (maksymalnaGlebokosc < 0) {
            throw new IllegalArgumentException("Głębokość nie może być ujemna.");
        }
        String koncowka = normalizujRozszerzenie(rozszerzenie);
        List<Path> wyniki = new ArrayList<>();
        Files.walkFileTree(katalog, Set.of(), maksymalnaGlebokosc, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                if (!dir.equals(katalog) && (attrs.isSymbolicLink() || attrs.isOther())) {
                    return FileVisitResult.SKIP_SUBTREE;
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path plik, BasicFileAttributes attrs) {
                if (attrs.isRegularFile() && maRozszerzenie(plik, koncowka)) {
                    wyniki.add(plik);
                }
                return FileVisitResult.CONTINUE;
            }
        });
        wyniki.sort(Comparator.comparing(plik -> katalog.relativize(plik).toString(),
                String.CASE_INSENSITIVE_ORDER));
        return List.copyOf(wyniki);
    }

    /** Przed kropką musi coś stać: plik nazwany ".txt" jest ukryty, a nie typu txt. */
    private static boolean maRozszerzenie(Path plik, String koncowka) {
        String nazwa = plik.getFileName().toString().toLowerCase(Locale.ROOT);
        return nazwa.length() > koncowka.length() && nazwa.endsWith(koncowka);
    }

    /** Wyszukuje fragment bez rozróżniania wielkości liter w plikach danego typu. */
    public static List<Path> znajdzWTresci(Path katalog, String rozszerzenie,
                                           String fragment) throws IOException {
        String szukany = Objects.requireNonNull(fragment, "fragment").toLowerCase(Locale.ROOT);
        List<Path> wyniki = new ArrayList<>();
        for (Path plik : znajdzPoRozszerzeniu(katalog, rozszerzenie, Integer.MAX_VALUE)) {
            if (plikZawiera(plik, szukany)) {
                wyniki.add(plik);
            }
        }
        return List.copyOf(wyniki);
    }

    /** Czyta plik linia po linii; strumień nie pozostaje otwarty po powrocie z metody. */
    private static boolean plikZawiera(Path plik, String szukany) throws IOException {
        try (BufferedReader czytnik = new BufferedReader(new InputStreamReader(
                     Files.newInputStream(plik, LinkOption.NOFOLLOW_LINKS),
                     StandardCharsets.UTF_8));
             Stream<String> linie = czytnik.lines()) {
            try {
                return linie.anyMatch(linia -> linia.toLowerCase(Locale.ROOT).contains(szukany));
            } catch (UncheckedIOException wyjatek) {
                throw wyjatek.getCause();
            }
        }
    }

    private static String normalizujRozszerzenie(String rozszerzenie) {
        String wynik = Objects.requireNonNull(rozszerzenie, "rozszerzenie")
                .strip().toLowerCase(Locale.ROOT);
        if (wynik.isEmpty() || wynik.equals(".")) {
            throw new IllegalArgumentException("Podaj rozszerzenie, na przykład txt.");
        }
        return wynik.startsWith(".") ? wynik : "." + wynik;
    }

    /** Usuwa drzewo od najgłębszych elementów; używane tylko do sprzątania demonstracji. */
    private static void usunDrzewo(Path katalog) throws IOException {
        if (!Files.exists(katalog)) {
            return;
        }
        List<Path> odKonca;
        try (Stream<Path> sciezki = Files.walk(katalog)) {
            try {
                odKonca = sciezki.sorted(Comparator.reverseOrder()).toList();
            } catch (UncheckedIOException wyjatek) {
                throw wyjatek.getCause();
            }
        }
        for (Path sciezka : odKonca) {
            Files.deleteIfExists(sciezka);
        }
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) throws IOException {
        Path demo = Files.createTempDirectory("files-walk-demo-");
        try {
            Path dokumenty = Files.createDirectories(demo.resolve("dokumenty"));
            Path archiwum = Files.createDirectories(dokumenty.resolve("archiwum"));
            Path kod = Files.createDirectories(demo.resolve("kod"));
            Files.writeString(demo.resolve("README.TXT"), "Projekt w języku Java\n",
                    StandardCharsets.UTF_8);
            Files.writeString(dokumenty.resolve("raport.txt"), "Raport kwartalny\nBez błędów\n",
                    StandardCharsets.UTF_8);
            Files.writeString(archiwum.resolve("stary.txt"), "Archiwalny projekt Java\n",
                    StandardCharsets.UTF_8);
            Files.writeString(kod.resolve("Main.java"), "public class Main {}\n",
                    StandardCharsets.UTF_8);

            System.out.println("Katalog demonstracyjny: " + demo);
            System.out.println("\nPliki TXT do głębokości 2:");
            for (Path plik : znajdzPoRozszerzeniu(demo, "txt", 2)) {
                System.out.printf("  %-25s %4d B%n", demo.relativize(plik), Files.size(plik));
            }

            System.out.println("\nPliki TXT zawierające słowo Java:");
            for (Path plik : znajdzWTresci(demo, ".txt", "java")) {
                System.out.println("  " + demo.relativize(plik));
            }
        } finally {
            usunDrzewo(demo);
            System.out.println("\nUsunięto katalog demonstracyjny.");
        }
    }
}

Jak użyć tego skryptu

  1. Zapisz kod jako PrzeszukiwanieKatalogow.java — nazwa pliku musi być identyczna z nazwą klasy publicznej — i uruchom poleceniem java PrzeszukiwanieKatalogow.java. Program tworzy niewielkie drzewo w katalogu tymczasowym, wykonuje dwa wyszukania i usuwa wszystkie dane w bloku finally.
  2. Przekaż własny katalog jako Path.of("dane") albo ścieżkę bezwzględną. Metoda znajdzPoRozszerzeniu przyjmuje rozszerzenie zarówno z kropką, jak i bez niej.
  3. Ustaw trzeci argument metody znajdzPoRozszerzeniu na żądaną głębokość: 1 obejmuje sam katalog i jego bezpośrednią zawartość, a Integer.MAX_VALUE oznacza całe drzewo.
  4. Do wyszukiwania treści wywołaj znajdzWTresci(katalog, "txt", "szukany fragment"). Metoda nie rozróżnia wielkości liter i czyta kolejne linie jako UTF-8.

Warto wiedzieć

  • walkFileTree zamyka uchwyty do katalogów w trakcie marszu, ale strumień linii otwarty przez BufferedReader.lines() nadal wymaga try-with-resources. Na Windowsie pozostawiony uchwyt potrafi zablokować usunięcie lub zmianę nazwy pliku.
  • Visitor pomija zagnieżdżone ścieżki raportowane bez podążania jako dowiązanie symboliczne albo obiekt isOther(); ten drugi warunek zatrzymuje typowe windowsowe złączenia katalogów. Twardy link wygląda jednak jak zwykły plik i standardowe API nie zdradza, gdzie utworzono jego pozostałe nazwy — jeśli katalog jest pod kontrolą napastnika, potrzebujesz dodatkowej izolacji systemowej.
  • Przeszukuj treść wyłącznie tych plików, o których wiesz, że są tekstem. Nie próbuj dekodować wszystkiego, co napotkasz — obraz, archiwum czy plik binarny nie jest poprawnym UTF-8 i odczyt skończy się wyjątkiem w środku strumienia.
  • Domyślna implementacja SimpleFileVisitor.visitFileFailed ponownie rzuca wyjątek, więc jeden podkatalog bez uprawnień nadal przerywa cały skan. Jeśli skan ma przeżyć takie miejsca, przesłoń tę metodę i dla oczekiwanych błędów zwracaj FileVisitResult.CONTINUE; pozostałych wyjątków nie połykaj, bo ukryjesz awarię dysku albo błędną ścieżkę.
#pliki #nio #path #utf-8 #stream api

Podobne skrypty