NodeJS 速習チュートリアル

Node.js File System

1. Node.js File System の紹介

Node.js の File System (fs) モジュールは、コンピューター上のファイルシステムを操作するための包括的なメソッド群を提供します。
これを利用することで、ファイル I/O 操作を 同期(Synchronous)非同期(Asynchronous) の両方の方法で実行できます。

注意: File System モジュールは Node.js の コアモジュール であるため、別途インストールする必要はありません。

2. File System モジュールのインポート

File System モジュールは、CommonJSrequire() または ES modulesimport 構文を使用してインポートできます。

2.1 CommonJS(Node.js のデフォルト)

const fs = require('fs');

2.2 ES Modules(Node.js 14以上、package.json で "type": "module" を設定)

import fs from 'fs';
// または特定のメソッドのみ:
// import { readFile, writeFile } from 'fs/promises';

2.3 Promise ベースの API

Node.js は、モダンなアプリケーションに推奨される fs/promises 名前空間で、Promise ベースの File System API を提供しています。

// Promise を使用 (Node.js 10.0.0+)
const fs = require('fs').promises;

// または分割代入(Destructuring)を使用
const { readFile, writeFile } = require('fs').promises;

// または ES modules で
// import { readFile, writeFile } from 'fs/promises';

3. よくあるユースケース

3.1 ファイル操作

  • ファイルの読み込みと書き込み
  • ファイルの作成と削除
  • ファイルへの追記(Append)
  • ファイルの名前変更と移動
  • ファイル権限の変更

3.2 ディレクトリ操作

  • ディレクトリの作成と削除
  • ディレクトリ内容の一覧表示
  • ファイル変更の監視(Watch)
  • ファイル/ディレクトリ情報の取得(Stats)
  • ファイルの存在確認

3.3 アドバンスド機能

  • ファイルストリーム
  • ファイル記述子(File descriptors)
  • シンボリックリンク
  • ファイル権限の管理

パフォーマンス・チップ: 大容量のファイルを扱う場合は、高いメモリ使用量を避けるために ストリームfs.createReadStream および fs.createWriteStream)の使用を検討してください。

4. ファイルの読み込み

Node.js は、コールバックベースと Promise ベースの両方のアプローチを含む、ファイルを読み込むためのいくつかのメソッドを提供しています。
最も一般的なメソッドは fs.readFile() です。

       注意: アプリケーションのクラッシュを防ぐため、ファイル操作を行う際は必ずエラーハンドリングを行ってください。

4.1 コールバックによるファイルの読み込み

以下は、伝統的なコールバックパターンを使用してファイルを読み込む方法です。

myfile.txt

これは myfile.txt の内容です。

テキストファイルを読み込み、その内容を返す Node.js ファイルを作成します:

例:コールバックを使用したファイルの読み込み

const fs = require('fs');

// コールバックを使用して非同期でファイルを読み込む
fs.readFile('myfile.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('ファイルの読み取り中にエラーが発生しました:', err);
    return;
  }
  console.log('ファイルの内容:', data);
});

// バイナリデータ(画像など)の場合はエンコーディングを省略
fs.readFile('image.png', (err, data) => {
  if (err) throw err;
  // data はファイル内容を含む Buffer です
  console.log('画像サイズ:', data.length, 'バイト');
});

4.2 Promise による読み込み(モダンな手法)

よりクリーンな async/await 構文のために、fs.promises または util.promisify を使用します。

例:async/await を使用したファイルの読み込み

// fs.promises を使用 (Node.js 10.0.0+)
const fs = require('fs').promises;

async function readFileExample() {
  try {
    const data = await fs.readFile('myfile.txt', 'utf8');
    console.log('ファイルの内容:', data);
  } catch (err) {
    console.error('ファイルの読み取り中にエラーが発生しました:', err);
  }
}

readFileExample();

// または util.promisify を使用 (Node.js 8.0.0+)
const { promisify } = require('util');
const readFileAsync = promisify(require('fs').readFile);

