LoginSignup
3
1

More than 5 years have passed since last update.

Files.walkFileTreeでディレクトリを再帰的に探索するデモコード

Last updated at Posted at 2019-02-16

概要

Javaでディレクトリの再帰的な探索を行う処理は、Java 1.7で導入されたFiles.walkFileTreeメソッドを使うと簡単に実装できます。
また、探索したそれぞれのファイル、ディレクトリに対して任意の処理を行うにはFileVisitorインターフェースを実装したクラスを使います。

walkFileTree

第1引数のstartには探索の起点となるpath、第2引数にFileVisitorのインスタンスを渡します。

walkFileTree
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException {
  // ...
}

環境

  • Windows 10 Professional
  • OpenJDK 11.0.2

参考

再帰的な処理を行うデモコード

このデモコードでは、探索する起点パス以下のファイル、ディレクトリの相対パス(起点パスからの)を取得しListにまとめます。またファイルの場合はMD5でチェックサムを計算します。

ComputeFileChecksumVisitor

SimpleFileVisitorクラスはFileVisitorインターフェースを実装した基本的なVisitorクラスです。このクラスを継承してファイル、ディレクトリに対する処理を実装します。

ファイルに対する操作

オーバーライドしたvisitFileは探索したファイル毎にコールバックされるメソッドで、ここでファイルの相対パス取得とチェックサムを計算する処理を実装しています。

ディレクトリに対する操作

preVisitDirectoryメソッドは探索したディレクトリ毎にコールバックされるメソッドです。ディレクトリではチェックサムの計算を行わないので相対パスだけ取得しています。
なお、ディレクトリに対する操作では他にpostVisitDirectory​というメソッドも用意されていますが、メソッド名にpre,postと付いているようにどのタイミングでコールバックされるかの違いになります。

コード

ComputeFileChecksumVisitor
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

public class ComputeFileChecksumVisitor extends SimpleFileVisitor<Path> {

    private final Path start;
    private final String hashAlg;
    private final List<FileItem> items = new ArrayList<>();

    public ComputeFileChecksumVisitor(Path start, String hashAlg) {
        this.start = start;
        this.hashAlg = hashAlg;
    }

    public List<FileItem> getResult() {
        return items;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        if (!dir.equals(start)) {
            FileItem item = new FileItem(relativePath(dir), "");
            items.add(item);
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        FileItem item = new FileItem(relativePath(file), checksum(file));
        items.add(item);
        return FileVisitResult.CONTINUE;
    }

    private Path relativePath(Path path) {
        if (path.startsWith(start)) {
            return path.subpath(start.getNameCount(), path.getNameCount());
        }
        throw new RuntimeException();
    }

    private String checksum(Path path) throws IOException {
        MessageDigest digest = null;
        try {
            digest = MessageDigest.getInstance(hashAlg);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        try (InputStream input = Files.newInputStream(path);
             DigestInputStream dInput = new DigestInputStream(input, digest)) {
            while (dInput.read() != -1) {}
        }
        return toHex(digest.digest());
    }

    private String toHex(byte[] bytes) {
        StringBuilder builder = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            builder.append(String.format("%02x", b));
            //builder.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
        }
        return builder.toString();
    }

}

FileItem

ComputeFileChecksumVisitorで探索したファイル、ディレクトリの情報を格納するクラスです。
pathフィールドは探索したファイル、ディレクトリの相対パス(起点パスからの)を格納します。
checksumフィールドは、探索したファイルのchecksumを、ディレクトリの場合は空文字を格納します。

コード

FileItem
import java.nio.file.Path;

public class FileItem implements Comparable<FileItem> {
    private Path path;
    private String checksum;

    public FileItem(Path path, String checksum) {
        this.path = path;
        this.checksum = checksum;
    }

    public Path getPath() {
        return path;
    }
    public String getChecksum() {
        return checksum;
    }

    @Override
    public int compareTo(FileItem o) {
        return this.compareTo(o);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((checksum == null) ? 0 : checksum.hashCode());
        result = prime * result + ((path == null) ? 0 : path.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FileItem other = (FileItem) obj;
        if (checksum == null) {
            if (other.checksum != null)
                return false;
        } else if (!checksum.equals(other.checksum))
            return false;
        if (path == null) {
            if (other.path != null)
                return false;
        } else if (!path.equals(other.path))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "FileItem [path=" + path + ", checksum=" + checksum + "]";
    }

}

デモ

以下のように同じディレクトリ構成のdir1dir2というディレクトリを作成しました。ファイル名もファイルの内容も同じですが、作成日時のタイムスタンプは異なります。
このdir1dir2をデモコードでそれぞれ探索してファイル、ディレクトリの一覧とファイルのチェックサムを取得して同じ構造かどうかをチェックします。

D:var
├─dir1
│  │  test1.txt
│  │  test2.txt
│  │
│  ├─aaa
│  │  └─ddd
│  ├─bbb
│  │      test3.txt
│  │
│  └─ccc
│          test4.txt
│
└─dir2
    │  test1.txt
    │  test2.txt
    │
    ├─aaa
    │  └─ddd
    ├─bbb
    │      test3.txt
    │
    └─ccc
            test4.txt

実行する

Demo
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class Demo {

    public static void main(String[] args) throws Exception {

        Path dir1 = Paths.get("D:", "var", "dir1");
        ComputeFileChecksumVisitor dir1Visit = new ComputeFileChecksumVisitor(dir1, "MD5");
        Files.walkFileTree(dir1, dir1Visit);

        Path dir2 = Paths.get("D:", "var", "dir2");
        ComputeFileChecksumVisitor dir2Visit = new ComputeFileChecksumVisitor(dir2, "MD5");
        Files.walkFileTree(dir2, dir2Visit);

        List<FileItem> dir1Files = dir1Visit.getResult();
        System.out.println("Root : " + dir1.toString());
        dir1Files.forEach(System.out::println);

        List<FileItem> dir2Files = dir2Visit.getResult();
        System.out.println("Root : " + dir2.toString());
        dir2Files.forEach(System.out::println);

        if (dir1Files.equals(dir2Files)) {
            System.out.println("equal");
        } else {
            System.out.println("not equal");
        }
    }

}

実行した結果

Root : D:\var\dir1
FileItem [path=aaa, hash=]
FileItem [path=aaa\ddd, hash=]
FileItem [path=bbb, hash=]
FileItem [path=bbb\test3.txt, hash=ed6e956a3d549303751e3238ab04bb46]
FileItem [path=ccc, hash=]
FileItem [path=ccc\test4.txt, hash=2c97af7af48689fc67a2700d9f051af6]
FileItem [path=test1.txt, hash=ac6a2aaa9317ef1f007c092c6a5fd75e]
FileItem [path=test2.txt, hash=811ad90a8dafc585bb64b23b6200969e]
Root : D:\var\dir2
FileItem [path=aaa, hash=]
FileItem [path=aaa\ddd, hash=]
FileItem [path=bbb, hash=]
FileItem [path=bbb\test3.txt, hash=ed6e956a3d549303751e3238ab04bb46]
FileItem [path=ccc, hash=]
FileItem [path=ccc\test4.txt, hash=2c97af7af48689fc67a2700d9f051af6]
FileItem [path=test1.txt, hash=ac6a2aaa9317ef1f007c092c6a5fd75e]
FileItem [path=test2.txt, hash=811ad90a8dafc585bb64b23b6200969e]
equal
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1