diff --git a/ruoyi-fastapi-backend/app.py b/ruoyi-fastapi-backend/app.py index 364d6b8..97818b6 100644 --- a/ruoyi-fastapi-backend/app.py +++ b/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) diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 3f104db..8b2121f 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/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: """ 缓存目录配置 diff --git a/ruoyi-fastapi-backend/module_admin/controller/common_controller.py b/ruoyi-fastapi-backend/module_admin/controller/common_controller.py index 73aa890..d298149 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/common_controller.py +++ b/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(...)): +@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)) diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py index 072269e..64e929e 100644 --- a/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py +++ b/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 diff --git a/ruoyi-fastapi-backend/module_admin/service/common_service.py b/ruoyi-fastapi-backend/module_admin/service/common_service.py index 447f28a..7c8399d 100644 --- a/ruoyi-fastapi-backend/module_admin/service/common_service.py +++ b/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) diff --git a/ruoyi-fastapi-backend/utils/upload_util.py b/ruoyi-fastapi-backend/utils/upload_util.py new file mode 100644 index 0000000..dc71255 --- /dev/null +++ b/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)