File uploads
This guide covers accepting file uploads in Tinka. You will learn how to:
- Accept a file, and its form fields, in one typed value
- Validate a file's size and type
- Save a file under a safe name
- Accept several files, and files nested in lists
- Change how large an upload may be
Overview
Browsers upload files in multipart/form-data forms. c.Body decodes them like any other form, and puts each file in a field of type *tinka.File, next to the form's other fields, so one type describes the whole form and one Valid method checks it.
Small files are kept in memory, and larger ones in temporary files. Either way they're removed when the request ends, so a handler saves the files it keeps.
Accept your first file
Declare a *tinka.File field with the name of the file input:
<form method="post" action="/profile" enctype="multipart/form-data">
<input name="name">
<input type="file" name="avatar" accept="image/png, image/jpeg">
<button>Save</button>
</form>package main
import (
"github.com/mucolabs/tinka"
)
type profileForm struct {
Name string `form:"name"`
Avatar *tinka.File `form:"avatar"`
}
type Profiles struct {
Dir string // where avatars are stored
}
func (h Profiles) Update(c *tinka.Ctx) (tinka.Response, error) {
in, err := c.Body[profileForm]()
if err != nil {
return nil, err
}
if in.Avatar != nil {
path, err := in.Avatar.Save(h.Dir)
if err != nil {
return nil, err
}
_ = path // store the path with the user's profile
}
return tinka.Back("/profile"), nil
}A file input left empty sends no file, so the field stays nil. A tinka.File describes the upload:
Validate uploaded files
Check files in Valid, with the form's other fields, so every problem comes back in one 422. Check Type for the kind of file, and Size for its weight:
func (f profileForm) Valid(ctx context.Context) map[string]string {
problems := map[string]string{}
if strings.TrimSpace(f.Name) == "" {
problems["name"] = "Enter your name."
}
switch {
case f.Avatar == nil:
case f.Avatar.Type != "image/png" && f.Avatar.Type != "image/jpeg":
problems["avatar"] = "Choose a PNG or JPEG picture."
case f.Avatar.Size > 2<<20:
problems["avatar"] = "Choose a picture under 2 MiB."
}
return problems
}Save files
Save copies a file into a directory, creating it if needed, under a new random name, and returns the path. It never uses the client's file name, which could overwrite another file or reach outside the directory. The name's extension matches the file's Type, not its name, so a file server that picks the Content-Type by extension serves it as what it is: a PNG uploaded as page.html is saved as .png, and can't run as a page.
SaveAs copies a file to a path you choose. It fails with an error matching fs.ErrExist when the path exists, instead of overwriting it:
err := in.Avatar.SaveAs(filepath.Join(h.Dir, user.ID+".png"))
if errors.Is(err, fs.ErrExist) {
// replace the old avatar deliberately: remove it, then save
}To serve saved files, send them with tinka.SendFile, or serve their directory with http.FileServerFS.
Accept several files
An input with multiple, or several inputs with the same name, sends several files. Declare a []*tinka.File field:
<input type="file" name="photos" multiple>type albumForm struct {
Title string `form:"title"`
Photos []*tinka.File `form:"photos"`
}Files nest like other form fields, so a list of photos, each with a caption, is a list of structs:
<input name="photos[0][caption]">
<input type="file" name="photos[0][image]">
<input name="photos[1][caption]">
<input type="file" name="photos[1][image]">type albumForm struct {
Title string `form:"title"`
Photos []struct {
Caption string `form:"caption"`
Image *tinka.File `form:"image"`
} `form:"photos"`
}Change the upload limit
A multipart body, files included, is at most 20 MiB, and has at most 1,000 parts. Raise the limit for the App with MaxUploadBytes, or for the routes that need it with tinka.BodyLimit:
app.With(tinka.BodyLimit(500<<20)).Post("/videos", uploadVideo)Up to 4 MiB of a form is kept in memory; the rest of its files go to temporary files in the system's temporary directory (TMPDIR on Unix), so a large upload costs disk, not memory.
See also: Body parser, Response, Exception handling