This commit is contained in:
seb
2026-10-14 15:07:47 +02:00
parent b8fdef4e11
commit a0d71f70b7
3 changed files with 187 additions and 3 deletions

View File

@@ -193,4 +193,144 @@ export const company = {
});
},
// #endregion
/**
* GET /api/company/export
* Packages company catalog and streams export (.zip) archive via Server-Sent Events with base64 chunks.
*
* @param {object} [callbacks]
* @param {(message: string) => void} [callbacks.onStatus]
* @param {(progress: { stage: string, percent?: number, [key: string]: any }) => void} [callbacks.onProgress]
* @param {(chunk: string) => void} [callbacks.onChunk]
* @param {(donePayload: { ok: boolean, filename: string, size: number, totalChunks: number, stats?: object }) => void} [callbacks.onDone]
* @param {(error: string) => void} [callbacks.onError]
* @param {object} [options]
* @param {AbortSignal} [options.signal]
* @param {boolean} [options.triggerDownload=true] Automatically trigger browser download when complete
* @returns {Promise<{ blob: Blob, filename: string, size: number, totalChunks: number, stats?: object }>}
*/
// #region GET /api/company/export — exportCatalog
async exportCatalog(callbacks = {}, { signal, triggerDownload = true } = {}) {
const response = await fetch('/api/company/export', {
method: 'GET',
headers: { Accept: 'text/event-stream' },
credentials: 'include',
signal,
});
if (!response.ok) {
let errMessage = 'Export failed';
try {
const error = await response.json();
errMessage = error.error || error.message || errMessage;
} catch {
// ignore JSON parse error
}
throw new Error(errMessage);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const base64Chunks = [];
let filename = 'company-export.zip';
let donePayload = null;
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n\n');
buffer = parts.pop() ?? ''; // keep trailing incomplete message
for (const part of parts) {
const lines = part.split('\n');
let event = 'message';
let data = '';
for (const line of lines) {
if (line.startsWith('event: ')) event = line.slice(7).trim();
else if (line.startsWith('data: ')) data = line.slice(6);
}
if (event === 'status') {
try {
const payload = JSON.parse(data);
callbacks.onStatus?.(payload.message || data);
} catch {
callbacks.onStatus?.(data);
}
} else if (event === 'progress') {
try {
const payload = JSON.parse(data);
callbacks.onProgress?.(payload);
} catch {
// ignore
}
} else if (event === 'chunk') {
base64Chunks.push(data);
callbacks.onChunk?.(data);
} else if (event === 'done') {
try {
const payload = JSON.parse(data);
donePayload = payload;
if (payload.filename) filename = payload.filename;
callbacks.onDone?.(payload);
} catch {
// ignore
}
} else if (event === 'error') {
let errorMsg = 'Export error';
try {
const payload = JSON.parse(data);
errorMsg = payload.message || errorMsg;
} catch {
errorMsg = data || errorMsg;
}
callbacks.onError?.(errorMsg);
throw new Error(errorMsg);
}
}
}
} finally {
reader.releaseLock();
}
// Reconstruct Blob from base64 chunks
const byteArrays = base64Chunks.map((b64) => {
const binaryStr = atob(b64);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return bytes;
});
const blob = new Blob(byteArrays, { type: 'application/zip' });
if (triggerDownload) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
return {
blob,
filename,
size: blob.size,
totalChunks: base64Chunks.length,
stats: donePayload?.stats,
};
},
// #endregion
};
export const exportCompanyCatalog = (callbacks, options) => company.exportCatalog(callbacks, options);