/
builder.cr
120 lines (103 loc) · 3.78 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
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
# Creates a new `FormData::Builder` which writes to *io*, using the
# multipart boundary *boundary*.
def initialize(@io : IO, @boundary = MIME::Multipart.generate_boundary)
@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.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)
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 = :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
fail "Cannot finish form-data: no body parts" if @state == :START
fail "Cannot finish form-data: already finished" if @state == :FINISHED
@io << "\r\n--" << @boundary << "--"
@state = :FINISHED
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("%a, %d %b %Y %H:%M:%S %z", io)
io << '"'
end
if modification_time = metadata.modification_time
io << %(; modification-date=")
modification_time.to_s("%a, %d %b %Y %H:%M:%S %z", io)
io << '"'
end
if read_time = metadata.read_time
io << %(; read-date=")
read_time.to_s("%a, %d %b %Y %H:%M:%S %z", io)
io << '"'
end
if size = metadata.size
io << %(; size=) << size
end
end
end
private def fail(msg)
raise FormData::Error.new(msg)
end
end
end