Пакет · sdk-s5w

Большие файлы

Видео, архивы, дампы баз весят сотни мегабайт и больше. files()->upload() справится с ними и сам, а uploads()->send() даёт то, что нужно для долгой загрузки: прогресс, id сессии и возможность продолжить с места обрыва — даже после перезапуска процесса.

Загрузить с прогрессом

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() режет файл на части того размера, который просит сервер (сейчас 8 МиБ), отправляет их по порядку и собирает файл в конце. Возвращает тот же File, что и обычная загрузка.

onProgress получает два числа: $sent — сколько байт уже на сервере, $total — размер файла. Он вызывается:

Когда $sent
в начале, до первой части 0
после каждой части, которую подтвердил сервер новое смещение
после восстановления, если часть дошла, а ответ потерялся тоже вызывается — прогресс не теряется
при resume() — первым вызовом место, с которого продолжаем
при дедупликации (передавать ничего не пришлось) один вызов сразу с $sent === $total

$total всегда больше нуля — пустую загрузку сервер не принимает, — так что деление безопасно.

Сохранить id сессии

Если процесс может прерваться посреди загрузки — задача в очереди, воркер, который перезапускают при деплое, — сохраните id сессии сразу после её открытия. Для этого есть onStart:

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 вызывается один раз, до передачи первого байта. Если содержимое оказалось в бакете и передавать ничего не нужно (дедупликация), сессии нет и onStart не вызывается.

Продолжить после сбоя

Сбой связи посреди загрузки

Обрыв одной части send() переживает сам: ждёт, спрашивает сервер, сколько тот получил, и продолжает с этого места. Если связь так и не вернулась — по умолчанию три попытки на часть с паузами 1 и 2 секунды, — send() бросает UploadInterruptedException. Сессия при этом остаётся на сервере, и её можно продолжить:

php
use Flytachi\Winter\SDK\S5w\Exception\UploadInterruptedException;

try {
  $file = $s5w->uploads()->send($path, folder: 'videos');
} catch (UploadInterruptedException $e) {
  $logger->warning("Загрузка прервана на {$e->offset()} из {$e->size()} байт");

  // позже — когда сеть вернётся
  $file = $s5w->uploads()->resume($e->sessionId(), $path);
}

Перезапуск процесса

Если процесс упал или его перезапустили, исключения уже не будет — но будет id сессии, который вы сохранили в onStart. По нему и продолжайте:

php
$uploadId = $job->uploadId();

if ($uploadId !== null) {
  $file = $s5w->uploads()->resume(
      $uploadId,
      $path,
      onProgress: fn(int $sent, int $total) => $job->saveProgress($sent, $total),
  );
}

resume() спрашивает сервер, сколько байт он уже держит, проверяет, что файл того же размера, что был заявлен при старте, и отправляет остаток. Первый вызов onProgress покажет, с какого места продолжили, — например 45%.

Что может пойти не так при resume() Что будет
сессии уже нет: прошло больше суток без новых частей, её отменили или уже завершили NotFoundException — начните загрузку заново
передан другой файл (размер не совпадает) S5wException с текстом … not the same file

Сессия живёт 24 часа

Незавершённая сессия удаляется сервером через 24 часа после последней полученной части. Пока сессия не завершена, место в квоте бакета она не занимает.

Дедупликация: не передавать то, что уже есть

Перед открытием сессии send() считает SHA-256 файла и сообщает его серверу. Если такое содержимое в бакете уже есть, сервер сразу создаёт файл, и передавать ничего не нужно:

php
$first  = $s5w->uploads()->send('/backups/big.sql', folder: 'backups');   // передаёт 100 МБ
$second = $s5w->uploads()->send('/backups/big.sql', folder: 'backups');   // ~0,3 с, ничего не передаёт

$second->deduplicated;   // true
$second->name;           // "big (1).sql"

Хеш считается одним проходом по файлу на диске — для гигабайтного файла это около секунды, и это окупается, если файл уже загружался. Если вы знаете, что содержимое новое, отключите проверку:

php
$s5w->uploads()->send($path, deduplicate: false);

С опциями обработки картинок (imageOptions) хеш не отправляется никогда: в бакете хранится обработанный результат, и хеш оригинала с ним не совпадёт. Подробнее — в разделе Конвейер загрузки.

Из памяти

Для больших данных, которые уже лежат в памяти, есть sendContent() — те же параметры, что у send(), только вместо пути байты и обязательное имя:

php
$file = $s5w->uploads()->sendContent($archiveBytes, 'export.zip', folder: 'exports');

resume() работает только с файлом на диске — данные из памяти после перезапуска процесса всё равно потеряны.

Таймауты

Каждая часть отправляется отдельным запросом с таймаутом transferTimeout (по умолчанию 300 секунд на часть). Для обычного канала этого с запасом: часть в 8 МиБ на скорости 10 МБ/с уходит меньше чем за секунду. Если канал очень медленный, увеличьте значение:

php
$config = (new S5wConfig($url, $key))->withTransferTimeout(900);

Число попыток на часть (3) не настраивается — это константа Uploads::ATTEMPTS.

Что выбрать

Нужно Вызов
просто загрузить, неважно какого размера files()->upload()
показать прогресс uploads()->send(…, onProgress: …)
продолжить после перезапуска uploads()->send(…, onStart: …) + uploads()->resume()
свой цикл, файл приходит кусками Загрузка по шагам

См. также