watermint.org - Takayuki Okazaki's note

Dropbox API: ネームスペースとパスの表現

Dropbox APIファイル操作をするときに必ず必要になってくるのがネームスペースの概念とファイルやフォルダへのパスです。実際にはほとんど意識することなく使えるのですが、知っていると見えてくる仕様もあるのでそのあたりを詳しく見てみましょう。

Dropboxがどのようにファイルを管理しているか

Dropboxがどのようにファイルを管理しているかみていきます。まずは次の記事を参照してみます。

これはStreaming同期という機能を紹介したブログエントリですがDropboxファイルシステムについても言及しています。Dropboxでは、ファイルシステムを抽象化するにあたってネームスペースという概念を定義しています。ネームスペースはすべてのユーザーのルートフォルダにあたり、また共有フォルダのように他のユーザーと共有しているスペースについては別のネームスペースが与えられます。

ユーザーがいくつかの共有フォルダを持っている場合、それぞれの共有フォルダのネームスペースは特定のパス以下にマウントされている状態となります。

パスの書式

APIドキュメントのPath formatsというセクションに詳しく書かれていますがいくつか特徴をピックアップしてみます。

  • ”” (空文字列)はルートフォルダを示す (ルートフォルダは “/” ではない)
  • すべてのファイルやフォルダはIDを持っていて、このIDでも表現できる (例: "id:abc123xyz")
  • IDで表現したフォルダからさらに相対パスを指定できる (例: "id:abc123xyz/hello.txt")
  • ネームスペースIDを指定して、相対パスを指定できる (例: "ns:12345/cupcake.png")
  • アルファベット大文字小文字の違いは無視されて、/A/B/c.txtも/a/b/c.txtも同一視される
  • パスの区切り文字は “/”. Windowsで利用されている “" ではありませんのでご注意

なお、ネームスペースIDやフォルダのIDを指定した場合、当然ながら該当フォルダなどへのアクセス権限が必要となります。

実行例

下図はルートフォルダからのファイル一覧を取得する例です。ルートフォルダから一覧を取得するためにpathには”“を指定しています。

curl -X POST https://api.dropboxapi.com/2/files/list_folder \
    --header "Authorization: Bearer <access token>" \
    --header "Content-Type: application/json" \
    --data "{\"path\": \"\"}"

ルートフォルダを示すためにもし、”/” のようにスラッシュを入れてしまうと次のようなエラーが返されます。

Error in call to API function “files/list_folder”: request body: path: Specify the root folder as an empty string rather than as “/”.

ネームスペース

ネームスペースについてもう少し詳しく見てみましょう。Namespace Guideにいくつか例がありますが、契約しているプランなどによって共有フォルダなどの種類に若干違いがあります。

ネームスペースとなるものとしては下記5つが列挙されています。いずれの場合も、ネームスペース = アクセス制御の範囲と考えると良いでしょう。

  • ユーザーのホームフォルダ
  • 共有フォルダ
  • チームフォルダ
  • チームルートフォルダ
  • アプリケーションフォルダ

ユーザーのホームフォルダはユーザー毎に作成されるものです。共有フォルダは共有フォルダとして新しくフォルダを共有した段階でそのフォルダが独立した別のネームスペースとして管理されます。

チームフォルダにはいくつか種類があるのですがどの場合でも独立したネームスペースとして管理されます。チームフォルダではチームフォルダ以下に作成したフォルダに追加のメンバーを設定することができるなど、共有フォルダにはできない設定が可能です。この場合、追加のメンバーを設定した下位のフォルダも実装上は別のネームスペースとして管理されることになります。

アプリケーションフォルダはアクセス範囲を制限した、Dropbox APIを利用するアプリケーション向けのフォルダです。各ユーザーのルートフォルダ以下「/アプリ/<アプリ名>」のようなフォルダにアプリケーション専用のデータを格納することができます。アプリケーションからみるとこのフォルダがルートフォルダとなり、ユーザーのホームフォルダなど他のネームスペースとなる部分にはアクセスできません。

Dropbox用ユーティリティ toolbox

