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

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

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

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

@ -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")
@commonController.post("/upload", dependencies=[Depends(LoginService.get_current_user), Depends(CheckUserInterfaceAuth('common'))]) async def common_upload(request: Request, file: UploadFile = File(...)):
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(...)):
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: logger.info('上传成功')
pass return ResponseUtil.success(model_content=upload_result.result)
CommonService.upload_service(CachePathConfig.PATH, taskPath, uploadId, file) else:
logger.info('上传成功') logger.warning('上传失败')
return JSONResponse( return ResponseUtil.failure(msg=upload_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")
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, background_tasks: BackgroundTasks, file_name: str = Query(alias='fileName'), delete: bool = Query()):
try: try:
def generate_file(): download_result = CommonService.download_services(background_tasks, file_name, delete)
with open(os.path.join(CachePathConfig.PATH, task_path, task_id, filename), 'rb') as response_file: if download_result.is_success:
yield from response_file logger.info(download_result.message)
return streaming_response_200(data=generate_file()) return ResponseUtil.streaming(data=download_result.result)
else:
logger.warning(download_result.message)
return ResponseUtil.failure(msg=download_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}") @commonController.get("/download/resource")
async def query_system_config(request: Request, config_key: str): async def common_download(request: Request, resource: str = Query()):
try: try:
# 获取全量数据 download_resource_result = CommonService.download_resource_services(resource)
config_query_result = await ConfigService.query_config_list_from_cache_services(request.app.state.redis, config_key) if download_resource_result.is_success:
logger.info('获取成功') logger.info(download_resource_result.message)
return response_200(data=config_query_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: except Exception as e:
logger.exception(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): 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

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

@ -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):
"""
通用上传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}') result = dict(
with open(filepath, 'wb') as f: is_success=True,
# 流式写出大型文件,这里的10代表10MB result=UploadResponseModel(
for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''): fileName=f'{UploadConfig.UPLOAD_PREFIX}/{relative_path}/{filename}',
f.write(chunk) 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