Google Apps Scriptを使用したGoogle Drive上のファイル移動

Google Driveを使用しているとファイルを別のフォルダへ移動させたくなる場合があります。本記事では、このファイル移動についてGoogle Apps Script (GAS)を使った2つの方法をご紹介します。丁度、ファイルの別フォルダへの移動について[Advanced Drive Service](https://developers.google.com/apps-script/advanced/drive)でも可能であると教わりましたので勉強を兼ねて調べましたので、覚書としてまとめました。

# DriveAppを使用した場合

## 20200903更新
2020年7月27日に[`addFile(File)`, `addFolder(Folder)`, `removeFile(File)`, `removeFolder(Folder)`のメソッドが廃止](https://developers.google.com/apps-script/reference/drive/folder#expandable-1)され、これと同時に、下記の新たなメソッドが追加されました。

- [moveTo(destination) in Class File](https://developers.google.com/apps-script/reference/drive/file#movetodestination)
- [moveTo(destination) in Class Folder](https://developers.google.com/apps-script/reference/drive/folder#moveTo(Folder))

これにより、ファイル、あるいはフォルダーの移動は下記のように行うことができるようになりました。

### ファイルの移動
```javascript
const fileId = "###"; // 移動させるファイルのファイルID
const folderId = "###"; // 移動先のフォルダID
const file = DriveApp.getFileById(fileId);
const folder = DriveApp.getFolderById(folderId);
file.moveTo(folder);
```

### フォルダの移動
```javascript
const srcFolderId = "###"; // 移動させるフォルダのフォルダID
const dstFolderId = "###"; // 移動先のフォルダID
const srcFolder = DriveApp.getFolderById(srcFolderId);
const dstFolder = DriveApp.getFolderById(dstFolderId);
srcFolder.moveTo(dstFolder);
```


@unauさんのご指摘を反映させていただきました。

下記スクリプトはStandalone scriptあるいはContainer bound scriptで使用します。
この方法ではコピー元ファイルのフォルダ情報にコピー先のフォルダ情報を追加し、その後コピー元のフォルダ情報を削除する流れです。新たなフォルダ情報を追加する前に元のフォルダ情報を削除しても問題ありません。


```gas:
  var sourcefile = [コピー元ファイル名]; // file name
  var destfolder = [コピー先フォルダ名]; // folder name
  var destfolder = DriveApp.getFoldersByName(destfolder).next();
  var file = DriveApp.getFilesByName(sourcefile).next();
  var sourcefolder = file.getParents().next();
  destfolder.addFile(file);
  sourcefolder.removeFile(file);
```

# Google Drive APIsを使用した場合

この方法では直接ファイルの情報を更新してフォルダ位置を変更する流れです。詳細は[こちら](https://developers.google.com/drive/v2/reference/)をご覧ください。この方法では事前の準備が必要です。

## 事前準備

1. スクリプトのプロジェクトを開いて、上部の　「リソース」 - 「Googleの拡張サービス」　 - 「Drive API」をONにする。

2. [Google Developers Console](https://console.developers.google.com/)のライブラリにて　「Google Drive API」　を検索して有効にする。

3. [Google Developers Console](https://console.developers.google.com/)でプロジェクト作成とClient ID, Client secretを取得し、codeを取得する。

4. アクセストークンを取得 : [こちらの記事](http://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)が分かりやすいと思います。

## GASのサンプルスクリプト

これを実行するには事前準備で取得したアクセストークンが必要です。実際に実行したとき、このような方法でもファイル移動ができるのかと感心させられました。

```gas:
var sourcefile = DriveApp.getFilesByName([コピー元ファイル名]).next().getId(); // file name
var destfolder = DriveApp.getFoldersByName([コピー先フォルダ名]).next().getId(); // folder name
var accesstoken = [アクセストークン];
UrlFetchApp.fetch('https://www.googleapis.com/drive/v2/files/' + sourcefile, {
    method:'put',
    headers: {
        'Authorization': 'Bearer ' + accesstoken,
        'uploadType' : 'multipart/related'
    },
    contentType: 'application/json',
    payload: JSON.stringify({'parents': [{id: destfolder}]})
});
```

UrlFetchApp.fetchを使用せずに拡張サービスを利用して直接Drive APIを使用すると、アクセストークンの入力は不要になり、下記のようにシンプルになります。

```gas:
Drive.Files.update(
  {"parents": [{"id": "## Folder ID ##"}]},
  "## File ID ##"
)
```

## ローカルからの使用

Google Drive APIsを使用する場合、DriveAppを使用する場合よりも事前準備は大変ですが、この方法ではGoogle Drive上だけでなく、ローカルからでもcurlなどでGASのサンプルスクリプトと同じ動作をさせることができます。

```shell-session:
$ curl -X PUT -H 'Content-Type: application/json' -H 'uploadType: multipart/related' -H 'Authorization: Bearer [アクセストークン]' https://www.googleapis.com/drive/v2/files/[コピー元ファイルID] -d '{"parents": [{"id": "[コピー先フォルダID]"}]}'
```

上記の実行時には移動先のフォルダIDと移動させたいファイルIDを直接指定する必要がありますので次のようなコマンドでファイル名、ファイルIDを取得して対応させてください。この詳細は[こちら](https://developers.google.com/drive/v2/reference/files/list)でご覧いただけます。リファレンスを見ていると、かなりのことができるようで楽しめそうです。

```shell-session:
$ curl -H 'Content-Type: application/json' -H 'Authorization: Bearer [アクセストークン]' https://www.googleapis.com/drive/v2/files -o list.json
```

URLへ"orderBy=folder"を付け加えるとfolderが先に表示されます。データはJSONで出力されますのでjqを使用してファイル名、ファイルIDをそれぞれ下記のように取得することができます。

```shell-session:
$ jq -r ".items[].title" list.json
ファイル名
$ jq -r ".items[].id" list.json
ファイルID
```


## はまったところ

初めはScriptApp.getOAuthToken()でアクセストークンを取得しようとして下記のようなエラーで困っていました。[説明](https://developers.google.com/apps-script/reference/script/script-app#getOAuthToken())を見ると、全てに対応していないとあったため、[こちらの記事](http://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)を参考にさせていただき、手動でアクセストークンを取得することで無事解決することができました。

```text:
{
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "insufficientPermissions",
    "message": "Insufficient Permission"
   }
  ],
  "code": 403,
  "message": "Insufficient Permission"
 }
}
```

GASは奥が深いですね。