Tugas berkas yang berulang — memindahkan hasil unduhan ke folder per kategori, mengarsipkan laporan lama, memberi nama ulang ratusan foto — adalah pekerjaan yang tidak layak dikerjakan tangan. Chapter sebelumnya memproses isi satu berkas secara bertahap dengan checkpoint; sekarang kita naik satu tingkat: menyapu pohon direktori, mencocokkan nama berkas dengan pola, lalu memindahkan berkas yang cocok secara otomatis. Java menyediakan dua bahan utama untuk pekerjaan ini: PathMatcher dengan sintaks glob yang akrab dari dunia shell, dan Files.walk yang menelusuri direktori berjenjang tanpa rekursi manual. Dua contoh pada chapter ini membentuk satu alat rapikan-unduhan yang utuh — dari pencocokan pola sampai pemindahan — dan keduanya bisa dijalankan ulang dengan hasil yang sama.

Pola Glob dengan PathMatcher #

Setiap otomasi berkas dimulai dari satu pertanyaan: berkas mana yang perlu disentuh? PathMatcher menjawabnya dengan pola glob — sintaks yang sama dengan *.txt di shell. Objek pencocok dibuat lewat FileSystems.getDefault().getPathMatcher("glob:..."): prefiks glob: memilih mesin pencocok glob, sedangkan prefiks regex: memakai mesin java.util.regex penuh. Untuk nama berkas, glob jauh lebih aman daripada regex karena karakter titik berarti titik sungguhan, bukan operator pola. Tabel berikut merangkum sintaks yang paling sering dipakai:

Pola globMakna
*.txtnama berakhir .txt pada satu direktori
**/*.javaberkas .java pada kedalaman berapa pun
*.{jpg,png,gif}beberapa ekstensi sekaligus
?tepat satu karakter apa pun
[abc], [a-z]satu karakter dari himpunan atau rentang

Program berikut menguji tiga pola terhadap lima nama berkas:

java PolaBerkas.java
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;

public class PolaBerkas {
    public static void main(String[] args) {
        PathMatcher teks = FileSystems.getDefault().getPathMatcher("glob:*.txt");
        PathMatcher gambar = FileSystems.getDefault().getPathMatcher("glob:*.{jpg,png,gif}");
        PathMatcher arsip = FileSystems.getDefault().getPathMatcher("glob:*202?*.zip");

        String[] nama = {
                "laporan.txt", "foto-budi.jpg", "icon.png",
                "cadangan-2025.zip", "data.csv"
        };
        for (String n : nama) {
            Path p = Path.of(n);
            System.out.printf("%-20s teks=%-5b gambar=%-5b arsip=%b%n",
                    n, teks.matches(p), gambar.matches(p), arsip.matches(p));
        }
    }
}

Output yang Dihasilkan:

text
laporan.txt          teks=true  gambar=false arsip=false
foto-budi.jpg        teks=false gambar=true  arsip=false
icon.png             teks=false gambar=true  arsip=false
cadangan-2025.zip    teks=false gambar=false arsip=true
data.csv             teks=false gambar=false arsip=false

Mari kita telaah baris-baris kuncinya:

  1. Baris getPathMatcher("glob:*.txt"): prefiks glob: wajib ada; tanpa prefiks, getPathMatcher melempar IllegalArgumentException karena string pola harus menyatakan mesin pencocoknya.
  2. Baris glob:*.{jpg,png,gif}: kurung kurawal mendefinisikan kelompok alternatif — satu pola menangkap tiga ekstensi sekaligus.
  3. Baris glob:*202?*.zip": tanda tanya menggantikan tepat satu karakter, sehingga cadangan-2025.zip cocok sementara cadangan-2025x.zip tidak.
  4. Baris teks.matches(p): pencocokan berlaku pada keseluruhan Path — pola *.txt cocok untuk path satu nama; untuk path absolut, polanya perlu diawali **/.

Menyapu Pohon Direktori dengan Files.walk #

