受け取りの流れ/ Receiving
URL を開いてから、手元にファイルが届くまで
受取人がすることは「送られてきた URL を開く」だけです。その先で、リンクがまだ生きているかを確かめ、必要な確認を順に通し、ファイルを渡す。この一連は 8 本の公開 API で構成されていて、それぞれが「何をして、何をしないか」をはっきり分けて持っています。ここではその動きを、全体像・時系列・画面の状態、の 3 つの角度から追います。
受け取りの全体像End-to-End Flow
発行の図と同じく、上段が業務としての流れ、下段がそれを実現している技術としての処理です。受取人はログインしていないため、身元の手がかりは「URL を知っていること」から始まります。
受取ページの表示・確認・配布はすべて同じオリジンの API(/api/share-links/*)を呼びます。本体ホストの API は呼びません。
状態の解決順Status Resolution
URL を開くと、まず「このリンクは今どういう状態か」を 1 回の問い合わせで解決します。上から順に当てはまった最初のものを返します。
| 順 | 状態 | 受取人に見えるもの | 記録 |
|---|---|---|---|
| 1 | 見つからない | 「リンクが見つかりません」+ URL の確認をお願いする案内 | 監査ログのみ(記録先の行が存在しないため) |
| 2 | 期限切れ | 「有効期限が切れています」+ 発行者へ再発行を依頼する案内 | アクセス記録 |
| 3 | 無効化済み | 「無効化されています」+ 発行者へ問い合わせる案内 | アクセス記録 |
| 4 | 回数上限 | 「ダウンロード回数の上限に達しました」 | アクセス記録 |
| 5 | パスワードが必要 | パスワード入力フォーム(ファイル名は出さない) | — |
| 6 | 本人確認が必要 | メールアドレス入力 → 確認コード入力 | — |
| 7 | 受け取り可 | 対象名・発行者名・期限・残り回数・大きさ(フォルダは件数と中身) | アクセス記録(開封) |
期限切れ・無効化・回数上限を、確認より先に伝える
素直に作ると「まず本人だと確かめてから、状態を教える」順になります。しかしそれをすると、受取人はパスワードを探し回った末に「期限切れでした」に着地します。パスワードが分からないのか、そもそも開けないのかが分からないまま時間を使わせることになります。
そこで、期限切れ・無効化・回数上限は確認を通す前に開示します。これらはリンクを持っている人にとって「探しても解決しない」情報なので、早く伝えるほど親切です。逆に、対象のファイル名は確認を通すまで出しません。URL だけを手に入れた第三者に、中身のヒントを与えないためです。
二段の確認Two Gates
パスワードと本人確認はそれぞれ独立に設定できます。両方が設定されたリンクは、両方を通らなければ開きません。「どちらか片方が通れば全部開く」形にはしていません。
| リンクの設定 | 受取人が通る道 |
|---|---|
| どちらも無し | URL を開く → 受け取り可 |
| パスワードのみ | URL を開く → パスワード → 受け取り可 |
| 本人確認のみ | URL を開く → メール入力 → 確認コード → 受け取り可 |
| 両方 | URL を開く → パスワード → メール入力 → 確認コード → 受け取り可 |
通過を持ち歩く仕組み
確認を 1 つ通るたびに、「何を通ったか」を書いた短命の Cookie を発行します。これは署名が付いていて改ざんできず、有効期間は 短時間です。次の確認を通ると、その分が足されて有効期間も更新されます。ダウンロードの時点で「このリンクに設定されている確認がすべて含まれているか」を見ます。
Cookie の中身はリンクの識別子・通過済みの確認・失効時刻だけで、照合のためにデータベースを引き直す必要がありません。属性は HttpOnly(スクリプトから読めない)・SameSite=Strict(他サイトからの遷移では送られない)・Secure(本番)で、リンクごとに別の Cookie として分かれています。
処理シーケンスSequence
同じ流れを、誰が誰に何を渡すか、という時間順で追います。破線で囲んだ 4 つの区画は条件によって通る/通らないものです(パスワードが設定されていなければ 1 つ目の区画は丸ごと飛ばします)。
⑯ で「行を押さえてから再検証する」のは、同時に複数のダウンロードが走ったときに回数の上限を超えさせないためです。㉑ の「同時に来ても 1 回だけ通る」も同じ考えで、ZIP の二重生成を防いでいます。
画面の状態遷移View States
受取ページが取りうる状態です。エラーの 4 つは行き止まりですが、いずれも次にどうすればよいかを必ず添えています。
②③ は、リンクに設定されているときだけ現れます。どちらも設定が無ければ ① から ④ へ直行します。
ファイルのダウンロードFile Download
ダウンロードボタンを押すと、API はファイルの中身ではなく、短時間だけ有効な URL を返します。ブラウザはその URL へ移動し、Cloud Storage から直接受け取ります。
| 段 | やること |
|---|---|
| 1 | 確認がすべて通っているかを Cookie で見る(足りなければ確認へ戻す) |
| 2 | 範囲を限定した読み取りで対象と最新版を取る(範囲外の指定はそもそも 0 件になる) |
| 3 | 署名付き URL を発行(ファイルも ZIP も短時間だけ有効。保存時のファイル名は元の名前) |
| 4 | 行を押さえて回数を再検証 → 超えていれば発行した URL を捨てて上限エラー |
| 5 | 回数を 1 増やし、アクセス記録と監査ログを残す |
| 6 | URL を返す。ブラウザが移動して取得を開始する |
段 3 と段 4 の順序が逆に見えるかもしれませんが、これは意図したものです。署名付き URL の発行は外部サービスへの呼び出しなので、データベースの行を押さえたまま行いたくありません。先に URL を作っておき、行を押さえるのは最後の一瞬だけにして、そこで条件を満たさなければ作った URL を捨てます。
ダウンロードを開始したことは、ボタンの下に文章でお知らせします。ブラウザのダウンロードバーが目立たない環境では「押したのに何も起きていない」ように見えるためです。
フォルダの ZIPFolder ZIP
フォルダを共有した場合は、配下のファイルを ZIP にまとめて渡します。ただし ZIP の生成には時間がかかるため、「準備」と「配布」を別々の入口に分けています。
保管期限は 72 時間、配布に使えるのは それより少し手前までです。この差は、自動削除の直前に配ってしまい「開けない ZIP の URL」を渡す事故を避けるための余裕です。
入口を分けた理由
受取画面に着いた時点で、裏側では静かに ZIP の準備を始めています(先読み)。押したときには多くの場合できあがっているので、待ち時間が実質ゼロになります。
ここで、もし「準備」と「配布」が同じ入口だったらどうなるか。先読みの呼び出しが、すでに準備済みだったときに配布として扱われ、誰も押していないのに回数を 1 消費します。「1 回だけ」のリンクなら、受取人が画面を開いた瞬間に使い切られてしまいます。準備は状態を進めるだけ、配布は渡すだけ、と役割を分けることで、この事故を構造的に起こせなくしました。
進捗の確認も同じ理由で「何も変えない読み取り」に徹しています。5 秒ごとの確認や、画面に戻ったときの再取得で回数が減っては困るからです。
しきい値を超えるフォルダ
合計 200 MB または 10 ファイルを超えるフォルダは ZIP にせず、配下の一覧を出して 1 件ずつダウンロードしていただきます。一覧は 100 件ずつ読み込み、101 件目以降にも到達できます。このとき回数は 1 ファイルにつき 1 回消費されるため、その旨を一覧の上に添えています。
開封の記録Opened
受取人が必要な確認をすべて通り、受取画面に着いた瞬間を「開封」として記録します。発行者が「渡した相手が開いてくれたか」を確認できるようにするためです。
ただし、受取画面は再読み込みやタブの切り替えで何度も表示されます。そのたびに記録すると、1 人の受取人が何十件もの記録を作ってしまいます。そこで、同じリンク × 同じ接続元について、直近の一定時間に 1 件だけ記録します。
この記録は、発行者側の一覧で「最終アクセス」として見えます。ダウンロードがまだ 0 件でも最終アクセスに時刻が入っていれば、「開いてはもらえたが、まだ受け取っていない」と分かります。