feat: 新增通用上传接口和通用下载接口
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
from fastapi import FastAPI, Request
|
||||
import uvicorn
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import uvicorn
|
||||
from contextlib import asynccontextmanager
|
||||
from module_admin.controller.login_controller import loginController
|
||||
from module_admin.controller.captcha_controller import captchaController
|
||||
@@ -19,6 +20,7 @@ from module_admin.controller.job_controller import jobController
|
||||
from module_admin.controller.server_controller import serverController
|
||||
from module_admin.controller.cache_controller import cacheController
|
||||
from module_admin.controller.common_controller import commonController
|
||||
from config.env import UploadConfig
|
||||
from config.get_redis import RedisUtil
|
||||
from config.get_db import init_create_table
|
||||
from config.get_scheduler import SchedulerUtil
|
||||
@@ -46,8 +48,7 @@ app = FastAPI(
|
||||
title='RuoYi-FastAPI',
|
||||
description='RuoYi-FastAPI接口文档',
|
||||
version='1.0.0',
|
||||
lifespan=lifespan,
|
||||
root_path='/dev-api'
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# 前端页面url
|
||||
@@ -65,6 +66,12 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 实例化UploadConfig,确保应用启动时上传目录存在
|
||||
upload_config = UploadConfig()
|
||||
|
||||
# 挂载静态文件路径
|
||||
app.mount(f"{upload_config.UPLOAD_PREFIX}", StaticFiles(directory=f"{upload_config.UPLOAD_PATH}"), name="profile")
|
||||
|
||||
|
||||
# 自定义token检验异常
|
||||
@app.exception_handler(AuthException)
|
||||
@@ -109,4 +116,4 @@ for controller in controller_list:
|
||||
app.include_router(router=controller.get('router'), tags=controller.get('tags'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(app='app:app', host="0.0.0.0", port=9099, reload=True)
|
||||
uvicorn.run(app='app:app', host="0.0.0.0", port=9099, root_path='/dev-api', reload=True)
|
||||
|
@@ -33,6 +33,34 @@ class RedisConfig:
|
||||
DB = 2
|
||||
|
||||
|
||||
class UploadConfig:
|
||||
"""
|
||||
上传配置
|
||||
"""
|
||||
UPLOAD_PREFIX = '/profile'
|
||||
UPLOAD_PATH = 'vf_admin/upload_path'
|
||||
UPLOAD_MACHINE = 'A'
|
||||
DEFAULT_ALLOWED_EXTENSION = [
|
||||
# 图片
|
||||
"bmp", "gif", "jpg", "jpeg", "png",
|
||||
# word excel powerpoint
|
||||
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
|
||||
# 压缩文件
|
||||
"rar", "zip", "gz", "bz2",
|
||||
# 视频格式
|
||||
"mp4", "avi", "rmvb",
|
||||
# pdf
|
||||
"pdf"
|
||||
]
|
||||
DOWNLOAD_PATH = 'vf_admin/download_path'
|
||||
|
||||
def __init__(self):
|
||||
if not os.path.exists(self.UPLOAD_PATH):
|
||||
os.makedirs(self.UPLOAD_PATH)
|
||||
if not os.path.exists(self.DOWNLOAD_PATH):
|
||||
os.makedirs(self.DOWNLOAD_PATH)
|
||||
|
||||
|
||||
class CachePathConfig:
|
||||
"""
|
||||
缓存目录配置
|
||||
|
@@ -1,87 +1,53 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import Depends, File, Form, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from config.env import CachePathConfig
|
||||
from config.get_db import get_db
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends, File, Query
|
||||
from module_admin.service.login_service import LoginService
|
||||
from module_admin.service.common_service import *
|
||||
from module_admin.service.config_service import ConfigService
|
||||
from utils.response_util import *
|
||||
from utils.log_util import *
|
||||
from module_admin.aspect.interface_auth import CheckUserInterfaceAuth
|
||||
from typing import Optional
|
||||
|
||||
commonController = APIRouter(prefix='/common', dependencies=[Depends(LoginService.get_current_user)])
|
||||
|
||||
|
||||
commonController = APIRouter(prefix='/common')
|
||||
|
||||
|
||||
@commonController.post("/upload", dependencies=[Depends(LoginService.get_current_user), Depends(CheckUserInterfaceAuth('common'))])
|
||||
async def common_upload(request: Request, taskPath: str = Form(), uploadId: str = Form(), file: UploadFile = File(...)):
|
||||
@commonController.post("/upload")
|
||||
async def common_upload(request: Request, file: UploadFile = File(...)):
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.join(CachePathConfig.PATH, taskPath, uploadId))
|
||||
except FileExistsError:
|
||||
pass
|
||||
CommonService.upload_service(CachePathConfig.PATH, taskPath, uploadId, file)
|
||||
logger.info('上传成功')
|
||||
return response_200(data={'filename': file.filename, 'path': f'/common/{CachePathConfig.PATHSTR}?taskPath={taskPath}&taskId={uploadId}&filename={file.filename}'}, message="上传成功")
|
||||
upload_result = CommonService.upload_service(request, file)
|
||||
if upload_result.is_success:
|
||||
logger.info('上传成功')
|
||||
return ResponseUtil.success(model_content=upload_result.result)
|
||||
else:
|
||||
logger.warning('上传失败')
|
||||
return ResponseUtil.failure(msg=upload_result.message)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return response_500(data="", message=str(e))
|
||||
return ResponseUtil.error(msg=str(e))
|
||||
|
||||
|
||||
@commonController.post("/uploadForEditor", dependencies=[Depends(LoginService.get_current_user), Depends(CheckUserInterfaceAuth('common'))])
|
||||
async def editor_upload(request: Request, baseUrl: str = Form(), uploadId: str = Form(), taskPath: str = Form(), file: UploadFile = File(...)):
|
||||
@commonController.get("/download")
|
||||
async def common_download(request: Request, background_tasks: BackgroundTasks, file_name: str = Query(alias='fileName'), delete: bool = Query()):
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.join(CachePathConfig.PATH, taskPath, uploadId))
|
||||
except FileExistsError:
|
||||
pass
|
||||
CommonService.upload_service(CachePathConfig.PATH, taskPath, uploadId, file)
|
||||
logger.info('上传成功')
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
content=jsonable_encoder(
|
||||
{
|
||||
'errno': 0,
|
||||
'data': {
|
||||
'url': f'{baseUrl}/common/{CachePathConfig.PATHSTR}?taskPath={taskPath}&taskId={uploadId}&filename={file.filename}'
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
download_result = CommonService.download_services(background_tasks, file_name, delete)
|
||||
if download_result.is_success:
|
||||
logger.info(download_result.message)
|
||||
return ResponseUtil.streaming(data=download_result.result)
|
||||
else:
|
||||
logger.warning(download_result.message)
|
||||
return ResponseUtil.failure(msg=download_result.message)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=jsonable_encoder(
|
||||
{
|
||||
'errno': 1,
|
||||
'message': str(e),
|
||||
}
|
||||
)
|
||||
)
|
||||
return ResponseUtil.error(msg=str(e))
|
||||
|
||||
|
||||
@commonController.get(f"/{CachePathConfig.PATHSTR}")
|
||||
async def common_download(request: Request, task_path: str = Query(alias='taskPath'), task_id: str = Query(alias='taskId'), filename: str = Query()):
|
||||
@commonController.get("/download/resource")
|
||||
async def common_download(request: Request, resource: str = Query()):
|
||||
try:
|
||||
def generate_file():
|
||||
with open(os.path.join(CachePathConfig.PATH, task_path, task_id, filename), 'rb') as response_file:
|
||||
yield from response_file
|
||||
return streaming_response_200(data=generate_file())
|
||||
download_resource_result = CommonService.download_resource_services(resource)
|
||||
if download_resource_result.is_success:
|
||||
logger.info(download_resource_result.message)
|
||||
return ResponseUtil.streaming(data=download_resource_result.result)
|
||||
else:
|
||||
logger.warning(download_resource_result.message)
|
||||
return ResponseUtil.failure(msg=download_resource_result.message)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return response_500(data="", message=str(e))
|
||||
|
||||
|
||||
@commonController.get("/config/query/{config_key}")
|
||||
async def query_system_config(request: Request, config_key: str):
|
||||
try:
|
||||
# 获取全量数据
|
||||
config_query_result = await ConfigService.query_config_list_from_cache_services(request.app.state.redis, config_key)
|
||||
logger.info('获取成功')
|
||||
return response_200(data=config_query_result, message="获取成功")
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return response_500(data="", message=str(e))
|
||||
return ResponseUtil.error(msg=str(e))
|
||||
|
@@ -1,4 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic.alias_generators import to_camel
|
||||
from typing import Optional, Any
|
||||
|
||||
|
||||
class CrudResponseModel(BaseModel):
|
||||
@@ -7,3 +9,16 @@ class CrudResponseModel(BaseModel):
|
||||
"""
|
||||
is_success: bool
|
||||
message: str
|
||||
result: Optional[Any] = None
|
||||
|
||||
|
||||
class UploadResponseModel(BaseModel):
|
||||
"""
|
||||
上传响应模型
|
||||
"""
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
|
||||
file_name: Optional[str] = None
|
||||
new_file_name: Optional[str] = None
|
||||
original_filename: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
|
@@ -1,5 +1,10 @@
|
||||
from fastapi import Request, BackgroundTasks
|
||||
import os
|
||||
from fastapi import UploadFile
|
||||
from datetime import datetime
|
||||
from config.env import UploadConfig
|
||||
from module_admin.entity.vo.common_vo import *
|
||||
from utils.upload_util import UploadUtil
|
||||
|
||||
|
||||
class CommonService:
|
||||
@@ -8,11 +13,75 @@ class CommonService:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def upload_service(cls, path: str, task_path: str, upload_id: str, file: UploadFile):
|
||||
def upload_service(cls, request: Request, file: UploadFile):
|
||||
"""
|
||||
通用上传service
|
||||
:param request: Request对象
|
||||
:param file: 上传文件对象
|
||||
:return: 上传结果
|
||||
"""
|
||||
if not UploadUtil.check_file_extension(file):
|
||||
result = dict(is_success=False, message='文件类型不合法')
|
||||
else:
|
||||
relative_path = f'upload/{datetime.now().strftime("%Y")}/{datetime.now().strftime("%m")}/{datetime.now().strftime("%d")}'
|
||||
dir_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path)
|
||||
try:
|
||||
os.makedirs(dir_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
filename = f'{file.filename.rsplit(".", 1)[0]}_{datetime.now().strftime("%Y%m%d%H%M%S")}{UploadConfig.UPLOAD_MACHINE}{UploadUtil.generate_random_number()}.{file.filename.rsplit(".")[-1]}'
|
||||
filepath = os.path.join(dir_path, filename)
|
||||
with open(filepath, 'wb') as f:
|
||||
# 流式写出大型文件,这里的10代表10MB
|
||||
for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''):
|
||||
f.write(chunk)
|
||||
|
||||
filepath = os.path.join(path, task_path, upload_id, f'{file.filename}')
|
||||
with open(filepath, 'wb') as f:
|
||||
# 流式写出大型文件,这里的10代表10MB
|
||||
for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''):
|
||||
f.write(chunk)
|
||||
result = dict(
|
||||
is_success=True,
|
||||
result=UploadResponseModel(
|
||||
fileName=f'{UploadConfig.UPLOAD_PREFIX}/{relative_path}/{filename}',
|
||||
newFileName=filename,
|
||||
originalFilename=file.filename,
|
||||
url=f'{request.base_url}{UploadConfig.UPLOAD_PREFIX[1:]}/{relative_path}/{filename}'
|
||||
),
|
||||
message='上传成功'
|
||||
)
|
||||
|
||||
return CrudResponseModel(**result)
|
||||
|
||||
@classmethod
|
||||
def download_services(cls, background_tasks: BackgroundTasks, file_name, delete: bool):
|
||||
"""
|
||||
下载下载目录文件service
|
||||
:param background_tasks: 后台任务对象
|
||||
:param file_name: 下载的文件名称
|
||||
:param delete: 是否在下载完成后删除文件
|
||||
:return: 上传结果
|
||||
"""
|
||||
filepath = os.path.join(UploadConfig.DOWNLOAD_PATH, file_name)
|
||||
if '..' in file_name:
|
||||
result = dict(is_success=False, message='文件名称不合法')
|
||||
elif not UploadUtil.check_file_exists(filepath):
|
||||
result = dict(is_success=False, message='文件不存在')
|
||||
else:
|
||||
result = dict(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功')
|
||||
if delete:
|
||||
background_tasks.add_task(UploadUtil.delete_file, filepath)
|
||||
return CrudResponseModel(**result)
|
||||
|
||||
@classmethod
|
||||
def download_resource_services(cls, resource: str):
|
||||
"""
|
||||
下载上传目录文件service
|
||||
:param resource: 下载的文件名称
|
||||
:return: 上传结果
|
||||
"""
|
||||
filepath = os.path.join(resource.replace(UploadConfig.UPLOAD_PREFIX, UploadConfig.UPLOAD_PATH))
|
||||
filename = resource.rsplit("/", 1)[-1]
|
||||
if '..' in filename or not UploadUtil.check_file_timestamp(filename) or not UploadUtil.check_file_machine(filename) or not UploadUtil.check_file_random_code(filename):
|
||||
result = dict(is_success=False, message='文件名称不合法')
|
||||
elif not UploadUtil.check_file_exists(filepath):
|
||||
result = dict(is_success=False, message='文件不存在')
|
||||
else:
|
||||
result = dict(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功')
|
||||
return CrudResponseModel(**result)
|
||||
|
83
ruoyi-fastapi-backend/utils/upload_util.py
Normal file
83
ruoyi-fastapi-backend/utils/upload_util.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import random
|
||||
import os
|
||||
from fastapi import UploadFile
|
||||
from datetime import datetime
|
||||
from config.env import UploadConfig
|
||||
|
||||
|
||||
class UploadUtil:
|
||||
"""
|
||||
上传工具类
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def generate_random_number(cls):
|
||||
"""
|
||||
生成3位数字构成的字符串
|
||||
"""
|
||||
random_number = random.randint(1, 999)
|
||||
|
||||
return f'{random_number:03}'
|
||||
|
||||
@classmethod
|
||||
def check_file_exists(cls, filepath):
|
||||
"""
|
||||
检查文件是否存在
|
||||
"""
|
||||
return os.path.exists(filepath)
|
||||
|
||||
@classmethod
|
||||
def check_file_extension(cls, file: UploadFile):
|
||||
"""
|
||||
检查文件后缀是否合法
|
||||
"""
|
||||
file_extension = file.filename.rsplit('.', 1)[-1]
|
||||
if file_extension in UploadConfig.DEFAULT_ALLOWED_EXTENSION:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def check_file_timestamp(cls, filename):
|
||||
"""
|
||||
校验文件时间戳是否合法
|
||||
"""
|
||||
timestamp = filename.rsplit('.', 1)[0].split('_')[-1].split(UploadConfig.UPLOAD_MACHINE)[0]
|
||||
try:
|
||||
datetime.strptime(timestamp, '%Y%m%d%H%M%S')
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def check_file_machine(cls, filename):
|
||||
"""
|
||||
校验文件机器码是否合法
|
||||
"""
|
||||
if filename.rsplit('.', 1)[0][-4] == UploadConfig.UPLOAD_MACHINE:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def check_file_random_code(cls, filename):
|
||||
"""
|
||||
校验文件随机码是否合法
|
||||
"""
|
||||
valid_code_list = [f"{i:03}" for i in range(1, 999)]
|
||||
if filename.rsplit('.', 1)[0][-3:] in valid_code_list:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def generate_file(cls, filepath):
|
||||
"""
|
||||
根据文件生成二进制数据
|
||||
"""
|
||||
with open(filepath, 'rb') as response_file:
|
||||
yield from response_file
|
||||
|
||||
@classmethod
|
||||
def delete_file(cls, filepath: str):
|
||||
"""
|
||||
根据文件路径删除对应文件
|
||||
"""
|
||||
os.remove(filepath)
|
Reference in New Issue
Block a user