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

Google Drive API のバックアップスクリプトを改良した。

先日暗号化したローカルファイルをAPI使ってGoogle DriveにUPするPerlスクリプトを作ったが、
ファイルを読み込んでLWPでうpする仕様だったから、どうにかしてファイルを読み込まずに(メモリを使わずに)直接送れないかと方法を模索してた。

LWP::UserAgentやHTTP::Requestのコード見たりもしたんだが、
HTTP::Request::Commonの説明ではContentに配列のリファレンスでファイル名を渡せばメモリ使わず送れるみたいに書いてあるんだが、
コード見てもよくわかんないし、どっちにしろmultipart/form-data形式しか無理っぽい感じで、Google Driveではmultipart/relatedで送る必要があるのでダメぽい。
LWP以外にfurlってのも見たが、コマンド名を変えてるだけで中身はLWPと同じような気配だった。


というわけで、定番で使い慣れたPerl用HTTP通信モジュールのLWPではどうもできなそうなんで、より低レベルなIO::Socket::SSLを使って実装することにした。


#!/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 IO::Socket::SSL;
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/$Folder/children?q=@{[&UrlEncode('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=>"Bearer $access_token") if($access_token);
    my $res=$ua->request($req);
    return($res->content);
}
###
sub FileUpload{
    my $dir=shift||'.';
    my $name=shift;
    my $type="Content-Type: ".shift."\r\n\r\n";
    my $boundary=$name.time;
    my($head,$json,$len);
    $json="Content-Type: application/json; charset=UTF-8\r\n\r\n{'title':'$name','parents':[{'id':'$Folder'}]}\r\n";
    $len=length($json)+length($type)+(-s("$dir/$name"))+((length($boundary)+4)*3)+4;
    $head.="POST /upload/drive/v2/files?uploadType=multipart HTTP/1.1\r\n";
    $head.="Host: www.googleapis.com\r\n";
    $head.="Authorization: Bearer $access_token\r\n";
    $head.="Content-Type: multipart/related; boundary=\"$boundary\"\r\n";
    $head.="Content-Length: $len\r\n\r\n";

    my $sock=IO::Socket::SSL->new('www.googleapis.com:443');
    $sock->autoflush(1);
    print $sock $head;
    print $sock "--$boundary\r\n";
    print $sock $json;
    print $sock "--$boundary\r\n";
    print $sock $type;
    open(FILE,"$dir/$name");
    while(<FILE>){
        print $sock $_;
    }
    close(FILE);
    print $sock "\r\n--$boundary--\r\n";
    my($line,$len,$res);
    $line=$sock->getline;
    return if(!$line=~/^HTTP\S*200/);
    while(($line=$sock->getline) ne "\r\n"){
        $len=$1 if($line=~/^Content-Length: (\d+)/);
    }
    $sock->read($res,$len);
    $sock->close;
    return($res);
}
###
sub UrlEncode{
    my $str=shift;
    $str=~s/([^\w\-\.\~])/'%'.uc(unpack('H2',$1))/eg;
    return($str);
}
###
こんな感じ。
FileUpload 部分をLWP::UserAgentからIO::Socket::SSLに変更した。

Socket使う場合はヘッダから直接printで出力しちゃえばいいだけだが、ファイル読み込まずにContent-Lengthを計算するのがめんどい。
あと、前はファイル中にboundaryが出現しないかチェックしていたが、読み込まないんで決め打ちで「ファイル名+UNIXタイム」にした。出現しちゃったら失敗すると思うが、出現するようなことはありえないだろう。

Googleからのレスポンスをヘッダ取り除いて抽出しないといけないが、
自動で切断はされないようなんでレスポンスのContent-Lengthを確認して適切なサイズでreadしないとダメぽい。

ついでに、前はバックアップフォルダのファイル取得に https://www.googleapis.com/drive/v2/files に検索クエリでフォルダを指定していたが、
https://www.googleapis.com/drive/v2/files/フォルダID/children にGETすればフォルダ内だけに限定できるぽい。
そっちの方がわかりやすいから変更した。
最上位のファイルを取得する方法がわからなかったのだが、フォルダIDを"root"にすればルートフォルダの取得ができるぽいようなことも書いてあった。
これなら普通のファイルシステムみたいに使えるね。


目的通りメモリを使わずにUPできているかは確認しにくいが、topで見た感じだと問題無さそう。
これで大きいファイルもUPして問題なくなったから、サーバーのバックアップファイルなんかもGoogle Driveに上げちゃおうかと思う。
サーバーからのUPは転送量がきついが、自宅でPULLする代わりにGoogleにPUSHすれば同じだよね。
自宅よりもGoogleの方がデータ消滅の危険は少ないと思うし、自宅のストレージ節約もできる。