Как в 1С проверить, что файл по ссылке уже был скачан, и избежать повторной загрузки?

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

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

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

Ошибочный подход: хеширование самой ссылки

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


Функция MD5ХешФайл(тСтрока) Экспорт    
    
    Хеш = Новый ХешированиеДанных(ХешФункция.MD5);    
    Хеш.ДобавитьФайл(тСтрока);
    
    Возврат Строка(Хеш.ХешСумма);  
    
КонецФункции

На первый взгляд может показаться, что метод Хеш.ДобавитьФайл(тСтрока) примет URL и вычислит хеш от содержимого файла, находящегося по этому URL. Однако это не так. Метод ДобавитьФайл() объекта ХешированиеДанных ожидает путь к локальному файлу на диске. Если вы передадите ему URL-адрес (строку, например, "http://example.com/file.zip"), он попытается обработать эту строку как путь к локальному файлу. В лучшем случае это приведет к ошибке, если файла с таким именем не существует. В худшем — он может вычислить хеш от *самой строки URL*, что абсолютно бесполезно для проверки содержимого удаленного файла.

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

Методы предотвращения повторной скачки файла

Для эффективной проверки и предотвращения повторной скачки файла мы можем использовать несколько методов, которые можно применять как по отдельности, так и в комбинации.

1. Проверка размера файла с помощью HEAD-запроса

Один из самых простых и быстрых способов получить некоторую информацию о файле на сервере без его полной загрузки — это выполнить HTTP-запрос типа HEAD. В отличие от GET-запроса, который загружает всё тело ответа, HEAD-запрос запрашивает только заголовки ответа. Среди этих заголовков обычно присутствует Content-Length, который указывает размер файла в байтах.

В 1С это можно реализовать следующим образом:


Функция РазмерФайлаHTTP(СоединениеHTTP, Знач ПутьНаСервере, Знач Заголовки = Неопределено)
    
    ЗапросHTTP = Новый HTTPЗапрос(ПутьНаСервере, Заголовки);
    Попытка
        // Метод ПолучитьЗаголовки выполняет HEAD-запрос
        ПолученныеЗаголовки = СоединениеHTTP.ПолучитьЗаголовки(ЗапросHTTP); 
    Исключение
        Возврат 0; // В случае ошибки возвращаем 0
    КонецПопытки;
    
    // Получаем значение заголовка Content-Length, приводя его к нижнему регистру для надежности
    РазмерСтрокой = ПолученныеЗаголовки.Заголовки["content-length"];
    
    ТипЧисло = Новый ОписаниеТипов("Число");
    РазмерФайла = ТипЧисло.ПривестиЗначение(РазмерСтрокой);
    
    Возврат РазмерФайла;
    
КонецФункции

Как это использовать:

  1. При первой загрузке файла, помимо самого файла, сохраните его размер.
  2. При последующих попытках загрузки, вызовите функцию РазмерФайлаHTTP для того же URL.
  3. Сравните полученный размер с сохраненным. Если размеры отличаются, файл на сервере изменился, и его можно скачивать. Если размеры совпадают, есть высокая вероятность, что файл не изменился.

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

2. Использование файлов с метаданными (манифестов)

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

Что может содержать файл метаданных:

Формат такого файла может быть любым: простой текстовый файл, JSON, XML.

Пример JSON-файла метаданных (update_info.json):


{
  "version": "2.1.0",
  "last_modified": "2023-10-27T10:30:00Z",
  "md5_hash": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
  "sha256_hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "file_size": 10240000,
  "download_url": "http://example.com/new_app_version.zip"
}

Как это использовать в 1С:

  1. Вместо того чтобы сразу пытаться скачать основной файл, 1С сначала скачивает этот небольшой файл метаданных.
  2. Парсит его (например, с помощью ЧтениеJSON или ЧтениеXML).
  3. Сравнивает полученную информацию (версию, хеш-сумму, дату, размер) с данными о локально скачанном файле.
  4. Если обнаруживаются различия, 1С принимает решение о необходимости скачивания основного файла по ссылке, указанной в файле метаданных.

