T-CREATOR

Node.js でディレクトリ操作:作成・削除・一覧取得

Node.js でディレクトリ操作:作成・削除・一覧取得

Node.js でアプリケーションを開発していると、「プロジェクトの初期構造を自動で作りたい」「ログファイルを日付別に整理したい」「不要なディレクトリを一括削除したい」といった場面に遭遇することがありませんか?

これらの課題を解決するのが、Node.js のディレクトリ操作です。ファイル操作と並んで重要な機能でありながら、意外と体系的に学ぶ機会が少ないのがディレクトリ操作の特徴です。

本記事では、ディレクトリの作成・削除・一覧取得といった基本操作から、実際のプロジェクトで活用できる階層構造の探索まで、Node.js でのディレクトリ操作を包括的に解説いたします。適切なディレクトリ管理により、より整理された保守性の高いアプリケーションを構築できるようになるでしょう。

ディレクトリ操作の基本概念

Node.js でディレクトリ操作を行う前に、基本的な概念を理解することが重要です。これらの知識により、より効果的で安全なディレクトリ管理が可能になります。

ディレクトリとファイルの違い

ファイルシステムにおいて、ディレクトリとファイルは根本的に異なる性質を持っています。

ディレクトリとファイルの基本的な違い

#項目ディレクトリファイル
1役割コンテナ(他の要素を格納)データの実体
2内容ファイルや他のディレクトリテキスト、バイナリデータ
3階層構造親子関係を形成リーフノード(末端)
4操作の複雑さ再帰的な処理が必要な場合あり単体での操作が基本
5削除の影響配下の全要素に影響単一ファイルのみ

Node.js でのディレクトリとファイルの判別は以下のように行います:

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

async function inspectFileSystemItem(itemPath) {
  try {
    const stats = await fs.stat(itemPath);

    const itemInfo = {
      path: itemPath,
      name: path.basename(itemPath),
      type: stats.isDirectory() ? 'directory' : 'file',
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime,
      permissions: stats.mode.toString(8),
    };

    if (stats.isDirectory()) {
      console.log(`📁 ディレクトリ: ${itemInfo.name}`);
      console.log(`   パス: ${itemInfo.path}`);
      console.log(
        `   作成日: ${itemInfo.created.toISOString()}`
      );
    } else {
      console.log(`📄 ファイル: ${itemInfo.name}`);
      console.log(`   パス: ${itemInfo.path}`);
      console.log(`   サイズ: ${itemInfo.size} bytes`);
      console.log(
        `   更新日: ${itemInfo.modified.toISOString()}`
      );
    }

    return itemInfo;
  } catch (error) {
    console.error(
      `❌ 項目の情報取得に失敗: ${error.message}`
    );
    return null;
  }
}

// 使用例
inspectFileSystemItem('./package.json');
inspectFileSystemItem('./node_modules');

絶対パスと相対パスの使い分け

パスの指定方法は、ディレクトリ操作の成功・失敗に大きく影響します。適切なパス指定により、環境に依存しない堅牢なコードを書けるでしょう。

パスの種類と特徴

javascriptconst path = require('path');

function demonstratePathTypes() {
  // 現在の作業ディレクトリ
  const currentDir = process.cwd();
  console.log('現在の作業ディレクトリ:', currentDir);

  // スクリプトファイルのディレクトリ
  const scriptDir = __dirname;
  console.log('スクリプトのディレクトリ:', scriptDir);

  // 絶対パスの例
  const absolutePath = path.join(
    currentDir,
    'data',
    'logs'
  );
  console.log('絶対パス:', absolutePath);

  // 相対パスの例
  const relativePath = './data/logs';
  console.log('相対パス:', relativePath);

  // 正規化されたパス
  const normalizedPath = path.resolve(relativePath);
  console.log('正規化パス:', normalizedPath);

  // プラットフォーム対応パス結合
  const crossPlatformPath = path.join(
    'data',
    'users',
    'uploads'
  );
  console.log(
    'クロスプラットフォームパス:',
    crossPlatformPath
  );

  return {
    currentDir,
    scriptDir,
    absolutePath,
    relativePath,
    normalizedPath,
    crossPlatformPath,
  };
}

demonstratePathTypes();

パス指定のベストプラクティス

#場面推奨方法理由
1設定ファイルの指定path.join(__dirname)スクリプト位置を基準にした確実性
2ユーザー入力の処理path.resolve()相対パスを絶対パスに正規化
3一時ディレクトリos.tmpdir()OS 標準の一時ディレクトリを活用
4クロスプラットフォームpath.join()パス区切り文字の自動調整
5外部からのパス入力path.normalize()パストラバーサル攻撃の防止

Node.js でのディレクトリ操作 API 概要

Node.js は豊富なディレクトリ操作 API を提供しています。用途に応じて適切な API を選択することが重要です。

主要なディレクトリ操作 API

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

// 主要なディレクトリ操作APIの概要
const directoryAPIs = {
  // ディレクトリ作成
  creation: {
    sync: 'fs.mkdirSync(path, options)',
    async: 'fs.mkdir(path, options, callback)',
    promise: 'fsPromises.mkdir(path, options)',
    description: 'ディレクトリを作成します',
  },

  // ディレクトリ削除
  deletion: {
    sync: 'fs.rmdirSync(path, options)',
    async: 'fs.rmdir(path, options, callback)',
    promise: 'fsPromises.rmdir(path, options)',
    newMethod: 'fsPromises.rm(path, {recursive: true})',
    description: 'ディレクトリを削除します',
  },

  // ディレクトリ内容の読み取り
  reading: {
    sync: 'fs.readdirSync(path, options)',
    async: 'fs.readdir(path, options, callback)',
    promise: 'fsPromises.readdir(path, options)',
    description: 'ディレクトリの内容を一覧取得します',
  },

  // ファイル・ディレクトリの情報取得
  stats: {
    sync: 'fs.statSync(path)',
    async: 'fs.stat(path, callback)',
    promise: 'fsPromises.stat(path)',
    description:
      'ファイル・ディレクトリの詳細情報を取得します',
  },

  // 存在確認
  access: {
    sync: 'fs.accessSync(path, mode)',
    async: 'fs.access(path, mode, callback)',
    promise: 'fsPromises.access(path, mode)',
    description:
      'ファイル・ディレクトリの存在や権限を確認します',
  },
};

function showAPIOverview() {
  console.log('📚 Node.js ディレクトリ操作 API 概要\n');

  Object.entries(directoryAPIs).forEach(
    ([category, api]) => {
      console.log(`## ${category.toUpperCase()}`);
      console.log(`目的: ${api.description}`);
      console.log(`同期版: ${api.sync}`);
      console.log(`非同期版: ${api.async}`);
      console.log(`Promise版: ${api.promise}`);
      if (api.newMethod) {
        console.log(`新API: ${api.newMethod}`);
      }
      console.log('');
    }
  );
}

showAPIOverview();

API 選択の指針

javascript// 用途別のAPI選択例
class DirectoryAPISelector {
  static getRecommendedAPI(useCase) {
    const recommendations = {
      'web-application': {
        preferred: 'Promise-based (async/await)',
        reason: 'ノンブロッキング処理でパフォーマンス向上',
        example:
          'await fsPromises.mkdir(path, {recursive: true})',
      },
      'cli-tool': {
        preferred: 'Synchronous',
        reason:
          'シンプルな制御フローと明確なエラーハンドリング',
        example: 'fs.mkdirSync(path, {recursive: true})',
      },
      'build-script': {
        preferred: 'Promise-based with error handling',
        reason: '複雑な処理フローと詳細なエラー報告',
        example:
          'try { await fsPromises.mkdir(path) } catch (error) { ... }',
      },
      'real-time-processing': {
        preferred: 'Callback-based or Promise-based',
        reason: 'イベント駆動でのレスポンシブな処理',
        example:
          'fs.mkdir(path, callback) or await fsPromises.mkdir(path)',
      },
    };

    return (
      recommendations[useCase] || {
        preferred: 'Promise-based',
        reason:
          '現代的なJavaScript開発での標準的なアプローチ',
        example: 'await fsPromises.mkdir(path, options)',
      }
    );
  }
}

