Достаём файл из ХранилищаЗначения: код, типы данных, ошибки

· 9 мин

Содержание

Частая ошибка: пытаются записать ХранилищеЗначения на диск как есть, и получают исключение или файл из пары килобайт мусора. Само хранилище не файл, а контейнер. Сначала из него надо достать значение методом Получить(), и только потом сохранять.

Если в реквизите лежал файл, значение окажется типом ДвоичныеДанные. Их уже можно записать на диск методом Записать() или отдать пользователю на клиенте. Ниже — полный путь: что внутри хранилища, как это достать, как не упасть на ошибках и как не уронить сервер на файле в сотни мегабайт.

Что на самом деле лежит в ХранилищеЗначения

Объект ХранилищеЗначения хранит один сериализованный объект и, по желанию, сжимает его. Положить можно почти что угодно: двоичные данные, картинку, таблицу значений, структуру, строку. Тип внутри не хранится в отдельном поле, его надо определять по содержимому.

Чаще всего в таких реквизитах встречается следующее:

  • 📎 Двоичные данные файла — документы, PDF, архивы. Самый частый случай.
  • 🖼 Картинка — логотипы, фото номенклатуры, подписи.
  • 📊 Таблица значений или структура — сохранённые настройки, прошлые варианты отчёта.
  • 🔤 Строка — длинный текст или base64-представление файла.

Откуда берётся хранилище в вашей базе, зависит от конфигурации. Это может быть реквизит справочника или документа, регистр сведений , константа. Откройте метаданные в конфигураторе и найдите реквизит с типом ХранилищеЗначения. Там же видно, как он называется в коде, и именно это имя нужно в запросе.

Не уверены, что лежит внутри? Сначала выведите ТипЗнч(Значение) в сообщение пользователю. Две минуты на проверку сэкономят час гадания, почему файл не открывается.

Базовый алгоритм: от реквизита до файла на диске

Работа всегда идёт в три действия: получить объект, достать значение из хранилища, обработать результат. Для файла на диске это выглядит так.

Шаг 1. Получить ссылку и прочитать реквизит

Берёте элемент справочника (или документ) и обращаетесь к реквизиту. Данные при этом читаются из базы, поэтому делайте это на сервере.

&НаСервере
Функция ДанныеФайлаНаСервере(Ссылка)
    Хранилище = Ссылка.ФайлХранилище; // имя вашего реквизита
    Данные = Хранилище.Получить();
    Возврат Данные;
КонецФункции

Имя ФайлХранилище в примере условное. Подставьте реквизит из своей конфигурации.

Шаг 2. Проверить тип полученного значения

Получить() вернёт Неопределено, если хранилище пустое. Если не проверить, следующая строка упадёт с малопонятной ошибкой.

Если Данные = Неопределено Тогда
    ВызватьИсключение "В реквизите нет данных";
КонецЕсли;

Если ТипЗнч(Данные) <> Тип("ДвоичныеДанные") Тогда
    ВызватьИсключение "Внутри хранилища не файл, а " + ТипЗнч(Данные);
КонецЕсли;

Шаг 3. Записать на диск

Для серверного процесса или обработки, которая работает на машине с доступом к каталогу, хватит Записать().

ИмяФайла = ПолучитьИмяВременногоФайла("pdf");
Данные.Записать(ИмяФайла);

Функция ПолучитьИмяВременногоФайла возвращает путь в каталоге временных файлов. Расширение передаёте сами: хранилище его не помнит.

Расширение и имя файла в ХранилищеЗначения не сохраняются. Если они нужны, ищите их в соседних реквизитах того же объекта, например в наименовании или отдельном поле «Расширение».

Двоичные данные: что с ними делать дальше

Когда Получить() вернул ДвоичныеДанные, у вас есть несколько путей. Выбор зависит от того, где нужен результат.

ЗадачаЧто использоватьНюанс
Сохранить файл на сервереДанные.Записать(Путь)Нужны права на каталог у пользователя службы 1С
Отдать файл пользователюПоместитьВоВременноеХранилище и ПолучитьФайлРаботает через клиент, нужен адрес и имя
Показать картинкуНовый Картинка(Данные)Только если внутри действительно изображение
Прочитать как текстПолучитьСтрокуИзДвоичныхДанныхНужна правильная кодировка
Распаковать архивНовый ЧтениеZipФайлаСначала запишите во временный файл
Вложить в письмо или ответПередавать ДвоичныеДанные как естьКопия на диск не нужна

Скачивание на клиенте

Пользователь работает за клиентом, и серверный путь на диске ему бесполезен. Нужна цепочка: серверная функция кладёт данные во временное хранилище, клиент получает адрес и сохраняет файл.

&НаСервере
Функция ПоместитьФайлВоВременноеХранилище(Ссылка)
    Данные = Ссылка.ФайлХранилище.Получить();
    Если Данные = Неопределено Тогда
        Возврат Неопределено;
    КонецЕсли;
    Возврат ПоместитьВоВременноеХранилище(Данные, УникальныйИдентификатор);
КонецФункции

&НаКлиенте
Процедура СкачатьФайл(Команда)
    Адрес = ПоместитьФайлВоВременноеХранилище(ТекущаяСтрока);
    Если Адрес = Неопределено Тогда
        Сообщить("Файл не найден");
        Возврат;
    КонецЕсли;
    ПолучитьФайл(Адрес, "документ.pdf", Истина);
КонецПроцедуры

Третий параметр Истина включает диалог выбора каталога. С Ложь платформа попробует сохранить без вопросов, но в веб-клиенте браузер может всё равно спросить подтверждение. В современных релизах платформы есть и асинхронные аналоги ПолучитьФайл. Их стоит использовать в новом коде, если ваша версия поддерживает.

Если внутри картинка

Для изображения хватит одной строки, дальше его можно вывести на форму или сохранить.

Картинка = Новый Картинка(Данные);
Картинка.Записать(ИмяФайла);

Если конструктор выдаёт ошибку, значит, внутри не изображение. Вернитесь к проверке типа из второго шага.

Если файл лежит в виде строки base64

Бывает, что файл когда-то сохранили как строку. Тогда Получить() вернёт Строка, а не двоичные данные. Конвертировать можно через Base64Значение(Строка), получите ДвоичныеДанные. Дальше всё как обычно.

Откуда брать данные: запросом или через объект

Когда файл нужен один, достаточно ссылки. Когда выгружаете сотни, запрос работает быстрее: вы выбираете только нужные поля и не поднимаете объекты целиком.

Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |    Справочник.Ссылка КАК Ссылка,
    |    Справочник.Наименование КАК Наименование,
    |    Справочник.ФайлХранилище КАК Хранилище
    |ИЗ
    |    Справочник.МойСправочник КАК Справочник
    |ГДЕ
    |    НЕ Справочник.ПометкаУдаления";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Данные = Выборка.Хранилище.Получить();
    // проверка типа и запись
КонецЦикла;

Поле типа ХранилищеЗначения нельзя использовать в условиях, группировке и сортировке запроса. Выбирать его можно, но фильтровать по содержимому нельзя. Если нужно отобрать только записи с файлом, заведите отдельный признак.

Не получайте хранилище в запросе по всей таблице, если файлов тысячи. Запрос вытащит все двоичные данные сразу, и памяти сервера может не хватить. Сначала выберите ссылки, потом читайте хранилища по одному.

Ошибки и пустые значения: как не упасть

Основные сбои при работе с хранилищем повторяются из базы в базу. Проверьте их по списку.

Диагностика: выберите симптом

Дополнительно оберните чтение в Попытка ... Исключение, если выгружаете пачку файлов. Так один битый элемент не остановит всю загрузку.

Попытка
    Данные = Выборка.Хранилище.Получить();
    Данные.Записать(Путь);
Исключение
    Сообщить("Ошибка у элемента " + Выборка.Наименование + ": "
        + ОписаниеОшибки());
КонецПопытки;

В журнал стоит писать не только текст ошибки, но и ссылку на проблемный элемент. Через неделю вы не вспомните, какой из трёх тысяч файлов оказался битым.

Большие файлы и производительность

Пока файл на несколько мегабайт, проблем нет. На сотнях мегабайт появляются нюансы: Получить() поднимает значение в память целиком. Ни частичного чтения, ни потока у хранилища нет.

