strom-research 1.6.0 → 1.7.0
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/README.md +6 -0
- package/assets/lang/cs.json +37 -3
- package/assets/lang/de.json +37 -3
- package/dist/cli/menu.js +28 -1
- package/dist/cli/ui.js +36 -2
- package/dist/commands/connectors.js +12 -8
- package/dist/commands/index.js +1 -0
- package/dist/commands/media.js +11 -3
- package/dist/commands/pack.js +131 -0
- package/dist/commands/session.js +5 -1
- package/dist/commands/sync.js +7 -1
- package/dist/core/pack.js +311 -0
- package/dist/core/sync.js +106 -2
- package/dist/core/tree.js +1 -1
- package/dist/core/views.js +10 -2
- package/dist/core/zip.js +226 -0
- package/dist/gedcom/export.js +1 -1
- package/package.json +1 -1
package/dist/core/zip.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// ZIP files without a dependency: written as they go (deflated where it helps,
|
|
2
|
+
// images stored as they are), read by their central directory. No ZIP64: a
|
|
3
|
+
// package over 4 GB or 65 535 files is refused. Names are UTF-8.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import zlib from "node:zlib";
|
|
7
|
+
import { StromError } from "./errors.js";
|
|
8
|
+
const LOCAL = 0x04034b50;
|
|
9
|
+
const CENTRAL = 0x02014b50;
|
|
10
|
+
const END = 0x06054b50;
|
|
11
|
+
const MAX = 0xffffffff;
|
|
12
|
+
/** Already compressed: deflating them again only costs time. */
|
|
13
|
+
const PACKED = /\.(jpe?g|png|gif|webp|tiff?|jp2|zip|gz|pdf|mp4|mov|mp3|heic)$/i;
|
|
14
|
+
export class ZipTooLarge extends StromError {
|
|
15
|
+
}
|
|
16
|
+
function dos(d) {
|
|
17
|
+
const y = Math.max(1980, d.getFullYear());
|
|
18
|
+
return {
|
|
19
|
+
time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
|
|
20
|
+
date: ((y - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export class ZipWriter {
|
|
24
|
+
fd;
|
|
25
|
+
offset = 0;
|
|
26
|
+
entries = [];
|
|
27
|
+
file;
|
|
28
|
+
constructor(file) {
|
|
29
|
+
this.file = file;
|
|
30
|
+
this.fd = fs.openSync(file, "w");
|
|
31
|
+
}
|
|
32
|
+
get bytes() {
|
|
33
|
+
return this.offset;
|
|
34
|
+
}
|
|
35
|
+
write(b) {
|
|
36
|
+
fs.writeSync(this.fd, b);
|
|
37
|
+
this.offset += b.length;
|
|
38
|
+
}
|
|
39
|
+
/** One file; `name` with forward slashes. */
|
|
40
|
+
add(name, data, opts = {}) {
|
|
41
|
+
let body = data;
|
|
42
|
+
let method = 0;
|
|
43
|
+
if (!PACKED.test(name) && !name.endsWith("/") && data.length > 64) {
|
|
44
|
+
const z = zlib.deflateRawSync(data);
|
|
45
|
+
if (z.length < data.length) {
|
|
46
|
+
body = z;
|
|
47
|
+
method = 8;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const n = Buffer.from(name, "utf8");
|
|
51
|
+
if (this.entries.length >= 0xffff || this.offset + 30 + n.length + body.length + 46 * (this.entries.length + 1) > MAX)
|
|
52
|
+
throw new ZipTooLarge("the package would be over 4 GB or 65 535 files");
|
|
53
|
+
const e = { name: n, crc: zlib.crc32(data), size: data.length, csize: body.length, method, offset: this.offset, ...dos(opts.mtime ?? new Date()), mode: opts.mode ?? (name.endsWith("/") ? 0o755 : 0o644) };
|
|
54
|
+
const h = Buffer.alloc(30);
|
|
55
|
+
h.writeUInt32LE(LOCAL, 0);
|
|
56
|
+
h.writeUInt16LE(20, 4);
|
|
57
|
+
h.writeUInt16LE(0x0800, 6); // names in UTF-8
|
|
58
|
+
h.writeUInt16LE(method, 8);
|
|
59
|
+
h.writeUInt16LE(e.time, 10);
|
|
60
|
+
h.writeUInt16LE(e.date, 12);
|
|
61
|
+
h.writeUInt32LE(e.crc, 14);
|
|
62
|
+
h.writeUInt32LE(e.csize, 18);
|
|
63
|
+
h.writeUInt32LE(e.size, 22);
|
|
64
|
+
h.writeUInt16LE(n.length, 26);
|
|
65
|
+
h.writeUInt16LE(0, 28);
|
|
66
|
+
this.write(h);
|
|
67
|
+
this.write(n);
|
|
68
|
+
this.write(body);
|
|
69
|
+
this.entries.push(e);
|
|
70
|
+
}
|
|
71
|
+
/** An empty folder (git needs its empty ones). */
|
|
72
|
+
addDir(name) {
|
|
73
|
+
this.add(name.endsWith("/") ? name : `${name}/`, Buffer.alloc(0));
|
|
74
|
+
}
|
|
75
|
+
/** A file from disk, its mode and time kept. */
|
|
76
|
+
addFile(name, file) {
|
|
77
|
+
const st = fs.statSync(file);
|
|
78
|
+
this.add(name, fs.readFileSync(file), { mode: st.mode & 0o777, mtime: st.mtime });
|
|
79
|
+
}
|
|
80
|
+
close() {
|
|
81
|
+
const start = this.offset;
|
|
82
|
+
for (const e of this.entries) {
|
|
83
|
+
const c = Buffer.alloc(46);
|
|
84
|
+
c.writeUInt32LE(CENTRAL, 0);
|
|
85
|
+
c.writeUInt16LE((3 << 8) | 20, 4); // made on Unix: the mode is kept
|
|
86
|
+
c.writeUInt16LE(20, 6);
|
|
87
|
+
c.writeUInt16LE(0x0800, 8);
|
|
88
|
+
c.writeUInt16LE(e.method, 10);
|
|
89
|
+
c.writeUInt16LE(e.time, 12);
|
|
90
|
+
c.writeUInt16LE(e.date, 14);
|
|
91
|
+
c.writeUInt32LE(e.crc, 16);
|
|
92
|
+
c.writeUInt32LE(e.csize, 20);
|
|
93
|
+
c.writeUInt32LE(e.size, 24);
|
|
94
|
+
c.writeUInt16LE(e.name.length, 28);
|
|
95
|
+
const dir = e.name[e.name.length - 1] === 0x2f;
|
|
96
|
+
c.writeUInt32LE((((dir ? 0o040000 : 0o100000) | e.mode) << 16 | (dir ? 0x10 : 0)) >>> 0, 38);
|
|
97
|
+
c.writeUInt32LE(e.offset, 42);
|
|
98
|
+
this.write(c);
|
|
99
|
+
this.write(e.name);
|
|
100
|
+
}
|
|
101
|
+
const end = Buffer.alloc(22);
|
|
102
|
+
end.writeUInt32LE(END, 0);
|
|
103
|
+
end.writeUInt16LE(this.entries.length, 8);
|
|
104
|
+
end.writeUInt16LE(this.entries.length, 10);
|
|
105
|
+
end.writeUInt32LE(this.offset - start, 12);
|
|
106
|
+
end.writeUInt32LE(start, 16);
|
|
107
|
+
this.write(end);
|
|
108
|
+
fs.closeSync(this.fd);
|
|
109
|
+
}
|
|
110
|
+
/** Give up: the half-written file goes. */
|
|
111
|
+
abort() {
|
|
112
|
+
try {
|
|
113
|
+
fs.closeSync(this.fd);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// closed already
|
|
117
|
+
}
|
|
118
|
+
fs.rmSync(this.file, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export class BadZip extends StromError {
|
|
122
|
+
}
|
|
123
|
+
/** A name a package may carry: relative, inside it, no drive, no "..". Undefined for anything else. */
|
|
124
|
+
export function safeEntryName(raw) {
|
|
125
|
+
const name = raw.replace(/\\/g, "/");
|
|
126
|
+
if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name) || name.includes("\0"))
|
|
127
|
+
return undefined;
|
|
128
|
+
const parts = name.split("/").filter((p) => p !== "" && p !== ".");
|
|
129
|
+
if (parts.some((p) => p === ".."))
|
|
130
|
+
return undefined;
|
|
131
|
+
return parts.join("/") + (name.endsWith("/") ? "/" : "");
|
|
132
|
+
}
|
|
133
|
+
/** The entries of a ZIP file (what a system's own "compress" makes too). Unsafe names: BadZip. */
|
|
134
|
+
export function readZip(file) {
|
|
135
|
+
const fd = fs.openSync(file, "r");
|
|
136
|
+
try {
|
|
137
|
+
const size = fs.fstatSync(fd).size;
|
|
138
|
+
const tail = Buffer.alloc(Math.min(size, 22 + 0xffff));
|
|
139
|
+
fs.readSync(fd, tail, 0, tail.length, size - tail.length);
|
|
140
|
+
let at = -1;
|
|
141
|
+
for (let i = tail.length - 22; i >= 0; i--)
|
|
142
|
+
if (tail.readUInt32LE(i) === END) {
|
|
143
|
+
at = i;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (at < 0)
|
|
147
|
+
throw new BadZip("not a ZIP file");
|
|
148
|
+
const count = tail.readUInt16LE(at + 10);
|
|
149
|
+
const cdSize = tail.readUInt32LE(at + 12);
|
|
150
|
+
const cdStart = tail.readUInt32LE(at + 16);
|
|
151
|
+
if (cdStart === MAX || count === 0xffff)
|
|
152
|
+
throw new BadZip("a ZIP64 file (over 4 GB) — unpack it with the system and give the folder");
|
|
153
|
+
const cd = Buffer.alloc(cdSize);
|
|
154
|
+
fs.readSync(fd, cd, 0, cdSize, cdStart);
|
|
155
|
+
const out = [];
|
|
156
|
+
let p = 0;
|
|
157
|
+
for (let i = 0; i < count; i++) {
|
|
158
|
+
if (cd.readUInt32LE(p) !== CENTRAL)
|
|
159
|
+
throw new BadZip("a damaged ZIP file");
|
|
160
|
+
const flags = cd.readUInt16LE(p + 8);
|
|
161
|
+
const nameLen = cd.readUInt16LE(p + 28);
|
|
162
|
+
const extraLen = cd.readUInt16LE(p + 30);
|
|
163
|
+
const commentLen = cd.readUInt16LE(p + 32);
|
|
164
|
+
const raw = cd.subarray(p + 46, p + 46 + nameLen);
|
|
165
|
+
const rawName = flags & 0x0800 ? raw.toString("utf8") : raw.toString("latin1");
|
|
166
|
+
const name = safeEntryName(rawName);
|
|
167
|
+
if (name === undefined)
|
|
168
|
+
throw new BadZip(`a file outside the package: ${rawName}`);
|
|
169
|
+
const attr = cd.readUInt32LE(p + 38) >>> 16;
|
|
170
|
+
out.push({
|
|
171
|
+
name,
|
|
172
|
+
method: cd.readUInt16LE(p + 10),
|
|
173
|
+
crc: cd.readUInt32LE(p + 16),
|
|
174
|
+
csize: cd.readUInt32LE(p + 20),
|
|
175
|
+
size: cd.readUInt32LE(p + 24),
|
|
176
|
+
offset: cd.readUInt32LE(p + 42),
|
|
177
|
+
dir: name.endsWith("/"),
|
|
178
|
+
link: (attr & 0o170000) === 0o120000,
|
|
179
|
+
});
|
|
180
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
fs.closeSync(fd);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** The content of one entry, its checksum checked. */
|
|
189
|
+
export function readEntry(file, e) {
|
|
190
|
+
const fd = fs.openSync(file, "r");
|
|
191
|
+
try {
|
|
192
|
+
const h = Buffer.alloc(30);
|
|
193
|
+
fs.readSync(fd, h, 0, 30, e.offset);
|
|
194
|
+
if (h.readUInt32LE(0) !== LOCAL)
|
|
195
|
+
throw new BadZip(`a damaged ZIP file (${e.name})`);
|
|
196
|
+
const start = e.offset + 30 + h.readUInt16LE(26) + h.readUInt16LE(28);
|
|
197
|
+
const body = Buffer.alloc(e.csize);
|
|
198
|
+
fs.readSync(fd, body, 0, e.csize, start);
|
|
199
|
+
const data = e.method === 0 ? body : e.method === 8 ? zlib.inflateRawSync(body) : undefined;
|
|
200
|
+
if (!data)
|
|
201
|
+
throw new BadZip(`${e.name}: a compression strom does not read (${e.method})`);
|
|
202
|
+
if (data.length !== e.size || zlib.crc32(data) !== e.crc)
|
|
203
|
+
throw new BadZip(`${e.name} is damaged in the ZIP file`);
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
fs.closeSync(fd);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Unpack into a folder (which must not hold the names already). Links and the system's own litter are left out. */
|
|
211
|
+
export function unzipTo(file, dir, entries = readZip(file)) {
|
|
212
|
+
const written = [];
|
|
213
|
+
for (const e of entries) {
|
|
214
|
+
if (e.link || e.name.startsWith("__MACOSX/") || path.posix.basename(e.name) === ".DS_Store")
|
|
215
|
+
continue;
|
|
216
|
+
const to = path.join(dir, ...e.name.split("/").filter(Boolean));
|
|
217
|
+
if (e.dir) {
|
|
218
|
+
fs.mkdirSync(to, { recursive: true });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
222
|
+
fs.writeFileSync(to, readEntry(file, e));
|
|
223
|
+
written.push(e.name);
|
|
224
|
+
}
|
|
225
|
+
return written;
|
|
226
|
+
}
|
package/dist/gedcom/export.js
CHANGED
|
@@ -120,7 +120,7 @@ export function exportGedcom(tree, opts = {}) {
|
|
|
120
120
|
// Places with coordinates, looked up by folded name.
|
|
121
121
|
const coords = new Map();
|
|
122
122
|
for (const pl of tree.list("place"))
|
|
123
|
-
if (pl.coords)
|
|
123
|
+
if (pl.coords && !pl.retracted)
|
|
124
124
|
for (const n of pl.names)
|
|
125
125
|
coords.set(foldText(n.name), pl.coords);
|
|
126
126
|
// What the research knows beyond the facts (the Strom profile, opts.research): read once.
|