概要
Javaでディレクトリの再帰的な探索を行う処理は、Java 1.7で導入されたFiles.walkFileTree
メソッドを使うと簡単に実装できます。
また、探索したそれぞれのファイル、ディレクトリに対して任意の処理を行うにはFileVisitor
インターフェースを実装したクラスを使います。
walkFileTree
第1引数のstartには探索の起点となるpath、第2引数にFileVisitorのインスタンスを渡します。
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
と付いているようにどのタイミングでコールバックされるかの違いになります。
コード
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を、ディレクトリの場合は空文字を格納します。
コード
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 + "]";
}
}
デモ
以下のように同じディレクトリ構成のdir1
とdir2
というディレクトリを作成しました。ファイル名もファイルの内容も同じですが、作成日時のタイムスタンプは異なります。
このdir1
とdir2
をデモコードでそれぞれ探索してファイル、ディレクトリの一覧とファイルのチェックサムを取得して同じ構造かどうかをチェックします。
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
実行する
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