Что реально помогает:

  1. Не держите много файлов в памяти сразу. Получили, записали, обнулили переменную, перешли к следующему.
  2. Выгружайте на сервере, а не через клиент. Передача большого файла клиенту через временное хранилище удваивает расход памяти.
  3. Разбивайте выгрузку на порции. Обрабатывайте по 50–100 элементов за проход, например в регламентном задании.
  4. Не запускайте массовую выгрузку в рабочее время. Чтение двоичных данных заметно нагружает сервер и базу.
  5. Отключите лишнее. Если выгрузка идёт из обработки, не открывайте формы и не пишите в журнал каждый шаг.

Для оценки: файл 100 МБ в хранилище займёт примерно 100 МБ памяти процесса при чтении. Серверу с несколькими параллельными сеансами это уже ощутимо. Цифра ориентировочная и зависит от сжатия при записи.

Если файлов много и они крупные, лучше вообще не держать их в хранилищах. Для новых данных используйте механизм присоединённых файлов с хранением в томах на диске. База тогда не пухнет, а выгрузка работает как копирование файлов.

Готовые сценарии: что обычно просят

Ниже три типовые задачи, которые решаются на тех же трёх действиях.

Выгрузить все файлы справочника в папку

Цикл по выборке из запроса, на каждой итерации получаете значение, проверяете тип и пишете на диск. Имя файла собирайте из наименования, но очищайте от символов, недопустимых в пути: \ / : * ? " < > |. Расширение берите из соседнего реквизита, иначе потом придётся угадывать.

Заменить хранилище на присоединённые файлы

Это частая задача при чистке базы. Алгоритм: прочитать данные из хранилища, создать присоединённый файл через механизм работы с файлами вашей конфигурации, очистить старый реквизит. Делайте на копии базы. Откат при ошибке миграции без резервной копии невозможен.

Показать содержимое прямо на форме

Если внутри картинка, поместите её во временное хранилище и привяжите к полю картинки на форме. Если PDF, отдавать нужно через скачивание: встроенного просмотра PDF в обычной форме нет.

Перед массовой выгрузкой
Выполнено: 0 / 6

Где запускать код и что проверить перед стартом

Самый быстрый способ разово достать файл — внешняя обработка. Её открывают через меню файла в режиме 1С:Предприятие, не трогая конфигурацию. Для регулярной выгрузки лучше расширение: оно переживает обновления и не требует снимать конфигурацию с поддержки.

Прежде чем писать код, ответьте себе на четыре вопроса:

  • 🔎 Где именно лежит реквизит: в справочнике, документе, регистре или константе?
  • 🔐 Есть ли у вас права на чтение этого объекта и на запись в каталог выгрузки?
  • 🖥 Файловая у вас база или клиент-серверная? От этого зависит, чей это диск.
  • 📦 Сколько файлов и какого размера? Десяток по мегабайту и десять тысяч по сто мегабайт решаются по-разному.

Отдельно про отладку. Поставьте точку останова сразу после Получить() и посмотрите значение в окне вычисления выражений. По иконке и типу сразу видно, что вернулось: двоичные данные, картинка или пустота. Это быстрее, чем гадать по тексту исключения.

Ещё одна мелочь, которая выручает: сохраните первые байты файла в журнал. У PDF файл начинается с символов %PDF, у ZIP и docx с PK. Если вместо этого вы видите что-то другое, значит, внутри не тот формат, который вы ожидали, или данные пережили сторонний обмен.

Частые вопросы

Можно ли получить файл из хранилища без программирования?
В обычном интерфейсе — нет. Реквизит типа ХранилищеЗначения пользователю не показывается как файл. Нужен код в обработке, внешней обработке или расширении, либо универсальная обработка с функцией выгрузки.
Чем отличается ХранилищеЗначения от присоединённого файла?
Хранилище — это поле внутри таблицы базы, одно значение без имени и расширения. Присоединённый файл — отдельный элемент справочника со своими реквизитами и опциональным хранением на диске.
Почему файл после выгрузки весит иначе, чем исходный?
Хранилище могло хранить сжатые данные. После Получить() и записи на диск вы получаете исходный размер. Разница в размере в базе и на диске — норма.
Можно ли получить значение из хранилища в запросе?
Выбрать поле можно, но получить из него данные придётся вызовом Получить() уже в коде. Фильтровать и группировать по хранилищу в запросе нельзя.
Что делать, если Получить() выдаёт ошибку десериализации?
Данные могли записать в другой версии платформы или с типом, которого нет в текущей конфигурации. Попробуйте открыть базу той же версией платформы, в которой данные создавались.