// 使用例
console.log(
  'Web アプリケーション:',
  DirectoryAPISelector.getRecommendedAPI('web-application')
);
console.log(
  'CLI ツール:',
  DirectoryAPISelector.getRecommendedAPI('cli-tool')
);

同期処理と非同期処理の特徴

ディレクトリ操作においても、ファイル操作と同様に同期・非同期の選択が重要です。

処理方式の比較表

#項目同期処理非同期処理
1実行のブロックあり(処理完了まで待機)なし(他の処理と並行実行)
2エラーハンドリングtry-catch で簡潔callback/Promise でやや複雑
3パフォーマンス単一処理では高速並行処理で全体的に高速
4用途初期化、CLI ツールWeb サーバー、リアルタイム処理
5コードの複雑さシンプルasync/await で改善

この基本概念を理解した上で、次のセクションから具体的なディレクトリ操作の実装方法を学んでいきましょう。

ディレクトリの作成

ディレクトリの作成は、プロジェクトの初期化、ログファイルの整理、ユーザーデータの管理など、様々な場面で必要になる基本的な操作です。Node.js では複数の方法でディレクトリを作成できますが、用途に応じて適切な方法を選択することが重要です。

fs.mkdir() の基本的な使い方

最も基本的なディレクトリ作成方法から始めましょう。

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

// 基本的なディレクトリ作成
async function createBasicDirectory() {
  const dirPath = path.join(__dirname, 'new-directory');

  try {
    await fs.mkdir(dirPath);
    console.log('✅ ディレクトリを作成しました:', dirPath);

    // 作成されたディレクトリの情報を確認
    const stats = await fs.stat(dirPath);
    console.log('作成日時:', stats.birthtime.toISOString());
    console.log('権限:', stats.mode.toString(8));

    return dirPath;
  } catch (error) {
    if (error.code === 'EEXIST') {
      console.log(
        '⚠️ ディレクトリは既に存在します:',
        dirPath
      );
      return dirPath;
    } else if (error.code === 'ENOENT') {
      console.error(
        '❌ 親ディレクトリが存在しません:',
        error.message
      );
      throw error;
    } else {
      console.error(
        '❌ ディレクトリ作成エラー:',
        error.message
      );
      throw error;
    }
  }
}

// 同期版の例
function createDirectorySync() {
  const dirPath = path.join(__dirname, 'sync-directory');

  try {
    fs.mkdirSync(dirPath);
    console.log(
      '✅ 同期的にディレクトリを作成しました:',
      dirPath
    );
    return dirPath;
  } catch (error) {
    if (error.code === 'EEXIST') {
      console.log(
        '⚠️ ディレクトリは既に存在します:',
        dirPath
      );
      return dirPath;
    }
    throw error;
  }
}

// 使用例
createBasicDirectory();
createDirectorySync();

再帰的ディレクトリ作成(recursive: true)

深い階層のディレクトリを一度に作成する場合は、recursive オプションが非常に便利です。

javascript// 再帰的ディレクトリ作成の高度な例
class DirectoryCreator {
  static async createPath(targetPath, options = {}) {
    const {
      recursive = true,
      mode = 0o755,
      dryRun = false,
      verbose = true,
    } = options;

    if (dryRun) {
      console.log(
        '🔍 ドライラン: 以下のディレクトリが作成されます'
      );
      this.showDirectoryStructure(targetPath);
      return { created: false, path: targetPath };
    }

    try {
      const result = await fs.mkdir(targetPath, {
        recursive,
        mode,
      });

      if (verbose) {
        if (result) {
          console.log(
            '✅ ディレクトリパスを作成しました:',
            result
          );
        } else {
          console.log(
            'ℹ️ ディレクトリは既に存在していました:',
            targetPath
          );
        }
      }

      return { created: !!result, path: targetPath };
    } catch (error) {
      console.error(
        '❌ 再帰的ディレクトリ作成エラー:',
        error.message
      );
      throw error;
    }
  }

  static showDirectoryStructure(dirPath) {
    const parts = dirPath.split(path.sep);
    let currentPath = '';

    parts.forEach((part, index) => {
      if (part) {
        currentPath = path.join(currentPath, part);
        const indent = '  '.repeat(index);
        console.log(`${indent}📁 ${part}/`);
      }
    });
  }

  static async createProjectStructure(projectName) {
    const baseDir = path.join(
      __dirname,
      'projects',
      projectName
    );

    const directories = [
      'src/components',
      'src/utils',
      'src/services',
      'tests/unit',
      'tests/integration',
      'docs/api',
      'config/environments',
      'logs/error',
      'logs/access',
      'public/assets/images',
      'public/assets/styles',
      'build/dist',
    ];

    console.log(
      `🚀 プロジェクト "${projectName}" の構造を作成中...`
    );

    const results = [];
    for (const dir of directories) {
      const fullPath = path.join(baseDir, dir);
      try {
        const result = await this.createPath(fullPath, {
          verbose: false,
        });
        results.push({ ...result, relativePath: dir });
        console.log(`  📁 ${dir}`);
      } catch (error) {
        console.error(`  ❌ ${dir}: ${error.message}`);
        results.push({
          created: false,
          path: fullPath,
          error: error.message,
        });
      }
    }

    console.log('✅ プロジェクト構造の作成完了');
    return results;
  }
}

// 使用例
async function demonstrateRecursiveCreation() {
  // 深い階層のディレクトリ作成
  await DirectoryCreator.createPath(
    './data/users/profiles/images'
  );

  // プロジェクト構造の作成
  await DirectoryCreator.createProjectStructure(
    'my-web-app'
  );

  // ドライラン例
  await DirectoryCreator.createPath(
    './test/deep/nested/structure',
    {
      dryRun: true,
    }
  );
}

demonstrateRecursiveCreation();

ディレクトリ権限の設定(mode 指定)

Unix 系システムでは、ディレクトリの権限設定が重要なセキュリティ要素です。

javascript// 権限設定の詳細例
class DirectoryPermissions {
  static getPermissionInfo(mode) {
    const octal = mode.toString(8).slice(-3);
    const permissions = {
      owner: this.parsePermissionBits(parseInt(octal[0])),
      group: this.parsePermissionBits(parseInt(octal[1])),
      others: this.parsePermissionBits(parseInt(octal[2])),
    };

    return {
      octal: octal,
      readable: this.formatPermissions(permissions),
      permissions,
    };
  }

  static parsePermissionBits(bits) {
    return {
      read: !!(bits & 4),
      write: !!(bits & 2),
      execute: !!(bits & 1),
    };
  }

  static formatPermissions(permissions) {
    const format = (perm) =>
      (perm.read ? 'r' : '-') +
      (perm.write ? 'w' : '-') +
      (perm.execute ? 'x' : '-');

    return (
      format(permissions.owner) +
      format(permissions.group) +
      format(permissions.others)
    );
  }

  static async createWithPermissions(
    dirPath,
    mode = 0o755
  ) {
    try {
      await fs.mkdir(dirPath, { mode, recursive: true });

      const stats = await fs.stat(dirPath);
      const permInfo = this.getPermissionInfo(stats.mode);

      console.log(`✅ ディレクトリ作成: ${dirPath}`);
      console.log(
        `   権限: ${permInfo.octal} (${permInfo.readable})`
      );

      return { path: dirPath, permissions: permInfo };
    } catch (error) {
      console.error(
        '❌ 権限付きディレクトリ作成エラー:',
        error.message
      );
      throw error;
    }
  }
}

// 様々な権限設定の例
async function demonstratePermissions() {
  const permissionExamples = [
    {
      path: './secure-data',
      mode: 0o700,
      description: '所有者のみアクセス可能',
    },
    {
      path: './shared-read',
      mode: 0o755,
      description: '所有者は全権限、他は読み取り・実行のみ',
    },
    {
      path: './group-write',
      mode: 0o775,
      description: '所有者とグループは全権限',
    },
    {
      path: './public-read',
      mode: 0o744,
      description: '所有者は全権限、他は読み取りのみ',
    },
  ];

  console.log('🔐 様々な権限設定でディレクトリを作成:\n');

  for (const example of permissionExamples) {
    await DirectoryPermissions.createWithPermissions(
      example.path,
      example.mode
    );
    console.log(`   説明: ${example.description}\n`);
  }
}

demonstratePermissions();

既存ディレクトリの重複処理

実際の開発では、既存のディレクトリが存在する場合の適切な処理が重要です。

javascript// 重複処理の高度な制御
class DirectoryConflictHandler {
  static async createSafely(dirPath, options = {}) {
    const {
      onExist = 'skip', // 'skip', 'error', 'backup', 'overwrite'
      backupSuffix = '.backup',
      mode = 0o755,
      recursive = true,
    } = options;

    try {
      // 既存確認
      const exists = await this.exists(dirPath);

      if (exists) {
        return await this.handleExistingDirectory(
          dirPath,
          onExist,
          backupSuffix
        );
      }

      // 新規作成
      await fs.mkdir(dirPath, { mode, recursive });
      console.log('✅ 新しいディレクトリを作成:', dirPath);

      return {
        created: true,
        action: 'created',
        path: dirPath,
      };
    } catch (error) {
      console.error(
        '❌ 安全なディレクトリ作成エラー:',
        error.message
      );
      throw error;
    }
  }

  static async handleExistingDirectory(
    dirPath,
    action,
    backupSuffix
  ) {
    switch (action) {
      case 'skip':
        console.log(
          '⏭️ 既存のディレクトリをスキップ:',
          dirPath
        );
        return {
          created: false,
          action: 'skipped',
          path: dirPath,
        };

      case 'error':
        throw new Error(
          `ディレクトリが既に存在します: ${dirPath}`
        );

      case 'backup':
        const backupPath = await this.createBackup(
          dirPath,
          backupSuffix
        );
        await fs.rmdir(dirPath, { recursive: true });
        await fs.mkdir(dirPath, { recursive: true });
        console.log(
          '🔄 既存ディレクトリをバックアップして再作成:',
          dirPath
        );
        return {
          created: true,
          action: 'backup-and-recreate',
          path: dirPath,
          backupPath,
        };

      case 'overwrite':
        await fs.rm(dirPath, {
          recursive: true,
          force: true,
        });
        await fs.mkdir(dirPath, { recursive: true });
        console.log(
          '🔄 既存ディレクトリを上書き:',
          dirPath
        );
        return {
          created: true,
          action: 'overwritten',
          path: dirPath,
        };

      default:
        throw new Error(`不正なアクション: ${action}`);
    }
  }

  static async createBackup(dirPath, suffix) {
    const timestamp = new Date()
      .toISOString()
      .replace(/[:.]/g, '-');
    const backupPath = `${dirPath}${suffix}-${timestamp}`;

    await fs.rename(dirPath, backupPath);
    console.log('📦 バックアップを作成:', backupPath);

    return backupPath;
  }

  static async exists(dirPath) {
    try {
      const stats = await fs.stat(dirPath);
      return stats.isDirectory();
    } catch (error) {
      return false;
    }
  }

  static async createMultiple(directories, options = {}) {
    const results = [];

    console.log(
      `📁 ${directories.length} 個のディレクトリを処理中...`
    );

    for (const dir of directories) {
      try {
        const result = await this.createSafely(
          dir,
          options
        );
        results.push(result);
      } catch (error) {
        results.push({
          created: false,
          action: 'error',
          path: dir,
          error: error.message,
        });
      }
    }

    // 結果の統計
    const stats = results.reduce((acc, result) => {
      acc[result.action] = (acc[result.action] || 0) + 1;
      return acc;
    }, {});

    console.log('\n📊 処理結果:');
    Object.entries(stats).forEach(([action, count]) => {
      console.log(`  ${action}: ${count} 個`);
    });

    return results;
  }
}

// 使用例
async function demonstrateConflictHandling() {
  const testDirectories = [
    './test-dirs/new-dir-1',
    './test-dirs/new-dir-2',
    './test-dirs/existing-dir',
    './test-dirs/backup-test',
  ];

  // 一部のディレクトリを事前に作成(テスト用)
  await fs.mkdir('./test-dirs/existing-dir', {
    recursive: true,
  });

  // 様々な重複処理設定でテスト
  console.log('🧪 スキップモードでテスト:');
  await DirectoryConflictHandler.createMultiple(
    testDirectories,
    {
      onExist: 'skip',
    }
  );

  console.log('\n🧪 バックアップモードでテスト:');
  await DirectoryConflictHandler.createMultiple(
    ['./test-dirs/existing-dir'],
    {
      onExist: 'backup',
    }
  );
}

demonstrateConflictHandling();

ディレクトリ作成の基本から応用まで学習したところで、次は削除操作について詳しく見ていきましょう。

ディレクトリの削除

ディレクトリの削除は、データの完全な消失を伴う危険な操作です。適切な安全策と確認機能を実装することで、誤削除を防ぎながら効率的なディレクトリ管理を実現できます。

fs.rmdir() と fs.rm() の違い

Node.js では複数のディレクトリ削除方法が提供されています。用途に応じて適切な方法を選択しましょう。

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

// 削除方法の比較と使い分け
class DirectoryDeletion {
  // 従来のrmdir()メソッド(空のディレクトリのみ)
  static async removeEmptyDirectory(dirPath) {
    try {
      await fs.rmdir(dirPath);
      console.log(
        '✅ 空のディレクトリを削除しました:',
        dirPath
      );
      return { success: true, method: 'rmdir' };
    } catch (error) {
      if (error.code === 'ENOTEMPTY') {
        console.error(
          '❌ ディレクトリが空ではありません:',
          dirPath
        );
        throw new Error(
          'ディレクトリが空ではないため削除できません'
        );
      } else if (error.code === 'ENOENT') {
        console.log(
          '⚠️ 削除対象のディレクトリが存在しません:',
          dirPath
        );
        return {
          success: false,
          method: 'rmdir',
          reason: 'not-found',
        };
      }
      throw error;
    }
  }

  // 新しいrm()メソッド(推奨)
  static async removeDirectory(dirPath, options = {}) {
    const {
      recursive = true,
      force = false,
      maxRetries = 3,
      retryDelay = 100,
    } = options;

    for (
      let attempt = 1;
      attempt <= maxRetries;
      attempt++
    ) {
      try {
        await fs.rm(dirPath, { recursive, force });
        console.log(
          '✅ ディレクトリを削除しました:',
          dirPath
        );
        return { success: true, method: 'rm', attempt };
      } catch (error) {
        if (error.code === 'ENOENT' && force) {
          console.log(
            '⚠️ 削除対象のディレクトリが存在しません:',
            dirPath
          );
          return {
            success: true,
            method: 'rm',
            reason: 'not-found',
          };
        }

        if (attempt < maxRetries) {
          console.log(
            `⏳ 削除に失敗、リトライ中... (${attempt}/${maxRetries})`
          );
          await new Promise((resolve) =>
            setTimeout(resolve, retryDelay)
          );
          continue;
        }

        throw error;
      }
    }
  }

  // 削除方法の比較デモ
  static async demonstrateDeletionMethods() {
    const testDirs = {
      empty: './test-delete/empty-dir',
      withFiles: './test-delete/dir-with-files',
      nested: './test-delete/nested/deep/structure',
    };

    // テスト用ディレクトリ作成
    await fs.mkdir(testDirs.empty, { recursive: true });
    await fs.mkdir(testDirs.withFiles, { recursive: true });
    await fs.mkdir(testDirs.nested, { recursive: true });
    await fs.writeFile(
      path.join(testDirs.withFiles, 'test.txt'),
      'test'
    );

    console.log('📋 削除方法の比較テスト:\n');

    // 空のディレクトリ削除(rmdir)
    console.log('1. 空のディレクトリをrmdirで削除:');
    await this.removeEmptyDirectory(testDirs.empty);

    // ファイル入りディレクトリ削除(rm)
    console.log('\n2. ファイル入りディレクトリをrmで削除:');
    await this.removeDirectory(testDirs.withFiles);

    // ネストしたディレクトリ削除(rm)
    console.log('\n3. ネストしたディレクトリをrmで削除:');
    await this.removeDirectory(testDirs.nested);
  }
}

DirectoryDeletion.demonstrateDeletionMethods();

安全な削除(確認機能付き)

実際のプロジェクトでは、誤削除を防ぐための確認機能が重要です。

javascript// 安全な削除システム
class SafeDirectoryDeletion {
  static async deleteWithConfirmation(
    dirPath,
    options = {}
  ) {
    const {
      interactive = true,
      dryRun = false,
      requireConfirmation = true,
      backupBeforeDelete = false,
    } = options;

    // 事前チェック
    const analysis = await this.analyzeDirectory(dirPath);

    if (!analysis.exists) {
      console.log(
        '⚠️ 削除対象のディレクトリが存在しません:',
        dirPath
      );
      return { deleted: false, reason: 'not-found' };
    }

    console.log('🔍 削除対象の分析結果:');
    console.log(`   パス: ${analysis.path}`);
    console.log(`   ファイル数: ${analysis.fileCount}`);
    console.log(
      `   ディレクトリ数: ${analysis.directoryCount}`
    );
    console.log(`   総サイズ: ${analysis.totalSize} bytes`);
    console.log(`   最終更新: ${analysis.lastModified}`);

    if (dryRun) {
      console.log(
        '🔍 ドライラン: 実際の削除は実行されません'
      );
      return {
        deleted: false,
        reason: 'dry-run',
        analysis,
      };
    }

    // 危険レベルの評価
    const riskLevel = this.assessRiskLevel(analysis);
    console.log(
      `⚠️ 削除リスクレベル: ${riskLevel.level} (${riskLevel.description})`
    );

    // 確認が必要な場合
    if (requireConfirmation && riskLevel.score >= 3) {
      const confirmed = await this.getConfirmation(
        analysis,
        riskLevel
      );
      if (!confirmed) {
        console.log(
          '❌ ユーザーにより削除がキャンセルされました'
        );
        return { deleted: false, reason: 'user-cancelled' };
      }
    }

    // バックアップ作成
    let backupPath = null;
    if (backupBeforeDelete) {
      backupPath = await this.createBackup(dirPath);
    }

    // 実際の削除実行
    try {
      await fs.rm(dirPath, {
        recursive: true,
        force: true,
      });
      console.log(
        '✅ ディレクトリの削除が完了しました:',
        dirPath
      );

      return {
        deleted: true,
        analysis,
        riskLevel,
        backupPath,
      };
    } catch (error) {
      console.error(
        '❌ 削除中にエラーが発生:',
        error.message
      );
      throw error;
    }
  }

  static async analyzeDirectory(dirPath) {
    try {
      const stats = await fs.stat(dirPath);

      if (!stats.isDirectory()) {
        throw new Error(
          '指定されたパスはディレクトリではありません'
        );
      }

      const analysis = {
        exists: true,
        path: dirPath,
        fileCount: 0,
        directoryCount: 0,
        totalSize: 0,
        lastModified: stats.mtime,
        created: stats.birthtime,
      };

      await this.scanDirectoryRecursive(dirPath, analysis);

      return analysis;
    } catch (error) {
      if (error.code === 'ENOENT') {
        return { exists: false, path: dirPath };
      }
      throw error;
    }
  }

  static async scanDirectoryRecursive(dirPath, analysis) {
    const items = await fs.readdir(dirPath);

    for (const item of items) {
      const itemPath = path.join(dirPath, item);

      try {
        const stats = await fs.stat(itemPath);

        if (stats.isDirectory()) {
          analysis.directoryCount++;
          await this.scanDirectoryRecursive(
            itemPath,
            analysis
          );
        } else {
          analysis.fileCount++;
          analysis.totalSize += stats.size;
        }

        // 最新の更新日時を追跡
        if (stats.mtime > analysis.lastModified) {
          analysis.lastModified = stats.mtime;
        }
      } catch (error) {
        console.warn(`⚠️ 項目の分析に失敗: ${itemPath}`);
      }
    }
  }

  static assessRiskLevel(analysis) {
    let score = 0;
    const factors = [];

    // ファイル数による評価
    if (analysis.fileCount > 100) {
      score += 2;
      factors.push('大量のファイル');
    } else if (analysis.fileCount > 10) {
      score += 1;
      factors.push('多数のファイル');
    }

    // サイズによる評価
    const sizeInMB = analysis.totalSize / (1024 * 1024);
    if (sizeInMB > 100) {
      score += 2;
      factors.push('大容量データ');
    } else if (sizeInMB > 10) {
      score += 1;
      factors.push('中容量データ');
    }

    // 更新日時による評価
    const daysSinceModified =
      (Date.now() - analysis.lastModified) /
      (1000 * 60 * 60 * 24);
    if (daysSinceModified < 1) {
      score += 2;
      factors.push('最近更新されたデータ');
    } else if (daysSinceModified < 7) {
      score += 1;
      factors.push('最近のデータ');
    }

    const levels = {
      0: { level: '低', description: '安全に削除可能' },
      1: { level: '低', description: '安全に削除可能' },
      2: { level: '中', description: '注意が必要' },
      3: { level: '中', description: '注意が必要' },
      4: { level: '高', description: '慎重な確認が必要' },
      5: { level: '高', description: '慎重な確認が必要' },
      6: {
        level: '非常に高',
        description: '極めて慎重な確認が必要',
      },
    };

    const riskLevel = levels[Math.min(score, 6)];

    return {
      score,
      ...riskLevel,
      factors,
    };
  }

  static async getConfirmation(analysis, riskLevel) {
    console.log('\n⚠️ 削除の確認:');
    console.log(`削除対象: ${analysis.path}`);
    console.log(
      `リスク要因: ${riskLevel.factors.join(', ')}`
    );
    console.log('削除を実行しますか? (yes/no)');

    // 実際の実装では、readline や inquirer などを使用
    // ここではシミュレーション
    const responses = ['yes', 'no'];
    const response =
      responses[
        Math.floor(Math.random() * responses.length)
      ];

    console.log(`ユーザー入力: ${response}`);
    return response.toLowerCase() === 'yes';
  }

  static async createBackup(dirPath) {
    const timestamp = new Date()
      .toISOString()
      .replace(/[:.]/g, '-');
    const backupPath = `${dirPath}.backup-${timestamp}`;

    // 実際のバックアップ処理(コピー)
    await this.copyDirectory(dirPath, backupPath);
    console.log(
      '📦 バックアップを作成しました:',
      backupPath
    );

    return backupPath;
  }

  static async copyDirectory(src, dest) {
    await fs.mkdir(dest, { recursive: true });
    const items = await fs.readdir(src);

    for (const item of items) {
      const srcPath = path.join(src, item);
      const destPath = path.join(dest, item);

      const stats = await fs.stat(srcPath);

      if (stats.isDirectory()) {
        await this.copyDirectory(srcPath, destPath);
      } else {
        await fs.copyFile(srcPath, destPath);
      }
    }
  }
}

// 使用例
async function demonstrateSafeDeletion() {
  // テスト用ディレクトリの作成
  const testDir = './safe-delete-test';
  await fs.mkdir(testDir, { recursive: true });

  // テストファイルの作成
  for (let i = 0; i < 5; i++) {
    await fs.writeFile(
      path.join(testDir, `file${i}.txt`),
      `テストファイル ${i} の内容`
    );
  }

  // 安全な削除のテスト
  await SafeDirectoryDeletion.deleteWithConfirmation(
    testDir,
    {
      dryRun: true,
      backupBeforeDelete: true,
    }
  );
}

demonstrateSafeDeletion();

ディレクトリの一覧取得

ディレクトリの内容を取得することで、ファイル管理システムやバックアップツールなど、様々なアプリケーションを構築できます。

fs.readdir() による基本的な一覧取得

javascript// 基本的なディレクトリ一覧取得
class DirectoryListing {
  static async getBasicListing(dirPath) {
    try {
      const items = await fs.readdir(dirPath);

      console.log(`📁 ディレクトリ内容: ${dirPath}`);
      console.log(`項目数: ${items.length}`);

      items.forEach((item, index) => {
        console.log(`  ${index + 1}. ${item}`);
      });

      return items;
    } catch (error) {
      console.error(
        '❌ ディレクトリ読み取りエラー:',
        error.message
      );
      throw error;
    }
  }

  static async getListingWithEncoding(
    dirPath,
    encoding = 'utf8'
  ) {
    try {
      // エンコーディングを指定した読み取り
      const items = await fs.readdir(dirPath, { encoding });

      console.log(
        `📁 ディレクトリ内容 (${encoding}): ${dirPath}`
      );

      return items;
    } catch (error) {
      console.error(
        '❌ エンコーディング指定読み取りエラー:',
        error.message
      );
      throw error;
    }
  }

  static async getBufferListing(dirPath) {
    try {
      // バッファとして読み取り(バイナリファイル名対応)
      const buffers = await fs.readdir(dirPath, {
        encoding: 'buffer',
      });

      const items = buffers.map((buffer) => ({
        buffer,
        utf8: buffer.toString('utf8'),
        hex: buffer.toString('hex'),
      }));

      console.log(
        `📁 バッファ形式でのディレクトリ内容: ${dirPath}`
      );
      items.forEach((item, index) => {
        console.log(
          `  ${index + 1}. ${item.utf8} (hex: ${item.hex})`
        );
      });

      return items;
    } catch (error) {
      console.error(
        '❌ バッファ読み取りエラー:',
        error.message
      );
      throw error;
    }
  }
}

// 使用例
async function demonstrateBasicListing() {
  const testDir = './test-listing';

  // テスト用ディレクトリとファイルを作成
  await fs.mkdir(testDir, { recursive: true });
  await fs.writeFile(
    path.join(testDir, 'file1.txt'),
    'test'
  );
  await fs.writeFile(
    path.join(testDir, 'file2.json'),
    '{}'
  );
  await fs.mkdir(path.join(testDir, 'subdir'));

  await DirectoryListing.getBasicListing(testDir);
  await DirectoryListing.getListingWithEncoding(testDir);
  await DirectoryListing.getBufferListing(testDir);
}

demonstrateBasicListing();

withFileTypes オプションの活用

withFileTypes オプションを使用することで、より詳細な情報を効率的に取得できます。

javascript// withFileTypesオプションを使った高度な一覧取得
class EnhancedDirectoryListing {
  static async getDetailedListing(dirPath) {
    try {
      const items = await fs.readdir(dirPath, {
        withFileTypes: true,
      });

      const result = {
        path: dirPath,
        files: [],
        directories: [],
        symbolicLinks: [],
        other: [],
        summary: {
          totalItems: items.length,
          fileCount: 0,
          directoryCount: 0,
          symbolicLinkCount: 0,
          otherCount: 0,
        },
      };

      console.log(`📋 詳細ディレクトリ一覧: ${dirPath}\n`);

      for (const item of items) {
        const itemInfo = {
          name: item.name,
          type: this.getItemType(item),
          fullPath: path.join(dirPath, item.name),
        };

        if (item.isFile()) {
          result.files.push(itemInfo);
          result.summary.fileCount++;
          console.log(`📄 ${item.name}`);
        } else if (item.isDirectory()) {
          result.directories.push(itemInfo);
          result.summary.directoryCount++;
          console.log(`📁 ${item.name}/`);
        } else if (item.isSymbolicLink()) {
          result.symbolicLinks.push(itemInfo);
          result.summary.symbolicLinkCount++;
          console.log(`🔗 ${item.name} -> (symlink)`);
        } else {
          result.other.push(itemInfo);
          result.summary.otherCount++;
          console.log(`❓ ${item.name} (${itemInfo.type})`);
        }
      }

      console.log('\n📊 統計:');
      console.log(
        `  ファイル: ${result.summary.fileCount}`
      );
      console.log(
        `  ディレクトリ: ${result.summary.directoryCount}`
      );
      console.log(
        `  シンボリックリンク: ${result.summary.symbolicLinkCount}`
      );
      console.log(`  その他: ${result.summary.otherCount}`);

      return result;
    } catch (error) {
      console.error(
        '❌ 詳細一覧取得エラー:',
        error.message
      );
      throw error;
    }
  }

  static getItemType(dirent) {
    if (dirent.isFile()) return 'file';
    if (dirent.isDirectory()) return 'directory';
    if (dirent.isSymbolicLink()) return 'symbolic-link';
    if (dirent.isBlockDevice()) return 'block-device';
    if (dirent.isCharacterDevice())
      return 'character-device';
    if (dirent.isFIFO()) return 'fifo';
    if (dirent.isSocket()) return 'socket';
    return 'unknown';
  }

  static async getFilteredListing(dirPath, filter) {
    const items = await fs.readdir(dirPath, {
      withFileTypes: true,
    });

    const filtered = items.filter(filter);

    console.log(`📋 フィルター適用後の一覧: ${dirPath}`);
    console.log(
      `全項目: ${items.length}, フィルター後: ${filtered.length}\n`
    );

    filtered.forEach((item) => {
      const type = this.getItemType(item);
      const icon = type === 'directory' ? '📁' : '📄';
      console.log(`${icon} ${item.name}`);
    });

    return filtered;
  }

  static async getSortedListing(dirPath, sortBy = 'name') {
    const items = await fs.readdir(dirPath, {
      withFileTypes: true,
    });

    let sorted;

    switch (sortBy) {
      case 'name':
        sorted = items.sort((a, b) =>
          a.name.localeCompare(b.name)
        );
        break;
      case 'type':
        sorted = items.sort((a, b) => {
          // ディレクトリを先に、その後ファイル
          if (a.isDirectory() && !b.isDirectory())
            return -1;
          if (!a.isDirectory() && b.isDirectory()) return 1;
          return a.name.localeCompare(b.name);
        });
        break;
      case 'extension':
        sorted = items.sort((a, b) => {
          const extA = path.extname(a.name).toLowerCase();
          const extB = path.extname(b.name).toLowerCase();
          return (
            extA.localeCompare(extB) ||
            a.name.localeCompare(b.name)
          );
        });
        break;
      default:
        sorted = items;
    }

    console.log(
      `📋 ソート済み一覧 (${sortBy}): ${dirPath}\n`
    );

    sorted.forEach((item, index) => {
      const type = this.getItemType(item);
      const icon = type === 'directory' ? '📁' : '📄';
      const ext = path.extname(item.name);
      console.log(
        `${index + 1}. ${icon} ${item.name}${
          ext ? ` (${ext})` : ''
        }`
      );
    });

    return sorted;
  }
}

// 使用例
async function demonstrateEnhancedListing() {
  const testDir = './enhanced-test';

  // 様々なファイルタイプのテストデータ作成
  await fs.mkdir(testDir, { recursive: true });
  await fs.writeFile(
    path.join(testDir, 'document.txt'),
    'text file'
  );
  await fs.writeFile(
    path.join(testDir, 'config.json'),
    '{"test": true}'
  );
  await fs.writeFile(
    path.join(testDir, 'script.js'),
    'console.log("hello");'
  );
  await fs.mkdir(path.join(testDir, 'assets'));
  await fs.mkdir(path.join(testDir, 'components'));

  // 基本的な詳細一覧
  await EnhancedDirectoryListing.getDetailedListing(
    testDir
  );

  console.log('\n' + '='.repeat(50) + '\n');

  // フィルター例: ディレクトリのみ
  await EnhancedDirectoryListing.getFilteredListing(
    testDir,
    (item) => item.isDirectory()
  );

  console.log('\n' + '='.repeat(50) + '\n');

  // フィルター例: JavaScriptファイルのみ
  await EnhancedDirectoryListing.getFilteredListing(
    testDir,
    (item) => item.isFile() && item.name.endsWith('.js')
  );

  console.log('\n' + '='.repeat(50) + '\n');

  // ソート例
  await EnhancedDirectoryListing.getSortedListing(
    testDir,
    'type'
  );
}

demonstrateEnhancedListing();

サイズや更新日時などの詳細情報取得

ディレクトリ一覧の取得と合わせて、各項目の詳細情報を取得することで、より高度なファイル管理機能を実装できます。

javascript// 詳細情報付きディレクトリ一覧
class DetailedDirectoryListing {
  static async getListingWithStats(dirPath) {
    try {
      const items = await fs.readdir(dirPath, {
        withFileTypes: true,
      });
      const results = [];

      console.log(
        `📋 詳細情報付きディレクトリ一覧: ${dirPath}\n`
      );

      for (const item of items) {
        const itemPath = path.join(dirPath, item.name);

        try {
          const stats = await fs.stat(itemPath);
          const itemDetail = {
            name: item.name,
            path: itemPath,
            type: item.isDirectory() ? 'directory' : 'file',
            size: stats.size,
            sizeFormatted: this.formatBytes(stats.size),
            created: stats.birthtime,
            modified: stats.mtime,
            accessed: stats.atime,
            permissions: stats.mode.toString(8),
            isDirectory: item.isDirectory(),
            isFile: item.isFile(),
            extension: path
              .extname(item.name)
              .toLowerCase(),
          };

          results.push(itemDetail);

          // コンソール出力
          const icon = itemDetail.isDirectory ? '📁' : '📄';
          const sizeInfo = itemDetail.isDirectory
            ? ''
            : ` (${itemDetail.sizeFormatted})`;
          console.log(
            `${icon} ${itemDetail.name}${sizeInfo}`
          );
          console.log(
            `   更新: ${itemDetail.modified.toLocaleDateString(
              'ja-JP'
            )} ${itemDetail.modified.toLocaleTimeString(
              'ja-JP'
            )}`
          );
          console.log(`   権限: ${itemDetail.permissions}`);
          console.log('');
        } catch (error) {
          console.warn(
            `⚠️ 詳細情報の取得に失敗: ${item.name}`
          );
          results.push({
            name: item.name,
            path: itemPath,
            type: 'unknown',
            error: error.message,
          });
        }
      }

      return results;
    } catch (error) {
      console.error(
        '❌ 詳細一覧取得エラー:',
        error.message
      );
      throw error;
    }
  }

  static formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';

    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return (
      parseFloat((bytes / Math.pow(k, i)).toFixed(2)) +
      ' ' +
      sizes[i]
    );
  }

  static async generateReport(dirPath) {
    const items = await this.getListingWithStats(dirPath);

    const report = {
      path: dirPath,
      generatedAt: new Date(),
      totalItems: items.length,
      files: items.filter((item) => item.isFile),
      directories: items.filter((item) => item.isDirectory),
      totalSize: items.reduce(
        (sum, item) => sum + (item.size || 0),
        0
      ),
      extensions: {},
      recentlyModified: [],
      largeFiles: [],
    };

    // ファイル拡張子の統計
    report.files.forEach((file) => {
      const ext = file.extension || 'no-extension';
      report.extensions[ext] =
        (report.extensions[ext] || 0) + 1;
    });

    // 最近更新されたファイル(7日以内)
    const sevenDaysAgo = new Date(
      Date.now() - 7 * 24 * 60 * 60 * 1000
    );
    report.recentlyModified = items
      .filter(
        (item) =>
          item.modified && item.modified > sevenDaysAgo
      )
      .sort((a, b) => b.modified - a.modified)
      .slice(0, 10);

    // 大きなファイル(1MB以上)
    report.largeFiles = report.files
      .filter((file) => file.size > 1024 * 1024)
      .sort((a, b) => b.size - a.size)
      .slice(0, 10);

    // レポート出力
    console.log('📊 ディレクトリレポート');
    console.log('='.repeat(50));
    console.log(`パス: ${report.path}`);
    console.log(
      `生成日時: ${report.generatedAt.toLocaleString(
        'ja-JP'
      )}`
    );
    console.log(`総アイテム数: ${report.totalItems}`);
    console.log(`ファイル数: ${report.files.length}`);
    console.log(
      `ディレクトリ数: ${report.directories.length}`
    );
    console.log(
      `総サイズ: ${this.formatBytes(report.totalSize)}`
    );

    console.log('\n📁 ファイル拡張子別統計:');
    Object.entries(report.extensions)
      .sort(([, a], [, b]) => b - a)
      .forEach(([ext, count]) => {
        console.log(`  ${ext}: ${count} ファイル`);
      });

    if (report.recentlyModified.length > 0) {
      console.log('\n🕒 最近更新されたファイル:');
      report.recentlyModified.forEach((item, index) => {
        console.log(
          `  ${index + 1}. ${
            item.name
          } (${item.modified.toLocaleDateString('ja-JP')})`
        );
      });
    }

    if (report.largeFiles.length > 0) {
      console.log('\n📦 大きなファイル:');
      report.largeFiles.forEach((item, index) => {
        console.log(
          `  ${index + 1}. ${item.name} (${
            item.sizeFormatted
          })`
        );
      });
    }

    return report;
  }
}

// 使用例
async function demonstrateDetailedListing() {
  const testDir = './detailed-test';

  // テストデータの作成
  await fs.mkdir(testDir, { recursive: true });
  await fs.writeFile(
    path.join(testDir, 'small.txt'),
    'small file'
  );
  await fs.writeFile(
    path.join(testDir, 'large.txt'),
    'x'.repeat(2 * 1024 * 1024)
  ); // 2MB
  await fs.writeFile(
    path.join(testDir, 'config.json'),
    '{"test": true}'
  );
  await fs.mkdir(path.join(testDir, 'subdir'));

  await DetailedDirectoryListing.generateReport(testDir);
}

demonstrateDetailedListing();

階層構造の探索

複雑なディレクトリ構造を効率的に探索することで、ファイル検索やバックアップシステムなどの高度な機能を実現できます。

再帰的なディレクトリ探索

javascript// 階層構造の探索クラス
class DirectoryTreeExplorer {
  static async exploreRecursively(dirPath, options = {}) {
    const {
      maxDepth = Infinity,
      includeFiles = true,
      includeDirectories = true,
      filter = null,
      currentDepth = 0,
    } = options;

    if (currentDepth >= maxDepth) {
      return {
        path: dirPath,
        depth: currentDepth,
        items: [],
        truncated: true,
      };
    }

    try {
      const items = await fs.readdir(dirPath, {
        withFileTypes: true,
      });
      const result = {
        path: dirPath,
        depth: currentDepth,
        items: [],
        stats: {
          totalFiles: 0,
          totalDirectories: 0,
          totalSize: 0,
        },
      };

      for (const item of items) {
        const itemPath = path.join(dirPath, item.name);

        try {
          const stats = await fs.stat(itemPath);
          const itemInfo = {
            name: item.name,
            path: itemPath,
            relativePath: path.relative(
              process.cwd(),
              itemPath
            ),
            type: item.isDirectory() ? 'directory' : 'file',
            depth: currentDepth + 1,
            size: stats.size,
            modified: stats.mtime,
            isDirectory: item.isDirectory(),
            isFile: item.isFile(),
          };

          // フィルター適用
          if (filter && !filter(itemInfo)) {
            continue;
          }

          if (item.isDirectory()) {
            result.stats.totalDirectories++;

            if (includeDirectories) {
              // 再帰的に探索
              const subResult =
                await this.exploreRecursively(itemPath, {
                  ...options,
                  currentDepth: currentDepth + 1,
                });

              itemInfo.children = subResult.items;
              itemInfo.stats = subResult.stats;

              // 統計の集計
              result.stats.totalFiles +=
                subResult.stats.totalFiles;
              result.stats.totalDirectories +=
                subResult.stats.totalDirectories;
              result.stats.totalSize +=
                subResult.stats.totalSize;

              result.items.push(itemInfo);
            }
          } else {
            result.stats.totalFiles++;
            result.stats.totalSize += stats.size;

            if (includeFiles) {
              result.items.push(itemInfo);
            }
          }
        } catch (error) {
          console.warn(`⚠️ 項目の分析に失敗: ${itemPath}`);
        }
      }

      return result;
    } catch (error) {
      console.error(
        `❌ ディレクトリ探索エラー: ${dirPath} - ${error.message}`
      );
      throw error;
    }
  }

  static async findFiles(rootPath, searchOptions = {}) {
    const {
      pattern = null,
      extension = null,
      minSize = 0,
      maxSize = Infinity,
      modifiedAfter = null,
      modifiedBefore = null,
      maxResults = Infinity,
    } = searchOptions;

    const results = [];

    const filter = (item) => {
      if (item.isDirectory) return true; // ディレクトリは継続探索のため通す

      // ファイルのみの条件チェック
      if (item.isFile) {
        // パターンマッチング
        if (
          pattern &&
          !item.name.match(new RegExp(pattern, 'i'))
        ) {
          return false;
        }

        // 拡張子チェック
        if (
          extension &&
          !item.name
            .toLowerCase()
            .endsWith(extension.toLowerCase())
        ) {
          return false;
        }

        // サイズチェック
        if (item.size < minSize || item.size > maxSize) {
          return false;
        }

        // 更新日時チェック
        if (
          modifiedAfter &&
          item.modified < modifiedAfter
        ) {
          return false;
        }

        if (
          modifiedBefore &&
          item.modified > modifiedBefore
        ) {
          return false;
        }

        return true;
      }

      return false;
    };

    const collectFiles = (node) => {
      if (node.isFile && filter(node)) {
        results.push(node);
        if (results.length >= maxResults) {
          return false; // 探索停止
        }
      }

      if (node.children) {
        for (const child of node.children) {
          if (collectFiles(child) === false) {
            return false;
          }
        }
      }

      return true;
    };

    const tree = await this.exploreRecursively(rootPath, {
      includeFiles: true,
      includeDirectories: true,
    });

    collectFiles(tree);

    console.log(
      `🔍 ファイル検索結果: ${results.length} 件`
    );
    results.forEach((file, index) => {
      console.log(
        `  ${index + 1}. ${
          file.relativePath
        } (${this.formatBytes(file.size)})`
      );
    });

    return results;
  }

  static async visualizeTree(dirPath, options = {}) {
    const {
      maxDepth = 3,
      showFiles = true,
      showSize = true,
      useUnicode = true,
    } = options;

    const tree = await this.exploreRecursively(dirPath, {
      maxDepth,
      includeFiles: showFiles,
    });

    console.log(`🌳 ディレクトリツリー: ${dirPath}\n`);

    const symbols = useUnicode
      ? {
          branch: '├── ',
          lastBranch: '└── ',
          pipe: '│   ',
          space: '    ',
        }
      : {
          branch: '|-- ',
          lastBranch: '`-- ',
          pipe: '|   ',
          space: '    ',
        };

    const renderNode = (
      node,
      prefix = '',
      isLast = true
    ) => {
      const symbol = isLast
        ? symbols.lastBranch
        : symbols.branch;
      const icon = node.isDirectory ? '📁' : '📄';
      const sizeInfo =
        showSize && !node.isDirectory
          ? ` (${this.formatBytes(node.size)})`
          : '';

      console.log(
        `${prefix}${symbol}${icon} ${node.name}${sizeInfo}`
      );

      if (node.children && node.children.length > 0) {
        const childPrefix =
          prefix + (isLast ? symbols.space : symbols.pipe);

        node.children.forEach((child, index) => {
          const isLastChild =
            index === node.children.length - 1;
          renderNode(child, childPrefix, isLastChild);
        });
      }
    };

    renderNode(tree, '', true);

    console.log(`\n📊 統計:`);
    console.log(`  ファイル数: ${tree.stats.totalFiles}`);
    console.log(
      `  ディレクトリ数: ${tree.stats.totalDirectories}`
    );
    console.log(
      `  総サイズ: ${this.formatBytes(
        tree.stats.totalSize
      )}`
    );

    return tree;
  }

  static formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (
      parseFloat((bytes / Math.pow(k, i)).toFixed(2)) +
      ' ' +
      sizes[i]
    );
  }
}

// 使用例
async function demonstrateTreeExploration() {
  const testDir = './tree-test';

  // 階層テストデータの作成
  await fs.mkdir(path.join(testDir, 'src/components'), {
    recursive: true,
  });
  await fs.mkdir(path.join(testDir, 'src/utils'), {
    recursive: true,
  });
  await fs.mkdir(path.join(testDir, 'docs'), {
    recursive: true,
  });

  await fs.writeFile(
    path.join(testDir, 'package.json'),
    '{}'
  );
  await fs.writeFile(
    path.join(testDir, 'README.md'),
    '# Test Project'
  );
  await fs.writeFile(
    path.join(testDir, 'src/index.js'),
    'console.log("main");'
  );
  await fs.writeFile(
    path.join(testDir, 'src/components/Button.js'),
    'export default Button;'
  );
  await fs.writeFile(
    path.join(testDir, 'docs/api.md'),
    '# API Documentation'
  );

  // ツリー表示
  await DirectoryTreeExplorer.visualizeTree(testDir);

  console.log('\n' + '='.repeat(50) + '\n');

  // JavaScript ファイルの検索
  await DirectoryTreeExplorer.findFiles(testDir, {
    extension: '.js',
    pattern: '.*',
  });

  console.log('\n' + '='.repeat(50) + '\n');

  // マークダウンファイルの検索
  await DirectoryTreeExplorer.findFiles(testDir, {
    extension: '.md',
  });
}

demonstrateTreeExploration();

特定条件での検索・フィルタリング

javascript// 高度な検索・フィルタリング機能
class AdvancedDirectorySearch {
  static async searchWithConditions(rootPath, conditions) {
    const results = {
      found: [],
      searched: 0,
      errors: [],
    };

    const searchRecursively = async (
      currentPath,
      depth = 0
    ) => {
      try {
        const items = await fs.readdir(currentPath, {
          withFileTypes: true,
        });
        results.searched += items.length;

        for (const item of items) {
          const itemPath = path.join(
            currentPath,
            item.name
          );

          try {
            const stats = await fs.stat(itemPath);
            const itemData = {
              name: item.name,
              path: itemPath,
              relativePath: path.relative(
                rootPath,
                itemPath
              ),
              type: item.isDirectory()
                ? 'directory'
                : 'file',
              size: stats.size,
              created: stats.birthtime,
              modified: stats.mtime,
              accessed: stats.atime,
              extension: path
                .extname(item.name)
                .toLowerCase(),
              depth,
              isDirectory: item.isDirectory(),
              isFile: item.isFile(),
            };

            // 条件チェック
            if (
              this.matchesConditions(itemData, conditions)
            ) {
              results.found.push(itemData);
            }

            // ディレクトリの場合は再帰探索
            if (
              item.isDirectory() &&
              (!conditions.maxDepth ||
                depth < conditions.maxDepth)
            ) {
              await searchRecursively(itemPath, depth + 1);
            }
          } catch (error) {
            results.errors.push({
              path: itemPath,
              error: error.message,
            });
          }
        }
      } catch (error) {
        results.errors.push({
          path: currentPath,
          error: error.message,
        });
      }
    };

    await searchRecursively(rootPath);
    return results;
  }

