@statewalker/webrun-files-sqlite 0.10.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/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/blocks/byte-reader.d.ts +19 -0
- package/dist/blocks/byte-reader.d.ts.map +1 -0
- package/dist/blocks/sha256.d.ts +11 -0
- package/dist/blocks/sha256.d.ts.map +1 -0
- package/dist/blocks/sqlite-files-api.d.ts +61 -0
- package/dist/blocks/sqlite-files-api.d.ts.map +1 -0
- package/dist/blocks/stream-codec.d.ts +40 -0
- package/dist/blocks/stream-codec.d.ts.map +1 -0
- package/dist/bytes.d.ts +3 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/cjs/index.cjs +1264 -0
- package/dist/codec.d.ts +47 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/drivers.d.ts +73 -0
- package/dist/drivers.d.ts.map +1 -0
- package/dist/esm/index.js +1253 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/sql.types.d.ts +25 -0
- package/dist/sql.types.d.ts.map +1 -0
- package/dist/sqlar-files-api.d.ts +52 -0
- package/dist/sqlar-files-api.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/blocks/byte-reader.ts +64 -0
- package/src/blocks/sha256.ts +116 -0
- package/src/blocks/sqlite-files-api.ts +587 -0
- package/src/blocks/stream-codec.ts +132 -0
- package/src/bytes.ts +8 -0
- package/src/codec.ts +88 -0
- package/src/drivers.ts +133 -0
- package/src/index.ts +25 -0
- package/src/sql.types.ts +25 -0
- package/src/sqlar-files-api.ts +413 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FileInfo,
|
|
3
|
+
FileStats,
|
|
4
|
+
FilesApi,
|
|
5
|
+
ListOptions,
|
|
6
|
+
ReadOptions,
|
|
7
|
+
} from "@statewalker/webrun-files";
|
|
8
|
+
import { comparePaths, normalizePath } from "@statewalker/webrun-files";
|
|
9
|
+
import { toBytes } from "./bytes.js";
|
|
10
|
+
import { type Codec, defaultCodec } from "./codec.js";
|
|
11
|
+
import type { SqlDriver } from "./sql.types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The table, verbatim from the SQLite Archive specification. Five columns,
|
|
15
|
+
* `name` as the primary key and nothing more: any extra column, index or table
|
|
16
|
+
* and other SQLAR tools may not recognise the archive.
|
|
17
|
+
*/
|
|
18
|
+
const SCHEMA = `CREATE TABLE IF NOT EXISTS sqlar(
|
|
19
|
+
name TEXT PRIMARY KEY, -- name of the file
|
|
20
|
+
mode INT, -- access permissions
|
|
21
|
+
mtime INT, -- last modification time
|
|
22
|
+
sz INT, -- original file size
|
|
23
|
+
data BLOB -- compressed content
|
|
24
|
+
)`;
|
|
25
|
+
|
|
26
|
+
// Full st_mode, type bits included, as archivers store it.
|
|
27
|
+
const FILE_MODE = 0o100644;
|
|
28
|
+
const DIR_MODE = 0o040755;
|
|
29
|
+
const LINK_MODE = 0o120777;
|
|
30
|
+
|
|
31
|
+
/** Links followed before a path counts as unresolvable (POSIX ELOOP territory). */
|
|
32
|
+
const MAX_SYMLINK_HOPS = 8;
|
|
33
|
+
|
|
34
|
+
export interface SqlarFilesApiOptions {
|
|
35
|
+
/** Defaults to {@link defaultCodec}. */
|
|
36
|
+
codec?: Codec;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A FilesApi whose whole state is one SQLite Archive table.
|
|
41
|
+
*
|
|
42
|
+
* SQLAR can also hold symlinks, which `FileKind` cannot express. The FilesApi
|
|
43
|
+
* view follows them the way POSIX `stat()` does — an unresolvable link is a
|
|
44
|
+
* missing path — and {@link symlink} / {@link readlink} expose the link itself.
|
|
45
|
+
*/
|
|
46
|
+
export class SqlarFilesApi implements FilesApi {
|
|
47
|
+
readonly #sql: SqlDriver;
|
|
48
|
+
readonly #codec: Codec;
|
|
49
|
+
|
|
50
|
+
constructor(sql: SqlDriver, opts: SqlarFilesApiOptions = {}) {
|
|
51
|
+
this.#sql = sql;
|
|
52
|
+
this.#codec = opts.codec ?? defaultCodec();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Creates the table if it is missing. Await it before any other call. */
|
|
56
|
+
async init(): Promise<void> {
|
|
57
|
+
await this.#sql.run(SCHEMA);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A missing path or a directory yields nothing; a file yields one chunk. */
|
|
61
|
+
async *read(path: string, options: ReadOptions = {}): AsyncIterable<Uint8Array> {
|
|
62
|
+
options.signal?.throwIfAborted();
|
|
63
|
+
const resolved = await this.#resolve(toName(path));
|
|
64
|
+
if (resolved?.stats.kind !== "file") return;
|
|
65
|
+
|
|
66
|
+
const rows = await this.#sql.all<{ data: unknown }>(
|
|
67
|
+
"SELECT data FROM sqlar WHERE name = ?",
|
|
68
|
+
resolved.name,
|
|
69
|
+
);
|
|
70
|
+
const content = await this.#content(resolved.name, resolved.stats.size, toBytes(rows[0]?.data));
|
|
71
|
+
options.signal?.throwIfAborted();
|
|
72
|
+
const start = options.start ?? 0;
|
|
73
|
+
const end = options.length === undefined ? content.byteLength : start + options.length;
|
|
74
|
+
const chunk = content.subarray(start, Math.min(end, content.byteLength));
|
|
75
|
+
if (chunk.byteLength > 0) yield chunk;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async write(
|
|
79
|
+
path: string,
|
|
80
|
+
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const name = toName(path);
|
|
83
|
+
const bytes = await concat(content);
|
|
84
|
+
await this.mkdir(parentName(name));
|
|
85
|
+
|
|
86
|
+
let data: Uint8Array = bytes;
|
|
87
|
+
if (bytes.byteLength >= this.#codec.minSize) {
|
|
88
|
+
const deflated = await this.#codec.deflate(bytes);
|
|
89
|
+
// Strictly shorter only: equal length is the plaintext marker.
|
|
90
|
+
if (deflated.byteLength < bytes.byteLength) data = deflated;
|
|
91
|
+
}
|
|
92
|
+
await this.#put(name, FILE_MODE, bytes.byteLength, data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `sz = 0` with `data IS NULL` is the format's only marker for a directory. */
|
|
96
|
+
async mkdir(path: string): Promise<void> {
|
|
97
|
+
const name = toName(path);
|
|
98
|
+
for (const dir of ancestorsAndSelf(name)) {
|
|
99
|
+
if (!(await this.#row(dir))) await this.#put(dir, DIR_MODE, 0, null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Writes a symlink row: `sz = -1`, the target verbatim as TEXT. */
|
|
104
|
+
async symlink(path: string, target: string): Promise<void> {
|
|
105
|
+
const name = toName(path);
|
|
106
|
+
await this.mkdir(parentName(name));
|
|
107
|
+
await this.#put(name, LINK_MODE, -1, target);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The stored target of a symlink, or `undefined` for anything else. */
|
|
111
|
+
async readlink(path: string): Promise<string | undefined> {
|
|
112
|
+
const row = await this.#row(toName(path));
|
|
113
|
+
return row && isSymlink(row) ? linkTarget(row) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async stats(path: string): Promise<FileStats | undefined> {
|
|
117
|
+
return (await this.#resolve(toName(path)))?.stats;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async exists(path: string): Promise<boolean> {
|
|
121
|
+
return (await this.#resolve(toName(path))) !== undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* SQLAR is a flat keyspace, so the tree is derived from names: a path that
|
|
126
|
+
* only exists as a prefix of other names is yielded as a directory, once,
|
|
127
|
+
* before anything under it. Link entries report their target's stats; a
|
|
128
|
+
* recursive listing never descends through one, which rules out cycles.
|
|
129
|
+
*/
|
|
130
|
+
/**
|
|
131
|
+
* Rows come back in name order, but implicit directories and resolved links
|
|
132
|
+
* do not sit where `comparePaths` puts them, so the entries are collected,
|
|
133
|
+
* sorted and filtered by `after`. This backend already loads every row of
|
|
134
|
+
* the directory; sorting them costs no extra reads.
|
|
135
|
+
*/
|
|
136
|
+
async *list(path: string, options: ListOptions = {}): AsyncIterable<FileInfo> {
|
|
137
|
+
const entries: FileInfo[] = [];
|
|
138
|
+
for await (const entry of this.#listUnordered(path, options)) entries.push(entry);
|
|
139
|
+
entries.sort((a, b) => comparePaths(a.path, b.path));
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (options.after === undefined || comparePaths(entry.path, options.after) > 0) yield entry;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async *#listUnordered(path: string, options: ListOptions): AsyncIterable<FileInfo> {
|
|
146
|
+
const requested = toName(path);
|
|
147
|
+
const resolved = await this.#resolve(requested);
|
|
148
|
+
if (resolved?.stats.kind !== "directory") return;
|
|
149
|
+
|
|
150
|
+
// Paths are reported under the requested name even when it is a link.
|
|
151
|
+
const base = resolved.name;
|
|
152
|
+
const range = descendants(base);
|
|
153
|
+
const rows = await this.#sql.all<StoredRow>(
|
|
154
|
+
`SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`,
|
|
155
|
+
...range.params,
|
|
156
|
+
);
|
|
157
|
+
const prefixLength = base === "" ? 0 : base.length + 1;
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
const relative = row.name.slice(prefixLength);
|
|
162
|
+
const segments = relative.split("/");
|
|
163
|
+
const depth = options.recursive ? segments.length : 1;
|
|
164
|
+
// Implicit directories on the way down to this row.
|
|
165
|
+
for (let i = 1; i < Math.min(depth + 1, segments.length); i++) {
|
|
166
|
+
const dir = segments.slice(0, i).join("/");
|
|
167
|
+
if (seen.has(dir)) continue;
|
|
168
|
+
seen.add(dir);
|
|
169
|
+
yield { kind: "directory", name: segments[i - 1], path: `/${joinName(requested, dir)}` };
|
|
170
|
+
}
|
|
171
|
+
if (segments.length > depth || seen.has(relative)) continue;
|
|
172
|
+
seen.add(relative);
|
|
173
|
+
const stats = isSymlink(row) ? (await this.#resolve(row.name))?.stats : toStats(row);
|
|
174
|
+
if (!stats) continue;
|
|
175
|
+
yield {
|
|
176
|
+
...stats,
|
|
177
|
+
name: segments[segments.length - 1],
|
|
178
|
+
path: `/${joinName(requested, relative)}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async remove(path: string): Promise<boolean> {
|
|
184
|
+
const name = toName(path);
|
|
185
|
+
const range = selfAndDescendants(name);
|
|
186
|
+
const found = await this.#sql.all(
|
|
187
|
+
`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
|
|
188
|
+
...range.params,
|
|
189
|
+
);
|
|
190
|
+
if (found.length === 0) return false;
|
|
191
|
+
await this.#sql.run(`DELETE FROM sqlar WHERE ${range.where}`, ...range.params);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Rows are copied verbatim — mode, sz and the stored data — so a compressed
|
|
197
|
+
* row is never inflated and deflated again. Only mtime is fresh.
|
|
198
|
+
*/
|
|
199
|
+
async copy(source: string, target: string): Promise<boolean> {
|
|
200
|
+
const from = toName(source);
|
|
201
|
+
const to = toName(target);
|
|
202
|
+
const range = selfAndDescendants(from);
|
|
203
|
+
const rows = await this.#sql.all<{
|
|
204
|
+
name: string;
|
|
205
|
+
mode: number;
|
|
206
|
+
sz: number;
|
|
207
|
+
dtype: string;
|
|
208
|
+
data: unknown;
|
|
209
|
+
}>(
|
|
210
|
+
`SELECT name, mode, sz, typeof(data) AS dtype, data FROM sqlar WHERE ${range.where}`,
|
|
211
|
+
...range.params,
|
|
212
|
+
);
|
|
213
|
+
if (rows.length === 0) return false;
|
|
214
|
+
|
|
215
|
+
await this.mkdir(parentName(to));
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
const name = joinName(to, row.name.slice(from.length).replace(/^\//, ""));
|
|
218
|
+
await this.#put(name, row.mode, row.sz, storedData(row.dtype, row.data));
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async move(source: string, target: string): Promise<boolean> {
|
|
224
|
+
if (!(await this.copy(source, target))) return false;
|
|
225
|
+
await this.remove(source);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** `length(data) = sz` is plaintext, `< sz` is compressed, `> sz` is corrupt. */
|
|
230
|
+
async #content(name: string, sz: number, data: Uint8Array): Promise<Uint8Array> {
|
|
231
|
+
if (data.byteLength === sz) return data;
|
|
232
|
+
if (data.byteLength > sz) {
|
|
233
|
+
throw new Error(`sqlar: ${name} has length(data)=${data.byteLength} > sz=${sz}`);
|
|
234
|
+
}
|
|
235
|
+
let inflated: Uint8Array;
|
|
236
|
+
try {
|
|
237
|
+
inflated = await this.#codec.inflate(data);
|
|
238
|
+
} catch (cause) {
|
|
239
|
+
// Codec errors name neither the file nor, for DecompressionStream, anything at all.
|
|
240
|
+
const reason = cause instanceof Error && cause.message ? cause.message : String(cause);
|
|
241
|
+
throw new Error(`sqlar: ${name} cannot be inflated: ${reason}`, { cause });
|
|
242
|
+
}
|
|
243
|
+
if (inflated.byteLength !== sz) {
|
|
244
|
+
throw new Error(`sqlar: ${name} inflated ${inflated.byteLength} bytes, sz ${sz}`);
|
|
245
|
+
}
|
|
246
|
+
return inflated;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The entry a stored name leads to, following symlinks in the final
|
|
251
|
+
* component. `undefined` for a missing path, a dangling link or a chain
|
|
252
|
+
* longer than {@link MAX_SYMLINK_HOPS}.
|
|
253
|
+
*/
|
|
254
|
+
async #resolve(start: string): Promise<Resolved | undefined> {
|
|
255
|
+
let name = start;
|
|
256
|
+
for (let hops = 0; ; hops++) {
|
|
257
|
+
if (name === "") return { name, stats: { kind: "directory" } };
|
|
258
|
+
const row = await this.#row(name);
|
|
259
|
+
if (!row) {
|
|
260
|
+
return (await this.#hasDescendants(name))
|
|
261
|
+
? { name, stats: { kind: "directory" } }
|
|
262
|
+
: undefined;
|
|
263
|
+
}
|
|
264
|
+
if (!isSymlink(row)) return { name, stats: toStats(row) };
|
|
265
|
+
if (hops === MAX_SYMLINK_HOPS) return undefined;
|
|
266
|
+
name = resolveTarget(row.name, linkTarget(row));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async #row(name: string): Promise<StoredRow | undefined> {
|
|
271
|
+
const rows = await this.#sql.all<StoredRow>(
|
|
272
|
+
`SELECT ${ROW_COLUMNS} FROM sqlar WHERE name = ?`,
|
|
273
|
+
name,
|
|
274
|
+
);
|
|
275
|
+
return rows[0];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async #hasDescendants(name: string): Promise<boolean> {
|
|
279
|
+
const range = descendants(name);
|
|
280
|
+
const rows = await this.#sql.all(
|
|
281
|
+
`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
|
|
282
|
+
...range.params,
|
|
283
|
+
);
|
|
284
|
+
return rows.length > 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async #put(name: string, mode: number, sz: number, data: Uint8Array | string | null) {
|
|
288
|
+
await this.#sql.run(
|
|
289
|
+
`INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)
|
|
290
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
291
|
+
mode = excluded.mode, mtime = excluded.mtime, sz = excluded.sz, data = excluded.data`,
|
|
292
|
+
name,
|
|
293
|
+
mode,
|
|
294
|
+
Math.floor(Date.now() / 1000), // SQLAR mtime is in seconds
|
|
295
|
+
sz,
|
|
296
|
+
data,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Row metadata without file content, so listings never load blobs. */
|
|
302
|
+
interface StoredRow {
|
|
303
|
+
name: string;
|
|
304
|
+
mode: number;
|
|
305
|
+
mtime: number;
|
|
306
|
+
sz: number;
|
|
307
|
+
/** `typeof(data)`: "null" for directories, "blob" for files, "text" for link targets. */
|
|
308
|
+
dtype: string;
|
|
309
|
+
/** The link target for a symlink row, NULL otherwise. */
|
|
310
|
+
target: unknown;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const ROW_COLUMNS =
|
|
314
|
+
"name, mode, mtime, sz, typeof(data) AS dtype, CASE WHEN sz = -1 THEN data END AS target";
|
|
315
|
+
|
|
316
|
+
interface Resolved {
|
|
317
|
+
/** The stored name the path resolved to. */
|
|
318
|
+
name: string;
|
|
319
|
+
stats: FileStats;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Classification follows sz and data only: other writers may not set mode's type bits. */
|
|
323
|
+
function isSymlink(row: StoredRow): boolean {
|
|
324
|
+
return row.sz === -1;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function toStats(row: StoredRow): FileStats {
|
|
328
|
+
if (row.sz === 0 && row.dtype === "null") return { kind: "directory" };
|
|
329
|
+
return { kind: "file", size: row.sz, lastModified: row.mtime * 1000 };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Every name strictly under `name`, as a range on the primary key. `LIKE`
|
|
334
|
+
* would be shorter and wrong: it is case-insensitive for ASCII, so "a/%" also
|
|
335
|
+
* matches "A/x", and it cannot use the index. "0" is the character after "/".
|
|
336
|
+
*/
|
|
337
|
+
function descendants(name: string): { where: string; params: string[] } {
|
|
338
|
+
if (name === "") return { where: "1", params: [] };
|
|
339
|
+
return { where: "(name > ? AND name < ?)", params: [`${name}/`, `${name}0`] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function selfAndDescendants(name: string): { where: string; params: string[] } {
|
|
343
|
+
if (name === "") return descendants(name);
|
|
344
|
+
const range = descendants(name);
|
|
345
|
+
return { where: `(name = ? OR ${range.where})`, params: [name, ...range.params] };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A FilesApi path as a stored name: normalised, without the leading slash. The root is "". */
|
|
349
|
+
function toName(path: string): string {
|
|
350
|
+
return normalizePath(path).slice(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function joinName(base: string, rest: string): string {
|
|
354
|
+
if (base === "") return rest;
|
|
355
|
+
return rest === "" ? base : `${base}/${rest}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parentName(name: string): string {
|
|
359
|
+
const i = name.lastIndexOf("/");
|
|
360
|
+
return i === -1 ? "" : name.slice(0, i);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function linkTarget(row: StoredRow): string {
|
|
364
|
+
return typeof row.target === "string"
|
|
365
|
+
? row.target
|
|
366
|
+
: new TextDecoder().decode(toBytes(row.target));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A link target as a stored name: absolute from the root, relative from the
|
|
371
|
+
* link's parent, with "." and ".." collapsed.
|
|
372
|
+
*/
|
|
373
|
+
function resolveTarget(linkName: string, target: string): string {
|
|
374
|
+
const out = target.startsWith("/") ? [] : parentName(linkName).split("/").filter(Boolean);
|
|
375
|
+
for (const segment of target.split("/")) {
|
|
376
|
+
if (segment === "" || segment === ".") continue;
|
|
377
|
+
if (segment === "..") out.pop();
|
|
378
|
+
else out.push(segment);
|
|
379
|
+
}
|
|
380
|
+
return out.join("/");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A selected `data` value, ready to bind again: NULL, TEXT or a byte blob. */
|
|
384
|
+
function storedData(dtype: string, data: unknown): Uint8Array | string | null {
|
|
385
|
+
if (dtype === "null") return null;
|
|
386
|
+
if (dtype === "text") {
|
|
387
|
+
return typeof data === "string" ? data : new TextDecoder().decode(toBytes(data));
|
|
388
|
+
}
|
|
389
|
+
return toBytes(data);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** "a/b/c" → ["a", "a/b", "a/b/c"]; the root yields nothing. */
|
|
393
|
+
function ancestorsAndSelf(name: string): string[] {
|
|
394
|
+
if (name === "") return [];
|
|
395
|
+
const parts = name.split("/");
|
|
396
|
+
return parts.map((_, i) => parts.slice(0, i + 1).join("/"));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function concat(content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>) {
|
|
400
|
+
const chunks: Uint8Array[] = [];
|
|
401
|
+
let total = 0;
|
|
402
|
+
for await (const chunk of content) {
|
|
403
|
+
chunks.push(chunk);
|
|
404
|
+
total += chunk.byteLength;
|
|
405
|
+
}
|
|
406
|
+
const out = new Uint8Array(total);
|
|
407
|
+
let at = 0;
|
|
408
|
+
for (const chunk of chunks) {
|
|
409
|
+
out.set(chunk, at);
|
|
410
|
+
at += chunk.byteLength;
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|