Dropbox APIを使ったプログラムを昨年から幾つか作っています。いまこぢんまりと作っているのがtoolboxというプログラムで今回はこれについて少し紹介させてください。

toolboxはGoで書いているプログラムで、ファイルの操作やDropbox Businessチーム管理などの機能をコマンドラインから実行できるようにしたい。というものです。 Dropbox APIのようなAPIを直接呼び出すのは面倒でも、コマンドラインになっていればシェルスクリプトなどからコマンドを組み合わせて簡単に使えるからです。

このtoolboxには類似のプロジェクトとしてdbxcliというものがあります。これも、コマンドラインからファイル管理やDropbox Businessチーム管理を実現するようなものです。

車輪の再発明的ですが解決しようとしている問題がすこし違うのでその点について紹介できればと思っています。

toolboxのねらい

ファイルの移動やコピー操作などはエクスプローラーやFinderなどでやれば簡単ですが、大量のファイルとなるとそうはいかないことが多いです。たとえば4TBのハードディスクから、4TB空きのあるNASへコピーするというのは意外とやっかいです。ネットワーク接続が切れて途中で失敗したり、パソコンがフリーズしたり、何かとトラブルにぶつかります。

NASからDropboxへ移行したり、前職で大量のファイルやサーバを管理していた経験から、こういったときにはrsyncなどのように信頼がおけ、帯域制御、再実行などもよく考えられたコマンドを利用するようにしています。

toolboxは単にコマンドラインで簡単に実行できるようにするというよりは、実運用において信頼性のおけるコマンドになることを目指しています。

大量ファイル・フォルダへの対応

Dropboxへアップロードしたファイル数が増加してくると、場合によってはパフォーマンスに影響がでると言われています。Dropbox で保存可能なファイル件数によれば30万件を超えるとパフォーマンスが低下してくるそうです。

実際に、自分のアカウントにNASから移行した70〜80万ファイルを置いていたときには、同期処理完了までの時間が気になることがありました。これらのファイルを別のフォルダに移動したり、保存しているフォルダ名を変更するとかなりの時間がかかってしまいます。

処理が遅いだけでいずれは終了するのですがもう少し早く解決できないかということで大量のファイル向けの処理をコマンドラインから実行できるようにしました。

利用方法

現在の実装では移動とコピーを実装しています。(まだちゃんと自動テストなど整備できていませんし、動作を保証するものではありませんのでご利用の際には自己責任で)

バイナリはGoでコンパイルしたものでWindows版(32bit)、Linux版(32bit)、macOS版(64bit)を用意しています。下図はmacOSでの実行例ですがコマンドラインから展開したファイルのうち、OSや環境に合わせたものを実行してもらうと利用方法が出力されます。macOS版はdarwinと名前のついたものです。

% ./tbx-32.1.0.0-darwin-10.6-amd64

Usage: ./tbx-32.1.0.0-darwin-10.6-amd64 COMMAND

Available commands:
  file       File operation


Run './tbx-32.1.0.0-darwin-10.6-amd64 COMMAND' for more information on a command.

please specify sub command

プログラムの引数にコマンド、サブコマンド、パラメータなどを指定していきます。ファイルを移動する場合には次のように実行します。

% ./tbx-32.1.0.0-darwin-10.6-amd64 file move /移動もとパス /移動先のパス

実行すると初回は認証のためのURLが表示されるので、Dropboxにログインしているブラウザに貼り付けて認可して、トークンをコピペします。このトークンは$HOME/.tbx以下に保存しているので2度目以降は聞かれません。

トークンを保存しているファイルの権限は他者に読み取られないよう設定していますが、それでも様々な攻撃方法があるかと思うので、ある程度コマンドの体裁が整ってきたら、トークンを保存しないように仕様変更しようかと思っています。

toolboxの設計

最初はひさしぶりにDDDなんかでモデリングして作ろうかと考えましたが、あきらめてほとんどコマンドをべた書きしています。いずれリファクタリングするかもしれません。

