- git 初始化 + .gitignore 补全(根 .env / logs / instance / __pycache__ 等) - 修复 utils/.gitignore 误将整个 utils/ 源码层忽略的问题,补入全部工具源码(含 ip2region.xdb) - 含后端全部代码:application / extensions / utils / config / migrations - 含前端 ui/(后续整体迁走) - 不含根 .env(含密钥,本地保留) Co-Authored-By: Claude <noreply@anthropic.com>
408 lines
14 KiB
Python
408 lines
14 KiB
Python
# +======================================================================
|
||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||
# +----------------------------------------------------------------------
|
||
# | Copyright (c) 2021-2026 珊瑚云团队 版权所有
|
||
# +----------------------------------------------------------------------
|
||
# | 开源协议: Apache-2.0
|
||
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||
# +----------------------------------------------------------------------
|
||
# | 官方网站: https://www.djangoadmin.cn
|
||
# +----------------------------------------------------------------------
|
||
# | 研发团队: 珊瑚云团队 荣誉出品
|
||
# +======================================================================
|
||
# | 版权和免责声明:
|
||
# | 一、知识产权声明
|
||
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||
# | 二、使用限制与禁止条款
|
||
# | 您理解并同意,禁止将本产品用于以下任何行为:
|
||
# | 1. 任何违反中华人民共和国法律法规的用途;
|
||
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||
# | 三、责任豁免
|
||
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||
# | 四、违约与法律责任
|
||
# | 如发现任何违反本声明的行为,本团队有权:
|
||
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||
# | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||
# | 五、许可范围说明
|
||
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||
# | 成的一切经济损失及商誉损害。
|
||
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||
# | 许可协议》附件为准。
|
||
# +======================================================================
|
||
|
||
"""
|
||
文件目录操作工具模块
|
||
=============================================================
|
||
|
||
该模块提供文件和目录操作的常用工具函数。
|
||
主要用于处理文件上传、目录创建、路径管理等场景,确保文件系统操作的
|
||
安全性和可靠性。包含目录创建、文件操作等基础功能。
|
||
|
||
主要功能:
|
||
1. 递归创建目录(mkdir)- 确保目标目录存在
|
||
2. 文件路径处理
|
||
3. 目录存在性检查
|
||
|
||
所属包: utils
|
||
所在文件: utils/file_utils.py
|
||
依赖模块: os - 操作系统接口模块
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# 常见文件类型的魔术字节签名
|
||
# 格式:扩展名 -> [签名列表],签名为 (offset, bytes) 元组
|
||
MAGIC_SIGNATURES = {
|
||
# 图片
|
||
'.jpg': [(0, b'\xff\xd8\xff')],
|
||
'.jpeg': [(0, b'\xff\xd8\xff')],
|
||
'.png': [(0, b'\x89PNG\r\n\x1a\n')],
|
||
'.gif': [(0, b'GIF87a'), (0, b'GIF89a')],
|
||
'.bmp': [(0, b'BM')],
|
||
'.webp': [(0, b'RIFF')],
|
||
'.ico': [(0, b'\x00\x00\x01\x00'), (0, b'\x00\x00\x02\x00')],
|
||
# 文档
|
||
'.pdf': [(0, b'%PDF')],
|
||
'.zip': [(0, b'PK\x03\x04')],
|
||
'.rar': [(0, b'Rar!\x1a\x07')],
|
||
'.7z': [(0, b'7z\xbc\xaf\x27\x1c')],
|
||
'.mp3': [(0, b'\xff\xfb'), (0, b'\xff\xf3'), (0, b'\xff\xf2'), (0, b'ID3')],
|
||
'.mp4': [(4, b'ftyp')],
|
||
# SVG 是文本格式,无固定二进制签名
|
||
}
|
||
|
||
|
||
def verify_file_magic(file_stream, ext_with_dot):
|
||
"""
|
||
校验文件魔术字节,验证文件实际内容与扩展名是否一致
|
||
|
||
Args:
|
||
file_stream: Flask上传文件对象(FileStorage),需支持seek/read
|
||
ext_with_dot: 带点的扩展名,如 '.jpg'
|
||
|
||
Returns:
|
||
tuple: (is_valid, message)
|
||
- is_valid (bool): 是否通过校验
|
||
- message (str): 提示信息
|
||
"""
|
||
signatures = MAGIC_SIGNATURES.get(ext_with_dot)
|
||
if not signatures:
|
||
# 无签名规则的文件类型,跳过魔术字节校验
|
||
return True, ''
|
||
|
||
try:
|
||
# 读取文件头部字节用于校验
|
||
header = file_stream.read(32)
|
||
file_stream.seek(0)
|
||
|
||
if not header:
|
||
return False, '文件内容为空'
|
||
|
||
for sig in signatures:
|
||
if isinstance(sig, tuple):
|
||
offset, sig_bytes = sig
|
||
if header[offset:offset + len(sig_bytes)] == sig_bytes:
|
||
return True, ''
|
||
elif header.startswith(sig):
|
||
return True, ''
|
||
|
||
return False, f'文件内容与扩展名 {ext_with_dot} 不匹配'
|
||
|
||
except Exception as e:
|
||
logger.error(f'魔术字节校验异常: {e}')
|
||
# 读取异常时不阻断上传,降级为仅扩展名校验
|
||
return True, ''
|
||
|
||
|
||
def mkdir(path):
|
||
"""
|
||
递归创建目录
|
||
=============================================================
|
||
|
||
检查指定路径的目录是否存在,如果不存在则递归创建所有必需的目录。
|
||
用于确保文件上传等操作前目标目录已存在。
|
||
|
||
参数:
|
||
path (str): 需要创建/检查的目录路径
|
||
|
||
返回:
|
||
bool: True - 目录创建成功或已存在
|
||
False - 目录创建失败(保留扩展用)
|
||
|
||
使用示例:
|
||
>>> # 确保上传目录存在
|
||
>>> upload_dir = '/data/uploads/2024/01'
|
||
>>> mkdir(upload_dir)
|
||
/data/uploads/2024/01 创建成功
|
||
True
|
||
|
||
注意事项:
|
||
1. 函数会自动创建多级不存在的目录
|
||
2. 如果目录已存在,不会重复创建
|
||
3. 需要有相应的文件系统权限
|
||
"""
|
||
|
||
# 去除首位空格
|
||
# 说明:消除用户输入中可能包含的前后空格,避免路径错误
|
||
path = path.strip()
|
||
|
||
# 去除尾部 \ 符号
|
||
# 说明:Windows系统路径可能以\结尾,去除后保持路径格式统一
|
||
path = path.rstrip("\\")
|
||
|
||
# 判断路径是否存在
|
||
# 函数:os.path.exists(path)
|
||
# 说明:检查指定路径的文件或目录是否存在
|
||
isExists = os.path.exists(path)
|
||
|
||
if not isExists:
|
||
# 如果不存在则创建目录
|
||
# 函数:os.makedirs(path)
|
||
# 说明:递归创建目录,如果中间目录不存在也会一并创建
|
||
# 例如:创建 /a/b/c 时,如果 /a 和 /a/b 都不存在,会一并创建
|
||
os.makedirs(path)
|
||
logger.debug(f'{path} 创建成功')
|
||
return True
|
||
else:
|
||
logger.debug(f'{path} 目录已存在')
|
||
return False
|
||
|
||
|
||
# ======================= 扩展:常用文件操作工具类 ==========================
|
||
|
||
class FileUtils:
|
||
"""
|
||
文件操作工具类
|
||
=============================================================
|
||
|
||
提供更多常用的文件和目录操作功能,封装为类方法便于调用。
|
||
包含文件读写、路径处理、文件信息获取等实用功能。
|
||
|
||
使用示例:
|
||
>>> # 获取文件大小
|
||
>>> size = FileUtils.get_file_size('/path/file.txt')
|
||
>>> print(f"{size} bytes")
|
||
|
||
>>> # 安全删除文件
|
||
>>> if FileUtils.safe_delete('/path/file.txt'):
|
||
... print("文件删除成功")
|
||
"""
|
||
|
||
@staticmethod
|
||
def get_file_size(file_path):
|
||
"""
|
||
获取文件大小
|
||
=============================================================
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
|
||
返回:
|
||
int: 文件大小(字节),如果文件不存在返回0
|
||
"""
|
||
if os.path.exists(file_path) and os.path.isfile(file_path):
|
||
return os.path.getsize(file_path)
|
||
return 0
|
||
|
||
@staticmethod
|
||
def get_file_extension(filename):
|
||
"""
|
||
获取文件扩展名
|
||
=============================================================
|
||
|
||
参数:
|
||
filename (str): 文件名或文件路径
|
||
|
||
返回:
|
||
str: 文件扩展名(小写),例如:'.jpg', '.txt'
|
||
"""
|
||
import os
|
||
return os.path.splitext(filename)[1].lower()
|
||
|
||
@staticmethod
|
||
def get_file_name_without_ext(filename):
|
||
"""
|
||
获取不带扩展名的文件名
|
||
=============================================================
|
||
|
||
参数:
|
||
filename (str): 文件名或文件路径
|
||
|
||
返回:
|
||
str: 不带扩展名的文件名
|
||
"""
|
||
import os
|
||
return os.path.splitext(os.path.basename(filename))[0]
|
||
|
||
@staticmethod
|
||
def safe_delete(file_path):
|
||
"""
|
||
安全删除文件
|
||
=============================================================
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
|
||
返回:
|
||
bool: True - 删除成功或文件不存在
|
||
False - 删除失败(如权限不足)
|
||
"""
|
||
try:
|
||
if os.path.exists(file_path) and os.path.isfile(file_path):
|
||
os.remove(file_path)
|
||
return True
|
||
except Exception as e:
|
||
logger.warning(f"文件删除失败: {e}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def read_file(file_path, encoding='utf-8'):
|
||
"""
|
||
读取文件内容
|
||
=============================================================
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
encoding (str): 文件编码,默认utf-8
|
||
|
||
返回:
|
||
str/None: 文件内容字符串,读取失败返回None
|
||
"""
|
||
try:
|
||
with open(file_path, 'r', encoding=encoding) as f:
|
||
return f.read()
|
||
except Exception as e:
|
||
logger.warning(f"文件读取失败: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def write_file(file_path, content, encoding='utf-8', mode='w'):
|
||
"""
|
||
写入文件内容
|
||
=============================================================
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
content (str): 要写入的内容
|
||
encoding (str): 文件编码,默认utf-8
|
||
mode (str): 写入模式,'w'覆盖写入,'a'追加写入
|
||
|
||
返回:
|
||
bool: True - 写入成功,False - 写入失败
|
||
"""
|
||
try:
|
||
# 确保目录存在
|
||
directory = os.path.dirname(file_path)
|
||
if directory and not os.path.exists(directory):
|
||
os.makedirs(directory)
|
||
|
||
with open(file_path, mode, encoding=encoding) as f:
|
||
f.write(content)
|
||
return True
|
||
except Exception as e:
|
||
logger.warning(f"文件写入失败: {e}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def list_files(directory, pattern=None):
|
||
"""
|
||
列出目录下的所有文件
|
||
=============================================================
|
||
|
||
参数:
|
||
directory (str): 目录路径
|
||
pattern (str): 文件匹配模式,如 '*.txt',None表示所有文件
|
||
|
||
返回:
|
||
list: 文件名列表
|
||
"""
|
||
import glob
|
||
|
||
if not os.path.exists(directory):
|
||
return []
|
||
|
||
if pattern:
|
||
search_path = os.path.join(directory, pattern)
|
||
return [os.path.basename(f) for f in glob.glob(search_path)]
|
||
else:
|
||
return [f for f in os.listdir(directory)
|
||
if os.path.isfile(os.path.join(directory, f))]
|
||
|
||
@staticmethod
|
||
def get_file_info(file_path):
|
||
"""
|
||
获取文件详细信息
|
||
=============================================================
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
|
||
返回:
|
||
dict: 包含文件信息的字典,文件不存在返回None
|
||
"""
|
||
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
||
return None
|
||
|
||
stat = os.stat(file_path)
|
||
return {
|
||
'name': os.path.basename(file_path),
|
||
'path': os.path.abspath(file_path),
|
||
'size': stat.st_size,
|
||
'size_format': FileUtils._format_size(stat.st_size),
|
||
'extension': FileUtils.get_file_extension(file_path),
|
||
'created_time': stat.st_ctime,
|
||
'modified_time': stat.st_mtime,
|
||
'is_hidden': os.path.basename(file_path).startswith('.')
|
||
}
|
||
|
||
@staticmethod
|
||
def _format_size(size_bytes):
|
||
"""
|
||
格式化文件大小显示
|
||
=============================================================
|
||
|
||
参数:
|
||
size_bytes (int): 文件大小(字节)
|
||
|
||
返回:
|
||
str: 格式化后的大小字符串,如 '1.5 MB'
|
||
"""
|
||
if size_bytes < 1024:
|
||
return f"{size_bytes} B"
|
||
elif size_bytes < 1024 * 1024:
|
||
return f"{size_bytes / 1024:.2f} KB"
|
||
elif size_bytes < 1024 * 1024 * 1024:
|
||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||
else:
|
||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||
|
||
|
||
# ======================= 便捷函数封装 ==========================
|
||
|
||
def get_file_size(file_path):
|
||
"""获取文件大小(字节)的便捷函数"""
|
||
return FileUtils.get_file_size(file_path)
|
||
|
||
|
||
def safe_delete(file_path):
|
||
"""安全删除文件的便捷函数"""
|
||
return FileUtils.safe_delete(file_path)
|
||
|
||
|
||
def get_file_extension(filename):
|
||
"""获取文件扩展名的便捷函数"""
|
||
return FileUtils.get_file_extension(filename)
|