174 lines
4.1 KiB
Go
174 lines
4.1 KiB
Go
package httpx
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/textproto"
|
|
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
)
|
|
|
|
// MultipartReader streams request parts and retains parsing failures, so a
|
|
// partially consumed or invalid upload cannot later complete successfully.
|
|
// It belongs to a single request and must be read sequentially.
|
|
type MultipartReader struct {
|
|
reader *multipart.Reader
|
|
input *multipartInput
|
|
current *MultipartPart
|
|
err error
|
|
done bool
|
|
}
|
|
|
|
// NextPart closes the preceding part and returns the next part. Like the
|
|
// standard library, it decodes quoted-printable content; NextRawPart does not.
|
|
// Only a valid final boundary yields io.EOF; malformed input yields an error
|
|
// with status 400. Body limits remain detectable as *http.MaxBytesError.
|
|
func (r *MultipartReader) NextPart() (*MultipartPart, error) { return r.next(false) }
|
|
|
|
func (r *MultipartReader) NextRawPart() (*MultipartPart, error) { return r.next(true) }
|
|
|
|
func (r *MultipartReader) next(raw bool) (*MultipartPart, error) {
|
|
if r.err != nil {
|
|
return nil, r.err
|
|
}
|
|
if r.done {
|
|
return nil, io.EOF
|
|
}
|
|
if r.current != nil {
|
|
if err := r.current.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var part *multipart.Part
|
|
var err error
|
|
if raw {
|
|
part, err = r.reader.NextRawPart()
|
|
} else {
|
|
part, err = r.reader.NextPart()
|
|
}
|
|
if err == io.EOF {
|
|
// The standard parser also returns bare EOF for truncated MIME
|
|
// headers. Require an actual closing delimiter before treating EOF
|
|
// as successful completion.
|
|
if !r.input.ended() {
|
|
return nil, r.record(io.ErrUnexpectedEOF)
|
|
}
|
|
r.done = true
|
|
return nil, io.EOF
|
|
}
|
|
if err != nil {
|
|
return nil, r.record(err)
|
|
}
|
|
r.current = &MultipartPart{Header: part.Header, part: part, owner: r}
|
|
return r.current, nil
|
|
}
|
|
|
|
func (r *MultipartReader) record(err error) error {
|
|
if err == nil || err == io.EOF {
|
|
return err
|
|
}
|
|
if r.err == nil {
|
|
var limit *http.MaxBytesError
|
|
if errors.As(err, &limit) {
|
|
r.err = err
|
|
} else {
|
|
r.err = apierrors.New(400, "INVALID_MULTIPART", "invalid multipart request body").WithCause(err)
|
|
}
|
|
}
|
|
return r.err
|
|
}
|
|
|
|
// MultipartPart is a streaming request part. Close drains unread content and
|
|
// reports read/parse errors; it never buffers the whole file or creates a file.
|
|
type MultipartPart struct {
|
|
Header textproto.MIMEHeader
|
|
part *multipart.Part
|
|
owner *MultipartReader
|
|
closed bool
|
|
err error
|
|
}
|
|
|
|
func (p *MultipartPart) FormName() string { return p.part.FormName() }
|
|
func (p *MultipartPart) FileName() string { return p.part.FileName() }
|
|
|
|
func (p *MultipartPart) Read(buf []byte) (int, error) {
|
|
if p.closed {
|
|
if p.err != nil {
|
|
return 0, p.err
|
|
}
|
|
return 0, io.EOF
|
|
}
|
|
n, err := p.part.Read(buf)
|
|
return n, p.owner.record(err)
|
|
}
|
|
|
|
func (p *MultipartPart) Close() error {
|
|
if !p.closed {
|
|
_, p.err = io.Copy(io.Discard, p)
|
|
p.closed = true
|
|
}
|
|
return p.err
|
|
}
|
|
|
|
// multipartInput observes closing-delimiter lines without buffering file
|
|
// content or changing bytes. The MIME parser still validates their syntax and
|
|
// placement. Keep only a prefix offset, including across reader boundaries.
|
|
type multipartInput struct {
|
|
io.Reader
|
|
boundary string
|
|
matched int
|
|
carriage, final, eof bool
|
|
}
|
|
|
|
func (r *multipartInput) Read(p []byte) (int, error) {
|
|
n, err := r.Reader.Read(p)
|
|
r.observe(p[:n])
|
|
if err == io.EOF {
|
|
r.eof = true
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (r *multipartInput) ended() bool {
|
|
return r.final || (r.eof && r.matched == len(r.boundary) && !r.carriage)
|
|
}
|
|
|
|
func (r *multipartInput) observe(data []byte) {
|
|
for len(data) > 0 && !r.final {
|
|
if r.matched < 0 {
|
|
newline := bytes.IndexByte(data, '\n')
|
|
if newline < 0 {
|
|
return
|
|
}
|
|
data, r.matched, r.carriage = data[newline+1:], 0, false
|
|
continue
|
|
}
|
|
c := data[0]
|
|
data = data[1:]
|
|
if r.matched < len(r.boundary) {
|
|
if c == r.boundary[r.matched] {
|
|
r.matched++
|
|
continue
|
|
}
|
|
} else {
|
|
switch {
|
|
case c == '\n':
|
|
r.final = true
|
|
continue
|
|
case !r.carriage && (c == ' ' || c == '\t'):
|
|
continue
|
|
case !r.carriage && c == '\r':
|
|
r.carriage = true
|
|
continue
|
|
}
|
|
}
|
|
r.matched, r.carriage = -1, false
|
|
if c == '\n' {
|
|
r.matched = 0
|
|
}
|
|
}
|
|
}
|