zh 5 meses atrás
pai
commit
a49cac601e
7 arquivos alterados com 356 adições e 42 exclusões
  1. 6 1
      .env
  2. 24 26
      .idea/workspace.xml
  3. 93 1
      app/api/endpoints.py
  4. 6 1
      app/core/config.py
  5. 2 2
      app/main.py
  6. 225 0
      app/services/img_minio_service.py
  7. 0 11
      test_main.http

+ 6 - 1
.env

@@ -3,8 +3,13 @@ PROJECT_NAME=MinIO FileManager
 API_V1_STR=/api/minio-manager
 
 # MinIO 配置
-MINIO_ENDPOINT=minio:9000
+MINIO_ENDPOINT=192.168.20.253:9000
 MINIO_ACCESS_KEY=6596a2de4a92cfe9
 MINIO_SECRET_KEY=809d6749bb70e83bca08b8e5a614f9d8
 MINIO_BUCKET_NAME=user-dufs
 MINIO_SECURE=False  # 如果是HTTPS则为True
+
+
+IMG_MINIO_ACCESS_KEY=b5c3c3e4f8101e01
+IMG_MINIO_SECRET_KEY=dee7898f13dbb7b5cbaedc5c26c6e084
+IMG_MINIO_BUCKET_NAME=img

+ 24 - 26
.idea/workspace.xml

@@ -5,26 +5,13 @@
   </component>
   <component name="ChangeListManager">
     <list default="true" id="faca4961-58ca-45f4-8a0e-73812dfa125a" name="更改" comment="">
-      <change afterPath="$PROJECT_DIR$/.env" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/minio_file_manager.iml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/api/__init__.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/api/endpoints.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/core/__init__.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/core/config.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/main.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/models/__init__.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/models/schemas.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/services/__init__.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/app/services/minio_service.py" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
-      <change afterPath="$PROJECT_DIR$/test_main.http" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/app/services/img_minio_service.py" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/.env" beforeDir="false" afterPath="$PROJECT_DIR$/.env" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/app/api/endpoints.py" beforeDir="false" afterPath="$PROJECT_DIR$/app/api/endpoints.py" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/app/core/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/app/core/config.py" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/app/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/app/main.py" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/test_main.http" beforeDir="false" />
     </list>
     <option name="SHOW_DIALOG" value="false" />
     <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -41,11 +28,16 @@
     </option>
   </component>
   <component name="Git.Settings">
+    <option name="RECENT_BRANCH_BY_REPOSITORY">
+      <map>
+        <entry key="$PROJECT_DIR$" value="token" />
+      </map>
+    </option>
     <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
   </component>
-  <component name="ProjectColorInfo"><![CDATA[{
-  "associatedIndex": 5
-}]]></component>
+  <component name="ProjectColorInfo">{
+  &quot;associatedIndex&quot;: 5
+}</component>
   <component name="ProjectId" id="35sKloTsijaSj4MdoM972beWAFB" />
   <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
   <component name="ProjectViewState">
@@ -57,7 +49,8 @@
     "FastAPI.minio_file_manager.executor": "Run",
     "RunOnceActivity.OpenProjectViewOnStart": "true",
     "RunOnceActivity.ShowReadmeOnStart": "true",
-    "git-widget-placeholder": "main",
+    "git-widget-placeholder": "master",
+    "last_opened_file_path": "D:/python/minio-manager/app/services",
     "node.js.detected.package.eslint": "true",
     "node.js.detected.package.tslint": "true",
     "node.js.selected.package.eslint": "(autodetect)",
@@ -68,6 +61,10 @@
   }
 }]]></component>
   <component name="RecentsManager">
+    <key name="CopyFile.RECENT_KEYS">
+      <recent name="D:\python\minio-manager\app\services" />
+      <recent name="D:\python\minio-manager\app\core" />
+    </key>
     <key name="MoveFile.RECENT_KEYS">
       <recent name="$PROJECT_DIR$" />
       <recent name="$PROJECT_DIR$/app" />
@@ -75,13 +72,13 @@
   </component>
   <component name="RunManager">
     <configuration name="minio_file_manager" type="Python.FastAPI">
-      <option name="additionalOptions" value="--host 0.0.0.0 --port 8000 --reload" />
+      <option name="additionalOptions" value="--host 0.0.0.0 --port 9002 --reload" />
       <option name="file" value="$PROJECT_DIR$/app/main.py" />
       <module name="minio_file_manager" />
       <option name="ENV_FILES" value="" />
       <option name="INTERPRETER_OPTIONS" value="" />
       <option name="PARENT_ENVS" value="true" />
-      <option name="SDK_HOME" value="$PROJECT_DIR$/../anaconda/miniconda3/envs/minio/bin/python" />
+      <option name="SDK_HOME" value="$PROJECT_DIR$/../conda/envs/minio/python.exe" />
       <option name="SDK_NAME" value="minio" />
       <option name="WORKING_DIRECTORY" value="" />
       <option name="IS_MODULE_SDK" value="false" />
@@ -126,6 +123,7 @@
     </option>
   </component>
   <component name="com.intellij.coverage.CoverageDataManagerImpl">
+    <SUITE FILE_PATH="coverage/minio_manager$minio_file_manager.coverage" NAME="minio_file_manager 覆盖结果" MODIFIED="1764076024158" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
     <SUITE FILE_PATH="coverage/minio_file_manager$minio_file_manager.coverage" NAME="minio_file_manager 覆盖结果" MODIFIED="1763911913066" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
   </component>
 </project>

+ 93 - 1
app/api/endpoints.py

@@ -1,11 +1,12 @@
 # app/api/endpoints.py
 from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Query
 from fastapi.responses import StreamingResponse
-from typing import List
+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 minio.error import S3Error
 
 router = APIRouter()
@@ -104,3 +105,94 @@ async def get_storage_info():
         return minio_service.get_storage_info()
     except S3Error as e:
         raise HTTPException(status_code=500, detail=str(e))
+
+
+
+@router.get("/img")
+async def preview_file(path: str):
+    try:
+        url = img_minio_service.get_presigned_url(path)
+        return {"url": url}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/img/upload")
+async def upload_image(
+        file: UploadFile = File(..., description="上传的图片文件"),
+        path: Optional[str] = Form(..., description="指定存储路径,不指定则使用随机文件名")
+):
+    """
+    上传图片到MinIO存储
+    """
+    try:
+        # 读取文件内容
+        file_content = await file.read()
+
+        # 生成对象名称:路径 + 文件名
+        if path:
+            # 清理路径格式,确保以/结尾
+            if not path.endswith('/'):
+                path = path + '/'
+            object_name = f"{path}{file.filename}"
+        else:
+            # 直接使用文件名
+            object_name = file.filename
+
+        # 上传到MinIO
+        stored_name = img_minio_service.upload_image_data(
+            image_data=file_content,
+            object_name=object_name,
+            content_type=file.content_type
+        )
+
+        return {
+            "message": "图片上传成功",
+            "object_name": stored_name,
+            "file_size": len(file_content),
+            "content_type": file.content_type
+        }
+
+    except Exception as e:
+        print(e)
+        raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}")
+
+
+@router.put("/img/rename")
+async def rename_image(
+        old_path: str = Form(..., description="原文件路径"),
+        new_path: str = Form(..., description="新文件路径"),
+        overwrite: bool = Form(False, description="是否覆盖已存在的新路径文件")
+):
+    """
+    重命名或移动图片文件
+    """
+    try:
+        # 检查原文件是否存在
+        if not img_minio_service.image_exists(old_path):
+            raise HTTPException(status_code=404, detail="原文件不存在")
+
+        # 检查新文件是否已存在
+        if not overwrite and img_minio_service.image_exists(new_path):
+            raise HTTPException(
+                status_code=400,
+                detail="目标文件已存在,如需覆盖请设置 overwrite=true"
+            )
+
+        # 执行重命名操作
+        success = img_minio_service.rename_image(old_path, new_path)
+
+        if success:
+            return {
+                "message": "文件重命名成功",
+                "old_path": old_path,
+                "new_path": new_path
+            }
+        else:
+            raise HTTPException(status_code=500, detail="文件重命名失败")
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        print(e)
+        raise HTTPException(status_code=500, detail=f"重命名失败: {str(e)}")

