Запись объекта ДвоичныеДанные на диск и выдача файла пользователю

· 9 мин

Содержание

Чтобы превратить двоичные данные в файл в 1С, достаточно одного вызова: ДД.Записать(ИмяФайла), где ДД — значение типа ДвоичныеДанные, а ИмяФайла — полный путь, например из ПолучитьИмяВременногоФайла("pdf"). Метод создаёт файл или перезаписывает существующий.

Сложности начинаются вокруг этой строки: где именно появится файл (на сервере или у пользователя), как не съесть память на файле в 500 МБ, как отдать результат в веб-клиент и не оставить мусор во временном каталоге. Всё это — общая механика платформы 1С:Предприятие 8.3, от конфигурации она не зависит.

Что хранит объект ДвоичныеДанные и чем он не является

ДвоичныеДанные — это неизменяемый набор байтов в памяти. У объекта нет имени файла, расширения, кодировки и даты. Только содержимое и размер.

Что умеет объект:

  • 📏 Размер() — длина в байтах;
  • 💾 Записать(ИмяФайла) или Записать(Поток) — сохранить на диск или в поток;
  • 📖 ОткрытьПотокДляЧтения() — читать содержимое кусками (платформа 8.3.9 и новее);
  • 🔁 передаваться между клиентом и сервером, класться во временное хранилище и в ХранилищеЗначения.

Изменить байты внутри готового объекта нельзя. Чтобы что-то дописать или заменить, собирают новый объект через поток или буфер.

Расширение файла берите из того места, откуда пришли данные: реквизит «Расширение» в справочнике файлов, заголовок Content-Type HTTP-ответа, имя вложения письма. Сами байты о формате ничего не скажут, если вы не разбираете сигнатуру вручную.

Главная ловушка — контекст. Код в серверной процедуре пишет файл на диск сервера 1С. Путь C:\Users\Ivanov\Desktop\report.pdf в &НаСервере создаст файл на сервере, если у пользователя службы есть права, и бухгалтер его никогда не увидит. В файловой базе на одном компьютере это незаметно, а после перехода на клиент-серверный вариант «вдруг перестаёт работать».

Откуда берутся двоичные данные: основные источники

Прежде чем записывать файл, данные нужно получить. Самые частые варианты собраны в таблице.

ИсточникКак получить ДвоичныеДанные
Файл на дискеНовый ДвоичныеДанные(ПолноеИмяФайла)
Строка Base64 (XML, JSON, ответ сервиса)ПолучитьДвоичныеДанныеИзBase64Строки(Строка) или Base64Значение(Строка)
Hex-строкаПолучитьДвоичныеДанныеИзHexСтроки(Строка)
Обычный текстПолучитьДвоичныеДанныеИзСтроки(Текст, КодировкаТекста.UTF8)
HTTP-ответОтвет.ПолучитьТелоКакДвоичныеДанные()
Реквизит типа ХранилищеЗначенияОбъект.Реквизит.Получить()
Временное хранилищеПолучитьИзВременногоХранилища(Адрес)
Присоединённый файл (БСП)РаботаСФайлами.ДвоичныеДанныеФайла(Ссылка)
Поток в памятиПоток.ЗакрытьИПолучитьДвоичныеДанные()

Пример: в ответе веб-сервиса пришёл PDF в Base64.

ДанныеФайла = ПолучитьДвоичныеДанныеИзBase64Строки(СтрокаBase64);
Если ДанныеФайла.Размер() = 0 Тогда
    ВызватьИсключение "Сервис вернул пустой файл";
КонецЕсли;

Проверка размера здесь не лишняя. Битая или пустая строка Base64 не всегда приводит к исключению, а пустой файл на выходе выглядит как «файл не открывается».

У ХранилищеЗначения.Получить() тип результата зависит от того, что туда положили. Если хранилище заполняли картинкой, вернётся Картинка, а не ДвоичныеДанные. Проверяйте: Если ТипЗнч(Значение) = Тип("ДвоичныеДанные") Тогда.

Запись двоичных данных во временный файл на сервере

Временный файл нужен, когда дальше данные обрабатывает что-то, умеющее работать только с файлами: внешняя компонента, ЧтениеZipФайла, ТабличныйДокумент.Прочитать(), утилита командной строки.

Шаг 1. Получите уникальное имя

ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");

Платформа вернёт путь во временном каталоге текущего пользователя ОС (на сервере — пользователя службы 1С). Расширение передавайте без точки. Сам файл функция не создаёт, только имя.

В конфигурациях на БСП 3.x принято вызывать ФайловаяСистема.ВременныйФайл("xlsx"), а удалять через ФайловаяСистема.УдалитьВременныйФайл(Путь). Обёртка аккуратнее работает с правами и пишет ошибки в журнал регистрации.

Шаг 2. Запишите данные

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

Шаг 3. Используйте файл и удалите его

