/
builder.cr
129 lines (113 loc) · 3.93 KB
/
builder.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
module HTTP::FormData
# Builds a multipart/form-data message.
#
# ### Example
#
# ```
# require "http"
#
# io = IO::Memory.new
# builder = HTTP::FormData::Builder.new(io, "aA47")
# builder.field("name", "joe")
# file = IO::Memory.new("file contents")
# builder.file("upload", file, HTTP::FormData::FileMetadata.new(filename: "test.txt"))
# builder.finish
# io.to_s # => "--aA47\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njoe\r\n--aA47\r\nContent-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n\r\nfile contents\r\n--aA47--"
# ```
class Builder
private enum State
START
FIELD
FINISHED
end
# Creates a new `FormData::Builder` which writes to *io*, using the
# multipart boundary *boundary*.
def initialize(@io : IO, @boundary = MIME::Multipart.generate_boundary)
@state = State::START
end
getter boundary
# Returns a content type header with correct boundary parameter.
#
# ```
# builder = HTTP::FormData::Builder.new(io, "a4VF")
# builder.content_type # => "multipart/form-data; boundary=\"a4VF\""
# ```
def content_type : String
String.build do |str|
str << "multipart/form-data; boundary=\""
HTTP.quote_string(@boundary, str)
str << '"'
end
end
# Adds a form part with the given *name* and *value*. *Headers* can
# optionally be provided for the form part.
def field(name : String, value, headers : HTTP::Headers = HTTP::Headers.new) : Nil
file(name, IO::Memory.new(value.to_s), headers: headers)
end
# Adds a form part called *name*, with data from *io* as the value.
# *Metadata* can be provided to add extra metadata about the file to the
# Content-Disposition header for the form part. Other headers can be added
# using *headers*.
def file(name : String, io, metadata : FileMetadata = FileMetadata.new, headers : HTTP::Headers = HTTP::Headers.new)
fail "Cannot add form part: already finished" if @state.finished?
headers["Content-Disposition"] = generate_content_disposition(name, metadata)
# We don't add a crlf before the first boundary if this is the first body part.
@io << "\r\n" unless @state.start?
@io << "--" << @boundary
headers.each do |name, values|
values.each do |value|
@io << "\r\n" << name << ": " << value
end
end
@io << "\r\n\r\n"
IO.copy(io, @io)
@state = State::FIELD
end
# Finalizes the multipart message, this method must be called before the
# generated multipart message written to the IO is considered valid.
def finish : Nil
case @state
in .start?
fail "Cannot finish form-data: no body parts"
in .finished?
fail "Cannot finish form-data: already finished"
in .field?
@io << "\r\n--" << @boundary << "--"
@state = State::FINISHED
end
end
private def generate_content_disposition(name, metadata)
String.build do |io|
io << "form-data; name=\""
HTTP.quote_string(name, io)
io << '"'
if filename = metadata.filename
io << "; filename=\""
HTTP.quote_string(filename, io)
io << '"'
end
if creation_time = metadata.creation_time
io << %(; creation-date=")
creation_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z")
io << '"'
end
if modification_time = metadata.modification_time
io << %(; modification-date=")
modification_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z")
io << '"'
end
if read_time = metadata.read_time
io << %(; read-date=")
read_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z")
io << '"'
end
if size = metadata.size
io << %(; size=) << size
end
end
end
private def fail(msg)
raise FormData::Error.new(msg)
end
end
end