Clouds 26.100.0: presigned URL для прямой загрузки в S3

6 мин чтения

В модуле clouds появился API для presigned URL: сервер подписывает ссылку, и клиент кладёт части multipart-загрузки прямо в S3, минуя PHP. Релиз маленький, шесть файлов с правками в classes/general/. Сигнатуры не ломаются, схема БД не меняется. В самом clouds эти методы никто не вызывает. Потребитель — загрузчик ui 26.600.0 (стратегия presigned, выключена по умолчанию). Если вы грузите в облако большие файлы своим кодом, API можно пробовать и напрямую.

Что может сломаться

Публичные сигнатуры никто не трогал. Поведение поменялось в четырёх местах.

UpdateProgress() теперь может вернуть false

CCloudStorageUpload::UpdateProgress($arUploadInfo, $bSuccess) раньше игнорировал результат $connection->lock($lockId, -1) и писал в b_clouds_file_upload в любом случае. Теперь результат проверяется. Если блокировку взять не удалось, метод сбрасывает _cache и возвращает false. Тело переписали на try/finally, поэтому лок снимается на всех ветках.

Метод protected, его результат видят Next() и Part(). Если он вернул false, они бросают CLO_STORAGE_UPLOAD_ERROR и сами возвращают false. Таймаут -1 в MySQL и PostgreSQL означает бесконечное ожидание, так что там false почти не случается. На MariaDB 11.4+ с clouds 26.100.0 блокировка не берётся, и multipart-загрузка в облако не работает. Причину называет комментарий к исправлению в clouds 26.150.0: «Negative GET_LOCK timeouts are rejected since MariaDB 11.4». Тот же лок с тем же -1 берёт новый setParts(). В 26.150.0 таймаут заменили на PROGRESS_LOCK_TIMEOUT = 1000000, подробности в разборе clouds 26.150.0.

У блокировки прогресса новое имя

Было 'CCloudStorageUpload::UpdateProgress(' . $this->_ID . ')', стало 'CCloudStorageUpload::progress(' . $this->_ID . ')'. Имя вынесли в protected-метод getProgressLockName(), теперь его делят UpdateProgress() и новый setParts(). Заметно это только при обновлении «на горячую», пока параллельно идут загрузки: старый и новый код берут разные локи и друг друга не видят. Обновляйтесь, когда ничего не грузится.

ETag в CompleteMultipartUpload() экранируется

CCloudStorageService_S3::CompleteMultipartUpload() при сборке XML теперь оборачивает ETag в htmlspecialcharsbx(). Раньше значение подставлялось как есть. ETag в кавычках теперь уходит в S3 как "…". Проверьте завершение multipart-загрузки на своём хранилище, особенно если оно S3-совместимое и не от Amazon.

Редирект на ресайз-файл зависит от версии main

В CCloudStorage при редиректе 301 на ресайз-файл вызов Uri::urnEncode($to_file, 'UTF-8') заменили на Uri::urnEncode($to_file) без второго аргумента. Результат теперь зависит от дефолта $charset в модуле main. На старом main дефолт 'UTF-8', и clouds 26.100.0 работает как раньше. В main 26.700.0 дефолт поменялся (подробности в разборе main 26.700.0). На UTF-8-сайтах ничего не меняется. На не-UTF-8 после установки clouds 26.100.0 и main 26.700.0 путь в редиректе больше не перекодируется в UTF-8, проверьте ссылки на ресайзы с кириллицей.

Новое: части файла едут в S3 мимо PHP

Раньше каждая часть multipart-загрузки проходила через PHP. Теперь сервер может выдать клиенту подписанный URL на PUT конкретной части, и клиент отправляет её в S3 сам. PHP остаётся начать сессию, раздать ссылки и завершить загрузку.

Цепочка из трёх слоёв, каждый делегирует ниже:

  • CCloudStorageUpload::presignPart(int $partNumber, int $expires, $obBucket = null, ?int $contentLength = null): ?string выдаёт URL для части активной сессии (после Start()). Вернёт null, если сессия не начата, бакет не инициализировался или сервис presigned не умеет.
  • CCloudStorageBucket::supportsPresignedUrls(): bool и CCloudStorageBucket::getPresignedMultiPartUrl(array $uploadInfo, int $partNumber, int $expires, ?int $contentLength = null): ?string проксируют вызов к сервису бакета.
  • CCloudStorageService::supportsPresignedUrls(): bool и CCloudStorageService::PresignMultiPartUrl(array $arBucket, array $uploadInfo, int $partNumber, int $expires, ?int $contentLength = null): ?string в базовом классе возвращают false и null. S3-совместимые наследники должны их переопределять.

