Package · sdk-s5w

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:

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

The whole loop

The same scenario send() runs, entirely in your code:

main/Services/BigService.php
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:

text
custom-20m.bin: 40%
custom-20m.bin: 80%
custom-20m.bin: 100%
→ File PfNZ2UNuy9sENtFn, hash matches

second run of the same file:
→ isDeduplicated(): nothing sent

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

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

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

Without 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.

php
/**
* @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

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

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