one
Version:
One is a React framework for web, iOS, and Android apps.
415 lines (412 loc) • 12.4 kB
JavaScript
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