Частный экспорт данных компании
Задание экспорта уже можно поставить в очередь. Теперь определим, что именно оно создаёт и кто может получить результат. Выгрузка — полноценная копия данных компании. Защита таблиц не помогает, если копия сохраняется по общему публичному адресу либо доступна старому участнику после отзыва членства.
В «Пространстве» небольшой экспорт хранится как JSON в строке задания. Он включает организацию, время снимка, проекты и задачи. Готовый результат получает только текущий участник выбранной компании с разрешением чтения. Полный код ExportWorker.cs и маршруты jobs находятся в архиве; файл и ответы при подготовке не создавались выполнением приложения.
Снимок связан одной транзакцией
Worker читает проекты и задачи в одной транзакции RepeatableRead. Это означает, что оба набора относятся к согласованному снимку базы, а не к двум произвольным моментам. Если между независимыми SELECT пользователь создаст задачу или удалит проект, выгрузка могла бы показать дочернюю строку без видимого родителя.
Для небольшого учебного примера транзакция достаточно короткая. На большой компании длительный снимок будет удерживать старые версии строк и потребует отдельного решения: разбиение, ограничение объёма, потоковая запись и срок операции. Нельзя обещать отсутствие нагрузки только потому, что экспорт выполняется в фоне. Он всё равно читает ту же базу.
Ниже приведён ожидаемый фрагмент JSON, а не фактически сформированный файл. Идентификаторы сокращены только в поясняющих значениях имен; в исходнике используются полные UUID.
{
"schemaVersion": 1,
"organizationId": "11111111-1111-1111-1111-111111111111",
"projects": [{ "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", "name": "План сезона", "version": 1 }],
"tasks": []
}
schemaVersion относится к формату выгрузки. Она не равна версии проекта и не увеличивается при переименовании. Если позднее изменится форма вложенной задачи, получатель сможет отличить новый формат. Без этого поля архив из прошлого года пришлось бы распознавать по случайному наличию свойств.
UUID организации в JSON позволяет человеку увидеть происхождение файла, но сам по себе не обеспечивает защиту. Проверка выполняется до выдачи результата. Если файл уже скачан на устройство участника, сервер не может гарантированно отозвать эту локальную копию. Это существенная граница экспорта, которую нельзя скрыть за словом «частный».
Область задания сохраняется в адресе
Маршрут получения имеет форму /orgs/{organizationId}/exports/{jobId}/result. Обработчик ищет строку по обоим значениям и повторно проверяет текущее членство. UUID задания не является паролем. Знание UUID северной выгрузки не даёт Борису получить её через адрес «Юга».
Если задание ещё queued или leased, возвращается 409 с кодом export_not_ready. Если оно завершилось ошибкой, клиент видит состояние failed и предметную причину. Пустой файл в ответе 200 был бы неоднозначным: его можно принять за корректный экспорт пустой компании. Состояние операции и содержимое результата должны различаться.
Перед выдачей проверяется result_expires_at. После срока результат недоступен, даже если строка задания пока сохраняется для истории. Отдельная очистка может удалить JSON позднее. Такое разделение позволяет немедленно прекратить скачивание без необходимости синхронно чистить большой объект прямо в HTTP-запросе.
Content-Disposition использует безопасное серверное имя, например space-export-{jobId}.json. Не вставляйте необработанное название компании в заголовок. Пользовательское имя может содержать кавычки или управляющие символы. Видимое название можно включить в JSON, а имя файла сформировать из контролируемых символов.
Какие данные входят в выгрузку
Наш формат включает действующие проекты и задачи видимых проектов. Удалённые проекты и аудит не добавляются автоматически. Для полного юридического либо архивного экспорта понадобился бы отдельный контракт состава. Учебная выгрузка предназначена для переноса рабочей структуры, поэтому её название и описание не обещают «абсолютно все данные».
В частности, исходные токены приглашений не существуют в базе после выдачи и не попадают в экспорт. Email других пользователей не нужен структуре проектов. Справочник тарифов следующего курса является общим, а финансовая история имеет отдельный срок и формат. Добавление каждого набора требует собственного решения о правах и назначении файла.
Задачи отбираются через связь с действующим проектом и той же организацией. RLS дополнительно ограничивает обе таблицы. Не считайте, что защита родителя автоматически ограничит любой запрос к дочерней таблице: отдельный SELECT задач должен иметь собственную область и политику. В полном проекте это видно и в SQL, и в схеме.
Время снимка хранится в результате. Время завершения задания может быть позднее, а время скачивания ещё позднее. Если участник создал проект после снимка, его отсутствие в файле ожидаемо. Сообщение интерфейса должно объяснять эту границу; иначе человек воспримет корректный старый снимок как потерю данных.
Отзыв доступа и очистка результата
Если Вера инициировала экспорт, а Анна отозвала её членство до обработки, worker завершает его отказом. Если Вера скачивала уже готовый результат после отзыва, HTTP-проверка также не разрешает выдачу. Эти проверки закрывают два разных промежутка времени. Только проверка при постановке задания оставила бы долгоживущее право старому участнику.
При переводе компании в deleting готовые пользовательские выгрузки также перестают выдаваться в обычном маршруте. Поэтому владельцу следует предложить получить экспорт до начала окончательной процедуры. Реальный продукт может предусмотреть специальное окно выгрузки после закрытия, но оно требует отдельного права и срока, а не случайного обхода состояния.
Для ручного наблюдения создайте два задания: северное от Анны и южное от Бориса. Ожидаемые JSON имеют разные organizationId и разные проекты. Поменяйте jobId местами, оставив адреса компаний: оба обращения должны получить 404. Затем завершите срок северного результата: наличие completed больше не даёт скачивание.
Ошибка результата не становится публичным файлом
Failed-задание не выдаёт внутренний stack trace вместо JSON. Такой ответ мог бы раскрыть путь сервера, запросы и параметры компании. Состояние возвращает небольшой код ошибки, а подробная причина остаётся в ограниченной диагностике. Completed без сохранённого результата тоже не считается корректным экспортом: в схеме обработки эти два изменения фиксируются вместе. Для больших файлов такой же контракт должен связывать существование частного объекта хранения и готовность строки задания.
Теперь экспорт имеет состав, согласованный снимок, срок и защищённый путь получения. Заключительный урок рассмотрит приостановку и удаление организации, связывая проекты, членства, приглашения и фоновые копии в одну процедуру жизненного цикла.