というのも「ファイルを移動する」という簡単なユースケースでも実際には様々な要件があったり、エラー対応が必要となります。そういったエラー処理をすべて抽象化しながら書いていくにはかなり手間がかかりますし、「ファイルを移動する」で使えていた経験が「ファイルをコピーする」では通用しないといった場合もありますので抽象化に時間をかけるよりは、ソースコードコピペは許容しつつ、自動テストに力を入れようと考えています。

いまのところ、まだ自動テストのカバレッジもほとんどなくて寂しい限りですが、コマンドがある程度揃ってきたらブラックボックステストを幾つか最初に追加する予定です。

動作仕様

最終的にはrsyncや、mv/cp/rmなどUNIXライクOSで見かけるコマンドと同じような操作感になるよう仕上げたいと思っているので仕様はまだころころ変えていく予定です。あしからずご了承ください。

また野望としてはwebサーバを立ち上げてWebベースのGUIてきなツールも提供できればと考えていますが、セキュリティーをどう担保するかと言った課題もありますし、まだ大分先になりそうです。

Dropbox API: ファイルのアップロード

今回はDropbox APIを使ってファイルをアップロードをする流れをご紹介します。ファイルアップロード処理は/files/uploadを使えば良いのですが、ファイルサイズが大きい場合には一つのリクエストで処理するのではなく分割してアップロードすることが求められます。

分割アップロードする場合には、次のように3つの手順をたどります。

  1. 分割した最初のチャンクをアップロード (戻り値としてセッションIDが得られます) - /files/upload_session/start
  2. セッションIDをもとに最後の一つ手前チャンクまでを追記アップロードします - /files/upload_session/append_v2
  3. セッションIDと最後のチャンクをアップロードします - /files/upload_session/finish

分割の閾値

分割するかどうかの閾値はAPIドキュメントに下記のように記載があります。

A single request should not upload more than 150 MB.

150MB以上であれば分割してアップロードすべし。とのことなのですが、気になるのはこの1MBが 1,000,000バイトなのか、1,048,576バイトなのか。です。 気になるので試してみたところ次の通りでした。

  • 150,000,000バイト … アップロード成功
  • 157,286,399バイト (150 * 1,048,576 - 1)… アップロード成功
  • 157,286,400バイト (150 * 1,048,576) … アップロード成功
  • 160,000,000バイト … アップロード成功

予想外のことですがとりあえず150MBを軽く超えてもエラーなど出ず、アップロードができるようです。 ただ、あまり一度にアップロードするサイズが大きくなりすぎると、途中で通信エラーが出た場合に再送するならばロスが大きくなるのでそこそこに分割した方が良いでしょう。

Goでの実装例

前回と同様にdropbox-sdk-go-unofficialを使った実装例です。まずはソースをご覧ください。

uploadSrc := "data.zip"
// 分割してアップロードするかどうかの閾値
var chunkedUploadThreshold, chunkSize int64
chunkedUploadThreshold = 150 * 1048576
chunkSize = chunkedUploadThreshold
config := dropbox.Config{
  Token: "トークン文字列",
}
client := files.New(config)
// ローカルファイルの情報取得
info, err := os.Lstat(uploadSrc)
if err != nil {
  return err
}
f, err := os.Open(uploadSrc)
if err != nil {
  return err
}
defer f.Close()
ci := files.NewCommitInfo("/アップロード先/data.zip")
// ファイルの日付、UTCで秒未満の単位は丸める
ci.ClientModified = info.ModTime().UTC().Round(time.Second)
if info.Size() < chunkedUploadThreshold {
  _, err = client.Upload(ci, f)
  return err
} else {
  // セッションを開始、最初のチャンクサイズ分だけアップロード
  r := io.LimitReader(f, chunkSize)
  s, err := client.UploadSessionStart(files.NewUploadSessionStartArg(), r)
  if err != nil {
    return err
  }
  // 最後から一つ手前のチャンクまで分割アップロード
  var uploaded int64 // アップロード済みサイズ
  uploaded = chunkSize
  for (info.Size() - uploaded) > chunkSize {
    cursor := files.NewUploadSessionCursor(s.SessionId, uint64(uploaded))
    arg := files.NewUploadSessionAppendArg(cursor)
    r = io.LimitReader(f, chunkSize)
    err = client.UploadSessionAppendV2(arg, r)
    if err != nil {
      return err
    }
    uploaded += chunkSize
  }
  // 最後のチャンクをアップロード
  cursor := files.NewUploadSessionCursor(s.SessionId, uint64(uploaded))
  arg := files.NewUploadSessionFinishArg(cursor, ci)
  _, err = client.UploadSessionFinish(arg, f)
  return err
}