Pola sudah ada; sasaran berikutnya adalah pohon direktori. Files.walk(akar) mengembalikan Stream<Path> yang malas (lazy) — direktori dibaca bertahap saat stream dikonsumsi. Konsekuensinya penting: pohon tidak boleh diubah selama stream masih terbuka, jadi pola yang benar dua tahap — kumpulkan dulu, ubah kemudian: seluruh hasil dikumpulkan ke List, stream ditutup, baru pemindahan berjalan. Program berikut merapikan folder unduhan/ dengan peta ekstensi ke kategori:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class RapihkanUnduhan {
    // Peta ekstensi -> kategori folder tujuan.
    private static final Map<String, String> KATEGORI = Map.of(
            "pdf", "dokumen", "txt", "dokumen", "md", "dokumen",
            "jpg", "gambar", "png", "gambar",
            "json", "data", "csv", "data",
            "py", "skrip");

    public static void main(String[] args) throws IOException {
        Path unduhan = Path.of("unduhan");
        Files.createDirectories(unduhan);

        // Pohon contoh dibuat oleh program agar contoh ini mandiri.
        for (String n : new String[]{"laporan.pdf", "catatan.txt", "tulisan.md",
                "foto-budi.jpg", "icon.png", "penjualan.json", "biaya.csv", "alat.py"}) {
            Files.writeString(unduhan.resolve(n), "contoh", StandardCharsets.UTF_8);
        }

        System.out.println("== sebelum ==");
        tampilkanPohon(unduhan);

        // Kumpulkan dulu seluruh berkas (stream ditutup) baru dipindahkan,
        // agar pohon tidak berubah di tengah penelusuran Files.walk.
        List<Path> daftar;
        try (Stream<Path> berkas = Files.walk(unduhan)) {
            daftar = berkas.filter(Files::isRegularFile).sorted().toList();
        }

        System.out.println("== pindahan ==");
        for (Path p : daftar) {
            String kategori = KATEGORI.get(ekstensi(p));
            if (kategori != null) {
                Path tujuanDir = unduhan.resolve(kategori);
                Files.createDirectories(tujuanDir);
                Files.move(p, tujuanDir.resolve(p.getFileName()));
                System.out.printf("%-16s -> %s/%n", p.getFileName(), kategori);
            }
        }

        System.out.println("== sesudah ==");
        tampilkanPohon(unduhan);
    }

    private static String ekstensi(Path p) {
        String nama = p.getFileName().toString();
        int titik = nama.lastIndexOf('.');
        return titik < 0 ? "" : nama.substring(titik + 1).toLowerCase();
    }

    private static void tampilkanPohon(Path akar) throws IOException {
        try (Stream<Path> semua = Files.walk(akar)) {
            semua.sorted().forEach(p -> {
                int kedalaman = p.equals(akar) ? 0
                        : akar.relativize(p).getNameCount();
                System.out.println("  ".repeat(kedalaman)
                        + (kedalaman == 0 ? "unduhan/" : p.getFileName()));
            });
        }
        System.out.println();
    }
}

Output yang Dihasilkan:

text
== sebelum ==
unduhan/
  alat.py
  biaya.csv
  catatan.txt
  foto-budi.jpg
  icon.png
  laporan.pdf
  penjualan.json
  tulisan.md

== pindahan ==
alat.py          -> skrip/
biaya.csv        -> data/
catatan.txt      -> dokumen/
foto-budi.jpg    -> gambar/
icon.png         -> gambar/
laporan.pdf      -> dokumen/
penjualan.json   -> data/
tulisan.md       -> dokumen/
== sesudah ==
unduhan/
  data
    biaya.csv
    penjualan.json
  dokumen
    catatan.txt
    laporan.pdf
    tulisan.md
  gambar
    foto-budi.jpg
    icon.png
  skrip
    alat.py

Mari kita telaah bagian-bagian pentingnya:

  1. Baris Files.createDirectories(unduhan): setara mkdir -p — membuat folder beserta parent yang belum ada, dan tidak protes bila folder sudah ada; Files.createDirectory yang ketat hanya untuk satu level.
  2. Blok try (Stream<Path> berkas = Files.walk(unduhan)): seluruh berkas dikumpulkan ke daftar dan diurutkan (sorted()) sebelum stream ditutup — urutan pemrosesan menjadi deterministik, dan pemindahan berjalan setelah penelusuran tuntas.
  3. Baris berkas.filter(Files::isRegularFile): Files.walk juga memasukkan direktori ke dalam stream; filter memastikan hanya berkas yang dipindahkan, bukan foldernya.
  4. Baris Files.move(p, tujuanDir.resolve(p.getFileName())): pada sistem berkas yang sama, move adalah operasi rename yang hampir seketika — tidak menyalin satu byte pun; parameter keduanya adalah path tujuan lengkap, bukan folder tujuan.
  5. Baris " ".repeat(kedalaman): indentasi dua spasi per level menghasilkan tampilan pohon yang mudah dibaca — dan karena hasil Files.walk diurutkan dulu, keluarannya selalu sama di setiap eksekusi.

Dari Perintah Shell ke API Java #

