UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

415 lines (412 loc) • 12.4 kB
import { directories, entry, parent, parts, serial, transfer, write } from "./opfs.mjs"; import { PathUtilities } from "./paths.mjs"; import { FileSystemReadableStreamSource, FileSystemWritableSink } from "./streams.mjs"; import { FileMode } from "./types.mjs"; import { assertAlgorithm, assertByteLength, assertDestination, assertEncoding, assertMode, assertOffset, assertReadable, assertUri, assertWritable, assertWriteBytes, assertWriteContent } from "./validate.mjs"; function unavailable(member) { return new Error(`${member} is not available on web`); } function server() { return typeof window === "undefined"; } function toBytes(content) { if (content instanceof ArrayBuffer) return new Uint8Array(content); if (content.buffer instanceof ArrayBuffer) { return new Uint8Array(content.buffer, content.byteOffset, content.byteLength); } const copy = new Uint8Array(content.byteLength); copy.set(content); return copy; } function toBase64(bytes) { let binary = ""; for (let i = 0; i < bytes.length; i += 32768) { binary += String.fromCharCode(...bytes.subarray(i, i + 32768)); } return btoa(binary); } async function fileEntry(uri) { const handle = await entry(parts(uri)); if (handle.kind !== "file") throw new Error("FileSystem: expected a file"); return handle; } async function readWholeFile(uri) { return new Uint8Array(await (await (await fileEntry(uri)).getFile()).arrayBuffer()); } async function writePositioned(uri, position, data) { const handle = await fileEntry(uri); const writable = await handle.createWritable({ keepExistingData: true }); try { await writable.write({ type: "write", position, data }); await writable.close(); } catch (error) { await writable.abort().catch(() => {}); throw error; } } var FileHandle = class { targetUri; mode; offsetValue = 0; closed = false; truncatePending; appendPending; constructor(uri, mode = FileMode.ReadWrite) { assertUri(uri, "FileHandle"); assertMode(mode, "FileHandle"); this.targetUri = uri; this.mode = mode; this.truncatePending = mode === FileMode.Truncate; this.appendPending = mode === FileMode.Append; } get offset() { if (this.closed) return null; return this.offsetValue; } set offset(value) { this.assertOpen("offset"); assertOffset(value, "FileHandle.offset"); this.offsetValue = value; this.appendPending = false; } get size() { if (this.closed) return null; throw unavailable("FileHandle.size"); } close() { this.closed = true; } readBytes(length) { this.assertOpen("readBytes"); assertReadable(this.mode, "FileHandle.readBytes"); assertByteLength(length, "FileHandle.readBytes"); if (server()) return Promise.resolve(new Uint8Array()); return serial(async () => { await this.prepare(); const bytes = await readWholeFile(this.targetUri); const slice = bytes.slice(this.offsetValue, this.offsetValue + length); this.offsetValue += slice.length; return slice; }); } readBytesSync(_length) { throw unavailable("FileHandle.readBytesSync"); } writeBytes(bytes) { this.assertOpen("writeBytes"); assertWritable(this.mode, "FileHandle.writeBytes"); assertWriteBytes(bytes, "FileHandle.writeBytes"); if (server()) return Promise.resolve(); const data = toBytes(bytes); return serial(async () => { await this.prepare(); let size = 0; try { size = (await (await fileEntry(this.targetUri)).getFile()).size; } catch (error) { if (error.name !== "NotFoundError") throw error; if (this.mode === FileMode.ReadOnly || this.mode === FileMode.ReadWrite) throw error; const names = parts(this.targetUri); await (await parent(names)).getFileHandle(names.at(-1), { create: true }); } const position = Math.min(this.offsetValue, size); await writePositioned(this.targetUri, position, data); this.offsetValue = position + data.length; }); } writeBytesSync(_bytes) { throw unavailable("FileHandle.writeBytesSync"); } async prepare() { if (this.truncatePending) { this.truncatePending = false; this.offsetValue = 0; const names = parts(this.targetUri); await write(await (await parent(names)).getFileHandle(names.at(-1), { create: true }), ""); } else if (this.appendPending) { this.appendPending = false; try { this.offsetValue = (await (await fileEntry(this.targetUri)).getFile()).size; } catch (error) { if (error.name !== "NotFoundError") throw error; this.offsetValue = 0; } } } assertOpen(verb) { if (this.closed) throw new Error(`FileHandle.${verb}: the handle is closed`); } }; function resolveDestination(source, destination, verb) { assertDestination(destination, verb); if (destination instanceof Directory) { return PathUtilities.join(destination.uri, source.name); } if (destination instanceof File) return destination.uri; throw new Error(`${verb}: expected a File or Directory destination`); } async function relocate(source, to, move, overwrite) { if (overwrite && parts(to).length === 1) { throw new Error("FileSystem: app root directories are protected"); } if (overwrite) { const names = parts(to); await (await parent(names)).removeEntry(names.at(-1), { recursive: true }).catch((error) => { if (error.name !== "NotFoundError") throw error; }); } await transfer(source, to, move); } var File = class { currentUri; constructor(...uris) { const joined = PathUtilities.join(...uris); assertUri(joined, "File"); this.currentUri = joined; } get uri() { return this.currentUri; } get name() { return PathUtilities.basename(this.currentUri); } get extension() { return PathUtilities.extname(this.currentUri); } get parentDirectory() { return new Directory(PathUtilities.dirname(this.currentUri)); } get exists() { throw unavailable("File.exists"); } get size() { throw unavailable("File.size"); } get type() { throw unavailable("File.type"); } get creationTime() { throw unavailable("File.creationTime"); } get lastModified() { throw unavailable("File.lastModified"); } text() { if (server()) return Promise.resolve(""); const uri = this.currentUri; return serial(async () => (await (await fileEntry(uri)).getFile()).text()); } textSync() { throw unavailable("File.textSync"); } bytes() { if (server()) return Promise.resolve(new Uint8Array()); const uri = this.currentUri; return serial(() => readWholeFile(uri)); } bytesSync() { throw unavailable("File.bytesSync"); } base64() { if (server()) return Promise.resolve(""); return this.bytes().then(toBase64); } base64Sync() { throw unavailable("File.base64Sync"); } async arrayBuffer() { return (await this.bytes()).buffer; } async json() { return JSON.parse(await this.text()); } info() { throw unavailable("File.info"); } digest(algorithm) { assertAlgorithm(algorithm, "File.digest"); if (server()) return Promise.reject(new Error("File.digest is not available on the server")); if (algorithm === "MD5") { return Promise.reject(new Error("File.digest: MD5 is not available on web")); } if (!globalThis.crypto?.subtle) return Promise.reject(unavailable("File.digest")); return this.bytes().then(async (bytes) => { const hash = await globalThis.crypto.subtle.digest(algorithm, bytes); return Array.from(new Uint8Array(hash)).map((byte) => byte.toString(16).padStart(2, "0")).join(""); }); } write(content, options) { assertWriteContent(content, "File.write"); if (server()) return Promise.resolve(); const append = options?.append ?? false; let data; if (typeof content === "string") { const encoding = options?.encoding ?? "utf8"; assertEncoding(encoding, "File.write"); if (encoding === "base64") { if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(content)) { throw new Error("FileSystem: invalid base64 contents"); } data = Uint8Array.from(atob(content), (char) => char.charCodeAt(0)); } else { data = content; } } else { if (options?.encoding !== void 0) assertEncoding(options.encoding, "File.write"); data = toBytes(content); } const uri = this.currentUri; return serial(async () => { if (append) { let existing = new Uint8Array(); try { existing = await readWholeFile(uri); } catch (error) { if (error.name !== "NotFoundError") throw error; } const next = typeof data === "string" ? new TextEncoder().encode(data) : data; const combined = new Uint8Array(existing.length + next.length); combined.set(existing); combined.set(next, existing.length); data = combined; } const names = parts(uri); await write(await (await parent(names)).getFileHandle(names.at(-1), { create: true }), data); }); } writeSync(_content, _options) { throw unavailable("File.writeSync"); } create(_options) { throw unavailable("File.create"); } delete() { throw unavailable("File.delete"); } copy(destination, options) { const to = resolveDestination(this, destination, "File.copy"); if (server()) return Promise.resolve(); const from = this.currentUri; return serial(() => relocate(from, to, false, options?.overwrite ?? false)); } copySync(_destination, _options) { throw unavailable("File.copySync"); } move(destination, options) { const to = resolveDestination(this, destination, "File.move"); if (to === this.currentUri || server()) return Promise.resolve(); const from = this.currentUri; return serial(() => relocate(from, to, true, options?.overwrite ?? false).then(() => { this.currentUri = to; })); } moveSync(_destination, _options) { throw unavailable("File.moveSync"); } rename(_newName) { throw unavailable("File.rename"); } open(mode = FileMode.ReadWrite) { assertMode(mode, "File.open"); return new FileHandle(this.currentUri, mode); } readableStream() { return new ReadableStream(new FileSystemReadableStreamSource(this.open(FileMode.ReadOnly))); } writableStream() { return new WritableStream(new FileSystemWritableSink(this.open(FileMode.WriteOnly))); } stream() { return this.readableStream(); } }; var Directory = class Directory { currentUri; constructor(...uris) { const joined = PathUtilities.join(...uris); assertUri(joined, "Directory"); this.currentUri = joined; } get uri() { return this.currentUri; } get name() { return PathUtilities.basename(this.currentUri); } get parentDirectory() { return new Directory(PathUtilities.join(this.currentUri, "..")); } get exists() { throw unavailable("Directory.exists"); } get size() { throw unavailable("Directory.size"); } list() { throw unavailable("Directory.list"); } info() { throw unavailable("Directory.info"); } create(_options) { throw unavailable("Directory.create"); } createFile(_name, _mimeType) { throw unavailable("Directory.createFile"); } createDirectory(_name) { throw unavailable("Directory.createDirectory"); } delete() { throw unavailable("Directory.delete"); } copy(destination, options) { const to = resolveDestination(this, destination, "Directory.copy"); if (server()) return Promise.resolve(); const from = this.currentUri; return serial(() => relocate(from, to, false, options?.overwrite ?? false)); } copySync(_destination, _options) { throw unavailable("Directory.copySync"); } move(destination, options) { const to = resolveDestination(this, destination, "Directory.move"); if (to === this.currentUri || server()) return Promise.resolve(); const from = this.currentUri; return serial(() => relocate(from, to, true, options?.overwrite ?? false).then(() => { this.currentUri = to; })); } moveSync(_destination, _options) { throw unavailable("Directory.moveSync"); } rename(_newName) { throw unavailable("Directory.rename"); } }; var Paths = class extends PathUtilities { static get cache() { return new Directory(directories.cache); } static get document() { return new Directory(directories.documents); } static get bundle() { throw unavailable("Paths.bundle"); } static get totalDiskSpace() { throw unavailable("Paths.totalDiskSpace"); } static get availableDiskSpace() { throw unavailable("Paths.availableDiskSpace"); } static info(..._uris) { throw unavailable("Paths.info"); } }; export { Directory, File, FileHandle, FileMode, Paths }; //# sourceMappingURL=classes.mjs.map