Пакет · sdk-s5w

Загрузка файлов

Для загрузки есть два метода: files()->upload() берёт файл с диска, files()->uploadContent() — байты из памяти. Оба возвращают готовый File и сами выбирают способ передачи по размеру.

Файл с диска

php
$file = $s5w->files()->upload('/var/data/report.pdf');

$file->id;              // "FXOkqegbdrNRWQgc"
$file->name;            // "report.pdf"
$file->folder;          // null — файл в корне бакета
$file->content->size;   // 248113

Вместо пути можно передать SplFileInfo — например, из DirectoryIterator:

php
foreach (new DirectoryIterator('/var/data/inbox') as $entry) {
  if ($entry->isFile()) {
      $s5w->files()->upload($entry, folder: 'inbox');
  }
}

Если файла нет или его нельзя прочитать, SDK бросит S5wException ещё до запроса.

В папку

Параметр folder: принимает имя папки или объект Folder:

php
$s5w->files()->upload($path, folder: 'invoices');

$avatars = $s5w->folders()->ensure('avatars', public: true);
$s5w->files()->upload($path, folder: $avatars);

Без folder: файл ложится в корень бакета. Папка должна существовать: если её нет, будет ValidationException с сообщением Folder "…" does not exist. Создать её заранее удобно через folders()->ensure() — см. Работа с папками и файлами.

Публичность определяет папка

У файла нет собственного флага «публичный» — он берётся из папки. Файл в публичной папке доступен по publicUrl без ключа, в приватной — только с ключом. Файл в корне бакета публичный. Если файл не должен быть доступен всем, кладите его в приватную папку.

Под своим именем

По умолчанию файл сохраняется под именем исходного файла. Параметр name: задаёт другое:

php
$s5w->files()->upload('/tmp/upload-7f3a.bin', folder: 'docs', name: 'Договор №42.pdf');

Сервер следит за расширением: оно определяется по содержимому, и если в имени указано другое известное расширение, сервер заменит его. Файл photo.png, внутри которого JPEG, сохранится как photo.jpg.

Файл из запроса пользователя

Файл, присланный в форме, PHP кладёт во временный файл со случайным именем. Если передать только путь к нему, в хранилище попадёт это случайное имя. Передавайте исходное имя через name::

main/Controllers/AvatarController.php
use Flytachi\Winter\Kernel\Http\Request\Annotation\RequestFile;
use Flytachi\Winter\Kernel\Http\Response\ResponseEntity;
use Flytachi\Winter\Kernel\Route\Annotation\PostMapping;

#[PostMapping('avatar')]
public function avatar(
  #[RequestFile('file', maxSize: '10MB', accept: ['image/*'])] array $file,
): ResponseEntity {
  $stored = $this->s5w->files()->upload(
      $file['tmp_name'],
      folder: 'avatars',
      name: $file['name'],
  );

  return ResponseEntity::ok($stored);
}

Если забыть name

Без name: файл сохранится как swoole.upfile.Ab3x.jpg — сервер добавит правильное расширение, но имя останется временным.

ResponseEntity::ok($stored) отдаст файл в JSON: все сущности SDK реализуют JsonSerializable, даты в них — строки ISO 8601.

Из памяти

Когда файла на диске нет — PDF, который только что собрало приложение, картинка из base64, экспорт в CSV, — используйте uploadContent(). Имя здесь обязательно: по нему сервер выберет расширение для текстовых форматов.

php
// сгенерированный документ
$pdf = $renderer->render($invoice);
$s5w->files()->uploadContent($pdf, "invoice-{$invoice->id}.pdf", folder: 'invoices');

// картинка из data-URL
[, $base64] = explode(',', $dataUrl, 2);
$s5w->files()->uploadContent(base64_decode($base64), 'signature.png', folder: 'signatures');

Картинки: уменьшить и перекодировать

Сервер умеет при загрузке уменьшить картинку, повернуть её по EXIF и перекодировать. Опции передаются объектом ImageOptions:

