Как предотвратить повторную загрузку одного и того же файла в 1С:Архитектура гарантированной однократной загрузки

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

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

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

Почему не работает стандартный код с управляемой блокировкой?

Для начала проанализируем типичный код, который часто пытаются использовать начинающие разработчики (и пример которого приведен в исходном вопросе темы):


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить();
ЭлементБлокировки.Область = "Документ.ВхФайл";
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("ИмяФайла", ИмяЗагружаемогоФайла);
Блокировка.Заблокировать();

При выполнении этого кода система выдаст ошибку выполнения. Выясним технические причины этого поведения:

  1. Отсутствие индексации реквизита. Платформа 1С:Предприятие позволяет устанавливать управляемую блокировку по конкретным полям объектов только в том случае, если эти поля проиндексированы СУБД. В конфигураторе у реквизита ИмяФайла документа ВхФайл должно быть включено свойство «Индексировать» (или «Индексировать с дополнительным упорядочиванием»). Если поле не проиндексировано, свойство метаданных «Поля блокировки данных» для этого объекта будет пустым, и платформа физически не сможет наложить выборочную блокировку.
  2. Жизненный цикл транзакции СУБД. Управляемые транзакционные блокировки (класс БлокировкаДанных) живут строго внутри транзакции СУБД. Они устанавливаются после вызова НачатьТранзакцию() и автоматически снимаются платформой в момент вызова ЗафиксироватьТранзакцию() или ОтменитьТранзакцию(). Попытка открыть транзакцию на клиенте при открытии формы и держать её запущенной до тех пор, пока пользователь интерактивно работает с файлом, — это грубейшая архитектурная ошибка. Длинные транзакции приводят к мгновенной блокировке всей СУБД, таймаутам у других пользователей и высокому риску взаимных блокировок (Deadlocks).
  3. Проблема нового (незаписанного) документа. Если документ еще не записан в базу данных, у него нет уникальной ссылки (UUID). Блокировать объект, которого физически не существует в таблицах СУБД, невозможно.

Таким образом, использование транзакционных блокировок для долгосрочного интерактивного контроля уникальности файлов невозможно по самой природе транзакций. Нам требуется принципиально другое решение.

Правильная архитектура: Использование вспомогательного регистра и пессимистической блокировки

Для решения задачи нам потребуется связать два механизма: короткую транзакционную блокировку (для бесконфликтной записи факта начала работы с файлом) и объектную пессимистическую блокировку платформы 1С, которая может удерживаться сколь угодно долго без удержания транзакции СУБД.

Разберем архитектуру решения по шагам.

Шаг 1. Создание вспомогательного объекта метаданных

Создадим новый независимый Регистр сведений. Назовем его, например, ЗанятыеФайлыПриЗагрузке.

Настроим его свойства следующим образом:

Шаг 2. Разработка механизма резервирования файла (Начало загрузки)

Когда пользователь нажимает кнопку «Загрузить файл» на форме документа, мы должны проверить, не занят ли файл прямо сейчас другим сеансом, и если свободен — зарезервировать его за текущей формой.

Рассмотрим процедуру, выполняемую на сервере:


&НаСервере
Функция ЗанятьФайлДляЗагрузки(Знач ХешФайла, Знач УникальныйИдентификаторФормы) Экспорт
    
    // Шаг 2.1. Открываем короткую транзакцию для безопасной проверки уникальности
    НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
    
    Попытка
        // Накладываем управляемую транзакционную блокировку на конкретный хеш в регистре
        Блокировка = Новый БлокировкаДанных;
        ЭлементБлокировки = Блокировка.Добавить();
        ЭлементБлокировки.Область = "РегистрСведений.ЗанятыеФайлыПриЗагрузке";
        ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
        ЭлементБлокировки.УстановитьЗначение("ХешФайла", ХешФайла);
        Блокировка.Заблокировать();
        
        // Проверяем, существует ли уже запись по этому файлу в регистре
        МенеджерЗаписи = РегистрыСведений.ЗанятыеФайлыПриЗагрузке.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.ХешФайла = ХешФайла;
        МенеджерЗаписи.Прочитать();
        
        Если МенеджерЗаписи.Выбран() Тогда
            // Файл уже кем-то занят или уже загружен в базу окончательно
            ОтменитьТранзакцию();
            Возврат Ложь;
        КонецЕсли;
        
        // Файл свободен. Записываем информацию о том, что данная форма заняла этот файл
        МенеджерЗаписи.ХешФайла = ХешФайла;
        МенеджерЗаписи.ИдентификаторФормы = УникальныйИдентификаторФормы;
        МенеджерЗаписи.Записать(Истина); // Замещаем запись
        
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ЗаписатьЛогСобытий("Ошибка блокировки или записи хеша файла: " + ОписаниеОшибки());
        Возврат Ложь;
    КонецПопытки;
    
    // Шаг 2.2. Накладываем пессимистическую блокировку на созданную запись регистра.
    // Эта блокировка НЕ держит транзакцию СУБД, но не позволит другим сеансам изменить или удалить эту запись
    // до тех пор, пока открыта форма с указанным УникальныйИдентификаторФормы.
    КлючЗаписиРегистра = РегистрыСведений.ЗанятыеФайлыПриЗагрузке.СоздатьКлючЗаписи(
        Новый Структура("ХешФайла", ХешФайла)
    );
    
    Попытка
        ЗаблокироватьДанныеДляРедактирования(КлючЗаписиРегистра, , УникальныйИдентификаторФормы);
        Возврат Истина;
    Исключение
        // Если по какой-то причине не удалось заблокировать (например, форма закрывается),
        // очистим временную запись в регистре
        ОчиститьВременнуюЗаписьРегистра(ХешФайла);
        Возврат Ложь;
    КонецПопытки;
    
