При разработке и сопровождении старых или сильно кастомизированных конфигураций 1С (например, БГУ 1.0, УТ 10.3) часто возникает задача прикрепления внешних файлов и сканов к документам. В современных конфигурациях этот функционал «из коробки» закрывается внедрением Библиотеки стандартных подсистем (БСП). Но что делать, если БСП в вашей базе отсутствует, а пользователям жизненно необходимо прикреплять сканы счетов или накладных к документу ПриходТоваров? Для автоматизации этого процесса можно использовать решение для штрихкодирования и автоматической загрузки сканов документов.
В рамках этой статьи мы подробно разберем, как спроектировать и написать подсистему хранения файлов с нуля. Мы проанализируем два полярных подхода: хранение бинарных данных непосредственно в базе данных 1С и хранение файлов во внешнем томе на диске.
Перед тем как перейти к написанию кода, мы должны четко взвесить все преимущества и недостатки обоих вариантов хранения файлов. От этого выбора зависит производительность всей информационной системы в будущем.
Вариант 1. Хранение файлов внутри базы данных (СУБД)
При таком подходе файлы преобразуются в двоичные данные и записываются в специальное поле таблицы СУБД с типом ХранилищеЗначения.
Преимущества:
Недостатки:
.1CD, где есть жесткие ограничения на размер одной таблицы).Вариант 2. Хранение файлов во внешнем каталоге (на диске сервера)
В этом случае файлы физически копируются в выделенную сетевую папку на сервере, а в базе данных 1С фиксируется только текстовая ссылка (путь) на этот файл.
Преимущества:
Недостатки:
Проанализировав обе стороны, мы выводим золотое правило: если база файловая, пользователей мало (до 5-10 человек) и суммарный объем файлов не превысит пары гигабайт — допустимо хранение в базе. Во всех остальных случаях (клиент-серверный вариант, активный документооборот) настоятельно рекомендуется использовать внешнее хранилище.
---
Если мы приняли решение хранить сканы в СУБД, ни в коем случае нельзя добавлять реквизит с типом ХранилищеЗначения непосредственно в сам документ ПриходТоваров. Это приведет к тому, что при выборке документов или открытии списков система будет пытаться прочитать тяжелые данные файлов, что вызовет сильные зависания интерфейса. Мы должны спроектировать отдельный подчиненный справочник.
Создадим новый справочник в конфигураторе со следующими свойствами:
ПриходТоваровПрисоединенныеФайлыДокументСсылка.ПриходТоваровИмяФайла (Тип: Строка, Длина: 150) — для хранения исходного имени файла.Расширение (Тип: Строка, Длина: 10) — для корректного открытия файла операционной системой.ФайлДанные (Тип: ХранилищеЗначения) — реквизит для хранения самого двоичного содержимого.Разберем по шагам, как программно поместить файл в базу данных. Для этого нам понадобится использовать механизм сжатия данных, чтобы минимизировать нагрузку на СУБД. Напишем процедуру записи файла:
Процедура ЗаписатьФайлВБазу(ДокументСсылка, ПолныйПутьКФайлу) Экспорт
ФайлНаДиске = Новый Файл(ПолныйПутьКФайлу);
Если Не ФайлНаДиске.Существует() Тогда
ВызватьИсключение "Указанный файл не найден на диске!";
КонецЕсли;
// Получаем двоичные данные файла
ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПолныйПутьКФайлу);
// Помещаем данные в ХранилищеЗначения с максимальным сжатием (уровень 9)
ХранилищеФайла = Новый ХранилищеЗначения(ДвоичныеДанныеФайла, Новый СжатиеДанных(9));
// Создаем запись в нашем подчиненном справочнике
НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
НовыйФайл.Владелец = ДокументСсылка;
НовыйФайл.Наименование = ФайлНаДиске.ИмяБезРасширения;
НовыйФайл.ИмяФайла = ФайлНаДиске.Имя;
НовыйФайл.Расширение = Сред(ФайлНаДиске.Расширение, 2); // убираем точку из расширения
НовыйФайл.ФайлДанные = ХранилищеФайла;
Попытка
НовыйФайл.Записать();
Сообщить("Файл успешно прикреплен к документу!");
Исключение
ЗаписьЖурналаРегистрации("УправлениеФайлами", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
ВызватьИсключение "Не удалось сохранить файл в базу данных.";
КонецПопытки;
КонецПроцедуры
Для обратного процесса — извлечения файла из базы данных и открытия его пользователю — нам необходимо выгрузить бинарные данные из СУБД во временный каталог на компьютере пользователя и запустить файл средствами операционной системы. Рассмотрим этот код:
Процедура ОткрытьФайлИзБазы(СсылкаНаФайл) Экспорт
// Получаем двоичные данные из хранилища значения
ДвоичныеДанныеФайла = СсылкаНаФайл.ФайлДанные.Получить();
Если ДвоичныеДанныеФайла = Неопределено Тогда
Предупреждение("Файл поврежден или не содержит данных!");
Возврат;
КонецЕсли;
// Формируем уникальный путь во временном каталоге пользователя
ИмяВременногоФайла = ПолучитьИмяВременногоФайла(СсылкаНаФайл.Расширение);
Попытка
// Записываем файл на локальный диск
ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
// Запускаем файл ассоциированным в Windows приложением
ЗапуститьПриложение(ИмяВременногоФайла);
Исключение
ВызватьИсключение "Не удалось открыть файл: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
---
Если мы выбрали путь внешнего хранения, нам необходимо разработать надежную архитектуру папок, которая исключит конфликты имен и не снизит скорость работы операционной системы на сервере.
Разберем три важнейших правила организации дискового архива:
D:\Shares\1C_Files\scan.pdf. Если завтра ваш сервер переименуют или вы решите перенести файлы на другой диск, все ссылки станут недействительными. Храните в константе базовый путь (например, \\Server1C\FilesShare\), а в справочнике файлов — только относительный путь внутри этого каталога.Год\Месяц\День\).скан.pdf, 1.jpg). Чтобы файлы не перезаписывали друг друга на сервере, при сохранении переименовывайте их в уникальное имя, генерируемое на основе GUID объекта 1С, сохраняя оригинальное «человеческое» имя только в реквизитах справочника.Спроектируем справочник ПриходТоваровПрисоединенныеФайлы под данный вариант работы. Нам понадобятся реквизиты:
ИмяФайла (Строка) — оригинальное имя файла.ОтносительныйПуть (Строка) — путь к файлу от базовой папки (например: 2026\03\30\a4fb23-76cf.pdf).Рассмотрим по шагам программный алгоритм сохранения файла на диск с распределением по датам и генерацией уникального имени:
Функция ПолучитьБазовыйКаталогФайлов()
// В реальной системе это значение должно считываться из константы
Возврат "\\ServerApp1C\StorageFiles1C\";
КонецФункции
Процедура СохранитьФайлНаДиске(ДокументСсылка, ИсходныйПутьКФайлу) Экспорт
БазовыйПуть = ПолучитьБазовыйКаталогФайлов();
ФайлИсточник = Новый Файл(ИсходныйПутьКФайлу);
Если Не ФайлИсточник.Существует() Тогда
ВызватьИсключение "Файл-источник не существует!";
КонецЕсли;
// Строим структуру папок по текущей дате
ТекущаяДатаДок = ТекущаяДата();
ОтносительныйКаталог = Формат(ТекущаяДатаДок, "ДФ=yyyy") + "\"
+ Формат(ТекущаяДатаДок, "ДФ=MM") + "\"
+ Формат(ТекущаяДатаДок, "ДФ=dd") + "\";
ПолныйКаталогНазначения = БазовыйПуть + ОтносительныйКаталог;
// Создаем каталог на диске, если он еще не существует
СоздатьКаталог(ПолныйКаталогНазначения);
// Генерируем уникальное имя файла с сохранением расширения
УникальноеИмяФайла = Строка(Новый УникальныйИдентификатор) + ФайлИсточник.Расширение;
ПолныйПутьНазначения = ПолныйКаталогНазначения + УникальноеИмяФайла;
ОтносительныйПутьКФайлу = ОтносительныйКаталог + УникальноеИмяФайла;
Попытка
// Копируем файл из исходного места в наше хранилище
КопироватьФайл(ИсходныйПутьКФайлу, ПолныйПутьНазначения);
// Фиксируем информацию в базе данных 1С
НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
НовыйФайл.Владелец = ДокументСсылка;
НовыйФайл.Наименование = ФайлИсточник.ИмяБезРасширения;
НовыйФайл.ИмяФайла = ФайлИсточник.Имя;
НовыйФайл.ОтносительныйПуть = ОтносительныйПутьКФайлу;
НовыйФайл.Записать();
Сообщить("Файл успешно перемещен в архив и привязан к документу!");
Исключение
ВызватьИсключение "Ошибка копирования файла на сервер: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Для открытия файла, сохраненного на диске, нам достаточно собрать полный путь из базового каталога и относительного пути, хранящегося в карточке нашего файла:
Процедура ОткрытьФайлСДиска(СсылкаНаФайл) Экспорт
БазовыйПуть = ПолучитьБазовыйКаталогФайлов();
ПолныйПутьКФайлу = БазовыйПуть + СсылкаНаФайл.ОтносительныйПуть;
ФайлНаСервере = Новый Файл(ПолныйПутьКФайлу);
Если Не ФайлНаСервере.Существует() Тогда
Предупреждение("Файл не найден в сетевом архиве! Возможно, он был удален вручную.");
Возврат;
КонецЕсли;
Попытка
ЗапуститьПриложение(ПолныйПутьКФайлу);
Исключение
ВызватьИсключение "Не удалось запустить файл: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
---
Если мы используем хранение файлов на диске, у нас возникает серьезная уязвимость: пользователи могут зайти по сети в общую папку и случайно стереть или модифицировать файлы сканов. Чтобы этого не произошло, мы должны настроить систему безопасности по всем правилам системного администрирования.
Настроим разграничение доступа следующим образом:
StorageFiles1C на уровне операционной системы Windows (или Linux-сервера). Они вообще не должны видеть эту сетевую папку в проводнике.USR1CV8 или аналогичная учетная запись службы).В такой конфигурации обычный пользователь физически не может повлиять на файлы на диске. Когда пользователь нажимает кнопку «Открыть файл» в интерфейсе 1С, клиентское приложение шлет запрос на сервер 1С. Серверная процедура, запущенная от имени привилегированного пользователя USR1CV8, забирает файл из закрытого хранилища, временно помещает его в веб-пространство или передает на клиентскую машину во временный каталог пользователя и только после этого открывает.
---
Если ваша конфигурация БГУ 1.0 или любая другая самописная база запускается через тонкий клиент или в браузере (веб-клиент), то описанные выше простые методы работы с файлами напрямую использовать не получится. Браузер из соображений безопасности блокирует прямой доступ к дискам сервера или локального компьютера пользователя. Все операции должны проходить асинхронно через временное хранилище 1С. Для разбора нюансов клиент-серверного взаимодействия подойдёт руководство по особенностям работы с файлами в управляемых формах 1С 8.3.
Разберем, как реализовать интерактивный выбор и отправку файла с компьютера пользователя на сервер 1С в управляемом приложении. Для этого мы будем использовать асинхронные методы:
&НаКлиенте
Процедура ПрикрепитьФайлУправляемаяФорма(Команда)
// Описываем параметры диалога выбора файлов
ПараметрыДиалога = Новый ПараметрыДиалогаВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ПараметрыДиалога.Фильтр = "Все файлы (*.*)|*.*|Изображения (*.jpg;*.png)|*.jpg;*.png|Документы PDF (*.pdf)|*.pdf";
ПараметрыДиалога.МножественныйВыбор = Ложь;
ПараметрыДиалога.Заголовок = "Выберите скан-копию для загрузки";
// Начинаем интерактивный выбор файла
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораФайла", ЭтотОбъект);
НачатьВыборФайлов(ОписаниеОповещения, ПараметрыДиалога);
КонецПроцедуры
&НаКлиенте
Процедура ПослеВыбораФайла(ВыбранныеФайлы, ДополнительныеПараметры) Экспорт
Если ВыбранныеФайлы = Неопределено Или ВыбранныеФайлы.Количество() = 0 Тогда
Возврат; // Пользователь передумал выбирать файл
КонецЕсли;
ПутьКФайлу = ВыбранныеФайлы[0];
Файл = Новый Файл(ПутьКФайлу);
// Готовим описание загружаемого файла
ОписаниеПередаваемогоФайла = Новый ОписаниеПередаваемогоФайла(ПутьКФайлу);
МассивФайлов = Новый Массив;
МассивФайлов.Добавить(ОписаниеПередаваемогоФайла);
// Помещаем файл во временное хранилище
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеЗагрузкиФайлаНаСервер", ЭтотОбъект, Файл.Имя);
НачатьПомещениеФайловНаСервер(ОписаниеОповещения,,, МассивФайлов, УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ПослеЗагрузкиФайлаНаСервер(РезультатЗагрузки, ДополнительныеПараметры) Экспорт
ИмяФайла = ДополнительныеПараметры;
Если РезультатЗагрузки = Неопределено Или РезультатЗагрузки.Количество() = 0 Тогда
ПоказатьПредупреждение(,"Не удалось загрузить файл во временное хранилище!");
Возврат;
КонецЕсли;
РезультатФайл = РезультатЗагрузки[0];
Если РезультатФайл.ОшибкаПередачи <> Неопределено Тогда
ПоказатьПредупреждение(,"Ошибка при передаче файла: " + РезультатФайл.ОшибкаПередачи);
Возврат;
КонецЕсли;
// Передаем адрес временного хранилища на сервер для окончательной записи
АдресВоВременномХранилище = РезультатФайл.Хранилище;
СохранитьФайлИзХранилищаНаСервере(АдресВоВременномХранилище, ИмяФайла, Объект.Ссылка);
КонецПроцедуры
На стороне сервера мы получаем двоичные данные из временного хранилища по переданному адресу и сохраняем их либо в СУБД, либо во внешний сетевой каталог по нашему выбору:
&НаСервереБезКонтекста
Процедура СохранитьФайлИзХранилищаНаСервере(АдресХранилища, ИмяФайла, ДокументСсылка)
// Извлекаем двоичные данные из временного адреса
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
ФайлНаДиске = Новый Файл(ИмяФайла);
// Пример записи в базу данных через справочник
НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
НовыйФайл.Владелец = ДокументСсылка;
НовыйФайл.Наименование = ФайлНаДиске.ИмяБезРасширения;
НовыйФайл.ИмяФайла = ИмяФайла;
НовыйФайл.Расширение = Сред(ФайлНаДиске.Расширение, 2);
// Сжимаем данные
НовыйФайл.ФайлДанные = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
Попытка
НовыйФайл.Записать();
УдалитьИзВременногоХранилища(АдресХранилища);
Исключение
ЗаписьЖурналаРегистрации("ФайлыСлужба", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
ВызватьИсключение "Не удалось сохранить прикрепленный файл на сервере.";
КонецПопытки;
КонецПроцедуры
---
Мы детально изучили методы реализации прикрепления файлов в 1С без использования БСП. Подводя итог, дадим финальные рекомендации по выбору архитектуры:
ХранилищеЗначения. Это позволит вам легко делать резервные копии стандартными средствами и избавит от сложной настройки прав доступа к общим папкам. Обязательно используйте сжатие файлов методом СжатиеДанных(9) при записи.Год\Месяц\День). Ограничивайте права доступа к сетевой папке на сервере для обеспечения безопасности данных.НачатьПомещениеФайловНаСервер.