Этот подход очень гибкий и широко используется, например, самой платформой 1С для обновления конфигураций (файлы .cfu и .mft) или внешними утилитами обновления.

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

3. Условные HTTP-запросы (Conditional Requests)

Протокол HTTP предоставляет встроенные механизмы для условных запросов, которые позволяют клиенту спрашивать сервер, изменился ли ресурс с момента последнего скачивания. Если ресурс не изменился, сервер возвращает статус 304 Not Modified без тела ответа, что позволяет избежать повторной загрузки.

Эти механизмы особенно эффективны, поскольку они являются частью стандарта HTTP и поддерживаются большинством веб-серверов. В 1С их можно использовать через объект HTTPЗапрос и свойство Заголовки.

3.1. Использование заголовков If-Modified-Since и Last-Modified

Механизм основан на дате последнего изменения файла:

  1. Сервер отправляет Last-Modified: При первой успешной загрузке файла сервер в ответных заголовках отправляет Last-Modified, указывая дату и время последнего изменения файла на сервере.
  2. Клиент сохраняет дату: Ваша система 1С сохраняет эту дату вместе с локальной копией файла.
  3. Клиент отправляет If-Modified-Since: При следующем запросе того же файла 1С формирует HTTPЗапрос и добавляет в него заголовок If-Modified-Since, передавая в нем сохраненную дату.
  4. Сервер проверяет: Если файл на сервере не был изменен с указанной даты, сервер вернет HTTP-статус 304 Not Modified без тела ответа. В противном случае, если файл изменился, сервер вернет статус 200 OK и полное тело файла.

Пример в 1С:


// Предполагаем, что СоединениеHTTP уже создано
// и у нас есть путь к файлу на сервере (ПутьНаСервере)
// и сохраненная дата последнего изменения (ДатаПоследнегоИзмененияФайла)

// Шаг 1: Получение заголовка Last-Modified при первой загрузке
// (После успешной загрузки файла, например, в Ссылка.Загрузить(HTTPЗапрос, ИмяЛокальногоФайла))
// ПолученныеЗаголовки = СоединениеHTTP.ПолучитьЗаголовки(HTTPЗапрос);
// Если ПолученныеЗаголовки.Заголовки.Свойство("last-modified") Тогда
//     ДатаПоследнегоИзмененияФайла = ПолученныеЗаголовки.Заголовки["last-modified"];
// КонецЕсли;

// Шаг 2: Создание запроса с If-Modified-Since для проверки
ЗапросHTTP = Новый HTTPЗапрос(ПутьНаСервере);

Если ЗначениеЗаполнено(ДатаПоследнегоИзмененияФайла) Тогда
    // Форматируем дату в нужный HTTP-формат (RFC 1123)
    // 1С не имеет встроенной функции для этого, можно использовать свою
    // или попробовать передать Объект.Дата в заголовок, но лучше явно
    // Для примера: Thu, 01 Dec 1994 16:00:00 GMT
    ЗапросHTTP.Заголовки.Вставить("If-Modified-Since", ФорматироватьДатуДляHTTP(ДатаПоследнегоИзмененияФайла));
КонецЕсли;

Попытка
    ОтветHTTP = СоединениеHTTP.ОтправитьДляПолученияДвоичныхДанных(ЗапросHTTP);
    
    Если ОтветHTTP.КодСостояния = 304 Тогда
        Сообщить("Файл не изменялся с " + ДатаПоследнегоИзмененияФайла + ". Используем локальную копию.");
        // Файл не изменился, дальнейшие действия не требуются
        Возврат Истина; 
    ИначеЕсли ОтветHTTP.КодСостояния = 200 Тогда
        Сообщить("Файл изменился или это первая загрузка. Загружаем новую версию.");
        // Сохраняем новую версию и новую дату Last-Modified
        // ... (код для сохранения файла) ...
        // ПолученныеЗаголовки = ОтветHTTP.Заголовки;
        // Если ПолученныеЗаголовки.Свойство("last-modified") Тогда
        //     ДатаПоследнегоИзмененияФайла = ПолученныеЗаголовки["last-modified"];
        // КонецЕсли;
        Возврат Ложь;
    Иначе
        Сообщить("Ошибка HTTP: " + ОтветHTTP.КодСостояния + " - " + ОтветHTTP.Статус);
        Возврат Ложь;
    КонецЕсли;
