MAATRIX / Блог / Экспорт забрал записи, а вложения остались у вендора

Экспорт забрал записи, а вложения остались у вендора

MAATRIX

Компания решает уйти от внешнего сервиса — CRM, тикет-системы, конструктора сайтов, чего угодно с формами и прикреплёнными файлами. Нажимают «Экспорт», получают аккуратный CSV или JSON со всеми карточками, полями, датами — и облегчённо выдыхают: данные наши, вендор больше не нужен. А через месяц выясняется, что в карточках клиентов есть поле «скан договора», и оно пустое. Договоров в экспорте не было — только ссылка на них, а ссылка вела на сервер вендора, доступ к которому уже отключили. Разберём, почему так происходит почти всегда, а не иногда, и как построить экспорт так, чтобы файлы не остались чужими.

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

Причина не в жадности вендора и не в баге — это следствие того, как большинство систем физически хранят данные. Структурированные записи (текст, числа, даты, статусы) живут в реляционной базе — таблицы, строки, поля. Файлы — совсем другая физическая сущность: это бинарные блобы, которые в базе хранить дорого и неудобно, поэтому почти все зрелые системы держат их отдельно — в объектном хранилище (S3-совместимом или собственном), в файловой системе на отдельном томе, иногда вообще у стороннего провайдера CDN.

В базе при этом лежит не сам файл, а его *описание*: имя, размер, MIME-тип и, главное, ссылка — путь до объекта в хранилище или URL. Когда вы запускаете «Экспорт данных», система в подавляющем большинстве случаев выгружает именно то, что лежит в базе — то есть записи и метаданные вложений, включая ссылку. Саму ссылку она честно отдаёт. Но ссылка — это указатель на ресурс, который физически находится на инфраструктуре вендора, а не сам ресурс.

Пока вы клиент — ссылка работает, файл открывается по клику, всё выглядит целостно. Как только договор с вендором заканчивается и доступ к аккаунту закрывается (или истекает срок хранения после отмены подписки — у многих SaaS это 30-90 дней), ссылка возвращает 403 или 404. Данные, по сути, никогда не покидали инфраструктуру вендора — экспортом забрали только их каталог.

Это не злой умысел — почти всегда так устроена архитектура систем изнутри, и вендору технически проще и дешевле сделать один механизм экспорта для структурированных данных (это стандартный SQL-дамп или выгрузка через API), чем городить отдельный, потенциально гигабайты и терабайты, поток бинарных файлов через тот же интерфейс. Функция «Экспорт всех данных» в UI обычно означает «экспорт того, что лежит в основной БД», и это условие редко проговаривается явно в описании кнопки.

Где искать разрыв: типичные места, где вложения теряются

Разрыв между записями и файлами проявляется в предсказуемых местах — если знать, куда смотреть, отследить его несложно.

  • CRM и системы поддержки — прикреплённые файлы к тикетам, сканы документов в карточках клиентов, вложения к письмам в интегрированной почте.
  • Конструкторы сайтов и e-commerce — изображения товаров, загруженные пользователем медиафайлы, PDF-каталоги.
  • Учётные и бухгалтерские сервисы — сканы первичных документов, приложенные к проводкам и актам.
  • Таск-трекеры и wiki — вложения к задачам, картинки в статьях базы знаний, приложенные файлы к комментариям.
  • HR-системы — резюме кандидатов, сканы документов сотрудников, приложенные к анкетам.

Общий признак — в модели данных этих систем есть таблица (или коллекция) с записями и отдельная сущность «attachment» / «file» / «media», связанная с записью внешним ключом. Экспорт записи отдаёт вам этот внешний ключ или прямую ссылку, но не байты файла.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как проверить заранее, а не постфактум

