バックアップスクリプト

サーバーを人為的ミスにより壊したときに、バックアップの必要性を感じたので、スクリプトを作りました。よろしかったらどうぞ。以下のプログラムを dar-backup.js と保存して、ご利用ください。実行するには、darとRhinojre(Java実行環境)が必要です。
(使用される際は、プログラムの内容を理解して使ってね)

2007/10/28追記、darを使うより、rdiff-backupを使った方がいいことを知りました。

/* 
 本ソースコードのライセンスはBSDライセンスです。
 
 darを使って、バックアップを行う、Rhino(JavaScript)のプログラムです。
 
 darを使って、特定のフォルダのバックアップを行うので、
 rsyncなどで、別のメディアやサーバーにコピーしてください。
 人為的ミスから守るためにも、RAID以外に、毎時および毎日のバックアップは必須です。
 
 (Javaのフォルダ)/bin/java -jar (Rhinoのフォルダ)/js.jar dar-backup.js hourly
 などで実行できます。hourly の部分をdailyにすると、毎日のバックアップになります。
 このスクリプト自体はcronで呼び出してください。
 
 hourly は 48〜96時間分のデータを保存します。
 午前0時に、フルバックアップを行い、毎時、差分バックアップを行います。
 
 daily は 2〜3週間分のデータを保存します。
 毎週日曜日に、フルバックアップを行い、毎日、差分バックアップを行います。
 
 バックアップを復元するには、以下の手順をとります。
 dar --extract フルバックアップのファイル名(末尾の.1.darは不要)
 dar --extract 差分バックアップのファイル名(末尾の.1.darは不要)
*/

/** バックアップ対象となるフォルダ(スラッシュで終えてください)*/
var BACKUP_TARGET = "/var/backup/"
/** バックアップデータの保存先フォルダ(スラッシュで終えてください)*/
var BACKUP_TO = "/root/backup/"
/** 最大何世代分保存するか。もし3ならば、48〜96時間、または、2〜3週間 */
var MAX_SAVE_GENERATION = 3;
/** darのパス */
var DAR_EXEC = "/usr/bin/dar"
//var DAR_EXEC = "/Program Files/dar64-2.3.6-i386-windows/dar.exe";
/** Windowsならtrue、UNIXならfalse */
var IS_CYGWIN = false;
/** ログのファイル名 */
var LOG_FILE = "/var/log/dar-backup.log";
/** Sendmailのパス */
var SENDMAIL_PATH = "/usr/sbin/sendmail";
//var SENDMAIL_PATH = "/bin/sendmail.exe";
/** エラーメールの From */
var MAIL_FROM = "root@localhost.jp";
/** エラーメールの送り先 */
var MAIL_TO = "hoge@me.jp";

/** 1日分のミリセカンド */
var ONE_DAY_MS = 24 * 60 * 60 * 1000;

importPackage(java.lang)
importPackage(java.io)
main(arguments)

/** メインとなる処理 */
function main(arguments) {
	// 第1引数はバックアップの種別
	var backupType = arguments[0];
	
	// バックアップ先フォルダ
	var folderName = null;
	var diffFileName = null;
	if(backupType == "hourly") {
		// 毎時バックアップの場合
		folderName = getToday();
		diffFileName = getHour();
	} else if(backupType == "daily") {
		// 毎日バックアップの場合
		folderName = getThisSunday();
		diffFileName = getToday();
	}
	if(folderName == null) return;
	
	// バックアップ元のフォルダの存在確認
	if(!new File(BACKUP_TARGET).exists()) {
		log("ERROR", "Backup target is not exists : " + BACKUP_TARGET);
		System.exit(3);
	}
	
	// バックアップ先のフォルダ作成
	var folderStr = BACKUP_TO + backupType + "/" + folderName + "/";
	new File(folderStr).mkdirs();
	
	// darを実行
	execDar(folderStr, diffFileName);
	
	// 古いフォルダを削除する
	delOldFolders(backupType);
}

