Google DriveをAPI経由でまじめに同期する

10/14

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ファイルを定期的に実行するように仕込んでおけば、勝手に同期が始まるはず。

ファイルが保存されているフォルダは深くするとどうなるかは未確認。


コメント: