首次代码提交
This commit is contained in:
+489
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
文件扫描模块
|
||||
提供目录大小扫描、文件列表等功能
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from typing import List, Dict, Optional, Callable, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from utils import format_size, is_virtual_filesystem, run_command
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileItem:
|
||||
"""文件/目录信息"""
|
||||
name: str # 名称
|
||||
path: str # 完整路径
|
||||
is_dir: bool # 是否为目录
|
||||
size: int # 大小(字节)
|
||||
modified_time: datetime # 修改时间
|
||||
permissions: str # 权限字符串
|
||||
owner: str # 所有者
|
||||
group: str # 所属组
|
||||
|
||||
@property
|
||||
def size_formatted(self) -> str:
|
||||
return format_size(self.size)
|
||||
|
||||
@property
|
||||
def modified_time_formatted(self) -> str:
|
||||
return self.modified_time.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
if self.is_dir:
|
||||
return "📁"
|
||||
elif self.name.endswith(('.log', '.log.gz', '.log.1', '.log.2')):
|
||||
return "📋"
|
||||
elif self.name.endswith(('.gz', '.tar', '.zip', '.7z', '.rar')):
|
||||
return "📦"
|
||||
elif self.name.endswith(('.conf', '.cfg', '.ini', '.yaml', '.yml', '.json')):
|
||||
return "⚙️"
|
||||
elif self.name.endswith(('.sh', '.bash', '.py', '.pl')):
|
||||
return "📜"
|
||||
elif self.name.endswith(('.txt', '.md', '.rst')):
|
||||
return "📄"
|
||||
else:
|
||||
return "📄"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanProgress:
|
||||
"""扫描进度信息"""
|
||||
current_path: str = ""
|
||||
scanned_dirs: int = 0
|
||||
scanned_files: int = 0
|
||||
total_size: int = 0
|
||||
elapsed_time: float = 0.0
|
||||
is_complete: bool = False
|
||||
|
||||
|
||||
class FileScanner:
|
||||
"""文件扫描器"""
|
||||
|
||||
# 虚拟文件系统目录列表
|
||||
VIRTUAL_DIRS = [
|
||||
'/proc',
|
||||
'/sys',
|
||||
'/dev',
|
||||
'/run',
|
||||
'/var/run',
|
||||
'/var/lock',
|
||||
'/tmp',
|
||||
]
|
||||
|
||||
# 系统日志文件模式(不包含子目录中的日志)
|
||||
SYSTEM_LOG_PATTERNS = [
|
||||
'*.log',
|
||||
'*.log.*',
|
||||
'*.gz',
|
||||
'syslog*',
|
||||
'messages*',
|
||||
'dmesg*',
|
||||
'kern.log*',
|
||||
'auth.log*',
|
||||
'daemon.log*',
|
||||
'debug*',
|
||||
'boot.log*',
|
||||
'faillog',
|
||||
'lastlog',
|
||||
'wtmp',
|
||||
'btmp',
|
||||
'utmp',
|
||||
]
|
||||
|
||||
def __init__(self, sudo_password: Optional[str] = None):
|
||||
self.sudo_password = sudo_password
|
||||
self._cancel_scan = False
|
||||
|
||||
def scan_directory_sizes(
|
||||
self,
|
||||
path: str,
|
||||
skip_virtual: bool = True,
|
||||
callback: Optional[Callable[[ScanProgress], None]] = None
|
||||
) -> Dict[str, int]:
|
||||
"""
|
||||
扫描目录下每个子目录的大小
|
||||
|
||||
参数:
|
||||
path: 要扫描的目录路径
|
||||
skip_virtual: 是否跳过虚拟文件系统目录
|
||||
callback: 进度回调函数
|
||||
|
||||
返回:
|
||||
{目录路径: 大小} 字典
|
||||
"""
|
||||
self._cancel_scan = False
|
||||
dir_sizes: Dict[str, int] = {}
|
||||
progress = ScanProgress()
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 获取顶层子目录
|
||||
entries = self._list_directory(path)
|
||||
|
||||
for entry in entries:
|
||||
if self._cancel_scan:
|
||||
break
|
||||
|
||||
entry_path = os.path.join(path, entry)
|
||||
|
||||
# 跳过虚拟文件系统
|
||||
if skip_virtual and self._is_virtual_dir(entry_path):
|
||||
continue
|
||||
|
||||
# 跳过其他挂载点
|
||||
if self._is_other_mountpoint(entry_path, path):
|
||||
continue
|
||||
|
||||
# 计算目录大小
|
||||
size = self._get_directory_size(entry_path, progress, callback)
|
||||
dir_sizes[entry_path] = size
|
||||
progress.total_size += size
|
||||
|
||||
except PermissionError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"扫描目录时发生错误: {e}")
|
||||
|
||||
progress.is_complete = True
|
||||
progress.elapsed_time = time.time() - start_time
|
||||
|
||||
if callback:
|
||||
callback(progress)
|
||||
|
||||
return dir_sizes
|
||||
|
||||
def list_directory(
|
||||
self,
|
||||
path: str,
|
||||
show_hidden: bool = False,
|
||||
sort_by: str = 'size',
|
||||
reverse: bool = True
|
||||
) -> List[FileItem]:
|
||||
"""
|
||||
列出目录下的文件和子目录
|
||||
|
||||
参数:
|
||||
path: 目录路径
|
||||
show_hidden: 是否显示隐藏文件
|
||||
sort_by: 排序方式 (name, size, time)
|
||||
reverse: 是否逆序
|
||||
|
||||
返回:
|
||||
FileItem 列表
|
||||
"""
|
||||
items: List[FileItem] = []
|
||||
|
||||
try:
|
||||
entries = self._list_directory(path)
|
||||
|
||||
for entry in entries:
|
||||
# 跳过隐藏文件(除非要求显示)
|
||||
if not show_hidden and entry.startswith('.'):
|
||||
continue
|
||||
|
||||
entry_path = os.path.join(path, entry)
|
||||
|
||||
try:
|
||||
item = self._get_file_item(entry_path, entry)
|
||||
if item:
|
||||
items.append(item)
|
||||
except (PermissionError, OSError):
|
||||
# 跳过无权限访问的文件
|
||||
pass
|
||||
|
||||
except PermissionError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"列出目录时发生错误: {e}")
|
||||
|
||||
# 排序
|
||||
if sort_by == 'name':
|
||||
items.sort(key=lambda x: x.name.lower(), reverse=reverse)
|
||||
elif sort_by == 'size':
|
||||
items.sort(key=lambda x: x.size, reverse=reverse)
|
||||
elif sort_by == 'time':
|
||||
items.sort(key=lambda x: x.modified_time, reverse=reverse)
|
||||
|
||||
# 目录排在前面
|
||||
items.sort(key=lambda x: (not x.is_dir, -x.size if reverse else x.size))
|
||||
|
||||
return items
|
||||
|
||||
def get_system_logs(self, mount_point: str) -> Tuple[List[FileItem], int]:
|
||||
"""
|
||||
获取 /var/log 下的系统日志文件(不包含子目录中的日志)
|
||||
|
||||
参数:
|
||||
mount_point: 分区挂载点
|
||||
|
||||
返回:
|
||||
(日志文件列表, 总大小)
|
||||
"""
|
||||
log_dir = os.path.join(mount_point, 'var', 'log')
|
||||
|
||||
if not os.path.exists(log_dir):
|
||||
return [], 0
|
||||
|
||||
logs: List[FileItem] = []
|
||||
total_size = 0
|
||||
|
||||
try:
|
||||
entries = self._list_directory(log_dir)
|
||||
|
||||
for entry in entries:
|
||||
entry_path = os.path.join(log_dir, entry)
|
||||
|
||||
# 只处理文件,不处理子目录
|
||||
if not os.path.isfile(entry_path):
|
||||
continue
|
||||
|
||||
# 检查是否为日志文件
|
||||
if self._is_log_file(entry):
|
||||
try:
|
||||
item = self._get_file_item(entry_path, entry)
|
||||
if item:
|
||||
logs.append(item)
|
||||
total_size += item.size
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
|
||||
except PermissionError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"获取系统日志时发生错误: {e}")
|
||||
|
||||
# 按大小排序
|
||||
logs.sort(key=lambda x: x.size, reverse=True)
|
||||
|
||||
return logs, total_size
|
||||
|
||||
def cancel_scan(self):
|
||||
"""取消正在进行的扫描"""
|
||||
self._cancel_scan = True
|
||||
|
||||
def _list_directory(self, path: str) -> List[str]:
|
||||
"""列出目录内容"""
|
||||
try:
|
||||
return os.listdir(path)
|
||||
except PermissionError:
|
||||
# 尝试使用 sudo
|
||||
if self.sudo_password:
|
||||
stdout, _, returncode = run_command(
|
||||
['ls', '-a', path],
|
||||
use_sudo=True,
|
||||
sudo_password=self.sudo_password
|
||||
)
|
||||
if returncode == 0:
|
||||
return [f for f in stdout.strip().split('\n') if f and f not in ('.', '..')]
|
||||
return []
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
def _get_directory_size(
|
||||
self,
|
||||
path: str,
|
||||
progress: ScanProgress,
|
||||
callback: Optional[Callable[[ScanProgress], None]] = None
|
||||
) -> int:
|
||||
"""递归计算目录大小"""
|
||||
if self._cancel_scan:
|
||||
return 0
|
||||
|
||||
total_size = 0
|
||||
|
||||
try:
|
||||
entries = self._list_directory(path)
|
||||
|
||||
for entry in entries:
|
||||
if self._cancel_scan:
|
||||
break
|
||||
|
||||
entry_path = os.path.join(path, entry)
|
||||
|
||||
# 跳过虚拟文件系统
|
||||
if self._is_virtual_dir(entry_path):
|
||||
continue
|
||||
|
||||
# 跳过其他挂载点
|
||||
if self._is_other_mountpoint(entry_path, path):
|
||||
continue
|
||||
|
||||
try:
|
||||
if os.path.islink(entry_path):
|
||||
# 跳过符号链接
|
||||
continue
|
||||
elif os.path.isdir(entry_path):
|
||||
# 递归计算子目录
|
||||
progress.current_path = entry_path
|
||||
progress.scanned_dirs += 1
|
||||
|
||||
if callback and progress.scanned_dirs % 100 == 0:
|
||||
callback(progress)
|
||||
|
||||
total_size += self._get_directory_size(entry_path, progress, callback)
|
||||
else:
|
||||
# 获取文件大小
|
||||
try:
|
||||
file_size = os.path.getsize(entry_path)
|
||||
total_size += file_size
|
||||
progress.scanned_files += 1
|
||||
progress.total_size += file_size
|
||||
|
||||
if callback and progress.scanned_files % 1000 == 0:
|
||||
callback(progress)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except PermissionError:
|
||||
# 无权限访问
|
||||
pass
|
||||
|
||||
except PermissionError:
|
||||
# 无权限访问目录,尝试使用 sudo
|
||||
if self.sudo_password:
|
||||
total_size = self._get_directory_size_sudo(path)
|
||||
|
||||
return total_size
|
||||
|
||||
def _get_directory_size_sudo(self, path: str) -> int:
|
||||
"""使用 sudo 获取目录大小"""
|
||||
stdout, _, returncode = run_command(
|
||||
['du', '-sb', path],
|
||||
use_sudo=True,
|
||||
sudo_password=self.sudo_password,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if returncode == 0:
|
||||
try:
|
||||
return int(stdout.split()[0])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
def _get_file_item(self, path: str, name: str) -> Optional[FileItem]:
|
||||
"""获取文件/目录信息"""
|
||||
try:
|
||||
stat_info = os.lstat(path)
|
||||
|
||||
# 获取所有者和组
|
||||
try:
|
||||
import pwd
|
||||
import grp
|
||||
owner = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||
group = grp.getgrgid(stat_info.st_gid).gr_name
|
||||
except (ImportError, KeyError):
|
||||
owner = str(stat_info.st_uid)
|
||||
group = str(stat_info.st_gid)
|
||||
|
||||
# 获取权限字符串
|
||||
permissions = self._format_permissions(stat_info.st_mode)
|
||||
|
||||
# 如果是目录,获取目录大小(使用 du)
|
||||
if os.path.isdir(path):
|
||||
size = self._get_dir_size_fast(path)
|
||||
else:
|
||||
size = stat_info.st_size
|
||||
|
||||
return FileItem(
|
||||
name=name,
|
||||
path=path,
|
||||
is_dir=os.path.isdir(path),
|
||||
size=size,
|
||||
modified_time=datetime.fromtimestamp(stat_info.st_mtime),
|
||||
permissions=permissions,
|
||||
owner=owner,
|
||||
group=group
|
||||
)
|
||||
|
||||
except (OSError, PermissionError):
|
||||
return None
|
||||
|
||||
def _get_dir_size_fast(self, path: str) -> int:
|
||||
"""快速获取目录大小(使用 du 命令)"""
|
||||
try:
|
||||
stdout, _, returncode = run_command(
|
||||
['du', '-sb', path],
|
||||
use_sudo=True,
|
||||
sudo_password=self.sudo_password,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if returncode == 0:
|
||||
return int(stdout.split()[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
def _format_permissions(self, mode: int) -> str:
|
||||
"""格式化文件权限为字符串"""
|
||||
perms = []
|
||||
|
||||
# 文件类型
|
||||
if os.path.isdir(mode):
|
||||
perms.append('d')
|
||||
elif os.path.islink(mode):
|
||||
perms.append('l')
|
||||
else:
|
||||
perms.append('-')
|
||||
|
||||
# 所有者权限
|
||||
perms.append('r' if mode & 0o400 else '-')
|
||||
perms.append('w' if mode & 0o200 else '-')
|
||||
perms.append('x' if mode & 0o100 else '-')
|
||||
|
||||
# 组权限
|
||||
perms.append('r' if mode & 0o040 else '-')
|
||||
perms.append('w' if mode & 0o020 else '-')
|
||||
perms.append('x' if mode & 0o010 else '-')
|
||||
|
||||
# 其他用户权限
|
||||
perms.append('r' if mode & 0o004 else '-')
|
||||
perms.append('w' if mode & 0o002 else '-')
|
||||
perms.append('x' if mode & 0o001 else '-')
|
||||
|
||||
return ''.join(perms)
|
||||
|
||||
def _is_virtual_dir(self, path: str) -> bool:
|
||||
"""检查是否为虚拟文件系统目录"""
|
||||
path = os.path.normpath(path)
|
||||
for vdir in self.VIRTUAL_DIRS:
|
||||
if path == vdir or path.startswith(vdir + '/'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_other_mountpoint(self, path: str, base_path: str) -> bool:
|
||||
"""检查是否为其他挂载点"""
|
||||
try:
|
||||
# 如果路径和基础路径的设备号不同,说明是不同的挂载点
|
||||
base_stat = os.stat(base_path)
|
||||
path_stat = os.stat(path)
|
||||
|
||||
if base_stat.st_dev != path_stat.st_dev:
|
||||
return True
|
||||
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def _is_log_file(self, filename: str) -> bool:
|
||||
"""检查是否为日志文件"""
|
||||
import fnmatch
|
||||
|
||||
for pattern in self.SYSTEM_LOG_PATTERNS:
|
||||
if fnmatch.fnmatch(filename, pattern):
|
||||
return True
|
||||
|
||||
# 检查文件扩展名
|
||||
log_extensions = ['.log', '.log.gz', '.log.1', '.log.2', '.log.3']
|
||||
for ext in log_extensions:
|
||||
if filename.endswith(ext):
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user