Browse Source

feat: 新增通用上传接口和通用下载接口

master
insistence 1 year ago
parent
commit
ccfe3350b3
  1. 15
      ruoyi-fastapi-backend/app.py
  2. 28
      ruoyi-fastapi-backend/config/env.py
  3. 100
      ruoyi-fastapi-backend/module_admin/controller/common_controller.py
  4. 17
      ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py
  5. 81
      ruoyi-fastapi-backend/module_admin/service/common_service.py
  6. 83
      ruoyi-fastapi-backend/utils/upload_util.py

15
ruoyi-fastapi-backend/app.py

@ -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)

28
ruoyi-fastapi-backend/config/env.py

@ -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:
"""
缓存目录配置

100
ruoyi-fastapi-backend/module_admin/controller/common_controller.py

@ -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(...)):
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="上传成功")
except Exception as e:
logger.exception(e)
return response_500(data="", message=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.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 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}'
},
}
)
)
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 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")
async def common_download(request: Request, background_tasks: BackgroundTasks, file_name: str = Query(alias='fileName'), delete: bool = 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_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 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):
@commonController.get("/download/resource")
async def common_download(request: Request, resource: str = Query()):
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="获取成功")
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))
return ResponseUtil.error(msg=str(e))

17
ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py

@ -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

81
ruoyi-fastapi-backend/module_admin/service/common_service.py

@ -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

@ -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)
Loading…
Cancel
Save