「FSの基礎:ファイルシステムの読み取り、書き込み、およびディレクトリ操作」
チャーリーは、会社のログ分析プラットフォームの保守を担当するバックエンドエンジニアです。毎日明け方、システムは約50,000行のサーバーログファイルを処理する必要があります。当初、彼はfs.readFileSyncを使用してログを1行ずつ読み込んでいましたが、読み込み処理中にプログラム全体が完全にフリーズし、他のすべてのリクエストがタイムアウトしてしまいました。fs.readFile を使った非同期読み取りに切り替えたところ、プログラムはディスク I/O を待機している間も他のリクエストに応答できるようになり、全体的なスループットが10倍に向上しました。この経験を通じて、彼は Node.js のファイルシステム API における同期処理と非同期処理の違いを深く理解するようになりました。
1. 学習内容
fs.readFile/fs.writeFile/fs.appendFile/fs.unlinkを使用して、ファイル操作を実行しますfs.mkdir/fs.readdir/fs.stat/fs.existsSyncを使用して、ディレクトリ操作を実行します- 同期メソッドと非同期メソッドの実行における違いを区別する
- 「エラーファースト・コールバック」
(err, data)モデルの理解 fs.promisesAPI を使用して、Promise を用いたファイル操作を実行する- 適切なファイルエンコーディング(utf8 / base64 / binary)を選択してください
2. fs モジュールの概要
fs モジュールは、ファイルシステム操作のための Node.js 組み込みモジュールであり、ファイルの読み書き、ディレクトリ管理、権限チェックなどの機能を提供します。各操作には通常、同期、非同期コールバック、非同期 Promise の 3 つのスタイルが用意されています。
| 機能 | 同期方式 | 非同期コールバック方式 | fs.promises メソッド |
|---|---|---|---|
| 命名機能 | xxxSync 接尾辞 |
接尾辞なし | fs.promises.xxx |
| 戻り値 | 結果を直接返す | コールバックを介して取得した undefined |
Promise を返す |
| イベントループをブロックする | はい | いいえ | いいえ |
| エラー処理 | try/catch |
コールバックの最初の引数 | .catch() / try-catch |
| おすすめのユースケース | 起動時の設定読み込み | 下位互換性 | 新規プロジェクトに最適 |
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を介して結果を通知します。
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
▶ 例:同期読み取りはプログラム全体をブロックする
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.');
Start Reading...
Reading complete,Number of lines:50000
This line must wait until the data has finished loading before it is executed.
▶ 例:イベントループをブロックしない非同期読み込み
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');
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) |
パス | ファイルを同時に削除 |
▶ 例:ファイルへの書き込みと追記
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);
});
});
});
Write complete
Addition Complete
Document Content:
Content on the first line
The second line added
▶ 例:fs.promises を使用してネストされたコールバックを回避する
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();
▶ 例:ファイルの削除
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) |
パス | パスが存在するか確認 |
▶ 例:ディレクトリの作成とその内容の表示
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();
The directory was successfully created
Table of Contents: [ 'app.log', 'error.log' ]
▶ 例:ファイルかディレクトリかを判別する
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();
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つのエラー処理手法の比較
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();
Callback Method - Error: ENOENT
PromiseMethod - Error: ENOENT
7. ファイルのエンコーディング
| コード | 説明 | 適用されるシナリオ | 例 |
|---|---|---|---|
'utf8' |
UTF-8 テキストエンコーディング(デフォルト) | テキストファイルの読み書き | ログ、設定、JSON |
'base64' |
Base64エンコーディング | 画像転送、バイナリからテキストへの変換 | 埋め込み画像、メールの添付ファイル |
'binary' ('latin1') |
生バイト | 低レベルのバイナリファイル操作 | 画像およびアーカイブの処理 |
null |
バッファオブジェクトに戻る | 生のバイトを操作する必要がある | ファイルの検証、ストリーム処理 |
▶ 例:同じファイルを異なるエンコーディングで読み込む
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();
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. 総合的な例:ファイル管理ツール
完全なファイル管理ワークフローを構築する:ディレクトリを作成 → 設定を書き込む → 読み込んで解析する → ログに追加する → 内容を一覧表示する。
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();
✓ 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)
❓ よくある質問
err になっているのですか?fs.promises と fs の違いは何ですか?fs.promises(または require('fs/promises'))は同じ機能を提供しますが、Promise を返します。async/await はネストされたコールバックの代替として使用できます。fsはコールバックスタイルを採用しています。機能的には両者は同一です。fs.stat(path) を使用して stats オブジェクトを取得し、stats.isFile() を呼び出してファイルかどうかを確認し、stats.isDirectory() を呼び出してディレクトリかどうかを確認します。readFile はファイル全体をメモリに読み込みますか?readFile はファイル全体をメモリに読み込みます。大容量のファイルを扱う場合は、メモリオーバーフローを防ぐために、fs.createReadStream を使用してストリーム単位で読み込むことをお勧めします。fs.mkdir の recursive オプションの目的は何ですか?{ recursive: true } を設定すると、mkdir -p と同様に、一度に複数の階層のネストされたディレクトリを作成できるようになります。また、ディレクトリがすでに存在していてもエラーは発生しません。📖 まとめ
- 重要な概念とその活用方法
- fs モジュールの主要な概念と使用法の概要
- 同期実行シーケンスと非同期実行シーケンスの基本概念と使用法
- ファイルの読み取りおよび書き込み操作の基本概念と使用方法
- ディレクトリ操作の基本概念と使用方法
- エラーファーストのコールバックとプロミスの主要な概念と使い方
- ファイルエンコーディングの基本概念と使用方法
- 包括的な例:ファイル管理ツールの基本概念と使い方
📝 練習問題
- このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
- 包括的な例を修正し、独自の拡張機能を追加する
- 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
- 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
- このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。



