Как получить файл из Хранилища значения и отправить его на FTP в регламентном задании 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки в среде 1С:Предприятие часто возникает задача хранения файлов (картинок, документов, отчетов) непосредственно в базе данных и их последующей автоматической обработки. Для хранения произвольных бинарных данных используется тип ХранилищеЗначения. Однако при работе с ним начинающие разработчики часто совершают архитектурные ошибки — например, путают адрес временного хранилища на сервере с реальными двоичными данными или некорректно организуют работу в веб-клиенте.

В этой статье мы подробно разберем весь жизненный цикл файла: от правильной асинхронной загрузки клиентом (включая особенности веб-клиента) до извлечения данных на сервере в регламентном задании и отправки их на удаленный FTP-сервер с автоматической очисткой временных ресурсов.

Типичная ошибка: почему в Хранилище значения оказывается «битый» файл?

Разберем типичную ошибку, с которой сталкиваются разработчики при записи файлов. Часто в коде можно встретить следующую конструкцию:


АдресВременногоХранилища = ПоместитьВоВременноеХранилище(ДвоичныеДанные);
Объект.РеквизитХранилище = Новый ХранилищеЗначения(АдресВременногоХранилища); // Ошибка!

В чем заключается проблема? В данном случае в базу данных записывается не сам файл, а всего лишь текстовая строка, содержащая навигационную ссылку во временное хранилище (например, "e1cib/tempstorage/..."). Временное хранилище «живет» только в течение сеанса пользователя. Когда регламентное задание запускается в фоновом режиме под другой сессией, оно пытается прочитать данные по этой ссылке, получает значение Неопределено, и вызов метода Записать() приводит к ошибке выполнения.

Чтобы этого избежать, в ХранилищеЗначения необходимо помещать непосредственно сам объект типа ДвоичныеДанные.

---

Шаг 1. Правильная загрузка файла в ХранилищеЗначения на клиенте и сервере

Современные стандарты разработки 1С (начиная с платформы 8.3.15) требуют полного отказа от синхронных вызовов при работе с файлами на клиенте. Это критически важно для корректной работы конфигурации в веб-клиенте (через браузеры). Рассмотрим правильную схему загрузки с использованием асинхронных методов.

Создадим на форме команду для выбора файла и его последующего сохранения:


&НаКлиенте
Асинх Процедура ЗагрузитьФайл(Команда)
    
    // Настраиваем параметры диалога выбора файла
    ПараметрыДиалога = Новый ПараметрыДиалогаПомещенияФайлов("Выберите файл для загрузки", Ложь);
    ПараметрыДиалога.Фильтр = "Все файлы (*.*)|*.*";
    
    // Асинхронно выбираем файл и помещаем его во временное хранилище сеанса
    РезультатЗагрузки = Ждать ПоместитьФайлНаСерверАсинх(,,, ПараметрыДиалога, УникальныйИдентификатор);
    
    Если РезультатЗагрузки = Неопределено Тогда
        Возврат; // Пользователь закрыл диалог выбора
    КонецЕсли;
    
    // Передаем адрес во временном хранилище и имя файла на сервер для записи
    ЗаписатьФайлВОбъектНаСервере(РезультатЗагрузки.Адрес, РезультатЗагрузки.СсылкаНаФайл.Имя);
    
КонецПроцедуры

Теперь на сервере извлечем двоичные данные из временного хранилища сеанса и запишем их в реквизит объекта типа ХранилищеЗначения. Обратите внимание: объект ДвоичныеДанные не хранит имя и расширение файла. Чтобы не потерять расширение (например, .png, .pdf, .xlsx), мы должны сохранять оригинальное имя файла в отдельный строковый реквизит базы данных.


&НаСервере
Процедура ЗаписатьФайлВОбъектНаСервере(АдресХранилища, ИмяФайла)
    
    // Извлекаем чистые ДвоичныеДанные из временного хранилища
    ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
    
    Если ТипЗнч(ДвоичныеДанные) = Тип("ДвоичныеДанные") Тогда
        
        // Преобразуем реквизит формы в прикладной объект базы данных
        ТекущийОбъект = РеквизитФормыВЗначение("Объект");
        
        // Помещаем ДвоичныеДанные в ХранилищеЗначения
        ТекущийОбъект.ДвоичДанные = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
        
        // Сохраняем имя файла для последующего восстановления расширения
        ТекущийОбъект.ИмяФайла = ИмяФайла;
        
        // Записываем объект в базу данных
        ТекущийОбъект.Записать();
        
        // Возвращаем измененные данные обратно в форму
        ЗначениеВРеквизитФормы(ТекущийОбъект, "Объект");
        
        ПоказатьПредупреждение(, "Файл успешно сохранен в базу данных!");
        
    КонецЕсли;
    
