feat: 新增通用上传接口和通用下载接口
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
import uvicorn
|
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
import uvicorn
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from module_admin.controller.login_controller import loginController
|
from module_admin.controller.login_controller import loginController
|
||||||
from module_admin.controller.captcha_controller import captchaController
|
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.server_controller import serverController
|
||||||
from module_admin.controller.cache_controller import cacheController
|
from module_admin.controller.cache_controller import cacheController
|
||||||
from module_admin.controller.common_controller import commonController
|
from module_admin.controller.common_controller import commonController
|
||||||
|
from config.env import UploadConfig
|
||||||
from config.get_redis import RedisUtil
|
from config.get_redis import RedisUtil
|
||||||
from config.get_db import init_create_table
|
from config.get_db import init_create_table
|
||||||
from config.get_scheduler import SchedulerUtil
|
from config.get_scheduler import SchedulerUtil
|
||||||
@@ -46,8 +48,7 @@ app = FastAPI(
|
|||||||
title='RuoYi-FastAPI',
|
title='RuoYi-FastAPI',
|
||||||
description='RuoYi-FastAPI接口文档',
|
description='RuoYi-FastAPI接口文档',
|
||||||
version='1.0.0',
|
version='1.0.0',
|
||||||
lifespan=lifespan,
|
lifespan=lifespan
|
||||||
root_path='/dev-api'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 前端页面url
|
# 前端页面url
|
||||||
@@ -65,6 +66,12 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 实例化UploadConfig,确保应用启动时上传目录存在
|
||||||
|
upload_config = UploadConfig()
|
||||||
|
|
||||||
|
# 挂载静态文件路径
|
||||||
|
app.mount(f"{upload_config.UPLOAD_PREFIX}", StaticFiles(directory=f"{upload_config.UPLOAD_PATH}"), name="profile")
|
||||||
|
|
||||||
|
|
||||||
# 自定义token检验异常
|
# 自定义token检验异常
|
||||||
@app.exception_handler(AuthException)
|
@app.exception_handler(AuthException)
|
||||||
@@ -109,4 +116,4 @@ for controller in controller_list:
|
|||||||
app.include_router(router=controller.get('router'), tags=controller.get('tags'))
|
app.include_router(router=controller.get('router'), tags=controller.get('tags'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
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:
|
class CachePathConfig:
|
||||||
"""
|
"""
|
||||||
缓存目录配置
|
缓存目录配置
|
||||||
|
@@ -1,87 +1,53 @@
|
|||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter
|
||||||
from fastapi import Depends, File, Form, Query
|
from fastapi import Depends, File, Query
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from config.env import CachePathConfig
|
|
||||||
from config.get_db import get_db
|
|
||||||
from module_admin.service.login_service import LoginService
|
from module_admin.service.login_service import LoginService
|
||||||
from module_admin.service.common_service import *
|
from module_admin.service.common_service import *
|
||||||
from module_admin.service.config_service import ConfigService
|
|
||||||
from utils.response_util import *
|
from utils.response_util import *
|
||||||
from utils.log_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")
|
||||||
|
async def common_upload(request: Request, file: UploadFile = File(...)):
|
||||||
|
|
||||||
@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(...)):
|
|
||||||
try:
|
try:
|
||||||
try:
|
upload_result = CommonService.upload_service(request, file)
|
||||||
os.makedirs(os.path.join(CachePathConfig.PATH, taskPath, uploadId))
|
if upload_result.is_success:
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
CommonService.upload_service(CachePathConfig.PATH, taskPath, uploadId, file)
|
|
||||||
logger.info('上传成功')
|
logger.info('上传成功')
|
||||||
return response_200(data={'filename': file.filename, 'path': f'/common/{CachePathConfig.PATHSTR}?taskPath={taskPath}&taskId={uploadId}&filename={file.filename}'}, message="上传成功")
|
return ResponseUtil.success(model_content=upload_result.result)
|
||||||
|
else:
|
||||||
|
logger.warning('上传失败')
|
||||||
|
return ResponseUtil.failure(msg=upload_result.message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(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'))])
|
@commonController.get("/download")
|
||||||
async def editor_upload(request: Request, baseUrl: str = Form(), uploadId: str = Form(), taskPath: str = Form(), file: UploadFile = File(...)):
|
async def common_download(request: Request, background_tasks: BackgroundTasks, file_name: str = Query(alias='fileName'), delete: bool = Query()):
|
||||||
try:
|
try:
|
||||||
try:
|
download_result = CommonService.download_services(background_tasks, file_name, delete)
|
||||||
os.makedirs(os.path.join(CachePathConfig.PATH, taskPath, uploadId))
|
if download_result.is_success:
|
||||||
except FileExistsError:
|
logger.info(download_result.message)
|
||||||
pass
|
return ResponseUtil.streaming(data=download_result.result)
|
||||||
CommonService.upload_service(CachePathConfig.PATH, taskPath, uploadId, file)
|
else:
|
||||||
logger.info('上传成功')
|
logger.warning(download_result.message)
|
||||||
return JSONResponse(
|
return ResponseUtil.failure(msg=download_result.message)
|
||||||
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}'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return JSONResponse(
|
return ResponseUtil.error(msg=str(e))
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
content=jsonable_encoder(
|
|
||||||
{
|
|
||||||
'errno': 1,
|
|
||||||
'message': str(e),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@commonController.get(f"/{CachePathConfig.PATHSTR}")
|
@commonController.get("/download/resource")
|
||||||
async def common_download(request: Request, task_path: str = Query(alias='taskPath'), task_id: str = Query(alias='taskId'), filename: str = Query()):
|
async def common_download(request: Request, resource: str = Query()):
|
||||||
try:
|
try:
|
||||||
def generate_file():
|
download_resource_result = CommonService.download_resource_services(resource)
|
||||||
with open(os.path.join(CachePathConfig.PATH, task_path, task_id, filename), 'rb') as response_file:
|
if download_resource_result.is_success:
|
||||||
yield from response_file
|
logger.info(download_resource_result.message)
|
||||||
return streaming_response_200(data=generate_file())
|
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:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return response_500(data="", message=str(e))
|
return ResponseUtil.error(msg=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))
|
|
||||||
|
@@ -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):
|
class CrudResponseModel(BaseModel):
|
||||||
@@ -7,3 +9,16 @@ class CrudResponseModel(BaseModel):
|
|||||||
"""
|
"""
|
||||||
is_success: bool
|
is_success: bool
|
||||||
message: str
|
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
|
import os
|
||||||
from fastapi import UploadFile
|
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:
|
class CommonService:
|
||||||
@@ -8,11 +13,75 @@ class CommonService:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def upload_service(cls, path: str, task_path: str, upload_id: str, file: UploadFile):
|
def upload_service(cls, request: Request, file: UploadFile):
|
||||||
|
"""
|
||||||
filepath = os.path.join(path, task_path, upload_id, f'{file.filename}')
|
通用上传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:
|
with open(filepath, 'wb') as f:
|
||||||
# 流式写出大型文件,这里的10代表10MB
|
# 流式写出大型文件,这里的10代表10MB
|
||||||
for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''):
|
for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''):
|
||||||
f.write(chunk)
|
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