最初に
個人で使えるストレージはいくつかあるかと思いますが、今回はDropbox
を使ってみました。
ただAPIやSDKを利用しようとすると中々情報が少なかったです。
そこで個人で試行錯誤した内容などをこちらの記事として公開することにしました。
間違ってる箇所があれば有識者の指摘をお待ちしています。
Dropbox 選定理由
- 無料で利用可能、容量はそこまで必要ではない
- 複数ユーザーがWeb上でファイルの共有が可能
- ユーザー追加の敷居が低い
共有ドライブとして使われる候補にGoogleDrive
があるのですが、他のユーザーがGoogleの規約的にアウトなファイルを入れると、ドライブの持ち主がBANされるとかされないとか。
個人のアカウントは今や各所で利用しているので、使えなくなるリスクは許容できませんでした。
あとはGCPのCloudStorageやAWSのS3あたりも無料枠を利用すれば良いのですが、Cloud系のユーザー権限管理をしたくなかったので今回は利用しません。
API利用開始
ここについては調べれば情報は出てくるので、ある程度省略します。
Dropbox V2 の認証方法を簡易的にまとめています。
詳細
Dropbox Developersでアプリを作成
App folder– Access to a single folder created specifically for your app.
作成時にアプリ用のフォルダを作るか求められるので、既存のフォルダを利用する必要がなければこれを選びましょう。
これを選ばない場合自身のDropboxの全てのフォルダにアクセスできるアプリになるので注意が必要です。
Settingsで以下の項目を取得
APIの設定で利用します
- App key
- App secret
Permissions 設定をする
ファイルを取得したりWeb上でアクセスできるリンクを生成するだけなら、これで良いでしょう。
アップロード機能を付けたい場合はwrite
の権限などを追加してください。
- account_info.read
- files.metadata.read
- files.content.read
- sharing.write
- file_requests.read
アクセスコードを取得する
{app_id}を先程のApp key
の値に置換してアクセス
注意点としてAPIの権限変更をした場合に再度実行しないと反映されませんでした。
Refersh Token を取得する
ここからWeb画面でできなくなります。
POSTできるなら何でもいいのですが、せっかくなのでaxiosで取得できるコードを残しておきます。
先程までに取得したApp key
, App secret
, アクセスコード
を変数として渡して実行してください。
表示されたのがRefresh Tokenです。
const options = {
method: 'POST',
url: 'https://api.dropbox.com/oauth2/token',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: {
client_id: process.env.DROPBOX_ID,
client_secret: process.env.DROPBOX_SECRET,
code: process.env.DROPBOX_ACCESS_CODE,
grant_type: 'authorization_code',
},
};
axios
.request(options)
.then((response: any) => {
console.log(response.data);
})
.catch((error: any) => {
console.error(error);
});
環境設定
環境変数として設定する
App key
, App secret
, Refersh Token
をenvファイルなどに設定します。
DROPBOX_ID=''
DROPBOX_SECRET=''
DROPBOX_REFRESH_TOKEN=''
ライブラリをインストールする
npm install dropbox
初期化方法
ここ以降dbx
とあればDropboxのクライアントを初期化したものとしてください。
import { Dropbox } from 'dropbox';
const dbx = new Dropbox({
clientId: process.env.DROPBOX_ID,
clientSecret: process.env.DROPBOX_SECRET,
refreshToken: process.env.DROPBOX_REFRESH_TOKEN,
});
ファイル情報を取得する
ファイル名とパスを指定してマッチしたデータを取得する
注意点として、『xxx1』などの末尾数字で検索をすると『xxx2』のファイルも取得できてしまう。類似パターン『xxx01』の場合は『xxx02』は取得できない。
検証はできていませんが、拡張子も『json』『jsonl』だったり『js』『jsx』などの類似パターンもあるため、拡張子までを含めた完全一致検索にも穴はできる可能性があります。
そのため自身でフィルタリングを実装する必要も出てきます。
const fileName = 'yoshi';
const pathName = '/test'
const response = await dbx.filesSearchV2({
query: fileName,
options: {
path: `${pathName}`,
},
});
メタデータを取り出す
かなりデータ構造の階層が深いのと、Dropboxの型ガードを途中で挟む必要があります。
const matches = response.result.matches;
const metadata = matches
.map((match) => match.metadata)
.filter((metadata): metadata is files.MetadataV2Metadata =>
isMetadataV2Metadata(metadata)
);
// 別関数で定義
const isMetadataV2Metadata = (
value: unknown
): value is files.MetadataV2Metadata => {
return value !== null && typeof value === 'object' && 'metadata' in value;
};
任意の必要なデータに加工/フィルタリングする
まずはファイル名で完全一致させて、今回必要なパス名を取得します。
参考 path_display: '/test/yoshi.jpg'
const pathDisplays = metaData
// metadataには類似した検索結果が含まれるため、厳密に名前を取り出す必要がある
// 取得したファイル名には拡張子が含まれるため、split()で取り出して比較する
.filter((metadata) => metadata.metadata.name.split('.')[0] === fileName)
// path_display は型にundefinedを含むため除外する必要がある
.map((data) => data.metadata.path_display)
.filter(
(pathDisplay): pathDisplay is Exclude<typeof pathDisplay, undefined> =>
pathDisplay !== undefined
);
ファイルの共有リンクを取得する
先程取得したパス名からではWeb上のファイルにURLでアクセスすることができません。
そのため共有リンクを取得/作成を行います。
共有リンクを取得
返り値がない、もしくはエラーが発生した場合に共有リンクを作成します。
※ try, catch は適宜利用してください
const response = await dbx.sharingListSharedLinks({
path: pathDisplay,
});
const [link] = response.result.links;
return link.url;
共有リンクを作成
const response = await dbx.sharingCreateSharedLinkWithSettings({
path: pathDisplay,
});
return response.result.url;
共有リンクからWebのダウンロードURLに変換する
取得or作成した共有リンクはあくまでもDropboxのサービス上で閲覧可能な状態になります。
GoogleDrive的に言い換えるならば、共有リンクをもらってアクセスしただけの状態です。
そのためダウンロード用のURLに置換する必要があります。
ここまでしてやっとWeb上でアクセス可能な画像ファイルとして扱うことが可能になりました。
const toDownloadUrl = (url: string): string => {
return url.replace('www.dropbox.com', 'dl.dropboxusercontent.com');
};
テストコードを実装する
結論からいうと自動モックは使えないためマニュアルモックが必要でした。
早速いつものようにprototypeをspyOnして設定して実行するとエラーに…
import { Dropbox } from 'dropbox';
jest.spyOn(Dropbox.prototype, 'filesSearchV2')
.mockRejectedValueOnce(new Error('テスト用エラー'));
Cannot spy on the filesSearchV2 property because it is not a function; undefined given instead. If you are trying to mock a property, use
jest.replaceProperty(object, 'filesSearchV2', value)
instead.
試しにライブラリ自体のソース検索をすると…
node_modules/dropbox/lib/routes.js
var routes = {};
// 中略
routes.filesSearchV2 = function (arg) {
return this.request('files/search_v2', arg, 'user', 'api', 'rpc', 'files.metadata.read');
};
// 中略
export { routes };
node_modules/dropbox/src/dropbox.js
import { routes } from '../lib/routes.js';
export default class Dropbox {
constructor(options) {
// 中略
Object.assign(this, routes);
}
// 中略
}
初期化するDropboxクラスはあるのですが、肝心のメソッドが後付されてるようです。
これではprototypeアクセスでのモック設定ができません。
テストファイルで初期化して設定することならできますが、これだとクラスのテストにしかならず、今回のようなメソッドの中で初期化している場合のテストには使えません。
const dbx = new Dropbox();
jest.spyOn(dbx, 'filesSearchV2');
そこでjest
のドキュメントからマニュアルモックの利用を試みました。
※ 詳細な手順や注意点はドキュメントを参照してください。
マニュアルモックとして想定する標準のテストを実装して返り値に設定します。
テスト実行中にdropbox
のライブラリが読み込まれると、このテスト用のクラスが自動で設定されることになります。
__mocks__/dropbox.ts
class Dropbox {
async filesSearchV2() {
const metadata = {
metadata: {
name: 'test_filename.jpg',
path_display: '/123456789001010101/test_filename.jpg',
},
};
return {
result: {
matches: [{ metadata }],
},
};
}
async sharingListSharedLinks() {
const url = 'https://www.dropbox.com/s/testdir/test_filename.jpg?dl=0';
return {
result: {
links: [{ url }],
},
};
}
async sharingCreateSharedLinkWithSettings() {
const url = 'https://www.dropbox.com/s/testdir/test_filename.jpg?dl=0';
return {
result: { url },
};
}
}
export { Dropbox };
マニュアルモックを設定すると常にこのテスト用の設定になるため、実際のライブラリをテストする際にはunmock
を使って解除する必要があります。
jest.unmock('dropbox');
まとめ
使用事例数が少ないのでDropboxSDKのドキュメントをかなり確認しました。
…が全部英語なので、ブラウザの翻訳機能などを活用して読み解く必要があります。
興味のある人は見てみるのも良いでしょう。大半の人は秒で閉じるんじゃないかな…
レスポンスのデータ構造が深かったり、テスト設定をしにくい構成なので、使いにくさを感じます。
特別な要件がなければ普通にS3やGCSをオススメしますが、それでも使いたいという人の役に立てば幸いです。