async function readWithPromisify() {
  try {
    const data = await readFileAsync('myfile.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readWithPromisify();

4.3 同期的なファイルの読み込み

シンプルなスクリプトでは同期メソッドを使用できますが、イベントループをブロックするため、本番環境のサーバーでは避けてください。

例:ファイルを同期的に読み込む

const fs = require('fs');

try {
  // ファイルを同期的に読み込む
  const data = fs.readFileSync('myfile.txt', 'utf8');
  console.log('ファイルの内容:', data);
} catch (err) {
  console.error('ファイルの読み取り中にエラーが発生しました:', err);
}
ベストプラクティス: テキストファイルを読み込んで Buffer ではなく文字列として取得したい場合は、必ず文字エンコーディング('utf8' など)を指定してください。

5. ファイルの作成と書き込み

Node.js は、ファイルの作成と書き込みのためのメソッドをいくつか提供しています。
一般的なアプローチは以下の通りです。

5.1 fs.writeFile() の使用

新しいファイルを作成するか、既存のファイルを指定された内容で上書きします。

例:ファイルへの書き込み

const fs = require('fs').promises;

async function writeFileExample() {
  try {
    // テキストをファイルに書き込む
    await fs.writeFile('myfile.txt', 'Hello, World!', 'utf8');

    // JSON データを書き込む
    const data = { name: 'John', age: 30, city: 'New York' };
    await fs.writeFile('data.json', JSON.stringify(data, null, 2), 'utf8');

    console.log('ファイルが正常に作成されました');
  } catch (err) {
    console.error('ファイルの書き込み中にエラーが発生しました:', err);
  }
}

writeFileExample();

5.2 fs.appendFile() の使用

内容をファイルに追記します。ファイルが存在しない場合は作成されます。

例:ファイルへの追記

const fs = require('fs').promises;

async function appendToFile() {
  try {
    // タイムスタンプ付きのログエントリを追記
    const logEntry = `${new Date().toISOString()}: アプリケーションが起動しました\n`;
    await fs.appendFile('app.log', logEntry, 'utf8');

    console.log('ログエントリが追加されました');
  } catch (err) {
    console.error('ファイルへの追記中にエラーが発生しました:', err);
  }
}

appendToFile();

5.3 ファイルハンドルの使用

ファイル操作をより細かく制御するには、ファイルハンドルを使用します。

例:ファイルハンドルの使用

const fs = require('fs').promises;

async function writeWithFileHandle() {
  let fileHandle;

  try {
    // 書き込み用にファイルを開く(存在しない場合は作成)
    fileHandle = await fs.open('output.txt', 'w');

    // コンテンツをファイルに書き込む
    await fileHandle.write('1行目\n');
    await fileHandle.write('2行目\n');
    await fileHandle.write('3行目\n');

    console.log('コンテンツが正常に書き込まれました');
  } catch (err) {
    console.error('ファイルへの書き込み中にエラーが発生しました:', err);
  } finally {
    // 必ずファイルハンドルを閉じる
    if (fileHandle) {
      await fileHandle.close();
    }
  }
}

writeWithFileHandle();

5.4 大容量ファイル向けのストリーム使用

大量のデータを書き込む場合は、メモリの大量消費を避けるためにストリームを使用します。

例:ストリームを使用した大容量ファイルの書き込み

const fs = require('fs');
const { pipeline } = require('stream/promises');
const { Readable } = require('stream');

async function writeLargeFile() {
  // 読み取り可能なストリームを作成(HTTP リクエストなどからも可能)
  const data = Array(1000).fill().map((_, i) => `Line ${i + 1}: ${'x'.repeat(100)}\n`);
  const readable = Readable.from(data);

  // ファイルへの書き込み用ストリームを作成
  const writable = fs.createWriteStream('large-file.txt');

  try {
    // 読み取りストリームから書き込みストリームへデータをパイプ(転送)する
    await pipeline(readable, writable);
    console.log('大容量ファイルが正常に書き込まれました');
  } catch (err) {
    console.error('ファイルの書き込み中にエラーが発生しました:', err);
  }
}

writeLargeFile();

ファイルフラグ: ファイルを開く際、さまざまなモードを指定できます:

  • 'w' - 書き込み用に開く(ファイルが作成されるか、既存の場合はサイズが0に切り詰められる)
  • 'wx' - 'w' と同様だが、パスが存在する場合は失敗する
  • 'w+' - 読み書き用に開く(ファイルが作成されるか、既存の場合は切り詰められる)
  • 'a' - 追記用に開く(ファイルが存在しない場合は作成される)
  • 'ax' - 'a' と同様だが、パスが存在する場合は失敗する
  • 'r+' - 読み書き用に開く(ファイルが存在している必要がある)

6. ファイルとディレクトリの削除

Node.js は、ファイルやディレクトリを削除するための複数のメソッドを提供しています。

6.1 単一ファイルの削除

ファイルを削除するには fs.unlink() を使用します。

例:ファイルの削除

const fs = require('fs').promises;

async function deleteFile() {
  const filePath = 'file-to-delete.txt';

  try {
    // 削除前にファイルが存在するか確認
    await fs.access(filePath);

    // ファイルを削除
    await fs.unlink(filePath);
    console.log('ファイルが正常に削除されました');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('ファイルが存在しません');
    } else {
      console.error('ファイルの削除中にエラーが発生しました:', err);
    }
  }
}

deleteFile();

6.2 複数ファイルの削除

複数のファイルを削除するには、Promise.all()fs.unlink() を組み合わせて使用します。

例:複数ファイルの削除

const fs = require('fs').promises;
const path = require('path');

async function deleteFiles() {
  const filesToDelete = [
    'temp1.txt',
    'temp2.txt',
    'temp3.txt'
  ];

  try {
    // すべてのファイルを並列で削除
    await Promise.all(
      filesToDelete.map(file =>
        fs.unlink(file).catch(err => {
          if (err.code !== 'ENOENT') {
            console.error(`${file} の削除中にエラーが発生しました:`, err);
          }
        })
      )
    );

    console.log('ファイルが正常に削除されました');
  } catch (err) {
    console.error('ファイル削除中にエラーが発生しました:', err);
  }
}

deleteFiles();

6.3 ディレクトリの削除

ディレクトリを削除する場合、ニーズに応じていくつかのオプションがあります。

例:ディレクトリの削除

const fs = require('fs').promises;
const path = require('path');

async function deleteDirectory(dirPath) {
  try {
    // ディレクトリが存在するか確認
    const stats = await fs.stat(dirPath);
   
    if (!stats.isDirectory()) {
      console.log('パスがディレクトリではありません');
      return;
    }

    // Node.js 14.14.0+ 用 (推奨)
    await fs.rm(dirPath, { recursive: true, force: true });

    // 古い Node.js バージョン用 (非推奨だがまだ動作する)
    // await fs.rmdir(dirPath, { recursive: true });

    console.log('ディレクトリが正常に削除されました');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('ディレクトリが存在しません');
    } else {
      console.error('ディレクトリの削除中にエラーが発生しました:', err);
    }
  }
}

// 使用例
deleteDirectory('directory-to-delete');

6.4 ディレクトリを削除せずに中身を空にする

ディレクトリ自体は残し、その中のすべてのファイルとサブディレクトリを削除します。

例:ディレクトリを空にする

const fs = require('fs').promises;
const path = require('path');

async function emptyDirectory(dirPath) {
  try {
    // ディレクトリを読み込む
    const files = await fs.readdir(dirPath, { withFileTypes: true });

    // すべてのファイルとディレクトリを並列で削除
    await Promise.all(
      files.map(file => {
        const fullPath = path.join(dirPath, file.name);
        return file.isDirectory()
          ? fs.rm(fullPath, { recursive: true, force: true })
          : fs.unlink(fullPath);
      })
    );

    console.log('ディレクトリの中身が正常に削除されました');
  } catch (err) {
    console.error('ディレクトリの消去中にエラーが発生しました:', err);
  }
}

// 使用例
emptyDirectory('directory-to-empty');

       セキュリティに関する注意: ファイルの削除、特に再帰オプションやワイルドカードを使用する場合は細心の注意を払ってください。ディレクトリトラバーサル攻撃を防ぐために、必ずファイルパスをバリデーションし、サニタイズしてください。

7. ファイルの名前変更と移動

fs.rename() メソッドは、ファイルの名前変更と移動の両方に使用できます。
これはファイルパスの変更を伴う操作において、非常に汎用性の高いメソッドです。

7.1 基本的なファイルの名前変更

同じディレクトリ内でファイル名を変更します。

例:ファイルの名前変更

const fs = require('fs').promises;

async function renameFile() {
  const oldPath = 'old-name.txt';
  const newPath = 'new-name.txt';

  try {
    // ソースファイルが存在するか確認
    await fs.access(oldPath);

    // 送り先のファイルが既に存在するか確認
    try {
      await fs.access(newPath);
      console.log('送り先のファイルが既に存在します');
      return;
    } catch (err) {
      // 送り先が存在しないため、処理を続行可能
    }

    // 名前変更を実行
    await fs.rename(oldPath, newPath);
    console.log('ファイル名が正常に変更されました');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('ソースファイルが存在しません');
    } else {
      console.error('ファイル名の変更中にエラーが発生しました:', err);
    }
  }
}

