Присоединённые файлы из кода: БСП, двоичные данные и HTTP
Содержание
Чтобы программно прикрепить файл к документу
в 1С, в типовых конфигурациях на БСП достаточно двух вызовов на сервере: РаботаСФайлами.ПараметрыДобавленияФайла() и РаботаСФайлами.ДобавитьФайл(). Первому передаёте владельца (ссылку на документ), имя и расширение, второму — адрес двоичных данных во временном хранилище. На выходе получаете ссылку на элемент присоединённого файла, который сразу виден в форме документа по кнопке со скрепкой.
Тема общая для платформы 8.3 и любых конфигураций на Библиотеке стандартных подсистем: Бухгалтерии 3.0, ЗУП 3.1, УТ 11, ERP. Если БСП в базе нет, файл хранят в реквизите типа ХранилищеЗначения или в собственном справочнике — этот вариант тоже разобран с кодом.
Где 1С хранит присоединённые файлы
Прежде чем писать код, надо понять, куда он запишет данные. В конфигурациях на БСП файл документа — это не реквизит самого документа, а элемент отдельного справочника.
Как устроено хранение:
- 📁 Справочник присоединённых файлов. Для объекта создаётся справочник с именем вида
<ИмяОбъекта>ПрисоединенныеФайлы, напримерРеализацияТоваровУслугПрисоединенныеФайлы. Связь с документом — через реквизитВладелецФайла. - 🗂 Справочник «Файлы». Общая файловая библиотека с папками. Там же живут версии файлов.
- 💾 Место хранения двоичных данных. Либо в самой информационной базе, либо в томах на диске. Режим задаётся администратором в разделе
Администрированиев настройках работы с файлами. - 📝 Карточка файла. Имя, расширение, размер, автор, дата изменения, описание, признак подписи ЭП и шифрования.
Код через программный интерфейс БСП не зависит от того, где лежат байты. Вы передаёте данные, а библиотека сама решает, писать их в базу или в том. Поэтому писать напрямую в регистры и служебные справочники подсистемы не нужно: при обновлении БСП их состав меняется.
Хранение в томах снижает размер базы и ускоряет резервное копирование. Для баз, где к документам цепляют сканы и фото, это почти обязательная настройка.
Как прикрепить файл к документу через БСП
Базовый алгоритм из трёх шагов:
- Получить двоичные данные файла (с диска сервера, с клиента, из HTTP-ответа).
- Поместить их во временное хранилище и получить адрес.
- Заполнить параметры добавления и вызвать
ДобавитьФайл.
Серверная процедура
Процедура в общем модуле с флажком «Сервер». Файл читается с диска сервера по полному пути.
Функция ПрикрепитьФайлКДокументу(ДокументСсылка, ПолноеИмяФайла) Экспорт
ФайлНаДиске = Новый Файл(ПолноеИмяФайла);
Если Не ФайлНаДиске.Существует() Тогда
ВызватьИсключение СтрШаблон("Файл не найден: %1", ПолноеИмяФайла);
КонецЕсли;
ДвоичныеДанные = Новый ДвоичныеДанные(ПолноеИмяФайла);
Адрес = ПоместитьВоВременноеХранилище(ДвоичныеДанные, Новый УникальныйИдентификатор);
ПараметрыФайла = РаботаСФайлами.ПараметрыДобавленияФайла();
ПараметрыФайла.ВладелецФайлов = ДокументСсылка;
ПараметрыФайла.ИмяБезРасширения = ФайлНаДиске.ИмяБезРасширения;
ПараметрыФайла.РасширениеБезТочки = СтрЗаменить(ФайлНаДиске.Расширение, ".", "");
ПараметрыФайла.ВремяИзмененияУниверсальное = ФайлНаДиске.ПолучитьВремяИзмененияУниверсальное();
ПрисоединенныйФайл = РаботаСФайлами.ДобавитьФайл(ПараметрыФайла, Адрес, , "Загружено обработкой");
УдалитьИзВременногоХранилища(Адрес);
Возврат ПрисоединенныйФайл;
КонецФункции
Четвёртый параметр ДобавитьФайл — описание файла, оно видно в списке присоединённых файлов. Третий — адрес извлечённого текста для полнотекстового поиска, его можно не заполнять.
Расширение у объекта Файл возвращается с точкой (.pdf), а БСП ждёт его без точки. Отсюда СтрЗаменить. Если в имени файла есть ещё точки, например акт.подписан.pdf, это не мешает: свойство Расширение берёт только последнюю часть.
Клиентская часть: файл с компьютера пользователя
Если файл выбирает пользователь, данные сначала нужно передать на сервер. В платформе 8.3.18 и выше удобно использовать асинхронный вызов с Ждать:
&НаКлиенте
Асинх Процедура ПрикрепитьФайл(Команда)
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинх(, , , , УникальныйИдентификатор);
Если ОписаниеФайла = Неопределено Или ОписаниеФайла.ПомещениеФайлаОтменено Тогда
Возврат;
КонецЕсли;
ПрикрепитьНаСервере(ОписаниеФайла.Адрес, ОписаниеФайла.СсылкаНаФайл.Имя);
КонецПроцедуры
&НаСервере
Процедура ПрикрепитьНаСервере(Адрес, ИмяФайла)
ФайлИнфо = Новый Файл(ИмяФайла);
ПараметрыФайла = РаботаСФайлами.ПараметрыДобавленияФайла();
ПараметрыФайла.ВладелецФайлов = Объект.Ссылка;
ПараметрыФайла.ИмяБезРасширения = ФайлИнфо.ИмяБезРасширения;
ПараметрыФайла.РасширениеБезТочки = СтрЗаменить(ФайлИнфо.Расширение, ".", "");
РаботаСФайлами.ДобавитьФайл(ПараметрыФайла, Адрес);
КонецПроцедуры
Передавайте УникальныйИдентификатор формы: тогда данные во временном хранилище живут, пока открыта форма, и не пропадут между вызовами.
Новый документ сначала нужно записать. У незаписанного документа пустая ссылка, и файл повиснет без владельца. Проверяйте Объект.Ссылка.Пустая() и предлагайте записать документ.
Прикрепление файла к документу без БСП
В самописной конфигурации или маленькой базе без библиотеки есть два рабочих способа.
| Способ | Когда подходит | Минусы |
|---|---|---|
Реквизит ХранилищеЗначения в документе | Один файл на документ, небольшой размер | Документ «тяжелеет», каждое открытие формы тянет данные |
| Свой справочник с владельцем-документом | Несколько файлов, нужен список и поиск | Надо сделать форму списка и команду открытия |
Табличная часть с ХранилищеЗначения | Несколько мелких вложений | Те же минусы, что у реквизита, только сильнее |
Реквизит типа ХранилищеЗначения
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.ФайлВложения = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
ДокументОбъект.ИмяФайлаВложения = ФайлНаДиске.Имя;
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
Достать файл обратно: ДвоичныеДанные = ДокументОбъект.ФайлВложения.Получить();. Уже сжатые форматы (PDF, JPG, ZIP) от СжатиеДанных почти не уменьшаются, для них можно передавать Новый СжатиеДанных(0) и не тратить время процессора.
Записывайте проведённый документ в режиме Запись, а не Проведение, если меняете только вложение. Иначе вы перепроведёте документ и перезапишете движения ради одной картинки.
Свой справочник файлов
Создайте справочник ФайлыДокументов с владельцем — нужными документами, реквизитами ИмяФайла (строка), Размер (число) и Данные (ХранилищеЗначения). Тогда документ не меняется вообще:
НовыйФайл = Справочники.ФайлыДокументов.СоздатьЭлемент();
НовыйФайл.Владелец = ДокументСсылка;
НовыйФайл.Наименование = ФайлНаДиске.ИмяБезРасширения;
НовыйФайл.ИмяФайла = ФайлНаДиске.Имя;
НовыйФайл.Размер = ДвоичныеДанные.Размер();
НовыйФайл.Данные = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
НовыйФайл.Записать();
Двоичные данные, временное хранилище и потоки
Любой путь прикрепления сходится к объекту ДвоичныеДанные. Получить его можно несколькими способами:
- 📄
Новый ДвоичныеДанные(ПолноеИмяФайла)— с диска сервера; - 🔤
Base64Значение(Строка)— из строки Base64, например из JSON; - 🌐
HTTPОтвет.ПолучитьТелоКакДвоичныеДанные()— из ответа внешнего сервиса; - 🧩
ПотокВПамяти.ЗакрытьИПолучитьДвоичныеДанные()— после сборки файла в памяти; - 📦
ПолучитьИзВременногоХранилища(Адрес)— если данные уже передали с клиента.
Временное хранилище: время жизни
Второй параметр ПоместитьВоВременноеХранилище определяет, сколько живут данные:
| Второй параметр | Когда удаляются данные |
|---|---|
| Не указан | После завершения текущего вызова сервера |
УникальныйИдентификатор формы | При закрытии формы |
| Другой уникальный идентификатор | В конце сеанса, если не удалить раньше |
При пакетной загрузке сотен файлов вызывайте УдалитьИзВременногоХранилища(Адрес) сразу после ДобавитьФайл. Иначе память рабочего процесса растёт до конца сеанса.
Потоки для больших и собранных файлов
Когда файл нужно сформировать или преобразовать, работайте через потоки, а не через склейку строк:
Поток = Новый ПотокВПамяти;
Запись = Новый ЗаписьДанных(Поток);
Запись.ЗаписатьСтроку("Номер;Дата;Сумма");
Запись.ЗаписатьСтроку(СтрШаблон("%1;%2;%3",
ДокументСсылка.Номер,
Формат(ДокументСсылка.Дата, "ДФ=dd.MM.yyyy"),
Формат(ДокументСсылка.СуммаДокумента, "ЧГ=0; ЧРД=,")));
Запись.Закрыть();
ДвоичныеДанные = Поток.ЗакрытьИПолучитьДвоичныеДанные();
Для чтения большого файла частями есть ДвоичныеДанные.ОткрытьПотокДляЧтения() и ФайловыйПоток. Если внешний сервис отдаёт файл кусками, соберите их функцией СоединитьДвоичныеДанные(МассивЧастей), а для отправки частями — РазделитьДвоичныеДанные(Данные, РазмерЧасти).
Чтобы не прикрепить один и тот же файл дважды, считайте хеш: Хеш = Новый ХешированиеДанных(ХешФункция.SHA256); Хеш.Добавить(ДвоичныеДанные); и сравнивайте Хеш.ХешСумма с сохранённым значением в своём реквизите или регистре.
Загрузка файлов из внешних систем и через HTTP-сервис
Два направления: 1С сама забирает файл по ссылке или внешняя система присылает его в 1С.
1С скачивает файл по URL
Соединение = Новый HTTPСоединение("files.example.ru", 443, , , , 60, Новый ЗащищенноеСоединениеOpenSSL);
Запрос = Новый HTTPЗапрос("/scans/act-1024.pdf");
Ответ = Соединение.Получить(Запрос);
Если Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение СтрШаблон("Сервер вернул код %1", Формат(Ответ.КодСостояния, "ЧГ=0"));
КонецЕсли;
ДвоичныеДанные = Ответ.ПолучитьТелоКакДвоичныеДанные();
Дальше — тот же ПоместитьВоВременноеХранилище и ДобавитьФайл. Таймаут 60 секунд задан шестым параметром: без него зависший сервер подвесит фоновое задание.
Внешняя система отправляет файл в 1С
Создайте в конфигураторе HTTP-сервис, например с шаблоном /attach/{docid} и методом POST. Тело запроса — сами байты файла, имя — в заголовке или параметре.
Функция AttachPOST(Запрос)
Идентификатор = Новый УникальныйИдентификатор(Запрос.ПараметрыURL["docid"]);
ДокументСсылка = Документы.РеализацияТоваровУслуг.ПолучитьСсылку(Идентификатор);
Если ДокументСсылка.ПолучитьОбъект() = Неопределено Тогда
Возврат Новый HTTPСервисОтвет(404);
КонецЕсли;
ИмяФайла = Запрос.ПараметрыЗапроса.Получить("name");
Если Не ЗначениеЗаполнено(ИмяФайла) Тогда
Возврат Новый HTTPСервисОтвет(400);
КонецЕсли;
Адрес = ПоместитьВоВременноеХранилище(Запрос.ПолучитьТелоКакДвоичныеДанные(), Новый УникальныйИдентификатор);
ФайлИнфо = Новый Файл(ИмяФайла);
ПараметрыФайла = РаботаСФайлами.ПараметрыДобавленияФайла();
ПараметрыФайла.ВладелецФайлов = ДокументСсылка;
ПараметрыФайла.ИмяБезРасширения = ФайлИнфо.ИмяБезРасширения;
ПараметрыФайла.РасширениеБезТочки = СтрЗаменить(ФайлИнфо.Расширение, ".", "");
РаботаСФайлами.ДобавитьФайл(ПараметрыФайла, Адрес);
УдалитьИзВременногоХранилища(Адрес);
Возврат Новый HTTPСервисОтвет(201);
КонецФункции
Если файл приходит внутри JSON строкой Base64, превратите её в данные через Base64Значение. Учтите, что Base64 раздувает объём примерно на треть: для файлов в десятки мегабайт лучше передавать байты телом запроса.
HTTP-сервис публикуется на веб-сервере и работает под пользователем 1С, от имени которого идёт подключение. Дайте этому пользователю права только на нужные справочники присоединённых файлов, а не полные права.
Версии и история изменений файла
Присоединённый файл документа в типовых конфигурациях хранит одну актуальную копию. Если вызвать ДобавитьФайл повторно с тем же именем, появится второй элемент, а не новая версия первого.
Варианты, если нужна история:
- Обновить существующий файл. В БСП есть процедура
РаботаСФайлами.ОбновитьФайл, она заменяет данные элемента. Старое содержимое при этом не сохраняется. - Хранить файл в справочнике «Файлы». Там поддерживаются версии: каждая правка создаёт элемент справочника версий, видно автора и дату.
- Вести историю самостоятельно. Новый элемент на каждую редакцию, в описании — номер редакции, а старые помечать на удаление.
Чтобы найти уже прикреплённые к документу файлы, используйте РаботаСФайлами.ЗаполнитьПрисоединенныеФайлыКОбъекту(Владелец, Массив) или запрос к справочнику присоединённых файлов с отбором по ВладелецФайла. Двоичные данные конкретного файла возвращает РаботаСФайлами.ДвоичныеДанныеФайла(ПрисоединенныйФайл).
Прежде чем заменять файл, проверьте, не подписан ли он ЭП. Подписанный документ после замены перестанет соответствовать подписи, и это не всегда заметно пользователю.
Производительность и очистка хранилища
Прикрепление одного файла — доли секунды. Проблемы начинаются на потоке: импорт архива сканов, ночная загрузка из почты, интеграция с маркетплейсом.
Что ускоряет и разгружает базу:
- ⚙️ Тома на диске. Таблица двоичных данных перестаёт расти, бэкап базы становится в разы меньше. Сами тома при этом нужно копировать отдельно.
- ⏱ Фоновые задания. Пакетную загрузку запускайте в фоне, а не в форме: пользователь не ждёт, сеанс не падает по таймауту.
- 🔁 Без внешних запросов в транзакции. Скачивание по HTTP делайте до
НачатьТранзакцию(). Иначе блокировки держатся, пока ждёте ответ сервера. - 🧹 Сразу освобождайте временное хранилище. Особенно в циклах.
- 🗜 Не сжимайте сжатое. PDF, JPG и архивы через
СжатиеДанных(9)гонять бессмысленно.
Удаление ненужных файлов
Файлы, помеченные на удаление, физически исчезают только после штатного удаления помеченных объектов. Для томов в БСП есть регламентное задание очистки ненужных файлов и проверка целостности томов: она находит «осиротевшие» файлы на диске и записи без файлов.
Перед массовой чисткой учтите:
- Сделайте резервную копию базы и каталога томов. Удалённый файл из тома не вернуть без бэкапа.
- Удаление помеченных объектов в рабочее время замедляет работу пользователей. Запускайте его вечером или в выходные.
- Удаляйте файл программно через пометку
УстановитьПометкуУдаления(Истина), а не прямымУдалить(), если в базе включён контроль ссылочной целостности.
Частые вопросы
Можно ли прикрепить файл к документу, который ещё не записан?
Как прикрепить файл в фоновом задании без формы?
ПоместитьВоВременноеХранилище с новым уникальным идентификатором и вызывайте РаботаСФайлами.ДобавитьФайл в серверном коде фонового задания. Автором файла станет пользователь, под которым запущено задание.Почему прикреплённый файл не виден в форме документа?
ВладелецФайла у созданного элемента.Есть ли ограничение на размер прикрепляемого файла?
ДобавитьФайл вызовет исключение, поэтому проверяйте ДвоичныеДанные.Размер() заранее.Как прикрепить файл в типовой конфигурации, не снимая её с поддержки?
РаботаСФайлами доступен из обоих вариантов.