Исключение
    Сообщить("Ошибка при выполнении HTTP-запроса: " + ОписаниеОшибки());
    Возврат Ложь;
КонецПопытки;

// Пример функции форматирования даты (может потребовать доработки для точного соответствия RFC 1123)
// В реальных условиях лучше использовать готовые библиотеки или компоненты,
// если требуется строгое соответствие формату.
// Для простоты, 1С может иногда сама преобразовать дату, но надежнее - явно.
Функция ФорматироватьДатуДляHTTP(Дата)
    Возврат Формат(Дата, "ДФ=yyyy-MM-dd'T'HH:mm:ss'Z'"); // Пример: "2023-10-27T10:30:00Z"
                                                     // Реальный HTTP-формат сложнее: "Thu, 01 Dec 1994 16:00:00 GMT"
                                                     // Для корректной работы сервер должен понимать этот формат.
                                                     // Часто 1С HTTP-сервисы могут быть менее строги к форматам дат.
КонецФункции

Примечание: Форматирование даты для HTTP-заголовков (RFC 1123) может быть нетривиальной задачей в 1С. Убедитесь, что формат даты, который вы отправляете в If-Modified-Since, корректно интерпретируется вашим веб-сервером.

3.2. Использование заголовков ETag и If-None-Match

ETag (entity tag) — это более надежный идентификатор конкретной версии ресурса. Это своего рода "отпечаток пальца" файла. Если файл на сервере изменяется даже на один бит, его ETag должен измениться. Этот механизм более точен, чем Last-Modified, так как дата изменения может оставаться прежней при быстрых изменениях контента, или же изменения могут быть неважными (например, изменение прав доступа к файлу), но при этом вы не хотите перекачивать файл. Веб-сервер сам генерирует ETag.

  1. Сервер отправляет ETag: При первой загрузке файла сервер в ответных заголовках отправляет ETag.
  2. Клиент сохраняет ETag: Ваша система 1С сохраняет этот ETag вместе с локальной копией файла.
  3. Клиент отправляет If-None-Match: При следующем запросе того же файла 1С формирует HTTPЗапрос и добавляет в него заголовок If-None-Match, передавая в нем сохраненный ETag.
  4. Сервер проверяет: Если ETag, полученный от клиента, совпадает с текущим ETag ресурса на сервере, сервер вернет HTTP-статус 304 Not Modified без тела ответа. Если ETag не совпадает, сервер вернет статус 200 OK и полное тело файла.

Преимущества ETag:

Если сервер поддерживает как If-Modified-Since, так и If-None-Match, то If-None-Match имеет приоритет.

Пример в 1С:


// Предполагаем, что СоединениеHTTP уже создано
// и у нас есть путь к файлу на сервере (ПутьНаСервере)
// и сохраненный ETag (СохраненныйETag)

// Шаг 1: Получение заголовка ETag при первой загрузке
// (После успешной загрузки файла)
// ПолученныеЗаголовки = ОтветHTTP.Заголовки;
// Если ПолученныеЗаголовки.Свойство("etag") Тогда
//     СохраненныйETag = ПолученныеЗаголовки["etag"];
// КонецЕсли;

// Шаг 2: Создание запроса с If-None-Match для проверки
ЗапросHTTP = Новый HTTPЗапрос(ПутьНаСервере);

Если ЗначениеЗаполнено(СохраненныйETag) Тогда
    ЗапросHTTP.Заголовки.Вставить("If-None-Match", СохраненныйETag);
КонецЕсли;