+ 6 - 1
app/core/config.py

@@ -7,10 +7,15 @@ class Settings(BaseSettings):
     API_V1_STR: str
 
     MINIO_ENDPOINT: str
+    MINIO_SECURE: bool = False
+
     MINIO_ACCESS_KEY: str
     MINIO_SECRET_KEY: str
     MINIO_BUCKET_NAME: str
-    MINIO_SECURE: bool = False
+
+    IMG_MINIO_ACCESS_KEY: str
+    IMG_MINIO_SECRET_KEY: str
+    IMG_MINIO_BUCKET_NAME: str
 
     class Config:
         env_file = ".env"

+ 2 - 2
app/main.py

@@ -12,7 +12,7 @@ app = FastAPI(
 # CORS 配置
 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["*"], # 生产环境建议改为具体的 ["http://localhost:8080"]
+    allow_origins=["*"],
     allow_credentials=True,
     allow_methods=["*"],
     allow_headers=["*"],
@@ -27,4 +27,4 @@ async def root():
 
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run("app.main:app", host="0.0.0.0", port=8080, reload=True)
+    uvicorn.run("app.main:app", host="0.0.0.0", port=9002, reload=True)

+ 225 - 0
app/services/img_minio_service.py

@@ -0,0 +1,225 @@
+import io
+
+from minio.commonconfig import CopySource
+
+from app.core.config import settings
+from minio import Minio
+from minio.error import S3Error
+from datetime import timedelta
+import os
+from typing import Optional, Tuple
+
+
+class ImgMinioService:
+    def __init__(self):
+        self.client = Minio(
+            settings.MINIO_ENDPOINT,
+            access_key=settings.IMG_MINIO_ACCESS_KEY,
+            secret_key=settings.IMG_MINIO_SECRET_KEY,
+            secure=settings.MINIO_SECURE,
+            region='us-east-1'  # 明确指定region
+        )
+        self.bucket = settings.IMG_MINIO_BUCKET_NAME
+        self._ensure_bucket_exists()
+
+    def _ensure_bucket_exists(self):
+        if not self.client.bucket_exists(self.bucket):
+            self.client.make_bucket(self.bucket)
+
+    def upload_image(self, file_path: str, object_name: Optional[str] = None,
+                     content_type: str = "image/jpeg") -> str:
+        """
+        上传图片到MinIO
+
+        Args:
+            file_path: 本地文件路径
+            object_name: MinIO中存储的对象名称,如果为None则使用文件名
+            content_type: 文件类型,默认为image/jpeg
+
+        Returns:
+            存储的对象名称
+        """
+        try:
+            # 如果未指定对象名称,使用文件名
+            if object_name is None:
+                object_name = os.path.basename(file_path)
+
+            # 确保对象名称是字符串
+            object_name = str(object_name)
+
+            # 上传文件
+            self.client.fput_object(
+                bucket_name=self.bucket,
+                object_name=object_name,
+                file_path=file_path,
+                content_type=content_type
+            )
+
+            return object_name
+
+        except S3Error as e:
+            print(f"上传图片错误: {e}")
+            raise e
+        except Exception as e:
+            print(f"上传文件时发生错误: {e}")
+            raise e
+
+    def upload_image_data(self, image_data: bytes, object_name: str,
+                          content_type: str = "image/jpeg") -> str:
+        """
+        上传图片数据到MinIO
+
+        Args:
+            image_data: 图片二进制数据
+            object_name: MinIO中存储的对象名称
+            content_type: 文件类型
+
+        Returns:
+            存储的对象名称
+        """
+        try:
+            # 将 bytes 数据包装成 BytesIO(类文件对象)
+            file_like_data = io.BytesIO(image_data)
+
+            # 上传文件数据
+            self.client.put_object(
+                bucket_name=self.bucket,
+                object_name=object_name,
+                data=file_like_data,  # 传入类文件对象
+                length=len(image_data),
+                content_type=content_type
+            )
+
+            return object_name
+
+        except S3Error as e:
+            print(f"上传图片数据错误: {e}")
+            raise e
+
+
+    def rename_image(self, old_path: str, new_path: str) -> bool:
+        """
+        重命名图片(通过复制后删除原文件实现)
+
+        Args:
+            old_path: 原文件路径
+            new_path: 新文件路径
+
+        Returns:
+            重命名是否成功
+        """
+        try:
+            # 检查原文件是否存在
+            try:
+                self.client.stat_object(self.bucket, old_path)
+            except S3Error as e:
+                print(f"原文件不存在: {e}")
+                return False
+
+            # 创建 CopySource 对象
+            copy_source = CopySource(self.bucket, old_path)
+
+            # 复制文件到新路径
+            self.client.copy_object(
+                self.bucket,
+                new_path,
+                copy_source
+            )
+
+            # 删除原文件
+            self.client.remove_object(self.bucket, old_path)
+
+            return True
+
+        except S3Error as e:
+            print(f"重命名图片错误: {e}")
+            # 如果复制成功但删除失败,尝试回滚
+            try:
+                self.client.remove_object(self.bucket, new_path)
+            except:
+                pass
+            return False
+
+    def delete_file(self, path: str, is_dir: bool = False):
+        if is_dir:
+            # 递归删除
+            objects_to_delete = self.client.list_objects(self.bucket, prefix=path, recursive=True)
+            for obj in objects_to_delete:
+                self.client.remove_object(self.bucket, obj.object_name)
+        else:
+            self.client.remove_object(self.bucket, path)
+
+    def get_presigned_url(self, path: str, expires_hours: int = 1):
+        """
+        生成预签名URL,用于临时访问私有文件
+        """
+        try:
+            return self.client.presigned_get_object(
+                self.bucket,
+                path,
+                expires=timedelta(hours=expires_hours)
+            )
+        except S3Error as e:
+            print(f"生成预签名URL错误: {e}")
+            raise e
+
+    def get_storage_info(self):
+        """获取存储桶使用情况"""
+        try:
+            # 获取存储桶的总大小
+            total_size = 0
+            objects = self.client.list_objects(self.bucket, recursive=True)
+
+            for obj in objects:
+                total_size += obj.size
+
+            # 这里可以设置存储桶的总容量(根据实际情况调整)
+            total_capacity = 10 * 1024 * 1024 * 1024  # 10GB 示例
+
+            used_percentage = (total_size / total_capacity) * 100 if total_capacity > 0 else 0
+
+            return {
+                "used_size": total_size,
+                "total_capacity": total_capacity,
+                "used_percentage": round(used_percentage, 1),
+                "available_size": total_capacity - total_size
+            }
+        except S3Error as e:
+            raise e
+
+    def list_images(self, prefix: str = "") -> list:
+        """
+        列出指定前缀的图片
+
+        Args:
+            prefix: 对象前缀,用于筛选
+
+        Returns:
+            图片对象列表
+        """
+        try:
+            objects = self.client.list_objects(self.bucket, prefix=prefix, recursive=True)
+            return [obj.object_name for obj in objects]
+        except S3Error as e:
+            print(f"列出图片错误: {e}")
+            raise e
+
+    def image_exists(self, path: str) -> bool:
+        """
+        检查图片是否存在
+
+        Args:
+            path: 图片路径
+
+        Returns:
+            是否存在
+        """
+        try:
+            self.client.stat_object(self.bucket, path)
+            return True
+        except S3Error:
+            return False
+
+
+# 实例化单例
+img_minio_service = ImgMinioService()

+ 0 - 11
test_main.http

@@ -1,11 +0,0 @@
-# Test your FastAPI endpoints
-
-GET http://127.0.0.1:8000/
-Accept: application/json
-
-###
-
-GET http://127.0.0.1:8000/hello/User
-Accept: application/json
-
-###