mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(前端): 统一表单搜索组件样式和布局 style: 优化表格和卡片样式,调整高度和间距 fix(上传): 修复文件上传组件name属性缺失问题 perf(IP定位): 优化IP定位逻辑,增加内网IP判断和重试机制 docs: 更新密码修改成功后的提示信息 chore: 清理无用代码和注释 ``` 这个提交消息包含了多个方面的改进: 1. 新增功能(feat):为用户表添加了avatar字段 2. 重构(refactor):统一了前端表单搜索组件的样式和布局 3. 样式优化(style):调整了表格和卡片的样式 4. 问题修复(fix):修复了文件上传组件name属性缺失的问题 5. 性能优化(perf):改进了IP定位服务的逻辑 6. 文档更新(docs):修改了密码修改成功后的提示信息 7. 杂项(chore):清理了无用的代码和注释 消息遵循了提交规范,每个部分都简明扼要地描述了变更内容。
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import random
|
|
import mimetypes
|
|
from datetime import datetime
|
|
from typing import List, Dict, Tuple
|
|
import aiofiles
|
|
from fastapi import UploadFile
|
|
from pathlib import Path
|
|
|
|
from app.config.setting import settings
|
|
from app.core.exceptions import CustomException
|
|
from app.core.logger import logger
|
|
|
|
class UploadUtil:
|
|
"""
|
|
上传工具类
|
|
"""
|
|
|
|
@staticmethod
|
|
def generate_random_number() -> str:
|
|
"""生成3位数字构成的字符串"""
|
|
return f'{random.randint(1, 999):03}'
|
|
|
|
@staticmethod
|
|
def check_file_exists(filepath: Path) -> bool:
|
|
"""检查文件是否存在"""
|
|
return filepath.exists()
|
|
|
|
@staticmethod
|
|
def check_file_extension(file: UploadFile) -> bool:
|
|
"""检查文件后缀是否合法"""
|
|
file_extension = mimetypes.guess_extension(file.content_type)
|
|
return file_extension in settings.ALLOWED_EXTENSIONS if file_extension else False
|
|
|
|
@staticmethod
|
|
def check_file_timestamp(filename: str) -> bool:
|
|
"""校验文件时间戳是否合法"""
|
|
try:
|
|
name_parts = filename.rsplit('.', 1)[0].split('_')
|
|
timestamp = name_parts[-1].split(settings.UPLOAD_MACHINE)[0]
|
|
datetime.strptime(timestamp, '%Y%m%d%H%M%S')
|
|
return True
|
|
except (ValueError, IndexError):
|
|
return False
|
|
|
|
@staticmethod
|
|
def check_file_machine(filename: str) -> bool:
|
|
"""校验文件机器码是否合法"""
|
|
try:
|
|
name_without_ext = filename.rsplit('.', 1)[0]
|
|
return len(name_without_ext) >= 4 and name_without_ext[-4] == settings.UPLOAD_MACHINE
|
|
except IndexError:
|
|
return False
|
|
|
|
@staticmethod
|
|
def check_file_random_code(filename: str) -> bool:
|
|
"""校验文件随机码是否合法"""
|
|
try:
|
|
code = filename.rsplit('.', 1)[0][-3:]
|
|
return code.isdigit() and 1 <= int(code) <= 999
|
|
except IndexError:
|
|
return False
|
|
|
|
@staticmethod
|
|
def check_file_size(file: UploadFile) -> bool:
|
|
"""校验文件大小是否合法"""
|
|
return file.size <= settings.MAX_FILE_SIZE
|
|
|
|
@classmethod
|
|
def generate_file_name(cls, filename: str) -> str:
|
|
"""生成文件名称"""
|
|
name, ext = filename.rsplit(".", 1)
|
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
return f'{name}_{timestamp}{settings.UPLOAD_MACHINE}{cls.generate_random_number()}.{ext}'
|
|
|
|
@staticmethod
|
|
def generate_file(filepath: Path, chunk_size: int = 8192):
|
|
"""根据文件生成二进制数据"""
|
|
with filepath.open('rb') as f:
|
|
while chunk := f.read(chunk_size):
|
|
yield chunk
|
|
|
|
@staticmethod
|
|
def delete_file(filepath: Path) -> bool:
|
|
"""
|
|
删除文件
|
|
:return: 删除是否成功
|
|
"""
|
|
try:
|
|
filepath.unlink(missing_ok=True)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
@classmethod
|
|
async def upload_file(cls, file: UploadFile) -> Tuple[str, Path]:
|
|
"""
|
|
文件上传
|
|
:param file: 上传的文件对象
|
|
:raises CustomException: 当文件类型不支持或大小超限时抛出
|
|
:return: (文件名, 文件路径)的元组
|
|
"""
|
|
# 文件校验
|
|
if not all([cls.check_file_extension(file), cls.check_file_size(file)]):
|
|
raise CustomException(msg='文件类型或大小不合法')
|
|
|
|
try:
|
|
# 构建完整的目录路径
|
|
dir_path = settings.UPLOAD_FILE_PATH.joinpath(datetime.now().strftime("%Y/%m/%d"))
|
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 生成文件名并保存
|
|
filename = cls.generate_file_name(file.filename)
|
|
filepath = dir_path / filename
|
|
# filepath.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 分块写入文件
|
|
chunk_size = 8 * 1024 * 1024 # 8MB chunks
|
|
async with aiofiles.open(filepath, 'wb') as f:
|
|
while chunk := await file.read(chunk_size):
|
|
await f.write(chunk)
|
|
|
|
# 返回相对路径
|
|
return filename, filepath
|
|
|
|
except Exception as e:
|
|
logger.error(f"文件上传失败: {e}")
|
|
raise CustomException(msg='文件上传失败')
|
|
|
|
@staticmethod
|
|
def get_file_tree(file_path: str) -> List[Dict]:
|
|
"""
|
|
获取文件树结构
|
|
:param file_path: 文件路径
|
|
:return: 文件树列表
|
|
"""
|
|
return [item for item in Path(file_path).iterdir()]
|