КонецПроцедуры

---

Шаг 2. Получение файла из базы данных в регламентном задании

Регламентные задания выполняются строго на сервере в фоновом сеансе. У них нет интерфейса и доступа к клиентским процедурам. Нам необходимо выбрать элементы справочника (или документа), извлечь данные из реквизита типа ХранилищеЗначения, сформировать временный файл на диске сервера и подготовить его к отправке.

Рассмотрим процедуру, которая будет вызываться регламентным заданием:


Процедура ВыполнитьВыгрузкуФайловНаFTP() Экспорт
    
    // Запрос для выборки элементов, содержащих файлы
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ФайлыСправочник.Ссылка КАК Ссылка,
        |   ФайлыСправочник.ИмяФайла КАК ИмяФайла,
        |   ФайлыСправочник.ДвоичДанные КАК ДвоичДанные
        |ИЗ
        |   Справочник.ХранилищеФайлов КАК ФайлыСправочник
        |ГДЕ
        |   НЕ ФайлыСправочник.ПометкаУдаления";
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Получаем объект ДвоичныеДанные из ХранилищаЗначения
        ДвоичныеДанные = Выборка.ДвоичДанные.Получить();
        
        // Если поле пустое, пропускаем итерацию
        Если ДвоичныеДанные = Неопределено Тогда
            ЗаписьЖурналаРегистрации("Выгрузка на FTP", УровеньЖурналаРегистрации.Предупреждение,, 
                Выборка.Ссылка, "В элементе отсутствует файл.");
            Продолжить;
        КонецЕсли;
        
        // Получаем расширение файла из сохраненного ранее имени
        ТочкаИндекса = Найти(Выборка.ИмяФайла, ".");
        Расширение = ?(ТочкаИндекса > 0, Сред(Выборка.ИмяФайла, ТочкаИндекса), "");
        
        // Создаем уникальный временный файл на сервере с правильным расширением
        ПутьКВременномуФайлу = ПолучитьИмяВременногоФайла(Расширение);
        
        Попытка
            // Записываем двоичные данные на жесткий диск сервера
            ДвоичныеДанные.Записать(ПутьКВременномуФайлу);
            
            // Отправляем файл на FTP-сервер
            ОтправитьФайлНаFTP(ПутьКВременномуФайлу, Выборка.ИмяФайла);
            
        Исключение
            ЗаписьЖурналаРегистрации("Выгрузка на FTP", УровеньЖурналаРегистрации.Ошибка,, 
                Выборка.Ссылка, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        КонецПопытки;
        
        // КРИТИЧЕСКИ ВАЖНО: Очищаем временные файлы на диске сервера
        Попытка
            УдалитьФайлы(ПутьКВременномуФайлу);
        Исключение
            // Логируем ошибку удаления, если файл оказался заблокирован
        КонецПопытки;
        
    КонецЦикла;
    
КонецПроцедуры

---

Шаг 3. Запись файла на 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С. Для того чтобы интеграция работала стабильно, придерживайтесь следующих правил:

  1. Не сохраняйте адреса временного хранилища в базу данных. Всегда извлекайте объект ДвоичныеДанные на сервере и упаковывайте его в конструктор Новый ХранилищеЗначения(Данные).
  2. Сохраняйте метаданные файлов отдельно. Рядом с реквизитом типа ХранилищеЗначения всегда создавайте строковый реквизит для хранения оригинального имени и расширения файла.
  3. Используйте асинхронность. Код работы с файлами на стороне клиента должен использовать методы Асинх / Ждать для совместимости с веб-клиентом.
  4. Освобождайте ресурсы сервера. Всегда удаляйте временные файлы методом УдалитьФайлы() после их отправки, предварительно закрыв все связанные с ними открытые файловые потоки.
← На главную