Настоящая реализация есть только в CCloudStorageService_S3. Там supportsPresignedUrls() отдаёт true, а PresignMultiPartUrl() подписывает PUT с query-параметрами partNumber и uploadId. Через наследование presigned получают CCloudStorageService_AmazonS3, CCloudStorageService_Yandex и CCloudStorageService_HotBox, а Google Storage и OpenStack-сервисы (Selectel Swift, Clodo, Rackspace) остаются на false (по коду в эталоне). В $uploadInfo обязательны ключи filePath и UploadId, без них вернётся null. Если передать $contentLength, заголовок Content-Length попадёт в SignedHeaders, и S3 отклонит часть другого размера, так что клиент не пришлёт 500 МБ вместо 5.

Подпись собирает protected-метод PresignUrl(). Это AWS Signature V4 в query string (X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Signature), payload равен UNSIGNED-PAYLOAD. Хост, PREFIX и CNAME берутся из GetFileSRC(), схема — https. Регион берётся из $arBucket['LOCATION'] с фолбэком на us-east-1. Если в настройках бакета есть SESSION_TOKEN, в ссылку добавляется X-Amz-Security-Token, так что временные STS-ключи тоже работают. Путь нормализует второй protected-метод, normalizePresignedCanonicalUri().

CCloudStorageService_Selectel_S3 наследуется от S3-сервиса, но supportsPresignedUrls() у него явно возвращает false. Для Selectel прямая загрузка отключена. Остальные сервисы в диффе не затронуты.

Второй новый публичный метод — CCloudStorageUpload::setParts(array $partsMap): bool. При прямой загрузке сервер не видит ETag'и частей, S3 отдаёт их клиенту. Перед Finish() клиент должен вернуть карту «номер части → ETag», и setParts() записывает её в сессию. Метод берёт блокировку, перечитывает NEXT_STEP из b_clouds_file_upload и заменяет Parts. Номера частей на входе считаются с единицы, внутри ключи хранятся с нуля ради совместимости с CompleteMultipartUpload.

Как этим пользоваться

Снаружи модуля clouds presignPart(), setParts() и getPresignedMultiPartUrl() никто не вызывает, внутри есть только цепочка делегирования (getPresignedMultiPartUrl() зовёт presignPart()). Готовый загрузчик есть в ui 26.600.0 (разбор ui 26.600.0). Стратегия presigned там выключена по умолчанию и включается ключом presignedChunkUpload в .settings.php → ui → uploader → settings. Свой код нужен, если вы грузите не через ui.uploader. Серверная обвязка примерно такая:

        <?php declare(strict_types=1);

namespace Vendor\Module\Application\Service;

use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;

final class DirectUploadService
{
    private const URL_TTL = 900;

    public function __construct()
    {
        Loader::requireModule('clouds');
    }

    /**
     * $upload — сессия, для которой уже вызван Start()
     * $contentLength — размер именно этой части; для последней части
     * передавайте её фактический размер, иначе S3 отклонит PUT
     */
    public function signPart(
        \CCloudStorageUpload $upload,
        \CCloudStorageBucket $bucket,
        int $partNumber,
        int $contentLength,
    ): Result {
        $result = new Result();

        if (!$bucket->Init() || !$bucket->supportsPresignedUrls()) {
            return $result->addError(new Error('Бакет не умеет presigned URL, грузим по старинке через PHP'));
        }

        $url = $upload->presignPart($partNumber, self::URL_TTL, $bucket, $contentLength);

        // пустая строка тоже означает ошибку, см. «Мелочи и находки»
        if ($url === null || $url === '') {
            return $result->addError(new Error('Не удалось подписать URL для части ' . $partNumber));
        }

        return $result->setData(['url' => $url]);
    }

    /**
     * @param array<int, string> $etags номер части (с 1) => ETag из ответа S3
     */
    public function commitParts(\CCloudStorageUpload $upload, array $etags): Result
    {
        $result = new Result();

        ksort($etags);

        if (!$upload->setParts($etags)) {
            $result->addError(new Error('Не удалось сохранить карту частей'));
        }

        // дальше обычный Finish()
        return $result;
    }
}

    

Контроллер сверху получается тонкий. Одно действие отдаёт клиенту ссылку на часть. Второе принимает собранные ETag'и и завершает загрузку. Клиент между ними делает PUT на подписанный URL и запоминает ETag из ответа.

$expires — время жизни ссылки в секундах, уходит в X-Amz-Expires. Ссылка на запись должна жить недолго, без нужды срок не раздувайте.

БД

Схема не менялась. setParts() пишет в уже существующее поле NEXT_STEP таблицы b_clouds_file_upload.

