| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- # 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="文件重命名失败")
|