Как скопировать файлы между томами хранения на сервере 1С и обновить пути в базе данных?

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

В процессе сопровождения информационных баз «1С:Предприятие» нередко возникает задача реорганизации хранения присоединенных файлов. Например, когда файлы изначально были загружены некорректно, дисковое пространство на одном из серверов хранения подходит к концу или требуется перенести файлы в новые сетевые каталоги (тома хранения). При этом важно не просто физически скопировать файлы на сервере, но и корректно обновить ссылки в соответствующих справочниках базы данных, чтобы пользователи не потеряли доступ к документам.

В этой статье мы подробно разберем, как правильно реализовать процесс переноса файлов на стороне сервера, как избежать типичных ошибок с правами доступа к сетевым ресурсам, как эффективно парсить пути файлов и как оптимизировать запросы к СУБД для обработки больших массивов данных.

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

При выполнении серверного кода переноса файлов (в процедурах с директивой компиляции &НаСервере или в привилегированных модулях) разработчики часто сталкиваются с ошибкой доступа к файловой системе. Рассмотрим подробнее, почему это происходит:

  1. Ограничение прав службы сервера 1С:Предприятия. В отличие от клиентского кода, который выполняется под правами текущего пользователя операционной системы Windows или Linux, серверный код исполняется от имени учетной записи, под которой запущена служба (агент) сервера 1С. По умолчанию это локальный пользователь USR1CV8 или SYSTEM. Данные учетные записи не имеют прав доступа к внешним сетевым ресурсам (путям формата UNC, например, \\server\volume\file.pdf).
  2. Использование профилей безопасности. Если в вашей информационной базе включены профили безопасности, платформа 1С будет блокировать любые попытки обращения к файловой системе за пределами временных папок, если целевые каталоги не добавлены в список разрешенных путей в настройках профиля.
  3. Различия операционных систем. Если сервер 1С работает под управлением Linux, он физически не сможет обратиться к сетевому пути Windows с обратными слэшами \. В таком случае сетевую папку необходимо предварительно примонтировать в локальную директорию 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С
                ОбъектФайла = СсылкаНаФайл.ПолучитьОбъект();
                ОбъектФайла.Том = СсылкаНовыйТом;
                // При необходимости обновляем относительный путь в пределах тома
                ОбъектФайла.ПутьКФайлу = ФайлИсточник.Имя; 
                ОбъектФайла.Записать();
                
                Сообщить("Файл успешно перенесен в новый том: " + ФайлИсточник.Имя);
            Иначе
                // Вариант Б: Файл "сирота". Переносим в архивную папку.
                ЦелевойПуть = ПутьАрхива + ФайлИсточник.Имя;
                КопироватьФайл(СтрокаТаблицы.РасположениеФайла, ЦелевойПуть);
                
                Сообщить("Файл без владельца отправлен в архив: " + ФайлИсточник.Имя);
            КонецЕсли;
            
        Исключение
            // Логируем ошибки выполнения для каждого конкретного файла
            ИнформацияОбОшибке = ИнформацияОбОшибке();
            Сообщить("Ошибка при обработке файла " + ФайлИсточник.Имя + ": " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке));
        КонецПопытки;
        
    КонецЦикла;

КонецПроцедуры

Подведем итоги

Процесс переноса файлов на сервере требует от разработчика понимания контекстов исполнения и ограничений операционных систем. Для успешного выполнения подобных задач всегда придерживайтесь следующих правил:

← На главную