# -*- coding: utf-8 -*- import os import shutil import hashlib import io import psutil from datetime import datetime from typing import List, Dict, Any, Optional from pathlib import Path from fastapi import UploadFile from PIL import Image import pylibmagic # 不要删除,否则import magic 导入启动报错ImportError: failed to find libmagic. Check your installation import magic from app.core.exceptions import CustomException from app.core.logger import logger from app.utils.excel_util import ExcelUtil from app.config.setting import settings from ...module_system.auth.schema import AuthSchema from .param import ResourceQueryParam from .schema import ( ResourceItemSchema, ResourceDirectorySchema, ResourceStatsSchema, ResourceSearchSchema, ResourceUploadSchema, ResourceMoveSchema, ResourceCopySchema, ResourceRenameSchema, ResourceCreateDirSchema, ResourceType ) class ResourceService: """ 资源管理模块服务层 - 管理系统静态文件目录 """ # 配置常量 MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB MAX_SEARCH_RESULTS = 1000 # 最大搜索结果数 MAX_PATH_DEPTH = 20 # 最大路径深度 @classmethod def _get_resource_root(cls) -> str: """获取资源管理根目录""" if not settings.STATIC_ENABLE: raise CustomException(msg='静态文件服务未启用') return str(settings.STATIC_ROOT) @classmethod def _get_safe_path(cls, path: str = None) -> str: """获取安全的文件路径""" resource_root = cls._get_resource_root() if not path: return resource_root # 清理路径,移除危险字符 path = path.strip().replace('..', '').replace('//', '/') # 规范化路径 if os.path.isabs(path): safe_path = os.path.normpath(path) else: safe_path = os.path.normpath(os.path.join(resource_root, path)) # 检查路径是否在允许的范围内 resource_root_abs = os.path.normpath(os.path.abspath(resource_root)) safe_path_abs = os.path.normpath(os.path.abspath(safe_path)) if not safe_path_abs.startswith(resource_root_abs): raise CustomException(msg=f'访问路径不在允许范围内: {path}') # 防止路径遍历攻击 if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH: # 限制最大目录深度 raise CustomException(msg=f'不安全的路径格式: {path}') return safe_path @classmethod def _path_exists(cls, path: str) -> bool: """检查路径是否存在""" try: safe_path = cls._get_safe_path(path) return os.path.exists(safe_path) except: return False @classmethod def _get_file_info(cls, file_path: str, base_url: Optional[str] = None) -> Dict[str, Any]: """获取文件信息""" try: safe_path = cls._get_safe_path(file_path) if not os.path.exists(safe_path): return {} stat = os.stat(safe_path) path_obj = Path(safe_path) resource_root = cls._get_resource_root() # 获取文件扩展名和类型 file_extension = path_obj.suffix.lower() if path_obj.suffix else None # 优先使用 magic 库检测 MIME 类型 file_type = None if os.path.isfile(safe_path): try: file_type = magic.from_file(safe_path, mime=True) except Exception as e: logger.debug(f"magic 库检测文件类型失败: {e}") # 如果 magic 检测失败或不可用,使用扩展名检测 if not file_type and file_extension: file_type = cls._get_mime_type_from_extension(file_extension) # 如果仍然没有类型,使用默认值 if not file_type: file_type = 'application/octet-stream' if os.path.isfile(safe_path) else None resource_type = cls._determine_resource_type(file_type, file_extension) # 计算相对路径 try: relative_path = os.path.relpath(safe_path, resource_root) except ValueError: relative_path = os.path.basename(safe_path) # 计算深度 try: depth = len(Path(safe_path).relative_to(resource_root).parts) except ValueError: depth = 0 # 生成HTTP URL路径而不是文件系统路径 if base_url: from urllib.parse import urljoin base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') relative_part = relative_path.lstrip('/') # 手动构建URL而不是使用urljoin,避免双斜杠问题 if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') return { 'name': path_obj.name, 'path': http_url, # 返回HTTP URL而不是文件系统路径 'relative_path': relative_path, 'is_file': os.path.isfile(safe_path), 'is_dir': os.path.isdir(safe_path), 'size': stat.st_size if os.path.isfile(safe_path) else None, 'file_type': file_type, 'file_extension': file_extension, 'resource_type': resource_type, 'created_time': datetime.fromtimestamp(stat.st_ctime), 'modified_time': datetime.fromtimestamp(stat.st_mtime), 'accessed_time': datetime.fromtimestamp(stat.st_atime), 'parent_path': str(path_obj.parent), 'depth': depth } except Exception as e: logger.error(f'获取文件信息失败: {str(e)}') return {} @classmethod async def get_directory_list_service( cls, auth: AuthSchema, path: Optional[str] = None, include_hidden: bool = False, base_url: Optional[str] = None ) -> Dict: """获取目录列表""" try: # 如果没有指定路径,使用静态文件根目录 if path is None: safe_path = cls._get_resource_root() # 对于根目录,返回静态URL路径 if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://') else: display_path = settings.STATIC_URL else: safe_path = cls._get_safe_path(path) # 对于子目录,生成相对于静态URL的路径 resource_root = cls._get_resource_root() try: relative_path = os.path.relpath(safe_path, resource_root) if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') relative_part = relative_path.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' display_path = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: display_path = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') except ValueError: if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://') else: display_path = settings.STATIC_URL if not os.path.exists(safe_path): raise CustomException(msg='目录不存在') if not os.path.isdir(safe_path): raise CustomException(msg='路径不是目录') items = [] total_files = 0 total_dirs = 0 total_size = 0 try: for item_name in os.listdir(safe_path): # 跳过隐藏文件 if not include_hidden and item_name.startswith('.'): continue item_path = os.path.join(safe_path, item_name) file_info = cls._get_file_info(item_path, base_url) if file_info: items.append(ResourceItemSchema(**file_info)) if file_info['is_file']: total_files += 1 total_size += file_info.get('size', 0) or 0 elif file_info['is_dir']: total_dirs += 1 except PermissionError: raise CustomException(msg='没有权限访问此目录') return ResourceDirectorySchema( path=display_path, # 返回HTTP URL路径而不是文件系统路径 name=os.path.basename(safe_path), items=items, total_files=total_files, total_dirs=total_dirs, total_size=total_size ).model_dump(mode='json') except CustomException: raise except Exception as e: logger.error(f'获取目录列表失败: {str(e)}') raise CustomException(msg=f'获取目录列表失败: {str(e)}') @classmethod async def _get_directory_stats(cls, path: str, include_hidden: bool = False) -> Dict[str, int]: """递归获取目录统计信息""" stats = {'files': 0, 'dirs': 0, 'size': 0} try: for root, dirs, files in os.walk(path): # 过滤隐藏目录 if not include_hidden: dirs[:] = [d for d in dirs if not d.startswith('.')] files = [f for f in files if not f.startswith('.')] stats['dirs'] += len(dirs) stats['files'] += len(files) for file in files: file_path = os.path.join(root, file) try: stats['size'] += os.path.getsize(file_path) except (OSError, IOError): continue except Exception: pass return stats @classmethod async def search_resources_service( cls, auth: AuthSchema, search: ResourceSearchSchema, base_url: Optional[str] = None ) -> List[Dict]: """搜索资源""" try: # 使用静态文件根目录作为搜索起点 search_root = cls._get_resource_root() results = [] for root, dirs, files in os.walk(search_root): # 控制搜索深度 try: depth = len(Path(root).relative_to(search_root).parts) except ValueError: depth = 0 if depth > search.max_depth: dirs.clear() # 阻止进一步深入 continue # 过滤隐藏文件夹(性能优化) if not search.include_hidden: dirs[:] = [d for d in dirs if not d.startswith('.')] files = [f for f in files if not f.startswith('.')] # 优化:先过滤文件名,再进行详细检查 if search.keyword: files = [f for f in files if search.keyword.lower() in f.lower()] # 搜索文件 for file in files: file_path = os.path.join(root, file) # 优化:先进行快速检查 if search.extensions: file_ext = os.path.splitext(file)[1].lower() if file_ext not in search.extensions: continue file_info = cls._get_file_info(file_path, base_url) if cls._match_search_criteria(file_info, search): results.append(file_info) # 限制结果数量防止内存溢出 if len(results) >= cls.MAX_SEARCH_RESULTS: logger.warning(f"搜索结果过多,已截断到前{cls.MAX_SEARCH_RESULTS}个") break if len(results) >= cls.MAX_SEARCH_RESULTS: break # 排序结果 return cls._sort_results(results, search) except Exception as e: logger.error(f'搜索资源失败: {str(e)}') raise CustomException(msg=f'搜索资源失败: {str(e)}') @classmethod def _match_search_criteria(cls, file_info: Dict, search: ResourceSearchSchema) -> bool: """检查文件是否匹配搜索条件""" if not file_info or not file_info.get('is_file'): return False # 关键词搜索 if search.keyword: if search.keyword.lower() not in file_info.get('name', '').lower(): return False # 文件类型搜索 if search.file_type: if search.file_type.lower() != file_info.get('file_type', '').lower(): return False # 资源类型搜索 if search.resource_type: if search.resource_type != file_info.get('resource_type'): return False # 文件大小搜索 file_size = file_info.get('size', 0) or 0 if search.min_size and file_size < search.min_size: return False if search.max_size and file_size > search.max_size: return False # 扩展名搜索 if search.extensions: file_ext = file_info.get('file_extension', '') if file_ext not in search.extensions: return False # 时间范围搜索 modified_time = file_info.get('modified_time') if modified_time: if search.start_date and modified_time < search.start_date: return False if search.end_date and modified_time > search.end_date: return False return True @classmethod def _sort_results(cls, results: List[Dict], search: ResourceSearchSchema) -> List[Dict]: """排序搜索结果""" sort_key = 'name' if hasattr(search, 'sort_by') and search.sort_by: sort_key = search.sort_by reverse = False if hasattr(search, 'sort_order') and search.sort_order == 'desc': reverse = True try: return sorted(results, key=lambda x: x.get(sort_key, ''), reverse=reverse) except: return results @classmethod async def upload_file_service( cls, auth: AuthSchema, file: UploadFile, target_path: Optional[str] = None, base_url: Optional[str] = None ) -> Dict: """上传文件到指定目录""" if not file or not file.filename: raise CustomException(msg="请选择要上传的文件") # 文件名安全检查 if '..' in file.filename or '/' in file.filename or '\\' in file.filename: raise CustomException(msg="文件名包含不安全字符") try: # 检查文件大小 content = await file.read() if len(content) > cls.MAX_UPLOAD_SIZE: raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024*1024)}MB") # 确定上传目录,如果没有指定目标路径,使用静态文件根目录 if target_path is None: safe_dir = cls._get_resource_root() else: safe_dir = cls._get_safe_path(target_path) # 创建目录(如果不存在) os.makedirs(safe_dir, exist_ok=True) # 生成文件路径 filename = file.filename file_path = os.path.join(safe_dir, filename) # 检查文件是否已存在 if os.path.exists(file_path): # 生成唯一文件名 base_name, ext = os.path.splitext(filename) counter = 1 while os.path.exists(file_path): new_filename = f"{base_name}_{counter}{ext}" file_path = os.path.join(safe_dir, new_filename) counter += 1 filename = os.path.basename(file_path) # 保存文件(使用已读取的内容) with open(file_path, 'wb') as f: f.write(content) # 获取文件信息 file_info = cls._get_file_info(file_path, base_url) # 生成相对于资源根目录的URL路径 resource_root = cls._get_resource_root() try: relative_path = os.path.relpath(file_path, resource_root) # 确保路径使用正斜杠(URL格式) file_url_path = relative_path.replace(os.sep, '/') # 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') file_url_part = file_url_path.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' file_url = f"{base_part}{static_part}/{file_url_part}".replace('//', '/').replace(':/', '://') else: file_url = f"{settings.STATIC_URL}/{file_url_path}".replace('//', '/') except ValueError: # 如果无法计算相对路径,使用文件名 filename = os.path.basename(file_path) if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') filename_part = filename.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' file_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://') else: file_url = f"{settings.STATIC_URL}/{filename}" logger.info(f"文件上传成功: {filename}") return ResourceUploadSchema( filename=filename, file_path=file_url, # 返回HTTP URL而不是文件系统路径 file_url=file_url, file_size=file_info.get('size', 0), resource_type=file_info.get('resource_type', ResourceType.OTHER), upload_time=datetime.now() ).model_dump(mode='json') except Exception as e: logger.error(f"文件上传失败: {str(e)}") raise CustomException(msg=f"文件上传失败: {str(e)}") @classmethod async def download_file_service(cls, auth: AuthSchema, file_path: str, base_url: Optional[str] = None) -> str: """下载文件(返回文件路径)""" try: safe_path = cls._get_safe_path(file_path) if not os.path.exists(safe_path): raise CustomException(msg='文件不存在') if not os.path.isfile(safe_path): raise CustomException(msg='路径不是文件') # 生成HTTP URL路径而不是返回文件系统路径 resource_root = cls._get_resource_root() try: relative_path = os.path.relpath(safe_path, resource_root) # 生成HTTP URL if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') relative_part = relative_path.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') logger.info(f"生成文件访问URL: {http_url}") return http_url except ValueError: # 如果无法计算相对路径,使用文件名 filename = os.path.basename(safe_path) if base_url: from urllib.parse import urljoin # 修复URL生成逻辑 base_part = base_url.rstrip('/') static_part = settings.STATIC_URL.lstrip('/') filename_part = filename.lstrip('/') if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): base_part += '/' http_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{filename}" logger.info(f"生成文件访问URL: {http_url}") return http_url except CustomException: raise except Exception as e: logger.error(f"下载文件失败: {str(e)}") raise CustomException(msg=f"下载文件失败: {str(e)}") @classmethod async def delete_file_service(cls, auth: AuthSchema, paths: List[str]) -> None: """删除文件或目录""" if not paths: raise CustomException(msg='删除失败,删除路径不能为空') for path in paths: try: safe_path = cls._get_safe_path(path) if not os.path.exists(safe_path): logger.warning(f"路径不存在,跳过: {path}") continue if os.path.isfile(safe_path): os.remove(safe_path) logger.info(f"删除文件成功: {safe_path}") elif os.path.isdir(safe_path): shutil.rmtree(safe_path) logger.info(f"删除目录成功: {safe_path}") except Exception as e: logger.error(f"删除失败 {path}: {str(e)}") raise CustomException(msg=f"删除失败 {path}: {str(e)}") @classmethod async def move_file_service(cls, auth: AuthSchema, data: ResourceMoveSchema) -> None: """移动文件或目录""" try: source_path = cls._get_safe_path(data.source_path) target_path = cls._get_safe_path(data.target_path) if not os.path.exists(source_path): raise CustomException(msg='源路径不存在') # 检查目标路径是否已存在 if os.path.exists(target_path): if not data.overwrite: raise CustomException(msg='目标路径已存在') else: # 删除目标路径 if os.path.isfile(target_path): os.remove(target_path) else: shutil.rmtree(target_path) # 确保目标目录存在 target_dir = os.path.dirname(target_path) os.makedirs(target_dir, exist_ok=True) # 移动文件 shutil.move(source_path, target_path) logger.info(f"移动成功: {source_path} -> {target_path}") except CustomException: raise except Exception as e: logger.error(f"移动失败: {str(e)}") raise CustomException(msg=f"移动失败: {str(e)}") @classmethod async def copy_file_service(cls, auth: AuthSchema, data: ResourceCopySchema) -> None: """复制文件或目录""" try: source_path = cls._get_safe_path(data.source_path) target_path = cls._get_safe_path(data.target_path) if not os.path.exists(source_path): raise CustomException(msg='源路径不存在') # 检查目标路径是否已存在 if os.path.exists(target_path) and not data.overwrite: raise CustomException(msg='目标路径已存在') # 确保目标目录存在 target_dir = os.path.dirname(target_path) os.makedirs(target_dir, exist_ok=True) # 复制文件或目录 if os.path.isfile(source_path): shutil.copy2(source_path, target_path) else: shutil.copytree(source_path, target_path, dirs_exist_ok=data.overwrite) logger.info(f"复制成功: {source_path} -> {target_path}") except CustomException: raise except Exception as e: logger.error(f"复制失败: {str(e)}") raise CustomException(msg=f"复制失败: {str(e)}") @classmethod async def rename_file_service(cls, auth: AuthSchema, data: ResourceRenameSchema) -> None: """重命名文件或目录""" try: old_path = cls._get_safe_path(data.old_path) if not os.path.exists(old_path): raise CustomException(msg='文件或目录不存在') # 生成新路径 parent_dir = os.path.dirname(old_path) new_path = os.path.join(parent_dir, data.new_name) if os.path.exists(new_path): raise CustomException(msg='目标名称已存在') # 重命名 os.rename(old_path, new_path) logger.info(f"重命名成功: {old_path} -> {new_path}") except CustomException: raise except Exception as e: logger.error(f"重命名失败: {str(e)}") raise CustomException(msg=f"重命名失败: {str(e)}") @classmethod async def create_directory_service(cls, auth: AuthSchema, data: ResourceCreateDirSchema) -> None: """创建目录""" try: parent_path = cls._get_safe_path(data.parent_path) if not os.path.exists(parent_path): raise CustomException(msg='父目录不存在') if not os.path.isdir(parent_path): raise CustomException(msg='父路径不是目录') # 生成新目录路径 new_dir_path = os.path.join(parent_path, data.dir_name) if os.path.exists(new_dir_path): raise CustomException(msg='目录已存在') # 创建目录 os.makedirs(new_dir_path) logger.info(f"创建目录成功: {new_dir_path}") except CustomException: raise except Exception as e: logger.error(f"创建目录失败: {str(e)}") raise CustomException(msg=f"创建目录失败: {str(e)}") @classmethod async def get_stats_service(cls, auth: AuthSchema, base_url: Optional[str] = None) -> Dict: """获取资源统计信息""" try: # 使用静态文件根目录 stats_root = cls._get_resource_root() # 获取磁盘空间信息 disk_usage = psutil.disk_usage(stats_root) total_space = disk_usage.total free_space = disk_usage.free used_space = disk_usage.used # 统计文件信息 total_files = 0 total_dirs = 0 total_size = 0 type_stats = {} extension_stats = {} for root, dirs, files in os.walk(stats_root): total_dirs += len(dirs) for file in files: file_path = os.path.join(root, file) try: file_info = cls._get_file_info(file_path, base_url) if file_info: total_files += 1 total_size += file_info.get('size', 0) or 0 # 类型统计 resource_type = file_info.get('resource_type') if resource_type: type_name = resource_type.value if hasattr(resource_type, 'value') else str(resource_type) type_stats[type_name] = type_stats.get(type_name, 0) + 1 # 扩展名统计 extension = file_info.get('file_extension', '') if extension: extension_stats[extension] = extension_stats.get(extension, 0) + 1 except Exception: continue return ResourceStatsSchema( mount_point=stats_root, total_files=total_files, total_dirs=total_dirs, total_size=total_size, free_space=free_space, used_space=used_space, total_space=total_space, type_stats=type_stats, extension_stats=extension_stats ).model_dump(mode='json') except Exception as e: logger.error(f"获取统计信息失败: {str(e)}") raise CustomException(msg=f"获取统计信息失败: {str(e)}") @classmethod async def export_resource_service(cls, data_list: List[Dict[str, Any]]) -> bytes: """导出资源列表""" mapping_dict = { 'name': '文件名', 'path': '文件路径', 'size': '文件大小', 'file_type': 'MIME类型', 'file_extension': '文件扩展名', 'resource_type': '资源类型', 'created_time': '创建时间', 'modified_time': '修改时间', 'parent_path': '父目录' } # 复制数据并转换状态 export_data = data_list.copy() for item in export_data: # 处理枚举值 if 'resource_type' in item and hasattr(item['resource_type'], 'value'): item['resource_type'] = item['resource_type'].value # 格式化文件大小 if item.get('size'): item['size'] = cls._format_file_size(item['size']) return ExcelUtil.export_list2excel(list_data=export_data, mapping_dict=mapping_dict) @classmethod def _determine_resource_type(cls, file_type: str, file_extension: str) -> ResourceType: """根据MIME类型和文件扩展名确定资源类型""" if not file_type: return ResourceType.OTHER if file_type.startswith('image/'): return ResourceType.IMAGE elif file_type.startswith('video/'): return ResourceType.VIDEO elif file_type.startswith('audio/'): return ResourceType.AUDIO elif file_type in ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/plain', 'text/csv']: return ResourceType.DOCUMENT elif file_type in ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed', 'application/gzip', 'application/x-tar']: return ResourceType.ARCHIVE else: return ResourceType.OTHER @classmethod def _format_file_size(cls, size_bytes: int) -> str: """格式化文件大小""" if size_bytes == 0: return "0B" size_names = ["B", "KB", "MB", "GB", "TB"] i = 0 while size_bytes >= 1024 and i < len(size_names) - 1: size_bytes /= 1024.0 i += 1 return f"{size_bytes:.2f}{size_names[i]}" @classmethod def _get_mime_type_from_extension(cls, file_extension: str) -> str: """根据文件扩展名获取MIME类型""" if not file_extension: return 'application/octet-stream' # 扩展更全面的MIME类型映射 mime_types = { # 图片类型 '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.tiff': 'image/tiff', # 视频类型 '.mp4': 'video/mp4', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.wmv': 'video/x-ms-wmv', '.flv': 'video/x-flv', '.webm': 'video/webm', '.mkv': 'video/x-matroska', '.m4v': 'video/x-m4v', # 音频类型 '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.flac': 'audio/flac', '.m4a': 'audio/mp4', # 文档类型 '.pdf': 'application/pdf', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.ppt': 'application/vnd.ms-powerpoint', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', '.txt': 'text/plain', '.csv': 'text/csv', '.rtf': 'application/rtf', # 代码文件 '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.xml': 'application/xml', '.py': 'text/x-python', '.java': 'text/x-java-source', # 压缩文件 '.zip': 'application/zip', '.rar': 'application/x-rar-compressed', '.7z': 'application/x-7z-compressed', '.tar': 'application/x-tar', '.gz': 'application/gzip', '.bz2': 'application/x-bzip2' } return mime_types.get(file_extension.lower(), 'application/octet-stream')