Попытка
    ОтветHTTP = СоединениеHTTP.ОтправитьДляПолученияДвоичныхДанных(ЗапросHTTP);
    
    Если ОтветHTTP.КодСостояния = 304 Тогда
        Сообщить("Файл не изменялся. Используем локальную копию.");
        Возврат Истина; 
    ИначеЕсли ОтветHTTP.КодСостояния = 200 Тогда
        Сообщить("Файл изменился или это первая загрузка. Загружаем новую версию.");
        // Сохраняем новую версию и новый ETag
        // ... (код для сохранения файла) ...
        // Если ОтветHTTP.Заголовки.Свойство("etag") Тогда
        //     СохраненныйETag = ОтветHTTP.Заголовки["etag"];
        // КонецЕсли;
        Возврат Ложь;
    Иначе
        Сообщить("Ошибка HTTP: " + ОтветHTTP.КодСостояния + " - " + ОтветHTTP.Статус);
        Возврат Ложь;
    КонецЕсли;
Исключение
    Сообщить("Ошибка при выполнении HTTP-запроса: " + ОписаниеОшибки());
    Возврат Ложь;
КонецПопытки;

Важный момент: Как было отмечено в дискуссии, HTTP-сервисы 1С поддерживают стандартные HTTP-заголовки, и это открывает широкие возможности для реализации сложной логики взаимодействия с веб-серверами.

4. Частичная загрузка файла (Range Header)

Если формат файла известен и версия или ключевая информация, по которой можно определить изменение, хранится в фиксированном месте (например, в заголовке файла), мы можем запросить только эту часть файла.

Заголовок Range позволяет клиенту запросить только часть ресурса (например, первые 1024 байта). Сервер, поддерживающий этот заголовок, ответит статусом 206 Partial Content и телом, содержащим запрошенный диапазон байтов.

Как это использовать:

  1. Вам нужно знать структуру файла и место, где хранится версия или уникальный идентификатор.
  2. Формируем HTTPЗапрос и добавляем заголовок Range, например: ЗапросHTTP.Заголовки.Вставить("Range", "bytes=0-1023"); для получения первых 1 КБ.
  3. Скачиваем эту часть файла, парсим ее и сравниваем с данными локальной копии.

Преимущества: Экономия трафика при наличии специфического формата файла. Недостатки: Требует глубокого знания формата файла, не универсально.

Рекомендации по реализации

  1. Комбинируйте подходы: Для максимальной эффективности и надежности лучше комбинировать несколько методов. Например, сначала проверять файл метаданных, а если его нет или он не подходит, использовать ETag/If-None-Match.
  2. Храните информацию локально: Всегда сохраняйте полученные с сервера данные (Last-Modified, ETag, размер, хеш-суммы из манифестов) вместе с локальной копией файла. Для этого подойдут регистры сведений, обычные файлы на диске или хранилище дополнительной информации об объектах.
  3. Проверяйте поддержку сервера: Не все веб-серверы одинаково хорошо поддерживают все HTTP-заголовки. Убедитесь, что ваш целевой сервер корректно обрабатывает HEAD-запросы, If-Modified-Since, If-None-Match и Range.
  4. Реализуйте функцию хеширования для локальных файлов: Если вы все же скачали файл, для проверки его целостности или сравнения с хешем из манифеста, вам потребуется функция хеширования локального файла. Пример такой функции (корректной для локальных файлов):
  5. 
    Функция ВычислитьХешЛокальногоФайла(ПутьКФайлу, МетодХеширования = "MD5") Экспорт
        
        // Проверяем существование файла
        Если Не ФайлСуществует(ПутьКФайлу) Тогда
            Возврат Неопределено;
        КонецЕсли;
    
        ХешФункцияДляВычисления;
        Если МетодХеширования = "MD5" Тогда
            ХешФункцияДляВычисления = ХешФункция.MD5;
        ИначеЕсли МетодХеширования = "SHA1" Тогда
            ХешФункцияДляВычисления = ХешФункция.SHA1;
        ИначеЕсли МетодХеширования = "SHA256" Тогда
            ХешФункцияДляВычисления = ХешФункция.SHA256;
        Иначе
            // Можно добавить другие методы или выбросить исключение
            Возврат Неопределено;
        КонецЕсли;
    
        Хеш = Новый ХешированиеДанных(ХешФункцияДляВычисления);
        Хеш.ДобавитьФайл(ПутьКФайлу);
        
        Возврат Строка(Хеш.ХешСумма);
        
    КонецФункции
    

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

← На главную