При организации интеграционных процессов и обмена данными между 1С и внешними системами часто используется протокол FTP. Одной из типичных задач является перенос обработанных файлов (например, файлов заказов, накладных или каталогов товаров в формате XML) из одной папки на сервере в другую — например, из папки «Входящие» в архивную директорию. Однако при повторном выполнении этой операции разработчики регулярно сталкиваются с неприятной ошибкой: «Ошибка работы с Интернет: ftp://мойадрес Quote command returned error».
В этой статье мы подробно разберем, почему возникает данная проблема, проанализируем подводные камни стандартных методов платформы 1С и рассмотрим несколько надежных способов перемещения файлов с автоматической перезаписью, которые предотвратят потерю важных данных.
Рассмотрим исходный программный код, который часто используют для решения задачи перемещения:
СоединениеFTP = ПодключитьсяКFTPСерверу();
СоединениеFTP.УстановитьТекущийКаталог("/otkuda");
ФайлыXML = СоединениеFTP.НайтиФайлы(СоединениеFTP.ТекущийКаталог(), "*.xml");
Для Каждого Файл Из ФайлыXML Цикл
СоединениеFTP.Переместить("/otkuda/" + Файл.Имя, "/kuda/" + Файл.Имя);
КонецЦикла;
В первый раз этот код отрабатывает безупречно. Но при повторном запуске, когда файлы с такими же именами уже присутствуют в целевом каталоге /kuda/, система аварийно завершает работу.
Дело в том, что метод Переместить() объекта FTPСоединение на уровне сетевых запросов транслируется в стандартные команды протокола FTP: RNFR (Rename From — переименовать из) и RNTO (Rename To — переименовать в). То есть физически файл не скачивается и не загружается заново, сервер просто меняет его путь (переименовывает).
Согласно спецификации протокола FTP (RFC 959), если клиент пытается переименовать файл, указав имя уже существующего в целевой папке файла, поведение сервера зависит от его внутренней конфигурации и операционной системы:
550 File already exists.В процессе обсуждения проблемы на форумах часто предлагают «быстрое» решение — обернуть перемещение в конструкцию Попытка...Исключение и в случае ошибки удалять файл из источника:
Для Каждого Файл Из ФайлыXML Цикл
Попытка
СоединениеFTP.Переместить("/otkuda/" + Файл.Имя, "/kuda/" + Файл.Имя);
Исключение
СоединениеFTP.Удалить("/otkuda/", Файл.Имя); // КРАЙНЕ ОПАСНО!
КонецПопытки;
КонецЦикла;
Внимание! Это критически опасный код, который приведет к безвозвратной потере данных! Проанализируем, что произойдет при его выполнении:
/otkuda/./kuda/ лежит старая, уже обработанная версия файла с таким же именем.Переместить() завершается ошибкой, так как файл в приемнике уже есть.Исключение, в котором программа просто удаляет новый (!) файл из папки /otkuda/.Теперь рассмотрим правильные, безопасные и эффективные способы решения этой задачи.
Самый надежный способ при работе через объект FTPСоединение — перед вызовом метода Переместить() проверить, существует ли файл в целевом каталоге. Если он там есть, его необходимо предварительно удалить средствами 1С, и только после этого выполнять перемещение. Это гарантирует, что актуальный файл из источника займет свое место в приемнике.
Реализуем этот алгоритм на практике:
СоединениеFTP = ПодключитьсяКFTPСерверу();
// Получаем список файлов в исходном каталоге
ФайлыXML = СоединениеFTP.НайтиФайлы("/otkuda", "*.xml");
Для Каждого Файл Из ФайлыXML Цикл
ИмяФайла = Файл.Имя;
ПутьИсточник = "/otkuda/" + ИмяФайла;
ПутьПриемник = "/kuda/" + ИмяФайла;
// Проверяем наличие точно такого же файла в каталоге-получателе
ФайлыВПриемнике = СоединениеFTP.НайтиФайлы("/kuda", ИмяФайла);
Если ФайлыВПриемнике.Количество() > 0 Тогда
Попытка
// Удаляем старый файл в приемнике, чтобы освободить место
СоединениеFTP.Удалить("/kuda", ИмяФайла);
Исключение
ЗаписьЖурналаРегистрации("FTP.ОшибкаУдаления",
УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось удалить старый файл в приемнике: " + ИмяФайла + ". Ошибка: " + ОписаниеОшибки());
Продолжить; // Переходим к следующему файлу, этот переместить не сможем
КонецПопытки;
КонецЕсли;
// Теперь перемещение гарантированно выполнится без ошибок перезаписи
Попытка
СоединениеFTP.Переместить(ПутьИсточник, ПутьПриемник);
Исключение
ЗаписьЖурналаРегистрации("FTP.ОшибкаПеремещения",
УровеньЖурналаРегистрации.Ошибка, , ,
"Не удалось переместить файл: " + ИмяФайла + ". Ошибка: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Данный подход хорош тем, что все операции происходят непосредственно на FTP-сервере без реального скачивания файлов на локальный диск компьютера или сервера 1С, что экономит интернет-трафик и время.
Мало кто из начинающих разработчиков знает, что глобальный метод платформы КопироватьФайл() умеет работать не только с локальными путями, но и с сетевыми протоколами, включая FTP. При этом на уровне протокола отправка файла через эту команду инициирует инструкцию перезаписи (команда STOR), которая по умолчанию разрешена на большинстве серверов.
Схема этого решения выглядит так: мы копируем файл напрямую из одного сетевого пути в другой (при этом старый файл автоматически перезаписывается), а затем удаляем оригинал из папки-источника.
Рассмотрим, как правильно составить код:
// Настройки подключения к серверу
ПараметрыFTP = "user:password@00.000.00.00:21"; // Шаблон: логин:пароль@адрес:порт
ФайлыXML = СоединениеFTP.НайтиФайлы("/otkuda", "*.xml");
Для Каждого Файл Из ФайлыXML Цикл
// Формируем полные URL-пути для функции КопироватьФайл
ПолныйПутьИсточник = "ftp://" + ПараметрыFTP + "/otkuda/" + Файл.Имя;
ПолныйПутьПриемник = "ftp://" + ПараметрыFTP + "/kuda/" + Файл.Имя;
Попытка
// Функция КопироватьФайл выполнит перезапись целевого файла автоматически
КопироватьФайл(ПолныйПутьИсточник, ПолныйПутьПриемник);
// После успешного копирования удаляем исходный файл
СоединениеFTP.Удалить("/otkuda", Файл.Имя);
Исключение
ЗаписьЖурналаРегистрации("FTP.Копирование",
УровеньЖурналаРегистрации.Ошибка, , ,
"Ошибка при копировании файла " + Файл.Имя + ": " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Важное примечание безопасности: При формировании строки подключения пароль пользователя передается в открытом виде внутри URL-адреса. Если логи ведения вашей системы записывают полные пути вызовов функций, это может стать уязвимостью. Используйте этот метод аккуратно.
Если файлы небольшие, или логика интеграции подразумевает, что перед перемещением в архив файл должен быть обязательно прочитан платформой 1С, наиболее безопасным решением будет промежуточное скачивание во временный каталог на сервере 1С.
Проанализируем этот сценарий по шагам:
Получить()./kuda/ с помощью метода Записать(). Данный метод по умолчанию перезаписывает существующие файлы./otkuda/ и локальный временный файл.Реализуем этот надежный алгоритм:
СоединениеFTP = ПодключитьсяКFTPСерверу();
ФайлыXML = СоединениеFTP.НайтиФайлы("/otkuda", "*.xml");
Для Каждого Файл Из ФайлыXML Цикл
// Создаем уникальное имя временного файла на локальном диске
ВременныйФайл = ПолучитьИмяВременногоФайла(Файл.Расширение);
Попытка
// Шаг 1. Скачиваем файл во временное хранилище
СоединениеFTP.Получить("/otkuda/" + Файл.Имя, ВременныйФайл);
// Шаг 2. Записываем файл в папку архива (перезапись произойдет автоматически)
СоединениеFTP.Записать(ВременныйФайл, "/kuda/" + Файл.Имя);
// Шаг 3. Удаляем исходный файл с FTP-сервера
СоединениеFTP.Удалить("/otkuda", Файл.Имя);
// Шаг 4. Очищаем локальный временный файл
УдалитьФайлы(ВременныйФайл);
Исключение
ЗаписьЖурналаРегистрации("FTP.ЛокальныйОбход",
УровеньЖурналаРегистрации.Ошибка, , ,
"Не удалось обработать файл через локальный буфер: " + Файл.Имя + ". Ошибка: " + ОписаниеОшибки());
// Пытаемся удалить временный файл в случае сбоя, чтобы не засорять диск
Попытка
УдалитьФайлы(ВременныйФайл);
Исключение
// Игнорируем ошибку удаления локального файла
КонецПопытки;
КонецПопытки;
КонецЦикла;
Если у вас есть доступ к администрированию целевого FTP-сервера, вы можете решить эту проблему раз и навсегда на системном уровне, не изменяя код в 1С. Для этого необходимо разрешить перезапись файлов при операции переименования (команда RNTO).
Настройки для различных серверов:
/etc/vsftpd.conf включен параметр write_enable=YES. В некоторых сборках за перезапись при переименовании отвечает скрытый параметр allow_writeable_chroot=YES.Мы детально проанализировали проблему работы с FTP в 1С и нашли лучшие пути её решения. Подведем итоги совместной работы:
Записать() изначально спроектирован с поддержкой автоматической перезаписи существующих данных.