-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
formdata.cr
230 lines (217 loc) · 6.93 KB
/
formdata.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
require "./formdata/**"
require "mime/multipart"
# Contains utilities for parsing `multipart/form-data` messages, which are
# commonly used for encoding HTML form data.
#
# ### Examples
#
# Commonly, you'll want to parse a from response from a HTTP request, and
# process it. An example server which performs this task is shown below.
#
# ```
# require "http"
#
# server = HTTP::Server.new do |context|
# name = nil
# file = nil
# HTTP::FormData.parse(context.request) do |part|
# case part.name
# when "name"
# name = part.body.gets_to_end
# when "file"
# file = File.tempfile("upload") do |file|
# IO.copy(part.body, file)
# end
# end
# end
#
# unless name && file
# context.response.respond_with_status(:bad_request)
# next
# end
#
# context.response << file.path
# end
#
# server.bind_tcp 8085
# server.listen
# ```
#
# To test the server, use the curl command below.
#
# ```console
# $ curl http://localhost:8085/ -F name=foo -F file=@/path/to/test.file
# /tmp/upload.Yxn7cc
# ```
#
# Another common case is sending formdata to a server using HTTP::Client. Here
# is an example showing how to upload a file to the server above in crystal.
#
# ```
# require "http"
#
# IO.pipe do |reader, writer|
# channel = Channel(String).new(1)
#
# spawn do
# HTTP::FormData.build(writer) do |formdata|
# channel.send(formdata.content_type)
#
# formdata.field("name", "foo")
# File.open("foo.png") do |file|
# metadata = HTTP::FormData::FileMetadata.new(filename: "foo.png")
# headers = HTTP::Headers{"Content-Type" => "image/png"}
# formdata.file("file", file, metadata, headers)
# end
# end
#
# writer.close
# end
#
# headers = HTTP::Headers{"Content-Type" => channel.receive}
# response = HTTP::Client.post("http://localhost:8085/", body: reader, headers: headers)
#
# puts "Response code #{response.status_code}"
# puts "File path: #{response.body}"
# end
# ```
module HTTP::FormData
# Parses a multipart/form-data message, yielding a `FormData::Parser`.
#
# ```
# require "http"
#
# form_data = "--aA40\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nfield data\r\n--aA40--"
# HTTP::FormData.parse(IO::Memory.new(form_data), "aA40") do |part|
# part.name # => "field1"
# part.body.gets_to_end # => "field data"
# end
# ```
#
# See: `FormData::Parser`
def self.parse(io, boundary)
parser = Parser.new(io, boundary)
while parser.has_next?
parser.next { |part| yield part }
end
end
# Parses a multipart/form-data message, yielding a `FormData::Parser`.
#
# ```
# require "http"
#
# headers = HTTP::Headers{"Content-Type" => "multipart/form-data; boundary=aA40"}
# body = "--aA40\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nfield data\r\n--aA40--"
# request = HTTP::Request.new("POST", "/", headers, body)
#
# HTTP::FormData.parse(request) do |part|
# part.name # => "field1"
# part.body.gets_to_end # => "field data"
# end
# ```
#
# See: `FormData::Parser`
def self.parse(request : HTTP::Request)
body = request.body
raise Error.new "Cannot extract form-data from HTTP request: body is empty" unless body
boundary = request.headers["Content-Type"]?.try { |header| MIME::Multipart.parse_boundary(header) }
raise Error.new "Cannot extract form-data from HTTP request: could not find boundary in Content-Type" unless boundary
parse(body, boundary) { |part| yield part }
end
# Parses a `Content-Disposition` header string into a field name and
# `FileMetadata`. Please note that the `Content-Disposition` header for
# `multipart/form-data` is not compatible with the original definition in
# [RFC 2183](https://tools.ietf.org/html/rfc2183), but are instead specified
# in [RFC 2388](https://tools.ietf.org/html/rfc2388).
def self.parse_content_disposition(content_disposition) : {String, FileMetadata}
filename = nil
creation_time = nil
modification_time = nil
read_time = nil
size = nil
name = nil
parts = content_disposition.split(';')
type = parts[0]
raise Error.new("Invalid Content-Disposition: not form-data") unless type == "form-data"
(1...parts.size).each do |i|
part = parts[i]
key, value = part.split('=', 2)
key = key.strip
value = value.strip
if value[0] == '"'
value = HTTP.dequote_string(value[1...-1])
end
case key
when "filename"
filename = value
when "creation-date"
creation_time = HTTP.parse_time value
when "modification-date"
modification_time = HTTP.parse_time value
when "read-date"
read_time = HTTP.parse_time value
when "size"
size = value.to_u64
when "name"
name = value
end
end
raise Error.new("Invalid Content-Disposition: no name field") unless name
{name, FileMetadata.new(filename, creation_time, modification_time, read_time, size)}
end
# Builds a multipart/form-data message, yielding a `FormData::Builder`
# object to the block which writes to *io* using *boundary*.
# `Builder#finish` is called on the builder when the block returns.
#
# ```
# require "http"
#
# io = IO::Memory.new
# HTTP::FormData.build(io, "boundary") do |builder|
# builder.field("foo", "bar")
# end
# io.to_s # => "--boundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--boundary--"
# ```
#
# See: `FormData::Builder`
def self.build(io, boundary = MIME::Multipart.generate_boundary)
builder = Builder.new(io, boundary)
yield builder
builder.finish
end
# Builds a multipart/form-data message, yielding a `FormData::Builder`
# object to the block which writes to *response* using *boundary.
# Content-Type is set on *response* and `Builder#finish` is called on the
# builder when the block returns.
#
# ```
# require "http"
#
# io = IO::Memory.new
# response = HTTP::Server::Response.new io
# HTTP::FormData.build(response, "boundary") do |builder|
# builder.field("foo", "bar")
# end
# response.close
#
# response.headers["Content-Type"] # => "multipart/form-data; boundary=\"boundary\""
# io.to_s # => "HTTP/1.1 200 OK\r\nContent-Type: multipart/form-data; boundary=\"boundary\"\r\nContent-Length: 75\r\n\r\n--boundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--boundary--"
# ```
#
# See: `FormData::Builder`
def self.build(response : HTTP::Server::Response, boundary = MIME::Multipart.generate_boundary)
builder = Builder.new(response, boundary)
yield builder
builder.finish
response.headers["Content-Type"] = builder.content_type
end
# Metadata which may be available for uploaded files.
record FileMetadata,
filename : String? = nil,
creation_time : Time? = nil,
modification_time : Time? = nil,
read_time : Time? = nil,
size : UInt64? = nil
class Error < Exception
end
end