# File uploads

> Accept files in multipart forms as typed fields, validate their size and type, and save them safely.

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`](body-parser.md) 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:

```html title="views/profile/edit.html"
<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>
```

```go title="profile.go"
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:

::::options

:::option{name="Filename" type="string"}
The file's name on the client, without any directory. Show it to people, but never use it as a path.
:::

:::option{name="Size" type="int64"}
The file's size in bytes.
:::

:::option{name="Type" type="string"}
The file's media type, such as `image/png`, read from its first bytes, so a file can't pass as an image by its name or its part's `Content-Type`. For formats those bytes can't tell apart, such as CSV and the zip-based office formats, it comes from the extension. A name never makes a file HTML, SVG, XML, a script, CSS or PDF: only its content can, so a script named `page.html` stays `application/octet-stream` or `text/plain`.
:::

:::option{name="Ext()" type="string"}
The extension of `Filename`, lowercased and without the dot: `png` for `Photo.PNG`. The client chose it, so check `Type` to know what the file is.
:::

:::option{name="Field" type="string"}
The form field the file came in, such as `photos[2]`.
:::

:::option{name="Open()" type="(multipart.File, error)"}
Opens the file for reading, for a handler that processes it instead of storing it.
:::

::::

## 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:

```go title="profile.go" ins={1-14}
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
}
```

:::warning
Check `Type`, never `Filename` or the part's `Content-Type` header, to decide what a file is. Both come from the client: a script named `avatar.png` and sent as `image/png` passes a check on either, and is then served to other users as whatever your App believed it was.

```go title="profile.go" del={1} ins={2}
	case f.Avatar.Ext() != "png":
	case f.Avatar.Type != "image/png":
```
:::

## 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:

```go
	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`](response.md), 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:

```html title="views/albums/new.html"
<input type="file" name="photos" multiple>
```

```go title="albums.go"
type albumForm struct {
	Title  string        `form:"title"`
	Photos []*tinka.File `form:"photos"`
}
```

Files nest like other [form fields](body-parser.md#form-fields), so a list of photos, each with a caption, is a list of structs:

```html title="views/albums/new.html"
<input name="photos[0][caption]">
<input type="file" name="photos[0][image]">
<input name="photos[1][caption]">
<input type="file" name="photos[1][image]">
```

```go title="albums.go"
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`:

```go title="main.go"
	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](body-parser.md), [Response](response.md), [Exception handling](exception-handling.md)
