@storyteller-platform/epub 0.5.1 → 0.6.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 +1483 -276
- package/dist/adapters/interface.cjs +16 -0
- package/dist/adapters/interface.d.cts +69 -0
- package/dist/adapters/interface.d.ts +69 -0
- package/dist/adapters/interface.js +0 -0
- package/dist/adapters/memory.cjs +103 -0
- package/dist/adapters/memory.d.cts +37 -0
- package/dist/adapters/memory.d.ts +37 -0
- package/dist/adapters/memory.js +83 -0
- package/dist/adapters/tmpfs.cjs +235 -0
- package/dist/adapters/tmpfs.d.cts +29 -0
- package/dist/adapters/tmpfs.d.ts +29 -0
- package/dist/adapters/tmpfs.js +178 -0
- package/dist/index.cjs +285 -280
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +283 -241
- package/dist/upgrade.d.cts +142 -19
- package/dist/upgrade.d.ts +142 -19
- package/package.json +2 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EpubStorageAdapter, EpubListEntry } from './interface.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts an EPUB archive to a temp directory and serves all I/O off
|
|
5
|
+
* the real filesystem.
|
|
6
|
+
* `Epub.using(TmpFsAdapter).from(...)` returns a writable {@link Epub}.
|
|
7
|
+
*/
|
|
8
|
+
declare class TmpFsAdapter implements EpubStorageAdapter {
|
|
9
|
+
readonly rootPath: string;
|
|
10
|
+
static readonly kind: "extracted-dir";
|
|
11
|
+
static readonly capabilities: {
|
|
12
|
+
readonly writable: true;
|
|
13
|
+
};
|
|
14
|
+
static init(source: string | Uint8Array): Promise<TmpFsAdapter>;
|
|
15
|
+
static initEmpty(): Promise<TmpFsAdapter>;
|
|
16
|
+
private constructor();
|
|
17
|
+
read(path: string): Promise<Uint8Array>;
|
|
18
|
+
read(path: string, encoding: "utf-8"): Promise<string>;
|
|
19
|
+
write(path: string, data: Uint8Array): Promise<void>;
|
|
20
|
+
write(path: string, data: string, encoding: "utf-8"): Promise<void>;
|
|
21
|
+
remove(path: string): Promise<void>;
|
|
22
|
+
archiveLength(path: string): Promise<number>;
|
|
23
|
+
list(): AsyncIterable<EpubListEntry>;
|
|
24
|
+
duplicate(): Promise<TmpFsAdapter>;
|
|
25
|
+
serialize(targetPath: string): Promise<void>;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { TmpFsAdapter };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__callDispose,
|
|
3
|
+
__using
|
|
4
|
+
} from "../chunk-BIEQXUOY.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
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";
|
|
17
|
+
import { pipeline } from "node:stream/promises";
|
|
18
|
+
import { fromBuffer, open } from "yauzl-promise";
|
|
19
|
+
import { ZipFile } from "yazl";
|
|
20
|
+
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
|
+
return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
|
|
45
|
+
}
|
|
46
|
+
function mintRootPath() {
|
|
47
|
+
return join(tmpdir(), `storyteller-platform-epub-${randomUUID()}.epub`);
|
|
48
|
+
}
|
|
49
|
+
class TmpFsAdapter {
|
|
50
|
+
constructor(rootPath) {
|
|
51
|
+
this.rootPath = rootPath;
|
|
52
|
+
}
|
|
53
|
+
static kind = "extracted-dir";
|
|
54
|
+
static capabilities = { writable: true };
|
|
55
|
+
static async init(source) {
|
|
56
|
+
const rootPath = mintRootPath();
|
|
57
|
+
try {
|
|
58
|
+
var _stack = [];
|
|
59
|
+
try {
|
|
60
|
+
const zipfile = typeof source === "string" ? await open(source) : await fromBuffer(Buffer.from(source));
|
|
61
|
+
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
62
|
+
stack.defer(async () => {
|
|
63
|
+
await zipfile.close();
|
|
64
|
+
});
|
|
65
|
+
for await (const entry of zipfile) {
|
|
66
|
+
if (entry.filename.endsWith(sep)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const writePath = join(rootPath, entry.filename);
|
|
70
|
+
const readStream = await entry.openReadStream();
|
|
71
|
+
await mkdir(dirname(writePath), { recursive: true });
|
|
72
|
+
const writeStream = createWriteStream(writePath);
|
|
73
|
+
await pipeline(readStream, writeStream);
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
var _error = _, _hasError = true;
|
|
77
|
+
} finally {
|
|
78
|
+
var _promise = __callDispose(_stack, _error, _hasError);
|
|
79
|
+
_promise && await _promise;
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
rmSync(rootPath, { force: true, recursive: true });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
return new TmpFsAdapter(rootPath);
|
|
86
|
+
}
|
|
87
|
+
static async initEmpty() {
|
|
88
|
+
const rootPath = mintRootPath();
|
|
89
|
+
await mkdir(rootPath, { recursive: true });
|
|
90
|
+
return new TmpFsAdapter(rootPath);
|
|
91
|
+
}
|
|
92
|
+
async read(path, encoding) {
|
|
93
|
+
return await readFile(path, encoding);
|
|
94
|
+
}
|
|
95
|
+
async write(path, data, encoding) {
|
|
96
|
+
await mkdir(dirname(path), { recursive: true });
|
|
97
|
+
await writeFile(path, data, encoding);
|
|
98
|
+
}
|
|
99
|
+
async remove(path) {
|
|
100
|
+
await rm(path);
|
|
101
|
+
}
|
|
102
|
+
async archiveLength(path) {
|
|
103
|
+
try {
|
|
104
|
+
const st = await stat(path);
|
|
105
|
+
return st.size;
|
|
106
|
+
} catch {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async *list() {
|
|
111
|
+
const entries = await readdir(this.rootPath, {
|
|
112
|
+
recursive: true,
|
|
113
|
+
withFileTypes: true
|
|
114
|
+
});
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.isDirectory()) continue;
|
|
117
|
+
const absolutePath = join(entry.parentPath, entry.name);
|
|
118
|
+
yield {
|
|
119
|
+
absolutePath,
|
|
120
|
+
relativePath: absolutePath.replace(`${this.rootPath}${sep}`, "")
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async duplicate() {
|
|
125
|
+
const newRoot = mintRootPath();
|
|
126
|
+
try {
|
|
127
|
+
await cp(this.rootPath, newRoot, { recursive: true });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
rmSync(newRoot, { force: true, recursive: true });
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
return new TmpFsAdapter(newRoot);
|
|
133
|
+
}
|
|
134
|
+
async serialize(targetPath) {
|
|
135
|
+
var _stack = [];
|
|
136
|
+
try {
|
|
137
|
+
const tmpArchivePath = join(
|
|
138
|
+
tmpdir(),
|
|
139
|
+
`storyteller-platform-epub-${randomUUID()}`
|
|
140
|
+
);
|
|
141
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
142
|
+
const zipfile = new ZipFile();
|
|
143
|
+
const writeStream = createWriteStream(tmpArchivePath);
|
|
144
|
+
writeStream.on("close", () => {
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
148
|
+
stack.defer(async () => {
|
|
149
|
+
writeStream.close();
|
|
150
|
+
await rm(tmpArchivePath, { force: true });
|
|
151
|
+
});
|
|
152
|
+
zipfile.outputStream.pipe(writeStream);
|
|
153
|
+
zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
|
|
154
|
+
compress: false
|
|
155
|
+
});
|
|
156
|
+
for await (const entry of this.list()) {
|
|
157
|
+
if (entry.relativePath === "mimetype") continue;
|
|
158
|
+
zipfile.addFile(entry.absolutePath, entry.relativePath, {
|
|
159
|
+
compress: !isAudioFile(entry.absolutePath)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
zipfile.end();
|
|
163
|
+
await promise;
|
|
164
|
+
await cp(tmpArchivePath, targetPath);
|
|
165
|
+
} catch (_) {
|
|
166
|
+
var _error = _, _hasError = true;
|
|
167
|
+
} finally {
|
|
168
|
+
var _promise = __callDispose(_stack, _error, _hasError);
|
|
169
|
+
_promise && await _promise;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
dispose() {
|
|
173
|
+
rmSync(this.rootPath, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
TmpFsAdapter
|
|
178
|
+
};
|