# File Uploads and Storage

Iwf separates multipart field parsing from file storage. Route and form decoding
can read ordinary multipart fields directly; applications provide file-aware
decoders only when they need uploaded file contents.

## 1. Multipart Data

The upload layer exposes:

```idris
record UploadedFile where
  constructor MkUploadedFile
  fieldName : String
  fileName : String
  contentType : String
  contents : String

record MultipartFormData where
  constructor MkMultipartFormData
  formFields : List (String, String)
  uploadedFiles : List UploadedFile
```

Use a `MultipartFileDecodeHook` when a route needs uploaded files:

```idris
decodeMultipartFormData : MultipartFileDecodeHook -> Request -> Either String MultipartFormData
```

Iwf also includes a framework parser for standard in-memory multipart uploads:

```idris
formData : Either String MultipartFormData
formData =
  parseMultipartFormData request
```

Because parsed `UploadedFile` values keep contents in memory, set explicit
limits for production endpoints:

```idris
limits : UploadLimits
limits =
  MkUploadLimits 10485760 52428800

formData : Either String MultipartFormData
formData =
  parseMultipartFormDataWithLimits limits request
```

`UploadLimits` checks the maximum bytes for each file and the maximum total
bytes across all parsed files. Use `defaultUploadLimits` when the endpoint does
not need a custom policy.

When a typed form needs only normal fields for route decoding, no hook is
required. `multipartFieldsOnlyHook` remains useful when you already have a
file-aware decoder and want to feed only its form fields into route decoding:

```idris
multipartFieldsOnlyHook fileAwareHook
```

Then read files from the full multipart data:

```idris
avatar : Maybe UploadedFile
avatar =
  multipartFile "avatar" formData

attachments : List UploadedFile
attachments =
  multipartFilesFor "attachments" formData
```

## 2. Local Storage

Store under a local directory:

```idris
avatarStorage : FileStorage
avatarStorage =
  localFileStorage (MkLocalFileStorage "uploads" "/uploads")

saveAvatar : UploadedFile -> IO (Either String StoredFile)
saveAvatar file =
  storeUploadedFile avatarStorage file
```

Local storage sanitizes filenames and returns a `StoredFile` descriptor with:

- backend
- key
- original filename
- content type
- byte length
- storage location
- public URL

Storage keys are collision-safe by default. `localStorageKey` and
`s3StorageKey` use `uploadCollisionSafeKey`, which combines a sanitized field
name, deterministic content hash, and sanitized original filename.

Persist metadata through the explicit descriptor helper:

```idris
metadata : StoredFileMetadata
metadata =
  storedFileMetadata storedFile
```

Use local storage for development, tests, or deployments where the upload
directory is persistent and backed up.

## 3. S3-Compatible Storage

Build an S3-compatible storage value:

```idris
s3Storage : S3FileStorage
s3Storage =
  MkS3FileStorage
    "https://s3.example.test"
    "my-bucket"
    "uploads"
    "https://cdn.example.test/uploads"
```

Iwf builds the PUT object request:

```idris
request : S3PutObject
request =
  s3PutObjectRequest s3Storage file
```

The app provides transport/signing:

```idris
saveToS3 : UploadedFile -> IO (Either String StoredFile)
saveToS3 file =
  storeUploadedFileS3 s3Storage transport file
```

This keeps cloud credentials and provider-specific request signing in the
application boundary. Use `signedS3Transport` when the app wants Iwf to run a
signing hook before the transport:

```idris
signer : S3RequestSigner
signer request =
  pure (Right ({ s3Headers := ("Authorization", "...") :: request.s3Headers } request))

transport : S3Transport
transport =
  signedS3Transport signer unsignedTransport
```

## 4. Private And Signed URLs

For private files, clear the public URL before persisting or returning the
descriptor:

```idris
privateFile : StoredFile
privateFile =
  privateStoredFile storedFile
```

For temporary signed URLs, provide an app-owned hook:

```idris
signer : SignedUrlHook
signer stored =
  pure (Right (stored.storedPublicUrl ++ "?signature=app-owned"))

signed : IO (Either String StoredFile)
signed =
  signStoredFileUrl signer storedFile
```

Iwf defines the hook boundary and updates the `StoredFile` descriptor. The app
owns signing algorithms, credentials, expiry, and access-control policy.

## 5. Forms

File forms should use direct HTML with a multipart route action. The route body
can include `UploadedFile`, `Maybe UploadedFile`, or `List UploadedFile`
fields:

```idris
uploadForm : ControllerContext -> Html
uploadForm context =
  <form method="post" action={pathTo uploadAvatarRoute} enctype="multipart/form-data">
    <label class="field">
      <span>Avatar</span>
      <input name={avatarField} type="file" />
    </label>
    <button type="submit" class="btn-primary">Upload</button>
  </form>
```

## Next

Read [Integrations, I18n, and Payments](integrations-i18n-and-payments.md).