Прежде чем полагаться на кнопку «Экспорт» как на единственный механизм миграции, стоит потратить час на разведку — это дешевле, чем разбирать последствия через месяц.

  1. Найдите документацию по экспорту именно для вашего тарифа. У многих SaaS экспорт вложений — платная функция или доступна только на верхних тарифах (иногда называется «Data Portability», «Bulk Export», «Backup API»).
  2. Сделайте тестовый экспорт на десяти записях с вложениями и откройте результат руками — посчитайте, сколько файлов реально скачалось рядом с данными, а не осталось ссылками.
  3. Проверьте наличие отдельного API для файлов. У систем на объектном хранилище часто есть отдельный эндпоинт вроде /api/v1/attachments/{id}/download, не входящий в общий экспорт данных, но доступный по тому же API-ключу.
  4. Уточните срок жизни ссылок и аккаунта после отмены подписки. Если он есть — это ваше жёсткое окно на скачивание, а не гипотетический запас времени.
  5. Оцените объём — иногда счёт идёт на сотни гигабайт или терабайты, и это уже не «скачать руками», а отдельная инженерная задача с местом на диске, полосой и временем на выполнение.

Если отдельного механизма для файлов нет вовсе — единственный путь чаще всего в лоб: проход по каждой ссылке из выгруженных метаданных и скачивание файла по HTTP, пока доступ ещё жив.

Практический подход: выгрузка файлов отдельным процессом

Когда у вложений нет своего «Экспорт всё за раз», их забирают программно — сценарий, который проходит по списку записей, для каждой достаёт ссылку на вложение и скачивает файл, сохраняя привязку к исходной записи.

Пример на Python — обход JSON-выгрузки записей и докачка вложений по ссылкам из метаданных:

import csv
import hashlib
import json
import time
from pathlib import Path
from urllib.parse import urlparse

import requests

records = json.loads(Path("export_records.json").read_text())
out_dir = Path("attachments")
out_dir.mkdir(exist_ok=True)

