空雲 Blog

Eye catchFirebaseStorage(CloudStorage)をEdgeRuntimeで操作する

publication: 2023/11/26
update:2024/02/20

FirebaseStorageを操作する方法

一般的な方法として、FirebaseStorageを操作する方法は以下のようになります。

  • OAuthを使う場合
    @firebase/storageパッケージを使う

  • PrivateKeyを使う場合
    firebase-adminパッケージを使う

今回やりたいのはPrivateKeyを使って、追加で認証を挟まずアクセス可能にする方法です。さらにNext.jsのEdgeRuntime対応です。一般的にはfirebase-adminを使うことになるのですが、このライブラリはNode.jsのフル機能を要求するため、EdgeRuntimeには対応していません。

要件を満たすものを探したのですがなかなか見つけられず、そうなると自分で作った方が早いという結論に至りました。

  • npmでパッケージ化したもの

https://www.npmjs.com/package/firebase-storage

RestAPIによるFirebaseStorageの操作

仕様は以下のところで公開されています。
https://cloud.google.com/storage/docs/json_api/v1

ということで早速作っていきましょう。

認証

FirebaseStorageをRestAPIで操作しようとする時、初っ端にそびえる最大の関門です。ドキュメントを見ても、まともに説明されていません。

必要なのはPrivateKeyとClientEmailからJWTでキーを作成することです。ここで問題になるのはcryptoのパッケージはEdgeRuntimeでは動かないということです。JWTの操作はcryptoを使っていないものを選ぶ必要があります。そのため、Edgeに対応しているjoseを使います。

認証用Tokenを使った最小サンプルは以下のようになります。

1import { SignJWT, importPKCS8 } from "jose";
2
3export const createToken = ({
4 clientEmail,
5 privateKey,
6}: {
7 clientEmail: string;
8 privateKey: string;
9}) =>
10 importPKCS8(privateKey, "RS256").then((key) =>
11 new SignJWT({
12 iss: clientEmail,
13 sub: clientEmail,
14 scope: "https://www.googleapis.com/auth/cloud-platform",
15 iat: Math.floor(Date.now() / 1000) - 30,
16 exp: Math.floor(Date.now() / 1000) + 3600,
17 })
18 .setProtectedHeader({ alg: "RS256", typ: "JWT" })
19 .sign(key)
20 );

RestAPIの呼び出し

残りは普通にRestAPIで操作するだけなので、まとめて紹介します。

1export interface StorageObject {
2 kind: string;
3 id: string;
4 selfLink: string;
5 mediaLink: string;
6 name: string;
7 bucket: string;
8 generation: string;
9 metageneration: string;
10 contentType: string;
11 storageClass: string;
12 size: string;
13 md5Hash: string;
14 cacheControl: string;
15 crc32c: string;
16 etag: string;
17 timeCreated: string;
18 updated: string;
19 timeStorageClassUpdated: string;
20}
21
22export const info = ({
23 token,
24 bucket,
25 name,
26}: {
27 token: string;
28 bucket: string;
29 name: string;
30}): Promise<StorageObject> => {
31 const url = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${name}`;
32 return fetch(url, {
33 method: "GET",
34 headers: {
35 Authorization: `Bearer ${token}`,
36 },
37 }).then((res) => {
38 if (res.status !== 200) throw new Error(res.statusText);
39 return res.json();
40 });
41};
42
43export const download = ({
44 token,
45 bucket,
46 name,
47}: {
48 token: string;
49 bucket: string;
50 name: string;
51}) => {
52 const url = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${name}?alt=media&no=${Date.now()}`;
53 return fetch(url, {
54 method: "GET",
55 headers: {
56 Authorization: `Bearer ${token}`,
57 },
58 }).then((res) => {
59 if (res.status !== 200) throw new Error(res.statusText);
60 return res.arrayBuffer();
61 });
62};
63
64export const upload = ({
65 token,
66 bucket,
67 name,
68 file,
69 published,
70 metadata,
71}: {
72 token: string;
73 bucket: string;
74 name: string;
75 file: Blob;
76 published?: boolean;
77 metadata?: { [key: string]: unknown };
78}) => {
79 const id = encodeURI(name);
80
81 const url = `https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?name=${id}&uploadType=multipart${
82 published ? "&predefinedAcl=publicRead" : ""
83 }`;
84 const body = new FormData();
85 body.append(
86 "",
87 new Blob([JSON.stringify({ metadata })], { type: "application/json" })
88 );
89 body.append("", file);
90 return fetch(url, {
91 method: "POST",
92 headers: {
93 Authorization: `Bearer ${token}`,
94 },
95 body: body,
96 }).then((res) => {
97 if (res.status !== 200) throw new Error(res.statusText);
98 return res.json();
99 });
100};
101
102export const del = ({
103 token,
104 bucket,
105 name,
106}: {
107 token: string;
108 bucket: string;
109 name: string;
110}) => {
111 const url = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${name}`;
112 return fetch(url, {
113 method: "DELETE",
114 headers: {
115 Authorization: `Bearer ${token}`,
116 },
117 }).then((res) => {
118 if (res.status !== 204) throw new Error(res.statusText);
119 return true;
120 });
121};
122
123export const list = ({
124 token,
125 bucket,
126}: {
127 token: string;
128 bucket: string;
129}): Promise<StorageObject[]> => {
130 const url = `https://storage.googleapis.com/storage/v1/b/${bucket}/o`;
131 return fetch(url, {
132 headers: {
133 Authorization: `Bearer ${token}`,
134 },
135 })
136 .then((res) => {
137 if (res.status !== 200) throw new Error(res.statusText);
138 return res.json();
139 })
140 .then((res) => res.items);
141};

ちなみにオブジェクトのデータを取得する時、StorageObjectのような構造になっているのですが、注意点として全て文字列型だということです。

https://cloud.google.com/storage/docs/json_api/v1/objects

上記ドキュメントには文字列以外の型が書いてありますが嘘です。全部文字列です。必要なら自分で型を変換してください。

その他の点として、upload時にmetadataを同時指定するのであればmultipartでデータを送る必要があります。データをFormDataにセットするだけなので、やり方さえ知っていれば簡単です。

まとめ

コード的には大した量にならず、簡単な内容で処理を書くことが出来ます。ただ、ドキュメントから具体的な使い方を理解するまでがそれなりに面倒です。