Javaでアーカイバ(java.util.zip関連)
java.util.zipあたりのAPIで簡易アーカイバを作ろうと以前から思ってたんですが、その時は2,3つまづく箇所があって挫折していました。今回なんとなく再開したら完成したのでメモしておきます。
発端
バックアップを単純なオペレーションで出来るようにしたいと思っていました。コピー→リネームでも面倒なくらいなので、1手順でできるようにしたかったのです。
ファイルまたはディレクトリを指定して実行すると、「タイムスタンプ+ファイル名」でバックアップファイルが作成できるようなツールにしようと思いました。
模索
適当にJavadocやサンプルから作ったところ、一見できたように見えました。が、一覧は見られるけど、展開しようとすると「書庫が壊れてます」とか出ます。あと、日本語のエントリ名が化けます。
エントリ名のパス区切り文字を'\'から'/'にしたら、「書庫が壊れ」るのが解消。
日本語のエントリが化けるのは、標準APIのはUTF-8固定だからダメで、"java.util.zip.*"のAPIを"ant.jar"内の"org.apache.tools.zip.*"の対応するAPIに差し替えると上手くいくようになります。ApacheのAPIでは、ZipOutputStreamでエンコードが指定できるようになっていて、デフォルトはOSのデフォルトエンコーディングになるようです。
参照
実装
文字コードとbatの点でWindows限定です。
こんな感じのbatファイルを作って、SendToに直接/間接(ショートカット)に突っ込んでおけば、右クリック→送る→"backupper"でアーカイブが作成できます。DOS窓が開くようになってますが、ちょっと手を入れればGUI版(javaw)もできるでしょう。
追記(2008.10.22):パスが長すぎると上手くいかないことがあるので、引数1を %1 から "%*" に変更してみました。副作用として、複数ファイルを指定するとエラーになります。元のバージョンでは、複数ファイルを指定すると、いずれか1つしか対象にならない問題がありました。
追記(2008.11.12):"%*" じゃなくて、クォートなしの %* じゃないとダメみたいです。
rem backupper.bat @echo off java -cp D:\workspace\Backupper\bin;C:\javalib\ant.jar tool.Backup %* D:/backup pause
Javaソースコード。"org.apache.tools.zip"の2つのインポートを削除すると、標準APIのほうが使用されます。それでも動きますが、前述の通り日本語名が化けます。
package tool; import java.io.*; import java.text.*; import java.util.*; import java.util.zip.*; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipOutputStream; /** * バックアップコマンド。 */ public final class Backup { private String rootpath; private File srcfile; private File dstdir; /** * Backupの生成。 * @param src 対象ファイル * @param dstdir 出力先ディレクトリ * @throws IOException I/Oエラー */ public Backup(File src, File dstdir) throws IOException { if (!src.exists()) { throw new FileNotFoundException("ファイルが存在しない:" + src); } if (!dstdir.isDirectory()) { throw new FileNotFoundException("出力ディレクトリが存在しない:" + dstdir); } String absolutePath = src.getAbsolutePath(); int length = absolutePath.length() - src.getName().length(); this.rootpath = absolutePath.substring(0, length).replace('\\', '/'); this.srcfile = src; this.dstdir = dstdir; } /** * バックアップを実行する。 * @throws IOException I/Oエラー */ public void execute() throws IOException { output("[[ backupper ]]"); output("出力しています..."); DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss-"); String filename = df.format(new Date()) + srcfile.getName() + ".zip"; File newfile = new File(dstdir, filename); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newfile)); try { addEntry(zos, srcfile); } finally { zos.close(); } output("出力しました:" + newfile); } /** * エントリを追加する。 * @param zos ZipOutputStream * @param file ファイル * @throws IOException I/Oエラー */ private void addEntry(ZipOutputStream zos, File file) throws IOException { if (file.isDirectory()) { // ディレクトリ for (File f : file.listFiles()) { addEntry(zos, f); } // ディレクトリをエントリに含めたい場合は以下を実行 // ZipEntry entry = new ZipEntry(getEntryName(file, true)); // zos.putNextEntry(entry); // entry.setMethod(java.util.zip.ZipEntry.STORED); // entry.setCrc(0); // entry.setSize(0); // entry.setCompressedSize(0); // entry.setTime(file.lastModified()); // zos.closeEntry(); } else { // ファイル ZipEntry entry = new ZipEntry(getEntryName(file, false)); zos.putNextEntry(entry); CheckedInputStream is = new CheckedInputStream(new FileInputStream(file), new CRC32()); try { int totalSize = 0; byte[] buffer = new byte[8192]; int length = 0; while ((length = is.read(buffer)) >= 0) { zos.write(buffer, 0, length); totalSize += length; } entry.setCrc(is.getChecksum().getValue()); entry.setSize(totalSize); entry.setCompressedSize(totalSize); entry.setTime(file.lastModified()); } finally { is.close(); } zos.closeEntry(); } } /** * エントリ名の取得。 * @param file ファイル * @param isDirectory ディレクトリの場合は <code>true</code> * @return エントリ名 */ private String getEntryName(File file, boolean isDirectory) { String entryName; String absolutePath = file.getAbsolutePath().replace('\\', '/'); if (absolutePath.startsWith(rootpath)) { entryName = absolutePath.substring(rootpath.length()); if (file.isDirectory() && !entryName.endsWith(String.valueOf('/'))) { entryName += '/'; } } else { entryName = file.getName(); } return entryName; } /** * 出力。 * @param object オブジェクト */ private static void output(Object object) { // ここを何かして出力先を変えたりする System.out.println(object); } /** * 処理の開始。 * @param args 実行時引数 */ public static void main(final String... args) { int status; try { if (args.length < 2) { System.out.println("USAGE: backupper [対象ディレクトリ/ファイル] [出力先ディレクトリ]"); } else { File srcfile = new File(args[0]); File dstdir = new File(args[1]); new Backup(srcfile, dstdir).execute(); } status = 0; } catch (IOException ex) { System.out.println(ex.getMessage()); status = 1; } catch (Throwable th) { th.printStackTrace(); status = 255; } if (status > 0) { System.exit(status); } } }