Мелочи и находки

  • PresignUrl() при пустых ACCESS_KEY/SECRET_KEY или неразбираемом URL возвращает ''. Обёртки при этом типизированы как ?string, а docblock про пустую строку молчит. Пустая строка спокойно проходит наружу через PresignMultiPartUrl() и presignPart(). Поэтому проверки === null мало, сверяйте и с пустой строкой, как в примере выше.
  • В storage_service_s3.php непривычно подробные для clouds англоязычные комментарии о том, где можно ошибиться с подписью. Ключи проходят через trim(), чтобы хвостовой перевод строки в сохранённом секрете не ломал HMAC. Схема принудительно https, потому что на HTTP S3 отвечает 301, а браузер превращает PUT в GET. USE_HTTPS = N уважается только как явный отказ, и в комментарии прямо назван сценарий «MinIO without TLS in dev».
  • CCloudStorageBucket::getPresignedMultiPartUrl() вызывает SetLocation(). Регион подписи PresignUrl() по собственному комментарию берёт из LOCATION бакета, а не из $this->location, но подписанный заголовок host у AmazonS3 и HotBox строится из location (по коду в эталоне), так что вызов нужен.
  • В setParts() и UpdateProgress() запрос по-прежнему собирается как "... WHERE ID = '" . $this->_ID . "'". Новый код повторяет старый стиль.
  • Новых @deprecated в релизе нет. В свежем паспорте API видны пометки на CCloudStorageService::GetObject() и классе CCloudSecurityService_AmazonS3, но они старые: в диффе эти места не тронуты.
  • VERSION_DATE — 2026-06-23, на эталон встало 2026-09-19.

Что делать

  • Обновляйте clouds в момент, когда нет активных загрузок в облако, потому что имя блокировки прогресса поменялось.
  • На MariaDB 11.4+ ставьте сразу clouds 26.150.0; при вызове Next()/Part() проверяйте false и $APPLICATION->GetException().
  • После обновления проверьте multipart-загрузку большого файла до конца. Экранирование ETag в CompleteMultipartUpload() стоит проверить на своём хранилище.
  • На не-UTF-8 сайтах после установки clouds 26.100.0 и main 26.700.0 проверьте ссылки на ресайз-картинки из облака с кириллицей в пути.
  • Если хотите прямую загрузку в S3, готовый загрузчик есть в ui 26.600.0. Свой код нужен, если вы грузите не через ui.uploader. Начинайте с supportsPresignedUrls(). Для Selectel, Google Storage и OpenStack держите фолбэк на загрузку через PHP.

Читайте дальше

clouds 26.150.0 Заметное Свежее

Clouds 26.150.0: таймаут -1 в блокировке загрузки заменили на миллион секунд ради MariaDB 11.4

В clouds 26.150.0 одна правка в `CCloudStorageUpload`, три строки кода с комментарием про MariaDB 11.4. На MariaDB 11.4 и новее блокировка прогресса multipart-загрузки в облако с таймаутом `-1` штатно не берётся, теперь таймаут положительный. API и схема БД не менялись.

2 мин
voximplant 26.700.0 Ломающее Свежее

Voximplant 26.700.0: оценку качества связи убрали, у notifyAdmins() появился тип

Модуль телефонии voximplant 26.700.0 вышел в коробочный Битрикс24 11 сентября 2026 года, по данным официального канала «Битрикс24 changelog». Из карточки звонка убрали оценку качества связи вместе с её JS-событиями, у `Im::notifyAdmins()` появился тип параметра, а публичные константы `SipStatusInfor...

3 мин
ui 26.687.0 Рутинное Свежее

UI 26.687.0: триал VibePlus в облаке и дизайн чипа TintedBitrixGpt

`Bitrix\UI\Controller\InfoHelper` при активации демо сначала спрашивает у модуля bitrix24, включён ли старт VibePlus, и если да, запускает триал VibePlus вместо обычного демо тарифа. Ветка срабатывает только при подключённом модуле bitrix24 и определённой константе `BX24_HOST_NAME`. В `ui.system.chi...

1 мин
ui 26.675.0 Ломающее Свежее

UI 26.675.0: ui.actionpanel переехал в бандл, а rich_text по-новому обходится с квадратными скобками

Модуль ui 26.675.0 собран 26 августа 2026. Из PHP поменялись `Converter` и `Whitelist` в `Bitrix\UI\Format\BBCode`, и пользовательские поля типа `rich_text` теперь иначе сохраняют и индексируют текст с квадратными скобками. На фронтенде старую панель групповых действий `ui.actionpanel` перевели на б...

4 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти