@cogenta/export 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +373 -0
  2. package/dist/backup.d.ts +64 -0
  3. package/dist/backup.d.ts.map +1 -0
  4. package/dist/backup.js +79 -0
  5. package/dist/backup.js.map +1 -0
  6. package/dist/content-export.d.ts +49 -0
  7. package/dist/content-export.d.ts.map +1 -0
  8. package/dist/content-export.js +197 -0
  9. package/dist/content-export.js.map +1 -0
  10. package/dist/content-import.d.ts +44 -0
  11. package/dist/content-import.d.ts.map +1 -0
  12. package/dist/content-import.js +221 -0
  13. package/dist/content-import.js.map +1 -0
  14. package/dist/crypto.d.ts +18 -0
  15. package/dist/crypto.d.ts.map +1 -0
  16. package/dist/crypto.js +151 -0
  17. package/dist/crypto.js.map +1 -0
  18. package/dist/format.d.ts +133 -0
  19. package/dist/format.d.ts.map +1 -0
  20. package/dist/format.js +71 -0
  21. package/dist/format.js.map +1 -0
  22. package/dist/gdpr.d.ts +79 -0
  23. package/dist/gdpr.d.ts.map +1 -0
  24. package/dist/gdpr.js +84 -0
  25. package/dist/gdpr.js.map +1 -0
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +24 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/media-export.d.ts +33 -0
  31. package/dist/media-export.d.ts.map +1 -0
  32. package/dist/media-export.js +64 -0
  33. package/dist/media-export.js.map +1 -0
  34. package/dist/restore.d.ts +53 -0
  35. package/dist/restore.d.ts.map +1 -0
  36. package/dist/restore.js +173 -0
  37. package/dist/restore.js.map +1 -0
  38. package/dist/tables.d.ts +28 -0
  39. package/dist/tables.d.ts.map +1 -0
  40. package/dist/tables.js +26 -0
  41. package/dist/tables.js.map +1 -0
  42. package/dist/zip-reader.d.ts +13 -0
  43. package/dist/zip-reader.d.ts.map +1 -0
  44. package/dist/zip-reader.js +97 -0
  45. package/dist/zip-reader.js.map +1 -0
  46. package/dist/zip-writer.d.ts +16 -0
  47. package/dist/zip-writer.d.ts.map +1 -0
  48. package/dist/zip-writer.js +143 -0
  49. package/dist/zip-writer.js.map +1 -0
  50. package/package.json +44 -0
