Files
phishingclub/backend/vendor/github.com/enetx/surf/multipart.go
T
RonniSkansing 2028391c3c vendor backend
Signed-off-by: RonniSkansing <rskansing@gmail.com>
2026-09-16 23:17:35 +02:00

210 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package surf
import (
"fmt"
"io"
"mime"
"mime/multipart"
"net/textproto"
"path/filepath"
"github.com/enetx/g"
"github.com/enetx/g/fs"
)
// Multipart represents multipart form data with fields and files.
type Multipart struct {
fields g.MapOrd[g.String, g.String]
files g.Slice[*MultipartFile]
retry bool
}
// MultipartFile represents a single file for multipart upload.
type MultipartFile struct {
fieldName g.String
fileName g.String
contentType g.String
file *fs.File
reader io.Reader
}
// NewMultipart creates a new empty Multipart object.
func NewMultipart() *Multipart {
return &Multipart{
fields: g.NewMapOrd[g.String, g.String](),
files: g.NewSlice[*MultipartFile](),
}
}
// Field adds a form field to the multipart.
func (m *Multipart) Field(name, value g.String) *Multipart {
m.fields.Insert(name, value)
return m
}
// File adds a physical file to the multipart.
func (m *Multipart) File(fieldName g.String, file *fs.File) *Multipart {
f := &MultipartFile{
fieldName: fieldName,
fileName: file.Name(),
file: file,
}
m.files.Push(f)
return m
}
// FileReader adds a file from io.Reader to the multipart.
func (m *Multipart) FileReader(fieldName, fileName g.String, reader io.Reader) *Multipart {
f := &MultipartFile{
fieldName: fieldName,
fileName: fileName,
reader: reader,
}
m.files.Push(f)
return m
}
// FileString adds a file from string content to the multipart.
func (m *Multipart) FileString(fieldName, fileName, content g.String) *Multipart {
f := &MultipartFile{
fieldName: fieldName,
fileName: fileName,
reader: content.Reader(),
}
m.files.Push(f)
return m
}
// FileBytes adds a file from byte slice to the multipart.
func (m *Multipart) FileBytes(fieldName, fileName g.String, data g.Bytes) *Multipart {
f := &MultipartFile{
fieldName: fieldName,
fileName: fileName,
reader: data.Reader(),
}
m.files.Push(f)
return m
}
// ContentType sets the content type for the last added file.
// Must be called immediately after File/FileReader/FileString/FileBytes.
func (m *Multipart) ContentType(ct g.String) *Multipart {
if last := m.files.Last(); last.IsSome() {
last.Some().contentType = ct
}
return m
}
// FileName overrides the filename for the last added file.
// Useful when you want a different name than the physical file.
func (m *Multipart) FileName(name g.String) *Multipart {
if last := m.files.Last(); last.IsSome() {
last.Some().fileName = name
}
return m
}
// Retry controls whether the multipart body should be buffered in memory
// to support retries on status codes (429, 503, 5xx, etc.).
//
// When set, the body is fully read into memory before sending,
// allowing the client to replay it on retry.
//
// Recommended only for small requests (≤ 5–10 MB).
func (m *Multipart) Retry() *Multipart {
m.retry = true
return m
}
// prepareWriter writes the multipart data to a writer and returns the content type and write error.
func (m *Multipart) prepareWriter(boundary func() g.String) (io.ReadCloser, string, error) {
pr, pw := io.Pipe()
writer := multipart.NewWriter(pw)
if boundary != nil {
if err := writer.SetBoundary(boundary().Std()); err != nil {
_ = pw.CloseWithError(err)
_ = pr.Close()
return nil, "", err
}
}
go func() {
err := func() error {
for key, val := range m.fields.Iter() {
part, err := writer.CreateFormField(key.Std())
if err != nil {
return err
}
if _, err := io.Copy(part, val.Reader()); err != nil {
return err
}
}
for file := range m.files.Iter() {
var reader io.Reader
if file.file != nil {
res := file.file.Open()
if res.IsErr() {
return fmt.Errorf("cannot open file %q: %w", file.file.Name(), res.Err())
}
opened := res.Ok()
defer opened.Close()
reader = opened.Std()
} else if file.reader != nil {
reader = file.reader
} else {
return fmt.Errorf("multipart file %q has no content source", file.fileName.Std())
}
ct := file.contentType.Std()
if ct == "" {
ext := filepath.Ext(file.fileName.Std())
ct = mime.TypeByExtension(ext)
if ct == "" {
ct = "application/octet-stream"
}
}
disposition := fmt.Sprintf(
`form-data; name="%s"; filename="%s"`,
escapeQuotes(file.fieldName),
escapeQuotes(file.fileName),
)
h := textproto.MIMEHeader{
"Content-Disposition": {disposition},
"Content-Type": {ct},
}
part, err := writer.CreatePart(h)
if err != nil {
return err
}
if _, err := io.Copy(part, reader); err != nil {
return err
}
}
return writer.Close()
}()
pw.CloseWithError(err)
}()
return pr, writer.FormDataContentType(), nil
}
func escapeQuotes(s g.String) string { return s.ReplaceMulti(`\`, `\\`, `"`, `\"`).Std() }