u
This commit is contained in:
@@ -7,6 +7,7 @@ import * as googleAuth from '../../db/googleAuth.js';
|
||||
import { clearCachedUser } from '../middleware/auth.js';
|
||||
import { initCompanyVfs } from '../../tools/vfs.js';
|
||||
import { executeCompanyImport } from '../../services/companyImport.js';
|
||||
import { executeCompanyExport } from '../../services/companyExport.js';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
@@ -605,6 +606,189 @@ export function register(app, basePath = '') {
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /company/export:
|
||||
* get:
|
||||
* tags: [Company]
|
||||
* summary: Export company catalog as zip archive (SSE)
|
||||
* description: |
|
||||
* Exports full company catalog data (categories hierarchy, articles, variants, brands, pictures)
|
||||
* into a round-trip compatible .zip archive matching the format of `/company/import`.
|
||||
* Streams progress events via Server-Sent Events (`text/event-stream`), followed by base64-encoded
|
||||
* binary chunks in `chunk` events and a final `done` event.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: company/export.md
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: stream
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: ['0', '1']
|
||||
* description: Force SSE stream mode (`1`) or direct binary download (`0`)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* application/zip:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* 400:
|
||||
* description: Bad request (no company selected)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* post:
|
||||
* tags: [Company]
|
||||
* summary: Export company catalog as zip archive (SSE)
|
||||
* description: POST variant of `/company/export` with identical SSE streaming semantics.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* application/zip:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* 400:
|
||||
* description: Bad request (no company selected)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region GET/POST /company/export
|
||||
|
||||
const handleCompanyExport = async (req, res) => {
|
||||
let companyId;
|
||||
try {
|
||||
companyId = requireSelectedCompany(req);
|
||||
} catch (error) {
|
||||
return res.status(error.statusCode || 400).json({ error: error.message });
|
||||
}
|
||||
|
||||
const wantsSse =
|
||||
req.headers.accept?.includes('text/event-stream') ||
|
||||
req.query.stream === '1' ||
|
||||
req.query.sse === '1' ||
|
||||
(!req.headers.accept?.includes('application/zip') && !req.path.endsWith('.zip') && req.query.download !== '1');
|
||||
|
||||
if (wantsSse) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
if (typeof res.flushHeaders === 'function') {
|
||||
res.flushHeaders();
|
||||
}
|
||||
|
||||
const sseWrite = (event, data) => {
|
||||
if (res.writableEnded || res.destroyed) return;
|
||||
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
res.write(`event: ${event}\ndata: ${payload}\n\n`);
|
||||
if (typeof res.flush === 'function') res.flush();
|
||||
};
|
||||
|
||||
sseWrite('status', { message: 'Export gestartet. Daten werden verarbeitet...' });
|
||||
|
||||
try {
|
||||
const outcome = await executeCompanyExport({
|
||||
googleAuthId: req.user.id,
|
||||
companyId,
|
||||
onStatus: (status) => sseWrite('status', status),
|
||||
onProgress: (counts) => sseWrite('progress', counts),
|
||||
});
|
||||
|
||||
// Stream binary chunks as base64 in SSE 'chunk' events
|
||||
const CHUNK_SIZE = 64 * 1024; // 64 KB binary slice per SSE event
|
||||
const { zipBuffer, filename, stats } = outcome;
|
||||
const totalChunks = Math.ceil(zipBuffer.length / CHUNK_SIZE);
|
||||
|
||||
sseWrite('status', {
|
||||
message: `Sende Archiv (${(zipBuffer.length / (1024 * 1024)).toFixed(2)} MB in ${totalChunks} Chunks)...`,
|
||||
});
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
if (res.writableEnded || res.destroyed) break;
|
||||
const slice = zipBuffer.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
||||
const base64 = slice.toString('base64');
|
||||
res.write(`event: chunk\ndata: ${base64}\n\n`);
|
||||
if (typeof res.flush === 'function') res.flush();
|
||||
}
|
||||
|
||||
sseWrite('done', {
|
||||
ok: true,
|
||||
filename,
|
||||
size: zipBuffer.length,
|
||||
totalChunks,
|
||||
stats,
|
||||
});
|
||||
res.end();
|
||||
} catch (err) {
|
||||
console.error('[Company Export] Export execution failed:', err);
|
||||
sseWrite('error', { message: err.message || 'Export fehlgeschlagen' });
|
||||
res.end();
|
||||
}
|
||||
} else {
|
||||
// Direct binary download
|
||||
try {
|
||||
const outcome = await executeCompanyExport({
|
||||
googleAuthId: req.user.id,
|
||||
companyId,
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${outcome.filename}"`);
|
||||
res.status(200).send(outcome.zipBuffer);
|
||||
} catch (err) {
|
||||
console.error('[Company Export] Export failed:', err);
|
||||
const status = companyErrorStatus(err);
|
||||
res.status(status).json({ error: err.message || 'Export fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app.get(`${basePath}/company/export`, handleCompanyExport);
|
||||
app.get(`${basePath}/company/export.zip`, handleCompanyExport);
|
||||
app.post(`${basePath}/company/export`, handleCompanyExport);
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /company/{id}:
|
||||
|
||||
Reference in New Issue
Block a user