Sebagian besar otomasi berkas terjemahan langsung dari kebiasaan shell — dan tiap padanannya di NIO membawa keunggulan tambahan:

TugasPerintah shellAPI Java
Pindahkan / ganti namamvFiles.move(asal, tujuan)
Buat folder berjenjangmkdir -pFiles.createDirectories(path)
HapusrmFiles.delete(path)
Telusuri berjenjangfindFiles.walk(akar)
Cocokkan polaglob shellPathMatcher

Versi Java berjalan lintas platform tanpa utilitas eksternal, menolak nama berkas tidak sah dengan exception jelas, dan — sebagaimana dibahas pada chapter Keamanan Interoperability — tidak membuka celah injeksi perintah karena tidak ada shell yang dieksekusi.

Alat kita kini mampu merapikan pohon direktori setiap kali dipanggil. Pertanyaan berikutnya adalah waktu panggilnya: tugas otomasi jarang sekali — biasanya tiap malam atau tiap pekan. Chapter berikutnya mengatur jadwal itu dari dalam JVM lewat ScheduledExecutorService, serta membandingkannya dengan penjadwal tingkat sistem operasi.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PolaBerkas.java

Unduh berkas
java
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;

public class PolaBerkas {
    public static void main(String[] args) {
        PathMatcher teks = FileSystems.getDefault().getPathMatcher("glob:*.txt");
        PathMatcher gambar = FileSystems.getDefault().getPathMatcher("glob:*.{jpg,png,gif}");
        PathMatcher arsip = FileSystems.getDefault().getPathMatcher("glob:*202?*.zip");

        String[] nama = {
                "laporan.txt", "foto-budi.jpg", "icon.png",
                "cadangan-2025.zip", "data.csv"
        };
        for (String n : nama) {
            Path p = Path.of(n);
            System.out.printf("%-20s teks=%-5b gambar=%-5b arsip=%b%n",
                    n, teks.matches(p), gambar.matches(p), arsip.matches(p));
        }
    }
}

RapihkanUnduhan.java

Unduh berkas
java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class RapihkanUnduhan {
    // Peta ekstensi -> kategori folder tujuan.
    private static final Map<String, String> KATEGORI = Map.of(
            "pdf", "dokumen", "txt", "dokumen", "md", "dokumen",
            "jpg", "gambar", "png", "gambar",
            "json", "data", "csv", "data",
            "py", "skrip");

    public static void main(String[] args) throws IOException {
        Path unduhan = Path.of("unduhan");
        Files.createDirectories(unduhan);

        // Pohon contoh dibuat oleh program agar contoh ini mandiri.
        for (String n : new String[]{"laporan.pdf", "catatan.txt", "tulisan.md",
                "foto-budi.jpg", "icon.png", "penjualan.json", "biaya.csv", "alat.py"}) {
            Files.writeString(unduhan.resolve(n), "contoh", StandardCharsets.UTF_8);
        }

        System.out.println("== sebelum ==");
        tampilkanPohon(unduhan);

        // Kumpulkan dulu seluruh berkas (stream ditutup) baru dipindahkan,
        // agar pohon tidak berubah di tengah penelusuran Files.walk.
        List<Path> daftar;
        try (Stream<Path> berkas = Files.walk(unduhan)) {
            daftar = berkas.filter(Files::isRegularFile).sorted().toList();
        }

        System.out.println("== pindahan ==");
        for (Path p : daftar) {
            String kategori = KATEGORI.get(ekstensi(p));
            if (kategori != null) {
                Path tujuanDir = unduhan.resolve(kategori);
                Files.createDirectories(tujuanDir);
                Files.move(p, tujuanDir.resolve(p.getFileName()));
                System.out.printf("%-16s -> %s/%n", p.getFileName(), kategori);
            }
        }

        System.out.println("== sesudah ==");
        tampilkanPohon(unduhan);
    }

    private static String ekstensi(Path p) {
        String nama = p.getFileName().toString();
        int titik = nama.lastIndexOf('.');
        return titik < 0 ? "" : nama.substring(titik + 1).toLowerCase();
    }

    private static void tampilkanPohon(Path akar) throws IOException {
        try (Stream<Path> semua = Files.walk(akar)) {
            semua.sorted().forEach(p -> {
                int kedalaman = p.equals(akar) ? 0
                        : akar.relativize(p).getNameCount();
                System.out.println("  ".repeat(kedalaman)
                        + (kedalaman == 0 ? "unduhan/" : p.getFileName()));
            });
        }
        System.out.println();
    }
}