При активной работе пользователей в системе 1С (особенно в конфигурациях для документооборота или учета заявок) база данных начинает стремительно увеличиваться в объемах. Если загружать тяжелые файлы — сканированные копии документов, фотографии, PDF-отчеты — напрямую в базу данных (в реквизиты с типом ХранилищеЗначения), это неизбежно приведет к замедлению работы всей системы, долгому созданию резервных копий и фрагментации индексов СУБД.
Проанализируем простую математику: если 4 специалиста создают по 300 заявок в месяц и к каждой прикрепляют файлы объемом около 2 Мб по 5 различным объектам учета, мы получаем прирост базы данных более чем на 12 Гб ежемесячно. За год размер базы увеличится на 140 Гб только за счет файлов. Логичным и профессиональным решением в данной ситуации является хранение тяжелых двоичных данных за пределами базы 1С — в выделенном сетевом каталоге или S3-хранилище, оставляя в самой системе лишь легкие текстовые ссылки на эти файлы.
В этой статье мы подробно разберем, как с нуля спроектировать и написать надежную, безопасную и быстродействующую систему хранения внешних файлов на платформе 1С 8.3 (для управляемых форм), минуя сложные механизмы Библиотеки стандартных подсистем (БСП).
Для связи объектов конфигурации (документов или справочников) с физическими файлами на сервере можно использовать разные подходы. Некоторые разработчики добавляют табличные части непосредственно к объектам или создают подчиненные справочники. Однако самым гибким и производительным решением является использование непериодического регистра сведений.
Рассмотрим преимущества использования регистра сведений:
Создадим в конфигураторе новый регистр сведений с именем ПрикрепленныеФайлы. Настроим его структуру следующим образом:
Объект: тип ЛюбаяСсылка. Установите свойство "Ведущее" в значение Истина. Это гарантирует, что при удалении основного документа или элемента справочника записи о его файлах в регистре будут очищены автоматически.ИдентификаторФайла: тип Строка(36). Сюда мы будем записывать уникальный идентификатор (GUID), который защитит файлы от совпадения имен на сервере.ИмяФайла: тип Строка(150). Используется для хранения оригинального имени файла с расширением (например, "Договор_поставки.pdf"), которое будет видеть пользователь в интерфейсе.ОтносительныйПуть: тип Строка(250). Позволяет хранить путь к файлу относительно корневого каталога (например, "2026/03/"). Это необходимо для иерархического распределения файлов на диске.Начинающие разработчики часто сталкиваются со следующей ошибкой компиляции: Тип не определен (ДиалогВыбораФайла). Проанализируем причину этой проблемы.
Платформа 1С жестко разделяет контекст выполнения кода на клиентский и серверный. Объект ДиалогВыбораФайла предназначен исключительно для работы с интерфейсом операционной системы пользователя. Он физически не существует на сервере приложений 1С. Если вы попытаетесь вызвать или объявить его в серверном общем модуле, модуле менеджера или в процедуре с директивой &НаСервере, система выдаст ошибку.
Помните золотое правило разработки в 1С: выбор файла всегда происходит на клиенте (в контексте &НаКлиенте), а физическая запись файла на диск и работа с базой данных — на сервере (в контексте &НаСервере или &НаСервереБезКонтекста).
В старых версиях платформы для выбора файла использовался метод Диалог.Выбрать(). В современных управляемых приложениях использование синхронных (модальных) методов запрещено. Нам необходимо использовать асинхронные вызовы, чтобы интерфейс программы не блокировался во время ожидания действий пользователя.
Разберем по шагам, как правильно реализовать выбор файла на клиенте с помощью конструкции Асинх / Ждать, которая поддерживается в актуальных версиях платформы 1С 8.3:
&НаКлиенте
Асинх Процедура ДобавитьФайлВыполнить(Команда)
Диалог = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
Диалог.МножественныйВыбор = Ложь;
Диалог.Заголовок = "Выберите файл для прикрепления";
Диалог.Фильтр = "Документы PDF (*.pdf)|*.pdf|Изображения (*.jpg; *.png)|*.jpg;*.png|Все файлы (*.*)|*.*";
// Используем асинхронный вызов с оператором Ждать
РезультатВыбора = Ждать Диалог.ВыбратьАсинх();
Если РезультатВыбора = Неопределено Тогда
Возврат; // Пользователь закрыл диалог выбора
КонецЕсли;
ПолныйПутьКФайлу = РезультатВыбора[0];
// Передаем файл на обработку
ЗагрузитьФайлНаСервер(ПолныйПутьКФайлу);
КонецПроцедуры
Если предоставить всем пользователям прямой сетевой доступ (права на чтение и запись) к общей папке вида \\Server\Files$\, возникает колоссальная угроза безопасности. В случае заражения одного из рабочих компьютеров вирусом-шифровальщиком, вредоносная программа мгновенно зашифрует весь архив прикрепленных документов компании.
Рассмотрим безопасную схему работы, при которой обычные пользователи вообще не имеют прямого доступа к сетевой папке на уровне операционной системы:
usr1cv8).ПоместитьФайлНаСерверАсинх.usr1cv8) забирает данные из временного хранилища и программно записывает их в закрытый сетевой каталог.Эта архитектура гарантирует абсолютную безопасность файлового архива.
Если складывать сотни тысяч файлов в один каталог на диске, файловая система операционной системы (особенно NTFS) начнет критически тормозить при попытке открыть папку, найти файл или записать новый. Чтобы этого избежать, организуем динамическое распределение файлов по вложенным папкам в зависимости от текущей даты (год и месяц).
Вместо структуры \\Server\Files$\ИмяФайла.pdf мы будем создавать иерархию вида: \\Server\Files$\2026\03\УникальныйИдентификатор.pdf.
Перейдем к практическому кодированию. Нам понадобится общая форма, которую мы сможем вызывать из любого документа или справочника, передавая в нее ссылку на объект-владелец. Назовем форму ФормаПрикрепленныхФайлов. На ней разместим таблицу со списком файлов и команды для их добавления, просмотра и удаления.
Ниже представлен программный код модуля этой управляемой формы:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Получаем ссылку на объект-владелец, переданную в параметрах формы
Параметры.Свойство("ОбъектВладелец", ОбъектВладелец);
Если Не ЗначениеЗаполнено(ОбъектВладелец) Тогда
ВызватьИсключение "Не указан объект-владелец для прикрепления файлов!";
КонецЕсли;
// Заполняем список файлов на форме при открытии
ОбновитьСписокФайловНаСервере();
КонецПроцедуры
&НаСервере
Процедура ОбновитьСписокФайловНаСервере()
// Очищаем таблицу файлов на форме
СписокФайлов.Очистить();
// Выбираем данные из регистра сведений
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПрикрепленныеФайлы.ИдентификаторФайла КАК ИдентификаторФайла,
| ПрикрепленныеФайлы.ИмяФайла КАК ИмяФайла,
| ПрикрепленныеФайлы.ОтносительныйПуть КАК ОтносительныйПуть
|ИЗ
| РегистрСведений.ПрикрепленныеФайлы КАК ПрикрепленныеФайлы
|ГДЕ
| ПрикрепленныеФайлы.Объект = &Объект";
Запрос.УстановитьПараметр("Объект", ОбъектВладелец);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
НоваяСтрока = СписокФайлов.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
КонецЦикла;
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Асинх Процедура ДобавитьФайл(Команда)
Диалог = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
Диалог.Заголовок = "Выберите файл для загрузки";
Диалог.МножественныйВыбор = Ложь;
Результат = Ждать Диалог.ВыбратьАсинх();
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
ПутьКЛокальномуФайлу = Результат[0];
ФайлНаДиске = Новый Файл(ПутьКЛокальномуФайлу);
// Помещаем файл во временное хранилище на сервере асинхронно
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинх(,,, ПутьКЛокальномуФайлу, УникальныйИдентификатор);
Если ОписаниеФайла <> Неопределено И ОписаниеФайла.ПомещениеВыполнено Тогда
// Вызываем серверную процедуру для переноса файла в архив
ИмяФайла = ФайлНаДиске.Имя;
РезультатЗагрузки = СохранитьФайлВАрхивеНаСервере(ОписаниеФайла.Адрес, ИмяФайла);
Если РезультатЗагрузки Тогда
ОбновитьСписокФайловНаСервере();
ПоказатьПредупреждение(, "Файл успешно прикреплен!");
Иначе
ПоказатьПредупреждение(, "Произошла ошибка при сохранении файла на сервере.");
EndЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция СохранитьФайлВАрхивеНаСервере(АдресВХранилище, ОригинальноеИмяФайла)
// Получаем двоичные данные из временного хранилища
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресВХранилище);
Если ДвоичныеДанныеФайла = Неопределено Тогда
Возврат Ложь;
КонецЕсли;
// Определяем пути хранения. В реальной системе путь лучше получать из констант
БазовыйКаталог = "\\Server\Files$\";
// Генерируем относительный путь на основе текущей даты
ТекущаяДатаСервера = ТекущаяДата();
ОтносительныйПуть = Формат(ТекущаяДатаСервера, "ДФ=yyyy") + "\" + Формат(ТекущаяДатаСервера, "ДФ=MM") + "\";
ПолныйКаталогНазначения = БазовыйКаталог + ОтносительныйПуть;
// Проверяем существование каталога, если его нет - создаем
Если Не НайтиФайлы(ПолныйКаталогНазначения).Количество() > 0 Тогда
СоздатьКаталог(ПолныйКаталогНазначения);
КонецЕсли;
// Генерируем уникальное имя файла для предотвращения конфликтов
НовыйGUID = Строка(Новый УникальныйИдентификатор);
РасширениеФайла = Сред(ОригинальноеИмяФайла, Найти(ОригинальноеИмяФайла, "."));
ИмяФайлаНаДиске = НовыйGUID + РасширениеФайла;
ПолныйПутьКФайлу = ПолныйКаталогНазначения + ИмяФайлаНаДиске;
Попытка
// Записываем двоичные данные на диск сервера
ДвоичныеДанныеФайла.Записать(ПолныйПутьКФайлу);
// Записываем информацию о связи файла и документа в регистр сведений
МенеджерЗаписи = РегистрыСведений.ПрикрепленныеФайлы.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ОбъектВладелец;
МенеджерЗаписи.ИдентификаторФайла = НовыйGUID;
МенеджерЗаписи.ИмяФайла = ОригинальноеИмяФайла;
МенеджерЗаписи.ОтносительныйПуть = ОтносительныйПуть;
МенеджерЗаписи.Записать(Истина);
УдалитьИзВременногоХранилища(АдресВХранилище);
Возврат Истина;
Исключение
ЗаписатьЛогОшибки(ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
КонецФункции
Для того чтобы пользователь мог просмотреть прикрепленный файл, мы должны реализовать обратный процесс: сервер считывает файл из закрытой папки, передает его клиенту, а клиент запускает ассоциированное в Windows приложение (например, Adobe Reader или MS Word).
Добавим обработчик открытия файла на форму:
&НаКлиенте
Асинх Процедура ОткрытьФайл(Команда)
ТекущаяСтрока = Элементы.СписокФайлов.ТекущаяСтрока;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
ДанныеСтроки = Элементы.СписокФайлов.ДанныеСтроки(ТекущаяСтрока);
// Получаем файл с сервера во временное хранилище
АдресФайлаВХранилище = ПолучитьФайлИзАрхиваНаСервере(ДанныеСтроки.ИдентификаторФайла);
Если ЗначениеЗаполнено(АдресФайлаВХранилище) Тогда
// Задаем имя файла для сохранения на клиенте
ИмяФайлаДляЗапуска = ПолучитьИмяВременногоФайла(ДанныеСтроки.ИмяФайла);
// Получаем файл из хранилища на клиентский компьютер
ПолученныйФайл = Ждать ПолучитьФайлССервераАсинх(АдресФайлаВХранилище, ИмяФайлаДляЗапуска);
Если ПолученныйФайл <> Неопределено Тогда
// Запускаем просмотр файла средствами операционной системы
ЗапуститьПриложение(ИмяФайлаДляЗапуска);
КонецЕсли;
Иначе
ПоказатьПредупреждение(, "Не удалось получить файл с сервера. Возможно, он был удален.");
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьФайлИзАрхиваНаСервере(ИдентификаторФайла)
// Ищем информацию о файле в регистре
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПрикрепленныеФайлы.ИмяФайла КАК ИмяФайла,
| ПрикрепленныеФайлы.ОтносительныйПуть КАК ОтносительныйПуть
|ИЗ
| РегистрСведений.ПрикрепленныеФайлы КАК ПрикрепленныеФайлы
|ГДЕ
| ПрикрепленныеФайлы.ИдентификаторФайла = &ИдентификаторФайла";
Запрос.УстановитьПараметр("ИдентификаторФайла", ИдентификаторФайла);
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
Возврат "";
КонецЕсли;
Выборка = Результат.Выбрать();
Выборка.Следующий();
БазовыйКаталог = "\\Server\Files$\";
РасширениеФайла = Сред(Выборка.ИмяФайла, Найти(Выборка.ИмяФайла, "."));
ПолныйПутьКФайлу = БазовыйКаталог + Выборка.ОтносительныйПуть + ИдентификаторФайла + РасширениеФайла;
ФайлНаСервере = Новый Файл(ПолныйПутьКФайлу);
Если ФайлНаСервере.Существует() Тогда
// Читаем двоичные данные файла и помещаем в хранилище
ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПолныйПутьКФайлу);
Возврат ПоместитьВоВременноеХранилище(ДвоичныеДанныеФайла);
Иначе
Возврат "";
КонецЕсли;
КонецФункции
При удалении файлов важно не только стирать запись о них из базы данных 1С, но и физически удалять их с диска сервера, чтобы не забивать дисковое пространство "мусорными" файлами. Для комплексной очистки базы от накопленных сведений применяется выборочное удаление организаций со всеми связанными документами и записями регистров.
Рассмотрим процедуру удаления файла из интерфейса:
&НаКлиенте
Асинх Процедура УдалитьФайл(Команда)
ТекущаяСтрока = Элементы.СписокФайлов.ТекущаяСтрока;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
ДанныеСтроки = Элементы.СписокФайлов.ДанныеСтроки(ТекущаяСтрока);
РежимДиалога = РежимДиалогаВопрос.ДаНет;
ТекстВопроса = "Вы действительно хотите безвозвратно удалить файл '" + ДанныеСтроки.ИмяФайла + "'?";
Ответ = Ждать ВопросАсинх(ТекстВопроса, РежимДиалога);
Если Ответ = КодВозвратаДиалога.Да Тогда
УдалениеПрошлоУспешно = УдалитьФайлНаСервере(ДанныеСтроки.ИдентификаторФайла);
Если УдалениеПрошлоУспешно Тогда
ОбновитьСписокФайловНаСервере();
ПоказатьПредупреждение(, "Файл успешно удален!");
Иначе
ПоказатьПредупреждение(, "Не удалось удалить файл. Обратитесь к администратору.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция УдалитьФайлНаСервере(ИдентификаторФайла)
// Получаем путь к файлу перед удалением записи
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПрикрепленныеФайлы.ИмяФайла КАК ИмяФайла,
| ПрикрепленныеФайлы.ОтносительныйПуть КАК ОтносительныйПуть
|ИЗ
| РегистрСведений.ПрикрепленныеФайлы КАК ПрикрепленныеФайлы
|ГДЕ
| ПрикрепленныеФайлы.ИдентификаторФайла = &ИдентификаторФайла";
Запрос.УстановитьПараметр("ИдентификаторФайла", ИдентификаторФайла);
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
Возврат Ложь;
КонецЕсли;
Выборка = Результат.Выбрать();
Выборка.Следующий();
БазовыйКаталог = "\\Server\Files$\";
РасширениеФайла = Сред(Выборка.ИмяФайла, Найти(Выборка.ИмяФайла, "."));
ПолныйПутьКФайлу = БазовыйКаталог + Выборка.ОтносительныйПуть + ИдентификаторФайла + РасширениеФайла;
Попытка
// Физическое удаление файла с диска сервера
ФайлНаСервере = Новый Файл(ПолныйПутьКФайлу);
Если ФайлНаСервере.Существует() Тогда
УдалитьФайлы(ПолныйПутьКФайлу);
КонецЕсли;
// Удаление записи из регистра сведений 1С
МенеджерЗаписи = РегистрыСведений.ПрикрепленныеФайлы.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ОбъектВладелец;
МенеджерЗаписи.ИдентификаторФайла = ИдентификаторФайла;
МенеджерЗаписи.Удалить();
Возврат Истина;
Исключение
ЗаписатьЛогОшибки(ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
КонецФункции
Хранение файлов в сетевой папке Windows (через протокол SMB) — это отличный классический вариант для локальных сетей. Однако в современных реалиях у него есть несколько существенных недостатков:
Проанализируем современные альтернативные способы хранения файлов, которые легко интегрировать в 1С:
Это самый передовой стандарт хранения двоичных данных. MinIO — это бесплатный софт с открытым исходным кодом, который системный администратор может за пару минут развернуть на любом локальном сервере Linux в вашей компании. Обмен файлами с 1С происходит по стандартному протоколу HTTP/HTTPS при помощи объекта HTTPСоединение.
Преимущества очевидны: высокая надежность, легкое масштабирование, доступ из любой точки мира без VPN и полная совместимость с любой операционной системой сервера 1С.
Протокол WebDAV позволяет работать с файлами на удаленном веб-сервере (например, Nextcloud или Яндекс.Диск) как с локальной файловой системой. В 1С отправка и получение файлов по WebDAV также реализуются через HTTP-запросы. Аналогичным способом можно настроить автоматический сбор прайс-листов с почты, FTP и HTTP для их последующей обработки в системе.
Таким образом, созданный нами сегодня каркас системы хранения файлов на базе регистра сведений 1С является универсальным фундаментом. Независимо от того, куда вы решите сохранять файлы в будущем — в сетевую папку Windows, локальный каталог Linux сервера или в защищенное облако S3 — структура метаданных внутри вашей конфигурации 1С останется неизменной.