Попытка
    Документ = Новый ТабличныйДокумент;
    Документ.Прочитать(ИмяВременногоФайла);
    // ... обработка документа
Исключение
    УдалитьФайлы(ИмяВременногоФайла);
    ВызватьИсключение;
КонецПопытки;

УдалитьФайлы(ИмяВременногоФайла);

Удаление в обеих ветках — обязательная часть алгоритма. Сервер работает месяцами, и забытые временные файлы копятся гигабайтами. Отдельно чистить временный каталог службы приходится именно из-за этого.

Если файл нужен только чтобы сразу прочитать его объектом, который принимает поток, временный файл не нужен вовсе. ЧтениеZipФайла, ЧтениеXML, ЧтениеJSON и ТабличныйДокумент.Прочитать() в актуальных версиях платформы принимают поток: передайте ДанныеФайла.ОткрытьПотокДляЧтения().

Потоки и буфер: большие файлы и сборка из частей

Метод Записать() держит в памяти весь объект целиком. Для файла в 5 МБ это неважно. Для выгрузки в 800 МБ на сервере с десятком сеансов — уже вопрос стабильности рабочего процесса.

Потоки (ФайловыйПоток, ПотокВПамяти) и объекты ЗаписьДанных/ЧтениеДанных появились в платформе 8.3.9. Они позволяют работать с данными частями.

Копирование через поток

ПотокЧтения = ДанныеФайла.ОткрытьПотокДляЧтения();
ПотокЗаписи = ФайловыеПотоки.ОткрытьДляЗаписи(ИмяФайла);
Попытка
    ПотокЧтения.КопироватьВ(ПотокЗаписи);
Исключение
    ПотокЧтения.Закрыть();
    ПотокЗаписи.Закрыть();
    ВызватьИсключение;
КонецПопытки;
ПотокЧтения.Закрыть();
ПотокЗаписи.Закрыть();

ФайловыеПотоки.ОткрытьДляЗаписи() создаёт файл заново, если он есть — обнуляет. Для дозаписи в конец используйте Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Дописать).

Чтение кусками через буфер

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

РазмерБлока = 1048576; // 1 МБ
Буфер = Новый БуферДвоичныхДанных(РазмерБлока);

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

Прочитано = ПотокЧтения.Прочитать(Буфер, 0, РазмерБлока);
Пока Прочитано > 0 Цикл
    ПотокЗаписи.Записать(Буфер, 0, Прочитано);
    Прочитано = ПотокЧтения.Прочитать(Буфер, 0, РазмерБлока);
КонецЦикла;

ПотокЧтения.Закрыть();
ПотокЗаписи.Закрыть();

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

Склейка файла из частей

Данные часто приходят порциями: постраничная выдача API, части письма, куски при загрузке через HTTP-сервис. Есть два способа собрать их в один файл.

СпособКогда подходитМинус
СоединитьДвоичныеДанные(МассивЧастей)Части небольшие, суммарно десятки мегабайтВесь результат в памяти
ЗаписьДанных поверх ФайловыйПотокКрупный итоговый файлБольше кода
ПотокВПамяти + ЗакрытьИПолучитьДвоичныеДанные()Нужны ДвоичныеДанные, а не файлТоже в памяти

Пример через ЗаписьДанных:

Поток = ФайловыеПотоки.ОткрытьДляЗаписи(ИмяФайла);
Запись = Новый ЗаписьДанных(Поток);
Для Каждого Часть Из МассивЧастей Цикл
    Запись.Записать(Часть);
КонецЦикла;
Запись.Закрыть();
Поток.Закрыть();

Обратная операция — РазделитьДвоичныеДанные(ДД, РазмерЧасти) — режет объект на массив частей заданного размера. Пригодится, если внешний сервис принимает файлы не больше определённого объёма.

Ошибки при записи файла и как их ловить

Запись на диск — внешняя операция. Она падает по причинам, которые от кода не зависят: нет прав, нет места, файл занят. Поэтому вызов всегда оборачивают в Попытка.

Попытка
    ДанныеФайла.Записать(ИмяФайла);
Исключение
    Инфо = ИнформацияОбОшибке();
    ЗаписьЖурналаРегистрации("Выгрузка файлов",
        УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(Инфо));
    ВызватьИсключение КраткоеПредставлениеОшибки(Инфо);
КонецПопытки;

Пользователю показывайте краткое представление, в журнал пишите подробное. Стек вызовов на экране бухгалтеру не поможет, а администратору без него не разобраться.

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

Не закрытый поток держит файл заблокированным до завершения сеанса или сборки мусора. Удалить такой файл через УдалитьФайлы() не получится. Закрывайте потоки и в ветке Исключение.

Как отдать файл пользователю: временное хранилище и сохранение на клиенте

