​ ​

google-drive-ruby TIPS

はじめまして.Farmnote で働く北村と申します.コンサドーレ札幌が好きでよく応援に行っています. 今年は小野選手に加えて稲本選手も札幌でプレーされます.楽しみですね.

Farmnote では GoogleDrive などのクラウドサービスを積極的に活用しています.

今回,データを取得/加工してから GoogleDrive へとアップロードするのに Ruby ライブラリ google-drive-ruby v1.0 を利用しました.その際にいくつかわかったことがあるので共有します.

以下に出てくるコード例は全て Ruby2.2.0 で動かしています.

google-drive-ruby(v1.0.0) で GoogleDrive へログインする

google-drive-ruby は 2014-12-25 に 0.3.11 から 1.0.0 となりました.

README にも Incompatibility introduced in ver. 1.0.0 という項があるとおり,0.3 系と互換性がない処理があるので注意しましょう.2015-01-08 現在 google-drive-ruby について書かれている記事の多くは 0.3 系での内容です.

特に 0.3 系では利用できていた GoogleDrive.login に GoogleDrive のユーザー名とパスワードを渡してログインするという方法が使えなくなりました. そのかわり GoogleDrive.login_with_oauth へアクセストークンを渡してログインすることになります.

「クライアントID」と「クライアントシークレット」の入手方法

How to use をみると, アクセストークン (access_token) を取得するために client_idclient_secret を設定しています. この値はどこで取得できるのでしょうか.

詳しくは Implementing Server-Side Authorization - Google Drive Web APIs — Google Developers: に書いてあります. ここに書いてあることを 1 つずつ進めてみましょう.

Google Developers Console で Drive API を有効にするアプリケーションの登録 にアクセスし,「新しいプロジェクトを作成」のまま続行ボタンを押します.

google-drive-ruby_tips-01.png

すると,次のような画面になるので,「新しいクライアントIDを作成」をクリックします.

google-drive-ruby_tips-02.png

「インストールされているアプリケーション」を選んで「同意画面を設定」をクリックします.

google-drive-ruby_tips-03.png

「メールアドレス」を選択,「サービス名」に任意の名前をつけて「保存」ボタンをクリックします. ここでは “GoogleDriveSpike” と名付けました.あとで名前を変えることもできます. (ちなみに,ここでメールアドレスを指定せずに「保存」ボタンを押すこともできるのですが,認証する際 invalid email address となり失敗します.指定しましょう)

google-drive-ruby_tips-04.png

同意画面を設定すると,再度クライアントIDを作成画面になるので「インストールされているアプリケーション」を選び,「インストールされているアプリケーションの種類」を「その他」にして「クライアントIDを作成」をクリックします.

google-drive-ruby_tips-05.png

すると以下のように「クライアントID」と「クライアントシークレット」を入手できました.この文字列を先程の client_idclient_secret へ設定します.

google-drive-ruby_tips-06.png

GoogleDrive へ初めてログインする

これで google-drive-ruby で GoogleDrive へログインする準備が整いました.まずは

$ gem install google_drive

してライブラリを取得しましょう.その後,以下のコードを実行すると

require "google_drive"

client = Google::APIClient.new
auth = client.authorization
auth.client_id = "272733497164-0psht39kfj4jrdup134ue1dv5a1bo52t.apps.googleusercontent.com" # 「クライアントID」の文字列
auth.client_secret = "xxxxxxxxxxxxxxxx" # 「クライアントシークレット」の文字列
auth.scope =
    "https://www.googleapis.com/auth/drive " +
    "https://spreadsheets.google.com/feeds/"
auth.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
print("1. Open this page:\n%s\n\n" % auth.authorization_uri)
print("2. Enter the authorization code shown in the page: ")
auth.code = $stdin.gets.chomp
auth.fetch_access_token!
access_token = auth.access_token
refresh_token = auth.refresh_token

session = GoogleDrive.login_with_oauth(access_token)

puts "your access_token is:\n#{access_token}"
puts "----"

puts "your refresh_token is:\n#{refresh_token}"
puts "----"

session.files.each do |file|
  puts file.title
end
puts "----"

puts "done!"

以下のような状態で止まります

