サイトの中で、サーバー上にあるものが正になってるコンテンツが怖いので、バックアップをDropboxに上げる仕組みを作ってみました。

[ Ubuntu18 / Node.js v11.2.0 ]

要件は以下の通りです。

  • バックアップ対象のファイルを、現在日時をファイル名にしたzipで固める。
  • そのzipファイルをDropboxの所定のフォルダにアップロードする。
  • cronで毎日実行する。

Dropboxにアップロードするところは、ここではNode.jsで作ります。標準モジュールだけでやります。

Dropboxの準備

Dropbox APIを使うためには、あらかじめDropboxアプリを用意しておく必要があります。

今回はこれ専用にアプリ登録してきました。好きなところにファイルをアップロードしたいので、Permission typeは『Full Dropbox』を選択します。

アプリ管理画面の『Generated access token』からアクセストークンを発行すれば準備完了です。

シェルを作る

Dropboxの準備ができたら、cronから呼び出すシェルを書きます。

バックアップ対象ファイルをzipで固めて、アップロード用スクリプトを呼び出すようにします。

backup.sh
#!/bin/sh
cd `dirname $0`

# 日時でファイル名作成
today=$(date "+%Y%m%d%H%M%S")
zipname=${today}.zip

# 日時のフォルダにバックアップ対象のファイルをコピー
sudo mkdir ${today}
sudo cp /path/to/file1 ${today}/file1
sudo cp -r /path/to/dir1 ${today}/dir1

# zipでまとめる (mオプションで元のファイルは消える)
sudo zip -rm ${zipname} ${today}

# Dropboxにアップロード
sudo node upload_to_dropbox.js ${zipname}
ret=$?
if [ ${ret} -eq 0 ]; then
	exit ${ret};  # リターンコードが0 (正常終了) 以外なら抜ける
fi

# 送信完了したらzip削除
sudo rm ${zipname}

あんまりシェル書いたことないからわからないんですけど、こんなに sudo って書くもんなんでしょうか?

Dropboxにアップロードするやつを作る

次に、シェル中から叩いてる upload_to_dropbox.js を作ります。これは引数でファイル名 (ここでは固めたzipファイル名) を受け取って、当該のファイルをDropboxの所定のフォルダにアップロードするスクリプトになります。

ここでは、渡されたファイル名は backup.sh を実行した時のカレントディレクトリから見たものとしてファイルパスに解決します。

アップロードにはDropbox APIの /files/upload エンドポイントを使います。このエンドポイントは Content-upload フォーマットなので、リクエストヘッダの Dropbox-API-Arg にパラメータを、リクエストボディにファイル本文をセットします。詳しいことはリファレンスを見てください。

upload_to_dropbox.js
const fs = require('fs');
const path = require('path');
const https = require('https');

const token = 'Dropboxのアクセストークン';
const remoteDir = '/path/to/backup';  // アップロード先のフォルダ

// パラメータ確認
if (process.argv.length < 3) {
	console.error('ファイル名が指定されていません。');
	process.exit(1);
}

// 対象ファイルパス取得
const target = process.argv[2];
let filepath;
if (target.startsWith(path.sep)) {
	filepath = target;  // 絶対パスの場合
} else {
	filepath = path.resolve(target);  // 相対パスの場合
}
const filename = filepath.split(path.sep).pop();  // ファイル名のみ取得

// ファイル読み込み
fs.readFile(filepath, { flag: 'r' }, (err, data) => {
	if (err) {
		console.error('ファイル読み込みに失敗しました。', err);
		process.exit(1);
	}

	// Dropboxにアップロード
	const url = 'https://content.dropboxapi.com/2/files/upload';
	const options = {
		method: 'POST',
		headers: {
			'Authorization': 'Bearer '+ token,
			'Content-type': 'application/octet-stream',
			'Dropbox-API-Arg': JSON.stringify({
				path: remoteDir + '/' + filename,
				mode: 'overwrite',
			}),
		},
	}
	const req = https.request(url, options, (res) => {
		res.setEncoding('utf8');
		res.on('data', (chunk) => {
			// ステータスコードが200以外なら失敗
			if (res.statusCode == 200) {
				process.exit(0);
			} else {
				console.error('Dropboxからエラーが返ってきました。', chunk);
				process.exit(1);
			}
		});
	});
	req.write(data);
	req.end();
	req.on('error', (e) => {
		// リクエスト失敗時
		console.error('ファイル送信に失敗しました。', e.message);
		process.exit(1);
	});
});

とりあえずこれで動きました。

cronで毎日実行させる

あとは、最初に作ったシェルをcronで毎日実行させます。ここでは毎日午前3時5分に ubuntu ユーザーで動くようにしました。エラー時は tmp にログを出すようにしました。

/etc/cron.d/my_site_backup
SHELL=/bin/bash
5 3 * * * ubuntu sh /path/to/backup.sh 2>> /tmp/my_site_backup_err.log

この記事の作りだとエラー時にゴミzipが残るのが微妙ですが、今のところ失敗したことはあんまり無いので、たぶん大丈夫だと思います。あとシェルの sudo 乱舞が気になりますが追い追い。

以上です。

参考にさせていただいた記事