404 Not Found

404 Not Found


nginx

「FSの基礎:ファイルシステムの読み取り、書き込み、およびディレクトリ操作」

チャーリーは、会社のログ分析プラットフォームの保守を担当するバックエンドエンジニアです。毎日明け方、システムは約50,000行のサーバーログファイルを処理する必要があります。当初、彼はfs.readFileSyncを使用してログを1行ずつ読み込んでいましたが、読み込み処理中にプログラム全体が完全にフリーズし、他のすべてのリクエストがタイムアウトしてしまいました。fs.readFile を使った非同期読み取りに切り替えたところ、プログラムはディスク I/O を待機している間も他のリクエストに応答できるようになり、全体的なスループットが10倍に向上しました。この経験を通じて、彼は Node.js のファイルシステム API における同期処理と非同期処理の違いを深く理解するようになりました。

1. 学習内容


2. fs モジュールの概要

fs モジュールは、ファイルシステム操作のための Node.js 組み込みモジュールであり、ファイルの読み書き、ディレクトリ管理、権限チェックなどの機能を提供します。各操作には通常、同期、非同期コールバック、非同期 Promise の 3 つのスタイルが用意されています。

機能 同期方式 非同期コールバック方式 fs.promises メソッド
命名機能 xxxSync 接尾辞 接尾辞なし fs.promises.xxx
戻り値 結果を直接返す コールバックを介して取得した undefined Promise を返す
イベントループをブロックする はい いいえ いいえ
エラー処理 try/catch コールバックの最初の引数 .catch() / try-catch
おすすめのユースケース 起動時の設定読み込み 下位互換性 新規プロジェクトに最適
JAVASCRIPT
const fs = require('fs');

// Synchronize
const data = fs.readFileSync('config.json', 'utf8');

// Asynchronous Callbacks
fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Promise
const fsPromises = require('fs/promises');
fsPromises.readFile('config.json', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

3. 同期実行シーケンスと非同期実行シーケンス

同期メソッドはイベントループをブロックし、ファイル操作が完了するまで後続のコードの実行を続行しません。一方、非同期メソッドは即座に処理を返し、ファイル操作が完了すると、コールバックまたはPromiseを介して結果を通知します。

100%
sequenceDiagram
    participant Main as Main Thread
    participant FS_Sync as Synchronous Read
    participant FS_Async as Asynchronous Reading
    participant Disk as Disk I/O

    Note over Main,Disk: Synchronous Execution Process
    Main->>FS_Sync: readFileSync('log.txt')
    FS_Sync->>Disk: Read a File(Blocking Wait)
    Disk-->>FS_Sync: Return Data
    FS_Sync-->>Main: Continue executing the following code
    Note right of Main: All other requests are on hold

    Note over Main,Disk: Asynchronous Execution Flow
    Main->>FS_Async: readFile('log.txt', callback)
    FS_Async->>Disk: Submit a read request
    FS_Async-->>Main: Return Now,Continue Execution
    Note right of Main: Can handle other requests
    Disk-->>FS_Async: I/O Done
    FS_Async-->>Main: Execute the callback function

▶ 例:同期読み取りはプログラム全体をブロックする

JAVASCRIPT
const fs = require('fs');

console.log('Start Reading...');
const data = fs.readFileSync('big-log.txt', 'utf8');
console.log('Reading complete,Number of lines:', data.split('\n').length);
console.log('This line must wait until the data has finished loading before it is executed.');
TEXT
Start Reading...
Reading complete,Number of lines:50000
This line must wait until the data has finished loading before it is executed.

▶ 例:イベントループをブロックしない非同期読み込み

JAVASCRIPT
const fs = require('fs');

console.log('Start Reading...');
fs.readFile('big-log.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('Reading complete,Number of lines:', data.split('\n').length);
});
console.log('This line is executed immediately,No need to wait for the data to be read');
TEXT
Start Reading...
This line is executed immediately,No need to wait for the data to be read
Reading complete,Number of lines:50000

4. ファイルの読み取りおよび書き込み操作

メソッド パラメータ 目的
fs.readFile(path, encoding, callback) パス、エンコーディング、コールバック ファイル全体の非同期読み込み
fs.readFileSync(path, encoding) パス、エンコーディング ファイル全体を同期的に読み込む
fs.writeFile(path, data, encoding, callback) パス、データ、エンコーディング、コールバック 非同期書き込み(上書き)
fs.writeFileSync(path, data, encoding) パス、データ、エンコーディング 同期書き込み(上書き)
fs.appendFile(path, data, encoding, callback) パス、データ、エンコーディング、コールバック 非同期追加
fs.appendFileSync(path, data, encoding) パス、データ、エンコーディング 追加されたコンテンツの同期
fs.unlink(path, callback) パス、コールバック 非同期ファイル削除
fs.unlinkSync(path) パス ファイルを同時に削除

▶ 例:ファイルへの書き込みと追記

JAVASCRIPT
const fs = require('fs');

