0
0

Dropbox APIを使ってファイル共有

Last updated at Posted at 2024-07-14

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からアプリを作成します。

Dropboxコンソール

アクセストークンを払い出すために、リフレッシュトークンを生成します。リフレッシュトークンがあれば、アクセストークンを生成できます。アクセストークンの有効期限は有限ですが、リフレッシュトークンは期限がないんです。
ブラウザから以下を表示します。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)からアクセストークンをいつでも生成できます。

/api/controllers/clipping-api/lib_dropbox.js
      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をそれぞれ示します。

詳細は、リファレンスを参照してください。

Dropbox for HTTP Developers

ファイルをアップロードして、自身の複数の端末から共有

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を使っています。

/api/controllers/clipping-api/lib_dropbox.js
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を設定したときに呼ばれます。

/api/controllers/clipping-api/swagger.yaml
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の処理を実装することになります。

/api/controllers/clipping-api/index.js
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に移動します。

Dropbox Console

image.png

右上の「ログイン」からログインします。

「アプリを作成」を選択します。

image.png

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にアカウントを作っていない場合は作成します。

Render.com

image.png

右上のDashboardを選択し、ログインします。

image.png

右上の「+New」をクリックし、「Web Service」を選択します。

image.png

image.png

「Public Git Repository」タブを選択し、以下のURLを入力し、「Connect」ボタンを押下します。

https://github.com/poruruba/ClippingService.git

image.png

Languageは「Node」です。
Instance TypeはFreeを選択します。

image.png

Environment Variablesのところに、DropboxのリフレッシュトークンとApp keyとApp seretを指定します。
名前はそれぞれ「DROPBOX_REFRESH_TOKEN」、「DROPBOX_CLIENT_ID」、「DROPBOX_CLIENT_SECRET」です。
また、「CLIPPING_API_KEY」も追加し、秘匿の値を指定します。

image.png

デプロイ完了まで5分近くかかります。

image.png

完了すると、左上に割り当てられたURLが表示されていますので、クリックして開きます。

image.png

右上にある「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です。

動作確認

テキストパネルの、アップロードの下のテキストエリアに適当な文字列を入力したのち、「アップロード」ボタンを押下します。
そして、「ダウンロード」ボタンを押下すると、その下に入力した文字列と同じ文字列が表示されたら成功です。

image.png

次に、ファイルパネルの「ファイルを選択」ボタンを押下して、適当なファイルを選択します。そして「アップロード」ボタンを押下します。
「チェック」ボタンを押下すると、ちゃんとアップロードされていることがわかります。

image.png

「ダウンロード」ボタンを押下すると、アップロードしたファイルがダウンロードできることがわかります。
そして「保存」ボタンを押します。そうすると、Dropboxにアップロードしてあるファイルがコピーされます。

image.png

先ほどアップロードしたファイルが表示されています。

ファイル名をクリックすると、同様にファイル保存ダイアログが表示されてダウンロードされているのがわかります。
また、「ワンタイム」ボタンをクリックすると、QRコードが表示されます。

image.png

このQRコードは、その下のテキストボックスに入力されている文字列と同じです。
このQRコードを他の方にお知らせすることで、他の方が同じファイルをダウンロードできます。このURLは4時間有効です。

また、「共有」ボタンを押すことでShared Linkとなり、永続的に他の方と共有できるようになります。

image.png

青い「共有」ボタンが表示され、こちらもQRコードが表示されます。このQRコードはこのファイルを削除するまでずっと有効です。

image.png

この時開かれるのはDropboxのページになります。今回は画像ファイルをアップロードしたのでそれに応じたページになるようです。

image.png

他の方からファイルを受け取りたい場合は、「アップロード」ボタンを押下します。

image.png

ここでも、QRコードが表示されるので、そのQRコードから開いたWebページから任意のファイルをアップロードすることができます。

以下のようなページが開かれます。

image.png

アップロードしたら、「保存・共有ファイル」パネルの右にある更新ボタンを押すとリストが更新されて、アップロードされたファイルがリストに表示されます。

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0