强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Deno 入门教程 / 第 09 章:文件 I/O

第 09 章:文件 I/O

9.1 文件读取

读取文本文件

// 读取整个文件为字符串
const text = await Deno.readTextFile("./data.txt");
console.log(text);

// 同步读取
const textSync = Deno.readTextFileSync("./data.txt");
console.log(textSync);

⚠️ 权限要求--allow-read

读取二进制文件

// 读取为 Uint8Array
const bytes = await Deno.readFile("./image.png");
console.log("文件大小:", bytes.length, "字节");

// 读取并处理图片(简单示例)
const header = bytes.slice(0, 8);
console.log("文件头:", Array.from(header).map(b => b.toString(16)));

使用 Deno.open 读取

// 打开文件获取文件句柄
const file = await Deno.open("./data.txt", { read: true });

// 读取全部内容
const content = await new Response(file.readable).text();
console.log(content);

file.close();

逐行读取

// 方法 1:使用 TextLineStream
import { TextLineStream } from "jsr:@std/streams/text-line-stream";

const file = await Deno.open("./large-file.txt");
const lines = file.readable
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(new TextLineStream());

for await (const line of lines) {
  console.log("行:", line);
}
file.close();

// 方法 2:手动解析
const text2 = await Deno.readTextFile("./data.txt");
const lines2 = text2.split("\n");
for (const line of lines2) {
  console.log(line.trim());
}

分块读取大文件

const file = await Deno.open("./large-file.bin", { read: true });
const reader = file.readable.getReader();

let totalBytes = 0;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  totalBytes += value.length;
  // 处理每个 chunk
  console.log(`已读取 ${totalBytes} 字节`);
}

file.close();
console.log(`总共读取 ${totalBytes} 字节`);

9.2 文件写入

写入文本文件

// 写入字符串
await Deno.writeTextFile("./output.txt", "Hello, Deno!\n");

// 追加写入
await Deno.writeTextFile("./log.txt", `[${new Date().toISOString()}] 新日志\n`, {
  append: true,
});

⚠️ 权限要求--allow-write

写入二进制数据

// 写入二进制数据
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
await Deno.writeFile("./binary.dat", data);

// 写入并创建目录
await Deno.mkdir("./output", { recursive: true });
await Deno.writeTextFile("./output/result.json", JSON.stringify({ success: true }));

使用 Deno.open 写入

// 打开文件用于写入
const file = await Deno.open("./output.txt", {
  write: true,
  create: true,      // 文件不存在则创建
  truncate: true,    // 清空已有内容
});

// 使用 Writer 接口
const encoder = new TextEncoder();
await file.write(encoder.encode("第一行\n"));
await file.write(encoder.encode("第二行\n"));

file.close();

使用 WritableStream

const file = await Deno.open("./stream-output.txt", {
  write: true,
  create: true,
});

const writer = file.writable.getWriter();
const encoder = new TextEncoder();

await writer.write(encoder.encode("流式写入第一行\n"));
await writer.write(encoder.encode("流式写入第二行\n"));
await writer.close();

复制文件(使用流)

// 高效复制文件
async function copyFile(src: string, dest: string) {
  const srcFile = await Deno.open(src, { read: true });
  const destFile = await Deno.open(dest, { write: true, create: true });
  
  await srcFile.readable.pipeTo(destFile.writable);
  // pipeTo 自动关闭两端
}

await copyFile("./source.mp4", "./dest.mp4");

9.3 文件信息与属性

// 获取文件信息(stat)
const info = await Deno.stat("./data.txt");
console.log("是文件:", info.isFile);
console.log("是目录:", info.isDirectory);
console.log("是符号链接:", info.isSymlink);
console.log("文件大小:", info.size, "字节");
console.log("修改时间:", info.mtime);
console.log("创建时间:", info.birthtime);
console.log("访问时间:", info.atime);

// 获取文件大小(简写)
const size = (await Deno.stat("./data.txt")).size;

// 检查文件是否存在
async function fileExists(path: string): Promise<boolean> {
  try {
    await Deno.stat(path);
    return true;
  } catch (error) {
    if (error instanceof Deno.errors.NotFound) {
      return false;
    }
    throw error;
  }
}

console.log(await fileExists("./data.txt"));  // true 或 false

文件权限

// 获取文件权限
const info = await Deno.stat("./data.txt");
console.log("文件模式:", info.mode?.toString(8));

// 修改文件权限(Unix)
await Deno.chmod("./script.sh", 0o755);

// 修改文件所有者(Unix)
await Deno.chown("./data.txt", 1000, 1000);  // uid, gid

9.4 目录操作

创建目录

// 创建单层目录
await Deno.mkdir("./new-dir");

// 递归创建多层目录
await Deno.mkdir("./parent/child/grandchild", { recursive: true });

读取目录

// 读取目录内容
const entries = [];
for await (const entry of Deno.readDir("./src")) {
  entries.push({
    name: entry.name,
    isFile: entry.isFile,
    isDirectory: entry.isDirectory,
    isSymlink: entry.isSymlink,
  });
}
console.log(entries);

删除目录

// 删除空目录
await Deno.remove("./empty-dir");

// 递归删除目录及内容
await Deno.remove("./dir-to-delete", { recursive: true });

重命名/移动

// 重命名文件
await Deno.rename("./old-name.txt", "./new-name.txt");

// 移动文件
await Deno.rename("./file.txt", "./archive/file.txt");

删除文件

// 删除文件
await Deno.remove("./file.txt");