fs.writeFile('output.txt', 'Content on the first line\n', 'utf8', (err) => {
  if (err) throw err;
  console.log('Write complete');

  fs.appendFile('output.txt', 'The second line added\n', 'utf8', (err) => {
    if (err) throw err;
    console.log('Addition Complete');

    fs.readFile('output.txt', 'utf8', (err, data) => {
      if (err) throw err;
      console.log('Document Content:\n', data);
    });
  });
});
TEXT
Write complete
Addition Complete
Document Content:
 Content on the first line
The second line added

▶ 例:fs.promises を使用してネストされたコールバックを回避する

JAVASCRIPT
const fs = require('fs/promises');

async function writeAndRead() {
  try {
    await fs.writeFile('output.txt', 'Content on the first line\n', 'utf8');
    console.log('Write complete');
    await fs.appendFile('output.txt', 'The second line added\n', 'utf8');
    console.log('Addition Complete');
    const data = await fs.readFile('output.txt', 'utf8');
    console.log('Document Content:\n', data);
  } catch (err) {
    console.error('Operation Failed:', err.message);
  }
}

writeAndRead();

▶ 例:ファイルの削除

JAVASCRIPT
const fs = require('fs/promises');

async function deleteFile() {
  try {
    await fs.unlink('output.txt');
    console.log('The file has been deleted');
  } catch (err) {
    console.error('Deletion Failed:', err.message);
  }
}

deleteFile();

5. ディレクトリ操作

メソッド パラメータ 目的
fs.mkdir(path, options, callback) パス、{recursive}、コールバック ディレクトリの作成
fs.readdir(path, options, callback) パス、{withFileTypes}、コールバック ディレクトリの内容を表示
fs.stat(path, callback) パス、コールバック ファイル/ディレクトリ情報の取得
fs.existsSync(path) パス パスが存在するか確認

▶ 例:ディレクトリの作成とその内容の表示

JAVASCRIPT
const fs = require('fs/promises');

async function dirOperations() {
  try {
    await fs.mkdir('logs', { recursive: true });
    console.log('The directory was successfully created');

    await fs.writeFile('logs/app.log', '2025-01-01 Server started\n', 'utf8');
    await fs.writeFile('logs/error.log', '2025-01-01 Connection timeout\n', 'utf8');

    const files = await fs.readdir('logs');
    console.log('Table of Contents:', files);
  } catch (err) {
    console.error('Operation Failed:', err.message);
  }
}

dirOperations();
TEXT
The directory was successfully created
Table of Contents: [ 'app.log', 'error.log' ]

▶ 例:ファイルかディレクトリかを判別する

JAVASCRIPT
const fs = require('fs/promises');

async function checkType() {
  const stats = await fs.stat('logs');
  console.log('logs This is the table of contents:', stats.isDirectory());
  console.log('logs It is a file:', stats.isFile());

  const fileStats = await fs.stat('logs/app.log');
  console.log('app.log It is a file:', fileStats.isFile());
  console.log('File size:', fileStats.size, 'bytes');
}

checkType();
TEXT
logs This is the table of contents: true
logs It is a file: false
app.log It is a file: true
File size: 29 bytes

6. エラーファーストのコールバックとプロミスの比較

Node.jsのファイルシステムAPIは、「エラーファースト・コールバック」という規約に従っています。つまり、コールバック関数の最初の引数は常にエラーオブジェクトであり、エラーがない場合はnullとなります。fs.promisesは、標準のPromiseメカニズムを使用してエラーを処理します。

比較項目 エラーファーストのコールバック fs.promises
関数のシグネチャ (err, data) => {} 戻り値 Promise<data>
誤判定 if (err) 確認 try/catch または .catch()
ネストの問題 コールバック地獄に陥りやすい async/await フラット化
代表的なシナリオ 既存プロジェクトとの互換性 新規プロジェクトへの推奨事項
インポート方法 require('fs') require('fs/promises')

▶ 例:2つのエラー処理手法の比較

JAVASCRIPT
const fsCallback = require('fs');
const fsPromise = require('fs/promises');

// Error-First Callback
fsCallback.readFile('not-exist.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Callback Method - Error:', err.code);
    return;
  }
  console.log(data);
});

