Запись объекта ДвоичныеДанные на диск и выдача файла пользователю
Содержание
Чтобы превратить двоичные данные в файл в 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-сервис. Есть два способа собрать их в один файл.
| Способ | Когда подходит | Минус |
|---|---|---|
СоединитьДвоичныеДанные(МассивЧастей) | Части небольшие, суммарно десятки мегабайт | Весь результат в памяти |
ЗаписьДанных поверх ФайловыйПоток | Крупный итоговый файл | Больше кода |
ПотокВПамяти + ЗакрытьИПолучитьДвоичныеДанные() | Нужны ДвоичныеДанные, а не файл | Тоже в памяти |
Пример через ЗаписьДанных:
Поток = ФайловыеПотоки.ОткрытьДляЗаписи(ИмяФайла);
Запись = Новый ЗаписьДанных(Поток);
Для Каждого Часть Из МассивЧастей Цикл
Запись.Записать(Часть);
КонецЦикла;
Запись.Закрыть();
Поток.Закрыть();
Обратная операция — РазделитьДвоичныеДанные(ДД, РазмерЧасти) — режет объект на массив частей заданного размера. Пригодится, если внешний сервис принимает файлы не больше определённого объёма.
Ошибки при записи файла и как их ловить
Запись на диск — внешняя операция. Она падает по причинам, которые от кода не зависят: нет прав, нет места, файл занят. Поэтому вызов всегда оборачивают в Попытка.
Попытка
ДанныеФайла.Записать(ИмяФайла);
Исключение
Инфо = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации("Выгрузка файлов",
УровеньЖурналаРегистрации.Ошибка, , ,
ПодробноеПредставлениеОшибки(Инфо));
ВызватьИсключение КраткоеПредставлениеОшибки(Инфо);
КонецПопытки;
Пользователю показывайте краткое представление, в журнал пишите подробное. Стек вызовов на экране бухгалтеру не поможет, а администратору без него не разобраться.
Не закрытый поток держит файл заблокированным до завершения сеанса или сборки мусора. Удалить такой файл через УдалитьФайлы() не получится. Закрывайте потоки и в ветке Исключение.
Как отдать файл пользователю: временное хранилище и сохранение на клиенте
Пользователь получает файл только через клиентское приложение. Схема одна для тонкого, толстого и веб-клиента:
- На сервере положить
ДвоичныеДанныево временное хранилище. - Вернуть на клиент адрес.
- На клиенте вызвать сохранение по адресу.
Серверная часть
&НаСервере
Функция ПодготовитьФайл()
ДанныеФайла = СформироватьДанные(); // ваша логика
Возврат ПоместитьВоВременноеХранилище(ДанныеФайла, УникальныйИдентификатор);
КонецФункции
Второй параметр — УникальныйИдентификатор формы. Тогда хранилище очистится автоматически при закрытии формы. Без него данные живут до конца сеанса.
Клиентская часть в конфигурации на БСП
&НаКлиенте
Процедура Выгрузить(Команда)
Адрес = ПодготовитьФайл();
ФайловаяСистемаКлиент.СохранитьФайл(Неопределено, Адрес, "Акт сверки.pdf");
КонецПроцедуры
ФайловаяСистемаКлиент.СохранитьФайл сама выбирает способ для текущего клиента: в тонком откроет диалог сохранения, в веб-клиенте отдаст файл браузеру. Расширение работы с файлами ставить не обязательно.
Без БСП
На чистой платформе используйте асинхронный метод НачатьПолучениеФайлаССервера(Адрес, ИмяФайла) с обработчиком оповещения, если версия платформы его поддерживает. В старом коде встречается ПолучитьФайл(Адрес, ИмяФайла, Истина). Это синхронный вызов, в веб-клиенте и при запрете модальности он работать не будет — при доработке такой код лучше переписать.
Как сформировать имя файла
Имя, которое увидит пользователь, собирайте из данных документа, а не оставляйте техническим. Например, "Счёт " + Номер + " от " + Формат(Дата, "ДФ=dd.MM.yyyy") + ".pdf". Номер документа часто содержит префикс со слешем или пробелами по краям — уберите их через СокрЛП() и замените недопустимые символы перед вызовом сохранения.
Если файлов несколько, не открывайте диалог сохранения на каждый. Упакуйте их в архив через ЗаписьZipФайла на сервере во временный файл, прочитайте архив в ДвоичныеДанные, удалите временный файл и отдайте пользователю один ZIP. В БСП для пакетной выдачи есть ФайловаяСистемаКлиент.СохранитьФайлы, она принимает массив описаний передаваемых файлов.
Не передавайте крупные ДвоичныеДанные возвратом серверной функции. Каждый вызов сервера сериализует их и гоняет по сети. Адрес временного хранилища — короткая строка, сами байты уходят на клиент один раз при сохранении.
Запись на клиенте и когда временный файл не нужен
Если данные уже на клиенте (например, пришли из хранилища для просмотра), в тонком и толстом клиенте можно записать их и открыть:
&НаКлиенте
Процедура ОткрытьНаКомпьютере(ДанныеФайла, Расширение)
ИмяФайла = ПолучитьИмяВременногоФайла(Расширение);
ДанныеФайла.Записать(ИмяФайла);
ЗапуститьПриложение(ИмяФайла);
КонецПроцедуры
В веб-клиенте прямого доступа к диску нет, этот код не подойдёт. Там работает только путь через временное хранилище и сохранение браузером.
Временный файл вообще не нужен, если:
- 🧾 файл надо положить в присоединённые файлы — передайте
ДвоичныеДанныеили адрес хранилища в функции БСП; - 📨 файл уходит вложением письма — вложения принимают двоичные данные напрямую;
- 🌐 файл отправляется HTTP-запросом —
Запрос.УстановитьТелоИзДвоичныхДанных(ДД); - 🗄 файл хранится в реквизите —
Новый ХранилищеЗначения(ДД, Новый СжатиеДанных(9)).
Каждая лишняя запись на диск — это права, место, блокировки и необходимость удалять за собой. Пишите файл, только когда без него не обойтись.
Частые вопросы
Можно ли получить ДвоичныеДанные без записи файла, если у меня есть только путь на компьютере пользователя?
ФайловаяСистемаКлиент.ЗагрузитьФайл в БСП или НачатьПомещениеФайлаНаСервер), затем на сервере читают ПолучитьИзВременногоХранилища(Адрес).Как узнать кодировку текста внутри ДвоичныхДанных?
ПолучитьСтрокуИзДвоичныхДанных(ДД, КодировкаТекста.UTF8).Чем Base64Значение отличается от ПолучитьДвоичныеДанныеИзBase64Строки?
ДвоичныеДанные. Вторая функция появилась вместе с набором функций преобразования в платформе 8.3.9 и читается однозначнее, в новом коде используют её.Почему файл, сохранённый в веб-клиенте, называется иначе?
\ / : * ? " < > | и сразу с расширением.Сколько живут данные во временном хранилище?
УдалитьИзВременногоХранилища(Адрес).