File size: 3,743 Bytes
8d28c3c
 
681ede6
230b1a5
681ede6
230b1a5
 
 
 
 
560aacd
 
230b1a5
560aacd
681ede6
560aacd
 
681ede6
560aacd
 
 
681ede6
560aacd
681ede6
 
230b1a5
 
8d28c3c
 
 
b212889
681ede6
cefab8e
b212889
230b1a5
b212889
681ede6
8d28c3c
 
681ede6
230b1a5
8d28c3c
681ede6
 
b212889
681ede6
8d28c3c
b212889
 
681ede6
 
 
 
 
 
8d28c3c
 
 
 
b212889
681ede6
 
 
 
 
8d28c3c
1820fc3
 
 
 
8490a22
681ede6
 
 
1820fc3
681ede6
1820fc3
8d28c3c
 
 
1820fc3
681ede6
 
 
1820fc3
 
 
681ede6
 
 
 
 
 
 
 
 
 
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))