1
2
3
4
5 package multipart
6
7 import (
8 "bytes"
9 "crypto/rand"
10 "errors"
11 "fmt"
12 "io"
13 "maps"
14 "net/textproto"
15 "slices"
16 "strings"
17 )
18
19
20 type Writer struct {
21 w io.Writer
22 boundary string
23 lastpart *part
24 }
25
26
27
28 func NewWriter(w io.Writer) *Writer {
29 return &Writer{
30 w: w,
31 boundary: randomBoundary(),
32 }
33 }
34
35
36 func (w *Writer) Boundary() string {
37 return w.boundary
38 }
39
40
41
42
43
44
45
46 func (w *Writer) SetBoundary(boundary string) error {
47 if w.lastpart != nil {
48 return errors.New("mime: SetBoundary called after write")
49 }
50
51 if len(boundary) < 1 || len(boundary) > 70 {
52 return errors.New("mime: invalid boundary length")
53 }
54 end := len(boundary) - 1
55 for i, b := range boundary {
56 if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
57 continue
58 }
59 switch b {
60 case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
61 continue
62 case ' ':
63 if i != end {
64 continue
65 }
66 }
67 return errors.New("mime: invalid boundary character")
68 }
69 w.boundary = boundary
70 return nil
71 }
72
73
74
75 func (w *Writer) FormDataContentType() string {
76 b := w.boundary
77
78
79 if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) {
80 b = `"` + b + `"`
81 }
82 return "multipart/form-data; boundary=" + b
83 }
84
85 func randomBoundary() string {
86 var buf [30]byte
87 _, err := io.ReadFull(rand.Reader, buf[:])
88 if err != nil {
89 panic(err)
90 }
91 return fmt.Sprintf("%x", buf[:])
92 }
93
94
95
96
97
98 func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error) {
99 if w.lastpart != nil {
100 if err := w.lastpart.close(); err != nil {
101 return nil, err
102 }
103 }
104 var b bytes.Buffer
105 if w.lastpart != nil {
106 fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary)
107 } else {
108 fmt.Fprintf(&b, "--%s\r\n", w.boundary)
109 }
110
111 for _, k := range slices.Sorted(maps.Keys(header)) {
112 for _, v := range header[k] {
113 fmt.Fprintf(&b, "%s: %s\r\n", k, v)
114 }
115 }
116 fmt.Fprintf(&b, "\r\n")
117 _, err := io.Copy(w.w, &b)
118 if err != nil {
119 return nil, err
120 }
121 p := &part{
122 mw: w,
123 }
124 w.lastpart = p
125 return p, nil
126 }
127
128 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
129
130 func escapeQuotes(s string) string {
131 return quoteEscaper.Replace(s)
132 }
133
134
135
136 func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
137 h := make(textproto.MIMEHeader)
138 h.Set("Content-Disposition", FileContentDisposition(fieldname, filename))
139 h.Set("Content-Type", "application/octet-stream")
140 return w.CreatePart(h)
141 }
142
143
144
145 func (w *Writer) CreateFormField(fieldname string) (io.Writer, error) {
146 h := make(textproto.MIMEHeader)
147 h.Set("Content-Disposition",
148 fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname)))
149 return w.CreatePart(h)
150 }
151
152
153
154 func FileContentDisposition(fieldname, filename string) string {
155 return fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
156 escapeQuotes(fieldname), escapeQuotes(filename))
157 }
158
159
160 func (w *Writer) WriteField(fieldname, value string) error {
161 p, err := w.CreateFormField(fieldname)
162 if err != nil {
163 return err
164 }
165 _, err = p.Write([]byte(value))
166 return err
167 }
168
169
170
171 func (w *Writer) Close() error {
172 if w.lastpart != nil {
173 if err := w.lastpart.close(); err != nil {
174 return err
175 }
176 w.lastpart = nil
177 }
178 _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
179 return err
180 }
181
182 type part struct {
183 mw *Writer
184 closed bool
185 we error
186 }
187
188 func (p *part) close() error {
189 p.closed = true
190 return p.we
191 }
192
193 func (p *part) Write(d []byte) (n int, err error) {
194 if p.closed {
195 return 0, errors.New("multipart: can't write to finished part")
196 }
197 n, err = p.mw.w.Write(d)
198 if err != nil {
199 p.we = err
200 }
201 return
202 }
203
View as plain text