File size: 3,743 Bytes
e50fc98
 
 
227e75d
e50fc98
227e75d
 
 
 
 
5a2169d
 
227e75d
5a2169d
e50fc98
5a2169d
 
e50fc98
5a2169d
 
 
e50fc98
5a2169d
e50fc98
 
227e75d
 
e50fc98
 
 
227e75d
e50fc98
 
227e75d
 
e50fc98
 
 
 
 
227e75d
e50fc98
 
 
227e75d
e50fc98
 
227e75d
 
e50fc98
 
 
 
 
 
 
 
 
 
227e75d
e50fc98
 
 
 
 
 
227e75d
e50fc98
 
227e75d
e50fc98
227e75d
e50fc98
 
 
 
227e75d
e50fc98
 
 
227e75d
e50fc98
 
 
 
227e75d
e50fc98
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# core/file_scanner.py

import chardet
from pathlib import Path
from typing import List, Optional, Set
from dataclasses import dataclass

@dataclass
class FileInfo:
    path: Path
    size: int
    extension: str
    content: Optional[str] = None
    encoding: Optional[str] = None

    @property
    def formatted_size(self) -> str:
        """ファイルサイズを見やすい単位で表示"""
        if self.size < 1024:
            return f"{self.size} B"
        elif self.size < 1024 * 1024:
            return f"{self.size / 1024:.1f} KB"
        else:
            return f"{self.size / (1024 * 1024):.1f} MB"


class FileScanner:
    """
    指定された拡張子のファイルだけを再帰的に検索し、ファイル内容を読み込むクラス。
    """
    EXCLUDED_DIRS = {
        '.git', '__pycache__', 'node_modules', 'venv',
        '.env', 'build', 'dist', 'target', 'bin', 'obj'
    }
    
    def __init__(self, base_dir: Path, target_extensions: Set[str]):
        """
        base_dir: 解析を開始するディレクトリ(Path)
        target_extensions: 対象とする拡張子の集合 (例: {'.py', '.js', '.md'})
        """
        self.base_dir = base_dir
        # 大文字・小文字のブレを吸収するために小文字化して保持
        self.target_extensions = {ext.lower() for ext in target_extensions}

    def _should_scan_file(self, path: Path) -> bool:
        """対象外フォルダ・拡張子を除外"""
        # 除外フォルダ判定
        if any(excluded in path.parts for excluded in self.EXCLUDED_DIRS):
            return False
        # 拡張子チェック
        if path.suffix.lower() in self.target_extensions:
            return True
        return False

    def _read_file_content(self, file_path: Path) -> (Optional[str], Optional[str]):
        """
        ファイル内容を読み込み、エンコーディングを判定して返す。
        先頭4096バイトをchardetで解析し、失敗時はcp932も試す。
        """
        try:
            with file_path.open('rb') as rb:
                raw_data = rb.read(4096)
                detect_result = chardet.detect(raw_data)
                encoding = detect_result['encoding'] if detect_result['confidence'] > 0.7 else 'utf-8'
            
            # 推定エンコーディングで読み込み
            try:
                with file_path.open('r', encoding=encoding) as f:
                    return f.read(), encoding
            except UnicodeDecodeError:
                # cp932 を再試行 (Windows向け)
                with file_path.open('r', encoding='cp932') as f:
                    return f.read(), 'cp932'
        except Exception:
            return None, None

    def scan_files(self) -> List[FileInfo]:
        """
        再帰的にファイルを探して、指定拡張子だけをFileInfoオブジェクトのリストとして返す。
        """
        if not self.base_dir.exists():
            raise FileNotFoundError(f"指定ディレクトリが見つかりません: {self.base_dir}")

        collected_files = []
        for entry in self.base_dir.glob("**/*"):
            if entry.is_file() and self._should_scan_file(entry):
                content, encoding = self._read_file_content(entry)
                file_info = FileInfo(
                    path=entry.resolve(),
                    size=entry.stat().st_size,
                    extension=entry.suffix.lower(),
                    content=content,
                    encoding=encoding
                )
                collected_files.append(file_info)
        # path の文字列表現でソート
        return sorted(collected_files, key=lambda x: str(x.path))