Large files
Videos, archives, database dumps weigh hundreds of megabytes and more. files()->upload() copes
with them on its own, and uploads()->send() gives what a long upload needs: progress, a session
id and the ability to continue where it broke — even after the process restarts.
Upload with progress
$file = $s5w->uploads()->send(
'/backups/db-2026-11-07.sql',
folder: 'backups',
onProgress: fn(int $sent, int $total) => printf("%d%%\n", $sent * 100 / $total),
);0%
8%
16%
…
99%
100%send() cuts the file into chunks of the size the server asks for (8 MiB at the moment), sends
them in order and assembles the file at the end. It returns the same File as an ordinary upload.
onProgress gets two numbers: $sent — how many bytes are already on the server, $total —
the file’s size. It is called:
| When | $sent |
|---|---|
| at the start, before the first chunk | 0 |
| after every chunk the server confirmed | the new offset |
| after recovery, if a chunk landed but its answer was lost | called too — progress is not lost |
on resume() — the first call |
where the upload continues from |
| on deduplication (nothing had to be sent) | once, right away with $sent === $total |
$total is always greater than zero — the server refuses an empty upload — so division is safe.
Keep the session id
If the process may stop in the middle of an upload — a queued job, a worker restarted on deploy —
save the session id right after it opens. That is what onStart is for:
use Flytachi\Winter\SDK\S5w\Entity\UploadSession;
$file = $s5w->uploads()->send(
$path,
folder: 'videos',
onStart: fn(UploadSession $session) => $job->saveUploadId($session->id),
onProgress: fn(int $sent, int $total) => $job->saveProgress($sent, $total),
);
$job->markDone($file->id);onStart is called once, before the first byte is sent. If the content turned out to be in the
bucket already and nothing has to be sent (deduplication), there is no session and onStart is
not called.
Continue after a failure
A connection failure mid-upload
send() survives a broken chunk on its own: it waits, asks the server how much it got, and
continues from there. If the connection never comes back — by default three attempts per chunk
with 1 and 2 second pauses — send() throws UploadInterruptedException. The session stays
on the server and can be continued:
use Flytachi\Winter\SDK\S5w\Exception\UploadInterruptedException;
try {
$file = $s5w->uploads()->send($path, folder: 'videos');
} catch (UploadInterruptedException $e) {
$logger->warning("Upload interrupted at {$e->offset()} of {$e->size()} bytes");
// later — once the network is back
$file = $s5w->uploads()->resume($e->sessionId(), $path);
}A process restart
If the process crashed or was restarted, there is no exception any more — but there is the
session id you saved in onStart. Continue by it:
$uploadId = $job->uploadId();
if ($uploadId !== null) {
$file = $s5w->uploads()->resume(
$uploadId,
$path,
onProgress: fn(int $sent, int $total) => $job->saveProgress($sent, $total),
);
}resume() asks the server how many bytes it already holds, checks that the file has the size
declared at the start, and sends the rest. The first onProgress call shows where it continued
from — 45%, for example.
What can go wrong on resume() |
What happens |
|---|---|
| the session is gone: more than a day without new chunks, it was aborted or already completed | NotFoundException — start the upload over |
| a different file was passed (the size does not match) | S5wException with … not the same file |
A session lives 24 hours
An unfinished session is removed by the server 24 hours after the last chunk it received. Until it is completed, a session takes no space in the bucket’s quota.
Deduplication: don’t send what is already there
Before opening a session send() computes the file’s SHA-256 and tells the server. If that
content is already in the bucket, the server creates the file at once and nothing has to be sent:
$first = $s5w->uploads()->send('/backups/big.sql', folder: 'backups'); // sends 100 MB
$second = $s5w->uploads()->send('/backups/big.sql', folder: 'backups'); // ~0.3 s, sends nothing
$second->deduplicated; // true
$second->name; // "big (1).sql"The hash takes one pass over the file on disk — about a second for a gigabyte — and pays off whenever the file was uploaded before. If you know the content is new, switch the check off:
$s5w->uploads()->send($path, deduplicate: false);With image processing options (imageOptions) the hash is never sent: the bucket stores the
processed result, and the original’s hash will not match it. More — in
Upload pipeline.
From memory
For large data already in memory there is sendContent() — the same parameters as send(), with
bytes instead of a path and a required name:
$file = $s5w->uploads()->sendContent($archiveBytes, 'export.zip', folder: 'exports');resume() works only with a file on disk — bytes in memory are gone after a restart anyway.
Timeouts
Each chunk is a separate request with the transferTimeout limit (300 seconds per chunk by
default). That is plenty for an ordinary connection: an 8 MiB chunk at 10 MB/s takes under a
second. On a very slow link raise the value:
$config = (new S5wConfig($url, $key))->withTransferTimeout(900);The number of attempts per chunk (3) is not configurable — it is the Uploads::ATTEMPTS
constant.
What to choose
| Need | Call |
|---|---|
| just upload, whatever the size | files()->upload() |
| show progress | uploads()->send(…, onProgress: …) |
| continue after a restart | uploads()->send(…, onStart: …) + uploads()->resume() |
| your own loop, the file arrives in pieces | Step-by-step uploads |
See also
Uploads::send()andUploads::resume()— every parameter- Upload pipeline — how retries and recovery work
- Chunked uploads on the server — the
/v1/uploadsprotocol