| import requests |
| from app.domain.image_models import ImageMetadata, ImageUploader, UploadResponse |
| from enum import Enum |
| from typing import Optional, Any |
|
|
| class UploadErrorType(Enum): |
| """上传错误类型枚举""" |
| NETWORK_ERROR = "network_error" |
| AUTH_ERROR = "auth_error" |
| INVALID_FILE = "invalid_file" |
| SERVER_ERROR = "server_error" |
| PARSE_ERROR = "parse_error" |
| UNKNOWN = "unknown" |
|
|
|
|
| class UploadError(Exception): |
| """图片上传错误异常类""" |
| |
| def __init__( |
| self, |
| message: str, |
| error_type: UploadErrorType = UploadErrorType.UNKNOWN, |
| status_code: Optional[int] = None, |
| details: Optional[dict] = None, |
| original_error: Optional[Exception] = None |
| ): |
| """ |
| 初始化上传错误异常 |
| |
| Args: |
| message: 错误消息 |
| error_type: 错误类型 |
| status_code: HTTP状态码 |
| details: 详细错误信息 |
| original_error: 原始异常 |
| """ |
| self.message = message |
| self.error_type = error_type |
| self.status_code = status_code |
| self.details = details or {} |
| self.original_error = original_error |
| |
| |
| full_message = f"[{error_type.value}] {message}" |
| if status_code: |
| full_message = f"{full_message} (Status: {status_code})" |
| if details: |
| full_message = f"{full_message} - Details: {details}" |
| |
| super().__init__(full_message) |
| |
| @classmethod |
| def from_response(cls, response: Any, message: Optional[str] = None) -> "UploadError": |
| """ |
| 从HTTP响应创建错误实例 |
| |
| Args: |
| response: HTTP响应对象 |
| message: 自定义错误消息 |
| """ |
| try: |
| error_data = response.json() |
| details = error_data.get("data", {}) |
| return cls( |
| message=message or error_data.get("message", "Unknown error"), |
| error_type=UploadErrorType.SERVER_ERROR, |
| status_code=response.status_code, |
| details=details |
| ) |
| except Exception: |
| return cls( |
| message=message or "Failed to parse error response", |
| error_type=UploadErrorType.PARSE_ERROR, |
| status_code=response.status_code |
| ) |
|
|
|
|
| class SmMsUploader(ImageUploader): |
| API_URL = "https://sm.ms/api/v2/upload" |
| |
| def __init__(self, api_key: str): |
| self.api_key = api_key |
| |
| def upload(self, file: bytes, filename: str) -> UploadResponse: |
| try: |
| |
| headers = { |
| "Authorization": f"Basic {self.api_key}" |
| } |
| |
| |
| files = { |
| "smfile": (filename, file, "image/png") |
| } |
| |
| |
| response = requests.post( |
| self.API_URL, |
| headers=headers, |
| files=files |
| ) |
| |
| |
| response.raise_for_status() |
| |
| |
| result = response.json() |
| |
| |
| if not result.get("success"): |
| raise UploadError(result.get("message", "Upload failed")) |
| |
| |
| data = result["data"] |
| image_metadata = ImageMetadata( |
| width=data["width"], |
| height=data["height"], |
| filename=data["filename"], |
| size=data["size"], |
| url=data["url"], |
| delete_url=data["delete"] |
| ) |
| |
| return UploadResponse( |
| success=True, |
| code="success", |
| message="Upload success", |
| data=image_metadata |
| ) |
| |
| except requests.RequestException as e: |
| |
| raise UploadError(f"Upload request failed: {str(e)}") |
| except (KeyError, ValueError) as e: |
| |
| raise UploadError(f"Invalid response format: {str(e)}") |
| except Exception as e: |
| |
| raise UploadError(f"Upload failed: {str(e)}") |
| |
| |
| class QiniuUploader(ImageUploader): |
| def __init__(self, access_key: str, secret_key: str): |
| self.access_key = access_key |
| self.secret_key = secret_key |
| |
| def upload(self, file: bytes, filename: str) -> UploadResponse: |
| |
| pass |
| |
| |
| class PicGoUploader(ImageUploader): |
| """Chevereto API 图片上传器""" |
| |
| def __init__(self, api_key: str, api_url: str = "https://www.picgo.net/api/1/upload"): |
| """ |
| 初始化 Chevereto 上传器 |
| |
| Args: |
| api_key: Chevereto API 密钥 |
| api_url: Chevereto API 上传地址 |
| """ |
| self.api_key = api_key |
| self.api_url = api_url |
| |
| def upload(self, file: bytes, filename: str) -> UploadResponse: |
| """ |
| 上传图片到 Chevereto 服务 |
| |
| Args: |
| file: 图片文件二进制数据 |
| filename: 文件名 |
| |
| Returns: |
| UploadResponse: 上传响应对象 |
| |
| Raises: |
| UploadError: 上传失败时抛出异常 |
| """ |
| try: |
| |
| headers = {} |
| |
| |
| request_url = self.api_url |
| |
| |
| if self.api_url == "https://www.picgo.net/api/1/upload": |
| headers["X-API-Key"] = self.api_key |
| else: |
| |
| from urllib.parse import urlparse, urlunparse, parse_qs, urlencode |
| parsed_url = urlparse(request_url) |
| query_params = parse_qs(parsed_url.query) |
| query_params["key"] = self.api_key |
| new_query = urlencode(query_params, doseq=True) |
| request_url = urlunparse(parsed_url._replace(query=new_query)) |
| |
| |
| files = { |
| "source": (filename, file) |
| } |
| |
| |
| response = requests.post( |
| request_url, |
| headers=headers, |
| files=files |
| ) |
| |
| |
| response.raise_for_status() |
| |
| |
| result = response.json() |
| |
| |
| if "success" in result and "result" in result: |
| |
| if result["success"]: |
| image_url = result["result"][0] if result["result"] and len(result["result"]) > 0 else "" |
| image_metadata = ImageMetadata( |
| width=0, |
| height=0, |
| filename=filename, |
| size=0, |
| url=image_url, |
| delete_url=None |
| ) |
| return UploadResponse( |
| success=True, |
| code="success", |
| message="Upload success", |
| data=image_metadata |
| ) |
| else: |
| raise UploadError( |
| message="Upload failed", |
| error_type=UploadErrorType.SERVER_ERROR, |
| status_code=400, |
| details=result |
| ) |
| |
| |
| |
| if result.get("status_code") != 200: |
| error_message = "Upload failed" |
| if "error" in result: |
| error_message = result["error"].get("message", error_message) |
| raise UploadError( |
| message=error_message, |
| error_type=UploadErrorType.SERVER_ERROR, |
| status_code=result.get("status_code"), |
| details=result.get("error") |
| ) |
| |
| |
| image_data = result.get("image", {}) |
| |
| |
| image_metadata = ImageMetadata( |
| width=image_data.get("width", 0), |
| height=image_data.get("height", 0), |
| filename=image_data.get("filename", filename), |
| size=image_data.get("size", 0), |
| url=image_data.get("url", ""), |
| delete_url=image_data.get("delete_url", None) |
| ) |
| |
| return UploadResponse( |
| success=True, |
| code="success", |
| message=result.get("success", {}).get("message", "Upload success"), |
| data=image_metadata |
| ) |
| |
| except requests.RequestException as e: |
| |
| raise UploadError( |
| message=f"Upload request failed: {str(e)}", |
| error_type=UploadErrorType.NETWORK_ERROR, |
| original_error=e |
| ) |
| except (KeyError, ValueError, TypeError) as e: |
| |
| raise UploadError( |
| message=f"Invalid response format: {str(e)}", |
| error_type=UploadErrorType.PARSE_ERROR, |
| original_error=e |
| ) |
| except UploadError: |
| |
| raise |
| except Exception as e: |
| |
| raise UploadError( |
| message=f"Upload failed: {str(e)}", |
| error_type=UploadErrorType.UNKNOWN, |
| original_error=e |
| ) |
|
|
|
|
| class CloudFlareImgBedUploader(ImageUploader): |
| """CloudFlare图床上传器""" |
|
|
| def __init__(self, auth_code: str, api_url: str, upload_folder: str = ""): |
| """ |
| 初始化CloudFlare图床上传器 |
| |
| Args: |
| auth_code: 认证码 |
| api_url: 上传API地址 |
| upload_folder: 上传文件夹路径(可选) |
| """ |
| self.auth_code = auth_code |
| self.api_url = api_url |
| self.upload_folder = upload_folder |
|
|
| def upload(self, file: bytes, filename: str) -> UploadResponse: |
| """ |
| 上传图片到CloudFlare图床 |
| |
| Args: |
| file: 图片文件二进制数据 |
| filename: 文件名 |
| |
| Returns: |
| UploadResponse: 上传响应对象 |
| |
| Raises: |
| UploadError: 上传失败时抛出异常 |
| """ |
| try: |
| |
| params = [] |
| if self.upload_folder: |
| params.append(f"uploadFolder={self.upload_folder}") |
| if self.auth_code: |
| params.append(f"authCode={self.auth_code}") |
| params.append("uploadNameType=origin") |
|
|
| request_url = f"{self.api_url}?{'&'.join(params)}" |
|
|
| |
| files = { |
| "file": (filename, file) |
| } |
| |
| |
| response = requests.post( |
| request_url, |
| files=files |
| ) |
| |
| |
| response.raise_for_status() |
| |
| |
| result = response.json() |
| |
| |
| if not result or not isinstance(result, list) or len(result) == 0: |
| raise UploadError( |
| message="Invalid response format", |
| error_type=UploadErrorType.PARSE_ERROR |
| ) |
| |
| |
| file_path = result[0].get("src") |
| if not file_path: |
| raise UploadError( |
| message="Missing file URL in response", |
| error_type=UploadErrorType.PARSE_ERROR |
| ) |
| |
| |
| base_url = self.api_url.split("/upload")[0] |
| full_url = file_path if file_path.startswith(("http://", "https://")) else f"{base_url}{file_path}" |
| |
| |
| image_metadata = ImageMetadata( |
| width=0, |
| height=0, |
| filename=filename, |
| size=0, |
| url=full_url, |
| delete_url=None |
| ) |
| |
| return UploadResponse( |
| success=True, |
| code="success", |
| message="Upload success", |
| data=image_metadata |
| ) |
| |
| except requests.RequestException as e: |
| |
| raise UploadError( |
| message=f"Upload request failed: {str(e)}", |
| error_type=UploadErrorType.NETWORK_ERROR, |
| original_error=e |
| ) |
| except (KeyError, ValueError, TypeError, IndexError) as e: |
| |
| raise UploadError( |
| message=f"Invalid response format: {str(e)}", |
| error_type=UploadErrorType.PARSE_ERROR, |
| original_error=e |
| ) |
| except UploadError: |
| |
| raise |
| except Exception as e: |
| |
| raise UploadError( |
| message=f"Upload failed: {str(e)}", |
| error_type=UploadErrorType.UNKNOWN, |
| original_error=e |
| ) |
| |
| class ImageUploaderFactory: |
| @staticmethod |
| def create(provider: str, **credentials) -> ImageUploader: |
| if provider == "smms": |
| return SmMsUploader(credentials["api_key"]) |
| elif provider == "qiniu": |
| return QiniuUploader( |
| credentials["access_key"], |
| credentials["secret_key"] |
| ) |
| elif provider == "picgo": |
| api_url = credentials.get("api_url") or "https://www.picgo.net/api/1/upload" |
| return PicGoUploader(credentials["api_key"], api_url) |
| elif provider == "cloudflare_imgbed": |
| return CloudFlareImgBedUploader( |
| credentials["auth_code"], |
| credentials["base_url"], |
| credentials.get("upload_folder", ""), |
| ) |
| raise ValueError(f"Unknown provider: {provider}") |
|
|