// 使用例
renameFile();

7.2 ディレクトリ間のファイル移動

fs.rename() を使用して、ディレクトリ間でファイルを移動できます。

例:ファイルを別のディレクトリへ移動

const fs = require('fs').promises;
const path = require('path');

async function moveFile() {
  const sourceFile = 'source/file.txt';
  const targetDir = 'destination';
  const targetFile = path.join(targetDir, 'file.txt');

  try {
    // ソースファイルが存在することを確認
    await fs.access(sourceFile);

    // ターゲットディレクトリが存在しない場合は作成
    await fs.mkdir(targetDir, { recursive: true });

    // ファイルを移動
    await fs.rename(sourceFile, targetFile);

    console.log('ファイルが正常に移動されました');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('ソースファイルが存在しません');
    } else if (err.code === 'EXDEV') {
      console.log('デバイスを跨ぐ移動が検出されました。コピー&削除フォールバックを使用します');
      await moveAcrossDevices(sourceFile, targetFile);
    } else {
      console.error('ファイルの移動中にエラーが発生しました:', err);
    }
  }
}

// デバイス間移動のためのヘルパー関数
async function moveAcrossDevices(source, target) {
  try {
    // ファイルをコピー
    await fs.copyFile(source, target);

    // 元のファイルを削除
    await fs.unlink(source);

    console.log('デバイスを跨ぐファイル移動が成功しました');
  } catch (err) {
    // エラーが発生した場合はクリーンアップ
    try { await fs.unlink(target); } catch (e) {}
    throw err;
  }
}