@@ -0,0 +1,173 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { CogentaError, identifier, sql, unsafeRaw, } from '@cogenta/core';
3
+ import { decryptStream } from './crypto.js';
4
+ import { openZip } from './zip-reader.js';
5
+ async function bufferEntry(zip, name) {
6
+ const chunks = [];
7
+ for await (const chunk of zip.read(name))
8
+ chunks.push(chunk);
9
+ return Buffer.concat(chunks);
10
+ }
11
+ export async function readBackupManifest(path) {
12
+ const zip = await openZip(path);
13
+ try {
14
+ const raw = await bufferEntry(zip, 'manifest.json');
15
+ return JSON.parse(raw.toString('utf8'));
16
+ }
17
+ finally {
18
+ await zip.close();
19
+ }
20
+ }
21
+ function tablePlaintext(zip, table, manifest, passphrase) {
22
+ const raw = zip.read(table.file);
23
+ if (!manifest.encrypted)
24
+ return raw;
25
+ if (passphrase === undefined) {
26
+ throw new CogentaError({
27
+ code: 'BACKUP_PASSPHRASE_REQUIRED',
28
+ message: 'This backup is encrypted; a passphrase is required to read it.',
29
+ hint: 'Pass the passphrase used at `cogenta backup --encrypt` time.',
30
+ });
31
+ }
32
+ return decryptStream(raw, passphrase);
33
+ }
34
+ /**
35
+ * Recomputes the manifest's checksum over the backup's own bytes and throws
36
+ * if it does not match — task 3's "somme de contrôle vérifiée avant
37
+ * restauration", called by `applyRestore` before it writes a single row, and
38
+ * exposed on its own for a caller (the admin's restore screen, `cogenta
39
+ * restore --dry-run`) that wants to check a file without touching a database.
40
+ */
41
+ export async function verifyBackup(path, options = {}) {
42
+ const manifest = await readBackupManifest(path);
43
+ const zip = await openZip(path);
44
+ try {
45
+ const hash = createHash('sha256');
46
+ for (const table of manifest.tables) {
47
+ for await (const chunk of tablePlaintext(zip, table, manifest, options.passphrase)) {
48
+ hash.update(chunk);
49
+ }
50
+ }
51
+ const checksum = hash.digest('hex');
52
+ if (checksum !== manifest.checksum) {
53
+ throw new CogentaError({
54
+ code: 'BACKUP_CHECKSUM_MISMATCH',
55
+ message: 'The backup file does not match its own checksum.',
56
+ hint: 'The file is corrupted or was modified after it was created. Restore from a different copy.',
57
+ details: { expected: manifest.checksum, found: checksum },
58
+ });
59
+ }
60
+ return manifest;
61
+ }
62
+ finally {
63
+ await zip.close();
64
+ }
65
+ }
66
+ /**
67
+ * "Ce qui sera écrasé, ce qui sera ajouté" (task 4) — a read-only pass that
68
+ * verifies the checksum and, for every table the target database already has
69
+ * created, counts what is already there. A table this database has never
70
+ * created (a fresh site, mid-migration) reports `rowsExisting: 0` rather than
71
+ * failing, since that is exactly the case an empty-site restore expects.
72
+ */
73
+ export async function previewRestore(path, db, options = {}) {
74
+ const manifest = await verifyBackup(path, options);
75
+ const tables = [];
76
+ for (const table of manifest.tables) {
77
+ let rowsExisting = 0;
78
+ try {
79
+ const result = await db.query(sql `select count(*) as n from ${identifier(table.name, db.dialect)}`);
80
+ rowsExisting = Number(result.rows[0]?.n ?? 0);
81
+ }
82
+ catch {
83
+ // The table does not exist yet on this database — a fresh site being
84
+ // restored into before its schema has been created. That is reported
85
+ // as "nothing there yet", not as a failure of the preview.
86
+ rowsExisting = 0;
87
+ }
88
+ tables.push({ name: table.name, rowsInBackup: table.rows, rowsExisting });
89
+ }
90
+ return { manifest, tables };
91
+ }
92
+ function* splitLines(buffer) {
93
+ let start = 0;
94
+ for (;;) {
95
+ const index = buffer.indexOf('\n', start);
96
+ if (index === -1) {
97
+ if (start < buffer.length)
98
+ yield buffer.slice(start);
99
+ return;
100
+ }
101
+ yield buffer.slice(start, index);
102
+ start = index + 1;
103
+ }
104
+ }
105
+ /**
106
+ * Restores every table of a backup, in the manifest's own order — the same
107
+ * dependency order `buildBackupTables` produced it in, so a foreign key never
108
+ * meets a row it references before that row exists.
109
+ *
110
+ * **Full restore only, by design (task 4).** This function does not decide
111
+ * who may call it; `packages/cli`'s `cogenta restore` is the only caller
112
+ * this codebase gives a full restore to — the admin API restores a *content*
113
+ * export instead (`importContent`), never a whole-database backup, because
114
+ * that would let a browser session overwrite the database it is itself
115
+ * running against.
116
+ */
117
+ export async function applyRestore(path, options) {
118
+ const manifest = await verifyBackup(path, options.passphrase === undefined ? {} : { passphrase: options.passphrase });
119
+ const zip = await openZip(path);
120
+ const report = [];
121
+ try {
122
+ for (const table of manifest.tables) {
123
+ let rows = 0;
124
+ let carry = '';
125
+ const tableIdentifier = identifier(table.name, options.db.dialect);
126
+ for await (const chunk of tablePlaintext(zip, table, manifest, options.passphrase)) {
127
+ carry += chunk.toString('utf8');
128
+ const lastNewline = carry.lastIndexOf('\n');
129
+ if (lastNewline === -1)
130
+ continue;
131
+ const complete = carry.slice(0, lastNewline);
132
+ carry = carry.slice(lastNewline + 1);
133
+ for (const line of splitLines(complete)) {
134
+ if (line.trim().length === 0)
135
+ continue;
136
+ await insertRow(options.db, tableIdentifier, table.name, JSON.parse(line));
137
+ rows += 1;
138
+ }
139
+ }
140
+ if (carry.trim().length > 0) {
141
+ await insertRow(options.db, tableIdentifier, table.name, JSON.parse(carry));
142
+ rows += 1;
143
+ }
144
+ report.push({ name: table.name, rows });
145
+ }
146
+ }
147
+ finally {
148
+ await zip.close();
149
+ }
150
+ return { tables: report };
151
+ }
152
+ function joinFragments(parts, separator) {
153
+ return parts.reduce((acc, next, index) => (index === 0 ? next : sql `${acc}${unsafeRaw(separator)}${next}`), unsafeRaw(''));
154
+ }
155
+ async function insertRow(db, table, tableName, row) {
156
+ const columns = Object.keys(row);
157
+ if (columns.length === 0)
158
+ return;
159
+ const columnList = joinFragments(columns.map((column) => identifier(column, db.dialect)), ', ');
160
+ const valueList = joinFragments(columns.map((column) => sql `${row[column]}`), ', ');
161
+ try {
162
+ await db.query(sql `insert into ${table} (${columnList}) values (${valueList})`);
163
+ }
164
+ catch (cause) {
165
+ throw new CogentaError({
166
+ code: 'RESTORE_CONFLICT',
167
+ message: `Could not insert a row into "${tableName}" while restoring.`,
168
+ hint: 'The target database is probably not empty. Restore into a freshly created database.',
169
+ cause,
170
+ });
171
+ }
172
+ }
173
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.js","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type CollectionDefinition, type TaxonomyDefinition } from '@cogenta/schema';
2
+ export interface BuildBackupTablesOptions {
3
+ readonly collections: readonly CollectionDefinition[];
4
+ readonly taxonomies: readonly TaxonomyDefinition[];
5
+ /**
6
+ * Tables outside the content schema that content may reference — users,
7
+ * media — listed first so a foreign key in a content table always finds
8
+ * its target already restored. Order is the caller's: `@cogenta/export`
9
+ * does not know `@cogenta/auth` or `@cogenta/core`'s media table exists,
10
+ * by design (it depends on neither), so it trusts whoever assembled the
11
+ * site to have put users before sessions and so on.
12
+ */
13
+ readonly before?: readonly string[];
14
+ /**
15
+ * Tables that may reference content — navigation menus, redirects, a
16
+ * commerce catalogue — listed last for the same reason, in reverse.
17
+ */
18
+ readonly after?: readonly string[];
19
+ }
20
+ /**
21
+ * Every physical table backup task 3 needs to know about, in an order a
22
+ * forward-only restore can insert into without ever meeting a foreign key
23
+ * before its target: `before`, then taxonomy terms, then content in
24
+ * dependency order (`orderByDependency`, the same helper table creation
25
+ * itself uses), then `after`.
26
+ */
27
+ export declare function buildBackupTables(options: BuildBackupTablesOptions): readonly string[];
28
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAKzB,KAAK,kBAAkB,EAGxB,MAAM,iBAAiB,CAAA;AAExB,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAA;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,SAAS,MAAM,EAAE,CAkBtF"}
package/dist/tables.js ADDED
@@ -0,0 +1,26 @@
1
+ import { blocksTable, entriesTable, orderByDependency, relationsOf, relationTable, taxonomyTable, versionsTable, } from '@cogenta/schema';
2
+ /**
3
+ * Every physical table backup task 3 needs to know about, in an order a
4
+ * forward-only restore can insert into without ever meeting a foreign key
5
+ * before its target: `before`, then taxonomy terms, then content in
6
+ * dependency order (`orderByDependency`, the same helper table creation
7
+ * itself uses), then `after`.
8
+ */
9
+ export function buildBackupTables(options) {
10
+ const tables = [...(options.before ?? [])];
11
+ for (const taxonomy of options.taxonomies) {
12
+ tables.push(taxonomyTable(taxonomy.name));
13
+ }
14
+ for (const collection of orderByDependency(options.collections)) {
15
+ tables.push(entriesTable(collection.name));
16
+ tables.push(versionsTable(collection.name));
17
+ tables.push(blocksTable(collection.name));
18
+ for (const relation of relationsOf(collection)) {
19
+ if (relation.many)
20
+ tables.push(relationTable(collection.name, relation.field));
21
+ }
22
+ }
23
+ tables.push(...(options.after ?? []));
24
+ return tables;
25
+ }
26
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAEX,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,aAAa,EAEb,aAAa,EACb,aAAa,GACd,MAAM,iBAAiB,CAAA;AAqBxB;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IACjE,MAAM,MAAM,GAAa,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAA;IAEpD,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3C,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,iBAAiB,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAChE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QAC1C,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QACzC,KAAK,MAAM,QAAQ,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/C,IAAI,QAAQ,CAAC,IAAI;gBAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;IACrC,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface ZipEntry {
2
+ readonly name: string;
3
+ readonly size: number;
4
+ readonly localHeaderOffset: number;
5
+ }
6
+ export interface ZipReader {
7
+ readonly entries: readonly ZipEntry[];
8
+ /** Streams one entry's raw (stored, uncompressed) bytes in fixed-size chunks. */
9
+ read(name: string): AsyncGenerator<Buffer>;
10
+ close(): Promise<void>;
11
+ }
12
+ export declare function openZip(path: string): Promise<ZipReader>;
13
+ //# sourceMappingURL=zip-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-reader.d.ts","sourceRoot":"","sources":["../src/zip-reader.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAA;IACrC,iFAAiF;IACjF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAWD,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA4D9D"}
@@ -0,0 +1,97 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { CogentaError } from '@cogenta/core';
3
+ /**
4
+ * Reads back what `zip-writer.ts` writes: a store-mode ZIP with a trailing
5
+ * data descriptor per entry. Random-access, via the file's own end-of-central-
6
+ * directory record — a backup or media archive is read from disk (never from
7
+ * an in-memory buffer), so seeking to exactly the bytes one entry needs costs
8
+ * nothing extra, and the file itself is never read in full.
9
+ */
10
+ const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
11
+ const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
12
+ const LOCAL_HEADER_FIXED_LENGTH = 30;
13
+ /** The end-of-central-directory record is 22 bytes, plus up to 65535 bytes of comment this writer never sets — 4 KiB covers any real file. */
14
+ const EOCD_SEARCH_WINDOW = 4096;
15
+ function notAnArchive(cause) {
16
+ return new CogentaError({
17
+ code: 'EXPORT_FORMAT_INVALID',
18
+ message: 'This file is not a Cogenta archive (no end-of-central-directory record found).',
19
+ hint: 'Restore only files produced by `cogenta backup` / `@cogenta/export`.',
20
+ ...(cause === undefined ? {} : { cause }),
21
+ });
22
+ }
23
+ export async function openZip(path) {
24
+ const handle = await open(path, 'r');
25
+ const stat = await handle.stat();
26
+ const tailLength = Math.min(EOCD_SEARCH_WINDOW, stat.size);
27
+ const tail = Buffer.alloc(tailLength);
28
+ await handle.read(tail, 0, tailLength, stat.size - tailLength);
29
+ let eocdOffset = -1;
30
+ for (let i = tail.length - 22; i >= 0; i -= 1) {
31
+ if (tail.readUInt32LE(i) === END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
32
+ eocdOffset = i;
33
+ break;
34
+ }
35
+ }
36
+ if (eocdOffset === -1) {
37
+ await handle.close();
38
+ throw notAnArchive();
39
+ }
40
+ const entryCount = tail.readUInt16LE(eocdOffset + 10);
41
+ const centralDirectorySize = tail.readUInt32LE(eocdOffset + 12);
42
+ const centralDirectoryStart = tail.readUInt32LE(eocdOffset + 16);
43
+ const centralDirectory = Buffer.alloc(centralDirectorySize);
44
+ await handle.read(centralDirectory, 0, centralDirectorySize, centralDirectoryStart);
45
+ const entries = [];
46
+ let cursor = 0;
47
+ for (let i = 0; i < entryCount; i += 1) {
48
+ if (centralDirectory.readUInt32LE(cursor) !== CENTRAL_DIRECTORY_SIGNATURE) {
49
+ await handle.close();
50
+ throw notAnArchive();
51
+ }
52
+ const size = centralDirectory.readUInt32LE(cursor + 24);
53
+ const nameLength = centralDirectory.readUInt16LE(cursor + 28);
54
+ const extraLength = centralDirectory.readUInt16LE(cursor + 30);
55
+ const commentLength = centralDirectory.readUInt16LE(cursor + 32);
56
+ const localHeaderOffset = centralDirectory.readUInt32LE(cursor + 42);
57
+ const nameStart = cursor + 46;
58
+ const name = centralDirectory.toString('utf8', nameStart, nameStart + nameLength);
59
+ entries.push({ name, size, localHeaderOffset });
60
+ cursor = nameStart + nameLength + extraLength + commentLength;
61
+ }
62
+ return {
63
+ entries,
64
+ async *read(name) {
65
+ const entry = entries.find((candidate) => candidate.name === name);
66
+ if (entry === undefined) {
67
+ throw new CogentaError({
68
+ code: 'EXPORT_FORMAT_INVALID',
69
+ message: `The archive has no entry named "${name}".`,
70
+ hint: 'The archive is incomplete or was not produced by this package.',
71
+ details: { name },
72
+ });
73
+ }
74
+ yield* readEntryBody(handle, entry);
75
+ },
76
+ close: () => handle.close(),
77
+ };
78
+ }
79
+ async function* readEntryBody(handle, entry) {
80
+ const header = Buffer.alloc(LOCAL_HEADER_FIXED_LENGTH);
81
+ await handle.read(header, 0, LOCAL_HEADER_FIXED_LENGTH, entry.localHeaderOffset);
82
+ const nameLength = header.readUInt16LE(26);
83
+ const extraLength = header.readUInt16LE(28);
84
+ const dataStart = entry.localHeaderOffset + LOCAL_HEADER_FIXED_LENGTH + nameLength + extraLength;
85
+ const CHUNK_SIZE = 64 * 1024;
86
+ let remaining = entry.size;
87
+ let position = dataStart;
88
+ while (remaining > 0) {
89
+ const size = Math.min(CHUNK_SIZE, remaining);
90
+ const buffer = Buffer.alloc(size);
91
+ await handle.read(buffer, 0, size, position);
92
+ yield buffer;
93
+ remaining -= size;
94
+ position += size;
95
+ }
96
+ }
97
+ //# sourceMappingURL=zip-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-reader.js","sourceRoot":"","sources":["../src/zip-reader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C;;;;;;GAMG;AAEH,MAAM,kCAAkC,GAAG,UAAU,CAAA;AACrD,MAAM,2BAA2B,GAAG,UAAU,CAAA;AAC9C,MAAM,yBAAyB,GAAG,EAAE,CAAA;AACpC,8IAA8I;AAC9I,MAAM,kBAAkB,GAAG,IAAI,CAAA;AAe/B,SAAS,YAAY,CAAC,KAAe;IACnC,OAAO,IAAI,YAAY,CAAC;QACtB,IAAI,EAAE,uBAAuB;QAC7B,OAAO,EAAE,gFAAgF;QACzF,IAAI,EAAE,sEAAsE;QAC5E,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;KAC1C,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IAChC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IACrC,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,CAAA;IAE9D,IAAI,UAAU,GAAG,CAAC,CAAC,CAAA;IACnB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,kCAAkC,EAAE,CAAC;YAChE,UAAU,GAAG,CAAC,CAAA;YACd,MAAK;QACP,CAAC;IACH,CAAC;IACD,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QACtB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACpB,MAAM,YAAY,EAAE,CAAA;IACtB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,GAAG,EAAE,CAAC,CAAA;IACrD,MAAM,oBAAoB,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,GAAG,EAAE,CAAC,CAAA;IAC/D,MAAM,qBAAqB,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,GAAG,EAAE,CAAC,CAAA;IAEhE,MAAM,gBAAgB,GAAG,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAA;IAC3D,MAAM,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,EAAE,oBAAoB,EAAE,qBAAqB,CAAC,CAAA;IAEnF,MAAM,OAAO,GAAe,EAAE,CAAA;IAC9B,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,IAAI,gBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,2BAA2B,EAAE,CAAC;YAC1E,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;YACpB,MAAM,YAAY,EAAE,CAAA;QACtB,CAAC;QACD,MAAM,IAAI,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;QACvD,MAAM,UAAU,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;QAC7D,MAAM,WAAW,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;QAC9D,MAAM,aAAa,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;QAChE,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;QACpE,MAAM,SAAS,GAAG,MAAM,GAAG,EAAE,CAAA;QAC7B,MAAM,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC,CAAA;QACjF,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAA;QAC/C,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,CAAA;IAC/D,CAAC;IAED,OAAO;QACL,OAAO;QACP,KAAK,CAAC,CAAC,IAAI,CAAC,IAAY;YACtB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;YAClE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,IAAI,YAAY,CAAC;oBACrB,IAAI,EAAE,uBAAuB;oBAC7B,OAAO,EAAE,mCAAmC,IAAI,IAAI;oBACpD,IAAI,EAAE,gEAAgE;oBACtE,OAAO,EAAE,EAAE,IAAI,EAAE;iBAClB,CAAC,CAAA;YACJ,CAAC;YACD,KAAK,CAAC,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QACrC,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC5B,CAAA;AACH,CAAC;AAED,KAAK,SAAS,CAAC,CAAC,aAAa,CAAC,MAAkB,EAAE,KAAe;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAA;IACtD,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,yBAAyB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAA;IAChF,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,iBAAiB,GAAG,yBAAyB,GAAG,UAAU,GAAG,WAAW,CAAA;IAEhG,MAAM,UAAU,GAAG,EAAE,GAAG,IAAI,CAAA;IAC5B,IAAI,SAAS,GAAG,KAAK,CAAC,IAAI,CAAA;IAC1B,IAAI,QAAQ,GAAG,SAAS,CAAA;IACxB,OAAO,SAAS,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;QAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACjC,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC5C,MAAM,MAAM,CAAA;QACZ,SAAS,IAAI,IAAI,CAAA;QACjB,QAAQ,IAAI,IAAI,CAAA;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface ZipWriter {
2
+ /**
3
+ * Streams one file into the archive. `data` may be a `Buffer` or an
4
+ * `AsyncIterable<Buffer>` (a `Readable` satisfies this) — either way, bytes
5
+ * are written to `sink` as they arrive rather than assembled in memory.
6
+ */
7
+ addFile(name: string, data: Buffer | AsyncIterable<Buffer>): Promise<void>;
8
+ /** Writes the central directory and end record. Call exactly once, last. */
9
+ finish(): Promise<void>;
10
+ }
11
+ export interface CreateZipWriterOptions {
12
+ /** Called with each chunk of the archive, in order. */
13
+ readonly write: (chunk: Buffer) => Promise<void> | void;
14
+ }
15
+ export declare function createZipWriter(options: CreateZipWriterOptions): ZipWriter;
16
+ //# sourceMappingURL=zip-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-writer.d.ts","sourceRoot":"","sources":["../src/zip-writer.ts"],"names":[],"mappings":"AAmDA,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1E,4EAA4E;IAC5E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACxD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,SAAS,CA+G1E"}
@@ -0,0 +1,143 @@
1
+ import { crc32 } from 'node:zlib';
2
+ import { CogentaError } from '@cogenta/core';
3
+ /**
4
+ * A streaming, store-only (uncompressed) ZIP writer. Zero dependencies (R9):
5
+ * `node:zlib` already has the one non-trivial piece a ZIP needs (`crc32`),
6
+ * and the container format itself is a few hundred bytes of bookkeeping.
7
+ *
8
+ * **Store, not deflate, on purpose.** The two things this writer bundles —
9
+ * media originals and NDJSON table dumps — are either already compressed
10
+ * (images) or compress cheaply enough at rest that a second pass buys little
11
+ * (`gzip -9` on a backup's NDJSON is a caller's choice, layered outside this
12
+ * writer, never inside it). Store mode also means every entry's compressed
13
+ * size equals its real size, known before the bytes are read — which is what
14
+ * lets this writer emit each entry's local header, data and data descriptor
15
+ * as they stream past, and hold nothing beyond the current entry in memory.
16
+ *
17
+ * A `.zip` is otherwise unremarkable: local file headers first, then a
18
+ * central directory naming every one of them, then one end-of-central-
19
+ * directory record. Every unzip tool reads it the same way.
20
+ */
21
+ const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
22
+ const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
23
+ const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
24
+ /** ZIP64 is not implemented: `@cogenta/agents`' reader does not need it either, and a backup or media archive over 4 GiB is a `docs/hebergement-mutualise.md`-scale exception, not the common case this package targets. */
25
+ const MAX_ENTRY_SIZE = 0xffffffff;
26
+ /** DOS date/time, fixed at the Unix epoch: content, not the archive's own timestamp, is what a Cogenta export means to preserve. */
27
+ const DOS_TIME = 0;
28
+ const DOS_DATE = 0b0000000000100001; // 1980-01-01, the DOS epoch
29
+ function u16(value) {
30
+ const buffer = Buffer.alloc(2);
31
+ buffer.writeUInt16LE(value, 0);
32
+ return buffer;
33
+ }
34
+ function u32(value) {
35
+ const buffer = Buffer.alloc(4);
36
+ buffer.writeUInt32LE(value, 0);
37
+ return buffer;
38
+ }
39
+ export function createZipWriter(options) {
40
+ const entries = [];
41
+ let offset = 0;
42
+ const write = async (chunk) => {
43
+ await options.write(chunk);
44
+ offset += chunk.length;
45
+ };
46
+ return {
47
+ async addFile(name, data) {
48
+ const nameBuffer = Buffer.from(name, 'utf8');
49
+ const localHeaderOffset = offset;
50
+ // Store mode with a trailing data descriptor: the size and CRC are not
51
+ // known until the last byte has streamed past, so the local header
52
+ // declares them as zero and a descriptor after the data carries the
53
+ // real values — a ZIP reader is required to accept this (general
54
+ // purpose bit 3), and it is what makes streaming a file of unknown
55
+ // length possible without buffering it first.
56
+ const generalPurposeFlag = 0b0000000000001000;
57
+ const localHeader = Buffer.concat([
58
+ u32(LOCAL_FILE_HEADER_SIGNATURE),
59
+ u16(20), // version needed to extract
60
+ u16(generalPurposeFlag),
61
+ u16(0), // compression method: store
62
+ u16(DOS_TIME),
63
+ u16(DOS_DATE),
64
+ u32(0), // crc-32 (deferred)
65
+ u32(0), // compressed size (deferred)
66
+ u32(0), // uncompressed size (deferred)
67
+ u16(nameBuffer.length),
68
+ u16(0), // extra field length
69
+ nameBuffer,
70
+ ]);
71
+ await write(localHeader);
72
+ let crc = 0;
73
+ let size = 0;
74
+ if (Buffer.isBuffer(data)) {
75
+ crc = crc32(data);
76
+ size = data.length;
77
+ await write(data);
78
+ }
79
+ else {
80
+ for await (const chunk of data) {
81
+ crc = crc32(chunk, crc);
82
+ size += chunk.length;
83
+ await write(chunk);
84
+ }
85
+ }
86
+ if (size > MAX_ENTRY_SIZE) {
87
+ throw new CogentaError({
88
+ code: 'EXPORT_ENTRY_TOO_LARGE',
89
+ message: `"${name}" is ${size} bytes; this writer does not implement ZIP64.`,
90
+ hint: `Split the archive, or keep entries under ${MAX_ENTRY_SIZE} bytes.`,
91
+ details: { name, size, max: MAX_ENTRY_SIZE },
92
+ });
93
+ }
94
+ const descriptor = Buffer.concat([
95
+ u32(0x08074b50), // optional but conventional signature
96
+ u32(crc >>> 0),
97
+ u32(size),
98
+ u32(size),
99
+ ]);
100
+ await write(descriptor);
101
+ entries.push({ name: nameBuffer, crc32: crc >>> 0, size, offset: localHeaderOffset });
102
+ },
103
+ async finish() {
104
+ const centralDirectoryStart = offset;
105
+ for (const entry of entries) {
106
+ const header = Buffer.concat([
107
+ u32(CENTRAL_DIRECTORY_SIGNATURE),
108
+ u16(20), // version made by
109
+ u16(20), // version needed to extract
110
+ u16(0b0000000000001000), // general purpose flag (data descriptor used)
111
+ u16(0), // compression method: store
112
+ u16(DOS_TIME),
113
+ u16(DOS_DATE),
114
+ u32(entry.crc32),
115
+ u32(entry.size),
116
+ u32(entry.size),
117
+ u16(entry.name.length),
118
+ u16(0), // extra field length
119
+ u16(0), // file comment length
120
+ u16(0), // disk number start
121
+ u16(0), // internal file attributes
122
+ u32(0), // external file attributes
123
+ u32(entry.offset),
124
+ entry.name,
125
+ ]);
126
+ await write(header);
127
+ }
128
+ const centralDirectorySize = offset - centralDirectoryStart;
129
+ const end = Buffer.concat([
130
+ u32(END_OF_CENTRAL_DIRECTORY_SIGNATURE),
131
+ u16(0), // this disk
132
+ u16(0), // disk with central directory start
133
+ u16(entries.length),
134
+ u16(entries.length),
135
+ u32(centralDirectorySize),
136
+ u32(centralDirectoryStart),
137
+ u16(0), // comment length
138
+ ]);
139
+ await write(end);
140
+ },
141
+ };
142
+ }
143
+ //# sourceMappingURL=zip-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-writer.js","sourceRoot":"","sources":["../src/zip-writer.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@cogenta/export",
3
+ "version": "0.2.3",
4
+ "description": "Content export/import, site backup/restore and GDPR export — export@1.0.",
5
+ "license": "MPL-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.11.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/cogenta-cms/cogenta.git",
27
+ "directory": "packages/export"
28
+ },
29
+ "dependencies": {
30
+ "@cogenta/core": "0.8.0",
31
+ "@cogenta/auth": "0.5.2",
32
+ "@cogenta/schema": "0.5.1"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^7.0.2",
36
+ "vitest": "^4.1.10"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "typecheck": "tsc -p tsconfig.typecheck.json",
41
+ "test": "vitest run --passWithNoTests",
42
+ "test:integration": "vitest run --config vitest.integration.config.ts --passWithNoTests"
43
+ }
44
+ }