アップロード先

CommitInfo構造体にパスやファイルの日付などを設定します。アップロード先パスですが、アップロード先ディレクトリ名だけでなく、アップロード先ファイル名も含めたパスを指定します。

autorenameというパラメータがありますが、これをtrueにすると同じパスにすでにファイルがある場合は新しくアップロードするファイルがdata (1).zipのように重複しないファイル名でアップロードされます。

ファイル更新日付

ファイル更新日付を指定する場合にはclient_modifiedパラメータを指定します。指定しない場合にはDropboxへファイルが保存された際のサーバ日時になります。指定はISO 8601フォーマットで、UTCとします。

Goの例ではUTCに変換した上で、下記のように秒以下を丸めています。

ci.ClientModified = info.ModTime().UTC().Round(time.Second)

分割アップロード

Goの場合には分割アップロードに便利なio.LimitReaderというクラスがありますのでこれを利用するといいでしょう。分割したいチャンクサイズを指定するとそこでEOFになってくれます。

/files/upload_session/startで最初のチャンクをアップロードしたら、セッションIDが得られます。

これを使って、以降は/files/upload_session/append_v2を呼び出してチャンクをアップロードしていきます。なお、ファイルは0バイト目から連続している必要があり、cursorで飛び飛びのオフセットを指定した場合にはincorrect_offsetエラーとなります。

このoffsetパラメータの目的はAPIドキュメントにあるとおり、通信エラーなどで重複送信や部分的なロスが発生した場合にデータの整合性を検出するためです。

We use this to make sure upload data isn’t lost or duplicated in the event of a network error.

海外旅行とSIMカード

海外旅行または海外出張に行くとき、忘れてはならないのはパスポートです。次に大事なのは財布だと思いますが、いまや財布と同じぐらいモバイルインターネット接続の重要度が上がってきていると思います。地図を見るにしても、タクシーに乗るにしてもスマホとアプリに頼り切る生活になっているからでしょう。

今年は出張と旅行で何度か海外に行きましたが、その際のSIMカード利用をレビューしたいと思います。数年前まではWifiルータを日本でレンタルしていくこともありましたが、最近はほとんどSIMフリー携帯/iPad+現地SIMです。 (注: 為替レートは執筆時点でのもの)

台湾

ここ数年、ゴールデンウィーク頃に台湾へ遊びに行っています。台湾はSIMが手に入れやすく、たとえば台北の松山空港でも荷物を受け取り、両替を済ませたらすぐ近くにSIMカードを売っているカウンターがあります。特にこだわりはありませんが、だいたいいつも中華電信の1週間500台湾ドル(約1900円)でインターネット使い放題のSIMを買っています。カウンターでパスポートとSIMフリー携帯を渡すとSIMを差し替えて設定してくれます。

速度も満足でき、特に電波状況がわるくて不便な目にあったということもありませんでした。

フィンランド、アイルランド、イギリス

今年はヨーロッパ方面に行く際にはMTX ConnectのSIMを使っています。日本からもオンラインでSIMを購入することができます。海外からの発送になるので出発の3〜4週間前ぐらいに買っておくと安心でしょう。

SIMカードは7ユーロ(約940円)程度で購入できます。7ユーロで買いますが、これはあとからパケット代金に充てることができるので実質的には無料と考えてもいいかもしれません。MTX Connectの場合はその後、クレジットカードないしPaypalなどでチャージ(top up)して従量課金または1GB〜3.5GBの定額プランなどを購入して利用します。

