@storyteller-platform/epub 0.7.0 → 0.8.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.
@@ -1,179 +1,252 @@
1
- import {
2
- __callDispose,
3
- __using
4
- } from "../chunk-BIEQXUOY.js";
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
5
53
  import { randomUUID } from "node:crypto";
6
54
  import { createWriteStream, rmSync } from "node:fs";
7
- import {
8
- cp,
9
- mkdir,
10
- readFile,
11
- readdir,
12
- rm,
13
- stat,
14
- writeFile
15
- } from "node:fs/promises";
16
- import { tmpdir } from "node:os";
55
+ import { cp, mkdir, readFile, readdir, rm, stat, writeFile, } from "node:fs/promises";
17
56
  import { pipeline } from "node:stream/promises";
18
- import { fromBuffer, open } from "yauzl-promise";
19
57
  import { ZipFile } from "yazl";
58
+ import { scratchPath } from "@storyteller-platform/fs";
59
+ import { MediaType } from "@storyteller-platform/media-types";
20
60
  import { dirname, join, sep } from "@storyteller-platform/path";
21
- const MP3_FILE_EXTENSIONS = [".mp3"];
22
- const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
23
- const AAC_FILE_EXTENSIONS = [".aac"];
24
- const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
25
- const OPUS_FILE_EXTENSIONS = [".opus"];
26
- const WAVE_FILE_EXTENSIONS = [".wav"];
27
- const AIFF_FILE_EXTENSIONS = [".aiff"];
28
- const FLAC_FILE_EXTENSIONS = [".flac"];
29
- const ALAC_FILE_EXTENSIONS = [".alac"];
30
- const WEBM_FILE_EXTENSIONS = [".weba"];
31
- const AUDIO_FILE_EXTENSIONS = [
32
- ...MP3_FILE_EXTENSIONS,
33
- ...AAC_FILE_EXTENSIONS,
34
- ...MPEG4_FILE_EXTENSIONS,
35
- ...OPUS_FILE_EXTENSIONS,
36
- ...OGG_FILE_EXTENSIONS,
37
- ...WAVE_FILE_EXTENSIONS,
38
- ...AIFF_FILE_EXTENSIONS,
39
- ...FLAC_FILE_EXTENSIONS,
40
- ...ALAC_FILE_EXTENSIONS,
41
- ...WEBM_FILE_EXTENSIONS
42
- ];
43
- function isAudioFile(filenameOrExt) {
44
- const lowered = filenameOrExt.toLowerCase();
45
- return AUDIO_FILE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
46
- }
61
+ import { openZip } from "../zip.js";
62
+ const PRECOMPRESSED_KINDS = new Set([
63
+ "audio",
64
+ "image",
65
+ "archive",
66
+ "package",
67
+ ]);
47
68
  function mintRootPath() {
48
- return join(tmpdir(), `storyteller-platform-epub-${randomUUID()}`);
69
+ return scratchPath(`storyteller-platform-epub-${randomUUID()}`);
49
70
  }
50
- class TmpFsAdapter {
51
- constructor(rootPath) {
52
- this.rootPath = rootPath;
53
- }
54
- static kind = "extracted-dir";
55
- static capabilities = { writable: true };
56
- static async init(source) {
57
- const rootPath = mintRootPath();
58
- try {
59
- var _stack = [];
60
- try {
61
- const zipfile = typeof source === "string" ? await open(source) : await fromBuffer(Buffer.from(source));
62
- const stack = __using(_stack, new AsyncDisposableStack(), true);
63
- stack.defer(async () => {
64
- await zipfile.close();
65
- });
66
- for await (const entry of zipfile) {
67
- if (entry.filename.endsWith("/")) {
68
- continue;
69
- }
70
- const writePath = join(rootPath, entry.filename);
71
- const readStream = await entry.openReadStream();
72
- await mkdir(dirname(writePath), { recursive: true });
73
- const writeStream = createWriteStream(writePath);
74
- await pipeline(readStream, writeStream);
75
- }
76
- } catch (_) {
77
- var _error = _, _hasError = true;
78
- } finally {
79
- var _promise = __callDispose(_stack, _error, _hasError);
80
- _promise && await _promise;
81
- }
82
- } catch (error) {
83
- rmSync(rootPath, { force: true, recursive: true });
84
- throw error;
71
+ /**
72
+ * Whether an entry name can be laid down inside the extract root.
73
+ *
74
+ * A `.` or `..` segment cannot name a real file, and joining one onto the
75
+ * root would walk out of it, so such an entry is unreachable here whether it
76
+ * came from a hostile archive or from an href that spelled it `%2E%2E`.
77
+ */
78
+ function isStorableEntryName(entry) {
79
+ return !entry
80
+ .split("/")
81
+ .some((segment) => segment === "" || segment === "." || segment === "..");
82
+ }
83
+ /**
84
+ * Extracts an EPUB archive to a temp directory and serves all I/O off
85
+ * the real filesystem.
86
+ * `Epub.using(TmpFsAdapter).from(...)` returns a writable {@link Epub}.
87
+ */
88
+ export class TmpFsAdapter {
89
+ rootPath;
90
+ static kind = "extracted-dir";
91
+ static capabilities = { writable: true };
92
+ static async init(source) {
93
+ const rootPath = mintRootPath();
94
+ try {
95
+ const env_1 = { stack: [], error: void 0, hasError: false };
96
+ try {
97
+ const zipfile = await openZip(source);
98
+ const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
99
+ stack.defer(async () => {
100
+ await zipfile.close();
101
+ });
102
+ for await (const entry of zipfile) {
103
+ if (entry.filename.endsWith("/")) {
104
+ // directory entries are skipped; parent dirs are created implicitly
105
+ continue;
106
+ }
107
+ if (!isStorableEntryName(entry.filename))
108
+ continue;
109
+ const writePath = join(rootPath, ...entry.filename.split("/"));
110
+ const readStream = await entry.openReadStream();
111
+ await mkdir(dirname(writePath), { recursive: true });
112
+ const writeStream = createWriteStream(writePath);
113
+ await pipeline(readStream, writeStream);
114
+ }
115
+ }
116
+ catch (e_1) {
117
+ env_1.error = e_1;
118
+ env_1.hasError = true;
119
+ }
120
+ finally {
121
+ const result_1 = __disposeResources(env_1);
122
+ if (result_1)
123
+ await result_1;
124
+ }
125
+ }
126
+ catch (error) {
127
+ rmSync(rootPath, { force: true, recursive: true });
128
+ throw error;
129
+ }
130
+ return new TmpFsAdapter(rootPath);
131
+ }
132
+ static async initEmpty() {
133
+ const rootPath = mintRootPath();
134
+ await mkdir(rootPath, { recursive: true });
135
+ return new TmpFsAdapter(rootPath);
136
+ }
137
+ constructor(rootPath) {
138
+ this.rootPath = rootPath;
139
+ }
140
+ diskPath(entry) {
141
+ if (!isStorableEntryName(entry)) {
142
+ const err = new Error(`ENOENT: no such entry in EPUB archive: ${entry}`);
143
+ err.code = "ENOENT";
144
+ throw err;
145
+ }
146
+ return join(this.rootPath, ...entry.split("/"));
85
147
  }
86
- return new TmpFsAdapter(rootPath);
87
- }
88
- static async initEmpty() {
89
- const rootPath = mintRootPath();
90
- await mkdir(rootPath, { recursive: true });
91
- return new TmpFsAdapter(rootPath);
92
- }
93
- async read(path, encoding) {
94
- return await readFile(path, encoding);
95
- }
96
- async write(path, data, encoding) {
97
- await mkdir(dirname(path), { recursive: true });
98
- await writeFile(path, data, encoding);
99
- }
100
- async remove(path) {
101
- await rm(path);
102
- }
103
- async archiveLength(path) {
104
- try {
105
- const st = await stat(path);
106
- return st.size;
107
- } catch {
108
- return 0;
148
+ async read(entry, encoding) {
149
+ return await readFile(this.diskPath(entry), encoding);
109
150
  }
110
- }
111
- async *list() {
112
- const entries = await readdir(this.rootPath, {
113
- recursive: true,
114
- withFileTypes: true
115
- });
116
- for (const entry of entries) {
117
- if (entry.isDirectory()) continue;
118
- const absolutePath = join(entry.parentPath, entry.name);
119
- yield {
120
- absolutePath,
121
- relativePath: absolutePath.replace(`${this.rootPath}${sep}`, "")
122
- };
151
+ async write(entry, data, encoding) {
152
+ const path = this.diskPath(entry);
153
+ await mkdir(dirname(path), { recursive: true });
154
+ await writeFile(path, data, encoding);
123
155
  }
124
- }
125
- async duplicate() {
126
- const newRoot = mintRootPath();
127
- try {
128
- await cp(this.rootPath, newRoot, { recursive: true });
129
- } catch (error) {
130
- rmSync(newRoot, { force: true, recursive: true });
131
- throw error;
156
+ async remove(entry) {
157
+ await rm(this.diskPath(entry));
132
158
  }
133
- return new TmpFsAdapter(newRoot);
134
- }
135
- async serialize(targetPath) {
136
- var _stack = [];
137
- try {
138
- const tmpArchivePath = join(
139
- tmpdir(),
140
- `storyteller-platform-epub-${randomUUID()}`
141
- );
142
- const { promise, resolve } = Promise.withResolvers();
143
- const zipfile = new ZipFile();
144
- const writeStream = createWriteStream(tmpArchivePath);
145
- writeStream.on("close", () => {
146
- resolve();
147
- });
148
- const stack = __using(_stack, new AsyncDisposableStack(), true);
149
- stack.defer(async () => {
150
- writeStream.close();
151
- await rm(tmpArchivePath, { force: true });
152
- });
153
- zipfile.outputStream.pipe(writeStream);
154
- zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
155
- compress: false
156
- });
157
- for await (const entry of this.list()) {
158
- if (entry.relativePath === "mimetype") continue;
159
- zipfile.addFile(entry.absolutePath, entry.relativePath, {
160
- compress: !isAudioFile(entry.absolutePath)
159
+ async exists(entry) {
160
+ try {
161
+ await stat(this.diskPath(entry));
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ async archiveLength(entry) {
169
+ try {
170
+ const st = await stat(this.diskPath(entry));
171
+ return st.size;
172
+ }
173
+ catch {
174
+ return 0;
175
+ }
176
+ }
177
+ async *list() {
178
+ const entries = await readdir(this.rootPath, {
179
+ recursive: true,
180
+ withFileTypes: true,
161
181
  });
162
- }
163
- zipfile.end();
164
- await promise;
165
- await cp(tmpArchivePath, targetPath);
166
- } catch (_) {
167
- var _error = _, _hasError = true;
168
- } finally {
169
- var _promise = __callDispose(_stack, _error, _hasError);
170
- _promise && await _promise;
182
+ for (const entry of entries) {
183
+ if (entry.isDirectory())
184
+ continue;
185
+ const absolutePath = join(entry.parentPath, entry.name);
186
+ yield {
187
+ absolutePath,
188
+ relativePath: absolutePath
189
+ .slice(this.rootPath.length + 1)
190
+ .split(sep)
191
+ .join("/"),
192
+ };
193
+ }
194
+ }
195
+ async duplicate() {
196
+ const newRoot = mintRootPath();
197
+ try {
198
+ await cp(this.rootPath, newRoot, { recursive: true });
199
+ }
200
+ catch (error) {
201
+ rmSync(newRoot, { force: true, recursive: true });
202
+ throw error;
203
+ }
204
+ return new TmpFsAdapter(newRoot);
205
+ }
206
+ async serialize(targetPath) {
207
+ const env_2 = { stack: [], error: void 0, hasError: false };
208
+ try {
209
+ const tmpArchivePath = mintRootPath();
210
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
211
+ const { promise, resolve, reject } = Promise.withResolvers();
212
+ const zipfile = new ZipFile();
213
+ const writeStream = createWriteStream(tmpArchivePath);
214
+ writeStream.on("close", () => {
215
+ resolve();
216
+ });
217
+ writeStream.on("error", reject);
218
+ zipfile.outputStream.on("error", reject);
219
+ const stack = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
220
+ stack.defer(async () => {
221
+ writeStream.close();
222
+ await rm(tmpArchivePath, { force: true });
223
+ });
224
+ zipfile.outputStream.pipe(writeStream);
225
+ zipfile.addBuffer(Buffer.from(MediaType.EPUB.mime), "mimetype", {
226
+ compress: false,
227
+ });
228
+ for await (const entry of this.list()) {
229
+ if (entry.relativePath === "mimetype")
230
+ continue;
231
+ zipfile.addFile(entry.absolutePath, entry.relativePath, {
232
+ compress: !PRECOMPRESSED_KINDS.has(MediaType.fromPath(entry.absolutePath)?.kind),
233
+ });
234
+ }
235
+ zipfile.end();
236
+ await promise;
237
+ await cp(tmpArchivePath, targetPath);
238
+ }
239
+ catch (e_2) {
240
+ env_2.error = e_2;
241
+ env_2.hasError = true;
242
+ }
243
+ finally {
244
+ const result_2 = __disposeResources(env_2);
245
+ if (result_2)
246
+ await result_2;
247
+ }
248
+ }
249
+ dispose() {
250
+ rmSync(this.rootPath, { recursive: true, force: true });
171
251
  }
172
- }
173
- dispose() {
174
- rmSync(this.rootPath, { recursive: true, force: true });
175
- }
176
252
  }
177
- export {
178
- TmpFsAdapter
179
- };
@@ -1,4 +1,4 @@
1
- interface Location {
1
+ export interface Location {
2
2
  url: {
3
3
  hierachical: boolean;
4
4
  opaque: boolean;
@@ -8,16 +8,14 @@ interface Location {
8
8
  column: number;
9
9
  context: string | null;
10
10
  }
11
- interface Message {
11
+ export interface Message {
12
12
  ID: string;
13
13
  severity: "FATAL" | "ERROR" | "WARNING" | "INFO" | "USAGE";
14
14
  message: string;
15
15
  additionalLocations: number;
16
16
  locations: Location[];
17
17
  }
18
- interface Report {
18
+ export interface Report {
19
19
  messages: Message[];
20
20
  }
21
- declare function epubcheck(epubFile: string): Promise<Report>;
22
-
23
- export { type Location, type Message, type Report, epubcheck };
21
+ export declare function epubcheck(epubFile: string): Promise<Report>;
package/dist/epubcheck.js CHANGED
@@ -1,36 +1,34 @@
1
- import "./chunk-BIEQXUOY.js";
2
1
  import { execFile } from "node:child_process";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import { readFile, unlink } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
4
  import { dirname, join } from "node:path";
7
5
  import { fileURLToPath } from "node:url";
8
6
  import { promisify } from "node:util";
7
+ import { scratchPath } from "@storyteller-platform/fs";
9
8
  const here = dirname(fileURLToPath(import.meta.url));
10
9
  const JAR = join(here, "vendors", "epubcheck-5.3.0", "epubcheck.jar");
11
- async function epubcheck(epubFile) {
12
- const output = join(tmpdir(), `epubcheck-${randomUUID()}.json`);
13
- const execFilePromise = promisify(execFile);
14
- try {
15
- await execFilePromise("java", [
16
- "-jar",
17
- JAR,
18
- "--json",
19
- output,
20
- epubFile
21
- ]).catch((error) => {
22
- if (error instanceof Error && error.message.includes("Check finished with errors")) {
23
- return;
24
- }
25
- throw error;
26
- });
27
- const json = await readFile(output, "utf8");
28
- return JSON.parse(json);
29
- } finally {
30
- await unlink(output).catch(() => {
31
- });
32
- }
10
+ export async function epubcheck(epubFile) {
11
+ const output = scratchPath(`epubcheck-${randomUUID()}.json`);
12
+ const execFilePromise = promisify(execFile);
13
+ try {
14
+ await execFilePromise("java", [
15
+ "-jar",
16
+ JAR,
17
+ "--json",
18
+ output,
19
+ epubFile,
20
+ ]).catch((error) => {
21
+ if (error instanceof Error &&
22
+ error.message.includes("Check finished with errors")) {
23
+ // dont throw, just let the caller handle it
24
+ return;
25
+ }
26
+ throw error;
27
+ });
28
+ const json = await readFile(output, "utf8");
29
+ return JSON.parse(json);
30
+ }
31
+ finally {
32
+ await unlink(output).catch(() => { });
33
+ }
33
34
  }
34
- export {
35
- epubcheck
36
- };