@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
574 lines (570 loc) • 19.5 kB
JavaScript
'use strict';
var async = require('../async.js');
var filetype = require('../filetype.js');
var object = require('../object.js');
var path = require('../path.js');
var reader = require('../reader.js');
var string = require('../string.js');
var result = require('../result.js');
var fs_util = require('./util.js');
require('../error.js');
var fs_errors = require('./errors.js');
var path_util = require('../path/util.js');
var reader_util = require('../reader/util.js');
var error_common = require('../error/common.js');
/**
* A slim version of the `fs` module for the browser.
*
* Normally, we should just use the `fs` module, however, if we don't want
* to include other parts that are not needed in the browser, we can use this
* module instead.
* @module
*/
const EOL = "\n";
/**
* Obtains the directory handle of the given path.
*
* NOTE: This function is only available in the browser.
*
* NOTE: If the `path` is not provided or is empty, the root directory handle
* will be returned.
*
* @example
* ```ts
* // with the default storage
* import { getDirHandle } from "@ayonli/jsext/fs";
*
* const dir = await getDirHandle("/path/to/dir");
* ```
*
* @example
* ```ts
* // with a user-selected directory as root (Chromium only)
* import { getDirHandle } from "@ayonli/jsext/fs";
*
* const root = await window.showDirectoryPicker();
* const dir = await getDirHandle("/path/to/dir", { root });
* ```
*
* @example
* ```ts
* // create the directory if not exist
* import { getDirHandle } from "@ayonli/jsext/fs";
*
* const dir = await getDirHandle("/path/to/dir", { create: true, recursive: true });
* ```
*
* @example
* ```ts
* // return the root directory handle
* import { getDirHandle } from "@ayonli/jsext/fs";
*
* const root = await getDirHandle();
* ```
*/
async function getDirHandle(path = "", options = {}) {
var _a;
if (typeof location === "object" && typeof location.origin === "string") {
path = string.stripStart(path, location.origin);
}
const { create = false, recursive = false } = options;
const paths = path_util.split(string.stripStart(path, "/")).filter(p => p !== ".");
const root = (_a = options.root) !== null && _a !== void 0 ? _a : await fs_util.rawOp(navigator.storage.getDirectory(), "directory");
let dir = root;
for (let i = 0; i < paths.length; i++) {
const _path = paths[i];
dir = await fs_util.rawOp(dir.getDirectoryHandle(_path, {
create: create && (recursive || (i === paths.length - 1)),
}), "directory");
}
return dir;
}
/**
* Obtains the file handle of the given path.
*
* NOTE: This function is only available in the browser.
*
* @example
* ```ts
* // with the default storage
* import { getFileHandle } from "@ayonli/jsext/fs";
*
* const file = await getFileHandle("/path/to/file.txt");
* ```
*
* @example
* ```ts
* // with a user-selected directory as root (Chromium only)
* import { getFileHandle } from "@ayonli/jsext/fs";
*
* const root = await window.showDirectoryPicker();
* const file = await getFileHandle("/path/to/file.txt", { root });
* ```
*
* @example
* ```ts
* // create the file if not exist
* import { getFileHandle } from "@ayonli/jsext/fs";
*
* const file = await getFileHandle("/path/to/file.txt", { create: true });
* ```
*/
async function getFileHandle(path$1, options = {}) {
var _a;
const dirPath = path.dirname(path$1);
const name = path.basename(path$1);
const dir = await getDirHandle(dirPath, { root: options.root });
return await fs_util.rawOp(dir.getFileHandle(name, {
create: (_a = options.create) !== null && _a !== void 0 ? _a : false,
}), "file");
}
async function exists(path, options = {}) {
try {
await stat(path, options);
return true;
}
catch (err) {
if (err instanceof Exception) {
if (err.name === "NotFoundError") {
return false;
}
}
throw err;
}
}
async function stat(target, options = {}) {
var _a, _b, _c, _d, _e;
if (typeof target === "object") {
if (target.kind === "file") {
const info = await fs_util.rawOp(target.getFile(), "file");
return {
name: target.name,
kind: "file",
size: info.size,
type: (_b = (_a = info.type) !== null && _a !== void 0 ? _a : filetype.getMIME(path.extname(target.name))) !== null && _b !== void 0 ? _b : "",
mtime: new Date(info.lastModified),
atime: null,
birthtime: null,
mode: 0,
uid: 0,
gid: 0,
isBlockDevice: false,
isCharDevice: false,
isFIFO: false,
isSocket: false,
};
}
else {
return {
name: target.name,
kind: "directory",
size: 0,
type: "",
mtime: null,
atime: null,
birthtime: null,
mode: 0,
uid: 0,
gid: 0,
isBlockDevice: false,
isCharDevice: false,
isFIFO: false,
isSocket: false,
};
}
}
else {
const { value: file, error: err, } = await result.try_(getFileHandle(target, options));
if (file) {
const info = await fs_util.rawOp(file.getFile(), "file");
return {
name: info.name,
kind: "file",
size: info.size,
type: (_d = (_c = info.type) !== null && _c !== void 0 ? _c : filetype.getMIME(path.extname(info.name))) !== null && _d !== void 0 ? _d : "",
mtime: new Date(info.lastModified),
atime: null,
birthtime: null,
mode: 0,
uid: 0,
gid: 0,
isBlockDevice: false,
isCharDevice: false,
isFIFO: false,
isSocket: false,
};
}
else if (((_e = object.as(err, Exception)) === null || _e === void 0 ? void 0 : _e.name) === "IsDirectoryError") {
return {
name: path.basename(target),
kind: "directory",
size: 0,
type: "",
mtime: null,
atime: null,
birthtime: null,
mode: 0,
uid: 0,
gid: 0,
isBlockDevice: false,
isCharDevice: false,
isFIFO: false,
isSocket: false,
};
}
else {
throw err;
}
}
}
async function mkdir(path, options = {}) {
if (await exists(path, { root: options.root })) {
throw new error_common.AlreadyExistsError(`File or folder already exists, mkdir '${path}'`);
}
await getDirHandle(path, { ...options, create: true });
}
async function ensureDir(path, options = {}) {
var _a;
if (await exists(path, options)) {
return;
}
try {
await mkdir(path, { ...options, recursive: true });
}
catch (err) {
if (((_a = object.as(err, Exception)) === null || _a === void 0 ? void 0 : _a.name) === "AlreadyExistsError") {
return;
}
else {
throw err;
}
}
}
async function* readDir(target, options = {}) {
const handle = typeof target === "object"
? target
: await getDirHandle(target, options);
yield* readDirHandle(handle, options);
}
async function* readDirHandle(dir, options = {}) {
const { base = "", recursive = false } = options;
const entries = dir.entries();
for await (const [_, entry] of entries) {
const _entry = fs_util.fixDirEntry({
name: entry.name,
kind: entry.kind,
relativePath: path.join(base, entry.name),
handle: entry,
});
yield _entry;
if (recursive && entry.kind === "directory") {
yield* readDirHandle(entry, {
base: _entry.relativePath,
recursive,
});
}
}
}
async function readTree(target, options = {}) {
const entries = (await reader.readAsArray(readDir(target, { ...options, recursive: true })));
const tree = fs_util.makeTree(target, entries, true);
if (!tree.handle && options.root) {
tree.handle = options.root;
}
return tree;
}
async function readFile(target, options = {}) {
const handle = typeof target === "object"
? target
: await getFileHandle(target, { root: options.root });
const file = await fs_util.rawOp(handle.getFile(), "file");
const arr = new Uint8Array(file.size);
let offset = 0;
let reader = reader_util.toAsyncIterable(file.stream());
if (options.signal) {
reader = async.abortable(reader, options.signal);
}
for await (const chunk of reader) {
arr.set(chunk, offset);
offset += chunk.length;
}
return arr;
}
async function readFileAsText(target, options = {}) {
const { encoding, ...rest } = options;
const file = await readFile(target, rest);
return await reader.readAsText(file, encoding);
}
async function readFileAsFile(target, options = {}) {
const handle = typeof target === "object"
? target
: await getFileHandle(target, { root: options.root });
const file = await fs_util.rawOp(handle.getFile(), "file");
return fs_util.fixFileType(file);
}
async function writeFile(target, data, options = {}) {
const handle = typeof target === "object"
? target
: await getFileHandle(target, { root: options.root, create: true });
const writer = await createFileHandleWritableStream(handle, options);
if (options.signal) {
const { signal } = options;
if (signal.aborted) {
await writer.abort(signal.reason);
throw fs_util.wrapFsError(signal.reason, "file");
}
else {
signal.addEventListener("abort", () => {
writer.abort(signal.reason);
});
}
}
try {
if (data instanceof Blob) {
await data.stream().pipeTo(writer);
}
else if (data instanceof ReadableStream) {
await data.pipeTo(writer);
}
else {
await writer.write(data);
await writer.close();
}
}
catch (err) {
throw fs_util.wrapFsError(err, "file");
}
}
async function writeLines(target, lines, options = {}) {
const current = await readFileAsText(target, options).catch(err => {
var _a;
if (((_a = object.as(err, Exception)) === null || _a === void 0 ? void 0 : _a.name) !== "NotFoundError") {
throw err;
}
else {
return "";
}
});
const lineEndings = current.match(/\r?\n/g);
let eol = EOL;
if (lineEndings) {
const crlf = lineEndings.filter(e => e === "\r\n").length;
const lf = lineEndings.length - crlf;
eol = crlf > lf ? "\r\n" : "\n";
}
let content = lines.join(eol);
if (!content.endsWith(eol)) {
if (eol === "\r\n" && content.endsWith("\r")) {
content += "\n";
}
else {
content += eol;
}
}
if (options.append && !current.endsWith(eol) && !content.startsWith(eol)) {
if (eol === "\r\n" && current.endsWith("\r")) {
if (!content.startsWith("\n")) {
content = "\n" + content;
}
}
else {
content = eol + content;
}
}
await writeFile(target, content, options);
}
async function truncate(target, size = 0, options = {}) {
const handle = typeof target === "object"
? target
: await getFileHandle(target, { root: options.root });
try {
const writer = await handle.createWritable({ keepExistingData: true });
await writer.truncate(size);
await writer.close();
}
catch (err) {
throw fs_util.wrapFsError(err, "file");
}
}
async function remove(path$1, options = {}) {
const parent = path.dirname(path$1);
const name = path.basename(path$1);
const dir = await getDirHandle(parent, { root: options.root });
await fs_util.rawOp(dir.removeEntry(name, options), "directory");
}
async function rename(oldPath, newPath, options = {}) {
return await copyInBrowser(oldPath, newPath, {
root: options.root,
recursive: true,
move: true,
});
}
async function copy(src, dest, options = {}) {
return copyInBrowser(src, dest, options);
}
async function copyInBrowser(src, dest, options = {}) {
var _a, _b;
if (typeof src === "object" && typeof dest !== "object") {
throw new TypeError("The destination must be a FileSystemHandle.");
}
else if (typeof dest === "object" && typeof src !== "object") {
throw new TypeError("The source must be a FileSystemHandle.");
}
else if (typeof src === "object" && typeof dest === "object") {
if (src.kind === "file") {
if (dest.kind === "file") {
return await copyFileHandleToFileHandle(src, dest);
}
else {
return await copyFileHandleToDirHandle(src, dest);
}
}
else if (dest.kind === "directory") {
if (!options.recursive) {
throw new fs_errors.InvalidOperationError("Cannot copy a directory without the 'recursive' option");
}
return await copyDirHandleToDirHandle(src, dest);
}
else {
throw new fs_errors.NotDirectoryError("The destination location is not a directory");
}
}
const oldParent = path.dirname(src);
const oldName = path.basename(src);
let oldDir = await getDirHandle(oldParent, { root: options.root });
const { value: oldFile, error: oldErr, } = await result.try_(fs_util.rawOp(oldDir.getFileHandle(oldName), "file"));
if (oldFile) {
const newParent = path.dirname(dest);
const newName = path.basename(dest);
let newDir = await getDirHandle(newParent, { root: options.root });
const { error: newErr, value: newFile, } = await result.try_(fs_util.rawOp(newDir.getFileHandle(newName, {
create: true,
}), "file"));
if (newFile) {
await copyFileHandleToFileHandle(oldFile, newFile);
if (options.move) {
await fs_util.rawOp(oldDir.removeEntry(oldName), "directory");
}
}
else if (((_a = object.as(newErr, Exception)) === null || _a === void 0 ? void 0 : _a.name) === "IsDirectoryError" && !options.move) {
// The destination is a directory, copy the file into the new path
// with the old name.
newDir = await fs_util.rawOp(newDir.getDirectoryHandle(newName), "directory");
await copyFileHandleToDirHandle(oldFile, newDir);
}
else {
throw newErr;
}
}
else if (((_b = object.as(oldErr, Exception)) === null || _b === void 0 ? void 0 : _b.name) === "IsDirectoryError") {
if (!options.recursive) {
throw new fs_errors.InvalidOperationError("Cannot copy a directory without the 'recursive' option");
}
const parent = oldDir;
oldDir = await fs_util.rawOp(oldDir.getDirectoryHandle(oldName), "directory");
const newDir = await getDirHandle(dest, { root: options.root, create: true });
await copyDirHandleToDirHandle(oldDir, newDir);
if (options.move) {
await fs_util.rawOp(parent.removeEntry(oldName, { recursive: true }), "directory");
}
}
else {
throw oldErr;
}
}
async function copyFileHandleToFileHandle(src, dest) {
try {
const srcFile = await src.getFile();
const destFile = await dest.createWritable();
await srcFile.stream().pipeTo(destFile);
}
catch (err) {
throw fs_util.wrapFsError(err, "file");
}
}
async function copyFileHandleToDirHandle(src, dest) {
try {
const srcFile = await src.getFile();
const newFile = await dest.getFileHandle(src.name, { create: true });
const destFile = await newFile.createWritable();
await srcFile.stream().pipeTo(destFile);
}
catch (err) {
throw fs_util.wrapFsError(err, "file");
}
}
async function copyDirHandleToDirHandle(src, dest) {
const entries = src.entries();
for await (const [_, entry] of entries) {
if (entry.kind === "file") {
try {
const oldFile = await entry.getFile();
const newFile = await dest.getFileHandle(entry.name, {
create: true,
});
const reader = oldFile.stream();
const writer = await newFile.createWritable();
await reader.pipeTo(writer);
}
catch (err) {
throw fs_util.wrapFsError(err, "file");
}
}
else {
const newSubDir = await fs_util.rawOp(dest.getDirectoryHandle(entry.name, {
create: true,
}), "directory");
await copyDirHandleToDirHandle(entry, newSubDir);
}
}
}
function createReadableStream(target, options = {}) {
return reader_util.resolveByteStream((async () => {
const handle = typeof target === "object"
? target
: await getFileHandle(target, { root: options.root });
const file = await fs_util.rawOp(handle.getFile(), "file");
return file.stream();
})());
}
function createWritableStream(target, options = {}) {
const { readable, writable } = new TransformStream();
const getHandle = typeof target === "object"
? Promise.resolve(target)
: getFileHandle(target, { root: options.root, create: true });
getHandle.then(handle => createFileHandleWritableStream(handle, options))
.then(stream => readable.pipeTo(stream));
return writable;
}
async function createFileHandleWritableStream(handle, options) {
var _a;
const stream = await fs_util.rawOp(handle.createWritable({
keepExistingData: (_a = options === null || options === void 0 ? void 0 : options.append) !== null && _a !== void 0 ? _a : false,
}), "file");
if (options.append) {
const file = await fs_util.rawOp(handle.getFile(), "file");
file.size && stream.seek(file.size);
}
return stream;
}
exports.EOL = EOL;
exports.copy = copy;
exports.createReadableStream = createReadableStream;
exports.createWritableStream = createWritableStream;
exports.ensureDir = ensureDir;
exports.exists = exists;
exports.getDirHandle = getDirHandle;
exports.getFileHandle = getFileHandle;
exports.mkdir = mkdir;
exports.readDir = readDir;
exports.readFile = readFile;
exports.readFileAsFile = readFileAsFile;
exports.readFileAsText = readFileAsText;
exports.readTree = readTree;
exports.remove = remove;
exports.rename = rename;
exports.stat = stat;
exports.truncate = truncate;
exports.writeFile = writeFile;
exports.writeLines = writeLines;
//# sourceMappingURL=web.js.map