КонецФункции

Шаг 3. Обработка закрытия формы или успешной записи документа

Блокировка, установленная методом ЗаблокироватьДанныеДляРедактирования, автоматически снимается платформой при закрытии формы (так как мы передали туда УникальныйИдентификаторФормы). Однако сама запись в регистре сведений ЗанятыеФайлыПриЗагрузке останется в базе. Нам необходимо корректно её обрабатывать.

Рассмотрим логику освобождения ресурсов:

Вариант А: Пользователь успешно записал документ. В этот момент файл официально становится частью информационной базы. Мы должны либо удалить запись из регистра ЗанятыеФайлыПриЗагрузке (чтобы файл можно было загрузить в другой документ, если это разрешено вашей бизнес-логикой), либо изменить статус этой записи, чтобы показать, что файл импортирован окончательно.

Вариант Б: Пользователь закрыл форму без сохранения документа. В обработчике события формы ПриЗакрытии на сервере мы обязательно должны удалить запись из нашего вспомогательного регистра, чтобы освободить файл для других пользователей.


&НаСервере
Процедура ОчиститьВременнуюЗаписьРегистра(Знач ХешФайла) Экспорт
    МенеджерЗаписи = РегистрыСведений.ЗанятыеФайлыПриЗагрузке.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.ХешФайла = ХешФайла;
    МенеджерЗаписи.Удалить();
КонецПроцедуры

В модуле управляемой формы документа пропишем следующий обработчик:


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

&НаСервере
Процедура ОчиститьВременнуюЗаписьРегистраНаСервере(Знач ХешФайла)
    ОчиститьВременнуюЗаписьРегистра(ХешФайла);
КонецПроцедуры

Повышение надежности: Почему проверка по имени файла — это уязвимость?

Как справедливо заметили участники обсуждения на форуме, контроль по реквизиту ИмяФайла обладает критической архитектурной слабостью. Разные пользователи на своих компьютерах могут пытаться загрузить абсолютно разные файлы с одинаковыми именами (например, Договор.pdf, Скан.jpg, Отчет.xlsx). Блокировка по имени файла заблокирует работу сотен пользователей, которые пытаются импортировать свои локальные файлы с дефолтными именами.

И наоборот: один и тот же файл может быть переименован (например, Договор_ред1.pdf и Договор_ред2.pdf) и загружен повторно, что нарушит бизнес-требование «один и только один раз».

Правильное решение: Контролировать уникальность файлов по их содержимому с помощью вычисления криптографической хеш-суммы (MD5 или SHA-256). Начиная с версии 8.3.10, в платформе 1С:Предприятие для этого есть мощный и быстрый встроенный объект ХешированиеДанных.

Рассмотрим пример функции, которая вычисляет уникальный хеш-код файла прямо перед его отправкой на сервер:


&НаКлиенте
Функция ПолучитьХешФайла(Знач ПутьКФайлу)
    
    // Используем встроенный механизм хеширования данных на платформе
    Хеширование = Новый ХешированиеДанных(ХешФункция.SHA256);
    
    // Читаем файл в бинарном режиме и передаем в объект хеширования
    ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПутьКФайлу);
    Хеширование.Добавить(ДвоичныеДанныеФайла);
    
    // Получаем строку хеша (длина строки SHA-256 будет ровно 64 символа)
    СтрокаХеша = СтрЗаменить(Строка(Хеширование.ХешСумма), " ", "");
    
    Возврат НРег(СтрокаХеша);
    
КонецФункции

Использование хеш-суммы полностью решает проблему коллизий имен файлов. Теперь, даже если пользователи назовут файлы одинаково, их хеш-суммы будут уникальными. А если они попытаются загрузить один и тот же файл под разными именами, система безошибочно определит дубликат и заблокирует повторную операцию.

Резюме предложенного решения

Мы спроектировали надежный контур контроля уникальности загружаемых файлов, состоящий из следующих звеньев:

  1. При выборе файла на клиенте вычисляется его уникальный цифровой отпечаток — SHA-256 хеш-сумма.
  2. Для проверки занятости этого хеша открывается сверхкороткая и безопасная транзакция СУБД, внутри которой накладывается управляемая блокировка (БлокировкаДанных) на запись вспомогательного регистра сведений ЗанятыеФайлыПриЗагрузке.
  3. Если хеш свободен, в регистр записывается признак владения этим файлом текущей формой документа.
  4. На запись регистра накладывается пессимистическая блокировка платформы (ЗаблокироватьДанныеДляРедактирования), которая живет ровно столько, сколько открыта интерактивная форма у пользователя, не нагружая СУБД транзакциями.
  5. При закрытии формы или при успешной записи документа временная блокировка аккуратно снимается, а запись в регистре удаляется либо обновляется в зависимости от логики вашей конфигурации.

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

← На главную