今年使ったのは1GB・15日(約20ユーロ)と2GB・30日(約40ユーロ)というプランです。フィンランド→アイルランド→イギリスなど複数国を回りましたがSIMを変える必要も無く、キャリアも自動検出されるもので良いのでかなり楽です。

接続は4Gではなく3Gになることが多いので、日本で利用するのと比べるとやや見劣りしますが地図を見たり、SNSを見るぐらいならさほど気にならないと思います。

アメリカとカナダ

北米には今年、アメリカ(ニューヨークとインディアナ)とカナダ(トロント)に行きました。同行者分ふくめ2台分準備しました。

T-MobileのSIM

ひとつはT-Mobileの通話とSMS、データ通信高速無制限使い放題というSIMを購入していきました。30日間有効で、カナダ・メキシコは最長15日間利用できるとのことです。価格は6,900円とヨーロッパで使っているSIMと比べるとかなり高い印象です。それでもWifiルータをレンタルするよりは安くすみます。

iPhone SEで利用していましたが、速度的にも満足のいくものでした。トロントに移動した際、うまくインターネット接続できなかったのですが日本で使っていたIIJmioの構成プロファイルを消し忘れていたというのに気づいて一件落着しました。

MTX Connect

アメリカの後、すぐにヨーロッパ出張の予定があったのでMTX Connectを使うことにしました。ニューヨークでは実際にMTX Connectを使って特に不自由なく空港でUberを呼んだりするところから、街歩きの際にも利用しました。

MTX ConnectのSIMでカバレッジをみるとアメリカやカナダもカバーしているように見えるので、これだけで済ませるつもりでしたが少し誤算がありました。

MTX Connectのカバレッジ

よくみると注があり、ケベック州のみとのこと。。結局トロントでは利用できず、後述のGigSkyを使うことにしました。

Apple SIMでGigSky

現地でSIMを調達するのも意外とタイムロスになるのでiPad Pro内蔵のApple SIMを使うことにしました。Apple SIMで契約するデータプランは、何かの記事で読んだからか、ものすごく高いという印象があったのですが、北米プラン2GB・30日で4,200円と意外にリーズナブルでした。

通信速度、接続状態も良く都市部だけでなく郊外でもストレス無く利用できました。

GigSkyのページをみると北米2GB・15日で30ドル(約3,400円)とApple SIMプランよりも若干リーズナブル。日数は減りますが旅程によってはGigSkyのSIMをあらかじめ日本で買っておいてもいいかもしれません。

総評

複数国を移動するような旅行の場合、カバレッジの広いGigSkyが圧倒的に便利ですが、旅程によってはMTX Connectも便利です。EU圏内では今年データローミング料金が撤廃されたというような動きもあったので、今後ますます便利なSIMが出てくるかと思います。

対応LTEバンドなど携帯電話側の対応も考えなければいけないですが、iPhoneなどカバレッジが広いSIMフリー端末があると、その点でもあまり悩まずすみます。

Dropbox API: 認証・認可について

今回はDropbox APIの認証・認可についてかいつまんでご紹介します。Dropbox APIの認証・認可はOAuth2を利用しています。公式SDKの実装サンプルなどを参考にJavaとGoでの実装例をもとに紹介していきます。

Dropbox APIの概要でもご紹介したとおり、実装前にはアプリケーションの登録が登録が必要です。登録がすんだらApp keyとApp Secretが得られますのでこれらを使いましょう。

Java

SDKにauthorizeというサンプルがありますのでこれを参考にすると良いでしょう。 一般的なOAuthの流れと比べて変わったところは特にありません。Dropbox SDK JavaのJavadocに簡単な流れがのっているのでこちらを引用してご紹介します。

// No Redirect Example
DbxRequestConfig requestConfig = new DbxRequestConfig("text-edit/0.1");
DbxAppInfo appInfo = DbxAppInfo.Reader.readFromFile("api.app");
DbxWebAuth auth = new DbxWebAuth(requestConfig, appInfo);
DbxWebAuth.Request authRequest = DbxWebAuth.newRequestBuilder()
    .withNoRedirect()
    .build();
