项目初次提交

This commit is contained in:
2025-04-11 08:54:28 +08:00
commit 9e14a3256f
220 changed files with 15673 additions and 0 deletions

7
utils/file/__init__.py Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/12/12 14:30
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : 简要说明

106
utils/file/aliyun_oss.py Normal file
View File

@ -0,0 +1,106 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/4/28 22:32
# @File : aliyun_oss.py
# @IDE : PyCharm
# @desc : 阿里云对象存储
import os.path
from fastapi import UploadFile
from pydantic import BaseModel
import oss2 # 安装依赖库pip install oss2
from oss2.models import PutObjectResult
from core.exception import CustomException
from core.logger import logger
from utils import status
from utils.file.file_manage import FileManage
from utils.file.file_base import FileBase
class BucketConf(BaseModel):
accessKeyId: str
accessKeySecret: str
endpoint: str
bucket: str
baseUrl: str
class AliyunOSS(FileBase):
"""
阿里云对象存储
常见报错https://help.aliyun.com/document_detail/185228.htm?spm=a2c4g.11186623.0.0.6de530e5pxNK76#concept-1957777
官方文档https://help.aliyun.com/document_detail/32026.html
使用Python SDK时大部分操作都是通过oss2.Service和oss2.Bucket两个类进行。
oss2.Service类用于列举存储空间。
oss2.Bucket类用于上传、下载、删除文件以及对存储空间进行各种配置。
"""
def __init__(self, bucket: BucketConf):
# 阿里云账号AccessKey拥有所有API的访问权限风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维请登录RAM控制台创建RAM用户。
auth = oss2.Auth(bucket.accessKeyId, bucket.accessKeySecret)
# yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1杭州为例Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
# 填写Bucket名称。
self.bucket = oss2.Bucket(auth, bucket.endpoint, bucket.bucket)
self.baseUrl = bucket.baseUrl
async def upload_image(self, path: str, file: UploadFile, max_size: int = 10) -> str:
"""
上传图片
:param path: path由包含文件后缀不包含Bucket名称组成的Object完整路径例如abc/efg/123.jpg。
:param file: 文件对象
:param max_size: 图片文件最大值,单位 MB默认 10MB
:return: 上传后的文件oss链接
"""
# 验证图片类型
await self.validate_file(file, max_size, self.IMAGE_ACCEPT)
# 生成文件路径
path = self.generate_relative_path(path, file.filename)
file_data = await file.read()
return await self.__upload_file_to_oss(path, file_data)
async def upload_video(self, path: str, file: UploadFile, max_size: int = 100) -> str:
"""
上传视频
:param path: path由包含文件后缀不包含Bucket名称组成的Object完整路径例如abc/efg/123.jpg。
:param file: 文件对象
:param max_size: 视频文件最大值,单位 MB默认 100MB
:return: 上传后的文件oss链接
"""
# 验证图片类型
await self.validate_file(file, max_size, self.VIDEO_ACCEPT)
# 生成文件路径
path = self.generate_relative_path(path, file.filename)
file_data = await file.read()
return await self.__upload_file_to_oss(path, file_data)
async def upload_file(self, path: str, file: UploadFile) -> str:
"""
上传文件
:param path: path由包含文件后缀不包含Bucket名称组成的Object完整路径例如abc/efg/123.jpg。
:param file: 文件对象
:return: 上传后的文件oss链接
"""
path = self.generate_relative_path(path, file.filename)
file_data = await file.read()
return await self.__upload_file_to_oss(path, file_data)
async def __upload_file_to_oss(self, path: str, file_data: bytes) -> str:
"""
上传文件到OSS
:param path: path由包含文件后缀不包含Bucket名称组成的Object完整路径例如abc/efg/123.jpg。
:param file_data: 文件数据
:return: 上传后的文件oss链接
"""
result = self.bucket.put_object(path, file_data)
assert isinstance(result, PutObjectResult)
if result.status != 200:
logger.error(f"文件上传到OSS失败状态码{result.status}")
raise CustomException("上传文件失败", code=status.HTTP_ERROR)
return self.baseUrl + path

148
utils/file/file_base.py Normal file
View File

