Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
HTTPを手で書いて学ぶ ファイルアップロードの仕組み
Search
ikuma-t
October 27, 2023
Technology
72
26k
HTTPを手で書いて学ぶ ファイルアップロードの仕組み
Kaigi on Rails 2023の登壇資料です!
ikuma-t
October 27, 2023
Tweet
Share
More Decks by ikuma-t
See All by ikuma-t
いまさらのStorybook
ikumatadokoro
0
37
これで最後にしたい! Astroと立ち向かう 6度目の個人ブログ再開発
ikumatadokoro
3
290
Panda CSS と Ark UI ではじめる個人開発
ikumatadokoro
2
770
見た目から始める生産性向上
ikumatadokoro
10
5.2k
ぼくが 美容師さんに伝えたかった バンドの話
ikumatadokoro
0
110
Railsアプリをコスパよく読むための環境整備
ikumatadokoro
2
750
たどころくん1号を支える技術
ikumatadokoro
1
190
なんだか うまくいっている を 自分たちの いつもどおり に 定着させるためのチーム戦略
ikumatadokoro
4
620
プロダクト開発を支えるペースメーカー
ikumatadokoro
1
270
Other Decks in Technology
See All in Technology
内製化によるシステムモダナイゼーションの実践
kazokmr
3
540
わたしとトラックポイント / TrackPoint tips
masahirokawahara
1
210
Databricksで構築する初めての複合AIシステム - ML15min
taka_aki
2
1.4k
新R25、乃木坂46 Mobileなどのファンビジネスを支えるマルチテナンシーなプラットフォームの全体像 / cam-multi-cloud
cyberagentdevelopers
PRO
1
110
Oracle Base Database Service 技術詳細
oracle4engineer
PRO
5
49k
omakaseしないための.rubocop.yml のつくりかた / How to Build Your .rubocop.yml to Avoid Omakase #kaigionrails
linkers_tech
3
340
Kubernetes Summit 2024 Keynote:104 在 GitOps 大規模實踐中的甜蜜與苦澀
yaosiang
0
280
Comparing Apache Flink and Spark for Modern Stream Data Processing
sharonx
0
190
Hotwire光の道とStimulus
nay3
5
2.3k
なんで、私がAWS Heroに!? 〜社外の広い世界に一歩踏み出そう〜
minorun365
PRO
4
820
カメラを用いた店内計測におけるオプトインの仕組みの実現 / ai-optin-camera
cyberagentdevelopers
PRO
0
100
生成AI×マルチテナントSaaSな新規事業を立ち上げる上でテックリードとして気を使った点の紹介
lunastera
0
540
Featured
See All Featured
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
47
5k
Unsuck your backbone
ammeep
668
57k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
25
1.8k
Designing on Purpose - Digital PM Summit 2013
jponch
115
6.9k
No one is an island. Learnings from fostering a developers community.
thoeni
19
3k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
159
15k
Producing Creativity
orderedlist
PRO
341
39k
Keith and Marios Guide to Fast Websites
keithpitt
408
22k
jQuery: Nuts, Bolts and Bling
dougneiner
61
7.5k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
664
120k
Six Lessons from altMBA
skipperchong
26
3.4k
Build The Right Thing And Hit Your Dates
maggiecrowley
32
2.4k
Transcript
1 HTTPを手で書いて学ぶ ファイルアップロードの仕組み ikuma-t
2 ikuma-t IkumaTadokoro ikumatdkr ikuma-t.com ikumatadokoro 株式会社エンペイで働く、フロントエンドが好きなエンジニア。 最近はよくパンケーキを焼いています。
3 「ファイルアップロード」は Webアプリケーションにおいて 割とよくある機能かと思います
4 わたしとファイルアップロード Excelファイルアップロードがなぜかバグるので、途中の通信を眺める multipart/form-dataが使えないツールでBase64エンコードでアップロード フロント側でリッチなアップロード作る オブジェクトストレージにアップロードする いずれも特段珍しくない体験
5 どれも割と普通の機能だけど 仕事で初めて実装した時は 毎回ググりまくっていた
6 発表のゴール ファイルアップロードの処理を抽象化して理解できるようになること 個別の事象で都度ググらなくても(GPTらなくても?)大丈夫になりたい よくわからないけど、とりあえず実装できている このファイル形式の場合はどうすればいいんだろう? とりあえず書いてある通りにしたけど、なんでBase64 エンコードしているんだろう? 先輩が作ってくれた共通処理にパラメータ突っ込んでい るけど、どういうリクエストなのかはよくわからない
フロントから直接オブジェクトストレージにアップする 処理、とりあえずドキュメントコピペしてみた and more... 「何がわからないのか」がわかる ファイルアップロードの全体地図が理解できているので、 最小限の調査で実装できる。 新しい仕組みができても既存知識をもとに理解できる。
7 注意事項 発表中における「HTTP」のバージョンは1.1とします。 HTTPとは何か?といった話はしません。 高トラフィックにも耐えられる画像アップロードサーバを作ろう!といった尖ったケースの話はしま せん。 タイトルに「手書き」とありますが、毛筆でHTTPを書いてHTTPの気持ちになる、といった発表では ありません。悪しからず。 時間配分に失敗したので、早口です!!ごめんなさい!!!
8 Chapter 1 「ファイル」ってなんだろう? 理解のための最初のロードマップ
9 「ファイル」アップロードと一口に言うけれど… プレーンテキスト 画像ファイル PDFファイル Officeファイル …など それぞれのアップロード処理をまったく別物として理解すると、 その分検索回数が増えていく(「画像 ファイルアップロード」、「Excel
ファイルアップロード」) ファイルも様々な種類がある
10 ファイルの種類が違っても 処理フローは大体同じ 結局みんなバイナリ
11 ファイル = バイナリ ファイルはすべてバイナリで表現される テキストエディタで読めるものをテキ ストデータそれ以外をバイナリデータ と分類・対比することも多い が、テキストもバイナリの一種である 多くの場合、最初の数バイトに「マジック
ナンバー」と呼ばれる識別子があり、これ によってファイル種別を識別する
12 ファイルの種類が違っても処理フローは大体同じ ファイル種別ごとに送信されたデータを解釈するためのルールは異なるが、抽象レベルでは同じような処理になる サーバ ブラウザ ユーザー サーバ ブラウザ ユーザー ファイルを選択(`input
type="file"` ) 任意の処理(バリデーションなど)を行う 送信ボタンやドラッグ& ドロップをトリガーに送信処理が実行される HTTP リクエストによってファイルを送信 HTTP リクエストでファイルを受け取る 任意の処理(データの抽出・加工・保存など)を行う
13 理解のための最初のロードマップ 各アプリケーションによらないHTTPリクエストを理解し、そこにつながるブラウザ・サーバ側の処理を明らかにする ことで、ファイルアップロード全体の理解を深めたい。
14 Chapter 2 POSTを使ったアップロード ファイルアップロードの基本形
15 POSTを使ったファイルアップロードの形式 アップロードによって新規にリソースを作成するのでPOST (HTTPメソッドの一般的な使い方)
16 POSTを使ったファイルアップロードの形式 メソッドがPOSTと決まっても、コンテンツをどうやって伝搬するかの方法はいくつかある
17 POSTを使ったファイルアップロードの形式 HTMLのform要素で扱える2つの形式、application/x-www-form-urlencoded と multipart/form-data から見ていきます。
18 form要素で使えるアップロード 1. application/x-www-form-urlencoded バイナリをテキストベースのフォーマットに含める <form> 要素を使って実行できる2つのアップロード 2. multipart/form-data バイナリをそのままリクエストに含める
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
20 application/x-www-form-urlencoded(parse) 仕様がいろんなところでされているのでややこしいですが、一例としてURL Standardに定義があります。
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 }
28 ここまでの整理 テキストベースのフォーマットを使ったアップロードでは、制御文字との都合でBase64なりでテキストに変換する必 要がある。変換できない場合は安全にアップロードできない。
29 form要素からのアップロード 2. multipart/form-data バイナリをそのままリクエストに含める <form> 要素を使って実行できる2つのアップロード 1. application/x-www-form-urlencoded バイナリをテキストベースのフォーマットに含める
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名 値を入力してください ファイル名 値を入力してください 種類 値を入力してください 実行する
32 multipart/form-dataのリクエストを生成する 基本的にはapplication/x-www-form-urlencodedと変わらない。 単純にファイルがバイナリそのまま埋め込めるようになったので、デコード等が不要になる。 HTTPサーバはboundaryを認識して値を取得する。 デモあり
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")
34 ここまでのまとめ 一部のMIMEタイプではバイナリをそのまま埋め込める。form要素からはmultipart/form-data、JavaScriptからは FormData経由でmultipart/form-dataを使ったり、application/octet-streamで直接埋め込むことができる。
35 Chapter 3 PUTを使ったアップロード オブジェクトストレージへのアップロード / 再開可能なアップロード
36 PUTを使ったファイルアップロードの形式 アップロードによって既存リソースを更新するのでPUT (HTTPメソッドの一般的な使い方)
37 オブジェクトストレージへのアップロード ストレージサービス クライアントアプリ ユーザー ストレージサービス クライアントアプリ ユーザー ファイルアップロードを要求 署名付きURL
やトークンを要求(ここはPOST ) 署名付きURL やトークンを発行 ファイルアップロードUI を提供 ファイルを選択する 署名付きURL やトークンを用いてファイルをアップロードする(ここはPUT ) ファイルアップロード完了 ユーザーにファイルアップロード完了をフィードバック オブジェクトを更新することからPUTを使っているだけであって、適切なMIMEタイプを指定して、バイナリを直接コ ンテンツに含める部分はこれまでと特に変わらない。
38 オブジェクトストレージならではの仕様 いくつかの要件に対応するため、オブジェクトストレージへのアップロード時には追加でいくつかの ヘッダフィールドを指定することができる。 例えば大規模ファイルをいくつかの単位に分割してアップロードする際、アップロード結果が正しい ことをチェックするために、Content-MD5ヘッダが使われることがある。 その他にも各ストレージサービスが用意した独自ヘッダがいくつかある。そちらについては各サービ スのドキュメント参照。
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
42 Conclusion まとめ
43 発表のゴール ファイルアップロードの処理を抽象化して理解できるようになること 個別の事象で都度ググらなくても(GPTらなくても?)大丈夫になりたい よくわからないけど、とりあえず実装できている このファイル形式の場合はどうすればいいんだろう? とりあえず書いてある通りにしたけど、なんでBase64 エンコードしているんだろう? 先輩が作ってくれた共通処理にパラメータ突っ込んでい るけど、どういうリクエストなのかはよくわからない
フロントから直接オブジェクトストレージにアップする 処理、とりあえずドキュメントコピペしてみた and more... 「何がわからないのか」がわかる ファイルアップロードの全体地図が理解できているので、 最小限の調査で実装できる。 新しい仕組みができても既存知識をもとに理解できる。
44 ファイルアップロードの全体地図
45 ファイルアップロードの全体地図
46 ファイルアップロードの全体地図
47 ファイルアップロードの全体地図
48 ファイルアップロードの全体地図
49 例示は理解の試金石 今回はRubyとシェルコマンドで動くサンプルを作り、理解の曖昧なファイルアップロードについて理解 を掘り進めてきました。 自分の理解が届く範囲で、動く最小のサンプルを作って理解を深めてみてはいかがでしょうか ——これは、僕たちが大事にしているスローガンだ。抽象的なことや複雑なことを「理解した」かどうかを試すには、 「例を作る」のがいいという意味になる。理解しているかどうか不安になったら、「例」を作ろう。 – 数学ガール
50 ご清聴いただきありがとうございました。