Пользователь получает файл только через клиентское приложение. Схема одна для тонкого, толстого и веб-клиента:

  1. На сервере положить ДвоичныеДанные во временное хранилище.
  2. Вернуть на клиент адрес.
  3. На клиенте вызвать сохранение по адресу.

Серверная часть

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

Второй параметр — УникальныйИдентификатор формы. Тогда хранилище очистится автоматически при закрытии формы. Без него данные живут до конца сеанса.

Клиентская часть в конфигурации на БСП

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

ФайловаяСистемаКлиент.СохранитьФайл сама выбирает способ для текущего клиента: в тонком откроет диалог сохранения, в веб-клиенте отдаст файл браузеру. Расширение работы с файлами ставить не обязательно.

Без БСП

На чистой платформе используйте асинхронный метод НачатьПолучениеФайлаССервера(Адрес, ИмяФайла) с обработчиком оповещения, если версия платформы его поддерживает. В старом коде встречается ПолучитьФайл(Адрес, ИмяФайла, Истина). Это синхронный вызов, в веб-клиенте и при запрете модальности он работать не будет — при доработке такой код лучше переписать.

Как сформировать имя файла

Имя, которое увидит пользователь, собирайте из данных документа, а не оставляйте техническим. Например, "Счёт " + Номер + " от " + Формат(Дата, "ДФ=dd.MM.yyyy") + ".pdf". Номер документа часто содержит префикс со слешем или пробелами по краям — уберите их через СокрЛП() и замените недопустимые символы перед вызовом сохранения.

Если файлов несколько, не открывайте диалог сохранения на каждый. Упакуйте их в архив через ЗаписьZipФайла на сервере во временный файл, прочитайте архив в ДвоичныеДанные, удалите временный файл и отдайте пользователю один ZIP. В БСП для пакетной выдачи есть ФайловаяСистемаКлиент.СохранитьФайлы, она принимает массив описаний передаваемых файлов.

Не передавайте крупные ДвоичныеДанные возвратом серверной функции. Каждый вызов сервера сериализует их и гоняет по сети. Адрес временного хранилища — короткая строка, сами байты уходят на клиент один раз при сохранении.

Запись на клиенте и когда временный файл не нужен

Если данные уже на клиенте (например, пришли из хранилища для просмотра), в тонком и толстом клиенте можно записать их и открыть:

&НаКлиенте
Процедура ОткрытьНаКомпьютере(ДанныеФайла, Расширение)
    ИмяФайла = ПолучитьИмяВременногоФайла(Расширение);
    ДанныеФайла.Записать(ИмяФайла);
    ЗапуститьПриложение(ИмяФайла);
КонецПроцедуры

В веб-клиенте прямого доступа к диску нет, этот код не подойдёт. Там работает только путь через временное хранилище и сохранение браузером.

Временный файл вообще не нужен, если:

  • 🧾 файл надо положить в присоединённые файлы — передайте ДвоичныеДанные или адрес хранилища в функции БСП;
  • 📨 файл уходит вложением письма — вложения принимают двоичные данные напрямую;
  • 🌐 файл отправляется HTTP-запросом — Запрос.УстановитьТелоИзДвоичныхДанных(ДД);
  • 🗄 файл хранится в реквизите — Новый ХранилищеЗначения(ДД, Новый СжатиеДанных(9)).

Каждая лишняя запись на диск — это права, место, блокировки и необходимость удалять за собой. Пишите файл, только когда без него не обойтись.

Перед выкладкой кода в рабочую базу
Выполнено: 0 / 7

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

Можно ли получить ДвоичныеДанные без записи файла, если у меня есть только путь на компьютере пользователя?
Нет, сервер не видит диск пользователя. Файл сначала помещают во временное хранилище с клиента (через ФайловаяСистемаКлиент.ЗагрузитьФайл в БСП или НачатьПомещениеФайлаНаСервер), затем на сервере читают ПолучитьИзВременногоХранилища(Адрес).
Как узнать кодировку текста внутри ДвоичныхДанных?
Объект кодировку не хранит. Её берут из метаданных источника или проверяют сигнатуру BOM в первых байтах. Затем текст получают через ПолучитьСтрокуИзДвоичныхДанных(ДД, КодировкаТекста.UTF8).
Чем Base64Значение отличается от ПолучитьДвоичныеДанныеИзBase64Строки?
Результат одинаковый — ДвоичныеДанные. Вторая функция появилась вместе с набором функций преобразования в платформе 8.3.9 и читается однозначнее, в новом коде используют её.
Почему файл, сохранённый в веб-клиенте, называется иначе?
Браузер может изменить имя: убрать запрещённые символы, добавить номер при совпадении. Задавайте имя без символов \ / : * ? " < > | и сразу с расширением.
Сколько живут данные во временном хранилище?
С идентификатором формы — до её закрытия. Без идентификатора — до конца сеанса. Принудительно очистить можно УдалитьИзВременногоХранилища(Адрес).