@ -0,0 +1,148 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/12/12 14:31
# @File : file_base.py
# @IDE : PyCharm
# @desc : 简要说明
import datetime
import os
from pathlib import Path
from aiopathlib import AsyncPath
from fastapi import UploadFile
from application.settings import TEMP_DIR, STATIC_ROOT
from core.exception import CustomException
from utils import status
from utils.tools import generate_string
class FileBase:
IMAGE_ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/x-icon"]
VIDEO_ACCEPT = ["video/mp4", "video/mpeg"]
AUDIO_ACCEPT = ["audio/wav", "audio/mp3", "audio/m4a", "audio/wma", "audio/ogg", "audio/mpeg", "audio/x-wav"]
ALL_ACCEPT = [*IMAGE_ACCEPT, *VIDEO_ACCEPT, *AUDIO_ACCEPT]
@classmethod
def get_random_filename(cls, suffix: str) -> str:
"""
生成随机文件名称,生成规则:当前时间戳 + 8位随机字符串拼接
:param suffix: 文件后缀
:return:
"""
if not suffix.startswith("."):
suffix = "." + suffix
return f"{str(int(datetime.datetime.now().timestamp())) + str(generate_string(8))}{suffix}"
@classmethod
def get_today_timestamp(cls) -> str:
"""
获取当天时间戳
:return:
"""
return str(int((datetime.datetime.now().replace(hour=0, minute=0, second=0)).timestamp()))
@classmethod
def generate_relative_path(cls, path: str, filename: str = None, suffix: str = None) -> str:
"""
生成相对路径,生成规则:自定义目录/当天日期时间戳/随机文件名称
1. filename 参数或者 suffix 参数必须填写一个
2. filename 参数和 suffix 参数都存在则优先取 suffix 参数为后缀
:param path: static 指定目录类别
:param filename: 文件名称,只用户获取后缀,不做真实文件名称,避免文件重复问题
:param suffix: 文件后缀
"""
if not filename and not suffix:
raise ValueError("filename 参数或者 suffix 参数必须填写一个")
elif not suffix and filename:
suffix = os.path.splitext(filename)[-1]
path = path.replace("\\", "/")
if path[0] == "/":
path = path[1:]
if path[-1] == "/":
path = path[:-1]
today = datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d")
return f"{path}/{today}/{cls.get_random_filename(suffix)}"
@classmethod
def generate_static_file_path(cls, path: str, filename: str = None, suffix: str = None) -> str:
"""
生成 static 静态文件路径,生成规则:自定义目录/当天日期时间戳/随机文件名称
1. filename 参数或者 suffix 参数必须填写一个
2. filename 参数和 suffix 参数都存在则优先取 suffix 参数为后缀
:param path: static 指定目录类别
:param filename: 文件名称,只用户获取后缀,不做真实文件名称,避免文件重复问题
:param suffix: 文件后缀
:return:
"""
return f"{STATIC_ROOT}/{cls.generate_relative_path(path, filename, suffix)}"
@classmethod
def generate_temp_file_path(cls, filename: str = None, suffix: str = None) -> str:
"""
生成临时文件路径,生成规则:
1. filename 参数或者 suffix 参数必须填写一个
2. filename 参数和 suffix 参数都存在则优先取 suffix 参数为后缀
:param filename: 文件名称
:param suffix: 文件后缀
:return:
"""
if not filename and not suffix:
raise ValueError("filename 参数或者 suffix 参数必须填写一个")
elif not suffix and filename:
suffix = os.path.splitext(filename)[-1]
return f"{cls.generate_temp_dir_path()}/{cls.get_random_filename(suffix)}"
@classmethod
def generate_temp_dir_path(cls) -> str:
"""
生成临时目录路径
:return:
"""
date = datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d")
file_dir = Path(TEMP_DIR) / date
if not file_dir.exists():
file_dir.mkdir(parents=True, exist_ok=True)
return str(file_dir).replace("\\", "/")
@classmethod
async def async_generate_temp_file_path(cls, filename: str) -> str:
"""
生成临时文件路径
:param filename: 文件名称
:return:
"""
return f"{await cls.async_generate_temp_dir_path()}/{filename}"
@classmethod
async def async_generate_temp_dir_path(cls) -> str:
"""
生成临时目录路径
:return:
"""
date = datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d")
file_dir = AsyncPath(TEMP_DIR) / date
path = file_dir / (generate_string(4) + str(int(datetime.datetime.now().timestamp())))
if not await path.exists():
await path.mkdir(parents=True, exist_ok=True)
return str(path).replace("\\", "/")
@classmethod
async def validate_file(cls, file: UploadFile, max_size: int = None, mime_types: list = None) -> bool:
"""
验证文件是否符合格式
:param file: 文件
:param max_size: 文件最大值,单位 MB
:param mime_types: 支持的文件类型
"""
if max_size:
size = len(await file.read()) / 1024 / 1024
if size > max_size:
raise CustomException(f"上传文件过大,不能超过{max_size}MB", status.HTTP_ERROR)
await file.seek(0)
if mime_types:
if file.content_type not in mime_types:
raise CustomException(f"上传文件格式错误,只支持 {'/'.join(mime_types)} 格式!", status.HTTP_ERROR)
return True

