В процессе сопровождения информационных баз «1С:Предприятие» нередко возникает задача реорганизации хранения присоединенных файлов. Например, когда файлы изначально были загружены некорректно, дисковое пространство на одном из серверов хранения подходит к концу или требуется перенести файлы в новые сетевые каталоги (тома хранения). При этом важно не просто физически скопировать файлы на сервере, но и корректно обновить ссылки в соответствующих справочниках базы данных, чтобы пользователи не потеряли доступ к документам.
В этой статье мы подробно разберем, как правильно реализовать процесс переноса файлов на стороне сервера, как избежать типичных ошибок с правами доступа к сетевым ресурсам, как эффективно парсить пути файлов и как оптимизировать запросы к СУБД для обработки больших массивов данных.
При выполнении серверного кода переноса файлов (в процедурах с директивой компиляции &НаСервере или в привилегированных модулях) разработчики часто сталкиваются с ошибкой доступа к файловой системе. Рассмотрим подробнее, почему это происходит:
USR1CV8 или SYSTEM. Данные учетные записи не имеют прав доступа к внешним сетевым ресурсам (путям формата UNC, например, \\server\volume\file.pdf).\. В таком случае сетевую папку необходимо предварительно примонтировать в локальную директорию Linux.Решение проблемы с правами доступа:
Для исправления ошибок доступа необходимо запустить службу сервера 1С:Предприятия от имени доменной учетной записи Active Directory, которой предоставлены полные права (чтение, запись, изменение) как на уровне общего сетевого доступа (Sharing), так и на уровне безопасности NTFS (Security) целевых каталогов.
Когда у нас есть только полная строка пути к файлу на диске, нам часто требуется выделить из нее имя файла, расширение или имя без расширения. Сделать это можно двумя основными способами.
Способ 1. Использование объекта Файл (Рекомендуемый)
Платформа 1С предоставляет удобный объект Файл, который позволяет парсить строки путей автоматически. Обратите внимание: свойства этого объекта (такие как имя и расширение) работают на основе строкового анализа, поэтому объект успешно вернет составляющие пути, даже если физически файл по указанному адресу еще не существует.
// Создаем объект для анализа пути
АнализаторФайла = Новый Файл("\\lux.ltcompany.com\ALL\1CExtenalFiles\GLOSS LED8.png");
ИмяФайла = АнализаторФайла.Имя; // Результат: "GLOSS LED8.png"
ИмяБезРасширения = АнализаторФайла.ИмяБезРасширения; // Результат: "GLOSS LED8"
РасширениеФайла = АнализаторФайла.Расширение; // Результат: ".png"
ПутьККаталогуФайла = АнализаторФайла.Путь; // Результат: "\\lux.ltcompany.com\ALL\1CExtenalFiles\"
Способ 2. Строковые функции (Альтернативный)
Если по какой-то причине требуется решить задачу чисто текстовыми методами без создания объектов платформы, можно заменить разделители путей на символы переноса строки и получить последнюю строку получившегося текста:
Функция ПолучитьИмяФайлаИзСтроки(Знач ПолныйПуть) Экспорт
// Заменяем обратные слэши на переносы строк
ТекстСПереносами = СтрЗаменить(ПолныйПуть, "\", Символы.ПС);
КоличествоСтрок = СтрЧислоСтрок(ТекстСПереносами);
Возврат СтрПолучитьСтроку(ТекстСПереносами, КоличествоСтрок);
КонецФункции
При написании обработок по переносу данных критически важно избегать выполнения запросов к СУБД внутри цикла (проблема "N+1 запросов"). Если у вас есть таблица из 10 000 файлов и для каждого файла вы будете вызывать отдельную функцию с запросом для поиска владельца или тома, процесс затянется на долгие часы и создаст колоссальную нагрузку на сервер.
Проанализируем правильный подход: мы должны собрать все уникальные пути или имена файлов в массив, передать этот массив в один пакетный запрос, получить соответствие путей и ссылок в памяти (например, выгрузить в Таблицу Значений или Соответствие) и затем быстро сопоставить данные в цикле без обращения к базе данных.
Рассмотрим готовый оптимизированный пример серверной процедуры, которая переносит файлы из старого расположения в новое, проверяет наличие владельца в 1С (чтобы отсеять «сиротские» файлы без владельца в архивную папку), физически копирует файлы и перезаписывает элементы справочника присоединенных файлов с указанием нового тома.
Для надежности копирования вместо стандартного метода КопироватьФайл() мы можем использовать чтение и запись через ДвоичныеДанные, что позволяет платформе более гибко управлять буферизацией при работе с сетью.
&НаСервере
Процедура ВыполнитьПереносФайловНаСервере()
УстановитьПривилегированныйРежим(Истина);
// Шаг 1. Собираем уникальные пути к файлам для пакетного запроса
МассивПутей = Новый Массив;
Для Каждого Стр Из Объект.СписокФайлов Цикл
Анализатор = Новый Файл(Стр.РасположениеФайла);
// Сохраняем относительный путь для поиска в справочнике присоединенных файлов
МассивПутей.Добавить(Анализатор.Имя);
КонецЦикла;
// Шаг 2. Одним запросом получаем данные присоединенных файлов и их владельцев
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПрисоединенныеФайлы.Ссылка КАК Ссылка,
| ПрисоединенныеФайлы.ВладелецФайла КАК ВладелецФайла,
| ПрисоединенныеФайлы.ПутьКФайлу КАК ПутьКФайлу,
| ПрисоединенныеФайлы.Том КАК Том
|ИЗ
| Справочник.БИТ_СТ_ГруппыНоменклатурыВКаталогеПрисоединенныеФайлы КАК ПрисоединенныеФайлы
|ГДЕ
| ПрисоединенныеФайлы.ПутьКФайлу В (&МассивПутей)";
Запрос.УстановитьПараметр("МассивПутей", МассивПутей);
ТаблицаБазыДанных = Запрос.Выполнить().Выгрузить();
// Для быстрого поиска в памяти построим индекс по пути файла
ТаблицаБазыДанных.Индексы.Добавить("ПутьКФайлу");
// Получаем ссылку на новый целевой том хранения
СсылкаНовыйТом = Справочники.ТомаХраненияФайлов.НайтиПоКоду("000000002");
ПутьАрхива = "\\lux.ltcompany.com\ALL\ArchiveFiles\";
// Шаг 3. Основной цикл переноса
Для Каждого СтрокаТаблицы Из Объект.СписокФайлов Цикл
ФайлИсточник = Новый Файл(СтрокаТаблицы.РасположениеФайла);
Если Не ФайлИсточник.Существует() Тогда
Сообщить("Файл не найден на диске: " + СтрокаТаблицы.РасположениеФайла);
Продолжить;
КонецЕсли;
// Ищем строку в кэше базы данных
СтрокаПоиска = Новый Структура("ПутьКФайлу", ФайлИсточник.Имя);
НайденныеСтроки = ТаблицаБазыДанных.НайтиСтроки(СтрокаПоиска);
ИмеетВладельца = Ложь;
СсылкаНаФайл = Неопределено;
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаБД = НайденныеСтроки[0];
Если ЗначениеЗаполнено(СтрокаБД.ВладелецФайла) Тогда
ИмеетВладельца = Истина;
СсылкаНаФайл = СтрокаБД.Ссылка;
КонецЕсли;
КонецЕсли;
Попытка
Если ИмеетВладельца Тогда
// Вариант А: Файл имеет владельца в 1С. Копируем в рабочий том.
ЦелевойПуть = СтрокаТаблицы.НовыйПутьХранения;
// Физическое копирование файла
КопироватьФайл(СтрокаТаблицы.РасположениеФайла, ЦелевойПуть);
// Обновление тома и пути в карточке файла в 1С
ОбъектФайла = СсылкаНаФайл.ПолучитьОбъект();
ОбъектФайла.Том = СсылкаНовыйТом;
// При необходимости обновляем относительный путь в пределах тома
ОбъектФайла.ПутьКФайлу = ФайлИсточник.Имя;
ОбъектФайла.Записать();
Сообщить("Файл успешно перенесен в новый том: " + ФайлИсточник.Имя);
Иначе
// Вариант Б: Файл "сирота". Переносим в архивную папку.
ЦелевойПуть = ПутьАрхива + ФайлИсточник.Имя;
КопироватьФайл(СтрокаТаблицы.РасположениеФайла, ЦелевойПуть);
Сообщить("Файл без владельца отправлен в архив: " + ФайлИсточник.Имя);
КонецЕсли;
Исключение
// Логируем ошибки выполнения для каждого конкретного файла
ИнформацияОбОшибке = ИнформацияОбОшибке();
Сообщить("Ошибка при обработке файла " + ФайлИсточник.Имя + ": " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Процесс переноса файлов на сервере требует от разработчика понимания контекстов исполнения и ограничений операционных систем. Для успешного выполнения подобных задач всегда придерживайтесь следующих правил:
Файл для быстрого и безопасного выделения имени и расширения файла;Попытка ... Исключение, чтобы ошибка копирования одного поврежденного файла не прерывала весь сеанс группового переноса.