/** darを実行 */
function execDar(folderStr, diffFileName) {
	// フルバックアップがあるかどうか調べ、なければ、フルバックアップ
	var fullBackup = new File(folderStr + "full.1.dar");
	var backupFile = null, result = 0;
	if(fullBackup.exists()) {
		// 差分バックアップ
		backupFile = toCygwinStyle(folderStr + diffFileName);
		result = runProcess([
			DAR_EXEC, 
			"--create", backupFile, 
			"--ref", toCygwinStyle(folderStr + "full"), 
			"--fs-root", toCygwinStyle(BACKUP_TARGET), 
			"--gzip", "--noconf", "-Q", "--no-warn",
			"--slice", "2G"]);
	} else {
		// フルバックアップ
		backupFile = toCygwinStyle(folderStr + "full");
		result = runProcess([
			DAR_EXEC, 
			"--create", backupFile, 
			"--fs-root", toCygwinStyle(BACKUP_TARGET), 
			"--gzip", "--noconf", "-Q", "--no-warn",
			"--slice", "2G"]);
	}
	if(result != 0) {
		log("ERROR", "Failed to create backup file (" + result + ") : " + backupFile);
		System.exit(1);
	}
	
	// 正しく書き込めたかテストする(ディスクが物理的に破損しているとここでエラーになります)
	result = runProcess([
		DAR_EXEC,
		"--test", backupFile,
		"--noconf", "-Q"]);
	if(result != 0) {
		log("ERROR", "Failed at backup file read test (" + result + ") : " + backupFile);
		System.exit(2);
	}
	
	// バックアップ成功をログに出力
	log("INFO", "Backup success : " + backupFile);
}

/** ログの出力 */
function log(category, message) {
	// ログファイルに書き出し
	var logStr = getMillisecond() + " " + category + " - " + message;
	var out = new PrintWriter(new FileWriter(LOG_FILE, true));
	out.println(logStr);
	out.close();
	
	// category != "INFO" ならメールする。
	if(category != "INFO") {
		if(new File(SENDMAIL_PATH).exists()) {
			// メールの文面
			var mail = "From: " + MAIL_FROM + "\n" +
				"To: " + MAIL_TO + "\n" + 
				"Subject: Error at dar-backup.js\n" +
				"\n" +
				logStr + "\n";
			
			// sendmailにて送信
			var process = Runtime.getRuntime().exec([SENDMAIL_PATH, "-t"]);
			var outStream = new OutputStreamWriter(process.getOutputStream());
			outStream.write(mail);
			outStream.close();
			var inStream = process.getInputStream();
			while(inStream.read() != -1);
			inStream.close();
			process.waitFor();
		}
	}
}

/** 古いフォルダを削除する */
function delOldFolders(backupType) {
	var listFolder = new File(BACKUP_TO + backupType);
	var list = listFolder.listFiles();
	for(var i = 0; i < list.length; i++) {
		// 年-月-日 の形式にマッチするかどうか?
		var delFolder = list[i];
		var result = delFolder.getName().match("(\\d+)-(\\d+)-(\\d+)");
		if(result == null || result.length != 4) continue;
		// Dateオブジェクトに変換
		var folderDate = new Date(result[1], result[2] - 1, result[3]);
		
		// 削除するかどうか判定し、削除実行
		var diff = new Date().getTime() - folderDate.getTime();
		if(backupType == "hourly") {
			// 毎時
			if(diff >= MAX_SAVE_GENERATION * ONE_DAY_MS) {
				// deleteが予約語につき、特殊なメソッド呼び出し。delFolder.delete()相当。
				delFolder["delete"](); 
			}
		} else if(backupType == "daily") {
			// 毎日
			if(diff >= MAX_SAVE_GENERATION * 7 * ONE_DAY_MS) {
				// deleteが予約語につき、特殊なメソッド呼び出し。delFolder.delete()相当。
				delFolder["delete"]();
			}
		}
	}
}

/** Cygwinの場合、頭に /cygdrive/c をつける */
function toCygwinStyle(str) {
	if(IS_CYGWIN) {
		return "/cygdrive/c" + str;
	} else {
		return str;
	}
}

/** 外部プロセスの実行 */
function runProcess(args) {
	var process = Runtime.getRuntime().exec(args);
	var inStream = process.getInputStream();
	while(inStream.read() != -1);
	inStream.close();
	return process.waitFor();
}

/** ミリセカンド単位の日付 */
function getMillisecond() {
	var d = new Date();
	return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " " 
		+ d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() + "," + d.getMilliseconds();
}

/** 今何時? */
function getHour() {
	var d = new Date();
	return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + "-" + d.getHours();
}

/** 今日の日付を返す */
function getToday() {
	var d = new Date();
	return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
}

/** 今週の日曜日の日付を返す */
function getThisSunday() {
	var d = toSunday(new Date());
	return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
}

/** 日曜日に変換 */
function toSunday(d) {
	d.setDate(d.getDate() - d.getDay());
	return d;
}