Реализация права на экспорт данных (Data Portability) в мобильном приложении
GDPR Article 20 и App Store Review Guideline 5.1.1 требуют предоставить пользователю возможность получить копию своих данных в машиночитаемом формате. Apple с 2022 года активно проверяет это при ревью приложений, работающих с персональными данными. Отсутствие функции экспорта — реальная причина отклонения.
Что включать в экспорт
Минимальный набор по GDPR: все данные, которые пользователь предоставил напрямую (профиль, настройки, контент), и данные, созданные в результате использования сервиса (история действий, транзакции, предпочтения).
Не обязательно включать: производные данные (analytics aggregates, ML-модели), технические логи (server access logs), данные других пользователей.
Форматы: JSON предпочтителен для machine-readability, CSV — для пользователей, которые хотят открыть в Excel. Архив ZIP с несколькими файлами — стандартная практика (как у Google Takeout).
Серверная реализация экспорта
Экспорт — потенциально тяжёлая операция. Не делайте синхронный ответ на HTTP-запрос:
POST /api/user/export-request
→ 202 Accepted { "job_id": "exp_xxxx", "estimated_minutes": 5 }
GET /api/user/export-request/exp_xxxx
→ 200 { "status": "processing" | "ready", "download_url": "...", "expires_at": "..." }
Фоновая задача (Celery, Sidekiq, Laravel Queue) собирает данные из всех таблиц, формирует архив, загружает в S3/хранилище с presigned URL на 24–72 часа. После завершения — push-уведомление или email.
Presigned URL с TTL критичен: не отдавайте прямые ссылки на S3 без авторизации — это утечка данных.
Клиентский флоу
// iOS — запрос экспорта и polling статуса
class DataExportViewModel: ObservableObject {
@Published var exportState: ExportState = .idle
func requestExport() async {
exportState = .requesting
let job = try await api.requestDataExport()
exportState = .processing(jobID: job.id)
await pollStatus(jobID: job.id)
}
private func pollStatus(jobID: String) async {
while true {
try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 секунд
let status = try await api.getExportStatus(jobID: jobID)
if status.isReady {
exportState = .ready(downloadURL: status.downloadURL!)
return
}
}
}
}
При получении ready — предлагаем пользователю сохранить файл через UIDocumentPickerViewController (iOS) или ActivityResultContracts.CreateDocument (Android). Не сохраняем в Documents автоматически без согласия.
Ограничение частоты запросов
Пользователь не должен иметь возможность запрашивать экспорт каждые 5 минут — это нагрузка на БД. Разумный лимит: один запрос в 24–48 часов. Отображаем дату последнего экспорта и время до следующей возможности.
Сроки — 1–3 дня: серверная очередь экспорта, сбор данных из всех источников, формирование архива, клиентский UI с polling и скачиванием.







