@gentbajko/slopify 2.1.1 → 2.3.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/dist/assets/document/SOURCE.txt +5 -1
- package/dist/assets/document/sample-cover.jpg +0 -0
- package/dist/edge/http/app.js +4 -0
- package/dist/edge/http/document-themes.js +80 -0
- package/dist/edge/http/pronunciations.js +17 -0
- package/dist/edge/http/storage.js +106 -21
- package/dist/kernel/db/migrate.js +10 -1
- package/dist/kernel/db/migrations/0016-document-themes.sql +10 -0
- package/dist/kernel/db/migrations/0017-legacy-dicemaster-theme.sql +12 -0
- package/dist/kernel/db/migrations/0018-backup-imports.sql +7 -0
- package/dist/slices/admission/schema.js +15 -3
- package/dist/slices/admission/start.js +16 -1
- package/dist/slices/article/source-lines.js +20 -0
- package/dist/slices/document/legacy-dicemaster.js +118 -0
- package/dist/slices/document/library.js +77 -0
- package/dist/slices/document/model.js +40 -5
- package/dist/slices/document/sample.js +51 -0
- package/dist/slices/document/sources.js +5 -10
- package/dist/slices/document/theme-schema.js +173 -0
- package/dist/slices/document/theme.js +25 -44
- package/dist/slices/narration/shared-glossary.js +51 -0
- package/dist/slices/play-drafts/convert.js +12 -3
- package/dist/slices/play-drafts/schema.js +7 -7
- package/dist/slices/project-templates/from-project.js +9 -1
- package/dist/slices/rebuild/preview-details.js +9 -2
- package/dist/slices/rebuild/recipe-document.js +2 -3
- package/dist/slices/rebuild/recipe-text.js +9 -1
- package/dist/slices/rebuild/runtime-document.js +2 -3
- package/dist/slices/rebuild/runtime-store.js +19 -3
- package/dist/slices/storage/backup-export.js +321 -0
- package/dist/slices/storage/backup-format.js +155 -0
- package/dist/slices/storage/backup-import.js +749 -0
- package/dist/slices/storage/portable.js +27 -1
- package/dist/slices/storage/tar.js +257 -0
- package/dist/web/assets/index-Cw1mnRrx.js +168 -0
- package/dist/web/assets/index-kAKdJa-U.css +1 -0
- package/dist/web/assets/pdf-BkdJFKQ-.js +54 -0
- package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +29 -0
- package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B6NbhpjD.css +0 -1
- package/dist/web/assets/index-DV01Cyjx.js +0 -142
|
@@ -345,6 +345,21 @@ function portableSettings(settings) {
|
|
|
345
345
|
}
|
|
346
346
|
return portable;
|
|
347
347
|
}
|
|
348
|
+
// The settings a full backup carries: the same known keys as above, each checked alone, so
|
|
349
|
+
// one this build does not know (or a damaged value) is left behind instead of refusing
|
|
350
|
+
// the whole export.
|
|
351
|
+
export function exportableSettings(settings) {
|
|
352
|
+
const out = {};
|
|
353
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
354
|
+
try {
|
|
355
|
+
Object.assign(out, portableSettings({ [key]: value }));
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Not portable: skipped.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
348
363
|
function storedJson(value) {
|
|
349
364
|
try {
|
|
350
365
|
return JSON.parse(value);
|
|
@@ -353,6 +368,11 @@ function storedJson(value) {
|
|
|
353
368
|
throw new Error("A portable setting does not contain valid JSON.");
|
|
354
369
|
}
|
|
355
370
|
}
|
|
371
|
+
// The same checks a v1 import makes on library rows, for rows a full backup carries.
|
|
372
|
+
export function checkLibraryRows(input) {
|
|
373
|
+
const parsed = manifestSchema.pick({ prompts: true, entries: true, voices: true }).parse(input);
|
|
374
|
+
validateLibraryRows({ ...parsed, templates: [] });
|
|
375
|
+
}
|
|
356
376
|
function validateLibraryRows(manifest) {
|
|
357
377
|
const promptIds = new Set();
|
|
358
378
|
const promptNames = new Set();
|
|
@@ -707,6 +727,12 @@ function exportUploadedFonts(paths, archive) {
|
|
|
707
727
|
return { id: font.id, extension: font.extension, bytes: font.content.byteLength };
|
|
708
728
|
});
|
|
709
729
|
}
|
|
730
|
+
export function installedUploadedFontFiles(paths) {
|
|
731
|
+
return installedUploadedFonts(paths).map((font) => ({
|
|
732
|
+
name: `${font.id}${font.extension}`,
|
|
733
|
+
bytes: font.content.byteLength,
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
710
736
|
function installedUploadedFonts(paths) {
|
|
711
737
|
const root = join(paths.dataDir, "fonts");
|
|
712
738
|
let names;
|
|
@@ -755,7 +781,7 @@ function planFontImport(paths, archive, fonts) {
|
|
|
755
781
|
function fontArchivePath(id, extension) {
|
|
756
782
|
return `fonts/${id}${extension}`;
|
|
757
783
|
}
|
|
758
|
-
function validFontContent(id, extension, content) {
|
|
784
|
+
export function validFontContent(id, extension, content) {
|
|
759
785
|
if (content.byteLength < 12 || content.byteLength > fontMaxBytes)
|
|
760
786
|
return false;
|
|
761
787
|
if (`uploaded-${createHash("sha256").update(content).digest("hex")}` !== id)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// The container a full backup travels in. A POSIX tar rather than the ZIP the settings-only
|
|
2
|
+
// backup used: a project can hold a two-hour video, and a tar is written and read front to
|
|
3
|
+
// back with nothing at the end to seek to, so the export streams from the files and the
|
|
4
|
+
// import streams to disk without either side holding an archive in memory. ZIP would need
|
|
5
|
+
// ZIP64 past 4 GB, which the ZIP reader here deliberately refuses. Only what a backup needs
|
|
6
|
+
// is spoken: regular files, with a PAX record for a long name or a size past the 8 GiB a
|
|
7
|
+
// ustar header can hold.
|
|
8
|
+
const block = 512;
|
|
9
|
+
const ustarNameMax = 100;
|
|
10
|
+
const ustarSizeMax = 0o77777777777;
|
|
11
|
+
const paxMax = 64 * 1024;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
14
|
+
export const tarEnd = new Uint8Array(block * 2);
|
|
15
|
+
// The header block(s) for one regular file: a PAX record first when the name or size does
|
|
16
|
+
// not fit the ustar fields.
|
|
17
|
+
export function tarHeader(entry) {
|
|
18
|
+
const name = encoder.encode(entry.name);
|
|
19
|
+
const long = name.byteLength > ustarNameMax;
|
|
20
|
+
const huge = entry.size > ustarSizeMax;
|
|
21
|
+
if (!long && !huge)
|
|
22
|
+
return ustarHeader(name, entry.size, "0");
|
|
23
|
+
const records = [
|
|
24
|
+
...(long ? [paxRecord("path", entry.name)] : []),
|
|
25
|
+
...(huge ? [paxRecord("size", String(entry.size))] : []),
|
|
26
|
+
].join("");
|
|
27
|
+
const pax = encoder.encode(records);
|
|
28
|
+
const placeholder = encoder.encode(`longname-${entry.name.length}`);
|
|
29
|
+
return concat([
|
|
30
|
+
ustarHeader(encoder.encode("PaxHeader"), pax.byteLength, "x"),
|
|
31
|
+
pax,
|
|
32
|
+
new Uint8Array(tarPadding(pax.byteLength)),
|
|
33
|
+
ustarHeader(long ? placeholder : name, huge ? 0 : entry.size, "0"),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
export function tarPadding(size) {
|
|
37
|
+
return (block - (size % block)) % block;
|
|
38
|
+
}
|
|
39
|
+
// Every byte one member adds to the archive, so an export can announce its length up front.
|
|
40
|
+
export function tarMemberBytes(entry) {
|
|
41
|
+
return tarHeader(entry).byteLength + entry.size + tarPadding(entry.size);
|
|
42
|
+
}
|
|
43
|
+
function paxRecord(key, value) {
|
|
44
|
+
// The length prefix counts itself, so it is found by fixed point.
|
|
45
|
+
const body = ` ${key}=${value}\n`;
|
|
46
|
+
const bytes = encoder.encode(body).byteLength;
|
|
47
|
+
const digits = String(bytes).length;
|
|
48
|
+
const length = String(bytes + digits).length > digits ? bytes + digits + 1 : bytes + digits;
|
|
49
|
+
return `${length}${body}`;
|
|
50
|
+
}
|
|
51
|
+
function ustarHeader(name, size, type) {
|
|
52
|
+
const header = new Uint8Array(block);
|
|
53
|
+
header.set(name.subarray(0, ustarNameMax), 0);
|
|
54
|
+
writeOctal(header, 100, 8, 0o644);
|
|
55
|
+
writeOctal(header, 108, 8, 0);
|
|
56
|
+
writeOctal(header, 116, 8, 0);
|
|
57
|
+
writeOctal(header, 124, 12, size);
|
|
58
|
+
writeOctal(header, 136, 12, 0);
|
|
59
|
+
header.fill(0x20, 148, 156);
|
|
60
|
+
header[156] = type.charCodeAt(0);
|
|
61
|
+
header.set(encoder.encode("ustar\u000000"), 257);
|
|
62
|
+
let sum = 0;
|
|
63
|
+
for (const byte of header)
|
|
64
|
+
sum += byte;
|
|
65
|
+
writeOctal(header, 148, 7, sum);
|
|
66
|
+
header[155] = 0x20;
|
|
67
|
+
return header;
|
|
68
|
+
}
|
|
69
|
+
function writeOctal(header, offset, length, value) {
|
|
70
|
+
const text = value.toString(8).padStart(length - 1, "0");
|
|
71
|
+
header.set(encoder.encode(text), offset);
|
|
72
|
+
header[offset + length - 1] = 0;
|
|
73
|
+
}
|
|
74
|
+
function concat(parts) {
|
|
75
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.byteLength, 0));
|
|
76
|
+
let offset = 0;
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
out.set(part, offset);
|
|
79
|
+
offset += part.byteLength;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
export class TarFormatError extends Error {
|
|
84
|
+
}
|
|
85
|
+
// Reads a tar from a byte stream, handing each regular file to `onEntry` in archive order.
|
|
86
|
+
export async function readTar(source, onEntry) {
|
|
87
|
+
const reader = new ByteReader(source[Symbol.asyncIterator]());
|
|
88
|
+
let pax = {};
|
|
89
|
+
while (true) {
|
|
90
|
+
const header = await reader.exact(block);
|
|
91
|
+
if (header === undefined)
|
|
92
|
+
throw new TarFormatError("The archive ends before its last file.");
|
|
93
|
+
if (header.every((byte) => byte === 0)) {
|
|
94
|
+
// The end marker is two zero blocks; anything after them is ignored like tar does.
|
|
95
|
+
await reader.drain();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
checkSum(header);
|
|
99
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
100
|
+
const size = readOctal(header, 124, 12);
|
|
101
|
+
if (type === "x" || type === "g") {
|
|
102
|
+
if (size > paxMax)
|
|
103
|
+
throw new TarFormatError("The archive has an oversized PAX header.");
|
|
104
|
+
const data = await reader.exact(size + tarPadding(size));
|
|
105
|
+
if (data === undefined)
|
|
106
|
+
throw new TarFormatError("The archive ends inside a PAX header.");
|
|
107
|
+
if (type === "x")
|
|
108
|
+
pax = parsePax(data.subarray(0, size));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (type !== "0" && type !== "\u0000")
|
|
112
|
+
throw new TarFormatError("The archive holds something other than plain files.");
|
|
113
|
+
const name = pax.path ?? ustarName(header);
|
|
114
|
+
const entrySize = pax.size ?? size;
|
|
115
|
+
pax = {};
|
|
116
|
+
// An object, so the state the handler's calls change is read back after them.
|
|
117
|
+
const status = { state: "unread" };
|
|
118
|
+
const body = {
|
|
119
|
+
chunks: async function* () {
|
|
120
|
+
if (status.state !== "unread")
|
|
121
|
+
throw new TarFormatError("A file was read twice.");
|
|
122
|
+
status.state = "reading";
|
|
123
|
+
let left = entrySize;
|
|
124
|
+
while (left > 0) {
|
|
125
|
+
const chunk = await reader.upTo(left);
|
|
126
|
+
if (chunk === undefined)
|
|
127
|
+
throw new TarFormatError("The archive ends inside a file.");
|
|
128
|
+
left -= chunk.byteLength;
|
|
129
|
+
yield chunk;
|
|
130
|
+
}
|
|
131
|
+
status.state = "done";
|
|
132
|
+
},
|
|
133
|
+
skip: async () => {
|
|
134
|
+
for await (const _ of body.chunks()) {
|
|
135
|
+
// Discarded: the importer does not keep this file.
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
await onEntry({ name, size: entrySize }, body);
|
|
140
|
+
if (status.state !== "done")
|
|
141
|
+
throw new TarFormatError("A file was not read to its end.");
|
|
142
|
+
const padding = tarPadding(entrySize);
|
|
143
|
+
if (padding > 0 && (await reader.exact(padding)) === undefined)
|
|
144
|
+
throw new TarFormatError("The archive ends inside a file.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function checkSum(header) {
|
|
148
|
+
const stored = readOctal(header, 148, 8);
|
|
149
|
+
let sum = 0;
|
|
150
|
+
for (let index = 0; index < block; index += 1)
|
|
151
|
+
sum += index >= 148 && index < 156 ? 0x20 : (header[index] ?? 0);
|
|
152
|
+
if (sum !== stored)
|
|
153
|
+
throw new TarFormatError("The archive has a damaged file header.");
|
|
154
|
+
}
|
|
155
|
+
function readOctal(header, offset, length) {
|
|
156
|
+
const text = String.fromCharCode(...header.subarray(offset, offset + length))
|
|
157
|
+
.replace(/\0.*$/s, "")
|
|
158
|
+
.trim();
|
|
159
|
+
if (!/^[0-7]*$/.test(text))
|
|
160
|
+
throw new TarFormatError("The archive has a damaged file header.");
|
|
161
|
+
return text === "" ? 0 : Number.parseInt(text, 8);
|
|
162
|
+
}
|
|
163
|
+
function ustarName(header) {
|
|
164
|
+
const field = (offset, length) => {
|
|
165
|
+
const raw = header.subarray(offset, offset + length);
|
|
166
|
+
const end = raw.indexOf(0);
|
|
167
|
+
return decode(end === -1 ? raw : raw.subarray(0, end));
|
|
168
|
+
};
|
|
169
|
+
const name = field(0, 100);
|
|
170
|
+
const prefix = field(345, 155);
|
|
171
|
+
return prefix === "" ? name : `${prefix}/${name}`;
|
|
172
|
+
}
|
|
173
|
+
function parsePax(data) {
|
|
174
|
+
const out = {};
|
|
175
|
+
let offset = 0;
|
|
176
|
+
while (offset < data.byteLength) {
|
|
177
|
+
// "<length> <key>=<value>\n", the length in bytes and counting the whole record.
|
|
178
|
+
const space = data.indexOf(0x20, offset);
|
|
179
|
+
const length = Number(String.fromCharCode(...data.subarray(offset, Math.max(offset, space))));
|
|
180
|
+
if (space === -1 ||
|
|
181
|
+
!Number.isSafeInteger(length) ||
|
|
182
|
+
length <= space - offset + 1 ||
|
|
183
|
+
offset + length > data.byteLength ||
|
|
184
|
+
data[offset + length - 1] !== 0x0a)
|
|
185
|
+
throw new TarFormatError("The archive has a damaged PAX header.");
|
|
186
|
+
const body = decode(data.subarray(space + 1, offset + length - 1));
|
|
187
|
+
const equals = body.indexOf("=");
|
|
188
|
+
const key = body.slice(0, equals);
|
|
189
|
+
const value = body.slice(equals + 1);
|
|
190
|
+
if (key === "path")
|
|
191
|
+
out.path = value;
|
|
192
|
+
if (key === "size") {
|
|
193
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
194
|
+
throw new TarFormatError("The archive has a damaged PAX header.");
|
|
195
|
+
out.size = Number(value);
|
|
196
|
+
}
|
|
197
|
+
offset += length;
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function decode(bytes) {
|
|
202
|
+
try {
|
|
203
|
+
return decoder.decode(bytes);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new TarFormatError("The archive has a file name that is not valid text.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
class ByteReader {
|
|
210
|
+
source;
|
|
211
|
+
pending = new Uint8Array(0);
|
|
212
|
+
ended = false;
|
|
213
|
+
constructor(source) {
|
|
214
|
+
this.source = source;
|
|
215
|
+
}
|
|
216
|
+
async fill() {
|
|
217
|
+
while (this.pending.byteLength === 0) {
|
|
218
|
+
if (this.ended)
|
|
219
|
+
return false;
|
|
220
|
+
const next = await this.source.next();
|
|
221
|
+
if (next.done === true) {
|
|
222
|
+
this.ended = true;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
this.pending = next.value;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
// Up to `limit` bytes, whatever arrived: file content streams in the sizes the socket sends.
|
|
230
|
+
async upTo(limit) {
|
|
231
|
+
if (!(await this.fill()))
|
|
232
|
+
return undefined;
|
|
233
|
+
const chunk = this.pending.subarray(0, limit);
|
|
234
|
+
this.pending = this.pending.subarray(chunk.byteLength);
|
|
235
|
+
return chunk;
|
|
236
|
+
}
|
|
237
|
+
async exact(length) {
|
|
238
|
+
const out = new Uint8Array(length);
|
|
239
|
+
let offset = 0;
|
|
240
|
+
while (offset < length) {
|
|
241
|
+
const chunk = await this.upTo(length - offset);
|
|
242
|
+
if (chunk === undefined)
|
|
243
|
+
return undefined;
|
|
244
|
+
out.set(chunk, offset);
|
|
245
|
+
offset += chunk.byteLength;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
async drain() {
|
|
250
|
+
this.pending = new Uint8Array(0);
|
|
251
|
+
while (!this.ended) {
|
|
252
|
+
const next = await this.source.next();
|
|
253
|
+
if (next.done === true)
|
|
254
|
+
this.ended = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|