All checks were successful
CD / Build and push images (push) Successful in 10m39s
CI / Lint, typecheck, test (push) Successful in 3m12s
CI / Auth e2e pack (push) Successful in 4m9s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
A signed-in account can export all of its own data — profile, a list of its memberships/grants, and the Markdown of its personal pond plus the shared ponds it owns — as one ZIP. Foreign content never appears: only owned ponds are bundled and the per-page read filter (reused from #65) runs for each. - Reuse the conversion-job queue as the async carrier: a `data_export` job whose worker branch resolves DataExportService via a token (no DI cycle), builds the ZIP, and stores it with an `expiresAt`. The download link 404s past expiry and an hourly scheduled purge drops the bytes (data minimization, security.md §Privacy). - Extract ExportService.appendPondMarkdown so the pond ZIP (#65) and the data export share one read-filtered pond archiver. - Rate-limit requests per account (RateLimitService); POST /users/me/data-export enqueues, GET /jobs/:id(/result) poll/download. - Settings UI "Export my data" (de+en); web share pollJob/downloadJobResult between the document and data export hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
import type { ConversionJobView } from '@dorfteich/shared';
|
|
|
|
import { ApiError, apiGet } from '../lib/api';
|
|
|
|
const POLL_INTERVAL_MS = 1000;
|
|
/** Default poll budget — a whole-account export (#68) can take longer than a
|
|
* single-page conversion, so callers may raise or lower it. */
|
|
const DEFAULT_MAX_POLLS = 300;
|
|
|
|
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
/** Poll a conversion job until it succeeds or fails, or the poll budget runs
|
|
* out (returns whatever the last status was). Shared by every export hook. */
|
|
export async function pollJob(
|
|
job: ConversionJobView,
|
|
maxPolls = DEFAULT_MAX_POLLS,
|
|
): Promise<ConversionJobView> {
|
|
let current = job;
|
|
for (
|
|
let poll = 0;
|
|
current.status !== 'succeeded' && current.status !== 'failed' && poll < maxPolls;
|
|
poll += 1
|
|
) {
|
|
await delay(POLL_INTERVAL_MS);
|
|
current = await apiGet<ConversionJobView>(`/jobs/${current.id}`);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/** Download a finished job's result and save it as `fileName`. The result
|
|
* endpoint is session-authenticated, so a plain `fetch` carries the cookie. */
|
|
export async function downloadJobResult(jobId: string, fileName: string): Promise<void> {
|
|
const response = await fetch(`/api/v1/jobs/${jobId}/result`);
|
|
if (!response.ok) {
|
|
throw new ApiError(response.status, { code: `http_${response.status}`, message: '' });
|
|
}
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = fileName;
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|