Google DriveをAPI経由でまじめに同期する
目次
経緯
今までgoogle-drive-ocamlfuseという素晴らしいツールとShell ScriptでGoogle Driveの同期を行っていた。
いつの頃からかエラーを吐いて同期できなくなっていた。
FUSEを使っているためFile SystemとしてGoogle Driveをマウントできる素晴らしいツールだったのだがエラーになったときにOSのパフォーマンスに直結してしまうため、とても便利な反面リスクがある。
そこで、Google Drive APIを使ってちゃんとソースファイルの同期を取ろうと考えたわけ。
構成図
graph TD
service1((Google Drive API))
resource1[(Blog Resources)]
resource2[(HTML Resources)]
script1[[Generate List]]
script2[[Download Files]]
script3[[Hexo Generate]]
file1[/File List/]
script1==>|1.Request|service1
service1-->|2.Response|script1
script1==>|3.Create File List|file1
file1-->|4.Open File List|script2
script2==>|5.Request|service1
service1-->|6.Response|script2
script2==>|7.Save Blog Resource|resource1
resource1-->|8.hexo g|script3
script3==>|9.Deploy|resource2
style service1 fill:#006400,color:#eee
style script1 fill:#00cc00,stroke:#666,color:#e6e
style script2 fill:#00cc00,stroke:#666,color:#f41
style script3 fill:#00cc00,stroke:#666
style resource1 fill:#F0E68C,stroke:#666
style resource2 fill:#10E68C,stroke:#666
style file1 fill:#f08643,stroke:#666
linkStyle 0 stroke:#666
linkStyle 1 stroke:#f08643
linkStyle 2 stroke:#f08643
linkStyle 3 stroke:#f08643
linkStyle 4 stroke:#666
linkStyle 5 stroke:#10E68C
linkStyle 6 stroke:#10E68C
linkStyle 7 stroke:#10E68C
linkStyle 8 stroke:#10E68C
Generate ListとDownload FilesってところをNode.jsで書いてみよう。
仕組みは見ての通りで、Google Drive APIを通じてMarkdownファイルのリストをまず作る。
そのリストに書かれたファイルを1個ずつダウンロードしていくというもの。
構築
ここでは、既に存在するGoogle Drive APIのサンプルスクリプトに追記する形で書いてみた。
今回npmからgoogleapis@^39.2.0をインストールしている。
Generate List
let listFiles=(auth, folderpath, savepath, filelist) => {
//Google Drive API
const drive = google.drive({version: 'v3', auth});
//query setting for parent directories
let folderqueryspec=null;
folderpath.map((folder)=>{
if(!folderqueryspec)folderqueryspec="name='"+folder+"'";
else folderqueryspec+=" or name='"+folder+"'";
});
//delete file list
fs.unlink(filelist, (err) => {
if(err)console.log(err);
});
//get parent directory
drive.files.list({
q: "trashed=false and mimeType = 'application/vnd.google-apps.folder' and ("+folderqueryspec+")",
pageSize: 5,
fields: 'nextPageToken, files(id, name, trashed, parents)',
}, (err, res) => {
if (err) return console.log('The API returned an error(Folder was not found): ' + err);
//query setting for source directories
const parentFolders = res.data.files;
let parentfolderqueryspec;
parentFolders.map((folder)=>{
if(!parentfolderqueryspec)parentfolderqueryspec="'"+folder.id+"' in parents";
else parentfolderqueryspec+=" or '"+folder.id+"' in parents";
});
//get source directories
drive.files.list({
q: "trashed=false and mimeType = 'application/vnd.google-apps.folder' and ("+parentfolderqueryspec+")",
fields: 'nextPageToken, files(id, name, trashed, parents, mimeType)',
}, (err, res) => {
if (err) return console.log('The API returned an error: ' + err);
parentfolders = res.data.files;
if (parentfolders.length) {
parentfolders.map((folder) => {
//get file object under the source directories
drive.files.list({
q: "trashed=false and (mimeType contains 'text/' or mimeType contains 'image/') and '"+folder.id+"' in parents",
pageSize: 1000,
fields: 'nextPageToken, files(id, name, trashed, parents, mimeType, copyRequiresWriterPermission, webContentLink)',
}, (err, res) => {
//save a file list
let markdownfiles=res.data.files;
markdownfiles.map((file) => {
fs.appendFile(filelist, file.name + " " + file.id + " " + file.mimeType + " " + folder.name + " " + folder.id + "\n", (err) => {
if(err)console.log(err);
});
});
});
});
} else {
console.log('No files found.');
}
});
});
}
HexoはMarkdownファイルや画像なんかのコンテンツを一つのディレクトリで保管している。
これをGoogle Driveにも用意して、そのファイルのリストを作成。
例えば、Webサーバの以下のディレクトリにソースが保存されているとする。
/var/www/hexo/source
├─/_drafs
├─/_posts
└─/assets
そしてGoogle Driveにも以下のディレクトリを作成しておく。
マイドライブ/hexo/source
├─/_drafs
├─/_posts
└─/assets
上記、listFiles()関数の引数folderpathってところに、Google Driveのパスを入れておく。
また、ダウンロードしたファイルの保存先も以下のようにしておく。
最後にはファイルのリストの保存先を。
let folderpath=['hexo','source']; //Google Drive folder
let savepath='/var/www/hexo/source'; //Hexo web sources
let filelist='/var/www/hexo/tmp/sourcefiles.list'; //file list
ファイルのリストの保存形式は以下の通り
ファイル名 ファイルID MIMETYPE 親フォルダ名 親フォルダID
なお同期するファイル数は1,000ファイルまでになっている。
Download Files
こちらのスクリプトはファイルのリストを開いて1行ずつファイルIDを取り出して、ファイルをダウンロードしていく。
こちらもAPIのサンプルスクリプトに追記する形にしている。
let deleteAllFiles=(dir)=>{
const filenames = fs.readdirSync(dir);
filenames.forEach((filename) => {
let excluded=false;
excludefiles.forEach((exfile)=>{
if(exfile===filename){
excluded=true;
}
});
if(!excluded){
const fullPath = path.join(dir, filename);
const stats = fs.statSync(fullPath);
if (stats.isFile()) {
console.log(fullPath);
fs.unlink(fullPath);
} else if (stats.isDirectory()) {
deleteAllFiles(fullPath);
}
}
});
}
let sleep=(waitSec) => {
return new Promise(function (resolve) {
setTimeout(function() { resolve() }, waitSec);
});
}
let listFiles=async (auth) => {
deleteAllFiles(savepath);
const rl=readline.createInterface({
input: fs.createReadStream(filelist),
//output: process.stdout,
crlfDelay: Infinity,
});
rl.on('line', async (line)=>{
let attributes=line.split(' ');
let fileobj={
id: attributes[1],
name: attributes[0],
mimeType: attributes[2],
folderName: attributes[3],
folderId: attributes[4],
}
await downloadFile(auth, fileobj);
});
}
let downloadFile=async (auth, file) =>{
const drive = google.drive({version: 'v3', auth});
let downloadfilename = file.name;
let downloadfilepath = savepath+'/'+file.folderName+'/'+file.name;
let downloadfileid = file.id;
let downloadmimetype = file.mimeType;
let dest = fs.createWriteStream(downloadfilepath);
await drive.files.get({
fileId: downloadfileid,
alt: 'media',
}, {responseType: 'stream'}, async (err,res) => {
try {
await res.data.on('end', () => {
console.log(downloadfilepath+' Done');
if(downloadfilename==gosync){
drive.files.delete({
fileId: downloadfileid,
});
fs.unlink(downloadfilepath, (err) => {
if(err){throw err;}
});
fs.writeFile(uploadfilepath, '', (err)=>{
if(err){throw err;}
});
}
}).on('error', (err) => {
console.log('Error during download', err);
}).pipe(dest);
}catch(err){
console.log('err: wait a while and try once again ' + downloadfilename);
sleep(ewaittimer).then(()=>{
downloadFile(auth, file);
});
}
});
}
まずは/var/www/hexo/source内のファイルを全部削除してからダウンロードし直すことで完全同期を試みている。
で、やっぱり消してほしくないものもあるかと思うので、そのときのために除外リストをexcludefilesという配列にファイル名とかで入れておく。
また同期を行うトリガーにするファイルを用意したそれがgosync。このファイルがあればダウンロードとhexo generateを行うこととする。
そして別途hexo generateを行うスクリプトを用意していて、そいつへの起動トリガーとなるためのuploadfileというのを作成することにした。
最後に今回一番面倒くさかったのが、Google Drive APIの仕様。
1秒間に10回以上リクエストされたらそれ以降のResponseにエラーを返すことになっているため、ファイルが多いと一回の.getメソッドで成功しないことがほとんど。
1ファイルごとの同期処理でもエラーになるため、エラーになったファイルは時間をおいて再度実行することに。非同期処理の醍醐味。
const excludefiles=['dont-remove-me.md','remain-me-all-the-way.md'];
const gosync='syncsyncsync.txt';
const uploadfilepath='/var/www/hexo/upload.flg';
const ewaittimer=1000;
ここでは紹介を省くが、uploadfileを見つけてhexo gするスクリプトは別に用意する。
これで2つのスクリプト
- savelist.js
- downloadfile.js
なんてファイルとして保存しておく。
最後に
上記jsファイルを定期的に実行するように仕込んでおけば、勝手に同期が始まるはず。
ファイルが保存されているフォルダは深くするとどうなるかは未確認。