itermultipart

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2024 License: MIT Imports: 15 Imported by: 0

README

itermultipart - A convenient way to work with multipart/form-data messages using iterators

Build Status Go Report Card Go Reference

Itermultipart simplifies reading of multipart messages by providing an iterator interface to multipart.Reader and creating multipart messages from iterators.

Features

  • Functions to convert multipart.Reader or http.Request to an iterator suitable for for range loop
  • Generate multipart messages from iterators. Message generator implements io.Reader interface, so it's suitable for http request body directly.
  • Convenient parts constructor with fluent interface
  • Zero-dependency

Reading parts

Reading parts via wrapped multipart.Reader is an efficient way to work with multipart messages. This also allows to be more flexible with limitation of message/part size, parts count and memory usage.

func SimpleFileSaveHandler(w http.ResponseWriter, *r http.Request) {
	for part, err := range itermultipart.PartsFromRequest(r) {
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Save part to file
		if part.FileName() == "" {
			http.Error(w, "File name is required", http.StatusBadRequest)
			return
		}

		file, err := os.Create(part.FileName())
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		
		_, err = io.Copy(file, part.Content)
		file.Close()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
}

Also, you can feed standard multipart.Reader to itermultipart.PartsFromReader function.

Creating HTTP request

Traditional way with multipart.Writer:

func CreateMultipartRequest() (*http.Request, error) {
	pr, pw := io.Pipe()
	mw := multipart.NewWriter(pw)
	
	go func() {
		// Write parts
		part, err := mw.CreateFormFile("file", "file.txt")
		if err != nil {
			pw.CloseWithError(err)
			return
		}
		
		_, err = part.Write([]byte("Hello, world!"))
		if err != nil {
			pw.CloseWithError(err)
			return
		}
		
		pw.CloseWithError(mw.Close())
	}()
	
	req, err := http.NewRequest("POST", "http://example.com/upload", pr)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", mw.FormDataContentType())
	
	return req, nil
}

As you may notice it requires extra goroutine and io.Pipe to work with multipart.Writer. This makes impossible to use some optimizations provided by io.ReaderFrom or io.WriterTo interfaces i.e. direct file-socket or socket-socket transfer.

However, with you can use itermultipart.Source to create multipart message from iterator:

func CreateMultipartRequest() (*http.Request, error) {
	src := itermultipart.NewSource(itermultipart.PartSeq(
		itermultipart.NewPart().
			SetFormName("file").
			SetFileName("file.txt").
			SetContentString("Hello, world!"),
	))
	req, err := http.NewRequest("POST", "http://example.com/upload", src)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", src.FormDataContentType())
	return req, nil
}

As you can see its much simpler and doesn't require extra goroutine and io.Pipe. PartSeq here is a simple helper that transforms list of parts to iterator.

Creating parts

itermultipart.NewPart provides a fluent interface to create parts:

part := itermultipart.NewPart().
	SetFormName("file").
	SetFileName("file.txt").
	SetContentString("Hello, world!")

Content may be set via methods:

  • SetContent - set content directly from io.Reader
  • SetContentString - use provided string as content
  • SetContentBytes - use provided byte slice as content

To define a content-type you can use:

  • SetContentType - set content type directly
  • SetContentTypeByExtension - set content type by file extension if SetFileName called before
  • DetectContentType - peeks first 512 bytes from content and tries to recognize content type

Even if you don't want to use itermultipart.Source to create a multipart message, itermultipart.NewPart still may be useful. It's method AddToWriter allows to add part to standard multipart.Writer:

func WritePartToMultipartWriter(w *multipart.Writer) error {
	return itermultipart.NewPart().
		SetFormName("file").
		SetFileName("file.txt").
		SetContentString("Hello, world!").
		AddToWriter(w)
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func PartSeq

func PartSeq(parts ...*Part) iter.Seq2[*Part, error]

PartSeq returns a sequence of parts from the provided list.

func PartsFromReader

func PartsFromReader(r *multipart.Reader, raw bool) iter.Seq2[*Part, error]

PartsFromReader reads each part from the provided multipart.Reader and yields it to the caller. If raw is true, it reads the raw part using multipart.Reader.NextRawPart. Note that Part becomes invalid on the next iteration so reference to it must not be held.

func PartsFromRequest

func PartsFromRequest(r *http.Request, raw bool) iter.Seq2[*Part, error]

PartsFromRequest reads each part from the http request and yields it to the caller. If raw is true, it reads the raw part using multipart.Part.NextRawPart. Note that Part becomes invalid on the next iteration so reference to it must not be held.

Example
package main

import (
	"fmt"
	"io"
	"maps"
	"net/http/httptest"
	"os"
	"slices"
	"strings"

	"github.com/xakep666/itermultipart"
)

func main() {
	message := `--boundary
Content-Disposition: form-data; name="myfile"; filename="example.txt"

contents of myfile
--boundary
Content-Disposition: form-data; name="key"

value for key
--boundary--`
	message = strings.ReplaceAll(message, "\n", "\r\n")
	r := httptest.NewRequest("POST", "/", strings.NewReader(message))
	r.Header.Set("Content-Type", "multipart/form-data; boundary=boundary")

	for part, err := range itermultipart.PartsFromRequest(r, false) {
		if err != nil {
			panic(err)
		}
		if part == nil {
			continue
		}

		fmt.Println("---headers---")
		for _, k := range slices.Sorted(maps.Keys(part.Header)) {
			fmt.Printf("%s: %s\n", k, part.Header[k])
		}
		fmt.Println("---identifiers---")
		if part.FormName() != "" {
			fmt.Println("name:", part.FormName())
		}
		if part.FileName() != "" {
			fmt.Println("filename:", part.FileName())
		}
		fmt.Println("---content---")
		io.Copy(os.Stdout, part.Content)
		fmt.Println()
	}
}
Output:

---headers---
Content-Disposition: [form-data; name="myfile"; filename="example.txt"]
---identifiers---
name: myfile
filename: example.txt
---content---
contents of myfile
---headers---
Content-Disposition: [form-data; name="key"]
---identifiers---
name: key
---content---
value for key

Types

type Part

type Part struct {
	Header  textproto.MIMEHeader
	Content io.Reader
	// contains filtered or unexported fields
}

Part represents a part of a multipart message.

func NewPart

func NewPart() *Part

NewPart creates a new part.

Example
package main

import (
	"fmt"
	"io"
	"maps"
	"os"
	"slices"

	"github.com/xakep666/itermultipart"
)

func main() {
	part := itermultipart.NewPart().
		SetFormName("customfile").
		SetFileName("example.txt").
		SetContentTypeByExtension().
		SetHeaderValue("X-Custom-Header", "value").
		SetContentString("Hello, World!")

	for _, k := range slices.Sorted(maps.Keys(part.Header)) {
		fmt.Printf("%s: %s\n", k, part.Header[k])
	}
	fmt.Println("---")
	io.Copy(os.Stdout, part.Content)
}
Output:

Content-Disposition: [form-data; filename=example.txt; name=customfile]
Content-Type: [text/plain; charset=utf-8]
X-Custom-Header: [value]
---
Hello, World!

func (*Part) AddHeaderValue

func (p *Part) AddHeaderValue(key, value string) *Part

AddHeaderValue adds the value to the given header key.

func (*Part) AddToWriter

func (p *Part) AddToWriter(mw *multipart.Writer) error

AddToWriter adds the part to the standard mime/multipart.Writer.

Example
package main

import (
	"bytes"
	"fmt"
	"mime/multipart"
	"strings"

	"github.com/xakep666/itermultipart"
)

func main() {
	var buf bytes.Buffer
	mw := multipart.NewWriter(&buf)
	mw.SetBoundary("boundary")

	itermultipart.NewPart().
		SetFormName("customfile").
		SetFileName("example.txt").
		SetContentTypeByExtension().
		SetHeaderValue("X-Custom-Header", "value").
		SetContentString("Hello, World!").
		AddToWriter(mw)

	itermultipart.NewPart().
		SetFormName("key").
		SetContentString("val").
		AddToWriter(mw)

	mw.Close()

	fmt.Println(strings.ReplaceAll(buf.String(), "\r\n", "\n"))
}
Output:

--boundary
Content-Disposition: form-data; filename=example.txt; name=customfile
Content-Type: text/plain; charset=utf-8
X-Custom-Header: value

Hello, World!
--boundary
Content-Disposition: form-data; name=key

val
--boundary--

func (*Part) ContentType

func (p *Part) ContentType() string

ContentType returns the content type of the part.

func (*Part) DetectContentType

func (p *Part) DetectContentType() *Part

DetectContentType detects the content type of the part using net/http.DetectContentType. It peeks the first 512 bytes of the content to determine the content type. Content must be already set before calling this method. If content-type cannot be detected, it sets the content type to "application/octet-stream". Note that this method modifies Content field of the part.

Example
package main

import (
	"fmt"

	"github.com/xakep666/itermultipart"
)

func main() {
	part := itermultipart.NewPart().
		SetFormName("customfile").
		SetFileName("example.txt").
		SetContentString("<html><body>test</body></html>").
		DetectContentType()

	fmt.Println(part.ContentType())
}
Output:

text/html; charset=utf-8

func (*Part) FileName

func (p *Part) FileName() string

FileName returns the filename parameter of the Part's Content-Disposition header. If not empty, the filename is passed through filepath.Base (which is platform dependent) before being returned.

func (*Part) FormName

func (p *Part) FormName() string

FormName returns the name parameter if p has a Content-Disposition of type "form-data". Otherwise, it returns the empty string.

func (*Part) MergeHeaders

func (p *Part) MergeHeaders(h textproto.MIMEHeader) *Part

MergeHeaders merges the given headers into the part's headers.

func (*Part) Reset

func (p *Part) Reset()

Reset resets the part to its initial state.

func (*Part) SetContent

func (p *Part) SetContent(content io.Reader) *Part

SetContent sets the content of the part.

func (*Part) SetContentBytes

func (p *Part) SetContentBytes(content []byte) *Part

SetContentBytes sets the content of the part to the given bytes.

func (*Part) SetContentString

func (p *Part) SetContentString(content string) *Part

SetContentString sets the content of the part to the given string.

func (*Part) SetContentType

func (p *Part) SetContentType(contentType string) *Part

SetContentType sets the content type of the part.

func (*Part) SetContentTypeByExtension

func (p *Part) SetContentTypeByExtension() *Part

SetContentTypeByExtension sets the content type of the part based on the file extension. If the file name was not set, it does nothing. The content type is set using mime.TypeByExtension so you can register custom types using mime.AddExtensionType.

func (*Part) SetFileName

func (p *Part) SetFileName(fileName string) *Part

SetFileName sets the file name of the part. It also sets the "Content-Type" header to "application/octet-stream" like multipart.Writer.CreateFormFile.

func (*Part) SetFormName

func (p *Part) SetFormName(formName string) *Part

SetFormName sets the form name of the part.

func (*Part) SetHeaderValue

func (p *Part) SetHeaderValue(key, value string) *Part

SetHeaderValue sets the value of the given header key.

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source is a generator of multipart message as you read from it.

func NewSource

func NewSource(parts iter.Seq2[*Part, error]) *Source

NewSource returns a new Source that generates a multipart message from provided part sequence. Part sequence must be finite. Source holds reference for Part only until it's fully read.

func (*Source) Boundary

func (s *Source) Boundary() string

Boundary returns the Source's boundary.

func (*Source) Close

func (s *Source) Close() error

Close closes the Source, preventing further reads.

func (*Source) FormDataContentType

func (s *Source) FormDataContentType() string

FormDataContentType returns the Content-Type for an HTTP multipart/form-data with this Source's Boundary.

func (*Source) Read

func (s *Source) Read(p []byte) (n int, err error)

Read implements io.Reader.

func (*Source) Reset

func (s *Source) Reset(parts iter.Seq2[*Part, error])

Reset resets the Source to use the provided part sequence.

func (*Source) SetBoundary

func (s *Source) SetBoundary(boundary string) error

SetBoundary overrides the Source's default randomly-generated boundary separator with an explicit value.

SetBoundary must be called before any parts are created, may only contain certain ASCII characters, and must be non-empty and at most 70 bytes long.

func (*Source) WriteTo

func (s *Source) WriteTo(target io.Writer) (int64, error)

WriteTo implements the io.WriterTo interface allowing some source-target optimizations to be used.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL