Spaces:
Sleeping
Sleeping
feat: add Cloudinary support as an alternative image storage mode
Browse files- app/core/config.py +6 -1
- app/services/storage.py +51 -0
- pyproject.toml +1 -0
app/core/config.py
CHANGED
|
@@ -73,7 +73,7 @@ class Settings(BaseSettings):
|
|
| 73 |
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/0"
|
| 74 |
|
| 75 |
# Storage Configurations
|
| 76 |
-
STORAGE_MODE: str = "local" # "local" hoặc "
|
| 77 |
UPLOAD_DIR: str = "uploads"
|
| 78 |
SERVER_HOST_URL: str = "http://localhost:8000"
|
| 79 |
|
|
@@ -83,6 +83,11 @@ class Settings(BaseSettings):
|
|
| 83 |
R2_SECRET_ACCESS_KEY: str = "change_me_r2_secret_key"
|
| 84 |
R2_ENDPOINT_URL: str = "https://change-me-account-id.r2.cloudflarestorage.com"
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
# Qdrant Vector DB
|
| 88 |
QDRANT_HOST: str = "localhost"
|
|
|
|
| 73 |
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/0"
|
| 74 |
|
| 75 |
# Storage Configurations
|
| 76 |
+
STORAGE_MODE: str = "local" # "local", "r2", hoặc "cloudinary"
|
| 77 |
UPLOAD_DIR: str = "uploads"
|
| 78 |
SERVER_HOST_URL: str = "http://localhost:8000"
|
| 79 |
|
|
|
|
| 83 |
R2_SECRET_ACCESS_KEY: str = "change_me_r2_secret_key"
|
| 84 |
R2_ENDPOINT_URL: str = "https://change-me-account-id.r2.cloudflarestorage.com"
|
| 85 |
|
| 86 |
+
# Cloudinary Configs
|
| 87 |
+
CLOUDINARY_CLOUD_NAME: Optional[str] = None
|
| 88 |
+
CLOUDINARY_API_KEY: Optional[str] = None
|
| 89 |
+
CLOUDINARY_API_SECRET: Optional[str] = None
|
| 90 |
+
|
| 91 |
|
| 92 |
# Qdrant Vector DB
|
| 93 |
QDRANT_HOST: str = "localhost"
|
app/services/storage.py
CHANGED
|
@@ -15,6 +15,16 @@ class StorageService:
|
|
| 15 |
aws_secret_access_key=settings.R2_SECRET_ACCESS_KEY,
|
| 16 |
endpoint_url=settings.R2_ENDPOINT_URL
|
| 17 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
else:
|
| 19 |
self.s3_client = None
|
| 20 |
|
|
@@ -42,6 +52,25 @@ class StorageService:
|
|
| 42 |
except Exception as e:
|
| 43 |
logging.error(f"Failed to upload local file: {e}")
|
| 44 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
else:
|
| 46 |
# Cloudflare R2 Upload
|
| 47 |
try:
|
|
@@ -85,6 +114,28 @@ class StorageService:
|
|
| 85 |
except Exception as e:
|
| 86 |
logging.error(f"Failed to delete local file: {e}")
|
| 87 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
else:
|
| 89 |
try:
|
| 90 |
if not self.s3_client:
|
|
|
|
| 15 |
aws_secret_access_key=settings.R2_SECRET_ACCESS_KEY,
|
| 16 |
endpoint_url=settings.R2_ENDPOINT_URL
|
| 17 |
)
|
| 18 |
+
elif settings.STORAGE_MODE == "cloudinary":
|
| 19 |
+
self.s3_client = None
|
| 20 |
+
import cloudinary
|
| 21 |
+
import cloudinary.uploader
|
| 22 |
+
cloudinary.config(
|
| 23 |
+
cloud_name=settings.CLOUDINARY_CLOUD_NAME,
|
| 24 |
+
api_key=settings.CLOUDINARY_API_KEY,
|
| 25 |
+
api_secret=settings.CLOUDINARY_API_SECRET,
|
| 26 |
+
secure=True
|
| 27 |
+
)
|
| 28 |
else:
|
| 29 |
self.s3_client = None
|
| 30 |
|
|
|
|
| 52 |
except Exception as e:
|
| 53 |
logging.error(f"Failed to upload local file: {e}")
|
| 54 |
return None
|
| 55 |
+
elif settings.STORAGE_MODE == "cloudinary":
|
| 56 |
+
try:
|
| 57 |
+
import cloudinary.uploader
|
| 58 |
+
# Parse folder path and filename from sanitized path
|
| 59 |
+
path_parts = sanitized_object_name.split("/")
|
| 60 |
+
folder = "wardrobe/" + "/".join(path_parts[:-1]) if len(path_parts) > 1 else "wardrobe"
|
| 61 |
+
filename = os.path.splitext(path_parts[-1])[0]
|
| 62 |
+
|
| 63 |
+
result = cloudinary.uploader.upload(
|
| 64 |
+
file_content,
|
| 65 |
+
folder=folder,
|
| 66 |
+
public_id=filename,
|
| 67 |
+
overwrite=True,
|
| 68 |
+
resource_type="auto"
|
| 69 |
+
)
|
| 70 |
+
return result.get("secure_url")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logging.error(f"Failed to upload file to Cloudinary: {e}")
|
| 73 |
+
return None
|
| 74 |
else:
|
| 75 |
# Cloudflare R2 Upload
|
| 76 |
try:
|
|
|
|
| 114 |
except Exception as e:
|
| 115 |
logging.error(f"Failed to delete local file: {e}")
|
| 116 |
return False
|
| 117 |
+
elif settings.STORAGE_MODE == "cloudinary":
|
| 118 |
+
try:
|
| 119 |
+
import cloudinary.uploader
|
| 120 |
+
import re
|
| 121 |
+
|
| 122 |
+
# Parse public_id from Cloudinary URL:
|
| 123 |
+
# https://res.cloudinary.com/<cloud_name>/image/upload/v<version>/<folder>/<public_id>.<ext>
|
| 124 |
+
if "res.cloudinary.com" in object_name_or_url:
|
| 125 |
+
match = re.search(r"/upload/(?:v\d+/)?(.+)$", object_name_or_url)
|
| 126 |
+
if match:
|
| 127 |
+
public_id_with_ext = match.group(1)
|
| 128 |
+
public_id = os.path.splitext(public_id_with_ext)[0]
|
| 129 |
+
else:
|
| 130 |
+
public_id = object_name_or_url
|
| 131 |
+
else:
|
| 132 |
+
public_id = object_name_or_url
|
| 133 |
+
|
| 134 |
+
result = cloudinary.uploader.destroy(public_id)
|
| 135 |
+
return result.get("result") == "ok"
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logging.error(f"Failed to delete file from Cloudinary: {e}")
|
| 138 |
+
return False
|
| 139 |
else:
|
| 140 |
try:
|
| 141 |
if not self.s3_client:
|
pyproject.toml
CHANGED
|
@@ -42,4 +42,5 @@ dependencies = [
|
|
| 42 |
"huggingface-hub>=0.23.0",
|
| 43 |
"matplotlib>=3.9.1",
|
| 44 |
"tqdm>=4.66.4",
|
|
|
|
| 45 |
]
|
|
|
|
| 42 |
"huggingface-hub>=0.23.0",
|
| 43 |
"matplotlib>=3.9.1",
|
| 44 |
"tqdm>=4.66.4",
|
| 45 |
+
"cloudinary>=1.41.0",
|
| 46 |
]
|