Пакет · sdk-s5w

Загрузка по шагам

uploads()->send() делает всю загрузку частями за вас. Но иногда цикл нужен свой: данные приходят кусками из другого источника, или решения о повторах и отмене вы хотите принимать сами. Для этого у uploads() есть пять методов — по одному на каждый запрос к серверу.

Пять шагов

Метод Запрос Что делает
start() POST /v1/uploads открывает сессию; проверяет папку и квоту до передачи байтов
append() PATCH /v1/uploads/{id} отправляет следующую часть
status() GET /v1/uploads/{id} спрашивает, сколько байт сервер уже держит
complete() POST /v1/uploads/{id}/complete превращает полученные байты в файл
abort() DELETE /v1/uploads/{id} отменяет сессию и удаляет полученное

Каждый метод, кроме complete() и abort(), возвращает UploadSession — «квитанцию» о состоянии загрузки:

php
$session->id;          // "6444c675-4884-4cbb-8324-c6f9a5bcd0e1"
$session->size;        // 100725660 — сколько всего нужно передать
$session->offset;      // 16777216  — сколько уже на сервере; отсюда начинается следующая часть
$session->chunkSize;   // 8388608   — каким размером сервер просит слать части
$session->expiresAt;   // когда сессию удалят без новых частей

$session->isComplete();      // offset === size
$session->remaining();       // size - offset
$session->isDeduplicated();  // содержимое уже было в бакете, файл готов

Полный цикл

Тот же сценарий, что делает send(), но целиком в вашем коде:

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. Открыть сессию. Всё, что сервер может отклонить, он отклонит здесь:
  //    нет папки, не хватает квоты. С хешем знакомое содержимое сразу становится файлом.
  $session = $uploads->start(
      name: basename($path),
      size: filesize($path),
      folder: 'backups',
      sha256: hash_file('sha256', $path),
  );

  if ($session->isDeduplicated()) {
      return $session->file;   // передавать нечего
  }

  $handle = fopen($path, 'rb');
  try {
      // 2. Части — строго по порядку, размером chunkSize.
      //    Каждый ответ — новая сессия со смещением сервера; читаем с него.
      while (!$session->isComplete()) {
          fseek($handle, $session->offset);
          $chunk = fread($handle, min($session->chunkSize, $session->remaining()));

          try {
              $session = $uploads->append($session, $chunk);
          } catch (ConflictException) {
              // Сервер на другом смещении (часть дошла дважды или потерялась):
              // спросить, где он, и продолжить оттуда.
              $session = $uploads->status($session);
              continue;
          }

          $this->logger->info(sprintf('%s: %d%%', $session->name, $session->offset * 100 / $session->size));
      }

      // 3. Все байты на сервере — собрать файл.
      return $uploads->complete($session);
  } catch (\Throwable $e) {
      // 4. Сдаёмся: освободить сессию и уже переданное.
      //    (Или сохранить $session->id, чтобы продолжить позже.)
      try {
          $uploads->abort($session);
      } catch (S5wException) {
          // уже нет
      }
      throw $e;
  } finally {
      fclose($handle);
  }
}

Результат на файле 20 МиБ:

text
custom-20m.bin: 40%
custom-20m.bin: 80%
custom-20m.bin: 100%
→ File PfNZ2UNuy9sENtFn, хеш совпал

второй запуск того же файла:
→ isDeduplicated(): ничего не передано

Чем этот цикл проще send()

Здесь нет повторов при обрыве связи: TransportException сразу ведёт к abort(). И хеш отправляется всегда, поэтому с imageOptions такой код использовать нельзя — см. Конвейер загрузки. Если нужна устойчивость к сбоям, берите send(); этот пример — когда цикл действительно должен быть вашим.

Правила частей

Сервер принимает части по жёстким правилам. SDK их не проверяет заранее — нарушение закончится ответом сервера и исключением.

Правило Если нарушить
размер части — от 4 до 16 МиБ; меньше можно только последней 400 → ApiException (Chunk is too small) или 413 → PayloadTooLargeException (Chunk is too large)
пустая часть 400 → ApiException (Chunk is empty)
части строго по порядку, без пропусков 409 → ConflictException
сумма частей не больше заявленного size 413 → PayloadTooLargeException
complete() только когда offset === size 409 → ConflictException, сессия остаётся открытой

Удобнее всего слать части размером $session->chunkSize — это значение подбирает сервер.

Проверка смещения

Когда в append() передаётся сессия, SDK добавляет к запросу заголовок Upload-Offset со смещением из неё. Сервер сверяет его со своим и, если они разошлись, отвечает 409, а не приклеивает часть не туда. Так повтор уже дошедшей части не испортит файл:

php
try {
  $session = $uploads->append($session, $chunk);
} catch (ConflictException) {
  $session = $uploads->status($session);   // узнать настоящее смещение и продолжить с него
}

Если у вас только id сессии (например, после перезапуска), передайте смещение явно — или спросите его у сервера:

php
$session = $uploads->status($uploadId);                 // offset, size, chunkSize — от сервера
$session = $uploads->append($session, $chunk);          // смещение подставится само

$uploads->append($uploadId, $chunk, offset: 16777216);  // или явно

Без смещения (append($uploadId, $chunk)) часть просто дописывается в конец — без проверки.

Пересылка данных по мере поступления

Самый частый повод писать цикл самому — данные идут потоком и целиком на диске не лежат. Размер должен быть известен заранее: он заявляется в start() и проверяется сервером.

php
/**
* @param iterable<string> $pieces куски данных любого размера, в правильном порядке
*/
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;

      // копим до размера части — сервер не примет кусок меньше 4 МиБ, если он не последний
      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);   // последняя часть — любого размера
  }

  return $uploads->complete($session);
}

Отмена

php
$uploads->abort($session);   // или $uploads->abort($uploadId)

Сервер удаляет сессию и уже полученные байты. Отменять необязательно: брошенная сессия сама удалится через 24 часа после последней части. abort() просто освобождает место сразу. Повторная отмена, как и отмена завершённой сессии, даёт NotFoundException.

Дедупликация вручную

Если передать в start() хеш содержимого, которое в бакете уже есть, сессии не будет — сервер сразу создаст файл:

php
$session = $uploads->start($name, $size, sha256: hash_file('sha256', $path));

if ($session->isDeduplicated()) {
  $file = $session->file;   // готово; id сессии пустой, chunkSize = 0
}

Хеш также проверяется при complete(): если переданные байты с ним не совпали, будет ValidationException (Uploaded data does not match the declared checksum), и сессия удаляется. С imageOptions хеш передавать не стоит — см. Конвейер загрузки.

См. также