# app/api/endpoints.py from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends from fastapi.responses import StreamingResponse from app.api.deps import get_request_context from typing import List, Optional import urllib.parse from app.models.schemas import FileItem, CreateFolderRequest from app.services.minio_service import minio_service from app.services.img_minio_service import img_minio_service from app.utils.path_utils import validate_path, sanitize_filename from app.utils.logger_utils import logger from app.core.config import settings from app.core.exceptions import ( NotFoundException, ValidationException, InternalServerException, ErrorMessage ) from minio.error import S3Error router = APIRouter(dependencies=[Depends(get_request_context)]) @router.get("/files", response_model=List[FileItem]) async def list_files( path: str = "/", ): # 验证路径 if path != "/": path = validate_path(path, allow_root=True) return minio_service.list_files(path) @router.post("/upload") async def upload_file( file: UploadFile = File(...), path: str = Form(...), ): """ 上传文件到MinIO存储 path: 完整的文件路径 (例如: folder/subfolder/image.png) """ # 验证和清理路径 path = validate_path(path) # 验证文件名 if not file.filename: raise ValidationException(detail="文件名不能为空") filename = sanitize_filename(file.filename) # 获取文件大小 file.file.seek(0, 2) size = file.file.tell() file.file.seek(0) # 检查文件大小限制 if size > settings.MAX_UPLOAD_SIZE: max_size_mb = settings.MAX_UPLOAD_SIZE / (1024 * 1024) raise HTTPException( status_code=413, detail=f"文件大小超过限制 ({max_size_mb}MB)" ) # 构建完整路径 full_path = f"{path}/{filename}" if path else filename logger.info(f"上传文件: {filename}, 目标路径: {full_path}, 大小: {size} bytes") minio_service.upload_file( file.file, full_path, file.content_type or "application/octet-stream", size, ) return {"message": "Upload successful", "filename": filename, "path": full_path} @router.post("/folder") async def create_folder( req: CreateFolderRequest, ): # 验证路径 path = validate_path(req.path) minio_service.create_folder(path) logger.info(f"创建文件夹: {path}") return {"message": "Folder created"} @router.get("/download") async def download_file( path: str, ): # 验证路径 path = validate_path(path) try: data_stream = minio_service.get_file_stream(path) except S3Error as e: if e.code == "NoSuchKey": logger.warning(f"文件不存在: {path}") raise NotFoundException(detail=ErrorMessage.FILE_NOT_FOUND) raise filename = path.split("/")[-1] quoted_filename = urllib.parse.quote(filename) logger.info(f"下载文件: {path}") return StreamingResponse( data_stream, media_type="application/octet-stream", headers={ "Content-Disposition": f"attachment; filename*=UTF-8''{quoted_filename}" } ) @router.delete("/delete") async def delete_item( path: str, ): # 验证路径 path = validate_path(path) is_dir = path.endswith("/") minio_service.delete_file(path, is_dir) logger.info(f"删除{'文件夹' if is_dir else '文件'}: {path}") return {"message": "Deleted successfully"} @router.get("/preview") async def preview_file( path: str, ): # 验证路径 path = validate_path(path) url = minio_service.get_presigned_url(path) return {"url": url} @router.get("/storage-info") async def get_storage_info(): return minio_service.get_storage_info() @router.get("/img") async def preview_image(path: str): # 验证路径 path = validate_path(path) url = img_minio_service.get_presigned_url(path) return {"url": url} @router.post("/img/upload") async def upload_image( file: UploadFile = File(..., description="上传的图片文件"), path: Optional[str] = Form(None, description="指定存储路径,不指定则使用文件名") ): """ 上传图片到MinIO存储 """ # 验证文件名 if not file.filename: raise ValidationException(detail="文件名不能为空") filename = sanitize_filename(file.filename) # 读取文件内容 file_content = await file.read() # 检查文件大小限制 if len(file_content) > settings.MAX_UPLOAD_SIZE: max_size_mb = settings.MAX_UPLOAD_SIZE / (1024 * 1024) raise HTTPException( status_code=413, detail=f"文件大小超过限制 ({max_size_mb}MB)" ) # 生成对象名称:路径 + 文件名 if path: # 验证和清理路径 path = validate_path(path) if not path.endswith('/'): path = path + '/' object_name = f"{path}{filename}" else: # 直接使用文件名 object_name = filename logger.info(f"上传图片: {filename}, 目标路径: {object_name}, 大小: {len(file_content)} bytes") # 上传到MinIO stored_name = img_minio_service.upload_image_data( image_data=file_content, object_name=object_name, content_type=file.content_type or "image/jpeg" ) return { "message": "图片上传成功", "object_name": stored_name, "file_size": len(file_content), "content_type": file.content_type } @router.put("/img/rename") async def rename_image( old_path: str = Form(..., description="原文件路径"), new_path: str = Form(..., description="新文件路径"), overwrite: bool = Form(False, description="是否覆盖已存在的新路径文件") ): """ 重命名或移动图片文件 """ # 验证路径 old_path = validate_path(old_path) new_path = validate_path(new_path) # 检查原文件是否存在 if not img_minio_service.image_exists(old_path): raise NotFoundException(detail="原文件不存在") # 检查新文件是否已存在 if not overwrite and img_minio_service.image_exists(new_path): raise ValidationException( detail="目标文件已存在,如需覆盖请设置 overwrite=true" ) # 执行重命名操作 success = img_minio_service.rename_image(old_path, new_path) if success: logger.info(f"重命名图片: {old_path} -> {new_path}") return { "message": "文件重命名成功", "old_path": old_path, "new_path": new_path } else: raise InternalServerException(detail="文件重命名失败")