В процессе разработки в среде 1С:Предприятие часто возникает задача хранения файлов (картинок, документов, отчетов) непосредственно в базе данных и их последующей автоматической обработки. Для хранения произвольных бинарных данных используется тип ХранилищеЗначения. Однако при работе с ним начинающие разработчики часто совершают архитектурные ошибки — например, путают адрес временного хранилища на сервере с реальными двоичными данными или некорректно организуют работу в веб-клиенте.
В этой статье мы подробно разберем весь жизненный цикл файла: от правильной асинхронной загрузки клиентом (включая особенности веб-клиента) до извлечения данных на сервере в регламентном задании и отправки их на удаленный FTP-сервер с автоматической очисткой временных ресурсов.
Разберем типичную ошибку, с которой сталкиваются разработчики при записи файлов. Часто в коде можно встретить следующую конструкцию:
АдресВременногоХранилища = ПоместитьВоВременноеХранилище(ДвоичныеДанные);
Объект.РеквизитХранилище = Новый ХранилищеЗначения(АдресВременногоХранилища); // Ошибка!
В чем заключается проблема? В данном случае в базу данных записывается не сам файл, а всего лишь текстовая строка, содержащая навигационную ссылку во временное хранилище (например, "e1cib/tempstorage/..."). Временное хранилище «живет» только в течение сеанса пользователя. Когда регламентное задание запускается в фоновом режиме под другой сессией, оно пытается прочитать данные по этой ссылке, получает значение Неопределено, и вызов метода Записать() приводит к ошибке выполнения.
Чтобы этого избежать, в ХранилищеЗначения необходимо помещать непосредственно сам объект типа ДвоичныеДанные.
---
Современные стандарты разработки 1С (начиная с платформы 8.3.15) требуют полного отказа от синхронных вызовов при работе с файлами на клиенте. Это критически важно для корректной работы конфигурации в веб-клиенте (через браузеры). Рассмотрим правильную схему загрузки с использованием асинхронных методов.
Создадим на форме команду для выбора файла и его последующего сохранения:
&НаКлиенте
Асинх Процедура ЗагрузитьФайл(Команда)
// Настраиваем параметры диалога выбора файла
ПараметрыДиалога = Новый ПараметрыДиалогаПомещенияФайлов("Выберите файл для загрузки", Ложь);
ПараметрыДиалога.Фильтр = "Все файлы (*.*)|*.*";
// Асинхронно выбираем файл и помещаем его во временное хранилище сеанса
РезультатЗагрузки = Ждать ПоместитьФайлНаСерверАсинх(,,, ПараметрыДиалога, УникальныйИдентификатор);
Если РезультатЗагрузки = Неопределено Тогда
Возврат; // Пользователь закрыл диалог выбора
КонецЕсли;
// Передаем адрес во временном хранилище и имя файла на сервер для записи
ЗаписатьФайлВОбъектНаСервере(РезультатЗагрузки.Адрес, РезультатЗагрузки.СсылкаНаФайл.Имя);
КонецПроцедуры
Теперь на сервере извлечем двоичные данные из временного хранилища сеанса и запишем их в реквизит объекта типа ХранилищеЗначения. Обратите внимание: объект ДвоичныеДанные не хранит имя и расширение файла. Чтобы не потерять расширение (например, .png, .pdf, .xlsx), мы должны сохранять оригинальное имя файла в отдельный строковый реквизит базы данных.
&НаСервере
Процедура ЗаписатьФайлВОбъектНаСервере(АдресХранилища, ИмяФайла)
// Извлекаем чистые ДвоичныеДанные из временного хранилища
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
Если ТипЗнч(ДвоичныеДанные) = Тип("ДвоичныеДанные") Тогда
// Преобразуем реквизит формы в прикладной объект базы данных
ТекущийОбъект = РеквизитФормыВЗначение("Объект");
// Помещаем ДвоичныеДанные в ХранилищеЗначения
ТекущийОбъект.ДвоичДанные = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
// Сохраняем имя файла для последующего восстановления расширения
ТекущийОбъект.ИмяФайла = ИмяФайла;
// Записываем объект в базу данных
ТекущийОбъект.Записать();
// Возвращаем измененные данные обратно в форму
ЗначениеВРеквизитФормы(ТекущийОбъект, "Объект");
ПоказатьПредупреждение(, "Файл успешно сохранен в базу данных!");
КонецЕсли;
КонецПроцедуры
---
Регламентные задания выполняются строго на сервере в фоновом сеансе. У них нет интерфейса и доступа к клиентским процедурам. Нам необходимо выбрать элементы справочника (или документа), извлечь данные из реквизита типа ХранилищеЗначения, сформировать временный файл на диске сервера и подготовить его к отправке.
Рассмотрим процедуру, которая будет вызываться регламентным заданием:
Процедура ВыполнитьВыгрузкуФайловНаFTP() Экспорт
// Запрос для выборки элементов, содержащих файлы
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФайлыСправочник.Ссылка КАК Ссылка,
| ФайлыСправочник.ИмяФайла КАК ИмяФайла,
| ФайлыСправочник.ДвоичДанные КАК ДвоичДанные
|ИЗ
| Справочник.ХранилищеФайлов КАК ФайлыСправочник
|ГДЕ
| НЕ ФайлыСправочник.ПометкаУдаления";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Получаем объект ДвоичныеДанные из ХранилищаЗначения
ДвоичныеДанные = Выборка.ДвоичДанные.Получить();
// Если поле пустое, пропускаем итерацию
Если ДвоичныеДанные = Неопределено Тогда
ЗаписьЖурналаРегистрации("Выгрузка на FTP", УровеньЖурналаРегистрации.Предупреждение,,
Выборка.Ссылка, "В элементе отсутствует файл.");
Продолжить;
КонецЕсли;
// Получаем расширение файла из сохраненного ранее имени
ТочкаИндекса = Найти(Выборка.ИмяФайла, ".");
Расширение = ?(ТочкаИндекса > 0, Сред(Выборка.ИмяФайла, ТочкаИндекса), "");
// Создаем уникальный временный файл на сервере с правильным расширением
ПутьКВременномуФайлу = ПолучитьИмяВременногоФайла(Расширение);
Попытка
// Записываем двоичные данные на жесткий диск сервера
ДвоичныеДанные.Записать(ПутьКВременномуФайлу);
// Отправляем файл на FTP-сервер
ОтправитьФайлНаFTP(ПутьКВременномуФайлу, Выборка.ИмяФайла);
Исключение
ЗаписьЖурналаРегистрации("Выгрузка на FTP", УровеньЖурналаРегистрации.Ошибка,,
Выборка.Ссылка, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
// КРИТИЧЕСКИ ВАЖНО: Очищаем временные файлы на диске сервера
Попытка
УдалитьФайлы(ПутьКВременномуФайлу);
Исключение
// Логируем ошибку удаления, если файл оказался заблокирован
КонецПопытки;
КонецЦикла;
КонецПроцедуры
---
Для надежной отправки файлов на FTP-сервер рекомендуется использовать объект FTPСоединение. Начиная с версии платформы 8.3.10, для оптимизации работы с памятью и исключения чрезмерной нагрузки на RAM сервера при передаче больших файлов, запись на FTP реализуется через ФайловыйПоток.
Важно помнить о необходимости закрытия потока чтения перед попыткой удаления временного файла с диска, иначе операционная система заблокирует операцию удаления.
Процедура ОтправитьФайлНаFTP(ПутьКЛокальномуФайлу, ИмяФайлаНаFTP)
СерверFTP = "ftp.yourserver.com";
ПортFTP = 21;
Пользователь = "ftp_user";
Пароль = "secure_password";
// Инициализируем защищенное или обычное FTP-соединение
// Параметр ПассивныйРежим (6-й параметр) рекомендуется устанавливать в Истина
Соединение = Новый FTPСоединение(СерверFTP, ПортFTP, Пользователь, Пароль,, Истина, 30);
// Открываем файловый поток для чтения
ПотокФайла = Новый ФайловыйПоток(ПутьКЛокальномуФайлу, РежимОткрытияФайла.Открыть, ДоступКФайлу.Чтение);
Попытка
// Записываем файл на удаленный FTP-сервер из потока
Соединение.Записать(ИмяФайлаНаFTP, ПотокФайла);
// Обязательно закрываем поток сразу после завершения передачи!
ПотокФайла.Закрыть();
ЗаписьЖурналаРегистрации("Выгрузка на FTP", УровеньЖурналаРегистрации.Информация,,,
"Файл " + ИмяФайлаНаFTP + " успешно отправлен на FTP-сервер.");
Исключение
// Гарантируем закрытие потока в случае возникновения ошибки
ПотокФайла.Закрыть();
ВызватьИсключение; // Передаем ошибку выше для корректного логирования
КонецПопытки;
КонецПроцедуры
---
Мы спроектировали надежный механизм для работы с файлами в 1С. Для того чтобы интеграция работала стабильно, придерживайтесь следующих правил:
ДвоичныеДанные на сервере и упаковывайте его в конструктор Новый ХранилищеЗначения(Данные).ХранилищеЗначения всегда создавайте строковый реквизит для хранения оригинального имени и расширения файла.Асинх / Ждать для совместимости с веб-клиентом.УдалитьФайлы() после их отправки, предварительно закрыв все связанные с ними открытые файловые потоки.