Appearance
文件与路径处理
运维脚本最频繁的操作:读配置、写报告、扫描日志目录、生成结果文件。Node.js 的文件操作主要用 node:fs/promises(异步 Promise 版),路径操作用 node:path。
Promise 版 fs:新代码的默认选择
Node.js 有三套 fs API:同步版(fs.readFileSync)、回调版(fs.readFile)、Promise 版(fs/promises.readFile)。新代码直接用 Promise 版——异步操作在 async/await 下读起来像同步代码,不阻塞事件循环,也避免了回调嵌套。
js
import { readFile } from "node:fs/promises";
const text = await readFile("config.json", "utf8");
// 不指定 utf8 的话返回的是 Buffer,不能当字符串用
console.log(text);Buffer 是 Node.js 里表示二进制数据的类型。读文件不指定编码时返回 Buffer,要手动 .toString() 才能得到字符串:
js
const data = await readFile("config.json"); // Buffer
console.log(data.toString("utf8")); // 手动转写入文件
writeFile 默认覆盖文件(文件不存在则创建):
js
import { writeFile } from "node:fs/promises";
const report = ["service\tstatus", "nginx\tactive", "sshd\tactive"].join("\n") + "\n";
await writeFile("report.tsv", report, "utf8");追加内容用 appendFile——适合日志流水和增量结果:
js
import { appendFile } from "node:fs/promises";
const line = `${new Date().toISOString()}\tnginx\tactive\n`;
await appendFile("check.log", line, "utf8");写 JSON 结果时加 null, 2 缩进:
js
const result = {
checkedAt: new Date().toISOString(),
services: [{ name: "nginx", active: true }]
};
await writeFile("result.json", JSON.stringify(result, null, 2), "utf8");JSON.stringify 的三个参数:第一个是数据对象;第二个是 replacer(对键值做二次处理,设为 null 表示不过滤);第三个是缩进空格数。不加第三个参数,输出是一长行——人读不了,grep 也不方便。
目录操作
js
import { mkdir, readdir } from "node:fs/promises";
// recursive: true——父目录不存在时一路创建,不需要先判 /data 有没有再判 /data/logs
await mkdir("logs/app", { recursive: true });
// 读目录
const names = await readdir(".");
console.log(names); // ['index.js', 'src', 'package.json']带类型信息的目录读取可以减少 stat 调用:
js
const entries = await readdir(".", { withFileTypes: true });
for (const entry of entries) {
// isDirectory() 不额外调 stat,比先读文件名再挨个 stat 效率高
console.log(entry.name, entry.isDirectory() ? "dir" : "file");
}路径:用 path.join,不手写拼接
js
import path from "node:path";
// path.join 自动处理分隔符——Linux 用 /,Windows 用 \
const file = path.join("/data", "app", "config.json");
console.log(file); // /data/app/config.json几个常用方法:
js
const file = "/data/app/config.json";
path.basename(file); // "config.json"——文件名
path.dirname(file); // "/data/app"——目录路径
path.extname(file); // ".json"——扩展名(含点)
path.resolve("config.json"); // 基于当前 process.cwd() 转绝对路径path.join 和 path.resolve 的区别:
path.join只是拼路径片段,用当前系统分隔符path.resolve会把相对路径基于process.cwd()解析成绝对路径
js
// 假设 process.cwd() === "/home/user"
path.join("data", "config.json"); // data/config.json(相对路径)
path.resolve("data", "config.json"); // /home/user/data/config.json(绝对路径)路径拼接不手写字符串。"/data/" + dir + "/" + file 在 Linux 上能跑,换到 Windows 上分隔符变成 \,路径变成混合斜杠,后续 fs 操作可能失败。
脚本自身路径与工作目录
这是最容易混淆的一对概念:
| 值 | 含义 | 什么时候用 |
|---|---|---|
process.cwd() | 当前工作目录——执行命令时所在目录 | 读取用户传入的文件 |
脚本目录 | .js 文件本身所在目录 | 读随脚本发布的模板、默认配置 |
ESM 里没有 CommonJS 的 __dirname,需要手动构造:
js
import { fileURLToPath } from "node:url";
import path from "node:path";
const currentFile = fileURLToPath(import.meta.url);
// import.meta.url 是当前文件的 file:// URL,fileURLToPath 转成系统路径
const currentDir = path.dirname(currentFile);
// 读取和脚本放在一起的文件
const template = path.join(currentDir, "templates", "nginx.conf");systemd、crontab、CI 里 process.cwd() 经常不是脚本所在目录。如果用相对路径读配置但依赖了 cwd,换一种执行方式就会报文件找不到——用脚本目录拼接模板路径,用 cwd 处理调用者传入的参数。
判断文件是否存在
js
import { access } from "node:fs/promises";
async function exists(file) {
try {
await access(file); // access 检查文件是否可访问(存在且有权限)
return true;
} catch {
return false;
}
}但先判存在再读文件有竞态窗口——判的时候还在,读的时候文件已经没了。巡检脚本里影响不大;长期运行的服务里,更安全的做法是直接读,失败再处理:
js
import { readFile } from "node:fs/promises";
async function readOptional(file) {
try {
return await readFile(file, "utf8");
} catch (err) {
if (err.code === "ENOENT") return null; // 文件不存在是预期情况
throw err; // 权限错误、磁盘错误则继续向上抛
}
}err.code 是 Node.js 文件系统错误码,来自操作系统的 errno。常见的有:
| code | 含义 |
|---|---|
ENOENT | 文件或目录不存在 |
EACCES | 权限不足 |
EISDIR | 目标是目录,但操作用了文件方式 |
ENOTDIR | 目标是文件,但路径里某段当目录用了 |
递归遍历目录
js
import { readdir } from "node:fs/promises";
import path from "node:path";
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await walk(fullPath)); // 递归子目录
} else {
files.push(fullPath);
}
}
return files;
}大目录(日志目录、备份目录、容器 overlay2)走递归遍历要小心。文件数爆炸时,全部塞进一个数组可能撑爆内存——对这种场景,限制扫描深度和文件数上限比事后优化更直接。
文件权限
js
import { writeFile, chmod } from "node:fs/promises";
await writeFile("run.sh", "#!/bin/bash\necho ok\n", "utf8");
await chmod("run.sh", 0o755); // 八进制权限——0o 前缀代表八进制| 八进制 | 权限 | 谁该用 |
|---|---|---|
0o644 | rw-r--r-- | 普通配置文件 |
0o600 | rw------- | 含密钥的文件 |
0o755 | rwxr-xr-x | 可执行脚本 |
含密钥的文件权限在写的时候就应该收紧到 0o600——chmod 在 writeFile 之后立即调用,避免短暂的时间窗口里文件以默认权限(通常是 0o644)暴露在磁盘上。
js
await writeFile(".token", secretValue, "utf8");
await chmod(".token", 0o600);