141
utils/file/file_manage.py Normal file
View File

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/12/5 8:45
# @File : file_manage.py
# @IDE : PyCharm
# @desc : 保存图片到本地
import asyncio
import io
import os
import zipfile
from application.settings import STATIC_ROOT, BASE_DIR, STATIC_URL
from fastapi import UploadFile
import sys
from core.exception import CustomException
from utils.file.file_base import FileBase
from aiopathlib import AsyncPath
import aioshutil
class FileManage(FileBase):
"""
上传文件管理
"""
def __init__(self, file: UploadFile, path: str):
self.path = self.generate_static_file_path(path, file.filename)
self.file = file
async def save_image_local(self, accept: list = None) -> dict:
"""
保存图片文件到本地
:param accept:
:return:
"""
if accept is None:
accept = self.IMAGE_ACCEPT
await self.validate_file(self.file, max_size=5, mime_types=accept)
return await self.async_save_local()
async def save_audio_local(self, accept: list = None) -> dict:
"""
保存音频文件到本地
:param accept:
:return:
"""
if accept is None:
accept = self.AUDIO_ACCEPT
await self.validate_file(self.file, max_size=50, mime_types=accept)
return await self.async_save_local()
async def save_video_local(self, accept: list = None) -> dict:
"""
保存视频文件到本地
:param accept:
:return:
"""
if accept is None:
accept = self.VIDEO_ACCEPT
await self.validate_file(self.file, max_size=100, mime_types=accept)
return await self.async_save_local()
async def async_save_local(self) -> dict:
"""
保存文件到本地
:return: 示例:
{
'local_path': 'D:\\business\\kinit_dev\\aicheckv2-api\\static\\system\\20240301\\1709303205HuYB3mrC.png',
'remote_path': '/media/system/20240301/1709303205HuYB3mrC.png'
}
"""
path = AsyncPath(self.path)
if sys.platform == "win32":
path = AsyncPath(self.path.replace("/", "\\"))
if not await path.parent.exists():
await path.parent.mkdir(parents=True, exist_ok=True)
await path.write_bytes(await self.file.read())
return {
"local_path": str(path),
"remote_path": STATIC_URL + str(path).replace(STATIC_ROOT, '').replace("\\", '/')
}
@classmethod
async def async_save_temp_file(cls, file: UploadFile) -> str:
"""
保存临时文件
:param file:
:return:
"""
temp_file_path = await cls.async_generate_temp_file_path(file.filename)
await AsyncPath(temp_file_path).write_bytes(await file.read())
return temp_file_path
@classmethod
async def unzip(cls, file: UploadFile, dir_path: str) -> str:
"""
解压 zip 压缩包
:param file:
:param dir_path: 解压路径
:return:
"""
if file.content_type != "application/x-zip-compressed":
raise CustomException("上传文件类型错误,必须是 zip 压缩包格式!")
# 读取上传的文件内容
contents = await file.read()
# 将文件内容转换为字节流
zip_stream = io.BytesIO(contents)
# 使用zipfile库解压字节流
with zipfile.ZipFile(zip_stream, "r") as zip_ref:
zip_ref.extractall(dir_path)
return dir_path
@staticmethod
async def async_copy_file(src: str, dst: str) -> None:
"""
异步复制文件
根目录为项目根目录,传过来的文件路径均为相对路径
:param src: 原始文件
:param dst: 目标路径。绝对路径
"""
if src[0] == "/":
src = src.lstrip("/")
src = AsyncPath(BASE_DIR) / src
if not await src.exists():
raise CustomException(f"{src} 源文件不存在!")
dst = AsyncPath(dst)
if not await dst.parent.exists():
await dst.parent.mkdir(parents=True, exist_ok=True)
await aioshutil.copyfile(src, dst)
@staticmethod
async def async_copy_dir(src: str, dst: str, dirs_exist_ok: bool = True) -> None:
"""
复制目录
:param src: 源目录
:param dst: 目标目录
:param dirs_exist_ok: 是否覆盖
"""
if not os.path.exists(dst):
raise CustomException("目标目录不存在!")
await aioshutil.copytree(src, dst, dirs_exist_ok=dirs_exist_ok)