Dropbox APIを使って、Dropboxにファイルをアップロードし、他の人とファイルを共有します。Dropboxを一時的な格納庫として利用します。
Dropbox APIで以下のことを実現します。
- ファイルをアップロードして、自身の複数の端末から共有できます。
- アップロードしたファイルを共有して、複数の人がダウンロードできます。
- 他の人からファイルのアップロードを受領します。
もろもろのソースコードはGithubに上げてあります。
構成要素
- プライベートサーバ : ブラウザで動作するクライアントとDropboxを仲介するためのサーバです。クライアント向けのWebページの提供とWebAPIの処理を担っています。また、DropboxからのWebhook呼び出しも受け取ります。
- Dropboxサーバ : 共有するファイルを管理してもらうサーバです。
- 管理クライアント(ブラウザ): 共有するファイルを管理するためのクライアントです。PWAです。
- 一般クライアント(ブラウザ): 共有を許可されたファイルの取得や、自身のファイルのアップロードを行います。
登場人物
- 管理者 : ファイルのすべての権限を有し、アップロードを受け取ったファイルやファイルの共有の設定を行います。一般ユーザにはURLを渡して、ファイルのダウンロードやアップロードを許可します。
- 一般ユーザ : 管理者から通知されたURLを使ってファイルのダウンロードやアップロードを行います。
これを実現するにあたり、以下を考慮します。
- プライベートサーバにはファイルの保存はしません。(Render.comに立ち上げます)
- 管理クライアントはPWAとし、アプリとして端末に登録することができるようにします。(クライアントは、Windows、Android、iPhoneを対象とします。)
- 保持するファイルはDropboxに置きます。Dropboxにアカウント登録しておきます。
- 一般クライアントは、Dropboxを介してファイルのアップロード/ダウンロードをします。推測されにくいURLからのアクセスとし、一般ユーザはDropboxのユーザである必要はないです。
- Dropbox APIのアクセスに必要なアクセストークンは、自分用のサーバ内に閉じます。
- 管理クライアントからのWebAPIアクセスは、プライベートサーバと共用する秘匿のAPIキーで保護します。
Dropbox APIを使う準備
Dropboxにアカウントを作成します。
DropboxコンソールのApp Consoleからアプリを作成します。
アクセストークンを払い出すために、リフレッシュトークンを生成します。リフレッシュトークンがあれば、アクセストークンを生成できます。アクセストークンの有効期限は有限ですが、リフレッシュトークンは期限がないんです。
ブラウザから以下を表示します。App keyは、作成したアプリに表示されています。
https://www.dropbox.com/oauth2/authorize?client_id=<App key>&token_access_type=offline&response_type=code
そうすると、ブラウザにアクセスコードが表示されます。
次に、以下のHTTP Postを発行すると、リフレッシュトークンが取得できます。
curl https://api.dropbox.com/oauth2/token -d code=[アクセスコード] -d grant_type=authorization_code -u client_id=[App key] -u client_secret=[App secret]
リフレッシュトークンが生成できましたので、大事にメモっておきます。
以下の呼び出しで、リフレッシュトークンとApp key(client_id)とApp secret(client_secret)からアクセストークンをいつでも生成できます。
var input = {
url: dropbox_base_url + "/oauth2/token",
params : {
grant_type: "refresh_token",
refresh_token: this.refresh_token,
client_id: this.client_id,
client_secret: this.client_secret,
},
content_type: "application/x-www-form-urlencoded"
};
var result = await do_http(input);
console.log(result);
Dropboxでのファイル構造
App Consoleからアプリを作成したときに「App folder name」を決めました。
そうすると、Dropbox内に「/アプリ/[App folder name]/」というフォルダ名が作成され、その中にアップロードされたファイルが格納されます。
Dropbox APIから呼び出す際には、/ は、/アプリ/[App folder name]/ に読み替えられるため意識する必要はありません。
格納する際に、同じファイル名もアップロードされる可能性があるため、アップロードしたファイルは以下のようにエポック時間(msec)のフォルダの中にアップロードします。エポック時間はアップロード時の時間です。
/[エポック時間(msec)]/XXX.png
Dropbox APIを使って実現すること
Dropbox APIには、様々なWebAPIが用意されており、組み合わせることで、やりたかったことが実現できます。利用したDropboxのWebAPIをそれぞれ示します。
詳細は、リファレンスを参照してください。
ファイルをアップロードして、自身の複数の端末から共有
file-upload
ファイルをDropboxにアップロードします。Dropbox上のファイルパスとファイルの内容を指定します。
file-delete
アップロードしたファイルを削除します。Dropbox上のファイルパスを指定します。
file-list_folder
アップロードしたファイルのリストを取得します。
アップロードしたファイルを共有して、複数の人がダウンロード
files-get_temporary_link
4時間期限付きでファイルをダウンロードできるURLを生成します。Dropbox上のファイルパスを指定します。
sharing-create_shared_link_with_settings
アップロードしたファイルを共有状態にします。Dropbox上のファイルパスを指定します。
sharing-list_shared_links
共有状態のファイルのリストを取得します。
他の人からファイルのアップロードを受領
file_requests-create
アップロード先(ファイルリクエスト)を作成します。アップロードしたいDropbox上のフォルダパスを指定します。
file_requests-update
今回は、ファイルリクエストを編集して、アップロードを停止するのに使います。ファイルリクエストのIDを指定します。
file_requests-delete
ファイルリクエストを終了します。ファイルリクエストのIDを指定します。
file_requests-list
現在待ち受け中のファイルリクエストのリストを取得します。
ソースコード
扱いやすいようにライブラリにしておきました。
npmのnode-fetch@2.7.0とform-dataを使っています。
const dropbox_base_url = "https://api.dropboxapi.com";
const dropbox_content_url = "https://content.dropboxapi.com";
const FormData = require('form-data');
const { URL, URLSearchParams } = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;
class Dropbox{
constructor(client_id, client_secret, refresh_token){
this.client_id = client_id;
this.client_secret = client_secret;
this.refresh_token = refresh_token;
this.token_created = 0;
}
async retrieve_access_token(){
var now = new Date().getTime();
if( (now - this.token_created) >= (50 * 60 * 1000) ){
var input = {
url: dropbox_base_url + "/oauth2/token",
params : {
grant_type: "refresh_token",
refresh_token: this.refresh_token,
client_id: this.client_id,
client_secret: this.client_secret,
},
content_type: "multipart/form-data"
};
var result = await do_http(input);
// console.log(result);
this.token = result.access_token;
this.token_created = now;
}
return this.token;
}
async upload(path, buffer){
await this.retrieve_access_token();
var params = {
path: path,
mode: "overwrite",
};
var input = {
url: dropbox_content_url + "/2/files/upload",
body: buffer,
content_type: "application/octet-stream",
headers: {
"Dropbox-API-Arg": JSON.stringify(params)
},
token: this.retrieve_access_token(),
};
var result = await do_http(input);
return result;
}
async delete(path){
await this.retrieve_access_token();
var input = {
url: dropbox_base_url + "/2/files/delete_v2",
token: this.token,
body: {
path: path
}
};
var result = await do_http(input);
return result;
}
async list(){
await this.retrieve_access_token();
var input = {
url: dropbox_base_url + "/2/files/list_folder",
token: this.token,
body: {
path: "",
recursive: true,
}
};
var result = await do_http(input);
// console.log(result);
var entries = result.entries;
while(result.has_more){
var input = {
url: dropbox_base_url + "/2/files/list_folder/continue",
token: this.token,
body: {
cursor: result.cursor
}
};
var result = await do_http(input);
// console.log(result);
entries = entries.concat(result.entries);
}
return entries;
}
async get_temporary_link(path){
await this.retrieve_access_token();
var input = {
url: dropbox_base_url + "/2/files/get_temporary_link",
token: this.token,
body: {
path: path
}
};
var result = await do_http(input);
return result;
}
async create_share_links(path){
await this.retrieve_access_token();
var settings = {
audience: "public",
allow_download: true
};
var body = {
path: path,
settings: settings
};
var input = {
url: dropbox_base_url + "/2/sharing/create_shared_link_with_settings",
token: this.token,
body: body
};
var result = await do_http(input);
return result;
}
async list_shared_links(){
await this.retrieve_access_token();
var input = {
url: dropbox_base_url + "/2/sharing/list_shared_links",
token: this.token,
body: {}
};
var result = await do_http(input);
// console.log(result);
var links = result.links;
while(result.has_more){
var input = {
url: dropbox_base_url + "/2/sharing/list_shared_links",
token: this.token,
body: {
cursor: result.cursor
}
};
var result = await do_http(input);
// console.log(result);
links = links.concat(result.links);
}
return links;
}
async make_create_request(path, title, description){
await this.retrieve_access_token();
var body = {
title: title,
description: description,
destination: path,
open: true
};
var input = {
url: dropbox_base_url + "/2/file_requests/create",
token: this.token,
body: body
};
var result = await do_http(input);
return result;
}
async close_request(id){
await this.retrieve_access_token();
var body = {
id: id,
open: false
};
var input = {
url: dropbox_base_url + "/2/file_requests/update",
token: this.token,
body: body
};
var result = await do_http(input);
// console.log(result);
var input = {
url: dropbox_base_url + "/2/file_requests/delete",
token: this.token,
body: {
ids: [id]
}
};
var result = await do_http(input);
return result;
}
async list_request(){
await this.retrieve_access_token();
var input = {
url: dropbox_base_url + "/2/file_requests/list_v2",
token: this.token,
body: {}
};
var result = await do_http(input);
// console.log(result);
var list = result.file_requests;
while(result.has_more){
var input = {
url: dropbox_base_url + "/2/file_requests/list_v2",
token: this.token,
body: {},
cursor: result.cursor
};
var result = await do_http(input);
// console.log(result);
list = list.concat(result.file_requests);
}
return list;
}
}
// input: url, method, headers, qs, body, params, response_type, content_type, token, api_key
async function do_http(input){
const method = input.method ? input.method : "POST";
const content_type = input.content_type ? input.content_type : "application/json";
const response_type = input.response_type ? input.response_type : "json";
const headers = new Headers();
if( content_type != "multipart/form-data" )
headers.append("Content-Type", content_type);
if( input.token )
headers.append("Authorization", "Bearer " + input.token);
if( input.api_key )
headers.append("x-api-key", input.api_key);
if( input.headers ){
for( const key in input.headers )
headers.append(key, input.headers[key]);
}
let body;
if( content_type == "application/x-www-form-urlencoded"){
body = new URLSearchParams(input.params);
}else if( content_type == "multipart/form-data"){
body = Object.entries(input.params).reduce((l, [k, v]) => { l.append(k, v); return l; }, new FormData());
}else if( content_type == "application/json" ){
if( input.body )
body = JSON.stringify(input.body);
}else{
body = input.body
}
const params = new URLSearchParams(input.qs);
var params_str = params.toString();
var postfix = (params_str == "") ? "" : ((input.url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));
return fetch(input.url + postfix, {
method: method,
body: body,
headers: headers
})
.then((response) => {
if (!response.ok)
throw new Error('status is not 200');
if( response_type == "json" )
return response.json();
else if( response_type == 'blob')
return response.blob();
else if( response_type == 'file'){
const disposition = response.headers.get('Content-Disposition');
let filename = "";
if( disposition ){
const parts = disposition.split(';').find(item => item.trim().startsWith("filename") );
filename = parts.trim().split(/=(.+)/)[1];
if (filename.toLowerCase().startsWith("utf-8''"))
filename = decodeURIComponent(filename.replace(/utf-8''/i, ''));
else
filename = filename.replace(/['"]/g, '');
}
return response.blob()
.then(blob =>{
return new File([blob], filename, { type: blob.type })
});
}
else if( response_type == 'binary')
return response.arrayBuffer();
else // response_type == "text"
return response.text();
});
}
module.exports = Dropbox;
DropboxからWebhookを受けるには
DropboxからWebhookを受け付けるプライベートサーバのエンドポイントには、GETとPOSTの2つのメソッドを受け取れるようにしておく必要があります。
GETは、最初のverification requestのときに呼ばれ、決まった応答を返す必要があります。
App Consoleで、Webhook URIsを設定したときに呼ばれます。
paths:
/clipping-webhook:
get:
responses:
200:
description: Success
schema:
type: string
post:
parameters:
- in: body
name: body
schema:
type: object
responses:
200:
description: Success
schema:
type: object
実際のWebhookはPOSTで呼ばれるので、基本的にはPOSTの処理を実装することになります。
async function process_webhook(event, context, callback){
console.log("process_webhook called");
if( event.httpMethod == 'GET'){
// verification requestの処理
var qs = event.queryStringParameters;
console.log(qs);
return new TextResponse("text/plain", qs.challenge);
}
// 実際のWebhookの内容
var body = JSON.parse(event.body);
console.log(body);
・・・
デプロイ方法
Dropboxのセットアップ
まだDropboxにアカウントを作っていない場合は作成します。
Dropbox Consoleに移動します。
右上の「ログイン」からログインします。
「アプリを作成」を選択します。
Choose an APIとして「Scoped access」を選択し、type of accessとして「App folder」を選択し、Name your appとして適当な名前を入力します。他の人と重複しないようにする必要があります。
次に、「Permission」タブを選択して、Permissionを設定します。
以下にチェックを入れます。
・files.metadata.write
・files.metadata.read
・files.content.write
・files.content.read
・sharing.write
・sharing.read
・file_request.write
・file_request.read
最後に、Submitをクリックします。
次に、「Settings」タブに戻って、Generated access tokenのところの「Generate」ボタンを押下します。そうすると、長い文字列のアクセストークンが生成されますのでメモっておきます。
Render.comにデプロイ
Render.comにアカウントを作っていない場合は作成します。
右上のDashboardを選択し、ログインします。
右上の「+New」をクリックし、「Web Service」を選択します。
「Public Git Repository」タブを選択し、以下のURLを入力し、「Connect」ボタンを押下します。
https://github.com/poruruba/ClippingService.git
Languageは「Node」です。
Instance TypeはFreeを選択します。
Environment Variablesのところに、DropboxのリフレッシュトークンとApp keyとApp seretを指定します。
名前はそれぞれ「DROPBOX_REFRESH_TOKEN」、「DROPBOX_CLIENT_ID」、「DROPBOX_CLIENT_SECRET」です。
また、「CLIPPING_API_KEY」も追加し、秘匿の値を指定します。
デプロイ完了まで5分近くかかります。
完了すると、左上に割り当てられたURLが表示されていますので、クリックして開きます。
右上にある「APIキー」ボタンを押下して、先ほど指定した「CLIPPING_API_KEY」の秘匿の値を指定します。
もう一度Ctrl+F5を押してリロードします。
これで成功したはずです。
ちなみに、このページはPWA対応にしているため、ChromeやSafariからアプリとして登録することができます。
DropboxのWebhook設定
Dropboxの App Consoleに戻って、Settingsタブを開きます。
「Webhook URIs」のところに、以下のURLを入力します。
https://[Render.comによって割り当てられたホスト名]/clipping-webhook
「Add」ボタンを押下して、EnableになればOKです。
動作確認
テキストパネルの、アップロードの下のテキストエリアに適当な文字列を入力したのち、「アップロード」ボタンを押下します。
そして、「ダウンロード」ボタンを押下すると、その下に入力した文字列と同じ文字列が表示されたら成功です。
次に、ファイルパネルの「ファイルを選択」ボタンを押下して、適当なファイルを選択します。そして「アップロード」ボタンを押下します。
「チェック」ボタンを押下すると、ちゃんとアップロードされていることがわかります。
「ダウンロード」ボタンを押下すると、アップロードしたファイルがダウンロードできることがわかります。
そして「保存」ボタンを押します。そうすると、Dropboxにアップロードしてあるファイルがコピーされます。
先ほどアップロードしたファイルが表示されています。
ファイル名をクリックすると、同様にファイル保存ダイアログが表示されてダウンロードされているのがわかります。
また、「ワンタイム」ボタンをクリックすると、QRコードが表示されます。
このQRコードは、その下のテキストボックスに入力されている文字列と同じです。
このQRコードを他の方にお知らせすることで、他の方が同じファイルをダウンロードできます。このURLは4時間有効です。
また、「共有」ボタンを押すことでShared Linkとなり、永続的に他の方と共有できるようになります。
青い「共有」ボタンが表示され、こちらもQRコードが表示されます。このQRコードはこのファイルを削除するまでずっと有効です。
この時開かれるのはDropboxのページになります。今回は画像ファイルをアップロードしたのでそれに応じたページになるようです。
他の方からファイルを受け取りたい場合は、「アップロード」ボタンを押下します。
ここでも、QRコードが表示されるので、そのQRコードから開いたWebページから任意のファイルをアップロードすることができます。
以下のようなページが開かれます。
アップロードしたら、「保存・共有ファイル」パネルの右にある更新ボタンを押すとリストが更新されて、アップロードされたファイルがリストに表示されます。
以上