Экспорт забрал записи, а вложения остались у вендора
Компания решает уйти от внешнего сервиса — 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, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКак проверить заранее, а не постфактум
Прежде чем полагаться на кнопку «Экспорт» как на единственный механизм миграции, стоит потратить час на разведку — это дешевле, чем разбирать последствия через месяц.
- Найдите документацию по экспорту именно для вашего тарифа. У многих SaaS экспорт вложений — платная функция или доступна только на верхних тарифах (иногда называется «Data Portability», «Bulk Export», «Backup API»).
- Сделайте тестовый экспорт на десяти записях с вложениями и откройте результат руками — посчитайте, сколько файлов реально скачалось рядом с данными, а не осталось ссылками.
- Проверьте наличие отдельного API для файлов. У систем на объектном хранилище часто есть отдельный эндпоинт вроде
/api/v1/attachments/{id}/download, не входящий в общий экспорт данных, но доступный по тому же API-ключу. - Уточните срок жизни ссылок и аккаунта после отмены подписки. Если он есть — это ваше жёсткое окно на скачивание, а не гипотетический запас времени.
- Оцените объём — иногда счёт идёт на сотни гигабайт или терабайты, и это уже не «скачать руками», а отдельная инженерная задача с местом на диске, полосой и временем на выполнение.
Если отдельного механизма для файлов нет вовсе — единственный путь чаще всего в лоб: проход по каждой ссылке из выгруженных метаданных и скачивание файла по 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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →