Iwf Guide
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:
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:
decodeMultipartFormData : MultipartFileDecodeHook -> Request -> Either String MultipartFormData
Iwf also includes a framework parser for standard in-memory multipart uploads:
formData : Either String MultipartFormData
formData =
parseMultipartFormData request
Because parsed UploadedFile values keep contents in memory, set explicit limits for production endpoints:
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:
multipartFieldsOnlyHook fileAwareHook
Then read files from the full multipart data:
avatar : Maybe UploadedFile
avatar =
multipartFile "avatar" formData
attachments : List UploadedFile
attachments =
multipartFilesFor "attachments" formData
2. Local Storage
Store under a local directory:
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:
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:
s3Storage : S3FileStorage
s3Storage =
MkS3FileStorage
"https://s3.example.test"
"my-bucket"
"uploads"
"https://cdn.example.test/uploads"
Iwf builds the PUT object request:
request : S3PutObject
request =
s3PutObjectRequest s3Storage file
The app provides transport/signing:
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:
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:
privateFile : StoredFile
privateFile =
privateStoredFile storedFile
For temporary signed URLs, provide an app-owned hook:
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:
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>