ジャンル不定の日記です。

Google Drive API使えた

先程はGoogle Developer Consoleで認証情報が設定できなくて後日にしようと思ってたが、Googleさん治ってたんで、Google Drive APIを使ったバックアップを導入した。
APIは、怪しいプログラムをインストールするよりも自分で作っちゃおう。って考えたので、慣れたPerl使った。


導入手順は、
  1. Google Developer Consoleでプロジェクトを作成。
  2. Drive APIを有効にして他は無効に。
  3. OAuth2.0クライアントIDを作成。(種類は「その他」で作った)
  4. "code"の取得。
  5. "refresh_token"の取得。
さっきできなくてハマったが、DNS関連だと思うがタイミングが悪いと3.のクライアントIDの作成ができない。

3.でクライアントIDを作成すると"クライアントID"と"クライアント シークレット"が表示されるが、両方共プログラムで使うのでメモっとく。

4.の"code"は、5.の"refresh_token"を取得するために必要でブラウザでURLにアクセスしてプログラムを許可すると表示される。
https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/drive&response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=[クライアント ID]
↑接続するURLはこんな感じ。
scopeはプログラムに与える権限で、"https://www.googleapis.com/auth/drive"はGoogle Driveの全権限。リードオンリーとかもできる。
response_typeは、"code"の場合はredirect_uriで指定したURLの末尾にクエリでつけられてリダイレクトされる。
クライアントプログラムを作るのでリダイレクト用のページなど存在しないが、その場合は"urn:ietf:wg:oauth:2.0:oob"を指定するとGoogleのページでコードを表示してくれる。
取得できたコードは5.の"refresh_token"を取得するために1回だけ必要。

5.の"refresh_token"の取得は、POSTでリクエストしないといけないので、curlコマンドを使って、
curl -d redirect_uri=urn:ietf:wg:oauth:2.0:oob -d client_id=[クライアント ID] -d client_secret=[クライアント シークレット] -d grant_type=authorization_code -d code=[ブラウザで取得したコード] https://accounts.google.com/o/oauth2/token
こんな感じでリクエストを投げるとJSON形式で返ってくる。
"refresh_token"だけが必要。


で、ファイルPATH(又はカレントディレクトリのファイル名)をパラメータで渡すとGoogle DriveにアップロードするPerlスクリプトを作った。
#!/usr/bin/perl
###
$userId='Google ID';
$client_id='クライアントID';
$client_secret='クライアント シークレット';
$refresh_token='refresh_token';

$Folder='フォルダID';

$Type{'gz'}='application/gzip';
###
###
use LWP::UserAgent;
use JSON;
###
my $res=&HttpRequest('POST',"https://www.googleapis.com/oauth2/v3/token","client_id=$client_id&client_secret=$client_secret&refresh_token=$refresh_token&grant_type=refresh_token");
our ($access_token)=$res=~/"access_token": "([^"]+)"/;
die if(!$access_token);

foreach(@ARGV){
    my ($dir,$name)=$_=~/^(.*?)\/?([^\/]+)$/;
    my ($ext)=$name=~/\.([^\.]+)$/;
    my $type=$Type{$ext}||'application/octet-stream';
    my $old=JSON::from_json(&HttpRequest('GET',"https://www.googleapis.com/drive/v2/files?q=@{[&UrlEncode('\''.$Folder.'\' in parents and title=\''.$name.'\'')]}"));
    my $new=JSON::from_json(&FileUpload($dir,$name,$type));
    if($new->{'id'}){
        foreach(@{$old->{'items'}}){
            &HttpRequest('DELETE',"https://www.googleapis.com/drive/v2/files/$_->{'id'}");
        }
    }
    sleep(1);
}
###
sub HttpRequest{
    my $method=shift;
    my $url=shift;
    my $body=shift;
    my $ua=LWP::UserAgent->new;
    my $req;
    if($method eq 'GET' || $method eq 'DELETE'){
        $req=HTTP::Request->new($method=>$url);
    }else{
        $req=HTTP::Request->new(POST=>$url);
        $req->content_type('application/x-www-form-urlencoded');
        $req->header(Content_length=>0) if(!$body);
        $req->content($body);
    }
    $req->header(Authorization=>"OAuth $access_token") if($access_token);
    my $res=$ua->request($req);
    return($res->content);
}
###
sub FileUpload{
    my $dir=shift;
    my $name=shift;
    my $type=shift;
    my($file,$count,$body);
    open(FILE,"$dir/$name");
    while(<FILE>){
        $file.=$_;
    }
    close(FILE);
    while($file=~/\-\-$name$count/){
        $count++;
    }
    $body="--$name$count\nContent-Type: application/json; charset=UTF-8\n\n{'title':'$name','parents':[{'id':'$Folder'}]}\n";
    $body.="--$name$count\nContent-Type: $type\n\n".$file."\n--$name$count--\n";
    my $ua=LWP::UserAgent->new;
    my $req=HTTP::Request->new(POST=>"https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart");
    $req->content_type("multipart/related; boundary=\"$name$count\"");
    $req->content_length(length($body));
    $req->content($body);
    $req->header(Authorization=>"OAuth $access_token");
    my $res=$ua->request($req);
    return($res->content);
}
###
sub UrlEncode{
    my $str=shift;
    $str=~s/([^\w\-\.\~])/'%'.uc(unpack('H2',$1))/eg;
    return($str);
}
###
こんな感じ。
赤字のとこは自分用に入力。

普通にファイルシステムを使う感じとはだいぶ違ってて、把握するのが結構難しかった。
普通のファイルシステムと一番違うのが、PATH的な概念がなくPATH指定で対象を選択できない。
ローテートするのが面倒かと思ったんだが、PATHが無いので同じ場所に同名ファイルを置けるんで、同じ場所にアップロードして成功したら古いの削除しちゃうだけで良いことに気づいたんで、そうした。
アップロードするファイルを置くフォルダは、PATHが無いので毎回検索クエリを投げなきゃダメかとも思ったが、
フォルダのIDはブラウザでフォルダにアクセスした時のURL末尾の文字列と同じだったんで、プログラムに直接設定する仕様にした。

%Typeで拡張子を設定しておけばアップロード時にファイルタイプを指定して送るが、未設定のファイル場合は"application/octet-stream"で送る。

ファイルPATHをパラメータとして渡すとアップロードするけど、パラメータのPATHはスペース区切りで複数可。
アップロードに1秒以上かかるだろうが、秒間10リクエストまでの制限があるようなので、1ファイル毎に1秒寝る。

アップロード成功したら対象フォルダ内の古い同名ファイルは削除するが、
失敗した場合(idが取得できなかった場合)は、古いのそのままにしとく。



てな感じで、Google DriveのAPIが使えたんで、
先日のファイル暗号化をバックアップスクリプトに導入して、今回のGoogle Driveのアップロードスクリプトを使ってクラウドでバックアップするようにした。
これで万が一自宅PCのデータがバックアップストレージからの復元すらできないような状況に陥っても、致命的な状況に陥るのは回避できそう。