php
use Flytachi\Winter\SDK\S5w\Option\ImageOptions;

$file = $s5w->files()->upload(
  $file['tmp_name'],
  folder: 'avatars',
  name: $file['name'],
  imageOptions: ImageOptions::webp(80)->fit(512, 512),
);

Частые варианты:

Задача Опции
WebP, качество по умолчанию ImageOptions::webp()
WebP, вписать в 1600×1600 ImageOptions::webp()->fit(1600, 1600)
только уменьшить по ширине, формат оставить ImageOptions::original()->fit(800)
AVIF с качеством 50 ImageOptions::avif(50)
JPEG, вписать в 1920 по ширине ImageOptions::jpeg(85)->fit(1920)

fit() только уменьшает и сохраняет пропорции: картинка 320×200 в рамке 160×160 станет 160×100. Оригинал не хранится — в бакет попадает результат. Имя следует за форматом: photo.png, перекодированный в WebP, сохранится как photo.webp. Все параметры и пределы — в справочнике ImageOptions.

Что сделал сервер, видно в поле processed ответа:

php
$p = $file->processed;

$p->applied;           // true
$p->operations;        // ["resize:400x267", "encode:webp@80"]
$p->source->width;     // 1200
$p->result->width;     // 400
$p->saved();           // 4251 — байт сэкономлено

Если сервер решил ничего не делать, applied будет false, а reason объяснит почему: nothing to do (картинка уже меньше рамки и в нужном формате), animated (анимации не трогаются), output is larger than source (результат вышел тяжелее, сохранён оригинал).

Опции только для картинок

Опции обработки, переданные с файлом, который не является картинкой, сервер отклоняет: ValidationException с сообщением Image options were given, but this file is not an image. Если вы загружаете файлы разных типов одним кодом, передавайте imageOptions только для картинок.

Поле processed есть только в ответе на загрузку. Когда вы позже читаете файл через get() или list(), оно равно null — сервер не хранит отчёт об обработке.

Дубликаты имён и содержимого

Одинаковые имена. Имя уникально внутри папки. Если файл с таким именем уже есть, сервер не перезапишет его, а сохранит новый под именем с номером:

php
$s5w->files()->upload('/tmp/notes.txt', folder: 'docs')->name;   // "notes.txt"
$s5w->files()->upload('/tmp/notes.txt', folder: 'docs')->name;   // "notes (1).txt"

Одинаковое содержимое. Сервер хранит одинаковые байты один раз на бакет (по SHA-256). Второй файл с тем же содержимым — это отдельный файл со своим id и именем, но место в бакете он не занимает. Об этом говорит флаг deduplicated:

php
$copy = $s5w->files()->upload('/tmp/notes.txt', folder: 'docs');
$copy->deduplicated;   // true — байты уже были в бакете, диск не потрачен

Удаление одного из таких файлов не затрагивает другие.

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

Ничего делать не нужно: upload() и uploadContent() сами переходят на загрузку частями, когда файл больше порога (по умолчанию 16 МиБ).

Размер Как уходит
до 16 МиБ включительно одним запросом POST /v1/files
больше 16 МиБ частями через /v1/uploads: с повтором оборванных частей и проверкой, нет ли такого содержимого в бакете

Порог меняется параметром chunkThreshold в S5wConfig (не больше ~31,9 МиБ — это предел одного запроса на сервере). Для прогресса и продолжения после сбоя вызывайте uploads()->send() напрямую — см. Большие файлы.

Если места нет

Когда квота бакета заполнена, загрузка получает PayloadTooLargeException (HTTP 413). Проверить свободное место заранее можно через check():

php
$storage = $s5w->check()->storage;

if (!$storage->fits(filesize($path))) {
  throw new RuntimeException('В бакете не хватает места');
}

Проверка заранее не гарантирует результат: между check() и загрузкой место может занять кто-то ещё, поэтому PayloadTooLargeException всё равно стоит обрабатывать.

См. также