// Promise Method
async function readWithPromise() {
  try {
    const data = await fsPromise.readFile('not-exist.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('PromiseMethod - Error:', err.code);
  }
}

readWithPromise();
TEXT
Callback Method - Error: ENOENT
PromiseMethod - Error: ENOENT

7. ファイルのエンコーディング

コード 説明 適用されるシナリオ
'utf8' UTF-8 テキストエンコーディング(デフォルト) テキストファイルの読み書き ログ、設定、JSON
'base64' Base64エンコーディング 画像転送、バイナリからテキストへの変換 埋め込み画像、メールの添付ファイル
'binary' ('latin1') 生バイト 低レベルのバイナリファイル操作 画像およびアーカイブの処理
null バッファオブジェクトに戻る 生のバイトを操作する必要がある ファイルの検証、ストリーム処理

▶ 例:同じファイルを異なるエンコーディングで読み込む

JAVASCRIPT
const fs = require('fs/promises');

async function readEncodings() {
  await fs.writeFile('sample.txt', 'Hello The World', 'utf8');

  const utf8Data = await fs.readFile('sample.txt', 'utf8');
  console.log('UTF-8:', utf8Data);

  const base64Data = await fs.readFile('sample.txt', 'base64');
  console.log('Base64:', base64Data);

  const bufferData = await fs.readFile('sample.txt');
  console.log('Buffer:', bufferData);
  console.log('Buffer Hexadecimal:', bufferData.toString('hex'));
}

readEncodings();
TEXT
UTF-8: Hello The World
Base64: SGVsbG8g5LiW55WM
Buffer: <Buffer 48 65 6c 6c 6f 20 e4 b8 96 e7 95 8c>
Buffer Hexadecimal: 48656c6c6f20e4b896e7958c

8. 総合的な例:ファイル管理ツール

完全なファイル管理ワークフローを構築する:ディレクトリを作成 → 設定を書き込む → 読み込んで解析する → ログに追加する → 内容を一覧表示する。

JAVASCRIPT
const fs = require('fs/promises');
const path = require('path');

async function fileManager() {
  const dir = 'project-data';
  const configPath = path.join(dir, 'config.json');
  const logPath = path.join(dir, 'app.log');

  try {
    // Step 1: Create a Directory
    await fs.mkdir(dir, { recursive: true });
    console.log('✓ The table of contents has been created:', dir);

    // Step 2: Write to the configuration file
    const config = {
      appName: 'LogAnalyzer',
      version: '1.0.0',
      maxLines: 50000,
      encoding: 'utf8'
    };
    await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
    console.log('✓ The configuration file has been written.:', configPath);

    // Step 3: Read and Parse the Configuration
    const raw = await fs.readFile(configPath, 'utf8');
    const parsed = JSON.parse(raw);
    console.log('✓ Configuration loaded:', parsed.appName, 'v' + parsed.version);

    // Step 4: Add a log entry
    const timestamp = new Date().toISOString();
    await fs.appendFile(logPath, `[${timestamp}] Service started\n`, 'utf8');
    await fs.appendFile(logPath, `[${timestamp}] Config loaded: ${parsed.maxLines} lines\n`, 'utf8');
    console.log('✓ The log has been appended.:', logPath);

    // Step 5: List the contents of the table of contents
    const entries = await fs.readdir(dir, { withFileTypes: true });
    console.log('✓ Table of Contents:');
    for (const entry of entries) {
      const type = entry.isDirectory() ? '[DIR]' : '[FILE]';
      const stats = await fs.stat(path.join(dir, entry.name));
      console.log(`  ${type} ${entry.name} (${stats.size} bytes)`);
    }
  } catch (err) {
    console.error('✗ Operation Failed:', err.message);
  }
}

fileManager();
TEXT
✓ The table of contents has been created: project-data
✓ The configuration file has been written.: project-data/config.json
✓ Configuration loaded: LogAnalyzer v1.0.0
✓ The log has been appended.: project-data/app.log
✓ Table of Contents:
  [FILE] app.log (106 bytes)
  [FILE] config.json (98 bytes)

❓ よくある質問

Q 同期メソッドはいつ使用すべきですか?
A アプリケーションの起動時に、設定ファイルの読み込みなど、1回限りの操作にのみ使用してください。実行時には、イベントループをブロックしてしまうため、同期メソッドは絶対に使用しないでください。
Q なぜコールバックの最初のパラメータが err になっているのですか?
A これは Node.js の「エラー優先コールバック」という慣例によるものです。この慣例では、開発者はデータを処理する前にエラーの有無を確認することが求められており、これにより例外が見落とされるのを防ぐことができます。
Q fs.promisesfs の違いは何ですか?
A fs.promises(または require('fs/promises'))は同じ機能を提供しますが、Promise を返します。async/await はネストされたコールバックの代替として使用できます。fsはコールバックスタイルを採用しています。機能的には両者は同一です。
Q パスがファイルかディレクトリかを判断するにはどうすればよいですか?
A fs.stat(path) を使用して stats オブジェクトを取得し、stats.isFile() を呼び出してファイルかどうかを確認し、stats.isDirectory() を呼び出してディレクトリかどうかを確認します。
Q readFile はファイル全体をメモリに読み込みますか?
A はい、readFile はファイル全体をメモリに読み込みます。大容量のファイルを扱う場合は、メモリオーバーフローを防ぐために、fs.createReadStream を使用してストリーム単位で読み込むことをお勧めします。
Q fs.mkdirrecursive オプションの目的は何ですか?
A { recursive: true } を設定すると、mkdir -p と同様に、一度に複数の階層のネストされたディレクトリを作成できるようになります。また、ディレクトリがすでに存在していてもエラーは発生しません。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%