files
Version:
Filesystem API easily usable with Promises and arrays
270 lines (238 loc) • 6.41 kB
JavaScript
// The best filesystem for promises and array manipulation
import run from "atocha";
import fs from "node:fs";
import fsp from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import { Readable } from "node:stream";
import swear from "swear";
// Find whether it's Linux or Mac, where we can use `find`
const mac = () => process.platform === "darwin";
const linux = () => process.platform === "linux";
// Retrieve the full, absolute path for the path
const abs = swear(async (name = ".", base = process.cwd()) => {
name = await name;
base = await base;
// Absolute paths do not need more absolutism
if (path.isAbsolute(name)) return name;
if (name.slice(0, 2) === "~/") {
base = await home();
name = name.slice(2);
}
// We are off-base here; recover the viable base option
if (!base || typeof base !== "string") {
base = process.cwd();
}
// Return the file/folder within the base
return join(base, name);
});
const copy = swear(async (src, dst) => {
src = await abs(src);
dst = await abs(dst);
await mkdir(dir(dst));
await fsp.copyFile(src, dst);
return dst;
});
// Get the directory from path
const dir = swear(async (name = ".") => {
name = await abs(name);
return path.dirname(name);
});
// Check whether a filename exists or not
const exists = swear(async (name) => {
name = await abs(name);
return fsp.access(name).then(
() => true,
() => false,
);
});
// Get the home directory: https://stackoverflow.com/a/9081436/938236
const home = swear((...args) => join(homedir(), ...args).then(mkdir));
// Put several path segments together
const join = swear((...parts) => abs(path.join(...parts)));
// List all the files in the folder
const list = swear(async (dir) => {
dir = await abs(dir);
return swear(fsp.readdir(dir)).map((file) => abs(file, dir));
});
// Create a new directory in the specified path
// Note: `recursive` flag on Node.js is ONLY for Mac and Windows (not Linux), so
// it's totally worthless for us
const mkdir = swear(async (name) => {
name = await abs(name);
// Create a recursive list of paths to create, from the highest to the lowest
const list = name
.split(path.sep)
.map((part, i, all) => all.slice(0, i + 1).join(path.sep))
.filter(Boolean);
// Build each nested path sequentially
for (let path of list) {
if (await exists(path)) continue;
await fsp.mkdir(path).catch(() => null);
}
return name;
});
const move = swear(async (src, dst) => {
try {
src = await abs(src);
dst = await abs(dst);
await mkdir(dir(dst));
await fsp.rename(src, dst);
return dst;
} catch (error) {
// Some OS/environments don't allow move, so copy it first
// and then remove the original
if (error.code === "EXDEV") {
await copy(src, dst);
await remove(src);
return dst;
} else {
throw error;
}
}
});
// Get the path's filename
const name = swear((file) => path.basename(file));
// Read the contents of a single file
const read = swear(async (name, options = {}) => {
name = await abs(name);
const type = options && options.type ? options.type : "text";
if (type === "text") {
return fsp.readFile(name, "utf-8").catch(() => null);
}
if (type === "json") {
return read(name).then(JSON.parse);
}
if (type === "raw" || type === "buffer") {
return fsp.readFile(name).catch(() => null);
}
if (type === "stream" || type === "web" || type === "webStream") {
const file = await fsp.open(name);
return file.readableWebStream();
}
if (type === "node" || type === "nodeStream") {
return fs.createReadStream(name);
}
});
// Delete a file or directory (recursively)
const remove = swear(async (name) => {
name = await abs(name);
if (name === "/") throw new Error("Cannot remove the root folder `/`");
if (!(await exists(name))) return name;
if (await stat(name).isDirectory()) {
// Remove all content recursively
await list(name).map(remove);
await fsp.rmdir(name).catch(() => null);
} else {
await fsp.unlink(name).catch(() => null);
}
return name;
});
const sep = path.sep;
// Get some interesting info from the path
const stat = swear(async (name) => {
name = await abs(name);
return fsp.lstat(name).catch(() => null);
});
// Get a temporary folder
const tmp = swear(async (...args) => {
const path = await join(tmpdir(), ...args);
return mkdir(path);
});
// Perform a recursive walk
const rWalk = (name) => {
const file = abs(name);
const deeper = async (file) => {
if (await stat(file).isDirectory()) {
return rWalk(file);
}
return [file];
};
// Note: list() already wraps the promise
return list(file)
.map(deeper)
.reduce((all, arr) => all.concat(arr), []);
};
// Attempt to make an OS walk, and fallback to the recursive one
const walk = swear(async (name) => {
name = await abs(name);
if (!(await exists(name))) return [];
if (linux() || mac()) {
try {
// Avoid double forward slash when it ends in "/"
name = name.replace(/\/$/, "");
// Attempt to invoke run (command may fail for large directories)
return await run(`find ${name} -type f`).split("\n").filter(Boolean);
} catch (error) {
// Fall back to rWalk() below
}
}
return rWalk(name).filter(Boolean);
});
// Create a new file with the specified contents
const write = swear(async (name, body = "") => {
name = await abs(name);
// If it's a WebStream, convert it to a normal node stream
if (body && body.pipeTo) {
body = Readable.fromWeb(body);
}
if (body && body.then) {
body = await body;
}
// If it's a type that is not a string nor a stream, convert it
// into plain text with JSON.stringify
if (
body &&
typeof body !== "string" &&
!body.pipe &&
!(body instanceof Buffer)
) {
body = JSON.stringify(body);
}
await mkdir(dir(name));
await fsp.writeFile(name, body, "utf-8");
return name;
});
const files = {
abs,
copy,
dir,
exists,
home,
join,
list,
mkdir,
move,
name,
read,
remove,
rename: move,
sep,
stat,
swear,
tmp,
walk,
write,
};
export {
abs,
copy,
dir,
exists,
home,
join,
list,
mkdir,
move,
name,
read,
remove,
move as rename,
sep,
stat,
swear,
tmp,
walk,
write,
};
export default files;