Google ドライブ に保存された請求書を、定期的にDirectCloud-BOXへバックアップする

この記事は[「Qiitaエンジニアフェスタ2021」](https://qiita.com/official-campaigns/engineer-festa/2021)の参加記事です。

業務で Googleドライブ を活用している方は多いことでしょう。
Googleドライブのファイルは削除後、30日間はゴミ箱に残っていますが、30日を超えると完全削除となり、元に戻せません。

意図せぬファイル削除は、意外に起こりがちです。大事なファイルを無意識に削除し、ドライブからなくなっていた、ということも、なきにしもあらずです。

そこで今回は、Googleドライブ内の書類をAPI経由で DirectCloud-BOX に保存し、自動でバックアップを取る業務効率化の方法を考えてみました。


# シナリオ

Googleドライブに保存されている請求書を、定期的に DirectClod-BOX にバックアップし、うっかり消失を防ぐ。請求書はタイトルに「invoice」が含まれているものとする。

こんな流れになります。

![スクリーンショット 2021-08-25 17.34.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/6f845e00-d47f-946c-8468-e6fa8f6ccc4c.png)

1日に一回、自動的にバックアップを実行し、データ保存・バックアップの効率化を実現します。


# 手順

* DirectCloud-BOX に、書類保存用のドライブを作成する
* APIの操作を行うための APIキーを発行する
* アクセストークンを取得する
* フォルダのノードを確認する
* Credential情報をGoogle Apps Scriptのプロパティ に登録する
* タイトルに「invoice」と書かれている請求書データを、DirectCloud-BOXへ送信、バックアップデータとする
    * Google Apps Script を利用してコードを書く
* 1日に1回の定期実行を設定し、自動的にバックアップが行われるようにする



# DirectCloud-BOXとは

オンラインで利用できるストレージサービス。BOX や DropBOX に近い利用感でした。


アカウントの発行は、[ウェブサイト](https://directcloud.jp/)から行います。

[小規模事業者向けのプラン](https://directcloud.jp/freeplan)もあります。5GBまで無料とのこと。

今回は、小規模事業者向けのフリープランで構築しました。

# DirectCloud-BOX に、書類保存用のドライブを作成する

管理者画面のメニューから [ユーザー] → [ユーザー管理]を選び、作業用ユーザーを作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/934deff3-4237-1705-caaf-934c00630190.png" width="480">

ユーザー作成後、改めてログインしなおします。ユーザー側の管理画面はこんな感じ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/8cd562fb-3158-270f-fcfb-3a75c8836c84.png"  width="480">

ユーザーが利用するドライブに、フォルダを作成します。このフォルダに、書類を保存していきます。

# APIキーを発行する

DirectCloud-BOX のAPIを利用するために、service ID と service key を発行します。
管理者アカウントで管理画面にログインし、メニューから [共有] → [カスタム設定] を開きます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/0bacd72b-d0a6-8156-0552-19935161cc25.png" width="480">


「DirectCloud-BOX API」というタブから「使用する」を選び保存します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/d922a734-b31a-1d9b-4f3e-11b3b0f90418.png" width="480">

「ユーザー」「管理者」のそれぞれに対して「Service」「Service Key」のAPIキーが発行されます。

今回はユーザーのAPIキーを使います。


# アクセストークンを取得する

APIリファレンスを見ながら、アクセストークンを取得してみます。

https://directcloud.jp/api_reference

curlを利用したアクセストークン取得の書式は以下となります。

```bash
curl --location --request POST 'https://api.directcloud.jp/openapi/jauth/token' \
--form 'service="${serviceの値}"' \
--form 'service_key="${service kery の値}"' \
--form 'code="${会社ID}"' \
--form 'id="${ユーザーID}"' \
--form 'password="${ユーザーパスワード}"'
```

リクエストが成功すると、アクセストークンが含まれたJSONが返ってきます。

```json
{
    "success": true,
    "access_token": "${アクセストークン文字列}",
    "expire": "2021-10-18 08:52:22",
    "expire_timestamp": 1634547142
}
```

 DirectCloud-BOXのトークンは60日間有効とのことです。有効期限日の情報も含まれています。


#  フォルダのノードを確認する

次に、フォルダのノードを確認します。「ノード」とは DirectCloud-BOXのフォルダに紐づけられたIDのようです。ノードの取得は、[「フォルダリストの照会」](https://directcloud.jp/api_reference/detail/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC/Folder#%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E7%85%A7%E4%BC%9A)のエンドポイントをコールすることで確認できます。

curl の書式はこんな感じになります。

```bash
curl --location --request GET 'https://api.directcloud.jp/openapp/v1/folders/index/' \
--header 'access_token: ${アクセストークンの文字列}' \
```

リクエストが成功すると、フォルダ名とそれに対応するノード番号を含むJSONが返ってきます。

```json
{
    "success": true,
    "lists": [
        {
            "dir_seq": "37938236",
            "node": "1",
            "name": "My Box",
            "type": "my",
            "datetime": "2021-08-15 18:26:50"
        },
        {
            "dir_seq": "37413572",
            "node": "1{2",
            "name": "Shared Box",
            "type": "shared",
            "datetime": "2021-08-06 16:35:37"
        }
    ]
}
```

typeが「my」になっているのが個人用フォルダ、「shared」になっているのが共同フォルダです。
この場合、個人用フォルダのノード値である「1」をつけてコールすると、個人フォルダのノード番号がゲットできます。

```bash
curl --location --request GET 'https://api.directcloud.jp/openapp/v1/folders/index/1' \
--header 'access_token: ${アクセストークンの文字列}' \
```

下記の場合「ABCD」という文字列を利用すると、「qiita-test」というフォルダ配下にデータのアップロードが可能となります。

```json
{
    "success": true,
    "lists": [
        {
            "dir_seq": "12345678",
            "node": "ABCD",
            "name": "qiita-test",
            "type": "my",
            "datetime": "2021-08-20 19:01:40"
        }
    ]
}
```

# Credential情報をGoogle Apps Scriptのプロパティ に登録する

DirectCloud-BOX API を利用するために必要な、APIキーやユーザーのID・パスワード情報などを、Google Apps Script の[「プロパティサービス」](https://developers.google.com/apps-script/reference/properties)に登録します。

プロパティサービスは、環境変数・ブラウザのWebストレージに当たる機能で、各種データの保存を行えます。

下記のスクリプトをエディタに書き込み、実行します。

```javascript
function myFunction() {

    const scriptProperties = PropertiesService.getScriptProperties();

    scriptProperties.setProperty('api_service', '${APIキーのService文字列}');
    scriptProperties.setProperty('api_service_key', '${APIキーのService Key文字列}');
    scriptProperties.setProperty('code', '${会社コード}');
    scriptProperties.setProperty('userid', '${ユーザーのID}');
    scriptProperties.setProperty('userpass', '${ユーザーのパスワード}');

    console.log(scriptProperties.getProperty('api_service'));
    console.log(scriptProperties.getProperty('api_service_key'));
    console.log(scriptProperties.getProperty('code'));
    console.log(scriptProperties.getProperty('userid'));
    console.log(scriptProperties.getProperty('userpass'));

}
```
プロパティサービスに登録後、登録データがログに表示されるので確認を行います。登録が正常に終わっていたら、このスクリプトは削除してOKです。



# タイトルに「invoice」と書かれている請求書データを、DirectCloud-BOXへファイルをバックアップする


DirectCloud-BOX側の設定が終わったら、Google Apps Script でコードを書きます。
フローはこんな感じです。

![flowchart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/d2cc3eab-d8d2-c723-ac57-a9ac2d605101.jpeg)

実際のコードはこちらです。

```javascript
const scriptProperties = PropertiesService.getScriptProperties();
const node = '${アップロードしたいフォルダのnode文字列}';

// ファイルを検索・アップロードする関数
// 最初にアクセストークンのチェックを行う
//　　タイトルに「invoice」が含まれるファイルを検索し、DirectCloud-BOXへアップロードする
   
function fileUpload() {
  initialize();
  const fileList = DriveApp.searchFiles("title contains 'invoice'");
  while (fileList.hasNext()) {
    //ファイルを取得
    let file = fileList.next();
    let uploadData = DriveApp.getFileById(file.getId());
    let blob = Utilities.newBlob(uploadData.getBlob().getBytes(), uploadData.getMimeType(), uploadData.getName());
    const header = {
      access_token: scriptProperties.getProperty('stored_access_token')
    }
    let formData = {
      'Filedata': blob
    }
    let params = {
      'method': 'post',
      'headers': header,
      'payload': formData
    };
    let response = UrlFetchApp.fetch("https://api.directcloud.jp/openapp/v1/files/upload/" + node, params);
    let content = response.getContentText();
    console.log(content);
  }
  Logger.log('ファイルが見つかりませんでした');
}

// トークンチェック関数
// GASのプロパティにアクセストークンがセットされていない、もしくはトークンの有効期限が切れている場合
// アクセストークンの再取得を行う

function initialize() {
  const date = new Date();
  const init_token = scriptProperties.getProperty('stored_access_token');
  const init_timestamp = scriptProperties.getProperty('stored_expire_timestamp');
  const init_unixtime = Date.parse(date) / 1000;
  if (init_timestamp < init_unixtime || !init_token) getToken();
}

//  DirectCloud-BOX APIの アクセストークンを取得する

function getToken() {
  const postdata = {
    'service': scriptProperties.getProperty('api_service'),
    'service_key': scriptProperties.getProperty('api_service_key'),
    'code': scriptProperties.getProperty('code'),
    'id': scriptProperties.getProperty('userid'),
    'password': scriptProperties.getProperty('userpass')
  };
  const options = {
    'payload': postdata
  }
  const response = UrlFetchApp.fetch('https://api.directcloud.jp/openapi/jauth/token', options);
  const obj = JSON.parse(response);
  scriptProperties.setProperty('stored_access_token', obj.access_token);
  scriptProperties.setProperty('stored_expire_timestamp', obj.expire_timestamp);
}

```

DirectCloud-BOXをAPI経由で操作するためには、アクセストークンが必要となります。
このため、ファイルのアップロードを行う前に、アクセストークンが存在するか、有効期限内かをチェックしています。(関数initialize)

トークンの文字列が存在しない、もしくは期限切れの場合、トークンの再取得を行い、GASの環境変数にあたる「プロパティサービス」に値を保存します。
(関数getToken)

* アクセストークン => stored_access_token
* トークンの有効期限 => stored_expire_timestamp

に、それぞれ保存しています。

トークンが準備できていたら、タイトルに「invoice」が含まれているファイルを検索し、DirecCloud-BOXにアップロードします。
(関数fileUpload)

実行結果はログに残しておき、GASの実行ログで確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/339e6ac5-7e77-d008-236f-ac8e5e3994ad.png">


DirectCloud-BOXを確認すると、Googleドライブからファイルが同期されたことがわかります。

![スクリーンショット 2021-08-22 7.32.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/7ed1917d-e55e-5164-edf1-f02252a7688b.png)


# 定期的に実行するように設定を行う

Google Apps Script には「トリガー」と呼ばれる、Cronに似た機能が存在します。
トリガーを設定して、1日に1回、ファイル同期を行うように設定します。

イベントのソースを「時間手動型」、時間ベースのトリガータイプを「日付」にすることで、1日に一回、定期的にGASが実行されます。

![スクリーンショット 2021-08-22 7.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/76b953b9-80ec-a166-f871-a40bd6b7b36f.png)

設定終了後は、トリガー一覧で確認できます。


![スクリーンショット 2021-08-22 7.38.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/01cc21b0-b3e9-6508-be13-b3fe82e27f6f.png)


# 未実装の仕様

今回はサンプル実装ということで、ファイルを DirectCloud-BOX にアップロードするまでを開発しました。

時間の関係で実装しなかった仕様をメモしておきます。

## ファイルのIDチェック

Google ドライブのファイルは、全て一意のIDを持っています。
スプレッドシートにアップロード済みのファイルIDを記入し、同期済みのファイルはアップロードをスキップするようにできれば、より無駄のない処理になります。

## 日本語をタイトルに含むファイルの送信

GASの関数「UrlFetchApp」を実行したタイミングで、日本語を含むタイトルが文字化けするという事象が確認できました。(文字化けするのはタイトルのみで、ファイルの中身は問題ない)

これは関数の仕様なのか、別に原因があるのか、どこかで解明したい。

# DirectCloud-BOX API に対する感想

今回、初めて　DirectCloud-BOX API　を使ってみたところ、気になる点がいくつかありました。最後に列記しておきます。


* メソッドがGETとPOSTだけ

APIのメソッドがGETとPOSTだけで、ファイルの削除を行うときは、削除用エンドポイントをPOSTでコールする、という仕様でした。

([例：ファイルの削除](https://directcloud.jp/api_reference/detail/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC/File#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%89%8A%E9%99%A4))

REST API でファイル削除を行う場合、「DELETE」メソッドでコールするケースが多いため、慣れない感じがありました。

* ユーザー権限に対するAPIキーは一種類だけ？

APIキーは管理画面から発行しますが、「管理者」「ユーザー」の２種類のみ発行となっています。つまり、ユーザーが１００人いる場合、全てのユーザーが同じIDとKeyを使い回す形になるのが気になりました。

OAuth2.0のように、ユーザの権限ごとにスコープを切れれば、もっと柔軟なAPI利用が実現できるかも、と感じました。

# 雑感

今回初めて DirectCloud-BOX を触りましたが、オンラインストレージサービスという市場の熱さを感じました。PPAPの利用を禁止する企業も増えているため、オンラインストレージの需要高くなっていきそうだな、など。


以上です。