-
Notifications
You must be signed in to change notification settings - Fork 0
✨feat: 프로필 이미지 업로드 기능 추가 #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d9e48d4
feat: add OCIObjectStorageService for profile image management
swfs0417 ff755c9
feat: 프로필 이미지 업로드 및 다운로드 기능 추가
swfs0417 0026dab
feat: .codex/config.toml 파일 삭제
swfs0417 27346ca
feat: OCIObjectStorageService에서 예외 처리 추가 및 public_url 기본값 사용 테스트 추가
swfs0417 9fcc35c
Support configurable OCI object storage authentication modes
swfs0417 556727c
Merge remote-tracking branch 'origin/main' into feat/profilepic
swfs0417 3f871ef
feat: OCIObjectStorageService에서 config_file 인증 방식 개선
swfs0417 058997b
feat: OCIObjectStorageService에서 config_file 인증 방식 개선 및 프로파일 설정 추가
swfs0417 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| from app.routes import auth, projects, upload, users | ||
| from app.routes import auth, profile_image, projects, users | ||
|
|
||
| __all__ = ["auth", "users", "projects", "upload"] | ||
| __all__ = ["auth", "users", "projects", "profile_image"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from fastapi import APIRouter, Depends, File, UploadFile | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from app.config.database import get_db | ||
| from app.deps.auth import require_associate | ||
| from app.exceptions import InvalidProfileImageError, ProfileImageTooLargeError | ||
| from app.models import User | ||
| from app.schemas import Response, UserDetail | ||
| from app.services import OCIObjectStorageService, UserService | ||
|
|
||
| router = APIRouter() | ||
|
|
||
| MAX_PROFILE_IMAGE_SIZE = 5 * 1024 * 1024 | ||
| PROFILE_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp"} | ||
|
|
||
|
|
||
| @router.post("/upload", response_model=Response[UserDetail]) | ||
| async def upload_profile_image( | ||
| file: UploadFile = File(...), | ||
| current_user: User = Depends(require_associate), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| if file.content_type not in PROFILE_IMAGE_TYPES: | ||
| raise InvalidProfileImageError() | ||
|
|
||
| body = await file.read() | ||
| if len(body) > MAX_PROFILE_IMAGE_SIZE: | ||
| raise ProfileImageTooLargeError() | ||
|
|
||
| storage = OCIObjectStorageService() | ||
| old_url = current_user.avatar_url | ||
| avatar_url = storage.upload_profile_image(current_user.id, file, body) | ||
| updated_user = UserService.update(db, current_user, avatar_url=avatar_url) | ||
| storage.delete_profile_image(current_user.id, old_url) | ||
|
|
||
| return Response(ok=True, data=updated_user) |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import os | ||
| from urllib.parse import quote, unquote, urlparse | ||
| from uuid import uuid4 | ||
|
|
||
| from fastapi import UploadFile | ||
|
|
||
| from app.exceptions import ObjectStorageError | ||
|
|
||
| PROFILE_IMAGE_EXTENSIONS = { | ||
| "image/jpeg": ".jpg", | ||
| "image/png": ".png", | ||
| "image/webp": ".webp", | ||
| } | ||
|
|
||
|
|
||
| class OCIObjectStorageService: | ||
| def __init__(self): | ||
| try: | ||
| import oci | ||
| except ImportError: | ||
| raise ObjectStorageError("oci package is not installed") | ||
|
|
||
| self.oci = oci | ||
| self.namespace = os.getenv("OCI_NAMESPACE", "") | ||
| self.bucket = os.getenv("OCI_BUCKET", "") | ||
| self.region = os.getenv("OCI_REGION", "") | ||
| if not all((self.namespace, self.bucket, self.region)): | ||
| raise ObjectStorageError("OCI object storage is not configured") | ||
|
|
||
| auth_mode = os.getenv("OCI_OBJECT_STORAGE_AUTH", "instance_principal") | ||
| try: | ||
| if auth_mode == "config_file": | ||
| config_file = os.getenv("OCI_CONFIG_FILE", "~/.oci/config") | ||
| profile = os.getenv("OCI_CONFIG_PROFILE", "DEFAULT") | ||
| config = ( | ||
| oci.config.from_file(config_file, profile) | ||
| if config_file | ||
| else oci.config.from_file(profile_name=profile) | ||
| ) | ||
| self.client = oci.object_storage.ObjectStorageClient(config) | ||
| elif auth_mode == "resource_principal": | ||
| signer = oci.auth.signers.get_resource_principals_signer() | ||
| self.client = oci.object_storage.ObjectStorageClient( | ||
| {"region": self.region}, signer=signer | ||
| ) | ||
| elif auth_mode == "instance_principal": | ||
| signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() | ||
| self.client = oci.object_storage.ObjectStorageClient( | ||
| {"region": self.region}, signer=signer | ||
| ) | ||
| else: | ||
| raise ValueError(f"Unsupported OCI authentication mode: {auth_mode}") | ||
| except Exception as exc: | ||
| raise ObjectStorageError("OCI object storage is not configured") from exc | ||
|
|
||
| def upload_profile_image(self, user_id: int, file: UploadFile, body: bytes) -> str: | ||
| object_name = ( | ||
| f"profiles/{user_id}/{uuid4()}{PROFILE_IMAGE_EXTENSIONS[file.content_type]}" | ||
| ) | ||
| try: | ||
| self.client.put_object( | ||
| self.namespace, | ||
| self.bucket, | ||
| object_name, | ||
| body, | ||
| content_type=file.content_type, | ||
| ) | ||
| except Exception as exc: | ||
| raise ObjectStorageError("Failed to upload profile image") from exc | ||
| return self.public_url(object_name) | ||
|
|
||
| def delete_profile_image(self, user_id: int, url: str | None) -> None: | ||
| object_name = self.object_name_from_url(url) | ||
| if not object_name or not object_name.startswith(f"profiles/{user_id}/"): | ||
| return | ||
| try: | ||
| self.client.delete_object(self.namespace, self.bucket, object_name) | ||
| except Exception: | ||
| pass | ||
|
|
||
| def public_url(self, object_name: str) -> str: | ||
| base_url = ( | ||
| os.getenv("OCI_PUBLIC_BASE_URL") | ||
| or f"https://objectstorage.{self.region}.oraclecloud.com/n/{self.namespace}/b/{self.bucket}/o" | ||
| ).rstrip("/") | ||
| return f"{base_url}/{quote(object_name, safe='')}" | ||
|
|
||
| def object_name_from_url(self, url: str | None) -> str | None: | ||
| if not url: | ||
| return None | ||
| path = urlparse(url).path | ||
| marker = f"/n/{self.namespace}/b/{self.bucket}/o/" | ||
| if marker in path: | ||
| return unquote(path.split(marker, 1)[1]) | ||
| base_url = os.getenv("OCI_PUBLIC_BASE_URL", "").rstrip("/") | ||
| if base_url and url.startswith(f"{base_url}/"): | ||
| return unquote(url[len(base_url) + 1 :]) | ||
| return None |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
S3Service, OCIObjectStorageService 이거 추상화 한번만 해주시겠어요 ?
혹시나 인프라 수정할 경우에 갈아끼기 쉽게 하는게 좋을것같아요
BucketService
ㄴ S3Service
ㄴ OCIObjectStorageService