Google Apps Scriptのプロジェクトのコードを外部からダウンロード/アップロード(アップロード編)

# Google Apps Scriptのプロジェクトのコードを外部からダウンロード/アップロード(アップロード編)

※なんとなく難しくない言葉で優し目に書いています。

Google Appsのマクロ環境であるGoogle Apps ScriptのプロジェクトのコードをGoogle Drive APIを利用して、
外部からダウンロード/アップロード出来るようになりました！

今回はその話について書いてきます。（アップロード編)
なお、ダウンロード編については[こちら](http://qiita.com/soundTricker@github/items/8873f29781d1e123cfa8)を参照して下さい。


## ドキュメントとか説明動画

ドキュメントは以下にあります。
英語がイケる人はこれを読めばいいです。

__リファレンス__
[Google Apps Script Reference#Import/Export](https://developers.google.com/apps-script/import-export#import_projects)

また英語が更にいける方は、Google Developer Liveで放送された、Youtubeを見るといいと思います。

__Youtube Google Developer Live__
[Apps Script Crush Course: Import/Export Apps Script Code](https://www.youtube.com/watch?v=lEVMu9KE6jk)


## アップロード(Import)

ダウンロードに比べて、アップロードは基本的に1つの方法しかありません。
Google Drive APIのfiles.importまたはfiles.updateを利用します。
importとupdateの違いは、新しいGASプロジェクトを作成するか、既存のGASプロジェクトを更新するかの違いです。

### 基本

#### 新規プロジェクトの作成

Google Drive APIのfiles.import endpointを利用します。
なお、APIを利用するには、OAuth等による認可が必要で、
OAuthの場合は以下のあたりがScopeとして必要です。

__Scope__

```
  https://www.googleapis.com/auth/drive
  https://www.googleapis.com/auth/drive.file
  https://www.googleapis.com/auth/drive.scripts //これが必要
```

__Reqeust__

```
POST https://www.googleapis.com/upload/drive/v2/files?convert=true //OAuth1でやっている場合はAPI KEYも必要
Authorization:  Bearer ya29.fakebearerestring
Content-Type:  application/vnd.google-apps.script+json
```

___RequestBody___
基本的にダウンロードした時と同じJSONを利用します。

```javascript
{
  "files": [
    {
      "name":"Code",
      "type":"server_js",
      "source":"function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);\n}\n"
    },
    {
      "name":"index",
      "type":"html",
      "source":"\u003chtml\u003e\n  \u003cbody\u003e\n    Hello, world!!\n  \u003c/body\u003e\n\u003c/html\u003e"
    }//今だと、htmlは新規の際には作れないっぽいです。 https://code.google.com/p/google-apps-script-issues/issues/detail?id=2916
  ]
}
```

上の感じでリクエストを投げれば新しいプロジェクトが作成出来ます。

#### 既存プロジェクトの更新

Google Drive APIのfiles.update endpointを利用します。
なお、APIを利用するには、OAuth等による認可が必要で、
OAuthの場合は以下のあたりがScopeとして必要です。

__Scope__

```
  https://www.googleapis.com/auth/drive
  https://www.googleapis.com/auth/drive.file
  https://www.googleapis.com/auth/drive.scripts //これが必要
```

__Reqeust__

```
POST https://www.googleapis.com/upload/drive/v2/files/{fileid} //OAuth1でやっている場合はAPI KEYも必要
Authorization:  Bearer ya29.fakebearerestring
Content-Type:  application/vnd.google-apps.script+json
```

___RequestBody___
基本的にダウンロードした時と同じJSONを利用します。
下のJSON中にも書きますが、JSONの書き方で追加、修正、削除が可能です。

ダウンロードした時のJSONとくらべて...

* sourceのみ更新した場合は更新
* nameのみ更新した場合は名前の変更
* fileをまるっと書かなかった場合は削除
    つまり、削除したくない場合は毎回全部のソースが含まれたをアップロードする必要があります。
* 新しいfileを追加した場合(idは不要)は新規追加

となります。

```javascript
{
  "files": [
    //sourceのみ変えた場合は更新
    {
      "id":"9basdfbd-749a-4as9b-b9d1-d64basdf803",
      "name":"Code",
      "type":"server_js",
      "source":"function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);//updated\n}\n"
    }, 
    /* fileを丸っとなくした場合は削除
    {
      "id":"3asf7c0d-1afb-4a9-8431-5asdfc79e7ae",
      "name":"index",
      "type":"html",
      "source":"\u003chtml\u003e\n  \u003cbody\u003e\n    New message!!\n  \u003c/body\u003e\n\u003c/html\u003e"
    }*/, 
    //名前を変えた場合はrename
    {
      "id":"9basdfbd-749a-4as9b-b9d1-d64basdf804",
      "name":"NewName",
      "type":"server_js",
      "source":"function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);//updated\n}\n"
    }, 
    //新しく追加した場合はファイルを新規作成
    {
      "id":"9basdfbd-749a-4as9b-b9d1-d64basdf804",
      "name":"NewFile",
      "type":"server_js",
      "source":"function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);//updated\n}\n"
    }
  ]
}
```

### 実際のコードで試してみる。

GASプロジェクトの作成や、更新ともにAPI Explorerでは出来ないので、
サンプルとして、GASからこれらを実施してみたいと思います。

#### 新しいGASプロジェクトを作成する

```javascript:新しいGASプロジェクトを作成する.js
var KEY = 'API ConsoleでKeyを作成して下さい。';

function 新しいGASプロジェクトを作成する() {
 
  var oauthConfig = UrlFetchApp.addOAuthService('drive');
  
  /*
  scopeには
  https://www.googleapis.com/auth/drive
  https://www.googleapis.com/auth/drive.file
  https://www.googleapis.com/auth/drive.scripts
  が多分必要です
  */
  var scope = 'https://www.googleapis.com/auth/drive+https://www.googleapis.com/auth/drive.file+https://www.googleapis.com/auth/drive.scripts';
  oauthConfig.setConsumerKey('anonymous');
  oauthConfig.setConsumerSecret('anonymous');
  oauthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+scope);
  oauthConfig.setAuthorizationUrl('https://accounts.google.com/OAuthAuthorizeToken');    
  oauthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
  
  //基本的にdownloadで取ってきたものと同じ構造です。
  var files = {
  "files": [
    {
      "name":"Code",
      "type":"server_js",
      "source":"function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);\n}\n"
    },
    {
      "name":"index.html",
      "type":"html",
      "source":"\u003chtml\u003e\n  \u003cbody\u003e\n    Hello, world!!\n  \u003c/body\u003e\n\u003c/html\u003e"
    } //今だと、htmlは新規の際には作れないっぽいです。 https://code.google.com/p/google-apps-script-issues/issues/detail?id=2916
  ]
};
  
  //content-typeが重要です。
  var postParams = {
    method:'post',
    oAuthServiceName: 'drive',
    oAuthUseToken: 'always',
    payload: JSON.stringify(files),
    contentType: 'application/vnd.google-apps.script+json'
  };
  
  //新規GASプロジェクトの作成 一旦Untitledという名前で作成されます
  //やろうと思えば一度にtitleの設定も可能ですが面倒臭いです。
  var json = UrlFetchApp.fetch("https://www.googleapis.com/upload/drive/v2/files?convert=true&key=" + KEY,postParams).getContentText();
  
  //名前を変更します。
  DriveApp.getFileById(JSON.parse(json).id).setName("新しく作った奴");
  Logger.log(json);

}
```

#### 既存のプロジェクトを更新する。

```javascript:既存プロジェクトの更新.js
var KEY = 'API ConsoleでKeyを作成して下さい。';
function 既存プロジェクトの更新() {

  //一回Fileをダウンロードしてくる
  var fileId = "ファイルのID";
  
  //OAuthの設定
  var oauthConfig = UrlFetchApp.addOAuthService('drive2');
  
  var scope = 'https://www.googleapis.com/auth/drive+https://www.googleapis.com/auth/drive.file+https://www.googleapis.com/auth/drive.scripts';
  oauthConfig.setConsumerKey('anonymous');
  oauthConfig.setConsumerSecret('anonymous');
  oauthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+scope);
  oauthConfig.setAuthorizationUrl('https://accounts.google.com/OAuthAuthorizeToken');    
  oauthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
  
  //ダウンロード用のUrlFetch Option
  var downloadOptions = {
    method:'get',
    oAuthServiceName: 'drive2',
    oAuthUseToken: 'always'
  };
  var url = DriveApp.getFileById(fileId).getDownloadUrl();
  
  //ダウンロード
  var json = UrlFetchApp.fetch(url,downloadOptions).getContentText();
  
  //JSON化
  var scriptObject = JSON.parse(json);
  
  //ソースを更新
  scriptObject.files[0].source += "\n//updated\nfunction c() {Logger.log('hoge')}";

  //ファイルを削除
  scriptObject.files[1] = null;
  scriptObject.files = scriptObject.files.filter(function(file) {return file});
  
  //名前の変更
  scriptObject.files[1].name += "updated";
  
  //ファイルを新規追加
  scriptObject.files.push({
    
    "name":"index",
    "type":"html",
    "source":"\u003chtml\u003e\n  \u003cbody\u003e\n    Hello, world!!\n  \u003c/body\u003e\n\u003c/html\u003e"
  });
  
  
  //update用のUrlFetch Option
  var updateOptions = {
    method:'put',
    oAuthServiceName: 'drive2',
    oAuthUseToken: 'always',
    contentType: 'application/vnd.google-apps.script+json',
    payload: JSON.stringify(scriptObject)
  };
  
  var uploadResponse = UrlFetchApp.fetch('https://www.googleapis.com/upload/drive/v2/files/' +fileId + "?key=" + KEY, updateOptions);
}
```

## Import/Exportで出来ないこと

大事なことなので書いておきます。
まだまだ出たばかりの機能なので出来ないことは実は多いです。

* Sheets、Docs、Forms、Sitesなど上で作成されたGAS Projectではこの機能は利用できません。
    この機能が利用できるのはDrive上に直接作成されたGAS Projectのみです。
* ScriptPropertiesやScriptDB、ログなどのリソースの取得、更新出来ません。
* Libraryなども外から設定はできません。 ※個人的に痛い
* GASを外部から起動、公開、バージョン追加みたいなことはできません。
* 上記のようにGAS中で自分自身のコードを書き換えても、実行中は書き換える前のコードとして動きます。


## 終わり

ということで今回はImport/ExportのExport編でした。

今回のImport/Exportはすごく熱い機能です。
今までGASを開発するにはScript Editorを使わざる得ませんでした。
しかし、今回のImport/Exportの機能によって以下の様なことができると期待出来ます。

* ローカル環境での開発(そのままコードをアップロード)
* CoffeeScriptなどのaltjsを利用して開発(コンパイルしてアップロード)
* Scriptのバックアップ
* CI環境を利用したDeploy

等など夢広がります。
僕もgrunt-gas-deployあたりを作ろうかなーとか考えてます( ｰ`дｰ´)ｷﾘｯ
 ※多分...