// 删除前检查
try {
  await Deno.remove("./file.txt");
  console.log("删除成功");
} catch (error) {
  if (error instanceof Deno.errors.NotFound) {
    console.log("文件不存在");
  } else {
    throw error;
  }
}

9.5 文件监听(Watch)

使用 Deno.watchFs

// 监听文件变化
const watcher = Deno.watchFs("./src");

for await (const event of watcher) {
  console.log("事件类型:", event.kind);   // "create" | "modify" | "remove" | "access" | "any" | "other"
  console.log("影响文件:", event.paths);
}

// 注意:这是一个无限循环,需要 Ctrl+C 停止

监听特定文件类型

const watcher = Deno.watchFs("./src", { recursive: true });

for await (const event of watcher) {
  // 只关注 .ts 文件的变化
  const tsFiles = event.paths.filter(p => p.endsWith(".ts"));
  if (tsFiles.length > 0) {
    console.log(`[TS] ${event.kind}:`, tsFiles);
  }
}

带防抖的文件监听

import { debounce } from "jsr:@std/async";

const watcher = Deno.watchFs("./src", { recursive: true });

const handleChanges = debounce((paths: string[]) => {
  console.log("文件变化,重新构建...", paths);
  // 执行构建逻辑
}, 300);

for await (const event of watcher) {
  if (["create", "modify", "remove"].includes(event.kind)) {
    handleChanges(event.paths);
  }
}

9.6 临时文件

使用 Deno.makeTempFile

// 创建临时文件
const tempFile = await Deno.makeTempFile({ suffix: ".json" });
console.log("临时文件:", tempFile);  // /tmp/tmpXXXXXX.json

// 写入数据
await Deno.writeTextFile(tempFile, JSON.stringify({ temp: true }));

// 使用完毕后删除
await Deno.remove(tempFile);

创建临时目录

const tempDir = await Deno.makeTempDir({ prefix: "myapp_" });
console.log("临时目录:", tempDir);  // /tmp/myapp_XXXXXX

// 在临时目录中创建文件
await Deno.writeTextFile(`${tempDir}/data.json`, "{}");

// 用完删除
await Deno.remove(tempDir, { recursive: true });

使用 try-finally 确保清理

async function withTempFile<T>(
  fn: (path: string) => Promise<T>,
  options?: Deno.MakeTempOptions
): Promise<T> {
  const tempFile = await Deno.makeTempFile(options);
  try {
    return await fn(tempFile);
  } finally {
    await Deno.remove(tempFile);
  }
}

// 使用
const result = await withTempFile(async (path) => {
  await Deno.writeTextFile(path, "临时数据");
  return await Deno.readTextFile(path);
});
console.log(result);  // "临时数据"

9.7 符号链接

// 创建符号链接
await Deno.symlink("./original.txt", "./link.txt");

// 读取符号链接目标
const target = await Deno.readLink("./link.txt");
console.log("链接目标:", target);  // ./original.txt

// 获取真实路径
const realPath = await Deno.realPath("./link.txt");
console.log("真实路径:", realPath);  // /absolute/path/to/original.txt

9.8 综合实战:日志文件管理器

import { ensureDir } from "jsr:@std/fs";
import { join } from "jsr:@std/path";
import { debounce } from "jsr:@std/async";

class LogManager {
  #logDir: string;
  #currentFile?: Deno.FsFile;

  constructor(logDir: string = "./logs") {
    this.#logDir = logDir;
  }

  async init() {
    await ensureDir(this.#logDir);
    const filename = `app-${new Date().toISOString().split("T")[0]}.log`;
    const filepath = join(this.#logDir, filename);
    this.#currentFile = await Deno.open(filepath, { write: true, create: true, append: true });
  }

  async log(level: string, message: string) {
    const timestamp = new Date().toISOString();
    const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
    const encoder = new TextEncoder();
    await this.#currentFile!.write(encoder.encode(line));
  }

  async info(message: string) { await this.log("info", message); }
  async warn(message: string) { await this.log("warn", message); }
  async error(message: string) { await this.log("error", message); }

  close() {
    this.#currentFile?.close();
  }

  // 清理旧日志
  async cleanOldLogs(days: number = 30) {
    const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
    for await (const entry of Deno.readDir(this.#logDir)) {
      if (entry.isFile && entry.name.endsWith(".log")) {
        const info = await Deno.stat(join(this.#logDir, entry.name));
        if (info.mtime && info.mtime.getTime() < cutoff) {
          await Deno.remove(join(this.#logDir, entry.name));
          console.log(`清理旧日志:${entry.name}`);
        }
      }
    }
  }
}

// 使用示例
const logger = new LogManager("./app-logs");
await logger.init();
await logger.info("应用启动");
await logger.warn("内存使用率偏高");
await logger.error("数据库连接失败");
logger.close();

9.9 本章小结

操作API权限
读取文本Deno.readTextFile()--allow-read
读取二进制Deno.readFile()--allow-read
写入文本Deno.writeTextFile()--allow-write
写入二进制Deno.writeFile()--allow-write
文件信息Deno.stat()--allow-read
创建目录Deno.mkdir()--allow-write
读取目录Deno.readDir()--allow-read
删除Deno.remove()--allow-write
重命名Deno.rename()--allow-read + --allow-write
监听Deno.watchFs()--allow-read
临时文件Deno.makeTempFile()--allow-read + --allow-write

📖 扩展阅读


下一章第 10 章:HTTP 服务器 → 使用 Deno 构建 Web 服务器。