Cloudflare WorkersでLINE Botに送った写真や動画をR2にアップロード


Cloudflare Workersの勉強中です。LINEに送った写真をR2にアップロードする処理を書いてみます。

`wrangler v3.22.4`で試してます。

## LINEアルバムに写真は載るけど動画は載らない

という、LINEの仕様問題があるのでグループに上げた動画をR2に保存する仕組みみたいなものを作りたいなと思いました。

（ちなみに最近はLYPプレミアムに加入すると動画アルバムも作れるぽいです。 https://premium.yahoo.co.jp/ )

## ファイルシステムが無い

Cloudflare Workersだとファイルシステムが無いのでローカルに保存後にアップロードみたいなことができません。

LINEのAPIにアクセス -> DL -> ArrayBufferに詰める -> R2に上げる といった流れでやってみました。

### 準備 - LINEから写真を受け取る

まずはオウム返し的なLINE Botを作りましょう。（記事は特に今の所ないですが）

### Fetch APIでファイルをダウンロード

Cloudflare Workers上だとLINEのNode.js SDKはおそらく使えないので、Fetch APIでDLする方法を調べます。

```js
const res = await fetch("https://example.com/path/to/image.png");
const arrayBuffer = await res.arrayBuffer();
```

こんな感じで受け取れるみたいですね。

参考: 

https://qiita.com/miyabisun/items/6eb693d04c6eec838fd6

### LINE Botに送信された写真をダウンロード

LINE Messaging APIの`https://api-data.line.me/v2/bot/message/(messageId)/content`のエンドポイントからDLできます。

DLするファイルが動画なのか画像なのかなども知りたいですが、`res.headers`で調べられそうでした。

参考: 
https://scrapbox.io/takker/fetch_API%E3%81%A7%E5%BF%9C%E7%AD%94%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B

```js
const API_ENDPOINT = `https://api-data.line.me/v2/bot/message/${event.message.id}/content`
const options = {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${env.CH_ACCESS_TOKEN}`,
    },
};

const res = await fetch(API_ENDPOINT, options);
const arrayBuffer = await res.arrayBuffer(); //ダウンロードしてArrayBufferに格納

//扱うファイルのタイプを取得 'video/mp4'だったり'image/jpeg'だったり
const headers = res.headers;
const filetype = headers.get('content-type');
```

### ファイルをR2にアップロード

こちらが参考になりました。

https://zenn.dev/steelydylan/scraps/3fad2d7ce50c1a


こんな感じでアップロード(put)できます。

```js
const bucket = env.NOBISUKE_BUCKET;

const response = await bucket.put(filename, arrayBuffer, {
    httpMetadata: {
        contentType: filetype,
    }
});
```

## まとめる - LINE Messagin APIから写真をDLしてR2にアップロードまで

まとめるとこんな感じでできました。

```js

//写真や動画のDL
const API_ENDPOINT = `https://api-data.line.me/v2/bot/message/${event.message.id}/content`
const options = {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${env.CH_ACCESS_TOKEN}`,
    },
};

const res = await fetch(API_ENDPOINT, options);
const arrayBuffer = await res.arrayBuffer();

//ファイルタイプ(MIMEタイプ)を取得
const headers = res.headers;
const filetype = headers.get('content-type');
console.log(filetype);

//ファイル名をLINEのMessageIDから生成
let filename = '';
if(filetype === 'video/mp4'){
    //filename = `${groupId}/${event.message.id}.mp4`; //GroupIDなどをフォルダ名にしたりも
    filename = `${event.message.id}.mp4`;
}else if(filetype === 'image/jpeg'){
    filename = `${event.message.id}.jpg`;
}

//R2のバケットにアップロード
const bucket = env.NOBISUKE_BUCKET;
const response = await bucket.put(filename, arrayBuffer, {
    httpMetadata: {
        contentType: filetype,
    }
});

console.log(`---R2にput成功...`)
console.log(response, response.key);
```

これで写真や動画がR2にアップロードされます。 だがしかし。

## 試してみて

### たまにキャンセルされる

Cloudflare Workersだとリクエストに対してCPUの時間が10msという制限がある模様なので、おそらく重たいファイルなどを扱うときにここで詰まるのかなと思いました。

僕が試した時は

- iPhoneで撮った写真はほぼ問題ないけど、2割くらいこけてた
- 8秒くらいの動画は短い動画は保存されて、30秒以上くらいの動画は全てこけてた

って感じでした。

### まとめ

ちょっと挙動的にどこに問題があるのか分かって無いですが、軽いサイズのファイルだとこれで問題なさそうです。

そもそも動画は[Cloudflare Stream](https://www.cloudflare.com/ja-jp/products/cloudflare-stream/)を使うのが良いのか？など調べつつでしたが無料スタートがなさそうだったので一旦こんなところでまとめました。

> ![スクリーンショット 2024-01-29 17.06.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/ced97a11-6ab8-d8a5-a697-29c297fb102b.png "スクリーンショット 2024-01-29 17.06.52.png")

たまにキャンセルされる問題はこうすると良いよって話があれば教えてもらえると嬉しいです。