Disk 26.1175.0: runPreviewGeneration отдаёт другой pull-тег, а TIFF, WAV и MKV спрятаны за фича-флагом
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Диск обновился с 26.1150.0 (разбор) до 26.1175.0 и пришёл в коробку вместе с main 26.800.0, который мы разобрали отдельно. Вендор пишет про два исправления общих дисков, но больше всего кода ушло на предпросмотр TIFF и поддержку TIFF, WAV и MKV во встроенном просмотрщике. Новые форматы включает фича-флаг FileViewerFormatsFlag, и по умолчанию он выключен.
Проверить свой код стоит тем, кто сам вызывает экшен runPreviewGeneration, наследует компонент disk.external.link или показывает пользователям MKV. Экшен теперь передаёт файл в transformer при каждом вызове, даже при выключенной опции disk_transform_files_on_open, а MKV без флага после обновления не конвертируется и не открывается в просмотрщике.
Что сломается
Перепишите ожидание после runPreviewGeneration
Controller\File::runPreviewGenerationAction() и Controller\AttachedObject::runPreviewGenerationAction() раньше возвращали результат View\Base::transformOnOpen(). Теперь экшены сами вызывают TransformerManager::transformToView($file) и подписывают текущего пользователя на pull-тег. В ответе previewGeneration осталось два статуса, success и not allowed, а no module и was transformed экшен больше не возвращает. data.pullTag при success приходил и раньше, но тег в нём теперь дисковый, DISKTRANSFORMATION<ID файла Диска>, а был mainTransform<ID из b_file>.
Вместе с transformOnOpen() с этого пути ушла isTransformationAllowedOnOpen(). Она не пускала на конвертацию файл, у которого уже есть view, файл больше лимита из настроек просмотрщика (для видео по умолчанию 300 МБ) и любой файл, если выключена опция disk_transform_files_on_open, а по умолчанию она выключена. Ушли и TransformerManager::checkTransformationAttempts() (отправлялся ли файл на конвертацию раньше), и сброс кеша BlogPostConnector::clearCacheByObjectId(). Теперь экшен передаёт файл в transformer при каждом вызове, независимо от опции. Если конвертация этого файла ещё идёт, transformer присоединит вызов к текущей команде. Из прежних ограничений остались только проверки самого transformer: файл больше его MAX_FILESIZE (для видео 3 ГБ) и пять неудачных конвертаций этого файла. Если ваш код полагался на опцию или лимит из настроек, проверяйте их сами.
Событие о готовности по-прежнему шлёт main, это команда transformationComplete на тег mainTransform<ID из b_file>. В новый дисковый тег Диск ничего не пишет, и фронтенд, который ждал событие по data.pullTag, его больше не получит. Штатный disk.file.transform.video подписывает пользователя на тег main ещё при рендере и сам опрашивает готовность, не дольше 90 секунд. В своём коде подписку на тег main делайте на сервере ((new \Bitrix\Main\UI\Viewer\Transformation\TransformerManager())->subscribeCurrentUserForTransformation($bFileId)), а на клиенте ждите событие main:
async function runPreviewGeneration(fileId, bFileId)
{
const response = await BX.ajax.runAction('disk.file.runPreviewGeneration', {
data: { fileId },
});
if (response.data.previewGeneration.status !== 'success')
{
// остался только 'not allowed'
showDownloadLink();
return;
}
// data.pullTag теперь DISKTRANSFORMATION<id>, событий на него нет.
// Готовность шлёт main на тег mainTransform<bFileId>, подписку на него делает сервер.
BX.addCustomEvent('onPullEvent-main', (command, params) => {
if (command === 'transformationComplete' && Number(params.fileId) === bFileId)
{
showPreview();
}
});
showPreparingState();
}
Для вложений то же самое делает disk.attachedObject.runPreviewGeneration с параметром attachedObjectId.
Уберите переопределение setDocumentSessionSource()
Из CDiskExternalLinkComponent (install/components/bitrix/disk.external.link/class.php) удалён protected-метод setDocumentSessionSource(). Его место занял private setDocumentSource(SessionManager, File, ?Version), который всегда вызывает и setFile(), и setVersion(). Версию компонент теперь берёт из $this->externalLink->getVersion(), а сверка $version->getObjectId() !== $file->getRealObjectId() из старого метода пропала.
Если вы наследовали компонент и переопределяли этот метод, ваш метод останется в классе, но компонент его больше не вызовет, и логика в нём молча перестанет работать. Вызов parent::setDocumentSessionSource() упадёт с Error. Новый метод private, переопределить его не получится. Найти такие места можно так:
grep -rn "setDocumentSessionSource" local/ bitrix/components/ bitrix/templates/ --include=*.php
Проверьте, как открываются MKV после обновления
Для .mkv при выключенном FileViewerFormatsFlag TransformerManager::transformToView() возвращает false, и конвертации нет. View\Video::render() отдаёт для такого файла пустую строку, а isHtmlAvailable() и переопределённый isTransformationAllowed() возвращают false. До 26.1175.0 особых условий для mkv в этих методах не было. Флаг объявлен с enabledByDefault(): false, поэтому MKV перестанет конвертироваться и открываться в просмотрщике сразу после обновления.
Остальные новые форматы без флага тоже не откроются. FileAttributes::refineType() отдаёт для .tif, .tiff, .wav и .mkv заглушку Renderer\Stub. С включённым флагом TIFF уходит в новый рендерер Tiff, WAV в Renderer\Audio.
Сверьте свой код с новым DiskProvider
QuickAccess\FileInfo\DiskProvider теперь работает с File|Version. Для AttachedObject с фиксированной версией (isSpecificVersion()) провайдер берёт саму Version. Раньше он брал getVersion()?->getObject(), то есть файл целиком. getSourceId() для версии возвращает DiskVersion:<id>, а конструктор на неподдерживаемом источнике бросает RuntimeException. Если вы наследуете провайдер или разбираете source ID, учтите новый префикс и исключение.
Новое
Включите FileViewerFormatsFlag на стенде
Bitrix\Disk\Config\Feature\FileViewerFormatsFlag сделан на фича-флагах ядра (Bitrix\Main\Config\Feature\AbstractFlag, механизм разобран в main 26.700.0). Состояние флага возвращает новый Configuration::isEnabledFileViewerFormats().
Флаг один на все четыре расширения, включить TIFF отдельно от MKV нельзя.
Превью TIFF собирает Imagick, поэтому перед включением проверьте расширение:
<?php declare(strict_types=1);
use Bitrix\Disk\Config\Feature\FileViewerFormatsFlag;
use Bitrix\Disk\Configuration;
use Bitrix\Main\Config\Feature;
use Bitrix\Main\Loader;
Loader::requireModule('disk');
$tiffReady = extension_loaded('imagick') && \Imagick::queryFormats('TIFF*') !== [];
if (!$tiffReady) {
exit("Imagick без поддержки TIFF: превью TIFF не соберутся\n");
}
if (!Configuration::isEnabledFileViewerFormats()) {
Feature::enable(FileViewerFormatsFlag::class);
}
echo Configuration::isEnabledFileViewerFormats() ? "флаг включён\n" : "флаг не включился\n";
Запускайте его после подключения пролога, например из своей консольной команды на bitrix.php. Без пролога Loader недоступен.
WAV заработает только вместе с main 26.800.0. В нём Bitrix\Main\UI\Viewer\Renderer\Audio научился принимать audio/wav и audio/x-wav, а Диск направляет .wav именно в этот рендерер.
Где брать превью TIFF
Internal\Service\TiffPreviewService (1099 строк) готовит PNG первой страницы TIFF. Точки входа getByFile(), getByVersion(), getByAttachedObject() и getByExternalLink() возвращают Result со статусом из TiffPreview\PreviewStatus (ready, preparing, error) и кодом ошибки из TiffPreview\ErrorCode. Снаружи превью доступно через экшены showTiffPreview в Controller\File, Controller\Version и Controller\AttachedObject с параметром ?string $previewToken = null. Фильтры скопированы с showMarkdown: Authentication(true) и CloseSession без Csrf, плюс HttpMethod только на GET. Без токена экшен отдаёт JSON со статусом, с валидным previewToken отдаёт сам PNG через BFile. Адреса строит UrlManager: getUrlForShowTiffPreview(), getUrlForShowTiffPreviewByFileId(), getUrlForShowTiffPreviewVersion() и getUrlForShowTiffPreviewAttached().
В просмотрщике TIFF рисует рендерер UI\Viewer\Renderer\Tiff (JS-тип tiff, image/tiff) через расширение disk.viewer.tiff-item с классом BX.Disk.Viewer.TiffItem. Пока сервер отвечает preparing, класс переспрашивает его каждые 4 секунды, но не больше 15 раз, это около минуты ожидания. Если выводите атрибуты файла сами, адрес превью в data-tiff-preview-url можно подменить через FileAttributes::setTiffPreviewUrlOverride() или дополнить параметрами через setTiffPreviewUrlParams().
Ожидание видео ограничено 90 секундами
View\Video переопределил isTransformationAllowed($size = 0) (для MKV без флага он возвращает false) и получил константы TRANSFORMATION_RETRY_INTERVAL = 5 и TRANSFORMATION_WAIT_TIMEOUT = 90. Компонент disk.file.transform.video принимает параметры RETRY_INTERVAL и WAIT_TIMEOUT и больше 90 секунд не ждёт, опрос готовности в script.js переписан. В JS есть отладочное событие mkv_wait_expired, по нему видно, что ожидание переделывали под MKV.
В TransformerManager метод getTransformerByFormat() стал protected, а transformToView() вызывает его через static::. Выбор трансформера по формату теперь можно переопределить в наследнике. Сам Диск (контроллеры и модель AttachedObject) вызывает TransformerManager по имени базового класса, поэтому наследник сработает только там, где вы вызываете его сами.
Мастер общих дисков убирает за собой хранилище
Исправление, о котором пишет вендор, сделано в шаблоне страницы disk_common (install/public/templates/.default/page_templates/disk_common/template.php). Путь из мастера сначала проходит через CommonStoragePathNormalizer::normalizeCommonStorageMountPoint($rawPath, $siteDir, $allowLegacyDuplicateCollapse). Нормализатор убирает query и fragment, меняет \ на /, склеивает повторные слеши и отрезает хвостовой index.php. С третьим параметром он ещё схлопывает повтор последнего сегмента под docs.
Хранилище создаёт новый Driver::addCommonStorageWithCreationStatus(), который возвращает ['storage' => Storage, 'created' => bool]. Если changeBaseUrl() не удался, шаблон удаляет хранилище от имени SystemUser, но только то, которое создал сам. Если потом не сохранится сам раздел, хранилище откатит main. В bitrix/modules/main/public/file_new.php из main 26.800.0 появилась функция BXRollbackDiskCommonStorageCreation(), она через CommonStorageCreationContext::consume() удаляет хранилище, созданное в этом запросе.
При этом file_new.php импортирует Bitrix\Disk\Integration\Fileman\* напрямую, а эти классы впервые появились в disk 26.1175.0. Вызов защищён только Loader::includeModule('disk'). Судя по коду, main 26.800.0 с Диском версии ниже 26.1175.0 упал бы при создании раздела с шаблоном disk_common. При штатном обновлении такой пары не получится, потому что в метаданных обновления main 26.800.0 требует disk 26.1175.0, а disk 26.1175.0 требует main 26.800.0 и ui 26.650.0.
Флаг created пригодится и в своих установщиках. Уже существующее хранилище метод возвращает с created = false. Даже если его успел создать параллельный запрос, внутренний addStorageIfNotExistWithCreationStatus() поймает DuplicateEntryException и перечитает запись, так что откат чужое хранилище не заденет:
<?php declare(strict_types=1);
namespace Vendor\Docs\Application\Service;
use Bitrix\Disk\Driver;
use Bitrix\Disk\Integration\Fileman\CommonStoragePathNormalizer;
use Bitrix\Disk\Storage;
use Bitrix\Disk\SystemUser;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
final class CommonStorageInstaller
{
public function install(string $entityId, string $name, string $siteId, string $rawPath): Result
{
$result = new Result();
$mountPoint = CommonStoragePathNormalizer::normalizeCommonStorageMountPoint($rawPath);
$creation = Driver::getInstance()->addCommonStorageWithCreationStatus([
'NAME' => $name,
'ENTITY_ID' => $entityId,
'SITE_ID' => $siteId,
], []);
$storage = $creation['storage'] ?? null;
if (!$storage instanceof Storage) {
return $result->addError(new Error('Хранилище не создано', 'STORAGE_NOT_CREATED'));
}
if (!$storage->changeBaseUrl($mountPoint)) {
if ($creation['created']) {
$storage->delete(SystemUser::SYSTEM_USER_ID);
}
return $result->addError(new Error("Не удалось привязать хранилище к {$mountPoint}", 'BASE_URL_FAILED'));
}
return $result->setData([
'storageId' => (int)$storage->getId(),
'created' => $creation['created'],
]);
}
}
Второе исправление, для вложенных папок /docs/название/, пока не подключено. Появилась служебная точка входа bitrix/modules/disk/services/docs_section.php с копиями в bitrix/services/disk/ и install/services/disk/. По докблоку она обслуживает терминальный маршрут /docs/{any}, когда подпуть физически не существует, и поднимается по дереву до ближайшего index.php внутри /docs. Но в этой версии на файл не ссылается ни один маршрут. В bitrix/routes/intranet.php docs/{any} по-прежнему ведёт на docs/index.php, а в urlrewrite.php правила для него нет.
БД
Новых таблиц и полей нет. Превью TIFF хранится в существующей b_file_preview через PreviewManager::setPreviewImageId() и FilePreviewTable. Сервис обновляет TOUCHED_AT не чаще раза в сутки и обнуляет PREVIEW_IMAGE_ID, если файл превью удалён. Ошибки конвертации кешируются на час в каталоге /disk/tiff_preview/errors. Ключ кеша собирается из метаданных b_file, поэтому после замены файла ключ меняется, и старая ошибка из кеша не вернётся (TiffPreviewService::buildConversionErrorCacheId()).
Мелочи и находки
- Первая конвертация TIFF идёт синхронно, прямо в веб-запросе, без модуля transformer. Комментарий в коде объясняет это рамками «ADR/SDD». Для неблокирующей схемы нужна отдельная постоянная очередь, а CAgent как очередь на каждый файл «не выбран». Перед Imagick сервис сверяет magic bytes TIFF и вызывает
pingImage()доreadImage(). Картинка ограничена 50 Мпикс и ужимается до 4096 px по большей стороне, исходник принимается до 100 МБ, на конвертацию даётся 15 секунд. Imagick получает лимиты на память 128 МБ, map 256 МБ, диск 512 МБ и один поток, после конвертации прежние значения возвращаются. - Одновременно конвертируют не больше двух процессов (
CONVERSION_SLOT_COUNT = 2). Семафор собран на именованных блокировках БД.acquireConversionSlot()перебирает$connection->lock('disk_tiff_preview_slot_N', 0), и ещё одна блокировка ставится на конкретный файл. Кому слот не достался, получаетpreparingи приходит позже. Такой семафор можно перенести к себе, очереди и Redis ему не нужны. - Готовый PNG отдаётся по подписанному токену.
Signerс сольюdisk.tiff.previewподписывает строку"<sourceFileId>:<previewImageId>", сверка идёт черезhash_equals. Если для файла готова быстрая отдача (QuickAccess), вместо токена используется зашифрованный параметр изFileDataParameterService, для превью там появилсяgetEncryptedPreviewFileData(). - Публичная ссылка, созданная на версию, теперь показывает TIFF и документы из этой версии. Имя, время и
FileAttributesдля неё берутся изVersion. - Из докблока
docs_section.phpвидно, как ядро запускает такие точки входа роутинга. Они выполняются до пролога, модули в них не подключить (нет ниLANGUAGE_ID, ни$APPLICATION), поэтому резолв сделан целиком на файловой системе. Код обёрнут в замыкание, чтобы переменные не утекли в глобальную область подключаемой страницы, а сегменты с%2fи%5cотправляются на запасную страницу.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
CDiskExternalLinkComponent::setDocumentSessionSource()
|
Удалено | — |