Step-by-step uploads
uploads()->send() does the whole chunked upload for you. Sometimes the loop has to be yours
though: data arrives in pieces from another source, or you want to decide on retries and
cancelling yourself. For that uploads() has five methods — one per request to the server.
Five steps
| Method | Request | What it does |
|---|---|---|
start() |
POST /v1/uploads |
opens a session; checks the folder and the quota before any byte moves |
append() |
PATCH /v1/uploads/{id} |
sends the next chunk |
status() |
GET /v1/uploads/{id} |
asks how many bytes the server already holds |
complete() |
POST /v1/uploads/{id}/complete |
turns the received bytes into a file |
abort() |
DELETE /v1/uploads/{id} |
cancels the session and deletes what was received |
Every method except complete() and abort() returns an UploadSession — the “receipt” for
the state of the upload:
$session->id; // "6444c675-4884-4cbb-8324-c6f9a5bcd0e1"
$session->size; // 100725660 — how much has to be sent in total
$session->offset; // 16777216 — how much is on the server; the next chunk starts here
$session->chunkSize; // 8388608 — the chunk size the server asks for
$session->expiresAt; // when the session is removed if no chunk arrives
$session->isComplete(); // offset === size
$session->remaining(); // size - offset
$session->isDeduplicated(); // the content was already in the bucket, the file is readyThe whole loop
The same scenario send() runs, entirely in your code:
use Flytachi\Winter\SDK\S5w\Entity\File;
use Flytachi\Winter\SDK\S5w\Exception\ConflictException;
use Flytachi\Winter\SDK\S5w\S5wException;
public function uploadCustom(string $path): File
{
$uploads = $this->s5w->uploads();
// 1. Open a session. Everything the server can refuse, it refuses here:
// no such folder, not enough quota. With the hash, known content becomes a file at once.
$session = $uploads->start(
name: basename($path),
size: filesize($path),
folder: 'backups',
sha256: hash_file('sha256', $path),
);
if ($session->isDeduplicated()) {
return $session->file; // nothing to send
}
$handle = fopen($path, 'rb');
try {
// 2. Chunks — strictly in order, of size chunkSize.
// Each answer is a new session with the server's offset; read from it.
while (!$session->isComplete()) {
fseek($handle, $session->offset);
$chunk = fread($handle, min($session->chunkSize, $session->remaining()));
try {
$session = $uploads->append($session, $chunk);
} catch (ConflictException) {
// The server is at another offset (a chunk landed twice or got lost):
// ask where it stands and continue from there.
$session = $uploads->status($session);
continue;
}
$this->logger->info(sprintf('%s: %d%%', $session->name, $session->offset * 100 / $session->size));
}
// 3. Every byte is on the server — assemble the file.
return $uploads->complete($session);
} catch (\Throwable $e) {
// 4. Giving up: release the session and what was sent.
// (Or keep $session->id to continue later.)
try {
$uploads->abort($session);
} catch (S5wException) {
// already gone
}
throw $e;
} finally {
fclose($handle);
}
}Result on a 20 MiB file:
custom-20m.bin: 40%
custom-20m.bin: 80%
custom-20m.bin: 100%
→ File PfNZ2UNuy9sENtFn, hash matches
second run of the same file:
→ isDeduplicated(): nothing sentHow this loop is simpler than send()
There are no retries on a broken connection here: a TransportException goes straight to
abort(). And the hash is always sent, so this code must not be used with imageOptions — see
Upload pipeline. If you
need resilience, take send(); this example is for when the loop really has to be yours.
Chunk rules
The server accepts chunks by strict rules. The SDK does not check them up front — breaking one ends in a server answer and an exception.
| Rule | If broken |
|---|---|
| a chunk is 4 to 16 MiB; only the last may be smaller | 400 → ApiException (Chunk is too small) or 413 → PayloadTooLargeException (Chunk is too large) |
| an empty chunk | 400 → ApiException (Chunk is empty) |
| chunks strictly in order, no gaps | 409 → ConflictException |
the chunks add up to no more than the declared size |
413 → PayloadTooLargeException |
complete() only when offset === size |
409 → ConflictException, the session stays open |
The easiest is to send chunks of $session->chunkSize — the server picks that value.
Offset check
When append() gets a session, the SDK adds an Upload-Offset header with its offset. The server
compares it to its own and answers 409 on a mismatch instead of gluing the chunk in the wrong
place. So repeating a chunk that already landed cannot corrupt the file:
try {
$session = $uploads->append($session, $chunk);
} catch (ConflictException) {
$session = $uploads->status($session); // learn the real offset and continue from it
}If all you have is a session id (after a restart, say), pass the offset explicitly — or ask the server for it:
$session = $uploads->status($uploadId); // offset, size, chunkSize — from the server
$session = $uploads->append($session, $chunk); // the offset is filled in
$uploads->append($uploadId, $chunk, offset: 16777216); // or explicitlyWithout an offset (append($uploadId, $chunk)) the chunk is simply appended — unchecked.
Relaying data as it arrives
The most common reason to write the loop yourself is data that streams in and never sits on disk
whole. The size must be known up front: it is declared in start() and checked by the server.
/**
* @param iterable<string> $pieces data pieces of any size, in the right order
*/
public function relay(iterable $pieces, string $name, int $size): File
{
$uploads = $this->s5w->uploads();
$session = $uploads->start($name, $size, folder: 'incoming');
$buffer = '';
foreach ($pieces as $piece) {
$buffer .= $piece;
// collect up to a chunk — the server refuses less than 4 MiB unless it is the last one
while (strlen($buffer) >= $session->chunkSize) {
$session = $uploads->append($session, substr($buffer, 0, $session->chunkSize));
$buffer = substr($buffer, $session->chunkSize);
}
}
if ($buffer !== '') {
$session = $uploads->append($session, $buffer); // the last chunk — any size
}
return $uploads->complete($session);
}Cancelling
$uploads->abort($session); // or $uploads->abort($uploadId)The server removes the session and the bytes received so far. Cancelling is optional: an
abandoned session is removed by itself 24 hours after its last chunk. abort() just frees the
space now. Cancelling again, like cancelling a completed session, gives NotFoundException.
Deduplication by hand
Pass start() the hash of content the bucket already holds and there will be no session — the
server creates the file at once:
$session = $uploads->start($name, $size, sha256: hash_file('sha256', $path));
if ($session->isDeduplicated()) {
$file = $session->file; // done; the session id is empty, chunkSize = 0
}The hash is also checked on complete(): if the bytes sent do not match it, you get
ValidationException (Uploaded data does not match the declared checksum) and the session is
removed. Do not send a hash together with imageOptions — see
Upload pipeline.
See also
Uploads— every method and parameterUploadSession— the receipt’s fields- Chunked uploads on the server — the whole protocol