String authorizeUrl = auth.authorize(authRequest);
System.out.println("1. Go to " + authorizeUrl);
System.out.println("2. Click \"Allow\" (you might have to log in first).");
System.out.println("3. Copy the authorization code.");
System.out.print("Enter the authorization code here: ");
String code = System.console().readLine();
if (code != null) {
    code = code.trim();
    DbxAuthFinish authFinish = webAuth.finishFromCode(code);
    DbxClientV2 client = new DbxClientV2(requestConfig, authFinish.getAccessToken());
    // APIを使った処理
}

この例ではApp Key・App Secretをファイルから読み込んでいます。api.appという名前で次のようなJSON形式ファイルを準備しておいてください。

{
  "key"    : "App keyと置き換え",
  "secret" : "App secretと置き換え"
}

大まかな流れとしては次の通りです。

  1. 設定情報などを準備 (DbxRequestConfig, DbxAppInfo)
  2. リクエストを準備
  3. 認証URLを生成し、ユーザーに認可をもとめる
  4. コードを受け取りトークンを生成
  5. DbxClientV2インスタンスを作成して各種APIコール

設定できるオプションとしては、DbxRequestConfigにはエラー時の再試行回数などがあります。

Go

個人的にGoで実装する際には非公式SDKのdropbox-sdk-go-unofficialを使っています。処理の流れは基本的にJavaでの実装と同じです。OAuth認証・認可処理自体はSDKとは関係なくgolang.org/x/oauth2を利用しています。

authCfg := &oauth2.Config{
	ClientID:     "App keyと置き換え",
	ClientSecret: "App Secretと置き換え",
	Scopes:       []string{},
	Endpoint: oauth2.Endpoint{
		AuthURL:  "https://www.dropbox.com/oauth2/authorize",
		TokenURL: "https://api.dropboxapi.com/oauth2/token",
	},
}
authorizeUrl := authCfg.AuthCodeURL(
	"csrf-stateの文字列",
	oauth2.SetAuthURLParam("response_type", "code"),
)
fmt.Println("1. Go to " + authorizeUrl);
fmt.Println("2. Click \"Allow\" (you might have to log in first).");
fmt.Println("3. Copy the authorization code.");
fmt.Print("Enter the authorization code here: ");
var code string
fmt.Scan(&code)
token, err := authCfg.Exchange(context.Background(), code)
if err == nil {
	dropboxCfg := dropbox.Config{
		Token: token.AccessToken,
	}
	client := files.New(dropboxCfg)
	// ...
}

認証時のロール

ちょっと変わったところとしてはリクエストを準備する際のロールについて紹介しておこうと思います。OAuthではscopesというパラメータを使ってアプリケーションに対し、認可する範囲を設定します。DropboxではDropbox APIの概要にてご紹介したとおり、アプリケーション登録時にアクセス権限を設定します。

このため、scopesパラメータを使わない場合も多いのですが一つだけ指定できるものがあります。

Dropboxでは個人向けDropbox (Dropbox Basic、Plus、Professional)アカウントと、Dropbox Businessアカウントをリンクすることでパソコン、スマートフォン、タブレットなどのアプリケーションで同時に二つのアカウントを利用できる機能があります。

このときに、アプリケーションの接続を会社のアカウントのみに限定したいとか、逆に個人向けのみに限定したいといった指定にscopesパラメータを利用します。

Javaの場合はDbxWebAuth.Request.BuilderwithRequireRole()メソッドへROLE_PERSONALまたはROLE_WORKを指定します。

トークンの無効化

一定の処理が終わって、ログアウトに相当するトークンの無効化処理をしたい場合の処理です。これは、User EndpointsとBusiness Endpointsで若干変わります。

User Endpointsの場合

User Endpointsの場合はauth/token/revokeにAccess Tokenを渡すだけです。

Business Endpointsの場合

Business Endpointsではauth/token/revokeに直接相当するAPIがありません。このためチーム管理者が管理者コンソールよりチーム向けアプリケーションを無効化する必要があります