NAME
Punk::Upload - an uploaded file from a multipart form
SYNOPSIS
post '/avatar' => sub {
my ($c) = @_;
my $up = $c->req->upload('avatar') or return $c->text('no file', 400);
$up->save("/var/uploads/" . $up->filename);
$c->json({ name => $up->filename, bytes => $up->size });
};
DESCRIPTION
A file part of a multipart/form-data request. $c->req->form parses the body once: ordinary fields become parameters ($c->param), and file parts become Punk::Upload objects reachable through $c->req->upload($name) (or $c->upload($name)) and $c->req->uploads. A field uploaded more than once yields an arrayref from uploads; upload gives the first.
A part smaller than 64 KiB is held in memory. A larger one is written to a temp file as it arrives, and the object carries a path to it rather than the bytes - so an upload costs a file and a few kilobytes, whatever its size.
METHODS
filename
The client-supplied file name. Do not trust it as a path. It is request bytes: it never names the temp file, and it should never name yours.
name
The form field name.
type
The part's Content-Type (application/octet-stream if the client sent none).
size
The byte length.
path
The temp file holding the bytes, or undef when the part was small enough to stay in memory.
The file belongs to the request and is removed when it ends - including when the handler died. Do not keep the path expecting the file to still be there; use save or "fh".
fh
A read handle on the temp file, or undef when the bytes are in memory.
The way to process a large upload without ever holding it: read it in chunks, hash it, hand it to something that takes a handle.
content
The uploaded bytes.
When the part is on disk this reads the whole file into memory, which is exactly what streaming it to disk avoided. That is fine for a small part and is why the method still exists and still works; on a large one it is the cost you were avoiding, paid in one line. "fh" or "path" instead.
save($path)
Put the uploaded bytes at $path. Returns true, croaks if it cannot.
When the part is already a file and $path is on the same filesystem, this is a rename - the bytes are not read or written at all. Across filesystems it copies in chunks, still without them passing through memory. A part held in memory is simply written out.
So the spill directory wants to be on the same filesystem as wherever you keep things. That is the difference between free and another whole copy of a large file.
WHERE THE TEMP FILES LIVE
upload_dir on the application, else TMPDIR, else /tmp:
upload_dir '/var/lib/myapp/incoming';
Name it, for two reasons. It decides the filesystem, which decides whether save is a rename. And it decides what is on that filesystem: uploads are attacker-controlled bytes taking attacker-chosen amounts of space, bounded by the server's request ceiling, and putting them somewhere with the room and the permissions you intended is better than discovering /tmp was shared with something that mattered.
Names there owe nothing to the client's filename, and every file is removed when its request ends.
SIZE, HONESTLY
An upload no longer arrives in memory at all. Measured end to end through a socket, into a handler that holds the upload:
128 MiB upload worker RSS 15.5 MiB
against roughly 275 MiB before the streaming work, when the bytes were resident four times over - the server's read buffer, the SV behind psgi.input, Punk's own slurp of the body, and the decoded part.
What still bounds you:
"max_body" in Hyperman is the largest request accepted. It is no longer a memory ceiling, so it can be raised for uploads without raising what a worker holds.
Chunked bodies are not spilled by Hyperman. A
Transfer-Encoding: chunkedrequest has noContent-Lengthto divert on and still accumulates in memory.Disk is now the resource an upload spends. A ceiling is still the thing standing between a form and a full filesystem, and streaming does not make attacker-controlled bytes safe to accept - only cheaper to receive.
SEE ALSO
Punk, "upload" in Punk::Request, "max_body" in Punk, "max_body: the request ceiling" in Hyperman.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)