Share
Embed
Transcript
50 slides
-
6 発表のゴール ファイルアップロードの処理を抽象化して理解できるようになること 個別の事象で都度ググらなくても(GPTらなくても?)大丈夫になりたい よくわからないけど、とりあえず実装できている このファイル形式の場合はどうすればいいんだろう? とりあえず書いてある通りにしたけど、なんでBase64 エンコードしているんだろう? 先輩が作ってくれた共通処理にパラメータ突っ込んでい るけど、どういうリクエストなのかはよくわからない
フロントから直接オブジェクトストレージにアップする 処理、とりあえずドキュメントコピペしてみた and more... 「何がわからないのか」がわかる ファイルアップロードの全体地図が理解できているので、 最小限の調査で実装できる。 新しい仕組みができても既存知識をもとに理解できる。 -
11 ファイル = バイナリ ファイルはすべてバイナリで表現される テキストエディタで読めるものをテキ ストデータそれ以外をバイナリデータ と分類・対比することも多い が、テキストもバイナリの一種である 多くの場合、最初の数バイトに「マジック
ナンバー」と呼ばれる識別子があり、これ によってファイル種別を識別する -
12 ファイルの種類が違っても処理フローは大体同じ ファイル種別ごとに送信されたデータを解釈するためのルールは異なるが、抽象レベルでは同じような処理になる サーバ ブラウザ ユーザー サーバ ブラウザ ユーザー ファイルを選択(`input
type="file"` ) 任意の処理(バリデーションなど)を行う 送信ボタンやドラッグ& ドロップをトリガーに送信処理が実行される HTTP リクエストによってファイルを送信 HTTP リクエストでファイルを受け取る 任意の処理(データの抽出・加工・保存など)を行う -
19 application/x-www-form-urlencoded データを送信する際に使用されるMIMEタイプ(データの形式)の1種 & と = という2つのテキストを意味のある文字(制御文字)として扱い、コンテンツを表現する 以降、便宜上「テキストで構文をつくるMIMEタイプ」を「テキストベースのMIMEタイプ」と呼びます。 6 firstname=hoge&lastname=fuga
1 POST /upload HTTP/1.1 2 Host: localhost 3 Content-Type: application/x-www-form-urlencoded 4 Content-Length: 28 5 -
21 &や=がコンテンツに入っていたらどうなる…? たとえば以下のようなリクエストを送ったらどうなるだろう? 送りたいのは「ruby&friends」 6 name=error.txt&file=ruby&friends 1 POST /upload HTTP/1.1
2 Host: localhost 3 Content-Length: 32 4 Content-Type: application/x-www-form-urlencoded 5 -
22 実際にやってみよう!(HTTPサーバ) ※ WEBRickでのデモですが、仕様に則っていれば他のサーバでも同じ挙動になるはずです。 9 File.write(File.join(PATH, name), file) 10 res.body
= 'File received' 1 require 'webrick' 2 require 'base64' 3 4 server = WEBrick::HTTPServer.new({ Port: 2000 }) 5 6 server.mount_proc '/upload' do |req, res| 7 # { name: リクエストのname の値, file: リクエストのfile の値 } 8 req.query.transform_keys(&:to_sym) in name:, file: 11 end 12 13 server.start デモあり -
23 実際にやってみよう!(HTTPリクエスト) ncコマンドでリクエストを送ります 2 POST /upload HTTP/1.1 3 Host: localhost
4 Content-Length: 32 5 Content-Type: application/x-www-form-urlencoded 6 7 name=error.txt&file=ruby&friends 1 printf "$(cat <<! 8 ! 9 )" | nc localhost 2000 デモあり -
24 実際にやってみよう(結果) 1. HTTP本文がそのまま届く(共通) `socket.read()`で中身を覗き見ると、自分が送ったHTTPリクエ ストが文字列として入っていた。 2. Content-Typeに則ってパース(共通) 0x26(&)で区切ってnameとvalueのペアにしていた。 application/x-www-form-urlencodedの仕様に沿った実装
3. コンテンツが誤って解釈される 本来はruby&friendsが正しいが、&がそのまま入っていることで そこで区切りが入ってしまっている。 これはかなり単純化した例だけれど、 フォーマットで使われている文字と コンテンツが重複しないための工夫が必要であ ることがわかる -
25 コンテンツが正しく届けられるための仕組み:エンコード パーセントエンコード & %26 %のあとに2桁の16進数が続く形式。非英数文字(&やスペー ス)を安全に送付するために利用。 formからsubmitする場合はこのエンコードが適用される。 Base64エンコード あ
44GC A~Z, a~z, 0~9, +, /の64種類の文字で構成された文字列に変 換する。 3バイトのバイナリが4バイトに変換される。 歴史的に使われてきた背景や変換後のデータ量から、バイナリを埋め込む際にはBase64エンコードが よく使われる。 -
26 Base64エンコードを行なってアップロード 1. HTTPリクエスト: Base64エンコードしたコンテンツを埋め込む 2. HTTPサーバ:コンテンツをBase64デコードしてから任意処理を行う 変更部分のみ抜粋 1 Content-Length:
36 # ここも変更 2 Content-Type: application/x-www-form-urlencoded 3 4 name=error.txt&file=cnVieSZmcmllbmRz # Base64 でエンコードしたもの 1 # アプリケーションロジック(ファイルを保存する)の前にデコード 2 decoded_file = Base64.decode64(file) 3 File.write(File.join(PATH, name), decoded_file) デモあり -
27 他のテキストベースのフォーマットでも同様に変換が必要 例: application/json JSONもJSON形式を表すためにいくつかの特殊文字を使用するので、同様に変換(クライアント側で Base64などでエンコードし、サーバ側でデコード)する必要がある。 他のテキストベースのフォーマットについても同様。 1 { 2
"name": "error.txt", 3 "file": "cnVieSZmcmllbmRz" 4 } -
30 multipart/form-data 異なる種類のデータを1つのHTTPリクエスト内で複数の部分に分割して送信することができる。 境界が明示的に分離されているので、バイナリをそのまま埋め込むことができる。 1 POST /upload HTTP/1.1 2 Host:
localhost 3 Content-Type: multipart/form-data; boundary=--KaigiOnRailsBoundary2023xyz 4 Content-Length: xxx 5 6 --KaigiOnRailsBoundary2023xyz # パート1 7 Content-Disposition: form-data; name="name" 8 Content-Type: text/plain 9 10 David 11 --KaigiOnRailsBoundary2023xyz # パート2 12 Content-Disposition: form-data; name="avatar" filename="avatar.png" 13 Content-Type: image/png 14 15 \x89\x50\... 16 --KaigiOnRailsBoundary2023xyz-- -
31 multipartのpart MIMEタイプ「multipart」の各part 1 require 'js' 2 require 'securerandom' 3
4 def characters 5 [*'0'..'9', *'A'..'Z', *'a'..'z', "'", '-', '_'] 6 end 7 8 def boundary 9 Array.new(rand(0..70)) do 10 characters.sample(random: SecureRandom) 11 end.join 12 end 13 14 # payload の表示・処理方法や、表示や処理に関わるファイル名等の付 15 def content_disposition(name, filename = nil) 16 filename = filename ? "; filename=#{filename}" : ' 17 "Content-Disposition: form-data; name=#{name}#{fil 18 end 19 20 def part(name, filename = nil, type = 'text/plain') 21 part_boundary = boundary 22 <<~PART 23 --#{part_boundary} 24 #{content_disposition(name, filename)} 25 Content-Type: #{type} 26 27 ※ コンテンツ(バイナリの場合はそのまま)がここに入ります 実行結果がここに表示されます。 Key名 値を入力してください ファイル名 値を入力してください 種類 値を入力してください 実行する -
33 その他のバイナリをそのまま埋め込める形式 application/octet-stream 任意の(もしくは不明な)バイナリを示すMIMEタイプ。こちらはdiscrete(個別)タイプなので単一のバイナリファ イルを直接コンテンツに放り込む。 (MIMEタイプはタイプ/サブタイプの形式を取り、タイプについて単一ファイルを取るdiscreteタイプか、複数を表す multipartに分かれる) FormData JavaScript側では`FormData`を使うことで、`multipart/form-data`と同じ形式で値の送信ができる。 1
const formData = new FormData() 2 formData.append("description", " いい写真です") 3 formData.append("avatar", blob, "avatar.png") -
37 オブジェクトストレージへのアップロード ストレージサービス クライアントアプリ ユーザー ストレージサービス クライアントアプリ ユーザー ファイルアップロードを要求 署名付きURL
やトークンを要求(ここはPOST ) 署名付きURL やトークンを発行 ファイルアップロードUI を提供 ファイルを選択する 署名付きURL やトークンを用いてファイルをアップロードする(ここはPUT ) ファイルアップロード完了 ユーザーにファイルアップロード完了をフィードバック オブジェクトを更新することからPUTを使っているだけであって、適切なMIMEタイプを指定して、バイナリを直接コ ンテンツに含める部分はこれまでと特に変わらない。 -
39 Content-Rangeによる再開可能なアップロード 一部のストレージサービスでは、ファイルアップロードがネットワーク回線不調等によって失敗した い場合に備え、再開可能なアップロードをできるようにしている。 このユースケースにあわせ、HTTPの意味内容を決めるRFC「HTTP Semantics」ではContent- Rangeを含めたPUTリクエストについて、オプション的な位置付けではあるが使用を認めている。 refs: HTTP Semantics
- 14.4 Content-Range: (https://www.ietf.org/archive/id/draft-ietf-httpbis-semantics- 16.html#section-14.4) ヘッダフィールドの一例として再開可能なアップロードをやってみる -
40 Content-Rangeによる再開可能なアップロード 「再開可能」にするための仕組みは「今どこまでアップロードできているか」を知り、それを踏まえ て「アップロードできていない部分からアップロードする」こと。 これを実現するために Content-Range ヘッダが使われることがある。 クライアントのアップロードが停止した際は、サーバ側に「今どこまで完了しているのか」をHTTP で確認し、その情報を元にContent-Rangeヘッダを付与してアップロードを途中から再開する。 1
PUT /resumable-upload?xxx HTTP/1.1 2 Host: localhost 3 Content-Length: 100000 4 Content-Range: bytes 10000-20000/20000 デモあり -
41 超簡易な再開可能なアップロードをやってみる 擬似的にアップロードを止めて、途中から再開する 1 printf "$(cat <<! 2 PUT /resumable-upload
HTTP/1.1 3 Host: localhost 4 Content-Length: 270 5 6 $(tail box/sample.txt) 7 ! 8 )" | nc localhost 711 1 nc localhost 711 <<! 2 GET /resumable-upload HTTP/1.1 3 Host: localhost 4 5 ! 1 printf "$(cat <<! 2 PUT /resumable-upload HTTP/1.1 3 Host: localhost 4 Content-Length: 180 5 Content-Range: bytes 90-270/270 6 7 $(tail -c +91 box/sample.txt) 8 ! 9 )" | nc localhost 711 1 printf "$(cat <<! 2 PUT /resumable-upload HTTP/1.1 3 Host: localhost 4 Content-Length: 90 5 Content-Range: bytes 180-270/270 6 7 $(tail -c +181 box/sample.txt) 8 ! 9 )" | nc localhost 711 -
43 発表のゴール ファイルアップロードの処理を抽象化して理解できるようになること 個別の事象で都度ググらなくても(GPTらなくても?)大丈夫になりたい よくわからないけど、とりあえず実装できている このファイル形式の場合はどうすればいいんだろう? とりあえず書いてある通りにしたけど、なんでBase64 エンコードしているんだろう? 先輩が作ってくれた共通処理にパラメータ突っ込んでい るけど、どういうリクエストなのかはよくわからない
フロントから直接オブジェクトストレージにアップする 処理、とりあえずドキュメントコピペしてみた and more... 「何がわからないのか」がわかる ファイルアップロードの全体地図が理解できているので、 最小限の調査で実装できる。 新しい仕組みができても既存知識をもとに理解できる。