ファイルシステム操作において、ディレクトリの作成・削除・一覧取得は基本中の基本です。しかし、Node.jsには複数のAPIパターンがあり、再帰的な操作やシンボリックリンクの扱いなど、理解すべきポイントが多くあります。
本記事では、fs/promisesモジュールを使用したディレクトリ操作を体系的に解説します。mkdirによるディレクトリ作成、readdirによる一覧取得、stat/lstatによるファイル情報取得、そしてwatch/watchFileによるファイル監視まで、実践的なコード例を通じて習得できます。
実行環境#
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
前提条件#
- JavaScriptの基礎知識(関数、オブジェクト、async/await)
- Node.jsの基本操作(スクリプト実行、npm利用)経験
- fsモジュールの基本的な理解
fsモジュールの読み書き基礎についてはNode.js fsモジュール入門を参照してください。
ディレクトリ操作APIの全体像#
Node.jsのディレクトリ操作に関連する主要なAPIを整理します。
主要API一覧#
| API |
用途 |
備考 |
mkdir |
ディレクトリ作成 |
recursiveオプションで深い階層も一括作成 |
rmdir |
空ディレクトリ削除 |
非推奨、rmを推奨 |
rm |
ファイル・ディレクトリ削除 |
recursive + forceでrm -rf相当 |
readdir |
ディレクトリ一覧取得 |
withFileTypesでDirent取得可能 |
opendir |
Dirオブジェクト取得 |
大量エントリに効率的 |
stat |
ファイル情報取得 |
シンボリックリンクを辿る |
lstat |
ファイル情報取得 |
シンボリックリンク自体の情報 |
watch |
ファイル監視 |
OSのファイルシステムイベント利用 |
watchFile |
ファイル監視 |
ポーリングベース |
API選択のフローチャート#
flowchart TD
A[ディレクトリ操作したい] --> B{操作の種類は?}
B -->|作成| C[mkdir]
B -->|削除| D{空のディレクトリ?}
D -->|はい| E[rmdir]
D -->|いいえ| F["rm + recursive: true"]
B -->|一覧取得| G{エントリ数は?}
G -->|少ない| H[readdir]
G -->|多い| I[opendir]
B -->|情報取得| J{シンボリックリンク?}
J -->|辿る| K[stat]
J -->|辿らない| L[lstat]
B -->|監視| M{精度重視?}
M -->|効率重視| N[watch]
M -->|確実性重視| O[watchFile]mkdirでディレクトリを作成する#
mkdirは、新しいディレクトリを作成する関数です。recursiveオプションを使用すると、親ディレクトリも含めて一括で作成できます。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import { mkdir } from 'node:fs/promises';
async function createDirectory() {
try {
// 単一のディレクトリを作成
await mkdir('./output');
console.log('ディレクトリを作成しました');
} catch (error) {
if (error.code === 'EEXIST') {
console.log('ディレクトリは既に存在します');
} else {
throw error;
}
}
}
await createDirectory();
|
recursiveオプションで深い階層を作成#
recursive: trueを指定すると、親ディレクトリが存在しない場合でも、必要なディレクトリをすべて作成します。これはUnixのmkdir -pコマンドに相当します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { mkdir } from 'node:fs/promises';
import { join } from 'node:path';
async function createNestedDirectories() {
const projectPath = join(process.cwd(), 'project', 'src', 'components');
try {
// recursive: trueで親ディレクトリも作成
const createdPath = await mkdir(projectPath, { recursive: true });
if (createdPath) {
console.log(`作成されたディレクトリ: ${createdPath}`);
} else {
console.log('ディレクトリは既に存在していました');
}
} catch (error) {
console.error('ディレクトリ作成エラー:', error.message);
throw error;
}
}
await createNestedDirectories();
|
recursive: trueの場合、戻り値は最初に作成されたディレクトリのパスです。すべてのディレクトリが既に存在する場合はundefinedが返されます。
パーミッションの指定#
Unix系OSでは、modeオプションでパーミッションを指定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { mkdir } from 'node:fs/promises';
async function createDirectoryWithPermission() {
// 所有者のみ読み書き実行可能なディレクトリを作成
await mkdir('./secure-data', {
mode: 0o700,
recursive: true
});
console.log('制限付きディレクトリを作成しました');
}
await createDirectoryWithPermission();
|
Windowsではmodeオプションは無視されます。
mkdtempで一時ディレクトリを作成#
ユニークな一時ディレクトリが必要な場合は、mkdtempを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, sep } from 'node:path';
async function createTempDirectory() {
// OSの一時ディレクトリ内にユニークなディレクトリを作成
const prefix = join(tmpdir(), 'myapp-');
const tempDir = await mkdtemp(prefix);
console.log(`一時ディレクトリ: ${tempDir}`);
// 例: /tmp/myapp-abc123 または C:\Users\...\AppData\Local\Temp\myapp-abc123
// 使用後はクリーンアップ
await rm(tempDir, { recursive: true, force: true });
console.log('一時ディレクトリを削除しました');
}
await createTempDirectory();
|
rmdir/rmでディレクトリを削除する#
ディレクトリの削除には、rmdirとrmの2つの関数があります。現在はrmの使用が推奨されています。
rmdirで空のディレクトリを削除#
rmdirは空のディレクトリのみを削除できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { rmdir, mkdir } from 'node:fs/promises';
async function removeEmptyDirectory() {
// テスト用ディレクトリを作成
await mkdir('./temp-empty', { recursive: true });
try {
// 空のディレクトリを削除
await rmdir('./temp-empty');
console.log('ディレクトリを削除しました');
} catch (error) {
if (error.code === 'ENOTEMPTY') {
console.error('ディレクトリが空ではありません');
} else if (error.code === 'ENOENT') {
console.error('ディレクトリが存在しません');
} else {
throw error;
}
}
}
await removeEmptyDirectory();
|
rmでディレクトリとその中身を削除#
rmは、Unixのrmコマンドに相当する関数です。recursiveとforceオプションを組み合わせることで、rm -rfと同じ動作を実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { rm, mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
async function removeDirectoryRecursively() {
const testDir = './test-delete';
// テスト用のディレクトリ構造を作成
await mkdir(join(testDir, 'sub', 'deep'), { recursive: true });
await writeFile(join(testDir, 'file.txt'), 'test content');
await writeFile(join(testDir, 'sub', 'nested.txt'), 'nested content');
console.log('テスト用ディレクトリを作成しました');
// ディレクトリとその中身をすべて削除
await rm(testDir, {
recursive: true, // サブディレクトリも削除
force: true // 存在しなくてもエラーにしない
});
console.log('ディレクトリを再帰的に削除しました');
}
await removeDirectoryRecursively();
|
rmオプションの詳細#
| オプション |
デフォルト |
説明 |
force |
false |
パスが存在しなくてもエラーにしない |
recursive |
false |
再帰的に削除する |
maxRetries |
0 |
失敗時のリトライ回数 |
retryDelay |
100 |
リトライ間隔(ミリ秒) |
1
2
3
4
5
6
7
8
9
10
11
12
|
import { rm } from 'node:fs/promises';
async function removeWithRetry() {
await rm('./busy-directory', {
recursive: true,
force: true,
maxRetries: 3, // 最大3回リトライ
retryDelay: 200 // 200ミリ秒間隔
});
}
await removeWithRetry();
|
ファイルがロックされている場合など、削除に失敗することがあります。maxRetriesとretryDelayを指定することで、リトライ処理を自動化できます。
readdirでディレクトリ一覧を取得する#
readdirは、ディレクトリ内のファイルやサブディレクトリの一覧を取得する関数です。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { readdir } from 'node:fs/promises';
async function listDirectory() {
try {
// ファイル名の配列を取得
const files = await readdir('./');
console.log('ディレクトリ内のファイル:');
files.forEach(file => console.log(` ${file}`));
return files;
} catch (error) {
if (error.code === 'ENOENT') {
console.error('ディレクトリが存在しません');
} else if (error.code === 'ENOTDIR') {
console.error('指定されたパスはディレクトリではありません');
} else {
throw error;
}
}
}
await listDirectory();
|
withFileTypesでファイル種別を取得#
withFileTypes: trueを指定すると、ファイル名の代わりにDirentオブジェクトの配列が返されます。これにより、追加のstat呼び出しなしにファイル種別を判定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
import { readdir } from 'node:fs/promises';
async function listDirectoryWithTypes() {
// Direntオブジェクトの配列を取得
const entries = await readdir('./', { withFileTypes: true });
const directories = [];
const files = [];
const symlinks = [];
for (const entry of entries) {
if (entry.isDirectory()) {
directories.push(entry.name);
} else if (entry.isFile()) {
files.push(entry.name);
} else if (entry.isSymbolicLink()) {
symlinks.push(entry.name);
}
}
console.log('ディレクトリ:', directories);
console.log('ファイル:', files);
console.log('シンボリックリンク:', symlinks);
return { directories, files, symlinks };
}
await listDirectoryWithTypes();
|
Direntオブジェクトのメソッド#
| メソッド |
説明 |
isFile() |
通常のファイルか |
isDirectory() |
ディレクトリか |
isSymbolicLink() |
シンボリックリンクか |
isBlockDevice() |
ブロックデバイスか |
isCharacterDevice() |
キャラクターデバイスか |
isFIFO() |
FIFOパイプか |
isSocket() |
ソケットか |
recursiveオプションで再帰的に取得#
Node.js 18.17.0以降では、recursive: trueオプションで再帰的にファイル一覧を取得できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { readdir } from 'node:fs/promises';
async function listAllFiles() {
// 再帰的にすべてのファイル・ディレクトリを取得
const allEntries = await readdir('./', {
recursive: true,
withFileTypes: true
});
console.log(`合計 ${allEntries.length} エントリ`);
// ファイルのみをフィルタ
const files = allEntries
.filter(entry => entry.isFile())
.map(entry => `${entry.parentPath}/${entry.name}`);
console.log('すべてのファイル:');
files.forEach(file => console.log(` ${file}`));
return files;
}
await listAllFiles();
|
opendirで大量エントリを効率的に処理#
大量のファイルがあるディレクトリでは、opendirを使用してイテレータとして処理するほうがメモリ効率が良くなります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { opendir } from 'node:fs/promises';
async function processLargeDirectory() {
const dir = await opendir('./large-directory');
let count = 0;
// for-await-ofで順次処理(メモリ効率が良い)
for await (const entry of dir) {
count++;
if (entry.isFile()) {
// ファイルに対する処理
console.log(`処理中: ${entry.name}`);
}
// 100件ごとに進捗表示
if (count % 100 === 0) {
console.log(`${count} エントリを処理しました`);
}
}
console.log(`合計 ${count} エントリを処理しました`);
}
await processLargeDirectory();
|
stat/lstatでファイル情報を取得する#
statとlstatは、ファイルやディレクトリの詳細情報を取得する関数です。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { stat } from 'node:fs/promises';
async function getFileInfo(path) {
try {
const stats = await stat(path);
console.log('ファイル情報:');
console.log(` タイプ: ${stats.isFile() ? 'ファイル' : 'ディレクトリ'}`);
console.log(` サイズ: ${stats.size} bytes`);
console.log(` 作成日時: ${stats.birthtime}`);
console.log(` 最終更新: ${stats.mtime}`);
console.log(` 最終アクセス: ${stats.atime}`);
return stats;
} catch (error) {
if (error.code === 'ENOENT') {
console.error('ファイルが存在しません:', path);
} else {
throw error;
}
}
}
await getFileInfo('./package.json');
|
Statsオブジェクトのプロパティ#
| プロパティ |
説明 |
size |
ファイルサイズ(バイト) |
mtime |
最終更新日時(Date) |
atime |
最終アクセス日時(Date) |
ctime |
ステータス変更日時(Date) |
birthtime |
作成日時(Date) |
mode |
パーミッションビット |
uid |
所有者のユーザーID |
gid |
所有者のグループID |
statとlstatの違い#
statはシンボリックリンクを辿ってリンク先の情報を取得しますが、lstatはシンボリックリンク自体の情報を取得します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import { stat, lstat, symlink, unlink, writeFile } from 'node:fs/promises';
async function compareStatAndLstat() {
// テスト用ファイルとシンボリックリンクを作成
await writeFile('./original.txt', 'Hello, World!');
try {
await symlink('./original.txt', './link.txt');
} catch (error) {
if (error.code !== 'EEXIST') throw error;
}
// statはリンク先のファイル情報を取得
const statResult = await stat('./link.txt');
console.log('stat結果:');
console.log(` isFile: ${statResult.isFile()}`); // true
console.log(` isSymbolicLink: ${statResult.isSymbolicLink()}`); // false
console.log(` size: ${statResult.size}`); // 13 (ファイル内容のサイズ)
// lstatはシンボリックリンク自体の情報を取得
const lstatResult = await lstat('./link.txt');
console.log('lstat結果:');
console.log(` isFile: ${lstatResult.isFile()}`); // false
console.log(` isSymbolicLink: ${lstatResult.isSymbolicLink()}`); // true
console.log(` size: ${lstatResult.size}`); // リンクのサイズ
// クリーンアップ
await unlink('./link.txt');
await unlink('./original.txt');
}
await compareStatAndLstat();
|
ファイルの存在確認#
statのエラーハンドリングでファイルの存在を確認できますが、公式には推奨されていません。ファイル操作を直接行い、エラーをハンドリングするほうが確実です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import { stat, access, constants } from 'node:fs/promises';
// 非推奨: statでの存在確認
async function existsWithStat(path) {
try {
await stat(path);
return true;
} catch (error) {
if (error.code === 'ENOENT') {
return false;
}
throw error;
}
}
// 推奨: accessでアクセス可能性を確認
async function isAccessible(path) {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
// より推奨: 直接操作してエラーハンドリング
import { readFile } from 'node:fs/promises';
async function readIfExists(path) {
try {
const content = await readFile(path, 'utf8');
return content;
} catch (error) {
if (error.code === 'ENOENT') {
return null; // ファイルが存在しない
}
throw error; // その他のエラーは再スロー
}
}
|
ディレクトリ統計情報の収集#
再帰的にディレクトリを走査し、統計情報を収集する実践的な例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
async function getDirectoryStats(dirPath) {
const stats = {
totalFiles: 0,
totalDirectories: 0,
totalSize: 0,
fileTypes: {}
};
async function processDirectory(currentPath) {
const entries = await readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentPath, entry.name);
if (entry.isDirectory()) {
stats.totalDirectories++;
await processDirectory(fullPath);
} else if (entry.isFile()) {
stats.totalFiles++;
const fileStats = await stat(fullPath);
stats.totalSize += fileStats.size;
// 拡張子ごとにカウント
const ext = entry.name.includes('.')
? entry.name.split('.').pop().toLowerCase()
: 'no-extension';
stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1;
}
}
}
await processDirectory(dirPath);
return stats;
}
// 使用例
const projectStats = await getDirectoryStats('./src');
console.log('ディレクトリ統計:');
console.log(` ファイル数: ${projectStats.totalFiles}`);
console.log(` ディレクトリ数: ${projectStats.totalDirectories}`);
console.log(` 合計サイズ: ${(projectStats.totalSize / 1024).toFixed(2)} KB`);
console.log(' ファイルタイプ:', projectStats.fileTypes);
|
watch/watchFileでファイルを監視する#
Node.jsには、ファイルやディレクトリの変更を監視する2つの方法があります。
watchとwatchFileの比較#
| 特徴 |
watch |
watchFile |
| 検出方式 |
OSのファイルシステムイベント |
ポーリング |
| 効率 |
高い |
低い(定期的にstatを実行) |
| 信頼性 |
プラットフォーム依存 |
安定している |
| リソース使用 |
少ない |
多い(CPU使用率が高い) |
| 推奨度 |
推奨 |
watchが使えない場合のみ |
watchの基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import { watch } from 'node:fs/promises';
async function watchDirectory() {
const ac = new AbortController();
const { signal } = ac;
// 10秒後に監視を停止
setTimeout(() => {
console.log('監視を停止します');
ac.abort();
}, 10000);
try {
const watcher = watch('./watched-folder', { signal });
console.log('ファイル監視を開始しました...');
for await (const event of watcher) {
console.log(`イベント: ${event.eventType}, ファイル: ${event.filename}`);
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('監視が正常に停止しました');
} else {
throw error;
}
}
}
await watchDirectory();
|
recursiveオプションでサブディレクトリも監視#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { watch } from 'node:fs/promises';
async function watchRecursively() {
const ac = new AbortController();
const { signal } = ac;
const watcher = watch('./project', {
signal,
recursive: true // サブディレクトリも監視
});
console.log('再帰的なファイル監視を開始...');
for await (const event of watcher) {
const { eventType, filename } = event;
// node_modulesは無視
if (filename && filename.includes('node_modules')) {
continue;
}
console.log(`[${eventType}] ${filename}`);
}
}
await watchRecursively();
|
Callback APIでのwatch使用#
fs/promisesのwatchはAsyncIteratorを返しますが、従来のCallback APIも使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import { watch } from 'node:fs';
function watchWithCallback() {
const watcher = watch('./watched-folder', (eventType, filename) => {
console.log(`[${eventType}] ${filename}`);
});
// changeイベントでも監視可能
watcher.on('change', (eventType, filename) => {
console.log(`変更検出: ${eventType} - ${filename}`);
});
watcher.on('error', (error) => {
console.error('監視エラー:', error);
});
watcher.on('close', () => {
console.log('監視を終了しました');
});
// 10秒後に停止
setTimeout(() => {
watcher.close();
}, 10000);
}
watchWithCallback();
|
watchFileでポーリング監視#
watchFileは、定期的にstatを実行してファイルの変更を検出します。watchが動作しない環境(NFSなど)で使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { watchFile, unwatchFile } from 'node:fs';
function watchFileWithPolling() {
const filename = './config.json';
watchFile(filename, { interval: 1000 }, (curr, prev) => {
// mtimeを比較して変更を検出
if (curr.mtimeMs !== prev.mtimeMs) {
console.log('ファイルが変更されました');
console.log(` 前回更新: ${prev.mtime}`);
console.log(` 今回更新: ${curr.mtime}`);
}
});
console.log(`${filename} の監視を開始しました(ポーリング間隔: 1秒)`);
// 監視を停止する場合
setTimeout(() => {
unwatchFile(filename);
console.log('監視を停止しました');
}, 30000);
}
watchFileWithPolling();
|
実践的なファイル監視ユーティリティ#
開発時に使用できる、実践的なファイル監視ユーティリティを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
import { watch } from 'node:fs/promises';
import { stat } from 'node:fs/promises';
import { join, extname } from 'node:path';
class FileWatcher {
constructor(options = {}) {
this.watchPath = options.path || './';
this.extensions = options.extensions || ['.js', '.ts', '.json'];
this.ignorePatterns = options.ignore || ['node_modules', '.git', 'dist'];
this.debounceMs = options.debounce || 100;
this.onChange = options.onChange || (() => {});
this.controller = new AbortController();
this.lastEvent = {};
}
shouldIgnore(filename) {
if (!filename) return true;
// 無視パターンをチェック
for (const pattern of this.ignorePatterns) {
if (filename.includes(pattern)) {
return true;
}
}
// 拡張子をチェック
const ext = extname(filename).toLowerCase();
if (this.extensions.length > 0 && !this.extensions.includes(ext)) {
return true;
}
return false;
}
debounce(filename) {
const now = Date.now();
const lastTime = this.lastEvent[filename] || 0;
if (now - lastTime < this.debounceMs) {
return true; // デバウンス中
}
this.lastEvent[filename] = now;
return false;
}
async start() {
const { signal } = this.controller;
try {
const watcher = watch(this.watchPath, {
signal,
recursive: true
});
console.log(`ファイル監視を開始: ${this.watchPath}`);
console.log(` 対象拡張子: ${this.extensions.join(', ')}`);
console.log(` 無視パターン: ${this.ignorePatterns.join(', ')}`);
for await (const event of watcher) {
const { eventType, filename } = event;
if (this.shouldIgnore(filename)) continue;
if (this.debounce(filename)) continue;
console.log(`[${eventType}] ${filename}`);
// コールバックを実行
await this.onChange({
type: eventType,
filename,
path: join(this.watchPath, filename)
});
}
} catch (error) {
if (error.name !== 'AbortError') {
throw error;
}
}
}
stop() {
this.controller.abort();
console.log('ファイル監視を停止しました');
}
}
// 使用例
const watcher = new FileWatcher({
path: './src',
extensions: ['.js', '.ts'],
ignore: ['node_modules', '.git', '__tests__'],
debounce: 200,
onChange: async (event) => {
console.log(`変更を検出: ${event.path}`);
// ここでビルドやテストを実行
}
});
await watcher.start();
|
watchのプラットフォーム依存性#
fs.watchはOSのファイルシステムイベントを使用するため、プラットフォームによって動作が異なります。
プラットフォーム別の実装#
| プラットフォーム |
使用技術 |
備考 |
| Linux |
inotify |
信頼性が高い |
| macOS |
kqueue(ファイル)、FSEvents(ディレクトリ) |
信頼性が高い |
| Windows |
ReadDirectoryChangesW |
ディレクトリ移動で問題あり |
注意点#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { watch } from 'node:fs';
function demonstrateCaveats() {
// 1. ファイル名がnullになることがある
watch('./dir', (eventType, filename) => {
if (filename === null) {
console.log('ファイル名を取得できませんでした');
return;
}
console.log(`${eventType}: ${filename}`);
});
// 2. 同じ変更で複数のイベントが発生することがある
// → デバウンス処理が必要
// 3. ネットワークファイルシステム(NFS、SMB)では動作しないことがある
// → watchFileを使用するか、chokidarなどのライブラリを検討
}
|
より信頼性の高い監視が必要な場合#
本番環境や複雑な監視要件には、chokidarなどのサードパーティライブラリの使用を検討してください。
1
2
3
4
5
6
7
8
9
10
11
12
|
import chokidar from 'chokidar';
// chokidarはクロスプラットフォームで信頼性が高い
const watcher = chokidar.watch('./src', {
ignored: /node_modules/,
persistent: true
});
watcher
.on('add', path => console.log(`追加: ${path}`))
.on('change', path => console.log(`変更: ${path}`))
.on('unlink', path => console.log(`削除: ${path}`));
|
実践的なディレクトリ操作ユーティリティ#
これまで学んだAPIを組み合わせて、実践的なユーティリティを実装します。
ディレクトリのコピー#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { readdir, stat, mkdir, copyFile } from 'node:fs/promises';
import { join } from 'node:path';
async function copyDirectory(src, dest) {
// 送信先ディレクトリを作成
await mkdir(dest, { recursive: true });
const entries = await readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
// 再帰的にコピー
await copyDirectory(srcPath, destPath);
} else if (entry.isFile()) {
await copyFile(srcPath, destPath);
}
// シンボリックリンクは必要に応じて処理
}
}
// 使用例
await copyDirectory('./src', './backup/src');
console.log('ディレクトリをコピーしました');
|
条件に一致するファイルを検索#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import { readdir, stat } from 'node:fs/promises';
import { join, extname } from 'node:path';
async function findFiles(dir, predicate) {
const results = [];
async function search(currentDir) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await search(fullPath);
} else if (entry.isFile()) {
const stats = await stat(fullPath);
if (await predicate(fullPath, stats, entry)) {
results.push(fullPath);
}
}
}
}
await search(dir);
return results;
}
// 使用例: 1MB以上のJavaScriptファイルを検索
const largeJsFiles = await findFiles('./src', async (path, stats) => {
return extname(path) === '.js' && stats.size > 1024 * 1024;
});
console.log('1MB以上のJSファイル:', largeJsFiles);
// 使用例: 30日以上更新されていないファイルを検索
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
const oldFiles = await findFiles('./logs', async (path, stats) => {
return stats.mtimeMs < thirtyDaysAgo;
});
console.log('30日以上更新されていないファイル:', oldFiles);
|
ディレクトリのクリーンアップ#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
import { readdir, stat, rm } from 'node:fs/promises';
import { join } from 'node:path';
async function cleanupOldFiles(dir, maxAgeDays) {
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const now = Date.now();
let deletedCount = 0;
let deletedSize = 0;
async function cleanup(currentDir) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await cleanup(fullPath);
// 空のディレクトリを削除
const remaining = await readdir(fullPath);
if (remaining.length === 0) {
await rm(fullPath, { recursive: true });
}
} else if (entry.isFile()) {
const stats = await stat(fullPath);
if (now - stats.mtimeMs > maxAgeMs) {
await rm(fullPath);
deletedCount++;
deletedSize += stats.size;
}
}
}
}
await cleanup(dir);
return {
deletedCount,
deletedSize,
deletedSizeFormatted: `${(deletedSize / 1024 / 1024).toFixed(2)} MB`
};
}
// 使用例: 7日以上古いログファイルを削除
const result = await cleanupOldFiles('./logs', 7);
console.log(`削除されたファイル: ${result.deletedCount}`);
console.log(`削除されたサイズ: ${result.deletedSizeFormatted}`);
|
まとめ#
Node.jsのディレクトリ操作について、以下のポイントを解説しました。
mkdirのrecursiveオプションで深い階層のディレクトリを一括作成
rmのrecursiveとforceオプションでrm -rf相当の削除が可能
readdirのwithFileTypesオプションでファイル種別を効率的に取得
statとlstatの違いはシンボリックリンクの扱い
watchはOS依存のファイルシステムイベントを使用し効率的
watchFileはポーリングベースで確実だがリソース消費が多い
ディレクトリ操作は、ビルドツール、CLIツール、ログ管理など、多くの場面で必要となります。fs/promisesのAPIを使いこなすことで、堅牢なファイルシステム操作を実装できるようになります。
次のステップとして、Node.jsのpathモジュール完全ガイドでクロスプラットフォームなパス操作を学ぶことで、より堅牢なファイルシステム操作が可能になります。
参考リンク#