File size: 9,801 Bytes
4e37375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""文件验证模块

提供音频文件格式验证、大小检查等功能。
"""

import magic
from pathlib import Path
from typing import List, Optional, Tuple
import mimetypes

from ..core.config import get_config
from ..utils.logger import get_task_logger


class FileValidator:
    """文件验证器"""
    
    # 支持的音频文件格式
    SUPPORTED_EXTENSIONS = {
        '.aac', '.amr', '.avi', '.flac', '.flv', '.m4a', '.mkv', 
        '.mov', '.mp3', '.mp4', '.mpeg', '.ogg', '.opus', '.wav', 
        '.webm', '.wma', '.wmv'
    }
    
    # 支持的MIME类型
    SUPPORTED_MIME_TYPES = {
        'audio/aac', 'audio/amr', 'audio/flac', 'audio/mp3', 'audio/mpeg',
        'audio/mp4', 'audio/ogg', 'audio/opus', 'audio/wav', 'audio/webm',
        'audio/x-wav', 'audio/x-flac', 'audio/x-m4a',
        'video/mp4', 'video/avi', 'video/x-flv', 'video/quicktime',
        'video/x-msvideo', 'video/webm', 'video/x-ms-wmv'
    }
    
    def __init__(self):
        """初始化文件验证器"""
        self.config = get_config()
        self.logger = get_task_logger(logger_name="transcript_service.validator")
        
        # 初始化libmagic
        try:
            self.magic = magic.Magic(mime=True)
        except Exception as e:
            self.logger.warning(f"无法初始化libmagic: {str(e)}, 将使用基础验证")
            self.magic = None
    
    def validate_file(self, file_path: Path) -> Tuple[bool, Optional[str]]:
        """验证单个文件
        
        Args:
            file_path: 文件路径
            
        Returns:
            (是否有效, 错误信息)
        """
        try:
            # 检查文件是否存在
            if not file_path.exists():
                return False, f"文件不存在: {file_path}"
            
            # 检查是否是文件
            if not file_path.is_file():
                return False, f"不是有效的文件: {file_path}"
            
            # 检查文件大小
            file_size = file_path.stat().st_size
            if file_size == 0:
                return False, f"文件为空: {file_path.name}"
            
            if file_size > self.config.app.max_file_size:
                size_mb = file_size / (1024 * 1024)
                max_size_mb = self.config.app.max_file_size / (1024 * 1024)
                return False, f"文件大小 {size_mb:.1f}MB 超过限制 {max_size_mb:.1f}MB: {file_path.name}"
            
            # 检查文件扩展名
            file_ext = file_path.suffix.lower()
            if file_ext not in self.SUPPORTED_EXTENSIONS:
                return False, f"不支持的文件格式 {file_ext}: {file_path.name}"
            
            # 检查MIME类型
            if not self._check_mime_type(file_path):
                return False, f"文件内容与扩展名不匹配: {file_path.name}"
            
            # 检查文件完整性
            if not self._check_file_integrity(file_path):
                return False, f"文件可能损坏或不完整: {file_path.name}"
            
            self.logger.info(f"文件验证通过: {file_path.name}")
            return True, None
            
        except Exception as e:
            error_msg = f"验证文件时发生错误: {file_path.name}, 错误: {str(e)}"
            self.logger.exception(error_msg)
            return False, error_msg
    
    def validate_multiple_files(self, file_paths: List[Path]) -> Tuple[List[Path], List[Tuple[Path, str]]]:
        """验证多个文件
        
        Args:
            file_paths: 文件路径列表
            
        Returns:
            (有效文件列表, 无效文件列表[(文件路径, 错误信息)])
        """
        # 检查文件数量
        if len(file_paths) > self.config.app.max_files_count:
            self.logger.warning(f"文件数量 {len(file_paths)} 超过限制 {self.config.app.max_files_count}")
        
        valid_files = []
        invalid_files = []
        
        for file_path in file_paths[:self.config.app.max_files_count]:
            is_valid, error_msg = self.validate_file(file_path)
            if is_valid:
                valid_files.append(file_path)
            else:
                invalid_files.append((file_path, error_msg))
        
        # 如果超过限制,记录被跳过的文件
        if len(file_paths) > self.config.app.max_files_count:
            skipped_count = len(file_paths) - self.config.app.max_files_count
            self.logger.warning(f"跳过了 {skipped_count} 个文件(超过批处理限制)")
        
        self.logger.info(f"文件验证完成: {len(valid_files)} 个有效文件, {len(invalid_files)} 个无效文件")
        return valid_files, invalid_files
    
    def _check_mime_type(self, file_path: Path) -> bool:
        """检查文件MIME类型
        
        Args:
            file_path: 文件路径
            
        Returns:
            MIME类型是否匹配
        """
        try:
            # 使用libmagic检查
            if self.magic:
                mime_type = self.magic.from_file(str(file_path))
                if mime_type in self.SUPPORTED_MIME_TYPES:
                    return True
            
            # 使用mimetypes作为备选方案
            mime_type, _ = mimetypes.guess_type(str(file_path))
            if mime_type and mime_type in self.SUPPORTED_MIME_TYPES:
                return True
            
            # 对于某些格式,检查文件头
            return self._check_file_header(file_path)
            
        except Exception as e:
            self.logger.warning(f"检查MIME类型时发生错误: {file_path.name}, 错误: {str(e)}")
            # 如果MIME检查失败,只要扩展名正确就通过
            return True
    
    def _check_file_header(self, file_path: Path) -> bool:
        """检查文件头部特征
        
        Args:
            file_path: 文件路径
            
        Returns:
            文件头是否匹配
        """
        try:
            with open(file_path, 'rb') as f:
                header = f.read(16)
            
            if not header:
                return False
            
            # 检查常见音频格式的文件头
            if header.startswith(b'ID3') or header[4:8] == b'ftyp':  # MP3, MP4
                return True
            elif header.startswith(b'RIFF') and b'WAVE' in header:  # WAV
                return True
            elif header.startswith(b'fLaC'):  # FLAC
                return True
            elif header.startswith(b'OggS'):  # OGG
                return True
            elif header.startswith(b'\xff\xfb') or header.startswith(b'\xff\xfa'):  # MP3
                return True
            
            # 如果无法识别文件头,但扩展名正确,就通过验证
            return True
            
        except Exception as e:
            self.logger.warning(f"检查文件头时发生错误: {file_path.name}, 错误: {str(e)}")
            return True
    
    def _check_file_integrity(self, file_path: Path) -> bool:
        """检查文件完整性
        
        Args:
            file_path: 文件路径
            
        Returns:
            文件是否完整
        """
        try:
            # 基础完整性检查:确保文件可以完全读取
            with open(file_path, 'rb') as f:
                # 读取文件开头和结尾
                f.read(1024)  # 读取前1KB
                f.seek(-min(1024, file_path.stat().st_size), 2)  # 读取后1KB
                f.read()
            
            return True
            
        except Exception as e:
            self.logger.warning(f"检查文件完整性时发生错误: {file_path.name}, 错误: {str(e)}")
            return False
    
    def get_file_info(self, file_path: Path) -> dict:
        """获取文件信息
        
        Args:
            file_path: 文件路径
            
        Returns:
            文件信息字典
        """
        try:
            stat = file_path.stat()
            
            # 获取MIME类型
            mime_type = None
            if self.magic:
                try:
                    mime_type = self.magic.from_file(str(file_path))
                except:
                    pass
            
            if not mime_type:
                mime_type, _ = mimetypes.guess_type(str(file_path))
            
            return {
                'name': file_path.name,
                'size': stat.st_size,
                'size_mb': round(stat.st_size / (1024 * 1024), 2),
                'extension': file_path.suffix.lower(),
                'mime_type': mime_type,
                'modified_time': stat.st_mtime,
                'is_supported': file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS
            }
            
        except Exception as e:
            self.logger.error(f"获取文件信息失败: {file_path.name}, 错误: {str(e)}")
            return {
                'name': file_path.name,
                'error': str(e)
            }
    
    def get_supported_formats(self) -> dict:
        """获取支持的文件格式信息
        
        Returns:
            支持的格式信息
        """
        return {
            'extensions': sorted(list(self.SUPPORTED_EXTENSIONS)),
            'mime_types': sorted(list(self.SUPPORTED_MIME_TYPES)),
            'max_file_size_mb': self.config.app.max_file_size / (1024 * 1024),
            'max_files_count': self.config.app.max_files_count
        }


# 全局文件验证器实例
file_validator = FileValidator()


def get_file_validator() -> FileValidator:
    """获取文件验证器实例
    
    Returns:
        文件验证器实例
    """
    return file_validator