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",
139 fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
140 escapeQuotes(fieldname), escapeQuotes(filename)))
141 h.Set("Content-Type", "application/octet-stream")
142 return w.CreatePart(h)
143 }
144
145
146
147 func (w *Writer) CreateFormField(fieldname string) (io.Writer, error) {
148 h := make(textproto.MIMEHeader)
149 h.Set("Content-Disposition",
150 fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname)))
151 return w.CreatePart(h)
152 }
153
154
155 func (w *Writer) WriteField(fieldname, value string) error {
156 p, err := w.CreateFormField(fieldname)
157 if err != nil {
158 return err
159 }
160 _, err = p.Write([]byte(value))
161 return err
162 }
163
164
165
166 func (w *Writer) Close() error {
167 if w.lastpart != nil {
168 if err := w.lastpart.close(); err != nil {
169 return err
170 }
171 w.lastpart = nil
172 }
173 _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
174 return err
175 }
176
177 type part struct {
178 mw *Writer
179 closed bool
180 we error
181 }
182
183 func (p *part) close() error {
184 p.closed = true
185 return p.we
186 }
187
188 func (p *part) Write(d []byte) (n int, err error) {
189 if p.closed {
190 return 0, errors.New("multipart: can't write to finished part")
191 }
192 n, err = p.mw.w.Write(d)
193 if err != nil {
194 p.we = err
195 }
196 return
197 }
198
View as plain text