diff --git a/app/router/private/avatar.py b/app/router/private/avatar.py index c7d74ec..165339c 100644 --- a/app/router/private/avatar.py +++ b/app/router/private/avatar.py @@ -40,6 +40,11 @@ async def upload_avatar( # check file check_image(content, 5 * 1024 * 1024, 256, 256) + if url := current_user.avatar_url: + path = storage.get_file_name_by_url(url) + if path: + await storage.delete_file(path) + filehash = hashlib.sha256(content).hexdigest() storage_path = f"avatars/{current_user.id}_{filehash}.png" if not await storage.is_exists(storage_path): diff --git a/app/router/private/cover.py b/app/router/private/cover.py index 2aa71ac..0a4c4ec 100644 --- a/app/router/private/cover.py +++ b/app/router/private/cover.py @@ -40,6 +40,11 @@ async def upload_cover( # check file check_image(content, 10 * 1024 * 1024, 3000, 2000) + if url := current_user.cover["url"]: + path = storage.get_file_name_by_url(url) + if path: + await storage.delete_file(path) + filehash = hashlib.sha256(content).hexdigest() storage_path = f"cover/{current_user.id}_{filehash}.png" if not await storage.is_exists(storage_path): diff --git a/app/router/private/team.py b/app/router/private/team.py index 4b8100f..509b650 100644 --- a/app/router/private/team.py +++ b/app/router/private/team.py @@ -115,6 +115,11 @@ async def update_team( if flag: check_image(flag, 2 * 1024 * 1024, 240, 120) + + if old_flag := team.flag_url: + path = storage.get_file_name_by_url(old_flag) + if path: + await storage.delete_file(path) filehash = hashlib.sha256(flag).hexdigest() storage_path = f"team_flag/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): @@ -122,6 +127,11 @@ async def update_team( team.flag_url = await storage.get_file_url(storage_path) if cover: check_image(cover, 10 * 1024 * 1024, 3000, 2000) + + if old_cover := team.cover_url: + path = storage.get_file_name_by_url(old_cover) + if path: + await storage.delete_file(path) filehash = hashlib.sha256(cover).hexdigest() storage_path = f"team_cover/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): diff --git a/app/storage/aws_s3.py b/app/storage/aws_s3.py index 4c8e6d9..aa76b21 100644 --- a/app/storage/aws_s3.py +++ b/app/storage/aws_s3.py @@ -1,5 +1,7 @@ from __future__ import annotations +from urllib.parse import urlparse + from .base import StorageService import aioboto3 @@ -101,3 +103,23 @@ class AWSS3StorageService(StorageService): return url except ClientError as e: raise RuntimeError(f"Failed to generate file URL: {e}") + + def get_file_name_by_url(self, url: str) -> str | None: + parsed = urlparse(url) + path = parsed.path.lstrip("/") + + # 1. 如果是 public_url_base 拼接出来的 + if self.public_url_base and url.startswith(self.public_url_base.rstrip("/")): + return path + + # 2. 如果是 S3 path-style: s3.amazonaws.com// + if parsed.netloc == "s3.amazonaws.com": + parts = path.split("/", 1) + return parts[1] if len(parts) > 1 else None + + # 3. 如果是 virtual-hosted-style: .s3..amazonaws.com/ + if ".s3." in parsed.netloc or parsed.netloc.endswith(".s3.amazonaws.com"): + return path + + # 4. 直接返回 path + return path or None diff --git a/app/storage/base.py b/app/storage/base.py index 534d7a1..5987e8b 100644 --- a/app/storage/base.py +++ b/app/storage/base.py @@ -30,5 +30,9 @@ class StorageService(abc.ABC): async def get_file_url(self, file_path: str) -> str: raise NotImplementedError + @abc.abstractmethod + def get_file_name_by_url(self, url: str) -> str | None: + raise NotImplementedError + async def close(self) -> None: pass diff --git a/app/storage/cloudflare_r2.py b/app/storage/cloudflare_r2.py index fbb08c7..ddb95ed 100644 --- a/app/storage/cloudflare_r2.py +++ b/app/storage/cloudflare_r2.py @@ -1,5 +1,7 @@ from __future__ import annotations +from urllib.parse import urlparse + from .aws_s3 import AWSS3StorageService @@ -24,3 +26,18 @@ class CloudflareR2StorageService(AWSS3StorageService): @property def endpoint_url(self) -> str: return f"https://{self.account_id}.r2.cloudflarestorage.com" + + def get_file_name_by_url(self, url: str) -> str | None: + if not url: + return None + + parsed = urlparse(url) + path = parsed.path.lstrip("/") + + if self.public_url_base and url.startswith(self.public_url_base.rstrip("/")): + return path + + if ".r2.cloudflarestorage.com" in parsed.netloc: + return path + + return path or None diff --git a/app/storage/local.py b/app/storage/local.py index b60eb35..af0e8ed 100644 --- a/app/storage/local.py +++ b/app/storage/local.py @@ -78,3 +78,8 @@ class LocalStorageService(StorageService): async def get_file_url(self, file_path: str) -> str: return f"{settings.server_url}file/{file_path.lstrip('/')}" + + def get_file_name_by_url(self, url: str) -> str | None: + if not url.startswith(str(settings.server_url)): + return None + return url[len(settings.server_url) + len("file/") :]