Загрузка по шагам
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 — «квитанцию» о
состоянии загрузки:
$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(), но целиком в вашем коде:
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 МиБ:
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, а не
приклеивает часть не туда. Так повтор уже дошедшей части не испортит файл:
try {
$session = $uploads->append($session, $chunk);
} catch (ConflictException) {
$session = $uploads->status($session); // узнать настоящее смещение и продолжить с него
}Если у вас только id сессии (например, после перезапуска), передайте смещение явно — или спросите его у сервера:
$session = $uploads->status($uploadId); // offset, size, chunkSize — от сервера
$session = $uploads->append($session, $chunk); // смещение подставится само
$uploads->append($uploadId, $chunk, offset: 16777216); // или явноБез смещения (append($uploadId, $chunk)) часть просто дописывается в конец — без проверки.
Пересылка данных по мере поступления
Самый частый повод писать цикл самому — данные идут потоком и целиком на диске не лежат.
Размер должен быть известен заранее: он заявляется в start() и проверяется сервером.
/**
* @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);
}Отмена
$uploads->abort($session); // или $uploads->abort($uploadId)Сервер удаляет сессию и уже полученные байты. Отменять необязательно: брошенная сессия сама
удалится через 24 часа после последней части. abort() просто освобождает место сразу.
Повторная отмена, как и отмена завершённой сессии, даёт NotFoundException.
Дедупликация вручную
Если передать в start() хеш содержимого, которое в бакете уже есть, сессии не будет — сервер
сразу создаст файл:
$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 хеш передавать не стоит — см.
Конвейер загрузки.
См. также
Uploads— все методы и параметрыUploadSession— поля квитанции- Загрузка частями на сервере — протокол целиком