// 使用例
moveFile();

7.3 ファイルの一括名前変更

パターンに一致する複数のファイルの名前を一括で変更します。

例:ファイルの一括名前変更

const fs = require('fs').promises;
const path = require('path');

async function batchRename() {
  const directory = 'images';
  const pattern = /^image(\d+)\.jpg$/;

  try {
    // ディレクトリの内容を読み込む
    const files = await fs.readdir(directory);

    // 各ファイルを処理
    for (const file of files) {
      const match = file.match(pattern);
      if (match) {
        const [_, number] = match;
        const newName = `photo-${number.padStart(3, '0')}.jpg`;
        const oldPath = path.join(directory, file);
        const newPath = path.join(directory, newName);

        // 新しい名前が古い名前と同じ場合はスキップ
        if (oldPath !== newPath) {
          await fs.rename(oldPath, newPath);
          console.log(`変更前: ${file} - 変更後: ${newName}`);
        }
      }
    }

    console.log('一括名前変更が完了しました');
  } catch (err) {
    console.error('一括名前変更中にエラーが発生しました:', err);
  }
}

batchRename();

7.4 アトミックな名前変更操作

クリティカルな操作では、一時ファイルを使用して アトミック性(Atomicity) を確保します。

例:アトミックなファイル更新

const fs = require('fs').promises;
const path = require('path');
const os = require('os');

async function updateFileAtomic(filePath, newContent) {
  const tempPath = path.join(
    os.tmpdir(),
    `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  );

  try {
    // 1. 一時ファイルに書き込む
    await fs.writeFile(tempPath, newContent, 'utf8');

    // 2. 一時ファイルが正しく書き込まれたか検証
    const stats = await fs.stat(tempPath);
    if (stats.size === 0) {
      throw new Error('一時ファイルが空です');
    }

    // 3. 名前を変更(ほとんどのシステムでアトミックな操作)
    await fs.rename(tempPath, filePath);

    console.log('ファイルがアトミックに更新されました');
  } catch (err) {
    // エラー発生時は一時ファイルを削除
    try { await fs.unlink(tempPath); } catch (e) {}

    console.error('アトミック更新に失敗しました:', err);
    throw err;
  }
}

// 使用例
updateFileAtomic('important-config.json', JSON.stringify({ key: 'value' }, null, 2));

クロスプラットフォームに関する注意:fs.rename() 操作は Unix 系システムではアトミックですが、Windows ではアトミックでない場合があります。クロスプラットフォームでアトミックな操作が必要な場合は、上記の例のような一時ファイルアプローチを検討してください。