CSVダウンロードを作るのに飽きたからGoogle Drive APIを使ってみた


# 動機

管理画面からホゲホゲの集計データを出力するためにCSVを作ってダウンロードする処理を作ることがよくありますが、CSVって罠が多いです（Shift_JISとか改行コードとか）。
そういうノウハウを駆使してCSVダウンロード機能を作るのにちょっと飽きたので他の方法を集計データを出力できないか考えてみました。

Google Appsを使っているなら社内データもGoogleに上げてOKです。そもそもOfficeを使ってなくてAppsを使ってる場合もあるので、はじめからGoogle Driveにファイルを置いとけば仕事の流れ的に便利なんじゃないかと思います。
だからこの記事ではCSVをGoogle Driveに上げる方法を検討してみます。

言語は個人的趣味によりGo言語を使います（他の言語でも同様のはず）。


# Google Drive APIを試す

公式ドキュメントの[QuickStart](https://developers.google.com/drive/web/quickstart/quickstart-go)にチュートリアルとサンプルコードがあるので、まずはこれをそのままやってみます。

ただし以下の箇所がサンプルコードと食い違ってるので注意。

```bash:getするoauthライブラリがサンプルと違う
go get google.golang.org/api/drive/v2
go get golang.org/x/oauth2/...

# サンプルではこちらを使う
go get code.google.com/p/goauth2
```

[goauth2](https://code.google.com/p/goauth2/)のページを見ると `goauth2 is deprecated. Use golang.org/x/oauth2 instead.` となってるのでいつかサンプルも書き換わってくれると思います（フィードバックは送信済み）。

## APIをつかうための準備をする

[Google Developer Console](https://console.developers.google.com/project)でプロジェクトを作ってDriveAPIの追加、認証情報の作成をします。
ここらへんの詳しいやり方は次の記事が参考になります。

[googleapi - Node.jsのOAuthを使ってGoogleDriveAPIを叩いてみた - Qiita](http://qiita.com/maximum80/items/645cae98c1094a80976c)

## サンプルコードを動かす

さてようやくコードを動かせる、と思いきやサンプルコードにも食い違いがありました。
importしてるgdriveのパッケージが違います。そのままだと動かないので修正したコードを貼っときます。

```go:gdrive_sample.go
package main

import (
	"code.google.com/p/goauth2/oauth"
	"fmt"
	// "code.google.com/p/google-api-go-client/drive/v2"
	"google.golang.org/api/drive/v2" // こっちに書き換え
	"log"
	"net/http"
	"os"
)

// Settings for authorization.
var config = &oauth.Config{
	ClientId:     "YOUR_CLIENT_ID",       // ここは作った認証情報のIDに書き換える
	ClientSecret: "YOUR_CLIENT_SECRET",   // ここもSecretに書き換える
	Scope:        "https://www.googleapis.com/auth/drive",
	RedirectURL:  "urn:ietf:wg:oauth:2.0:oob",
	AuthURL:      "https://accounts.google.com/o/oauth2/auth",
	TokenURL:     "https://accounts.google.com/o/oauth2/token",
}

// Uploads a file to Google Drive
func main() {

	// Generate a URL to visit for authorization.
	authUrl := config.AuthCodeURL("state")
	log.Printf("Go to the following link in your browser: %v\n", authUrl)
	t := &oauth.Transport{
		Config:    config,
		Transport: http.DefaultTransport,
	}

	// Read the code, and exchange it for a token.
	log.Printf("Enter verification code: ")
	var code string
	fmt.Scanln(&code)
	_, err := t.Exchange(code)
	if err != nil {
		log.Fatalf("An error occurred exchanging the code: %v\n", err)
	}

	// Create a new authorized Drive client.
	svc, err := drive.New(t.Client())
	if err != nil {
		log.Fatalf("An error occurred creating Drive client: %v\n", err)
	}

	// Define the metadata for the file we are going to create.
	f := &drive.File{
		Title:       "My Document",
		Description: "My test document",
	}

	// Read the file data that we are going to upload.
	m, err := os.Open("document.txt")
	if err != nil {
		log.Fatalf("An error occurred reading the document: %v\n", err)
	}

	// Make the API request to upload metadata and file data.
	r, err := svc.Files.Insert(f).Media(m).Do()
	if err != nil {
		log.Fatalf("An error occurred uploading the document: %v\n", err)
	}
	log.Printf("Created: ID=%v, Title=%v\n", r.Id, r.Title)
}
```

```bash:アップロード
$ echo "hello gdrive" > document.txt
$ go run gdrive_sample.go
```

ブラウザで見れば、自分のGoogleDriveのルートフォルダにアップロードされてるはずです。

# トークンキャッシュを使うように改造

サンプルでは1回ごとにブラウザ経由で認証コードを取得する必要があります。何度もやってると面倒なのでファイルにキャッシュする機能を付けます。
最初に1回はブラウザ起動する必要がありますが、次回から有効期限が過ぎてたら自動で更新してくれるようになります。

```go:TokenCache付きサンプル
package main

import (
	"code.google.com/p/goauth2/oauth"
	"fmt"
	// "code.google.com/p/google-api-go-client/drive/v2"
	"google.golang.org/api/drive/v2" // こっちに書き換え
	"log"
	"net/http"
	"os"
)

// Settings for authorization.
var config = &oauth.Config{
	ClientId:     "YOUR_CLIENT_ID",
	ClientSecret: "YOUR_CLIENT_SECRET",
	Scope:        "https://www.googleapis.com/auth/drive",
	RedirectURL:  "urn:ietf:wg:oauth:2.0:oob",
	AuthURL:      "https://accounts.google.com/o/oauth2/auth",
	TokenURL:     "https://accounts.google.com/o/oauth2/token",
	TokenCache:   oauth.CacheFile("cache.json"), // キャッシュするファイル名を指定
}

// Uploads a file to Google Drive
func main() {
	t := &oauth.Transport{
		Config:    config,
		Transport: http.DefaultTransport,
	}

	// キャッシュがあったらブラウザ認証しない
	_, err := config.TokenCache.Token()
	if err != nil {
		// Generate a URL to visit for authorization.
		authUrl := config.AuthCodeURL("state")
		log.Printf("Go to the following link in your browser: %v\n", authUrl)

		// Read the code, and exchange it for a token.
		log.Printf("Enter verification code: ")
		var code string
		fmt.Scanln(&code)
		_, err := t.Exchange(code)
		if err != nil {
			log.Fatalf("An error occurred exchanging the code: %v\n", err)
		}
	}

	// Create a new authorized Drive client.
	svc, err := drive.New(t.Client())
	if err != nil {
		log.Fatalf("An error occurred creating Drive client: %v\n", err)
	}

	// Define the metadata for the file we are going to create.
	f := &drive.File{
		Title:       "My Document",
		Description: "My test document",
	}

	// Read the file data that we are going to upload.
	m, err := os.Open("document.txt")
	if err != nil {
		log.Fatalf("An error occurred reading the document: %v\n", err)
	}

	// Make the API request to upload metadata and file data.
	r, err := svc.Files.Insert(f).Media(m).Do()
	if err != nil {
		log.Fatalf("An error occurred uploading the document: %v\n", err)
	}
	log.Printf("Created: ID=%v, Title=%v\n", r.Id, r.Title)
}
```

サンプルと同じように実行すると一回目はブラウザ認証を求められますが、二回目以降は `cache.json` のおかげで何も入力を求められずにアップロードできます。

サーバサイドで動かす場合、いちいちブラウザ認証するのが面倒なのでこういう機能は必要ですね！　そもそもバッチ処理だと無理だし。


ちなみに `goauth2` にはTokenCacheがありますが、`oauth2` には無いので自前で作る必要があるみたいです。


# CSVを作ってアップロードする

ではいよいよCSVをGoogle Driveにアップロードしてみます。

上記のサンプルコードではファイルを開いてアップロードしています。サーバ側でいちいちファイルに落としてからアップロードするのは面倒なのでCSV文字列をCSVとしてアップロードしてみます。

使うのは [bytes.Reader]() と MimeType です。
書き換えたコードはこちら。

```go:gdrive_csv_upload.go
package main

import (
	"bytes" // 追加
	"code.google.com/p/goauth2/oauth"
	"fmt"
	// "code.google.com/p/google-api-go-client/drive/v2"
	"google.golang.org/api/drive/v2" // こっちに書き換え
	"log"
	"net/http"
)

// Settings for authorization.
var config = &oauth.Config{
	ClientId:     "YOUR_CLIENT_ID",
	ClientSecret: "YOUR_CLIENT_SECRET",
	Scope:        "https://www.googleapis.com/auth/drive",
	RedirectURL:  "urn:ietf:wg:oauth:2.0:oob",
	AuthURL:      "https://accounts.google.com/o/oauth2/auth",
	TokenURL:     "https://accounts.google.com/o/oauth2/token",
	TokenCache:   oauth.CacheFile("cache.json"), // キャッシュするファイル名を指定
}

// Uploads a file to Google Drive
func main() {
	t := &oauth.Transport{
		Config:    config,
		Transport: http.DefaultTransport,
	}

	// キャッシュがあったらブラウザ認証しない
	_, err := config.TokenCache.Token()
	if err != nil {
		// Generate a URL to visit for authorization.
		authUrl := config.AuthCodeURL("state")
		log.Printf("Go to the following link in your browser: %v\n", authUrl)

		// Read the code, and exchange it for a token.
		log.Printf("Enter verification code: ")
		var code string
		fmt.Scanln(&code)
		_, err := t.Exchange(code)
		if err != nil {
			log.Fatalf("An error occurred exchanging the code: %v\n", err)
		}
	}

	// Create a new authorized Drive client.
	svc, err := drive.New(t.Client())
	if err != nil {
		log.Fatalf("An error occurred creating Drive client: %v\n", err)
	}

	// Define the metadata for the file we are going to create.
	f := &drive.File{
		Title:       "My Document",
		Description: "My test document",
		MimeType:    "text/csv", // 追加
	}

	// CSV文字列
	nameList := `no, name
1, アブドゥル
2, イギー
3, 花京院
`
	csvReader := bytes.NewReader([]byte(nameList))

	// Make the API request to upload metadata and file data.
	r, err := svc.Files.Insert(f).Media(csvReader).Do()
	if err != nil {
		log.Fatalf("An error occurred uploading the document: %v\n", err)
	}
	log.Printf("Created: ID=%v, Title=%v\n", r.Id, r.Title)
}
```

同じように実行するとGDriveにCSVファイルがアップロードされているはずです。

「アプリで開く」からスプレッドシートを選ぶとちゃんと開けるはずです。




# その他いろいろ

調査途中のこと、ハマってるとこ、情報求むなこと

## 公開範囲

アップロードするときに指定できるはずですが、まだ調べてません。Google Appsの場合、同じ会社のひとに公開できれば良いので最初からその公開範囲にしときたいですね。

File構造体のなかに `Permission` があるので指定できるはず。

[drive - GoDoc](https://godoc.org/google.golang.org/api/drive/v2#File)

公式APIドキュメントだとこのへんか。

[Share Files   |   Drive REST API   |   Google Developers](https://developers.google.com/drive/web/manage-sharing)


**追記：[googleapi - Google Drive APIでアップロードしたファイルの公開範囲を変更する - Qiita](http://qiita.com/deltam/items/e13479ef36ac6729063e)**


## サービスアカウント認証だとファイルが見えない

今回はふつうにOAuth2.0の認証情報を使いましたが、Developer Consoleだと「サービスアカウント」という認証のタイプがあります。こちらはプライベートキーを発行してアクセスするのでサーバサイドで完結するアプリにはちょうど良さそうに思えます。

それでいろいろ調べながらサンプルコードを改造してたんですが、アップロードは成功するのにブラウザ上からは見えないという謎の現象に遭遇しました。いろいろ検索してみたらつぎの質問を見つけました。

[I can't see the files and folders created via code in my Google Drive - Stack Overflow](http://stackoverflow.com/questions/12211859/i-cant-see-the-files-and-folders-created-via-code-in-my-google-drive)

……よく分からないけど制限があるみたい。回避する方法はあるのかもしれないけど、OAuth2.0を使ったほうが早そうなので今回はこっちにしました。

サービスアカウント認証でもアップロードしてブラウザで見れた、そもそも認証タイプの使い方が違う、など情報知ってる方がいたらコメントくださいm(__)m


## Convert(true)にするとCSVがGoogleドキュメントと紐付いてしまう

上記のコードでCSVアップロードをしたあと、Googleスプレッドシートで開くともうひとつのファイルが作られてしまいます。どうにかアップロード時に変換してくれないものかとMimeTypeをいろいろ試したのですが、そのときに遭遇した現象。

Convertはデフォルトだとfalseです。 `MimeType: "text/csv"` にして `Convert(true)` にすれば始めからスプレッドシートとしてファイルを作ってくれるかと思ったら、なぜかGoogleドキュメント（テキストファイル）として認識されてしまいました。「アプリで開く」を選ぶとスプレッドシートが出てきてくれません。

次のMimeTypeを試したんですが、全部Googleドキュメントに紐付いてしまいました。

* `text/csv`
* `application/vnd.ms-excel`
  * .xls
* `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
  * .xlsx
* `application/vnd.google-apps.spreadsheet`
  * Googleスプレッドシート



## スプレッドシートで開いた画面のURLが欲しい

管理画面からこんな遷移でCSVが見れると素敵。

* CSV出力ボタン
* リンクが表示される or リダイレクト
* Googleスプレッドシートの画面

ところがAPI経由で取れる情報ではGoogleスプレッドシートのURLが分からない（ダウンロードリンクは分かるが、そもそもダウンロードを無くしたいのだった）。ファイルのIDは分かるけど上記で書いた「スプレッドシートで開くと別ファイルが作られる」問題とのコンボで、そのIDからスプレッドシートのURLは分からない。

どうにも最後の一歩が欠けるかんじでイヤですね。

これも情報求む！です。







つづく（と思う）。