При разработке интеграционных механизмов, выгрузок или процедур обмена часто возникает задача временного сохранения файлов (например, картинок или документов) на диск для их последующей обработки: архивации в ZIP, отправки по FTP или передачи в веб-сервисы. После успешной отправки временные файлы необходимо удалить, чтобы не засорять дисковое пространство сервера или локальной машины.
Однако на этапе удаления разработчики нередко сталкиваются с неприятным исключением: «Ошибка доступа к файлу» при вызове метода УдалитьФайлы(). При этом сторонние утилиты (например, Unlocker) могут показывать, что файл никем не занят, а сам разработчик при ручной проверке через проводник Windows без проблем удаляет этот файл. В рамках этой статьи мы подробно разберем, почему возникает такое поведение, развенчаем популярные мифы среди разработчиков 1С и рассмотрим правильные методы обхода этой проблемы.
Рассмотрим классический сценарий, с которым столкнулся автор темы. Из документов информационной базы извлекаются присоединенные файлы (картинки JPEG). В зависимости от настроек системы, файлы могут храниться либо непосредственно в базе данных (в виде двоичных данных), либо в специализированных томах на диске (в рамках подсистемы БСП «Работа с файлами»).
После копирования файлов во временный каталог создается XML-файл манифеста, все файлы упаковываются в ZIP-архив и отправляются на внешний FTP-сервер. По завершении процесса система пытается очистить временную папку, но на операциях с картинками вылетает в исключение.
Исходный код выгрузки файлов выглядит следующим образом:
// Пример копирования файлов из тома или базы данных
Если Картинка.Значение.ТипХраненияФайла = Перечисления.ТипыХраненияФайлов.ВТомахНаДиске Тогда
// Проверить фактическое существование файла на томе
Файл = Новый Файл(Картинка.Значение.ПутьКФайлу);
Если Файл.Существует() Тогда
КопироватьФайл(Картинка.Значение.ПутьКФайлу, ИмяФайлаПриемника);
Фото.Вставить(Картинка.Ключ, ИмяФайлаПриемника);
КонецЕсли;
ИначеЕсли Картинка.Значение.ТипХраненияФайла = Перечисления.ТипыХраненияФайлов.ВИнформационнойБазе Тогда
СсылкаНаДвоичныеДанные = РаботаСФайлами.ДанныеФайла(ОбСсылка[Картинка.Ключ], УИД).СсылкаНаДвоичныеДанныеФайла;
ДвоичныеДанные = ПолучитьИзВременногоХранилища(СсылкаНаДвоичныеДанные);
ДвоичныеДанные.Записать(ИмяФайлаПриемника);
Фото.Вставить(Картинка.Ключ, ИмяФайлаПриемника);
КонецЕсли;
После упаковки файлов в архив выполняется попытка очистки:
// Попытка удаления временных файлов
УдалитьФайлы(ИмяФайлаПриемника); // Здесь возникает исключение "Ошибка доступа к файлу"
Давайте проанализируем, почему это происходит и какие заблуждения часто мешают разработчикам быстро найти верное решение.
В процессе обсуждения подобных проблем на форумах часто озвучивается мнение, что метод КопироватьФайл() работает асинхронно. Якобы 1С запускает процесс копирования средствами операционной системы и сразу же передает управление следующей строке кода. В результате, когда система доходит до шага УдалитьФайлы(), файл все еще занят копированием, что и вызывает ошибку доступа.
Это техническое заблуждение. Метод глобального контекста КопироватьФайл() является абсолютно синхронным (блокирующим). Поток выполнения кода не перейдет к следующей строке до тех пор, пока операционная система полностью не завершит запись файла-приемника на диск и не закроет дескриптор записи. Асинхронным аналогом в платформе является метод НачатьКопированиеФайла(), требующий описания процедур обратного вызова (оповещений). Если в вашем коде используется стандартный КопироватьФайл(), то задержки во времени (использование циклов ожидания, вызовов команды пинга для создания паузы) не имеют практического смысла и лишь замедляют работу конфигурации.
Истинная причина кроется в особенностях хранения файлов в томах при использовании Библиотеки Стандартных Подсистем (БСП) и поведении файловой системы Windows.
КопироватьФайл() операционная система создает новый файл-приемник. В то время как права доступа безопасности (NTFS/ACL) наследуются от целевой папки, системные атрибуты самого файла (такие как «Только чтение», «Скрытый» и т.д.) полностью копируются с исходного файла. Таким образом, ваш временный файл-приемник сразу после создания получает признак «Только чтение».УдалитьФайлы() при попытке удалить файл с установленным атрибутом «Только чтение» завершается ошибкой доступа (Access Denied), генерируя исключение. В то же время проводник Windows при ручном удалении может молча игнорировать этот атрибут (или запрашивать подтверждение в зависимости от настроек UAC), что и вводит разработчиков в заблуждение при интерактивной проверке.Для того чтобы гарантированно и без ошибок удалять временные файлы, перед вызовом УдалитьФайлы() необходимо проверять наличие признака «Только чтение» и программно сбрасывать его.
Рассмотрим, как это сделать с помощью объекта Файл:
// Безопасное удаление файла с предварительным снятием атрибута "Только чтение"
ВыбФайл = Новый Файл(ИмяФайлаПриемника);
Если ВыбФайл.Существует() Тогда
// Проверяем, установлен ли атрибут "Только чтение"
Если ВыбФайл.ПолучитьТолькоЧтение() Тогда
// Снимаем ограничение
ВыбФайл.УстановитьТолькоЧтение(Ложь);
КонецЕсли;
// Теперь удаление пройдет успешно
Попытка
УдалитьФайлы(ИмяФайлаПриемника);
Исключение
ЗаписьЖурналаРегистрации("ОчисткаВременныхФайлов",
УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось удалить файл: " + ИмяФайлаПриемника + ". Описание: " + ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
Стоит отметить, что в самой БСП (например, в модулях работы с файлами) разработчики фирмы «1С» используют точно такой же подход: после выгрузки или копирования файлов из томов они явно сбрасывают унаследованный атрибут программным путем.
Если даже после снятия атрибута «Только чтение» файлы продолжают выдавать ошибку доступа, проанализируем еще два важных фактора, которые часто встречаются при автоматизации выгрузок.
Если вы упаковываете картинки в ZIP-архив с помощью встроенного объекта 1С ЗаписьZipФайла, очень важно корректно завершать работу с этим объектом. Если в процессе добавления файлов в архив или при его записи возникает ошибка (например, из-за нехватки места или некорректных символов в путях), код может аварийно прерваться.
Если объект ЗаписьZipФайла не был закрыт методом Записать(), процесс rphost (на сервере) или клиентский сеанс (в файловой базе) продолжит монопольно удерживать дескрипторы всех упаковываемых файлов. Защитить код от этого можно с помощью конструкции Попытка — Исключение:
Архиватор = Новый ЗаписьZipФайла(ПутьКАрхиву);
Попытка
Для Каждого ФайлДляДобавления Из СписокФайлов Цикл
Архиватор.Добавить(ФайлДляДобавления);
КонецЦикла;
Архиватор.Записать();
Исключение
// В случае ошибки архивации закрываем дескрипторы, чтобы освободить файлы
Архиватор = Неопределено;
ВызватьИсключение;
КонецПопытки;
При создании файлов большого объема (особенно картинок высокого разрешения или PDF-документов) в системных временных каталогах, они могут быть кратковременно заблокированы службами операционной системы:
Для минимизации таких конфликтов рекомендуется настраивать исключения в антивирусном ПО для рабочих каталогов сервера 1С и папок с временными файлами.
Согласно стандартам разработки «1С:Совместимо», для любых промежуточных манипуляций с файлами следует использовать уникальные временные каталоги. Это гарантирует отсутствие конфликтов имен при параллельной работе пользователей.
Рассмотрим комплексный шаблон создания, обработки и последующей рекурсивной очистки временного каталога с учетом снятия блокировок:
// Шаг 1. Создаем уникальный временный каталог
УникальныйПуть = ПолучитьИмяВременногоФайла("");
СоздатьКаталог(УникальныйПуть);
Попытка
// Шаг 2. Выполняем полезную работу (копирование, упаковка, отправка)
ИмяФайлаПриемника = УникальныйПуть + ПолучитьРазделительПути() + "image.jpg";
КопироватьФайл(ПутьКФайлуВТоме, ИмяФайлаПриемника);
// [Ваш код архивации и отправки на FTP]
Исключение
// Обработка ошибок основной логики
ЗаписьЖурналаРегистрации("ИнтеграцияФайлы", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
КонецПопытки;
// Шаг 3. Гарантированная очистка временной директории
ОчиститьКаталогРекурсивно(УникальныйПуть);
// Функция рекурсивного удаления каталога с обработкой атрибутов файлов
Процедура ОчиститьКаталогРекурсивно(ПутьККаталогу) Экспорт
ВыборкаФайлов = НайтиФайлы(ПутьККаталогу, "*", Истина);
// Сначала обрабатываем все файлы в каталоге
Для Каждого Файл Из ВыборкаФайлов Цикл
Если Файл.ЭтоФайл() Тогда
Если Файл.ПолучитьТолькоЧтение() Тогда
Файл.УстановитьТолькоЧтение(Ложь);
КонецЕсли;
Попытка
УдалитьФайлы(Файл.ПолноеИмя);
Исключение
// Логируем, но не прерываем очистку остальных файлов
ЗаписьЖурналаРегистрации("ОчисткаКаталога", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось удалить файл: " + Файл.ПолноеИмя + ". Описание: " + ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
КонецЦикла;
// Теперь удаляем сам каталог
Попытка
УдалитьФайлы(ПутьККаталогу);
Исключение
ЗаписьЖурналаРегистрации("ОчисткаКаталога", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось удалить каталог: " + ПутьККаталогу + ". Описание: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Использование этого шаблона позволит вам навсегда забыть об ошибках совместного доступа и обеспечит стабильную работу ваших интеграционных решений в любых условиях.