manifest_path = Path("attachments_manifest.csv")
with manifest_path.open("w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["record_id", "field", "original_url", "local_path", "sha256", "status"])

    for record in records:
        record_id = record["id"]
        for field, url in record.get("attachments", {}).items():
            if not url:
                continue
            parsed = urlparse(url)
            ext = Path(parsed.path).suffix or ".bin"
            local_name = f"{record_id}_{field}{ext}"
            local_path = out_dir / local_name

            status = "ok"
            digest = ""
            try:
                resp = requests.get(url, timeout=30)
                resp.raise_for_status()
                local_path.write_bytes(resp.content)
                digest = hashlib.sha256(resp.content).hexdigest()
            except requests.RequestException as e:
                status = f"error: {e}"

            writer.writerow([record_id, field, url, str(local_path), digest, status])
            time.sleep(0.2)  # не долбить API вендора без пауз

Ключевая идея — не сам код (у каждого вендора своя структура ссылок и авторизация, часто нужен токен в заголовке или подписанный URL), а манифест: CSV или таблица, где на каждую скачанную запись жёстко зафиксировано, из какой записи, из какого поля и по какой исходной ссылке она получена. Без этого манифеста через неделю у вас будет папка с десятью тысячами файлов вида IMG_20250314.jpg и scan.pdf без единого шанса понять, к какому клиенту или договору какой из них относится — совпадающие исходные имена файлов у разных записей это гарантируют.

Практические нюансы, которые всплывают на реальных выгрузках:

  • Rate limit вендора. Массовая докачка тысяч файлов через API почти всегда упирается в лимит запросов — нужны паузы и retry с экспоненциальной задержкой, иначе выгрузка обрывается на середине без явной ошибки.
  • Истекающие подписанные URL. Ссылки на S3-совместимое хранилище часто подписаны и живут ограниченное время (минуты-часы) — если экспорт метаданных и скачивание файлов разнесены по времени, часть ссылок к моменту скачивания уже не работает, и нужен новый цикл выгрузки метаданных.
  • Дубликаты и переименования. Один и тот же файл может быть прикреплён к нескольким записям — решите заранее, дублировать физически или хранить один раз со ссылками из нескольких записей в манифесте.
  • Проверка целостности. Контрольная сумма (как в примере выше) — это единственный способ достоверно убедиться, что файл скачался целиком, а не оборвался на середине из-за разрыва соединения.

Куда класть выгруженные файлы и как связать их с новой системой

Само по себе скачивание — половина задачи. Вторая половина — сделать так, чтобы в новой системе (или в собственном хранилище) вложения снова были доступны по клику из карточки записи, а не лежали архивом «на всякий случай».

Практичная схема для перехода на свою инфраструктуру:

КомпонентНазначение
Объектное хранилище (S3-совместимое, MinIO или облачный провайдер)Хранение бинарных файлов вложений
Таблица attachments в вашей БДid, record_id, original_vendor_url, storage_key, mime_type, sha256, uploaded_at
Скрипт импортаЧитает манифест, заливает файл в хранилище, пишет строку в attachments со ссылкой на новый storage_key
СверкаКоличество файлов в манифесте = количеству объектов в хранилище = количеству строк в attachments

Такая схема заодно решает смежную проблему, которая часто всплывает при переносе больших объёмов бинарных данных: не пытайтесь класть файлы прямо в реляционную БД как BLOB — это отдельно бьёт по размеру бэкапов и скорости обычных запросов к записям, подробнее это разобрано в статье про антипаттерн хранения файлов в базе данных. Правильное место для файлов — объектное хранилище или файловая система, а в БД — только ссылка и метаданные, ровно так же, как это было устроено у вендора, просто теперь под вашим контролем.

Если объём вложений исчисляется терабайтами, сама миграция файлового хранилища — отдельная инженерная задача с планированием диска, полосы и времени: она разобрана отдельно в статье про перенос файлового хранилища на терабайты.

Сверка после переноса: как убедиться, что ничего не потеряно

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

Минимальный набор сверок, который стоит сделать до того, как отключать доступ к вендору окончательно:

  • Количество. Сколько записей с непустым полем вложения было в исходной выгрузке — и сколько файлов реально лежит в новом хранилище. Расхождение — сигнал разбирать построчно.
  • Статусы в манифесте. Пройтись по колонке status из докачки — все ли ok, или есть error, которые надо перезапустить, пока вендор ещё доступен.
  • Выборочное открытие. Взять 20-30 случайных записей из разных периодов (старые и свежие часто ведут себя по-разному) и открыть вложение руками — не просто убедиться, что файл существует, а что это действительно тот файл: не пустой, не битый, открывается нужным приложением.
  • Контрольные суммы вложений с обеих сторон, если вендор отдаёт sha256 или etag в метаданных — тогда сверка автоматическая, а не выборочная.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Вендор говорит, что экспортирует «все данные» — почему тогда файлов нет?

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

Можно ли доверять автоматическим инструментам миграции между похожими сервисами?

Частично. Такие инструменты обычно умеют переносить структурированные записи через API вендора и вендора-получателя, но вложения переносят далеко не все — и даже когда переносят, стоит сделать свою резервную копию файлов отдельно, потому что после переноса связь «инструмент — оба вендора» может больше не понадобиться, а сверить нечем.

Что делать, если срок доступа к вендору уже истёк и ссылки не открываются?

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

Нужно ли сохранять оригинальные ссылки вендора после переноса?

Да, в манифесте — как минимум на время сверки и как аудиторский след, откуда взялся файл. После подтверждённой успешной сверки эту колонку можно архивировать отдельно от рабочей БД, но не удалять сразу.

Как быть, если у вендора вообще нет API, только веб-интерфейс?

В этом случае остаётся только скачивание руками по одному файлу или через автоматизацию браузера (headless-скрипт, кликающий по ссылкам «скачать») — трудоёмко, но для не слишком большого объёма вложений это рабочий, пусть и медленный, путь.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →