@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.
- package/LICENSE +373 -0
- package/dist/backup.d.ts +64 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +79 -0
- package/dist/backup.js.map +1 -0
- package/dist/content-export.d.ts +49 -0
- package/dist/content-export.d.ts.map +1 -0
- package/dist/content-export.js +197 -0
- package/dist/content-export.js.map +1 -0
- package/dist/content-import.d.ts +44 -0
- package/dist/content-import.d.ts.map +1 -0
- package/dist/content-import.js +221 -0
- package/dist/content-import.js.map +1 -0
- package/dist/crypto.d.ts +18 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +151 -0
- package/dist/crypto.js.map +1 -0
- package/dist/format.d.ts +133 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +71 -0
- package/dist/format.js.map +1 -0
- package/dist/gdpr.d.ts +79 -0
- package/dist/gdpr.d.ts.map +1 -0
- package/dist/gdpr.js +84 -0
- package/dist/gdpr.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/media-export.d.ts +33 -0
- package/dist/media-export.d.ts.map +1 -0
- package/dist/media-export.js +64 -0
- package/dist/media-export.js.map +1 -0
- package/dist/restore.d.ts +53 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/restore.js +173 -0
- package/dist/restore.js.map +1 -0
- package/dist/tables.d.ts +28 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +26 -0
- package/dist/tables.js.map +1 -0
- package/dist/zip-reader.d.ts +13 -0
- package/dist/zip-reader.d.ts.map +1 -0
- package/dist/zip-reader.js +97 -0
- package/dist/zip-reader.js.map +1 -0
- package/dist/zip-writer.d.ts +16 -0
- package/dist/zip-writer.d.ts.map +1 -0
- package/dist/zip-writer.js +143 -0
- package/dist/zip-writer.js.map +1 -0
- package/package.json +44 -0
package/dist/restore.js
ADDED
|
@@ -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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"}
|
package/dist/tables.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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
|
+
}
|