| 258 | |
| 259 | |
| 260 | class S3FileStorage(FileStorageInterface): |
| 261 | def __init__(self): |
| 262 | self.access_key_id = settings.s3_access_key_id |
| 263 | self.secret_access_key = settings.s3_secret_access_key |
| 264 | self.bucket_name = settings.s3_bucket_name |
| 265 | self.s3_hostname = settings.s3_hostname |
| 266 | self.region_name = settings.s3_region_name |
| 267 | self.signature_version = settings.s3_signature_version |
| 268 | self.endpoint_url = settings.s3_endpoint_url or f"https://{self.s3_hostname}" |
| 269 | self.aws_session_token = settings.aws_session_token |
| 270 | self.addressing_style = str(settings.s3_addressing_style or "auto").lower() |
| 271 | self.proxy = settings.s3_proxy |
| 272 | self.session = aioboto3.Session( |
| 273 | aws_access_key_id=self.access_key_id, |
| 274 | aws_secret_access_key=self.secret_access_key, |
| 275 | ) |
| 276 | if not settings.s3_endpoint_url: |
| 277 | self.endpoint_url = f"https://{self.s3_hostname}" |
| 278 | else: |
| 279 | # 如果提供了 s3_endpoint_url,则优先使用它 |
| 280 | self.endpoint_url = settings.s3_endpoint_url |
| 281 | |
| 282 | def _client_config(self) -> Config: |
| 283 | config = {"signature_version": self.signature_version} |
| 284 | s3_config = {} |
| 285 | if self.addressing_style in {"path", "virtual", "auto"}: |
| 286 | s3_config["addressing_style"] = self.addressing_style |
| 287 | if s3_config: |
| 288 | config["s3"] = s3_config |
| 289 | return Config(**config) |
| 290 | |
| 291 | def _client(self): |
| 292 | return self.session.client( |
| 293 | "s3", |
| 294 | endpoint_url=self.endpoint_url, |
| 295 | aws_session_token=self.aws_session_token, |
| 296 | region_name=self.region_name, |
| 297 | config=self._client_config(), |
| 298 | ) |
| 299 | |
| 300 | async def save_file(self, file: UploadFile, save_path: str): |
| 301 | async with self._client() as s3: |
| 302 | # 使用 upload_fileobj 流式上传,避免将整个文件加载到内存 |
| 303 | await s3.upload_fileobj( |
| 304 | file.file, |
| 305 | self.bucket_name, |
| 306 | save_path, |
| 307 | ExtraArgs={"ContentType": file.content_type or "application/octet-stream"}, |
| 308 | ) |
| 309 | |
| 310 | async def delete_file(self, file_code: FileCodes): |
| 311 | async with self._client() as s3: |
| 312 | await s3.delete_object( |
| 313 | Bucket=self.bucket_name, Key=await file_code.get_file_path() |
| 314 | ) |
| 315 | |
| 316 | async def get_file_response(self, file_code: FileCodes): |
| 317 | try: |
nothing calls this directly
no outgoing calls
no test coverage detected