Package · sdk-s5w

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

php
$file = $s5w->uploads()->send(
  '/backups/db-2026-11-07.sql',
  folder: 'backups',
  onProgress: fn(int $sent, int $total) => printf("%d%%\n", $sent * 100 / $total),
);
bash
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:

php
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:

php
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:

php
$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:

php
$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:

php
$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:

php
$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:

php
$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