Upload
Overview
Pogo Upload lets PHP applications create signed upload intents while a Caddy/Go handler receives the raw request body, enforces limits, writes to storage, computes a checksum, and notifies a PHP worker when the upload completes or fails.
It is for large or controlled single-file uploads. It is not a file manager, media processor, CDN, object-storage abstraction, virus scanner, or replacement for normal framework uploads for small forms.
Status and fit
Pogo Upload is experimental. Its v1 module has a local filesystem backend and process-local progress tracking.
Use it when:
- A normal authenticated controller should authorize and create upload intents.
- PHP should avoid spending request time moving large upload bodies.
- One raw file per request is enough.
- Your completion handler can be idempotent.
Avoid it when:
- You need multipart form parsing.
- You need durable distributed progress.
- You need antivirus scanning, media processing, or CDN publishing in the module.
- Your framework needs to receive an
UploadedFileobject directly.
Build
Compile the module into FrankenPHP:
xcaddy build \
--with github.com/dunglas/frankenphp@v1.12.4 \
--with github.com/dunglas/frankenphp/caddy@v1.12.4 \
--with github.com/y-l-g/upload/module@master
Caddy configuration
pogo_upload has two parts: a global app that defines stores, and an HTTP handler that accepts signed upload URLs for one store.
{
frankenphp
pogo_upload {
store default {
worker public/upload-worker.php
signing_secret {$POGO_UPLOAD_SECRET}
backend local {
root storage/app/pogo-uploads
}
token_ttl 15m
max_upload_bytes 1073741824
max_concurrency 32
read_timeout 30s
complete_timeout 10s
progress_ttl 10m
}
}
}
:80 {
handle /_pogo/upload/* {
pogo_upload default
}
php_server
}
Store directives:
| Directive | Required | Default | Description |
|---|---|---|---|
worker | Yes | none | PHP worker script that receives completion and failure events. |
signing_secret | Yes | none | Secret used to sign upload URLs. Keep it stable across reloads and replicas. |
backend local | Yes | local | The v1 storage backend. |
root | Yes | none | Local backend root directory. |
token_ttl | No | 15m | Default signed URL lifetime. |
max_upload_bytes | No | 1073741824 | Store-level hard upload limit. |
max_concurrency | No | 32 | Max in-flight uploads for the store. |
read_timeout | No | 30s | Max idle time while reading the request body. |
complete_timeout | No | 10s | Max wait for the PHP completion worker. |
progress_ttl | No | 10m | Time finished progress records remain visible. |
Application integration
The current repository exposes native PHP functions directly. Framework services can wrap these functions, but no Laravel or Symfony upload package is shipped here yet.
Create an upload intent from a normal authenticated controller:
$intent = pogo_upload_create([
'key' => 'users/'.$userId.'/avatars/'.$uuid.'.jpg',
'filename' => 'avatar.jpg',
'content_types' => ['image/jpeg', 'image/png'],
'max_bytes' => 5242880,
'overwrite' => false,
'expires_in' => 900,
'metadata' => [
'user_id' => (string) $userId,
'purpose' => 'avatar',
],
]);
The client uploads one raw file body to the returned url with the returned HTTP method.
PHP API
pogo_upload_create(array $intent, string $store = 'default'): array;
pogo_upload_progress(string $uploadId, string $store = 'default'): ?array;
pogo_upload_cancel(string $uploadId, string $store = 'default'): bool;
pogo_upload_status(?string $store = null): string;
Intent fields:
| Field | Required | Description |
|---|---|---|
key | Yes | Backend object key. Must be relative and normalized. |
filename | No | Original filename for application metadata. |
content_types | No | Accepted request Content-Type values. |
max_bytes | Yes | Positive maximum body size. |
overwrite | No | Defaults to false; rejects existing final keys when false. |
expires_in | No | Signed URL lifetime in seconds. Defaults to store token_ttl. |
metadata | No | String map returned unchanged in worker events. |
pogo_upload_status() returns JSON with readiness, configured limits, active upload counts, completed/failed/cancelled counters, token/content-type/size rejections, bytes received, backend failures, and worker event failures.
Worker events
The worker is a normal FrankenPHP worker script that receives JSON and returns JSON.
Completed event:
{
"type": "completed",
"upload_id": "upl_01jz2k4v4x9s8m1px6h0y7f2am",
"store": "default",
"key": "users/123/uploads/avatar.jpg",
"filename": "avatar.jpg",
"content_type": "image/jpeg",
"bytes": 428129,
"sha256": "1ecb9f...",
"metadata": {
"user_id": "123",
"purpose": "avatar"
}
}
Failure event:
{
"type": "failed",
"upload_id": "upl_01jz2k4v4x9s8m1px6h0y7f2am",
"store": "default",
"key": "users/123/uploads/avatar.jpg",
"reason": "client_aborted",
"bytes_received": 1048576,
"metadata": {
"user_id": "123",
"purpose": "avatar"
}
}
Expected worker response:
{
"ok": true
}
If the completed handler returns ok: false or times out, the module reports an upload failure to the client and attempts to delete the stored object. Failure event handler errors are logged and counted but do not change an already-failed HTTP response.
Operations
- Treat returned upload URLs as bearer tokens.
- Keep token TTLs short.
- Use HTTPS in production.
- Store objects outside the public web root unless the application explicitly publishes them.
- Use shared signing secrets and storage roots when multiple replicas can accept uploads.
- Do not expose object keys, token values, filenames, or metadata in metrics.
Troubleshooting
- Invalid token: check
signing_secret, expiry, and whether the client is using the returned upload URL unchanged. - Content type rejected: make sure the request
Content-Typematches the intent'scontent_types. - Upload too large: check both the intent
max_bytesand storemax_upload_bytes. - Unknown progress: progress is process-local and can expire after
progress_ttlor disappear after restart.