W, [2015-01-08T14:28:40.517766 #29160]  WARN -- : Google::APIClient - Please provide :application_name and :application_version when initializing the client
1. Open this page:
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=272733497164-0psht39kfj4jrdup134ue1dv5a1bo52t.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/drive%20https://spreadsheets.google.com/feeds/

2. Enter the authorization code shown in the page:

ここで表示されている URL をコピーしてブラウザからアクセスすると,以下のような認可画面になります.四角で囲ったところが,設定したクライアント名になっていますね.「承認する」ボタンを押しましょう.

google-drive-ruby_tips-07.png

すると以下のように,文字列が表示されているだけのシンプルな画面になります.コードをコピーしましょう.

google-drive-ruby_tips-08.png

コピーしたコードをコンソールに貼りつけ,Enterキーを押しましょう.

W, [2015-01-08T14:58:34.791925 #30061]  WARN -- : Google::APIClient - Please provide :application_name and :application_version when initializing the client
1. Open this page:
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=272733497164-0psht39kfj4jrdup134ue1dv5a1bo52t.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/drive%20https://spreadsheets.google.com/feeds/

2. Enter the authorization code shown in the page: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (ブラウザからコピーしたコード)
your access_token is:
ya29.9QAM8xNvqxhmo6yxGUHH4V6jBYS3-cKXFLz9EECQg3hDo2lFaC76thAUN6sBAHrGWHFkoqpNcIlwVw
----
your refresh_token is:
1/qOWykRNv5LonZExro1Vv2HZ5HVrJ3xBCNKMcaoqIMrI
----
(GoogleDriveに保存しているファイルの一覧)
----
done!

うまくいけば done! と表示されるはずです.

もし失敗した場合,エラーメッセージをみるとおおまかな原因が推測できます.

  • "error" : "invalid_client" => クライアントIDとクライアントシークレットの値がおかしいので再確認しましょう
  • "error" : "invalid_grant" => クライアントIDとクライアントシークレットは正しく,コンソールへ貼りつけたコードがおかしいので,再実行してコードを再び貼りつけましょう

GoogleDrive へ 2 回目以降ログインする

初回のログインはうまくできたでしょうか.この方法で認証すると,毎回ブラウザを開く必要があり,自動化するのが大変ですね. そこで,先ほどコンソールに記載されていた access_token を利用して以下のようにログインすることにします.

require "google_drive"

session = GoogleDrive.login_with_oauth("ya29.9QAM8xNvqxhmo6yxGUHH4V6jBYS3-cKXFLz9EECQg3hDo2lFaC76thAUN6sBAHrGWHFkoqpNcIlwVw")

session.files.each do |file|
  puts file.title
end
puts "----"

puts "done!"

いかがでしょうか.先程と同じようにファイル一覧が表示されましたか. それではこのアクセストークンを保存しておいて,毎回利用するとよさそうですね.

しかし,1 時間以上経った後に同じコードを実行すると以下のようなエラーになります. アクセストークンには制限時間があり,その制限時間を超えたものはGoogleDriveで受けつけなくなります.

api_client.rb:650:in `block (2 levels) in execute!': Invalid Credentials (Google::APIClient::AuthorizationError)

どうすればよいでしょう. 実は先ほどコンソールに記載されていた refresh_token があれば

  1. クライアントID
  2. クライアントシークレット
  3. リフレッシュトークン

の 3 つから何度でもアクセストークンを取得することができます. 以下のコードではアクセストークンを明示的に与えてはいませんが,リフレッシュトークンを利用してプログラム中でアクセストークンを取得しています.

require "google_drive"

client = Google::APIClient.new
auth = client.authorization
auth.client_id = "272733497164-0psht39kfj4jrdup134ue1dv5a1bo52t.apps.googleusercontent.com" # 「クライアントID」の文字列
auth.client_secret = "xxxxxxxxxxxxxxxx" # 「クライアントシークレット」の文字列
auth.refresh_token = "1/qOWykRNv5LonZExro1Vv2HZ5HVrJ3xBCNKMcaoqIMrI"
auth.fetch_access_token!

session = GoogleDrive.login_with_oauth(client)

session.files.each do |file|
  puts file.title
end
puts "----"

puts "done!"

うまく表示できたでしょうか.

アクセストークンが時間切れになる(GoogleDriveの今の仕様だと1時間です)ような期間動かすようなプログラムの場合は アクセストークンではなくリフレッシュトークンを保持しておき,アクセストークンは必要に応じてプログラム中で再取得するような形にしておくと良いでしょう.

ライブラリ側でのリフレッシュトークンを利用したアクセストークンの取得機能

今までは GoogleDrive.login_with_oauth を利用していましたが,GoogleDrive.saved_session を読むと,上に書いたのと同じようなことを行なっていることがわかります.

GoogleDrive.saved_session の場合は, 初回は ~/.ruby_google_drive.token に JSON 形式でリフレッシュトークンを保存し, 2 回目以降はそのリフレッシュトークンを利用して自動的にアクセストークンを取得してくれるようですね.

また,google-drive-ruby が利用しているライブラリ google-api-ruby-client の 2015-01-08 現在最新版にも APIClient#authorizationerrorhandler というアクセストークンを再取得する仕組みが用意されているようなのですが,google-drive-ruby の 1.0.0 ではうまく動作していないようです.協調して動作してくれると楽になりますね.

scope を弱めて利用する

今まで見てきたコードは GoogleDrive の全ての機能にアクセスすることができます. 一見便利ですが,必要もないのに何でもできてしまうのはミスをしたときや,意図しない相手にトークンが漏れたときに被害範囲を限定できず,好ましくありません. そこで,以下の作業では「できる事」を「やりたい事に必要な分だけ」に弱めて利用します.

GoogleDrive では scope というものを指定して「できる事」を変えます. scope の種類は Choose Auth Scopes - Google Drive Web APIs — Google Developers: にあります. (日本語版もありました)

今回は scope を https://www.googleapis.com/auth/drive.file (アプリで作成されたファイルまたは開かれたファイルへのファイルごとのアクセス) に弱めて利用します. 以下のコードでアクセストークンを再取得しましょう.その時のブラウザの認可内容も変わっていますね.

require "google_drive"

client = Google::APIClient.new
auth = client.authorization
auth.client_id = "272733497164-0psht39kfj4jrdup134ue1dv5a1bo52t.apps.googleusercontent.com" # 「クライアントID」の文字列
auth.client_secret = "xxxxxxxxxxxxxxxx" # 「クライアントシークレット」の文字列
auth.scope = "https://www.googleapis.com/auth/drive.file" # <= ここが先ほどと変わりました
auth.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
print("1. Open this page:\n%s\n\n" % auth.authorization_uri)
print("2. Enter the authorization code shown in the page: ")
auth.code = $stdin.gets.chomp
auth.fetch_access_token!
access_token = auth.access_token

puts "your access_token is:\n#{access_token}"

google-drive-ruby_tips-09.png

これより以下のコードは全てこのスコープで得たアクセストークンで行なっています.

scope を弱めたときの google-drive-ruby 利用の注意点

google-drive-ruby の GoogleDrive::Session#root_collection を呼び出すとエラーになります.GoogleDrive の元となる "root" オブジェクトは,このアプリで作成したものではないため,権限がなく取得できないためのようです. root_collection を利用しているメソッドは結構あるので注意してください.

require "google_drive"

session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")
session.root_collection

# => File not found: 1AKdsoap-dasZ8PVA (Google::APIClient::ClientError)

ファイルのアップロードのしかた

まずはファイルをアップロードしてみましょう

require "google_drive"
require "tempfile"

csv = <<__CSV__
1,日本語,aaa
2,,bbb
3,ほげ,ccc
4,時間,#{Time.now.to_s}
__CSV__

title = "google_drive_api_test-1"

Tempfile.open([title, ".csv"]) do |file|
  file << csv # [下準備] ファイルを作る

  session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")
  session.upload_from_file(file, title)
end

お使いの GoogleDrive をブラウザで開くと google_drive_api_test-1 がアップロードされていると思います.

google-drive-ruby_tips-10.png

ファイルのアップロードには

あたりを使うとよいでしょう.

ある名前のファイルがなければ新規作成,あればファイルを上書きする書きかた

GoogleDrive では同じ名前のファイルが1つのフォルダに複数存在することができます. 上で書いたプログラムをもう一度動かして,GoogleDrive を再ロードすると図のように同じ名前のファイルが 1 つから 2 つへと増えています.

google-drive-ruby_tips-11.png

今回は同じ名前のファイルを更新し続けたかったため,もし同じ名前のファイルがあれば上書きするようなプログラムを用意します. 以下のプログラムを 2 回実行することで,ファイルが更新されるか確かめましょう. まず一度実行します.

require "google_drive"
require "tempfile"

csv = <<__CSV__
1,日本語,aaa
2,,bbb
3,ほげ,ccc
4,時間,#{Time.now.to_s}
__CSV__

title = "google_drive_api_test-2"

Tempfile.open([title, ".csv"]) do |file|
  file << csv

  session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")
  google_file = session.file_by_title(title)
  if google_file
    google_file.update_from_file(file)
  else
    session.upload_from_file(file, title)
  end
end

うまくアップロードされていますね.

google-drive-ruby_tips-12.png

次に実行したときにファイルの内容が更新されるか確かめられるよう,時間をチェックしておきます. (時間が遅いのは自宅でこのスクリーンショットを撮ったためです><)

google-drive-ruby_tips-13.png

それでは同じものを再度実行しましょう.ファイルは増えていませんね.

google-drive-ruby_tips-14.png

内容をみると,時間が更新されています.これによりファイルが更新できていることを確認できました.

google-drive-ruby_tips-15.png

ファイルの更新には

あたりを使うとよいでしょう.

フォルダの作りかた

今まではファイルをフラットな場所に置いていました.関連するファイル毎にまとめて収納したいですよね. GoogleDriveではファイルを収納するところをフォルダと呼んでいます.(ディレクトリではないんですね)

GoogleDriveでのフォルダの説明は Work with Folders - Google Drive Web APIs -- Google Developers: にあります.(日本語版もありました.)

日本語版の資料に

フォルダとは、MIME タイプが application/vnd.google-apps.folder で、拡張子を持たないファイルです

とあるとおり,GoogleDrive でのフォルダは単なるファイルです. あまり意識していませんでしたが,考えてみると普段使っている OS も同じでしたね.

それでは google-drive-ruby でフォルダを作成するにはどうしたらいいでしょうか. もし scope が https://www.googleapis.com/auth/drive (何でもできる) であれば,以下のように作成することができます.

require "google_drive"

session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")
session.root_collection.create_subcollection("subfolder")

今回の scope https://www.googleapis.com/auth/drive.file の場合, root からのフォルダ作成を google-drive-ruby に用意された API で行う方法は見つけられませんでした.(もしわかれば教えてください) そこで google-drive-ruby が利用している,低レイヤーの google-api-ruby-client を利用して直接フォルダを作成します.

require "google_drive"

folder_name = "subfolder"

session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")

# 以下のコードは scope が https://www.googleapis.com/auth/drive なら
# session.root_collection.create_subcollection(folder_name)
# で代替可能
folder = session.drive.files.insert.request_schema.new(
  {
    "title" => folder_name,
    "mimeType" => "application/vnd.google-apps.folder"
  }
)
session.client.execute(api_method: session.drive.files.insert,
                       body_object: folder)

このコードを実行するとご覧の通りにフォルダを作成することができます. (もしうまくいかなかった場合, session.client.execute の返り値を確認してみてください. google-api-ruby-client では GoogleDrive との API がエラーになった場合でも例外にはならず,返り値にエラーの内容を内包しているようでした)

google-drive-ruby_tips-16.png

フォルダの下へのファイルの収めかた

GoogleDrive でフォルダへファイルを収めるにはどうしたらよいでしょうか. GoogleDrive の API では「ファイルをフォルダの ID に結びつける」という形で行うことができます. 「フォルダへファイルを収める」という私のマインドセットとは少し異なっていて面白かったです.

実際のところ,GoogleDrive では,同じファイルを複数のフォルダに属させることができるようです. フォルダというよりはタグのイメージの方がしっくりくるかもしれません.

ともかく,フォルダを開くとファイルが表示されるようにしてみましょう. 先程作成した subfolder へ,その前に作成した google_drive_api_test-2 という名前のファイルを移動させます. これだけですと google_drive_api_test-2 が root と subfolder の両方に表示されるので, root からは google_drive_api_test-2 を削除します.これも低レイヤーの API を利用します. 弱めの scope で利用しようとすると root 部分でのフォルダ操作で少し不便ですね.何かいい方法が思いついたらライブラリへ pull request を送ろうと思います.

require "google_drive"

folder_name = "subfolder"
file_name = "google_drive_api_test-2"

session = GoogleDrive.login_with_oauth("xxxxxxxxxxxxxxxx")

file = session.file_by_title(file_name)
folder = session.file_by_title(folder_name)
folder.add(file)

# 以下のコードは scope が https://www.googleapis.com/auth/drive なら
# session.root_collection.remove(file)
# で代替可能
parents =session.client.execute(api_method: session.drive.parents.list,
                                parameters: {"fileId" => file.id})
root = parents.data.items.find(&:is_root)
session.client.execute(api_method: session.drive.parents.delete,
                       parameters: {
                         "fileId" => file.id,
                         “parentId” => root.id
                       })

実行して結果をみましょう. root フォルダからは消えていますね.

google-drive-ruby_tips-17.png

subfolder の中をみると,意図した通りに google_drive_api_test-2 があります. うまくできたようです.

google-drive-ruby_tips-18.png

まとめ

  • Google の OAuth2 クライアントの作成方法を知りました
  • OAuth2 クライアントを用いた google-drive-ruby(v1.0) でのログイン方法を知りました.また,再ログインするにはどのトークンを保持しておけばよいか知りました.
  • google-drive-ruby(v1.0) を用いた GoogleDrive への操作権限の変更方法を知りました
  • google-drive-ruby(v1.0) を用いた GoogleDrive へのファイルのアップロード方法を知りました
  • google-drive-ruby(v1.0) を用いた GoogleDrive へのファイルの更新方法を知りました
  • google-drive-ruby(v1.0) を用いた GoogleDrive のフォルダ作成方法を知りました
  • google-drive-ruby(v1.0) を用いた GoogleDrive のファイルをフォルダへ移動する方法を知りました

ローカル環境から GoogleDrive への操作が一通りできるようになりましたね. これを機会にみなさまと GoogleDrive の仲が一層深まることを願っております.

このエントリーをはてなブックマークに追加