ข้ามไปเนื้อหาหลัก

Category: reference

Node.js File System — fs/promises, path, Common Patterns

อ่าน เขียน คัดลอก ลบไฟล์และ directory ด้วย fs/promises และ path module — patterns ที่ใช้บ่อยใน scripts

· อ่านประมาณ 4 นาที

สารบัญ

Import

import fs from 'node:fs/promises'; // promise-based (แนะนำ)
import path from 'node:path';
import { existsSync } from 'node:fs'; // sync version (ใช้กับ startup check)

ใช้ node: prefix เพื่อบอกว่าเป็น built-in module ชัดเจน (Node.js 14.18+)


อ่านไฟล์

// อ่านเป็น string
const content = await fs.readFile('config.json', 'utf-8');
const config = JSON.parse(content);

// อ่านเป็น Buffer (binary)
const buffer = await fs.readFile('image.png');

// อ่าน JSON สั้นกว่า
const data = JSON.parse(await fs.readFile('data.json', 'utf-8'));

// อ่านหลายไฟล์พร้อมกัน
const [a, b, c] = await Promise.all([
  fs.readFile('file-a.txt', 'utf-8'),
  fs.readFile('file-b.txt', 'utf-8'),
  fs.readFile('file-c.txt', 'utf-8'),
]);

เขียนไฟล์

// เขียนทับ (ถ้าไม่มีไฟล์จะสร้างใหม่)
await fs.writeFile('output.txt', 'Hello World', 'utf-8');

// เขียน JSON
await fs.writeFile('config.json', JSON.stringify(config, null, 2));

// ต่อท้ายไฟล์ (append)
await fs.appendFile('log.txt', `${new Date().toISOString()} - Event happened\n`);

// เขียนไฟล์ binary
await fs.writeFile('image.png', buffer);

Path Module

import path from 'node:path';

// ต่อ path (จัดการ separator อัตโนมัติ)
path.join('src', 'components', 'Button.tsx'); // 'src/components/Button.tsx'

// resolve เป็น absolute path
path.resolve('src', 'index.ts'); // '/project/src/index.ts'
path.resolve(__dirname, '../config'); // ใช้ __dirname ใน CommonJS

// ดึง parts
path.basename('/path/to/file.ts');           // 'file.ts'
path.basename('/path/to/file.ts', '.ts');    // 'file' (ตัด extension)
path.extname('/path/to/file.ts');            // '.ts'
path.dirname('/path/to/file.ts');            // '/path/to'

// แยก path
path.parse('/home/user/file.ts');
// { root: '/', dir: '/home/user', base: 'file.ts', ext: '.ts', name: 'file' }

// ตรวจสอบ relative/absolute
path.isAbsolute('/etc/config'); // true
path.isAbsolute('relative/path'); // false

Directory Operations

// สร้าง directory (รวม parent)
await fs.mkdir('dist/assets/images', { recursive: true });

// อ่านรายการไฟล์ใน directory
const files = await fs.readdir('src');
// ['components', 'pages', 'styles', 'index.ts']

// อ่านพร้อม metadata
const entries = await fs.readdir('src', { withFileTypes: true });
for (const entry of entries) {
  if (entry.isFile())      console.log('File:', entry.name);
  if (entry.isDirectory()) console.log('Dir:', entry.name);
  if (entry.isSymbolicLink()) console.log('Link:', entry.name);
}

// ลบ directory (รวมเนื้อหา)
await fs.rm('dist', { recursive: true, force: true });
// หรือ node 14.14+
await fs.rmdir('dist', { recursive: true });

// เช็คว่ามีอยู่ไหม
try {
  await fs.access('config.json');
  console.log('exists');
} catch {
  console.log('not found');
}
// หรือใช้ sync version
if (existsSync('config.json')) { ... }

Copy และ Rename

// คัดลอกไฟล์
await fs.copyFile('source.txt', 'destination.txt');

// ย้ายไฟล์ (rename + move)
await fs.rename('old-path/file.txt', 'new-path/file.txt');

// ลบไฟล์
await fs.unlink('temp.txt');

File Metadata

const stat = await fs.stat('package.json');

stat.size;           // bytes
stat.mtime;          // Date — last modified
stat.atime;          // Date — last accessed
stat.isFile();       // true
stat.isDirectory();  // false

// lstat ไม่ follow symbolic links
const lstat = await fs.lstat('symlink');
lstat.isSymbolicLink(); // true ถ้าเป็น symlink

Pattern: รวบรวมไฟล์ใน Directory แบบ Recursive

import fs from 'node:fs/promises';
import path from 'node:path';

async function* walk(dir: string): AsyncGenerator<string> {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      yield* walk(fullPath); // recurse
    } else {
      yield fullPath;
    }
  }
}

// หาไฟล์ .md ทั้งหมด
for await (const file of walk('src/content')) {
  if (file.endsWith('.md')) {
    console.log(file);
  }
}

Pattern: อ่าน/แก้/เขียน JSON Config

async function updateConfig(configPath: string, updates: Record<string, unknown>) {
  let config: Record<string, unknown> = {};
  try {
    const content = await fs.readFile(configPath, 'utf-8');
    config = JSON.parse(content);
  } catch {
    // ไม่มีไฟล์ — เริ่มใหม่
  }
  Object.assign(config, updates);
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
}

await updateConfig('~/.config/myapp/settings.json', { theme: 'dark' });

Pattern: Atomic Write

import { randomUUID } from 'node:crypto';

async function atomicWrite(filePath: string, content: string) {
  const tmp = `${filePath}.${randomUUID()}.tmp`;
  try {
    await fs.writeFile(tmp, content, 'utf-8');
    await fs.rename(tmp, filePath); // atomic บน same filesystem
  } catch (err) {
    await fs.unlink(tmp).catch(() => {}); // cleanup ถ้า error
    throw err;
  }
}

Atomic write ป้องกันไฟล์ถูกเขียนครึ่งๆ ถ้าโปรแกรม crash ระหว่างทาง


Pattern: Watch Files

const watcher = fs.watch('src', { recursive: true });

for await (const { eventType, filename } of watcher) {
  console.log(`${eventType}: ${filename}`);
  // 'change: pages/index.astro'
  // 'rename: components/Button.tsx'
}

// หรือ callback style
import { watch } from 'node:fs';
const watcher = watch('src', { recursive: true }, (event, filename) => {
  console.log(event, filename);
});
watcher.close();

Error Handling

import { constants } from 'node:fs';

async function safeRead(filePath: string): Promise<string | null> {
  try {
    return await fs.readFile(filePath, 'utf-8');
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
      return null; // ไม่มีไฟล์
    }
    if ((err as NodeJS.ErrnoException).code === 'EACCES') {
      throw new Error(`Permission denied: ${filePath}`);
    }
    throw err; // error อื่น
  }
}

Common error codes:

  • ENOENT — ไม่มีไฟล์/directory
  • EEXIST — มีอยู่แล้ว
  • EACCES / EPERM — permission error
  • EISDIR — คาดว่าเป็น file แต่เจอ directory
  • ENOTDIR — คาดว่าเป็น directory แต่เจอ file