  static matchesConditions(item, conditions) {
    // 名前パターン
    if (
      conditions.namePattern &&
      !item.name.match(
        new RegExp(conditions.namePattern, 'i')
      )
    ) {
      return false;
    }

    // ファイルタイプ
    if (conditions.type && item.type !== conditions.type) {
      return false;
    }

    // 拡張子
    if (
      conditions.extensions &&
      !conditions.extensions.includes(item.extension)
    ) {
      return false;
    }

    // サイズ範囲
    if (
      conditions.minSize &&
      item.size < conditions.minSize
    ) {
      return false;
    }
    if (
      conditions.maxSize &&
      item.size > conditions.maxSize
    ) {
      return false;
    }

    // 日付範囲
    if (
      conditions.modifiedAfter &&
      item.modified < conditions.modifiedAfter
    ) {
      return false;
    }
    if (
      conditions.modifiedBefore &&
      item.modified > conditions.modifiedBefore
    ) {
      return false;
    }

    // 深さ
    if (
      conditions.maxDepth &&
      item.depth > conditions.maxDepth
    ) {
      return false;
    }

    // カスタムフィルター
    if (
      conditions.customFilter &&
      !conditions.customFilter(item)
    ) {
      return false;
    }

    return true;
  }

  static async createSearchIndex(rootPath) {
    console.log('📚 検索インデックスを作成中...');

    const index = {
      created: new Date(),
      rootPath,
      files: new Map(),
      directories: new Map(),
      extensions: new Map(),
      sizeDistribution: {
        small: 0, // < 1KB
        medium: 0, // 1KB - 1MB
        large: 0, // 1MB - 100MB
        huge: 0, // > 100MB
      },
    };

    const results = await this.searchWithConditions(
      rootPath,
      {}
    );

    results.found.forEach((item) => {
      if (item.isFile) {
        index.files.set(item.path, item);

        // 拡張子別インデックス
        if (!index.extensions.has(item.extension)) {
          index.extensions.set(item.extension, []);
        }
        index.extensions
          .get(item.extension)
          .push(item.path);

        // サイズ分布
        if (item.size < 1024) {
          index.sizeDistribution.small++;
        } else if (item.size < 1024 * 1024) {
          index.sizeDistribution.medium++;
        } else if (item.size < 100 * 1024 * 1024) {
          index.sizeDistribution.large++;
        } else {
          index.sizeDistribution.huge++;
        }
      } else {
        index.directories.set(item.path, item);
      }
    });

    console.log('✅ インデックス作成完了');
    console.log(`   ファイル数: ${index.files.size}`);
    console.log(
      `   ディレクトリ数: ${index.directories.size}`
    );
    console.log(`   拡張子種類: ${index.extensions.size}`);

    return index;
  }

  static async duplicateFileFinder(rootPath) {
    console.log('🔍 重複ファイルを検索中...');

    const fileHashes = new Map();
    const duplicates = [];

    const results = await this.searchWithConditions(
      rootPath,
      { type: 'file' }
    );

    // ファイルサイズでグループ化(高速化のため)
    const sizeGroups = new Map();
    results.found.forEach((file) => {
      if (!sizeGroups.has(file.size)) {
        sizeGroups.set(file.size, []);
      }
      sizeGroups.get(file.size).push(file);
    });

    // 同じサイズのファイルのみハッシュ計算
    for (const [size, files] of sizeGroups) {
      if (files.length > 1) {
        for (const file of files) {
          try {
            const content = await fs.readFile(file.path);
            const hash = require('crypto')
              .createHash('md5')
              .update(content)
              .digest('hex');

            if (!fileHashes.has(hash)) {
              fileHashes.set(hash, []);
            }
            fileHashes.get(hash).push(file);
          } catch (error) {
            console.warn(
              `⚠️ ハッシュ計算失敗: ${file.path}`
            );
          }
        }
      }
    }

    // 重複の特定
    for (const [hash, files] of fileHashes) {
      if (files.length > 1) {
        duplicates.push({
          hash,
          files,
          size: files[0].size,
          count: files.length,
        });
      }
    }

    console.log(
      `🔍 重複ファイル検索結果: ${duplicates.length} グループ`
    );
    duplicates.forEach((group, index) => {
      console.log(
        `\n${index + 1}. 重複グループ (${
          group.count
        } ファイル, ${this.formatBytes(group.size)}):`
      );
      group.files.forEach((file) => {
        console.log(`   📄 ${file.relativePath}`);
      });
    });

    return duplicates;
  }

  static formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (
      parseFloat((bytes / Math.pow(k, i)).toFixed(2)) +
      ' ' +
      sizes[i]
    );
  }
}

// 使用例
async function demonstrateAdvancedSearch() {
  const testDir = './search-test';

  // テストデータ作成
  await fs.mkdir(path.join(testDir, 'images'), {
    recursive: true,
  });
  await fs.mkdir(path.join(testDir, 'documents'), {
    recursive: true,
  });

  await fs.writeFile(
    path.join(testDir, 'large-file.txt'),
    'x'.repeat(2 * 1024 * 1024)
  );
  await fs.writeFile(
    path.join(testDir, 'small-file.txt'),
    'small'
  );
  await fs.writeFile(
    path.join(testDir, 'config.json'),
    '{"test": true}'
  );
  await fs.writeFile(
    path.join(testDir, 'documents/report.pdf'),
    'fake pdf content'
  );
  await fs.writeFile(
    path.join(testDir, 'images/photo.jpg'),
    'fake image data'
  );

  // 重複ファイル作成
  await fs.copyFile(
    path.join(testDir, 'small-file.txt'),
    path.join(testDir, 'small-file-copy.txt')
  );

  // 検索例: 1MB以上のファイル
  console.log('🔍 1MB以上のファイルを検索:');
  const largeFiles =
    await AdvancedDirectorySearch.searchWithConditions(
      testDir,
      {
        type: 'file',
        minSize: 1024 * 1024,
      }
    );

  largeFiles.found.forEach((file) => {
    console.log(
      `  📄 ${
        file.relativePath
      } (${AdvancedDirectorySearch.formatBytes(file.size)})`
    );
  });

  console.log('\n' + '='.repeat(50) + '\n');

  // 重複ファイル検索
  await AdvancedDirectorySearch.duplicateFileFinder(
    testDir
  );
}

demonstrateAdvancedSearch();

まとめ

Node.js でのディレクトリ操作について、基本的な概念から実用的な活用方法まで包括的に解説いたしました。

学習した内容の振り返り

基本操作の習得

  • fs.mkdir() による柔軟なディレクトリ作成
  • 再帰的作成(recursive: true)の活用
  • 権限設定(mode)による セキュリティ確保
  • fs.rm()fs.rmdir() の使い分け
  • 安全な削除のための確認システム実装

一覧取得の高度な活用

  • fs.readdir() の基本的な使用方法
  • withFileTypes オプションによる効率的な情報取得
  • フィルタリングとソート機能の実装
  • 詳細情報(サイズ、更新日時、権限)の取得

階層構造の探索

  • 再帰的なディレクトリ探索アルゴリズム
  • 深さ制限付きの安全な探索
  • 条件指定による高度な検索機能
  • ツリー表示とビジュアライゼーション

実際の開発での活用ポイント

パフォーマンス最適化

  • 同期処理と非同期処理の適切な選択
  • 大量のファイル処理時のメモリ管理
  • エラーハンドリングとリトライ機能の実装

セキュリティ考慮

  • パストラバーサル攻撃の防止
  • 適切な権限設定による不正アクセス防止
  • 入力値の検証とサニタイゼーション

運用・保守性

  • 豊富なログ出力による操作の可視化
  • ドライラン機能による安全な検証
  • バックアップ機能による データ保護

今後の発展への道筋

本記事で学習したディレクトリ操作の知識により、以下のような高度なアプリケーション開発に挑戦できるでしょう:

  • ファイル管理システム: 大規模なファイル群の効率的な管理
  • バックアップツール: 増分バックアップや重複除去機能
  • ビルドシステム: プロジェクト構造の自動生成と管理
  • ログ解析ツール: 階層化されたログファイルの解析
  • 開発ツール: プロジェクトテンプレートの生成と配布

ディレクトリ操作は、ファイルシステムを扱うあらゆるアプリケーションの基盤となる重要な技術です。本記事の内容を実際のプロジェクトで活用し、より洗練されたソリューションを構築していただければ幸いです。

関連リンク