Node.js ファイル読み書き完全解説:fs モジュールの使い方

Node.js でのアプリケーション開発において、ファイルやディレクトリの操作は避けて通れない重要な機能です。設定ファイルの読み込み、ログの出力、データの永続化など、あらゆる場面でファイルシステムとの連携が必要になります。
多くの開発者が「ファイル操作は難しそう」「エラーハンドリングが複雑」と感じられるかもしれませんが、Node.js の fs(File System)モジュール を理解することで、これらの操作を安全かつ効率的に実行できるようになります。
この記事では、fs モジュールの基本的な使い方から、実際のプロジェクトで活用できる高度なテクニックまで、ファイル操作に関する包括的な知識をお届けいたします。適切なファイル操作の実装により、より堅牢で信頼性の高いアプリケーションの構築が可能になります。
fs モジュールとは何か
fs モジュールは、Node.js に標準で組み込まれているファイルシステム操作用のモジュールです。ファイルの読み書きからディレクトリの管理まで、ファイルシステムに関するあらゆる操作を提供しています。
ファイルシステム操作の基本概念
現代の Web アプリケーションでは、以下のようなファイル操作が頻繁に必要になります:
主要なファイル操作の用途
# | 操作種別 | 具体例 | 使用場面 |
---|---|---|---|
1 | 読み込み | 設定ファイル、テンプレート | アプリ起動時、動的コンテンツ生成 |
2 | 書き込み | ログファイル、レポート出力 | エラー記録、データエクスポート |
3 | ディレクトリ管理 | アップロード先作成、一時ファイル整理 | ファイル管理システム |
4 | 監視 | 設定変更検知、ホットリロード | 開発環境、動的設定更新 |
fs モジュールの特徴
javascript// fs モジュールの基本的な読み込み
const fs = require('fs');
const fsPromises = require('fs').promises;
// または ESM での読み込み
import fs from 'fs';
import { promises as fsPromises } from 'fs';
fs モジュールには、同じ機能に対して複数のバリエーションが提供されています:
- 同期版 (
Sync
接尾辞):処理が完了するまでブロック - 非同期コールバック版:従来のコールバック形式
- Promise 版 (
fs.promises
):async/await で使用可能
同期処理と非同期処理の違い
ファイル操作における同期と非同期の選択は、アプリケーションのパフォーマンスに大きく影響します。
同期処理の特徴
javascriptconst fs = require('fs');
console.log('処理開始');
try {
// 同期的ファイル読み込み(ブロッキング)
const data = fs.readFileSync('./config.json', 'utf8');
console.log('ファイル読み込み完了');
const config = JSON.parse(data);
console.log('設定:', config);
} catch (error) {
console.error('エラー:', error.message);
}
console.log('処理終了');
このコードでは、readFileSync
が完了するまで次の行は実行されません。
非同期処理(コールバック版)
javascriptconst fs = require('fs');
console.log('処理開始');
fs.readFile('./config.json', 'utf8', (error, data) => {
if (error) {
console.error('エラー:', error.message);
return;
}
console.log('ファイル読み込み完了');
try {
const config = JSON.parse(data);
console.log('設定:', config);
} catch (parseError) {
console.error('JSON パースエラー:', parseError.message);
}
});
console.log('処理終了'); // ファイル読み込み前に実行される
非同期処理(Promise 版)
javascriptconst { promises: fs } = require('fs');
async function loadConfig() {
console.log('処理開始');
try {
const data = await fs.readFile('./config.json', 'utf8');
console.log('ファイル読み込み完了');
const config = JSON.parse(data);
console.log('設定:', config);
return config;
} catch (error) {
console.error('エラー:', error.message);
throw error;
} finally {
console.log('処理終了');
}
}
loadConfig();
パフォーマンス比較
# | 処理方式 | メリット | デメリット | 適用場面 |
---|---|---|---|---|
1 | 同期 | シンプルな制御フロー | ブロッキング、スケーラビリティ問題 | 初期化処理、CLI ツール |
2 | 非同期コールバック | ノンブロッキング | コールバック地獄 | レガシーコード |
3 | 非同期 Promise | モダンな構文、エラーハンドリング | やや複雑 | Web アプリケーション |
基本的なファイル読み込み操作
ファイル読み込みは、fs モジュールの最も基本的で重要な機能です。設定ファイル、テンプレート、データファイルなど、様々な場面で活用されます。
readFile と readFileSync の使い方
基本的な readFile の使用例
javascriptconst fs = require('fs');
const path = require('path');
// テキストファイルの読み込み
function readTextFile() {
const filePath = path.join(
__dirname,
'data',
'sample.txt'
);
fs.readFile(filePath, 'utf8', (error, data) => {
if (error) {
if (error.code === 'ENOENT') {
console.error(
'ファイルが見つかりません:',
filePath
);
} else if (error.code === 'EACCES') {
console.error(
'ファイルへのアクセス権限がありません:',
filePath
);
} else {
console.error(
'ファイル読み込みエラー:',
error.message
);
}
return;
}
console.log('ファイル内容:');
console.log(data);
});
}
readTextFile();
Promise ベースの読み込み
javascriptconst { promises: fs } = require('fs');
const path = require('path');
async function readTextFileAsync() {
const filePath = path.join(
__dirname,
'data',
'sample.txt'
);
try {
const data = await fs.readFile(filePath, 'utf8');
console.log('ファイル内容:');
console.log(data);
return data;
} catch (error) {
console.error('ファイル読み込みエラー:', error.message);
throw error;
}
}
// 使用例
readTextFileAsync()
.then((content) => {
console.log('読み込み成功');
})
.catch((error) => {
console.error('処理失敗:', error.message);
});
複数ファイルの並行読み込み
javascriptconst { promises: fs } = require('fs');
const path = require('path');
async function readMultipleFiles() {
const files = [
'config.json',
'package.json',
'README.md',
];
try {
// 並行して複数ファイルを読み込み
const results = await Promise.all(
files.map(async (fileName) => {
const filePath = path.join(__dirname, fileName);
try {
const content = await fs.readFile(
filePath,
'utf8'
);
return {
fileName,
success: true,
content,
size: content.length,
};
} catch (error) {
return {
fileName,
success: false,
error: error.message,
};
}
})
);
// 結果の処理
results.forEach((result) => {
if (result.success) {
console.log(
`✅ ${result.fileName}: ${result.size} 文字`
);
} else {
console.log(
`❌ ${result.fileName}: ${result.error}`
);
}
});
return results;
} catch (error) {
console.error('並行読み込みエラー:', error.message);
throw error;
}
}
readMultipleFiles();
エンコーディングとバッファの理解
ファイル読み込み時のエンコーディング指定は、正しいデータ処理のために重要です。
エンコーディングの指定
javascriptconst { promises: fs } = require('fs');
async function demonstrateEncodings() {
const filePath = './data/japanese-text.txt';
try {
// UTF-8 として読み込み(推奨)
const utf8Content = await fs.readFile(filePath, 'utf8');
console.log('UTF-8:', utf8Content);
// バッファとして読み込み(エンコーディング指定なし)
const buffer = await fs.readFile(filePath);
console.log('バッファサイズ:', buffer.length, 'bytes');
console.log(
'バッファ(最初の10バイト):',
buffer.slice(0, 10)
);
// バッファを手動で UTF-8 に変換
const manualUtf8 = buffer.toString('utf8');
console.log('手動変換:', manualUtf8);
// 他のエンコーディング例
const base64Content = buffer.toString('base64');
console.log(
'Base64:',
base64Content.slice(0, 50) + '...'
);
} catch (error) {
console.error(
'エンコーディング処理エラー:',
error.message
);
}
}
demonstrateEncodings();
バイナリファイルの処理
javascriptconst { promises: fs } = require('fs');
const path = require('path');
async function readBinaryFile() {
const imagePath = path.join(
__dirname,
'images',
'logo.png'
);
try {
// バイナリファイルはエンコーディングを指定しない
const buffer = await fs.readFile(imagePath);
console.log('ファイル情報:');
console.log('- サイズ:', buffer.length, 'bytes');
console.log('- 最初の4バイト:', buffer.slice(0, 4));
// PNG ファイルの署名確認
const pngSignature = Buffer.from([
0x89, 0x50, 0x4e, 0x47,
]);
if (buffer.slice(0, 4).equals(pngSignature)) {
console.log('✅ 有効な PNG ファイルです');
} else {
console.log('❌ PNG ファイルではありません');
}
// ファイルの一部を Base64 として出力
const base64Preview = buffer
.slice(0, 100)
.toString('base64');
console.log('Base64 プレビュー:', base64Preview);
return buffer;
} catch (error) {
console.error(
'バイナリファイル読み込みエラー:',
error.message
);
throw error;
}
}
readBinaryFile();
エラーハンドリングの基本
適切なエラーハンドリングは、堅牢なアプリケーション開発に不可欠です。
詳細なエラーハンドリング
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class FileReader {
static async readFileWithRetry(filePath, options = {}) {
const {
encoding = 'utf8',
maxRetries = 3,
retryDelay = 1000,
} = options;
let lastError;
for (
let attempt = 1;
attempt <= maxRetries;
attempt++
) {
try {
console.log(
`📖 ファイル読み込み試行 ${attempt}/${maxRetries}: ${filePath}`
);
const data = await fs.readFile(filePath, encoding);
console.log(`✅ 読み込み成功 (${attempt}回目)`);
return data;
} catch (error) {
lastError = error;
console.log(
`❌ 試行 ${attempt} 失敗:`,
error.message
);
// エラータイプ別の処理
switch (error.code) {
case 'ENOENT':
console.log(
'ファイルまたはディレクトリが存在しません'
);
// ファイルが存在しない場合はリトライしない
throw new Error(
`ファイルが見つかりません: ${filePath}`
);
case 'EACCES':
console.log(
'ファイルへのアクセス権限がありません'
);
throw new Error(`アクセス拒否: ${filePath}`);
case 'EISDIR':
console.log('指定されたパスはディレクトリです');
throw new Error(
`ディレクトリが指定されました: ${filePath}`
);
case 'EMFILE':
case 'ENFILE':
console.log(
'ファイルハンドルが不足しています(リトライします)'
);
break;
default:
console.log('予期しないエラー:', error.code);
}
// 最後の試行でない場合は待機
if (attempt < maxRetries) {
console.log(
`⏳ ${retryDelay}ms 後にリトライします...`
);
await new Promise((resolve) =>
setTimeout(resolve, retryDelay)
);
}
}
}
throw new Error(
`ファイル読み込みに失敗しました (${maxRetries}回試行): ${lastError.message}`
);
}
static async readJSONFile(filePath) {
try {
const content = await this.readFileWithRetry(
filePath,
{ encoding: 'utf8' }
);
// JSON パースのエラーハンドリング
try {
const data = JSON.parse(content);
console.log('✅ JSON パース成功');
return data;
} catch (parseError) {
throw new Error(
`JSON パースエラー: ${parseError.message}`
);
}
} catch (error) {
console.error(
'JSON ファイル読み込みエラー:',
error.message
);
throw error;
}
}
}
// 使用例
async function demonstrateErrorHandling() {
try {
// 存在するファイルの読み込み
const config = await FileReader.readJSONFile(
'./package.json'
);
console.log(
'設定ファイル読み込み成功:',
Object.keys(config)
);
// 存在しないファイルの読み込み(エラーテスト)
await FileReader.readFileWithRetry('./nonexistent.txt');
} catch (error) {
console.error('最終エラー:', error.message);
}
}
demonstrateErrorHandling();
設定ファイル読み込みの実践例
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class ConfigManager {
constructor(configDir = './config') {
this.configDir = configDir;
this.cache = new Map();
}
async loadConfig(environment = 'development') {
const cacheKey = `config-${environment}`;
// キャッシュから取得
if (this.cache.has(cacheKey)) {
console.log('📦 キャッシュから設定を取得');
return this.cache.get(cacheKey);
}
try {
// 基本設定の読み込み
const baseConfig = await this.readConfigFile(
'base.json'
);
// 環境別設定の読み込み(オプション)
let envConfig = {};
try {
envConfig = await this.readConfigFile(
`${environment}.json`
);
console.log(
`📋 環境別設定 (${environment}) を読み込みました`
);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
console.log(
`⚠️ 環境別設定 (${environment}) が見つかりません(基本設定のみ使用)`
);
}
// 設定のマージ
const mergedConfig = {
...baseConfig,
...envConfig,
environment,
loadedAt: new Date().toISOString(),
};
// キャッシュに保存
this.cache.set(cacheKey, mergedConfig);
console.log('✅ 設定読み込み完了');
return mergedConfig;
} catch (error) {
console.error('設定読み込みエラー:', error.message);
throw error;
}
}
async readConfigFile(fileName) {
const filePath = path.join(this.configDir, fileName);
try {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
throw Object.assign(
new Error(
`設定ファイルが見つかりません: ${fileName}`
),
{ code: 'ENOENT' }
);
}
throw new Error(
`設定ファイル読み込みエラー (${fileName}): ${error.message}`
);
}
}
clearCache() {
this.cache.clear();
console.log('🗑️ 設定キャッシュをクリアしました');
}
}
// 使用例
async function demonstrateConfigManager() {
const configManager = new ConfigManager('./config');
try {
// 開発環境の設定読み込み
const devConfig = await configManager.loadConfig(
'development'
);
console.log('開発設定:', devConfig);
// 本番環境の設定読み込み
const prodConfig = await configManager.loadConfig(
'production'
);
console.log('本番設定:', prodConfig);
// キャッシュからの再読み込み
const cachedConfig = await configManager.loadConfig(
'development'
);
console.log(
'キャッシュされた設定:',
cachedConfig.loadedAt
);
} catch (error) {
console.error('設定管理エラー:', error.message);
}
}
demonstrateConfigManager();
ファイル書き込み操作の詳細
ファイル書き込みは、ログ出力、データ保存、レポート生成など、Node.js アプリケーションの重要な機能です。適切な書き込み方法を理解することで、データの整合性と安全性を確保できます。
writeFile と writeFileSync の活用
基本的な writeFile の使用例
javascriptconst { promises: fs } = require('fs');
const path = require('path');
async function basicWriteExample() {
const outputDir = path.join(__dirname, 'output');
const filePath = path.join(outputDir, 'example.txt');
try {
// ディレクトリが存在しない場合は作成
await fs.mkdir(outputDir, { recursive: true });
const content = `ファイル書き込みテスト
作成日時: ${new Date().toISOString()}
Node.js バージョン: ${process.version}`;
await fs.writeFile(filePath, content, 'utf8');
console.log('✅ ファイル書き込み完了:', filePath);
// 書き込み確認
const writtenContent = await fs.readFile(
filePath,
'utf8'
);
console.log('書き込み内容確認:');
console.log(writtenContent);
} catch (error) {
console.error('ファイル書き込みエラー:', error.message);
throw error;
}
}
basicWriteExample();
JSON データの書き込み
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class DataManager {
static async saveJSON(data, filePath, options = {}) {
const {
indent = 2,
backup = true,
atomic = true,
} = options;
try {
// バックアップの作成
if (backup) {
try {
await fs.access(filePath);
const backupPath = `${filePath}.backup`;
await fs.copyFile(filePath, backupPath);
console.log(
'📦 バックアップを作成しました:',
backupPath
);
} catch (error) {
// ファイルが存在しない場合はバックアップ不要
if (error.code !== 'ENOENT') {
throw error;
}
}
}
// JSON文字列に変換
const jsonString = JSON.stringify(data, null, indent);
if (atomic) {
// アトミック書き込み(一時ファイル使用)
const tempPath = `${filePath}.tmp`;
await fs.writeFile(tempPath, jsonString, 'utf8');
await fs.rename(tempPath, filePath);
console.log('✅ アトミック書き込み完了:', filePath);
} else {
// 直接書き込み
await fs.writeFile(filePath, jsonString, 'utf8');
console.log('✅ 直接書き込み完了:', filePath);
}
return true;
} catch (error) {
console.error('JSON 保存エラー:', error.message);
throw error;
}
}
static async loadJSON(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(
`JSON ファイルが見つかりません: ${filePath}`
);
}
throw new Error(
`JSON 読み込みエラー: ${error.message}`
);
}
}
}
// 使用例
async function demonstrateJSONOperations() {
const dataPath = path.join(
__dirname,
'data',
'user-settings.json'
);
const userData = {
userId: 'user123',
preferences: {
theme: 'dark',
language: 'ja',
notifications: true,
},
lastLogin: new Date().toISOString(),
version: '1.0.0',
};
try {
// データの保存
await DataManager.saveJSON(userData, dataPath, {
indent: 2,
backup: true,
atomic: true,
});
// データの読み込み
const loadedData = await DataManager.loadJSON(dataPath);
console.log('読み込んだデータ:', loadedData);
} catch (error) {
console.error('データ操作エラー:', error.message);
}
}
demonstrateJSONOperations();
追記モードと上書きモードの使い分け
ログファイルの追記処理
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class Logger {
constructor(logDir = './logs') {
this.logDir = logDir;
this.logFile = path.join(
logDir,
`app-${this.getDateString()}.log`
);
}
async init() {
try {
// ログディレクトリの作成
await fs.mkdir(this.logDir, { recursive: true });
console.log(
'📁 ログディレクトリを準備しました:',
this.logDir
);
} catch (error) {
console.error(
'ログディレクトリ作成エラー:',
error.message
);
throw error;
}
}
async log(level, message, metadata = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
metadata,
pid: process.pid,
};
const logLine = JSON.stringify(logEntry) + '\n';
try {
// appendFile を使用して追記
await fs.appendFile(this.logFile, logLine, 'utf8');
// コンソールにも出力
console.log(
`[${timestamp}] ${level.toUpperCase()}: ${message}`
);
} catch (error) {
console.error('ログ書き込みエラー:', error.message);
// ログ書き込みが失敗してもアプリケーションは継続
}
}
async info(message, metadata) {
await this.log('info', message, metadata);
}
async warn(message, metadata) {
await this.log('warn', message, metadata);
}
async error(message, metadata) {
await this.log('error', message, metadata);
}
async debug(message, metadata) {
await this.log('debug', message, metadata);
}
getDateString() {
const now = new Date();
return now.toISOString().split('T')[0]; // YYYY-MM-DD
}
async rotateLogs(maxFiles = 7) {
try {
const files = await fs.readdir(this.logDir);
const logFiles = files
.filter(
(file) =>
file.startsWith('app-') && file.endsWith('.log')
)
.map((file) => ({
name: file,
path: path.join(this.logDir, file),
stats: null,
}));
// ファイル情報の取得
for (const file of logFiles) {
try {
file.stats = await fs.stat(file.path);
} catch (error) {
console.warn(
`ファイル情報取得失敗: ${file.name}`
);
}
}
// 作成日時でソート(古い順)
logFiles.sort((a, b) => {
if (!a.stats || !b.stats) return 0;
return a.stats.birthtime - b.stats.birthtime;
});
// 古いファイルを削除
if (logFiles.length > maxFiles) {
const filesToDelete = logFiles.slice(
0,
logFiles.length - maxFiles
);
for (const file of filesToDelete) {
await fs.unlink(file.path);
console.log(
`🗑️ 古いログファイルを削除: ${file.name}`
);
}
}
console.log(
`✅ ログローテーション完了 (保持: ${Math.min(
logFiles.length,
maxFiles
)} ファイル)`
);
} catch (error) {
console.error(
'ログローテーションエラー:',
error.message
);
}
}
}
// 使用例
async function demonstrateLogging() {
const logger = new Logger('./logs');
try {
await logger.init();
// 様々なレベルのログ出力
await logger.info('アプリケーション開始', {
version: '1.0.0',
});
await logger.debug('デバッグ情報', {
userId: 'user123',
});
await logger.warn('警告メッセージ', {
reason: 'メモリ使用量が高い',
});
await logger.error('エラー発生', {
error: 'データベース接続失敗',
code: 'DB_CONNECTION_ERROR',
});
// ログローテーション
await logger.rotateLogs(5);
} catch (error) {
console.error('ログ処理エラー:', error.message);
}
}
demonstrateLogging();
ファイル権限と安全な書き込み
安全なファイル書き込みの実装
javascriptconst { promises: fs } = require('fs');
const path = require('path');
const crypto = require('crypto');
class SecureFileWriter {
static async safeWrite(filePath, content, options = {}) {
const {
encoding = 'utf8',
mode = 0o644, // rw-r--r--
checksum = true,
maxSize = 10 * 1024 * 1024, // 10MB
} = options;
try {
// コンテンツサイズの確認
const contentSize = Buffer.byteLength(
content,
encoding
);
if (contentSize > maxSize) {
throw new Error(
`ファイルサイズが制限を超えています: ${contentSize} > ${maxSize} bytes`
);
}
// ディレクトリの安全性確認
const dir = path.dirname(filePath);
await this.ensureSafeDirectory(dir);
// 一時ファイルパスの生成
const tempPath = `${filePath}.tmp.${crypto
.randomBytes(8)
.toString('hex')}`;
try {
// 一時ファイルに書き込み
await fs.writeFile(tempPath, content, {
encoding,
mode,
});
// チェックサムの検証(オプション)
if (checksum) {
const written = await fs.readFile(
tempPath,
encoding
);
if (written !== content) {
throw new Error(
'書き込み後のデータ検証に失敗しました'
);
}
}
// アトミックに本ファイルに移動
await fs.rename(tempPath, filePath);
console.log('✅ 安全な書き込み完了:', filePath);
return true;
} catch (error) {
// 一時ファイルのクリーンアップ
try {
await fs.unlink(tempPath);
} catch (cleanupError) {
console.warn(
'一時ファイル削除失敗:',
cleanupError.message
);
}
throw error;
}
} catch (error) {
console.error('安全書き込みエラー:', error.message);
throw error;
}
}
static async ensureSafeDirectory(dirPath) {
try {
// ディレクトリの作成(再帰的)
await fs.mkdir(dirPath, {
recursive: true,
mode: 0o755,
});
// ディレクトリの権限確認
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
throw new Error(
`指定されたパスはディレクトリではありません: ${dirPath}`
);
}
// 書き込み権限の確認
try {
await fs.access(dirPath, fs.constants.W_OK);
} catch (error) {
throw new Error(
`ディレクトリに書き込み権限がありません: ${dirPath}`
);
}
console.log(
'📁 ディレクトリの安全性を確認しました:',
dirPath
);
} catch (error) {
throw new Error(
`ディレクトリ確保エラー: ${error.message}`
);
}
}
static async createBackup(filePath) {
try {
await fs.access(filePath);
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '-');
const backupPath = `${filePath}.backup.${timestamp}`;
await fs.copyFile(filePath, backupPath);
console.log('📦 バックアップ作成:', backupPath);
return backupPath;
} catch (error) {
if (error.code === 'ENOENT') {
// ファイルが存在しない場合はバックアップ不要
return null;
}
throw new Error(
`バックアップ作成エラー: ${error.message}`
);
}
}
}
// 使用例
async function demonstrateSecureWrite() {
const outputPath = path.join(
__dirname,
'secure-data',
'important.json'
);
const sensitiveData = {
userId: 'user123',
apiKey:
'secret-key-' +
crypto.randomBytes(16).toString('hex'),
settings: {
encryption: true,
backup: true,
},
timestamp: new Date().toISOString(),
};
try {
// バックアップの作成
await SecureFileWriter.createBackup(outputPath);
// 安全な書き込み
await SecureFileWriter.safeWrite(
outputPath,
JSON.stringify(sensitiveData, null, 2),
{
encoding: 'utf8',
mode: 0o600, // rw-------(所有者のみ読み書き可能)
checksum: true,
maxSize: 1024 * 1024, // 1MB制限
}
);
console.log('✅ 機密データの安全な保存が完了しました');
} catch (error) {
console.error('安全書き込み処理エラー:', error.message);
}
}
demonstrateSecureWrite();
ファイルロックの実装
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class FileLock {
constructor(filePath, timeout = 30000) {
this.filePath = filePath;
this.lockPath = `${filePath}.lock`;
this.timeout = timeout;
this.locked = false;
}
async acquire() {
const startTime = Date.now();
while (Date.now() - startTime < this.timeout) {
try {
// ロックファイルの作成(排他的)
await fs.writeFile(
this.lockPath,
process.pid.toString(),
{ flag: 'wx' }
);
this.locked = true;
console.log(
'🔒 ファイルロックを取得しました:',
this.lockPath
);
return true;
} catch (error) {
if (error.code === 'EEXIST') {
// ロックファイルが既に存在する場合
try {
// 既存ロックの有効性確認
const lockContent = await fs.readFile(
this.lockPath,
'utf8'
);
const lockPid = parseInt(lockContent);
// プロセスが生きているかチェック
try {
process.kill(lockPid, 0); // シグナル0で存在確認のみ
// プロセスが生きている場合は待機
console.log(
'⏳ 他のプロセスがロック中です。待機中...'
);
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
continue;
} catch (killError) {
// プロセスが存在しない場合は古いロックファイルを削除
console.log(
'🗑️ 古いロックファイルを削除します'
);
await fs.unlink(this.lockPath);
continue;
}
} catch (readError) {
// ロックファイルの読み込みに失敗した場合も削除
await fs.unlink(this.lockPath);
continue;
}
} else {
throw new Error(
`ロック取得エラー: ${error.message}`
);
}
}
}
throw new Error(
`ロック取得タイムアウト: ${this.timeout}ms`
);
}
async release() {
if (!this.locked) {
return;
}
try {
await fs.unlink(this.lockPath);
this.locked = false;
console.log(
'🔓 ファイルロックを解放しました:',
this.lockPath
);
} catch (error) {
console.warn('ロック解放警告:', error.message);
}
}
async withLock(operation) {
await this.acquire();
try {
return await operation();
} finally {
await this.release();
}
}
}
// 使用例
async function demonstrateFileLock() {
const dataPath = path.join(__dirname, 'shared-data.json');
async function updateSharedData(workerId) {
const lock = new FileLock(dataPath);
await lock.withLock(async () => {
console.log(`🔧 Worker ${workerId}: データ更新開始`);
// 現在のデータを読み込み
let data = {};
try {
const content = await fs.readFile(dataPath, 'utf8');
data = JSON.parse(content);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
// ファイルが存在しない場合は初期化
data = { updates: [] };
}
// データの更新(時間のかかる処理をシミュレート)
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
data.updates.push({
workerId,
timestamp: new Date().toISOString(),
value: Math.random(),
});
// データの保存
await fs.writeFile(
dataPath,
JSON.stringify(data, null, 2),
'utf8'
);
console.log(`✅ Worker ${workerId}: データ更新完了`);
});
}
// 複数のワーカーが同時にデータを更新
const workers = Array.from({ length: 3 }, (_, i) =>
updateSharedData(i + 1)
);
try {
await Promise.all(workers);
// 最終結果の確認
const finalData = JSON.parse(
await fs.readFile(dataPath, 'utf8')
);
console.log('📊 最終データ:', finalData);
} catch (error) {
console.error('並行更新エラー:', error.message);
}
}
demonstrateFileLock();
ディレクトリ操作とファイル管理
ディレクトリの操作は、ファイル整理、一時ファイル管理、アップロードファイルの処理など、多くの場面で必要になります。効率的なディレクトリ管理により、アプリケーションの保守性が大幅に向上します。
ディレクトリの作成・削除・一覧取得
基本的なディレクトリ操作
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class DirectoryManager {
static async createDirectory(dirPath, options = {}) {
const { recursive = true, mode = 0o755 } = options;
try {
await fs.mkdir(dirPath, { recursive, mode });
console.log(
'📁 ディレクトリを作成しました:',
dirPath
);
// 作成されたディレクトリの情報を確認
const stats = await fs.stat(dirPath);
return {
path: dirPath,
created: stats.birthtime,
permissions: stats.mode.toString(8),
};
} catch (error) {
if (error.code === 'EEXIST') {
console.log(
'📁 ディレクトリは既に存在します:',
dirPath
);
return { path: dirPath, exists: true };
}
throw new Error(
`ディレクトリ作成エラー: ${error.message}`
);
}
}
static async listDirectory(dirPath, options = {}) {
const {
recursive = false,
includeStats = true,
filter = null,
} = options;
try {
const result = {
path: dirPath,
files: [],
directories: [],
total: 0,
};
if (recursive) {
return await this.listDirectoryRecursive(
dirPath,
filter,
includeStats
);
} else {
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
try {
const stats = includeStats
? await fs.stat(itemPath)
: null;
const itemInfo = {
name: item,
path: itemPath,
stats: stats
? {
size: stats.size,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
modified: stats.mtime,
created: stats.birthtime,
permissions: stats.mode.toString(8),
}
: null,
};
// フィルターの適用
if (filter && !filter(itemInfo)) {
continue;
}
if (stats?.isDirectory()) {
result.directories.push(itemInfo);
} else {
result.files.push(itemInfo);
}
result.total++;
} catch (statError) {
console.warn(
`項目の情報取得失敗: ${item} - ${statError.message}`
);
}
}
return result;
}
} catch (error) {
throw new Error(
`ディレクトリ一覧取得エラー: ${error.message}`
);
}
}
static async listDirectoryRecursive(
dirPath,
filter,
includeStats,
depth = 0
) {
const result = {
path: dirPath,
files: [],
directories: [],
subdirectories: {},
total: 0,
depth,
};
try {
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
try {
const stats = includeStats
? await fs.stat(itemPath)
: null;
const itemInfo = {
name: item,
path: itemPath,
stats: stats
? {
size: stats.size,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
modified: stats.mtime,
created: stats.birthtime,
permissions: stats.mode.toString(8),
}
: null,
};
if (filter && !filter(itemInfo)) {
continue;
}
if (stats?.isDirectory()) {
result.directories.push(itemInfo);
// 再帰的に子ディレクトリを処理
result.subdirectories[item] =
await this.listDirectoryRecursive(
itemPath,
filter,
includeStats,
depth + 1
);
result.total +=
result.subdirectories[item].total;
} else {
result.files.push(itemInfo);
}
result.total++;
} catch (statError) {
console.warn(
`項目の情報取得失敗: ${item} - ${statError.message}`
);
}
}
return result;
} catch (error) {
throw new Error(
`再帰的ディレクトリ一覧取得エラー: ${error.message}`
);
}
}
static async removeDirectory(dirPath, options = {}) {
const { force = false, recursive = true } = options;
try {
// ディレクトリの存在確認
await fs.access(dirPath);
if (recursive) {
// 再帰的削除(Node.js 14.14.0以降)
await fs.rm(dirPath, { recursive: true, force });
console.log(
'🗑️ ディレクトリを削除しました:',
dirPath
);
} else {
// 空のディレクトリのみ削除
await fs.rmdir(dirPath);
console.log(
'🗑️ 空のディレクトリを削除しました:',
dirPath
);
}
return true;
} catch (error) {
if (error.code === 'ENOENT') {
console.log(
'⚠️ 削除対象のディレクトリが存在しません:',
dirPath
);
return false;
} else if (error.code === 'ENOTEMPTY') {
throw new Error(
'ディレクトリが空ではありません。recursive: true を使用してください'
);
}
throw new Error(
`ディレクトリ削除エラー: ${error.message}`
);
}
}
}
// 使用例
async function demonstrateDirectoryOperations() {
const baseDir = path.join(__dirname, 'test-directory');
try {
// ディレクトリ構造の作成
await DirectoryManager.createDirectory(baseDir);
await DirectoryManager.createDirectory(
path.join(baseDir, 'documents')
);
await DirectoryManager.createDirectory(
path.join(baseDir, 'images')
);
await DirectoryManager.createDirectory(
path.join(baseDir, 'temp', 'cache')
);
// サンプルファイルの作成
const files = [
'documents/readme.txt',
'documents/config.json',
'images/logo.png',
'temp/temp-data.txt',
'temp/cache/session.json',
];
for (const file of files) {
const filePath = path.join(baseDir, file);
await fs.writeFile(
filePath,
`サンプルファイル: ${file}`,
'utf8'
);
}
// ディレクトリ一覧の取得(非再帰)
console.log('\n📋 ディレクトリ一覧(非再帰):');
const listing = await DirectoryManager.listDirectory(
baseDir
);
console.log(`ファイル数: ${listing.files.length}`);
console.log(
`ディレクトリ数: ${listing.directories.length}`
);
// ディレクトリ一覧の取得(再帰)
console.log('\n📋 ディレクトリ一覧(再帰):');
const recursiveListing =
await DirectoryManager.listDirectory(baseDir, {
recursive: true,
filter: (item) => !item.name.startsWith('.'), // 隠しファイルを除外
});
console.log(`総ファイル数: ${recursiveListing.total}`);
// フィルター付き一覧取得
console.log('\n📋 JSON ファイルのみ:');
const jsonFiles = await DirectoryManager.listDirectory(
baseDir,
{
recursive: true,
filter: (item) => item.name.endsWith('.json'),
}
);
jsonFiles.files.forEach((file) =>
console.log(` ${file.name}: ${file.path}`)
);
} catch (error) {
console.error('ディレクトリ操作エラー:', error.message);
}
}
demonstrateDirectoryOperations();
ファイルの存在確認と情報取得
高度なファイル情報取得
javascriptconst { promises: fs } = require('fs');
const path = require('path');
const crypto = require('crypto');
class FileInspector {
static async getFileInfo(filePath) {
try {
const stats = await fs.stat(filePath);
const absolutePath = path.resolve(filePath);
const info = {
path: absolutePath,
name: path.basename(filePath),
extension: path.extname(filePath),
directory: path.dirname(absolutePath),
size: {
bytes: stats.size,
kb: Math.round((stats.size / 1024) * 100) / 100,
mb:
Math.round((stats.size / (1024 * 1024)) * 100) /
100,
},
timestamps: {
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
changed: stats.ctime,
},
permissions: {
mode: stats.mode,
octal: stats.mode.toString(8),
string: this.getModeString(stats.mode),
},
type: {
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
isSymbolicLink: stats.isSymbolicLink(),
isSocket: stats.isSocket(),
isFIFO: stats.isFIFO(),
isCharacterDevice: stats.isCharacterDevice(),
isBlockDevice: stats.isBlockDevice(),
},
};
// ファイルの場合は追加情報を取得
if (stats.isFile()) {
info.checksum = await this.calculateChecksum(
filePath
);
info.mimeType = this.guessMimeType(filePath);
}
return info;
} catch (error) {
if (error.code === 'ENOENT') {
return null; // ファイルが存在しない
}
throw new Error(
`ファイル情報取得エラー: ${error.message}`
);
}
}
static async exists(filePath) {
try {
await fs.access(filePath);
return true;
} catch (error) {
return false;
}
}
static async isReadable(filePath) {
try {
await fs.access(filePath, fs.constants.R_OK);
return true;
} catch (error) {
return false;
}
}
static async isWritable(filePath) {
try {
await fs.access(filePath, fs.constants.W_OK);
return true;
} catch (error) {
return false;
}
}
static async isExecutable(filePath) {
try {
await fs.access(filePath, fs.constants.X_OK);
return true;
} catch (error) {
return false;
}
}
static async calculateChecksum(
filePath,
algorithm = 'sha256'
) {
try {
const content = await fs.readFile(filePath);
const hash = crypto.createHash(algorithm);
hash.update(content);
return hash.digest('hex');
} catch (error) {
throw new Error(
`チェックサム計算エラー: ${error.message}`
);
}
}
static getModeString(mode) {
const octal = mode.toString(8);
const permissions = octal.slice(-3);
const translatePermission = (perm) => {
const p = parseInt(perm);
let str = '';
str += p & 4 ? 'r' : '-';
str += p & 2 ? 'w' : '-';
str += p & 1 ? 'x' : '-';
return str;
};
return permissions
.split('')
.map(translatePermission)
.join('');
}
static guessMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.txt': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.xml': 'application/xml',
'.pdf': 'application/pdf',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.zip': 'application/zip',
};
return mimeTypes[ext] || 'application/octet-stream';
}
static async findDuplicateFiles(dirPath) {
const duplicates = new Map();
const scanDirectory = async (dir) => {
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
await scanDirectory(itemPath);
} else if (stats.isFile()) {
const checksum = await this.calculateChecksum(
itemPath
);
if (duplicates.has(checksum)) {
duplicates.get(checksum).push(itemPath);
} else {
duplicates.set(checksum, [itemPath]);
}
}
}
};
await scanDirectory(dirPath);
// 重複ファイルのみ返す
const result = [];
for (const [checksum, files] of duplicates) {
if (files.length > 1) {
result.push({ checksum, files });
}
}
return result;
}
}
// 使用例
async function demonstrateFileInspection() {
const testFiles = [
'./package.json',
'./nonexistent.txt',
__filename,
];
for (const filePath of testFiles) {
console.log(`\n🔍 ファイル検査: ${filePath}`);
// 存在確認
const exists = await FileInspector.exists(filePath);
console.log(`存在: ${exists ? '✅' : '❌'}`);
if (!exists) continue;
// 権限確認
const readable = await FileInspector.isReadable(
filePath
);
const writable = await FileInspector.isWritable(
filePath
);
const executable = await FileInspector.isExecutable(
filePath
);
console.log(
`権限: 読み取り${readable ? '✅' : '❌'} 書き込み${
writable ? '✅' : '❌'
} 実行${executable ? '✅' : '❌'}`
);
// 詳細情報
const info = await FileInspector.getFileInfo(filePath);
if (info) {
console.log(
`サイズ: ${info.size.bytes} bytes (${info.size.kb} KB)`
);
console.log(`タイプ: ${info.mimeType}`);
console.log(
`権限: ${info.permissions.string} (${info.permissions.octal})`
);
console.log(
`更新日: ${info.timestamps.modified.toISOString()}`
);
if (info.checksum) {
console.log(
`SHA256: ${info.checksum.slice(0, 16)}...`
);
}
}
}
// 重複ファイル検索(現在のディレクトリ)
console.log('\n🔍 重複ファイル検索:');
try {
const duplicates =
await FileInspector.findDuplicateFiles('./');
if (duplicates.length === 0) {
console.log('重複ファイルは見つかりませんでした');
} else {
duplicates.forEach((duplicate, index) => {
console.log(`\n重複グループ ${index + 1}:`);
duplicate.files.forEach((file) =>
console.log(` ${file}`)
);
});
}
} catch (error) {
console.error('重複ファイル検索エラー:', error.message);
}
}
demonstrateFileInspection();
ファイルとディレクトリの移動・コピー
高度なファイル操作
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class FileOperations {
static async copyFile(
sourcePath,
destPath,
options = {}
) {
const {
overwrite = false,
preserveTimestamps = true,
createDirectories = true,
} = options;
try {
// ソースファイルの存在確認
const sourceStats = await fs.stat(sourcePath);
if (!sourceStats.isFile()) {
throw new Error('ソースがファイルではありません');
}
// 宛先ディレクトリの作成
if (createDirectories) {
const destDir = path.dirname(destPath);
await fs.mkdir(destDir, { recursive: true });
}
// 宛先の存在確認
const destExists = await FileInspector.exists(
destPath
);
if (destExists && !overwrite) {
throw new Error(
'宛先ファイルが既に存在します(overwrite: false)'
);
}
// ファイルコピーの実行
await fs.copyFile(sourcePath, destPath);
// タイムスタンプの保持
if (preserveTimestamps) {
await fs.utimes(
destPath,
sourceStats.atime,
sourceStats.mtime
);
}
console.log(
`✅ ファイルコピー完了: ${sourcePath} → ${destPath}`
);
// コピー後の検証
const destStats = await fs.stat(destPath);
return {
success: true,
sourceSize: sourceStats.size,
destSize: destStats.size,
sizeMatch: sourceStats.size === destStats.size,
};
} catch (error) {
throw new Error(
`ファイルコピーエラー: ${error.message}`
);
}
}
static async moveFile(
sourcePath,
destPath,
options = {}
) {
const { createDirectories = true } = options;
try {
// 宛先ディレクトリの作成
if (createDirectories) {
const destDir = path.dirname(destPath);
await fs.mkdir(destDir, { recursive: true });
}
// rename を使用した高速移動を試行
try {
await fs.rename(sourcePath, destPath);
console.log(
`✅ ファイル移動完了(rename): ${sourcePath} → ${destPath}`
);
return { method: 'rename' };
} catch (renameError) {
// 異なるファイルシステム間の移動の場合はコピー+削除
if (renameError.code === 'EXDEV') {
console.log(
'⚠️ ファイルシステム間の移動のため、コピー+削除を実行します'
);
const copyResult = await this.copyFile(
sourcePath,
destPath,
{ overwrite: true }
);
if (copyResult.sizeMatch) {
await fs.unlink(sourcePath);
console.log(
`✅ ファイル移動完了(copy+delete): ${sourcePath} → ${destPath}`
);
return { method: 'copy-delete', ...copyResult };
} else {
throw new Error(
'コピー後のサイズが一致しません'
);
}
} else {
throw renameError;
}
}
} catch (error) {
throw new Error(
`ファイル移動エラー: ${error.message}`
);
}
}
static async copyDirectory(
sourcePath,
destPath,
options = {}
) {
const {
overwrite = false,
filter = null,
preserveTimestamps = true,
} = options;
const results = {
copiedFiles: 0,
copiedDirectories: 0,
skippedFiles: 0,
errors: [],
};
const copyRecursive = async (src, dest) => {
try {
const stats = await fs.stat(src);
if (stats.isDirectory()) {
// ディレクトリの作成
await fs.mkdir(dest, { recursive: true });
results.copiedDirectories++;
// タイムスタンプの保持
if (preserveTimestamps) {
await fs.utimes(dest, stats.atime, stats.mtime);
}
// 子要素の処理
const items = await fs.readdir(src);
for (const item of items) {
const srcItem = path.join(src, item);
const destItem = path.join(dest, item);
// フィルターの適用
if (filter && !filter(srcItem)) {
results.skippedFiles++;
continue;
}
await copyRecursive(srcItem, destItem);
}
} else if (stats.isFile()) {
// ファイルのコピー
const destExists = await FileInspector.exists(
dest
);
if (destExists && !overwrite) {
console.log(`⏭️ スキップ(既存): ${src}`);
results.skippedFiles++;
return;
}
await fs.copyFile(src, dest);
results.copiedFiles++;
// タイムスタンプの保持
if (preserveTimestamps) {
await fs.utimes(dest, stats.atime, stats.mtime);
}
console.log(`📄 コピー: ${src} → ${dest}`);
}
} catch (error) {
const errorInfo = {
source: src,
destination: dest,
error: error.message,
};
results.errors.push(errorInfo);
console.error(
`❌ コピーエラー: ${src} - ${error.message}`
);
}
};
try {
await copyRecursive(sourcePath, destPath);
console.log(`\n📊 ディレクトリコピー完了:`);
console.log(` ファイル: ${results.copiedFiles}`);
console.log(
` ディレクトリ: ${results.copiedDirectories}`
);
console.log(` スキップ: ${results.skippedFiles}`);
console.log(` エラー: ${results.errors.length}`);
return results;
} catch (error) {
throw new Error(
`ディレクトリコピーエラー: ${error.message}`
);
}
}
static async syncDirectories(
sourceDir,
targetDir,
options = {}
) {
const {
deleteExtra = false,
dryRun = false,
compareContent = false,
} = options;
const changes = {
toAdd: [],
toUpdate: [],
toDelete: [],
unchanged: [],
};
const getFileList = async (dir, basePath = '') => {
const result = new Map();
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const relativePath = path.join(basePath, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
const subItems = await getFileList(
itemPath,
relativePath
);
for (const [key, value] of subItems) {
result.set(key, value);
}
} else {
result.set(relativePath, {
fullPath: itemPath,
stats,
checksum: compareContent
? await FileInspector.calculateChecksum(
itemPath
)
: null,
});
}
}
return result;
};
try {
console.log('🔍 ディレクトリ同期の分析中...');
const sourceFiles = await getFileList(sourceDir);
const targetFiles = await getFileList(targetDir);
// 追加・更新が必要なファイル
for (const [
relativePath,
sourceInfo,
] of sourceFiles) {
const targetInfo = targetFiles.get(relativePath);
if (!targetInfo) {
changes.toAdd.push(relativePath);
} else {
const needsUpdate =
sourceInfo.stats.mtime >
targetInfo.stats.mtime ||
sourceInfo.stats.size !==
targetInfo.stats.size ||
(compareContent &&
sourceInfo.checksum !== targetInfo.checksum);
if (needsUpdate) {
changes.toUpdate.push(relativePath);
} else {
changes.unchanged.push(relativePath);
}
}
}
// 削除が必要なファイル
if (deleteExtra) {
for (const [relativePath] of targetFiles) {
if (!sourceFiles.has(relativePath)) {
changes.toDelete.push(relativePath);
}
}
}
console.log(`\n📊 同期分析結果:`);
console.log(` 追加: ${changes.toAdd.length}`);
console.log(` 更新: ${changes.toUpdate.length}`);
console.log(` 削除: ${changes.toDelete.length}`);
console.log(` 不変: ${changes.unchanged.length}`);
if (dryRun) {
console.log(
'\n🔍 ドライラン(実際の変更は行いません)'
);
return changes;
}
// 実際の同期処理
console.log('\n🔄 同期処理を開始...');
// ファイルの追加
for (const relativePath of changes.toAdd) {
const sourcePath = path.join(
sourceDir,
relativePath
);
const targetPath = path.join(
targetDir,
relativePath
);
await this.copyFile(sourcePath, targetPath, {
createDirectories: true,
});
}
// ファイルの更新
for (const relativePath of changes.toUpdate) {
const sourcePath = path.join(
sourceDir,
relativePath
);
const targetPath = path.join(
targetDir,
relativePath
);
await this.copyFile(sourcePath, targetPath, {
overwrite: true,
});
}
// ファイルの削除
for (const relativePath of changes.toDelete) {
const targetPath = path.join(
targetDir,
relativePath
);
await fs.unlink(targetPath);
console.log(`🗑️ 削除: ${targetPath}`);
}
console.log('✅ ディレクトリ同期完了');
return changes;
} catch (error) {
throw new Error(
`ディレクトリ同期エラー: ${error.message}`
);
}
}
}
// 使用例
async function demonstrateFileOperations() {
const sourceDir = path.join(__dirname, 'test-source');
const targetDir = path.join(__dirname, 'test-target');
try {
// テスト用ディレクトリとファイルの作成
await DirectoryManager.createDirectory(sourceDir);
await fs.writeFile(
path.join(sourceDir, 'file1.txt'),
'テストファイル1の内容'
);
await fs.writeFile(
path.join(sourceDir, 'file2.txt'),
'テストファイル2の内容'
);
await DirectoryManager.createDirectory(
path.join(sourceDir, 'subdir')
);
await fs.writeFile(
path.join(sourceDir, 'subdir', 'file3.txt'),
'サブディレクトリのファイル'
);
console.log('📁 テスト用ファイル構造を作成しました');
// ディレクトリコピー
console.log('\n📂 ディレクトリコピーのテスト:');
await FileOperations.copyDirectory(
sourceDir,
targetDir,
{
overwrite: true,
filter: (filePath) =>
!filePath.includes('file2.txt'), // file2.txt を除外
}
);
// ファイル移動
console.log('\n📦 ファイル移動のテスト:');
const moveSource = path.join(sourceDir, 'file1.txt');
const moveDest = path.join(
sourceDir,
'moved-file1.txt'
);
await FileOperations.moveFile(moveSource, moveDest);
// ディレクトリ同期(ドライラン)
console.log(
'\n🔄 ディレクトリ同期のテスト(ドライラン):'
);
await FileOperations.syncDirectories(
sourceDir,
targetDir,
{
deleteExtra: true,
dryRun: true,
compareContent: true,
}
);
} catch (error) {
console.error('ファイル操作デモエラー:', error.message);
}
}
demonstrateFileOperations();
高度な活用例と実践パターン
ここまでの基本操作を組み合わせることで、実際のプロジェクトで活用できる高度なファイル操作パターンを実装できます。
ストリームを使った大容量ファイル処理
大容量ファイルを効率的に処理するためには、ストリームを活用することが重要です。
ストリームベースのファイル処理
javascriptconst fs = require('fs');
const { pipeline } = require('stream');
const { Transform } = require('stream');
const zlib = require('zlib');
const crypto = require('crypto');
const path = require('path');
class StreamProcessor {
static async processLargeFile(
inputPath,
outputPath,
options = {}
) {
const {
compress = false,
encrypt = false,
encryptionKey = null,
chunkSize = 64 * 1024, // 64KB
progress = false,
} = options;
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(inputPath);
const writeStream = fs.createWriteStream(outputPath);
// 処理チェーンの構築
const transforms = [];
// 進捗表示用トランスフォーム
if (progress) {
let processedBytes = 0;
const progressTransform = new Transform({
transform(chunk, encoding, callback) {
processedBytes += chunk.length;
process.stdout.write(
`\r処理済み: ${(
processedBytes /
1024 /
1024
).toFixed(2)} MB`
);
this.push(chunk);
callback();
},
});
transforms.push(progressTransform);
}
// 暗号化トランスフォーム
if (encrypt && encryptionKey) {
const cipher = crypto.createCipher(
'aes256',
encryptionKey
);
transforms.push(cipher);
}
// 圧縮トランスフォーム
if (compress) {
const gzipStream = zlib.createGzip();
transforms.push(gzipStream);
}
// パイプライン実行
pipeline(
readStream,
...transforms,
writeStream,
(error) => {
if (progress) {
console.log(''); // 改行
}
if (error) {
console.error(
'ストリーム処理エラー:',
error.message
);
reject(error);
} else {
console.log('✅ ストリーム処理完了');
resolve();
}
}
);
});
}
static async splitLargeFile(
inputPath,
outputDir,
chunkSize = 10 * 1024 * 1024
) {
const inputStats = await fs.promises.stat(inputPath);
const totalSize = inputStats.size;
const numChunks = Math.ceil(totalSize / chunkSize);
console.log(
`📊 ファイル分割: ${totalSize} bytes → ${numChunks} チャンク`
);
const readStream = fs.createReadStream(inputPath);
let currentChunk = 0;
let currentChunkSize = 0;
let writeStream = null;
await fs.promises.mkdir(outputDir, { recursive: true });
const createNewChunk = () => {
if (writeStream) {
writeStream.end();
}
const chunkPath = path.join(
outputDir,
`chunk_${currentChunk.toString().padStart(4, '0')}`
);
writeStream = fs.createWriteStream(chunkPath);
currentChunkSize = 0;
console.log(`📦 新しいチャンク作成: ${chunkPath}`);
};
createNewChunk();
return new Promise((resolve, reject) => {
readStream.on('data', (chunk) => {
if (
currentChunkSize + chunk.length > chunkSize &&
currentChunkSize > 0
) {
currentChunk++;
createNewChunk();
}
writeStream.write(chunk);
currentChunkSize += chunk.length;
});
readStream.on('end', () => {
if (writeStream) {
writeStream.end();
}
console.log('✅ ファイル分割完了');
resolve({ chunks: currentChunk + 1, totalSize });
});
readStream.on('error', reject);
});
}
static async mergeChunks(
inputDir,
outputPath,
chunkPattern = 'chunk_'
) {
const files = await fs.promises.readdir(inputDir);
const chunkFiles = files
.filter((file) => file.startsWith(chunkPattern))
.sort()
.map((file) => path.join(inputDir, file));
console.log(
`🔗 ${chunkFiles.length} チャンクを結合中...`
);
const writeStream = fs.createWriteStream(outputPath);
for (const chunkFile of chunkFiles) {
console.log(`📎 結合中: ${path.basename(chunkFile)}`);
const readStream = fs.createReadStream(chunkFile);
await new Promise((resolve, reject) => {
readStream.on('data', (chunk) => {
writeStream.write(chunk);
});
readStream.on('end', resolve);
readStream.on('error', reject);
});
}
writeStream.end();
console.log('✅ ファイル結合完了');
}
}
// 使用例
async function demonstrateStreamProcessing() {
const testFile = path.join(
__dirname,
'large-test-file.txt'
);
const processedFile = path.join(
__dirname,
'processed-file.txt.gz'
);
const chunksDir = path.join(__dirname, 'chunks');
const mergedFile = path.join(
__dirname,
'merged-file.txt'
);
try {
// テスト用大容量ファイルの作成
console.log('📝 テスト用ファイルを作成中...');
const testContent =
'これは大容量ファイルのテストです。\n'.repeat(100000);
await fs.promises.writeFile(testFile, testContent);
// ストリーム処理(圧縮)
console.log('\n🔄 ストリーム処理(圧縮):');
await StreamProcessor.processLargeFile(
testFile,
processedFile,
{
compress: true,
progress: true,
}
);
// ファイル分割
console.log('\n✂️ ファイル分割:');
const splitResult =
await StreamProcessor.splitLargeFile(
testFile,
chunksDir,
500 * 1024
);
console.log(
`分割結果: ${splitResult.chunks} チャンク、総サイズ: ${splitResult.totalSize} bytes`
);
// ファイル結合
console.log('\n🔗 ファイル結合:');
await StreamProcessor.mergeChunks(
chunksDir,
mergedFile
);
// 結果検証
const originalContent = await fs.promises.readFile(
testFile,
'utf8'
);
const mergedContent = await fs.promises.readFile(
mergedFile,
'utf8'
);
if (originalContent === mergedContent) {
console.log('✅ ファイル分割・結合の検証成功');
} else {
console.log('❌ ファイル分割・結合の検証失敗');
}
} catch (error) {
console.error(
'ストリーム処理デモエラー:',
error.message
);
}
}
demonstrateStreamProcessing();
ファイル監視とリアルタイム処理
ファイルシステムの変更を監視することで、リアルタイムな処理を実現できます。
高度なファイル監視システム
javascriptconst fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
class FileWatcher extends EventEmitter {
constructor(watchPath, options = {}) {
super();
this.watchPath = watchPath;
this.options = {
recursive: true,
debounceTime: 100,
ignorePatterns: [
/node_modules/,
/\.git/,
/\.DS_Store/,
],
...options,
};
this.watchers = new Map();
this.debounceTimers = new Map();
this.fileStates = new Map();
}
async start() {
console.log('👁️ ファイル監視を開始:', this.watchPath);
// 初期状態の記録
await this.scanDirectory(this.watchPath);
// ディレクトリ監視の開始
await this.setupWatcher(this.watchPath);
this.emit('ready');
}
async setupWatcher(dirPath) {
try {
const watcher = fs.watch(
dirPath,
{ recursive: this.options.recursive },
(eventType, filename) => {
if (filename) {
const fullPath = path.join(dirPath, filename);
this.handleFileChange(eventType, fullPath);
}
}
);
this.watchers.set(dirPath, watcher);
watcher.on('error', (error) => {
console.error(
`監視エラー (${dirPath}):`,
error.message
);
this.emit('error', error);
});
} catch (error) {
console.error(
'監視セットアップエラー:',
error.message
);
throw error;
}
}
handleFileChange(eventType, filePath) {
// 無視パターンのチェック
if (this.shouldIgnore(filePath)) {
return;
}
// デバウンス処理
const debounceKey = filePath;
if (this.debounceTimers.has(debounceKey)) {
clearTimeout(this.debounceTimers.get(debounceKey));
}
this.debounceTimers.set(
debounceKey,
setTimeout(async () => {
await this.processFileChange(eventType, filePath);
this.debounceTimers.delete(debounceKey);
}, this.options.debounceTime)
);
}
async processFileChange(eventType, filePath) {
try {
const exists = await this.fileExists(filePath);
const previousState = this.fileStates.get(filePath);
if (exists) {
const stats = await fs.promises.stat(filePath);
const currentState = {
size: stats.size,
mtime: stats.mtime.getTime(),
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
};
if (!previousState) {
// 新規ファイル
this.fileStates.set(filePath, currentState);
this.emit('add', {
path: filePath,
stats: currentState,
eventType,
});
} else if (
this.hasFileChanged(previousState, currentState)
) {
// ファイル変更
this.fileStates.set(filePath, currentState);
this.emit('change', {
path: filePath,
stats: currentState,
previousStats: previousState,
eventType,
});
}
} else if (previousState) {
// ファイル削除
this.fileStates.delete(filePath);
this.emit('unlink', {
path: filePath,
previousStats: previousState,
eventType,
});
}
} catch (error) {
console.error(
`ファイル変更処理エラー (${filePath}):`,
error.message
);
}
}
async scanDirectory(dirPath) {
try {
const items = await fs.promises.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
if (this.shouldIgnore(itemPath)) {
continue;
}
try {
const stats = await fs.promises.stat(itemPath);
this.fileStates.set(itemPath, {
size: stats.size,
mtime: stats.mtime.getTime(),
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
});
if (
stats.isDirectory() &&
this.options.recursive
) {
await this.scanDirectory(itemPath);
}
} catch (statError) {
console.warn(`ファイル情報取得失敗: ${itemPath}`);
}
}
} catch (error) {
console.error(
`ディレクトリスキャンエラー: ${dirPath}`,
error.message
);
}
}
shouldIgnore(filePath) {
return this.options.ignorePatterns.some((pattern) => {
if (pattern instanceof RegExp) {
return pattern.test(filePath);
} else if (typeof pattern === 'string') {
return filePath.includes(pattern);
}
return false;
});
}
hasFileChanged(previous, current) {
return (
previous.size !== current.size ||
previous.mtime !== current.mtime
);
}
async fileExists(filePath) {
try {
await fs.promises.access(filePath);
return true;
} catch (error) {
return false;
}
}
stop() {
console.log('🛑 ファイル監視を停止');
// 全ての監視を停止
for (const watcher of this.watchers.values()) {
watcher.close();
}
this.watchers.clear();
// デバウンスタイマーをクリア
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
this.emit('stop');
}
}
// 実際のプロジェクトでの活用例
class AutoProcessor {
constructor(inputDir, outputDir) {
this.inputDir = inputDir;
this.outputDir = outputDir;
this.watcher = new FileWatcher(inputDir, {
ignorePatterns: [/\.tmp$/, /\.log$/],
});
this.setupEventHandlers();
}
setupEventHandlers() {
this.watcher.on('add', (event) => {
console.log(`➕ ファイル追加: ${event.path}`);
this.processNewFile(event.path);
});
this.watcher.on('change', (event) => {
console.log(`🔄 ファイル変更: ${event.path}`);
this.processChangedFile(event.path);
});
this.watcher.on('unlink', (event) => {
console.log(`➖ ファイル削除: ${event.path}`);
this.handleFileRemoval(event.path);
});
}
async processNewFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
try {
switch (ext) {
case '.json':
await this.processJsonFile(filePath);
break;
case '.txt':
await this.processTextFile(filePath);
break;
case '.csv':
await this.processCsvFile(filePath);
break;
default:
console.log(`⚠️ 未対応のファイル形式: ${ext}`);
}
} catch (error) {
console.error(
`ファイル処理エラー (${filePath}):`,
error.message
);
}
}
async processJsonFile(filePath) {
const content = await fs.promises.readFile(
filePath,
'utf8'
);
const data = JSON.parse(content);
// データの変換・加工
const processed = {
...data,
processedAt: new Date().toISOString(),
sourceFile: filePath,
};
const outputPath = path.join(
this.outputDir,
`processed-${path.basename(filePath)}`
);
await fs.promises.writeFile(
outputPath,
JSON.stringify(processed, null, 2)
);
console.log(`✅ JSON ファイル処理完了: ${outputPath}`);
}
async processTextFile(filePath) {
const content = await fs.promises.readFile(
filePath,
'utf8'
);
// テキストの分析・変換
const lines = content.split('\n');
const analysis = {
fileName: path.basename(filePath),
lineCount: lines.length,
wordCount: content.split(/\s+/).length,
charCount: content.length,
processedAt: new Date().toISOString(),
};
const outputPath = path.join(
this.outputDir,
`analysis-${path.basename(filePath, '.txt')}.json`
);
await fs.promises.writeFile(
outputPath,
JSON.stringify(analysis, null, 2)
);
console.log(
`✅ テキストファイル分析完了: ${outputPath}`
);
}
async processCsvFile(filePath) {
const content = await fs.promises.readFile(
filePath,
'utf8'
);
const lines = content.trim().split('\n');
if (lines.length < 2) {
throw new Error(
'CSV ファイルにデータが不足しています'
);
}
const headers = lines[0].split(',');
const records = lines.slice(1).map((line) => {
const values = line.split(',');
const record = {};
headers.forEach((header, index) => {
record[header.trim()] = values[index]?.trim() || '';
});
return record;
});
const outputPath = path.join(
this.outputDir,
`converted-${path.basename(filePath, '.csv')}.json`
);
await fs.promises.writeFile(
outputPath,
JSON.stringify(records, null, 2)
);
console.log(`✅ CSV ファイル変換完了: ${outputPath}`);
}
async processChangedFile(filePath) {
console.log(`🔄 ファイル更新を再処理: ${filePath}`);
await this.processNewFile(filePath);
}
async handleFileRemoval(filePath) {
// 対応する出力ファイルも削除
const baseName = path.basename(
filePath,
path.extname(filePath)
);
const outputPattern = new RegExp(
`(processed|analysis|converted)-${baseName}`
);
try {
const outputFiles = await fs.promises.readdir(
this.outputDir
);
for (const file of outputFiles) {
if (outputPattern.test(file)) {
const outputPath = path.join(
this.outputDir,
file
);
await fs.promises.unlink(outputPath);
console.log(
`🗑️ 関連出力ファイルを削除: ${outputPath}`
);
}
}
} catch (error) {
console.error(
'関連ファイル削除エラー:',
error.message
);
}
}
async start() {
await fs.promises.mkdir(this.outputDir, {
recursive: true,
});
await this.watcher.start();
console.log('🚀 自動処理システムが開始されました');
}
stop() {
this.watcher.stop();
console.log('⏹️ 自動処理システムが停止されました');
}
}
// 使用例
async function demonstrateFileWatching() {
const inputDir = path.join(__dirname, 'watch-input');
const outputDir = path.join(__dirname, 'watch-output');
// 入力ディレクトリの作成
await fs.promises.mkdir(inputDir, { recursive: true });
const processor = new AutoProcessor(inputDir, outputDir);
try {
await processor.start();
// テストファイルの作成(遅延を入れて変更をシミュレート)
setTimeout(async () => {
console.log('\n📝 テストファイルを作成...');
// JSON ファイル
await fs.promises.writeFile(
path.join(inputDir, 'test-data.json'),
JSON.stringify(
{ message: 'Hello World', timestamp: Date.now() },
null,
2
)
);
// テキストファイル
await fs.promises.writeFile(
path.join(inputDir, 'sample.txt'),
'これはテストファイルです。\n複数行のテキストが含まれています。\n自動処理のテストを行います。'
);
// CSV ファイル
await fs.promises.writeFile(
path.join(inputDir, 'data.csv'),
'name,age,city\nTaro,25,Tokyo\nHanako,30,Osaka\nJiro,28,Kyoto'
);
}, 1000);
// 10秒後に停止
setTimeout(() => {
processor.stop();
}, 10000);
} catch (error) {
console.error('ファイル監視デモエラー:', error.message);
}
}
demonstrateFileWatching();
実際のプロジェクトでの活用事例
設定管理システム
javascriptconst { promises: fs } = require('fs');
const path = require('path');
class ConfigurationManager {
constructor(configDir = './config') {
this.configDir = configDir;
this.cache = new Map();
this.watchers = new Map();
this.validators = new Map();
}
// 設定の検証ルールを登録
addValidator(configName, validator) {
this.validators.set(configName, validator);
}
// 設定ファイルの読み込み
async load(configName, options = {}) {
const {
watch = false,
required = true,
defaultValue = null,
} = options;
const configPath = path.join(
this.configDir,
`${configName}.json`
);
try {
const content = await fs.readFile(configPath, 'utf8');
const config = JSON.parse(content);
// バリデーション
if (this.validators.has(configName)) {
const validator = this.validators.get(configName);
const validation = validator(config);
if (!validation.valid) {
throw new Error(
`設定検証エラー (${configName}): ${validation.errors.join(
', '
)}`
);
}
}
// キャッシュに保存
this.cache.set(configName, config);
// ファイル監視の設定
if (watch && !this.watchers.has(configName)) {
this.setupConfigWatcher(configName, configPath);
}
console.log(`✅ 設定読み込み完了: ${configName}`);
return config;
} catch (error) {
if (error.code === 'ENOENT' && !required) {
console.log(
`⚠️ 設定ファイルが見つかりません(デフォルト値使用): ${configName}`
);
return defaultValue;
}
throw new Error(
`設定読み込みエラー (${configName}): ${error.message}`
);
}
}
// 設定ファイルの保存
async save(configName, config) {
// バリデーション
if (this.validators.has(configName)) {
const validator = this.validators.get(configName);
const validation = validator(config);
if (!validation.valid) {
throw new Error(
`設定検証エラー (${configName}): ${validation.errors.join(
', '
)}`
);
}
}
const configPath = path.join(
this.configDir,
`${configName}.json`
);
// ディレクトリの作成
await fs.mkdir(this.configDir, { recursive: true });
// バックアップの作成
try {
await fs.access(configPath);
const backupPath = `${configPath}.backup.${Date.now()}`;
await fs.copyFile(configPath, backupPath);
console.log(`📦 バックアップ作成: ${backupPath}`);
} catch (error) {
// ファイルが存在しない場合はバックアップ不要
}
// 設定の保存
await fs.writeFile(
configPath,
JSON.stringify(config, null, 2)
);
// キャッシュの更新
this.cache.set(configName, config);
console.log(`✅ 設定保存完了: ${configName}`);
}
// ファイル監視の設定
setupConfigWatcher(configName, configPath) {
const watcher = fs.watch(
configPath,
async (eventType) => {
if (eventType === 'change') {
console.log(
`🔄 設定ファイル変更検知: ${configName}`
);
try {
// 設定の再読み込み
await this.load(configName, { watch: false });
console.log(
`✅ 設定自動更新完了: ${configName}`
);
} catch (error) {
console.error(
`設定自動更新エラー (${configName}):`,
error.message
);
}
}
}
);
this.watchers.set(configName, watcher);
console.log(`👁️ ファイル監視開始: ${configName}`);
}
// 設定の取得(キャッシュから)
get(configName) {
return this.cache.get(configName);
}
// 全設定の一覧
list() {
return Array.from(this.cache.keys());
}
// リソースのクリーンアップ
cleanup() {
for (const watcher of this.watchers.values()) {
watcher.close();
}
this.watchers.clear();
console.log(
'🧹 設定管理リソースをクリーンアップしました'
);
}
}
// 使用例とバリデーター
async function demonstrateConfigurationManager() {
const configManager = new ConfigurationManager(
'./app-config'
);
// データベース設定のバリデーター
configManager.addValidator('database', (config) => {
const errors = [];
if (!config.host) errors.push('host は必須です');
if (
!config.port ||
config.port < 1 ||
config.port > 65535
)
errors.push(
'port は 1-65535 の範囲で指定してください'
);
if (!config.database)
errors.push('database は必須です');
return {
valid: errors.length === 0,
errors,
};
});
// API 設定のバリデーター
configManager.addValidator('api', (config) => {
const errors = [];
if (!config.baseUrl) errors.push('baseUrl は必須です');
if (config.timeout && config.timeout < 1000)
errors.push(
'timeout は 1000ms 以上で指定してください'
);
return {
valid: errors.length === 0,
errors,
};
});
try {
// 設定の保存
await configManager.save('database', {
host: 'localhost',
port: 5432,
database: 'myapp',
username: 'user',
password: 'password',
ssl: false,
});
await configManager.save('api', {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
rateLimit: {
requests: 100,
window: 60000,
},
});
// 設定の読み込み(監視付き)
const dbConfig = await configManager.load('database', {
watch: true,
});
const apiConfig = await configManager.load('api', {
watch: true,
});
console.log('\n📋 読み込まれた設定:');
console.log('データベース:', dbConfig);
console.log('API:', apiConfig);
// 設定一覧の表示
console.log('\n📂 管理中の設定:', configManager.list());
// 5秒後にクリーンアップ
setTimeout(() => {
configManager.cleanup();
}, 5000);
} catch (error) {
console.error('設定管理デモエラー:', error.message);
}
}
demonstrateConfigurationManager();
まとめ
Node.js の fs モジュールは、ファイルシステム操作の強力なツールセットを提供しています。この記事では、基本的な読み書き操作から高度なストリーム処理、リアルタイム監視まで、実際のプロジェクトで活用できる包括的な知識をお届けいたしました。
重要なポイントのまとめ
# | 項目 | 重要な概念 | 実践での活用 |
---|---|---|---|
1 | 基本操作 | 同期・非同期・Promise の使い分け | パフォーマンスとコードの可読性のバランス |
2 | エラーハンドリング | 適切な例外処理とリトライ機構 | 堅牢なアプリケーションの構築 |
3 | ファイル書き込み | アトミック操作とバックアップ | データの整合性と安全性の確保 |
4 | ディレクトリ管理 | 再帰処理とフィルタリング | 効率的なファイル整理システム |
5 | ストリーム処理 | 大容量ファイルの効率的な処理 | メモリ使用量の最適化 |
6 | リアルタイム監視 | ファイル変更の検知と自動処理 | 動的な業務フロー自動化 |
開発における実践的な指針
適切なファイル操作の実装により、以下のような価値を提供できます:
- 信頼性の向上: 適切なエラーハンドリングにより、予期しない状況でも安定した動作を実現
- パフォーマンスの最適化: ストリーム処理により、大容量ファイルを効率的に処理
- 保守性の確保: モジュール化された設計により、機能の拡張と修正が容易
- ユーザー体験の改善: リアルタイム処理により、レスポンシブなアプリケーション体験を提供
fs モジュールをマスターすることで、データ処理、設定管理、ログ処理、ファイル変換など、様々な用途でより効果的なソリューションを構築できるようになります。
今回ご紹介したテクニックを実際のプロジェクトで活用し、より堅牢で効率的な Node.js アプリケーションの開発にお役立てください。継続的な学習と実践により、さらに高度なファイル操作スキルを身につけることができるでしょう。
関連リンク
- review
アジャイル初心者でも大丈夫!『アジャイルサムライ − 達人開発者への道』Jonathan Rasmusson
- review
人生が作品になる!『自分の中に毒を持て』岡本太郎
- review
体調不良の 99%が解決!『眠れなくなるほど面白い 図解 自律神経の話』小林弘幸著で学ぶ、現代人必須の自律神経コントロール術と人生を変える健康革命
- review
衝撃の事実!『睡眠こそ最強の解決策である』マシュー・ウォーカー著が明かす、99%の人が知らない睡眠の驚くべき真実と人生を変える科学的メカニズム
- review
人生が激変!『嫌われる勇気』岸見一郎・古賀史健著から学ぶ、アドラー心理学で手に入れる真の幸福と自己実現
- review
もう無駄な努力はしない!『イシューからはじめよ』安宅和人著で身につけた、99%の人が知らない本当に価値ある問題の見つけ方