argius note

プログラミング関連

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に差し替えると上手くいくようになります。ApacheAPIでは、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);
        }
    }

}