blob 이미지 업로드 완료
parent
6917a76d60
commit
12e6f7357c
|
|
@ -20,6 +20,7 @@ from app.home.schemas.home_schema import (
|
|||
MarketingAnalysis,
|
||||
ProcessedInfo,
|
||||
)
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from app.utils.chatgpt_prompt import ChatgptService
|
||||
from app.utils.common import generate_task_id
|
||||
from app.utils.nvMapScraper import NvMapScraper
|
||||
|
|
@ -178,10 +179,11 @@ IMAGES_JSON_EXAMPLE = """[
|
|||
|
||||
|
||||
@router.post(
|
||||
"/image/{task_id}/upload",
|
||||
"/image/upload/server/{task_id}",
|
||||
include_in_schema=False,
|
||||
summary="이미지 업로드",
|
||||
description="""
|
||||
task_id에 연결된 이미지를 업로드합니다.
|
||||
task_id에 연결된 이미지를 서버에 업로드합니다.
|
||||
|
||||
## 요청 방식
|
||||
multipart/form-data 형식으로 전송합니다.
|
||||
|
|
@ -218,12 +220,12 @@ jpg, jpeg, png, webp, heic, heif
|
|||
### 2. cURL로 테스트
|
||||
```bash
|
||||
# 바이너리 파일만 업로드
|
||||
curl -X POST "http://localhost:8000/image/test-task-001/upload" \\
|
||||
curl -X POST "http://localhost:8000/image/upload/server/test-task-001" \\
|
||||
-F "files=@/path/to/image1.jpg" \\
|
||||
-F "files=@/path/to/image2.png"
|
||||
|
||||
# URL + 바이너리 파일 동시 업로드
|
||||
curl -X POST "http://localhost:8000/image/test-task-001/upload" \\
|
||||
curl -X POST "http://localhost:8000/image/upload/server/test-task-001" \\
|
||||
-F 'images_json=[{"url":"https://example.com/image.jpg"}]' \\
|
||||
-F "files=@/path/to/local_image.jpg"
|
||||
```
|
||||
|
|
@ -232,7 +234,7 @@ curl -X POST "http://localhost:8000/image/test-task-001/upload" \\
|
|||
```python
|
||||
import requests
|
||||
|
||||
url = "http://localhost:8000/image/test-task-001/upload"
|
||||
url = "http://localhost:8000/image/upload/server/test-task-001"
|
||||
files = [
|
||||
("files", ("image1.jpg", open("image1.jpg", "rb"), "image/jpeg")),
|
||||
("files", ("image2.png", open("image2.png", "rb"), "image/png")),
|
||||
|
|
@ -252,7 +254,7 @@ print(response.json())
|
|||
- **images**: 업로드된 이미지 목록
|
||||
|
||||
## 저장 경로
|
||||
- 바이너리 파일: /media/{날짜}/{uuid7}/{파일명}
|
||||
- 바이너리 파일: /media/image/{날짜}/{uuid7}/{파일명}
|
||||
""",
|
||||
response_model=ImageUploadResponse,
|
||||
responses={
|
||||
|
|
@ -285,7 +287,9 @@ async def upload_images(
|
|||
|
||||
if has_files and files:
|
||||
for idx, f in enumerate(files):
|
||||
print(f"[upload_images] file[{idx}]: filename={f.filename}, size={f.size}, content_type={f.content_type}")
|
||||
print(
|
||||
f"[upload_images] file[{idx}]: filename={f.filename}, size={f.size}, content_type={f.content_type}"
|
||||
)
|
||||
|
||||
if not has_images_json and not has_files:
|
||||
raise HTTPException(
|
||||
|
|
@ -312,16 +316,24 @@ async def upload_images(
|
|||
if has_files and files:
|
||||
for f in files:
|
||||
is_valid_ext = _is_valid_image_extension(f.filename)
|
||||
is_not_empty = f.size is None or f.size > 0 # size가 None이면 아직 읽지 않은 것
|
||||
is_real_file = f.filename and f.filename != "filename" # Swagger 빈 파일 체크
|
||||
print(f"[upload_images] Checking file: {f.filename}, size={f.size}, is_valid_ext={is_valid_ext}, is_real_file={is_real_file}")
|
||||
is_not_empty = (
|
||||
f.size is None or f.size > 0
|
||||
) # size가 None이면 아직 읽지 않은 것
|
||||
is_real_file = (
|
||||
f.filename and f.filename != "filename"
|
||||
) # Swagger 빈 파일 체크
|
||||
print(
|
||||
f"[upload_images] Checking file: {f.filename}, size={f.size}, is_valid_ext={is_valid_ext}, is_real_file={is_real_file}"
|
||||
)
|
||||
|
||||
if f and is_real_file and is_valid_ext and is_not_empty:
|
||||
valid_files.append(f)
|
||||
else:
|
||||
skipped_files.append(f.filename or "unknown")
|
||||
|
||||
print(f"[upload_images] valid_files count: {len(valid_files)}, skipped: {skipped_files}")
|
||||
print(
|
||||
f"[upload_images] valid_files count: {len(valid_files)}, skipped: {skipped_files}"
|
||||
)
|
||||
|
||||
# 유효한 데이터가 하나도 없으면 에러
|
||||
if not url_images and not valid_files:
|
||||
|
|
@ -358,7 +370,7 @@ async def upload_images(
|
|||
)
|
||||
img_order += 1
|
||||
|
||||
# 2. 바이너리 파일 저장
|
||||
# 2. 바이너리 파일을 media에 저장
|
||||
if valid_files:
|
||||
today = date.today().strftime("%Y-%m-%d")
|
||||
# 한 번의 요청에서 업로드된 모든 이미지는 같은 폴더에 저장
|
||||
|
|
@ -371,22 +383,27 @@ async def upload_images(
|
|||
original_name = file.filename or "image"
|
||||
ext = _get_file_extension(file.filename) # type: ignore[arg-type]
|
||||
# 파일명에서 확장자 제거 후 순서 추가
|
||||
name_without_ext = original_name.rsplit(".", 1)[0] if "." in original_name else original_name
|
||||
name_without_ext = (
|
||||
original_name.rsplit(".", 1)[0]
|
||||
if "." in original_name
|
||||
else original_name
|
||||
)
|
||||
filename = f"{name_without_ext}_{img_order:03d}{ext}"
|
||||
|
||||
save_path = upload_dir / filename
|
||||
|
||||
# 파일 저장
|
||||
# media에 파일 저장
|
||||
await _save_upload_file(file, save_path)
|
||||
|
||||
# 이미지 URL 생성
|
||||
# media 기준 URL 생성
|
||||
img_url = f"/media/image/{today}/{batch_uuid}/{filename}"
|
||||
img_name = file.filename or filename
|
||||
print(f"[upload_images] File saved to media - path: {save_path}, url: {img_url}")
|
||||
|
||||
image = Image(
|
||||
task_id=task_id,
|
||||
img_name=img_name,
|
||||
img_url=img_url,
|
||||
img_url=img_url, # Media URL을 DB에 저장
|
||||
img_order=img_order,
|
||||
)
|
||||
session.add(image)
|
||||
|
|
@ -403,7 +420,8 @@ async def upload_images(
|
|||
)
|
||||
img_order += 1
|
||||
|
||||
print(f"[upload_images] Committing {len(result_images)} images to database...")
|
||||
saved_count = len(result_images)
|
||||
print(f"[upload_images] Committing {saved_count} images to database...")
|
||||
await session.commit()
|
||||
print("[upload_images] Commit successful!")
|
||||
|
||||
|
|
@ -412,263 +430,216 @@ async def upload_images(
|
|||
total_count=len(result_images),
|
||||
url_count=len(url_images),
|
||||
file_count=len(valid_files),
|
||||
saved_count=saved_count,
|
||||
images=result_images,
|
||||
)
|
||||
|
||||
|
||||
# @router.post(
|
||||
# "/generate",
|
||||
# summary="기본 영상 생성 요청",
|
||||
# description="""
|
||||
# 고객 정보만 받아 영상 생성 작업을 시작합니다. (이미지 없음)
|
||||
@router.post(
|
||||
"/image/upload/blob/{task_id}",
|
||||
summary="이미지 업로드 (Azure Blob)",
|
||||
description="""
|
||||
task_id에 연결된 이미지를 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
# ## 요청 필드
|
||||
# - **customer_name**: 고객명/가게명 (필수)
|
||||
# - **region**: 지역명 (필수)
|
||||
# - **detail_region_info**: 상세 지역 정보 (선택)
|
||||
# - **attribute**: 음악 속성 정보 (genre, vocal, tempo, mood)
|
||||
## 요청 방식
|
||||
multipart/form-data 형식으로 전송합니다.
|
||||
|
||||
# ## 반환 정보
|
||||
# - **task_id**: 작업 고유 식별자 (UUID7)
|
||||
# - **status**: 작업 상태
|
||||
# - **message**: 응답 메시지
|
||||
# """,
|
||||
# response_model=GenerateResponse,
|
||||
# response_description="생성 작업 시작 결과",
|
||||
# tags=["generate"],
|
||||
# )
|
||||
# async def generate(
|
||||
# request_body: GenerateRequest,
|
||||
# background_tasks: BackgroundTasks,
|
||||
# session: AsyncSession = Depends(get_session),
|
||||
# ):
|
||||
# """기본 영상 생성 요청 처리 (이미지 없음)"""
|
||||
# # UUID7 생성 및 중복 검사
|
||||
# while True:
|
||||
# task_id = str(uuid7())
|
||||
# existing = await session.execute(
|
||||
# select(Project).where(Project.task_id == task_id)
|
||||
# )
|
||||
# if existing.scalar_one_or_none() is None:
|
||||
# break
|
||||
## 요청 필드
|
||||
- **images_json**: 외부 이미지 URL 목록 (JSON 문자열, 선택)
|
||||
- **files**: 이미지 바이너리 파일 목록 (선택)
|
||||
|
||||
# # Project 생성 (이미지 없음)
|
||||
# project = Project(
|
||||
# store_name=request_body.customer_name,
|
||||
# region=request_body.region,
|
||||
# task_id=task_id,
|
||||
# detail_region_info=json.dumps(
|
||||
# {
|
||||
# "detail": request_body.detail_region_info,
|
||||
# "attribute": request_body.attribute.model_dump(),
|
||||
# },
|
||||
# ensure_ascii=False,
|
||||
# ),
|
||||
# )
|
||||
# session.add(project)
|
||||
# await session.commit()
|
||||
# await session.refresh(project)
|
||||
**주의**: images_json 또는 files 중 최소 하나는 반드시 전달해야 합니다.
|
||||
|
||||
# background_tasks.add_task(task_process, request_body, task_id, project.id)
|
||||
## 지원 이미지 확장자
|
||||
jpg, jpeg, png, webp, heic, heif
|
||||
|
||||
# return {
|
||||
# "task_id": task_id,
|
||||
# "status": "processing",
|
||||
# "message": "생성 작업이 시작되었습니다.",
|
||||
# }
|
||||
## images_json 예시
|
||||
```json
|
||||
[
|
||||
{"url": "https://example.com/image1.jpg"},
|
||||
{"url": "https://example.com/image2.jpg", "name": "외관"}
|
||||
]
|
||||
```
|
||||
|
||||
## 바이너리 파일 업로드 테스트 방법
|
||||
|
||||
# @router.post(
|
||||
# "/generate/urls",
|
||||
# summary="URL 기반 영상 생성 요청",
|
||||
# description="""
|
||||
# 고객 정보와 이미지 URL을 받아 영상 생성 작업을 시작합니다.
|
||||
### cURL로 테스트
|
||||
```bash
|
||||
# 바이너리 파일만 업로드
|
||||
curl -X POST "http://localhost:8000/image/upload/blob/test-task-001" \\
|
||||
-F "files=@/path/to/image1.jpg" \\
|
||||
-F "files=@/path/to/image2.png"
|
||||
|
||||
# ## 요청 필드
|
||||
# - **customer_name**: 고객명/가게명 (필수)
|
||||
# - **region**: 지역명 (필수)
|
||||
# - **detail_region_info**: 상세 지역 정보 (선택)
|
||||
# - **attribute**: 음악 속성 정보 (genre, vocal, tempo, mood)
|
||||
# - **images**: 이미지 URL 목록 (필수)
|
||||
# URL + 바이너리 파일 동시 업로드
|
||||
curl -X POST "http://localhost:8000/image/upload/blob/test-task-001" \\
|
||||
-F 'images_json=[{"url":"https://example.com/image.jpg"}]' \\
|
||||
-F "files=@/path/to/local_image.jpg"
|
||||
```
|
||||
|
||||
# ## 반환 정보
|
||||
# - **task_id**: 작업 고유 식별자 (UUID7)
|
||||
# - **status**: 작업 상태
|
||||
# - **message**: 응답 메시지
|
||||
# """,
|
||||
# response_model=GenerateResponse,
|
||||
# response_description="생성 작업 시작 결과",
|
||||
# tags=["generate"],
|
||||
# )
|
||||
# async def generate_urls(
|
||||
# request_body: GenerateUrlsRequest,
|
||||
# session: AsyncSession = Depends(get_session),
|
||||
# ):
|
||||
# """URL 기반 영상 생성 요청 처리"""
|
||||
# # UUID7 생성 및 중복 검사
|
||||
# while True:
|
||||
# task_id = str(uuid7())
|
||||
# existing = await session.execute(
|
||||
# select(Project).where(Project.task_id == task_id)
|
||||
# )
|
||||
# if existing.scalar_one_or_none() is None:
|
||||
# break
|
||||
## 반환 정보
|
||||
- **task_id**: 작업 고유 식별자
|
||||
- **total_count**: 총 업로드된 이미지 개수
|
||||
- **url_count**: URL로 등록된 이미지 개수
|
||||
- **file_count**: 파일로 업로드된 이미지 개수 (Blob에 저장됨)
|
||||
- **images**: 업로드된 이미지 목록
|
||||
|
||||
# # Project 생성 (이미지 정보 제외)
|
||||
# project = Project(
|
||||
# store_name=request_body.customer_name,
|
||||
# region=request_body.region,
|
||||
# task_id=task_id,
|
||||
# detail_region_info=json.dumps(
|
||||
# {
|
||||
# "detail": request_body.detail_region_info,
|
||||
# "attribute": request_body.attribute.model_dump(),
|
||||
# },
|
||||
# ensure_ascii=False,
|
||||
# ),
|
||||
# )
|
||||
# session.add(project)
|
||||
## 저장 경로
|
||||
- 바이너리 파일: Azure Blob Storage ({task_id}/{파일명})
|
||||
""",
|
||||
response_model=ImageUploadResponse,
|
||||
responses={
|
||||
200: {"description": "이미지 업로드 성공"},
|
||||
400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse},
|
||||
},
|
||||
tags=["image"],
|
||||
)
|
||||
async def upload_images_blob(
|
||||
task_id: str,
|
||||
images_json: Optional[str] = Form(
|
||||
default=None,
|
||||
description="외부 이미지 URL 목록 (JSON 문자열)",
|
||||
example=IMAGES_JSON_EXAMPLE,
|
||||
),
|
||||
files: Optional[list[UploadFile]] = File(
|
||||
default=None, description="이미지 바이너리 파일 목록"
|
||||
),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ImageUploadResponse:
|
||||
"""이미지 업로드 (URL + Azure Blob Storage)"""
|
||||
print(f"[upload_images_blob] START - task_id: {task_id}")
|
||||
|
||||
# # Image 레코드 생성 (독립 테이블, task_id로 연결)
|
||||
# for idx, img_item in enumerate(request_body.images):
|
||||
# # name이 있으면 사용, 없으면 URL에서 추출
|
||||
# img_name = img_item.name or _extract_image_name(img_item.url, idx)
|
||||
# image = Image(
|
||||
# task_id=task_id,
|
||||
# img_name=img_name,
|
||||
# img_url=img_item.url,
|
||||
# img_order=idx,
|
||||
# )
|
||||
# session.add(image)
|
||||
# 1. 진입 검증
|
||||
has_images_json = images_json is not None and images_json.strip() != ""
|
||||
has_files = files is not None and len(files) > 0
|
||||
|
||||
# await session.commit()
|
||||
if not has_images_json and not has_files:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="images_json 또는 files 중 하나는 반드시 제공해야 합니다.",
|
||||
)
|
||||
|
||||
# return {
|
||||
# "task_id": task_id,
|
||||
# "status": "processing",
|
||||
# "message": "생성 작업이 시작되었습니다.",
|
||||
# }
|
||||
# 2. images_json 파싱
|
||||
url_images: list[ImageUrlItem] = []
|
||||
if has_images_json:
|
||||
try:
|
||||
parsed = json.loads(images_json)
|
||||
if isinstance(parsed, list):
|
||||
url_images = [ImageUrlItem(**item) for item in parsed if item]
|
||||
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"images_json 파싱 오류: {str(e)}",
|
||||
)
|
||||
|
||||
# 3. 유효한 파일만 필터링
|
||||
valid_files: list[UploadFile] = []
|
||||
skipped_files: list[str] = []
|
||||
if has_files and files:
|
||||
for f in files:
|
||||
is_valid_ext = _is_valid_image_extension(f.filename)
|
||||
is_not_empty = f.size is None or f.size > 0
|
||||
is_real_file = f.filename and f.filename != "filename"
|
||||
|
||||
# async def _save_upload_file(file: UploadFile, save_path: Path) -> None:
|
||||
# """업로드 파일을 지정된 경로에 저장"""
|
||||
# save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# async with aiofiles.open(save_path, "wb") as f:
|
||||
# content = await file.read()
|
||||
# await f.write(content)
|
||||
if f and is_real_file and is_valid_ext and is_not_empty:
|
||||
valid_files.append(f)
|
||||
else:
|
||||
skipped_files.append(f.filename or "unknown")
|
||||
|
||||
print(f"[upload_images_blob] valid_files: {len(valid_files)}, url_images: {len(url_images)}")
|
||||
|
||||
# def _get_file_extension(filename: str | None) -> str:
|
||||
# """파일명에서 확장자 추출"""
|
||||
# if not filename:
|
||||
# return ".jpg"
|
||||
# ext = Path(filename).suffix.lower()
|
||||
# return ext if ext else ".jpg"
|
||||
if not url_images and not valid_files:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"유효한 이미지가 없습니다. 지원 확장자: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}. 건너뛴 파일: {skipped_files}",
|
||||
)
|
||||
|
||||
result_images: list[ImageUploadResultItem] = []
|
||||
img_order = 0
|
||||
|
||||
# @router.post(
|
||||
# "/generate/upload",
|
||||
# summary="파일 업로드 기반 영상 생성 요청",
|
||||
# description="""
|
||||
# 고객 정보와 이미지 파일을 받아 영상 생성 작업을 시작합니다.
|
||||
# 1. URL 이미지 저장
|
||||
for url_item in url_images:
|
||||
img_name = url_item.name or _extract_image_name(url_item.url, img_order)
|
||||
|
||||
# ## 요청 필드 (multipart/form-data)
|
||||
# - **customer_name**: 고객명/가게명 (필수)
|
||||
# - **region**: 지역명 (필수)
|
||||
# - **detail_region_info**: 상세 지역 정보 (선택)
|
||||
# - **attribute**: 음악 속성 정보 JSON 문자열 (필수)
|
||||
# - **images**: 이미지 파일 목록 (필수, 복수 파일)
|
||||
image = Image(
|
||||
task_id=task_id,
|
||||
img_name=img_name,
|
||||
img_url=url_item.url,
|
||||
img_order=img_order,
|
||||
)
|
||||
session.add(image)
|
||||
await session.flush()
|
||||
print(f"[upload_images_blob] URL saved - id: {image.id}, img_name: {img_name}")
|
||||
|
||||
# ## 반환 정보
|
||||
# - **task_id**: 작업 고유 식별자 (UUID7)
|
||||
# - **status**: 작업 상태
|
||||
# - **message**: 응답 메시지
|
||||
# - **uploaded_count**: 업로드된 이미지 개수
|
||||
# """,
|
||||
# response_model=GenerateUploadResponse,
|
||||
# response_description="생성 작업 시작 결과",
|
||||
# tags=["generate"],
|
||||
# )
|
||||
# async def generate_upload(
|
||||
# customer_name: str = Form(..., description="고객명/가게명"),
|
||||
# region: str = Form(..., description="지역명"),
|
||||
# attribute: str = Form(..., description="음악 속성 정보 (JSON 문자열)"),
|
||||
# images: list[UploadFile] = File(..., description="이미지 파일 목록"),
|
||||
# detail_region_info: str | None = Form(None, description="상세 지역 정보"),
|
||||
# session: AsyncSession = Depends(get_session),
|
||||
# ):
|
||||
# """파일 업로드 기반 영상 생성 요청 처리"""
|
||||
# # attribute JSON 파싱 및 검증
|
||||
# try:
|
||||
# attribute_dict = json.loads(attribute)
|
||||
# attribute_info = AttributeInfo(**attribute_dict)
|
||||
# except json.JSONDecodeError:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail="attribute는 유효한 JSON 형식이어야 합니다."
|
||||
# )
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=f"attribute 검증 실패: {e}")
|
||||
result_images.append(
|
||||
ImageUploadResultItem(
|
||||
id=image.id,
|
||||
img_name=img_name,
|
||||
img_url=url_item.url,
|
||||
img_order=img_order,
|
||||
source="url",
|
||||
)
|
||||
)
|
||||
img_order += 1
|
||||
|
||||
# # 이미지 파일 검증
|
||||
# if not images:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail="최소 1개 이상의 이미지 파일이 필요합니다."
|
||||
# )
|
||||
# 2. 바이너리 파일을 Azure Blob Storage에 직접 업로드 (media 저장 없음)
|
||||
if valid_files:
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
|
||||
# # UUID7 생성 및 중복 검사
|
||||
# while True:
|
||||
# task_id = str(uuid7())
|
||||
# existing = await session.execute(
|
||||
# select(Project).where(Project.task_id == task_id)
|
||||
# )
|
||||
# if existing.scalar_one_or_none() is None:
|
||||
# break
|
||||
for file in valid_files:
|
||||
original_name = file.filename or "image"
|
||||
ext = _get_file_extension(file.filename) # type: ignore[arg-type]
|
||||
name_without_ext = (
|
||||
original_name.rsplit(".", 1)[0]
|
||||
if "." in original_name
|
||||
else original_name
|
||||
)
|
||||
filename = f"{name_without_ext}_{img_order:03d}{ext}"
|
||||
|
||||
# # 저장 경로 생성: media/날짜/task_id/
|
||||
# today = date.today().strftime("%Y%m%d")
|
||||
# upload_dir = MEDIA_ROOT / today / task_id
|
||||
# 파일 내용 읽기
|
||||
file_content = await file.read()
|
||||
print(f"[upload_images_blob] Uploading {filename} ({len(file_content)} bytes) to Blob...")
|
||||
|
||||
# # Project 생성 (이미지 정보 제외)
|
||||
# project = Project(
|
||||
# store_name=customer_name,
|
||||
# region=region,
|
||||
# task_id=task_id,
|
||||
# detail_region_info=json.dumps(
|
||||
# {
|
||||
# "detail": detail_region_info,
|
||||
# "attribute": attribute_info.model_dump(),
|
||||
# },
|
||||
# ensure_ascii=False,
|
||||
# ),
|
||||
# )
|
||||
# session.add(project)
|
||||
# Azure Blob Storage에 직접 업로드
|
||||
upload_success = await uploader.upload_image_bytes(file_content, filename)
|
||||
|
||||
# # 이미지 파일 저장 및 Image 레코드 생성
|
||||
# for idx, file in enumerate(images):
|
||||
# # 각 이미지에 고유 UUID7 생성
|
||||
# img_uuid = str(uuid7())
|
||||
# ext = _get_file_extension(file.filename)
|
||||
# filename = f"{img_uuid}{ext}"
|
||||
# save_path = upload_dir / filename
|
||||
if upload_success:
|
||||
blob_url = uploader.public_url
|
||||
img_name = file.filename or filename
|
||||
|
||||
# # 파일 저장
|
||||
# await _save_upload_file(file, save_path)
|
||||
image = Image(
|
||||
task_id=task_id,
|
||||
img_name=img_name,
|
||||
img_url=blob_url,
|
||||
img_order=img_order,
|
||||
)
|
||||
session.add(image)
|
||||
await session.flush()
|
||||
print(f"[upload_images_blob] Blob saved - id: {image.id}, blob_url: {blob_url}")
|
||||
|
||||
# # Image 레코드 생성 (독립 테이블, task_id로 연결)
|
||||
# img_url = f"/media/{today}/{task_id}/{filename}"
|
||||
# image = Image(
|
||||
# task_id=task_id,
|
||||
# img_name=file.filename or filename,
|
||||
# img_url=img_url,
|
||||
# img_order=idx,
|
||||
# )
|
||||
# session.add(image)
|
||||
result_images.append(
|
||||
ImageUploadResultItem(
|
||||
id=image.id,
|
||||
img_name=img_name,
|
||||
img_url=blob_url,
|
||||
img_order=img_order,
|
||||
source="blob",
|
||||
)
|
||||
)
|
||||
img_order += 1
|
||||
else:
|
||||
print(f"[upload_images_blob] Failed to upload {filename}")
|
||||
skipped_files.append(filename)
|
||||
|
||||
# await session.commit()
|
||||
saved_count = len(result_images)
|
||||
print(f"[upload_images_blob] Committing {saved_count} images...")
|
||||
await session.commit()
|
||||
print(f"[upload_images_blob] Done! saved_count: {saved_count}")
|
||||
|
||||
# return {
|
||||
# "task_id": task_id,
|
||||
# "status": "processing",
|
||||
# "message": "생성 작업이 시작되었습니다.",
|
||||
# "uploaded_count": len(images),
|
||||
# }
|
||||
return ImageUploadResponse(
|
||||
task_id=task_id,
|
||||
total_count=len(result_images),
|
||||
url_count=len(url_images),
|
||||
file_count=len(valid_files) - len(skipped_files),
|
||||
saved_count=saved_count,
|
||||
images=result_images,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -203,7 +203,9 @@ class ImageUploadResultItem(BaseModel):
|
|||
img_name: str = Field(..., description="이미지명")
|
||||
img_url: str = Field(..., description="이미지 URL")
|
||||
img_order: int = Field(..., description="이미지 순서")
|
||||
source: Literal["url", "file"] = Field(..., description="이미지 소스 (url 또는 file)")
|
||||
source: Literal["url", "file", "blob"] = Field(
|
||||
..., description="이미지 소스 (url: 외부 URL, file: 로컬 서버, blob: Azure Blob)"
|
||||
)
|
||||
|
||||
|
||||
class ImageUploadResponse(BaseModel):
|
||||
|
|
@ -213,4 +215,5 @@ class ImageUploadResponse(BaseModel):
|
|||
total_count: int = Field(..., description="총 업로드된 이미지 개수")
|
||||
url_count: int = Field(..., description="URL로 등록된 이미지 개수")
|
||||
file_count: int = Field(..., description="파일로 업로드된 이미지 개수")
|
||||
saved_count: int = Field(..., description="Image 테이블에 저장된 row 수")
|
||||
images: list[ImageUploadResultItem] = Field(..., description="업로드된 이미지 목록")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
"""
|
||||
Home Worker 모듈
|
||||
|
||||
이미지 업로드 관련 백그라운드 작업을 처리합니다.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
|
||||
MEDIA_ROOT = Path("media")
|
||||
|
||||
|
||||
async def save_upload_file(file: UploadFile, save_path: Path) -> None:
|
||||
"""업로드 파일을 지정된 경로에 저장"""
|
||||
save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
async with aiofiles.open(save_path, "wb") as f:
|
||||
content = await file.read()
|
||||
await f.write(content)
|
||||
|
||||
|
||||
async def upload_image_to_blob(
|
||||
task_id: str,
|
||||
file: UploadFile,
|
||||
filename: str,
|
||||
save_dir: Path,
|
||||
) -> tuple[bool, str, str]:
|
||||
"""
|
||||
이미지 파일을 media에 저장하고 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
Args:
|
||||
task_id: 작업 고유 식별자
|
||||
file: 업로드할 파일 객체
|
||||
filename: 저장될 파일명
|
||||
save_dir: media 저장 디렉토리 경로
|
||||
|
||||
Returns:
|
||||
tuple[bool, str, str]: (업로드 성공 여부, blob_url 또는 에러 메시지, media_path)
|
||||
"""
|
||||
save_path = save_dir / filename
|
||||
media_path = str(save_path)
|
||||
|
||||
try:
|
||||
# 1. media에 파일 저장
|
||||
await save_upload_file(file, save_path)
|
||||
print(f"[upload_image_to_blob] File saved to media: {save_path}")
|
||||
|
||||
# 2. Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
upload_success = await uploader.upload_image(file_path=str(save_path))
|
||||
|
||||
if upload_success:
|
||||
return True, uploader.public_url, media_path
|
||||
else:
|
||||
return False, f"Failed to upload {filename} to Blob", media_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"[upload_image_to_blob] Error: {e}")
|
||||
return False, str(e), media_path
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database.session import get_worker_session
|
||||
from app.home.schemas.home_schema import GenerateRequest
|
||||
from app.lyric.models import Lyric
|
||||
from app.utils.chatgpt_prompt import ChatgptService
|
||||
|
||||
|
||||
async def _save_lyric(task_id: str, project_id: int, lyric_prompt: str) -> int:
|
||||
"""Lyric 레코드를 DB에 저장 (status=processing, lyric_result=null)"""
|
||||
async with get_worker_session() as session:
|
||||
lyric = Lyric(
|
||||
task_id=task_id,
|
||||
project_id=project_id,
|
||||
status="processing",
|
||||
lyric_prompt=lyric_prompt,
|
||||
lyric_result=None,
|
||||
)
|
||||
session.add(lyric)
|
||||
await session.commit()
|
||||
await session.refresh(lyric)
|
||||
print(f"Lyric saved: id={lyric.id}, task_id={task_id}, status=processing")
|
||||
return lyric.id
|
||||
|
||||
|
||||
async def _update_lyric_status(lyric_id: int, status: str, lyric_result: str | None = None) -> None:
|
||||
"""Lyric 레코드의 status와 lyric_result를 업데이트"""
|
||||
async with get_worker_session() as session:
|
||||
result = await session.execute(select(Lyric).where(Lyric.id == lyric_id))
|
||||
lyric = result.scalar_one_or_none()
|
||||
if lyric:
|
||||
lyric.status = status
|
||||
if lyric_result is not None:
|
||||
lyric.lyric_result = lyric_result
|
||||
await session.commit()
|
||||
print(f"Lyric updated: id={lyric_id}, status={status}")
|
||||
|
||||
|
||||
async def lyric_task(
|
||||
task_id: str,
|
||||
project_id: int,
|
||||
customer_name: str,
|
||||
region: str,
|
||||
detail_region_info: str,
|
||||
language: str = "Korean",
|
||||
) -> None:
|
||||
"""가사 생성 작업: ChatGPT로 가사 생성 및 Lyric 테이블 저장/업데이트"""
|
||||
service = ChatgptService(
|
||||
customer_name=customer_name,
|
||||
region=region,
|
||||
detail_region_info=detail_region_info,
|
||||
language=language,
|
||||
)
|
||||
|
||||
# Lyric 레코드 저장 (status=processing, lyric_result=null)
|
||||
lyric_prompt = service.build_lyrics_prompt()
|
||||
lyric_id = await _save_lyric(task_id, project_id, lyric_prompt)
|
||||
|
||||
# GPT 호출
|
||||
result = await service.generate(prompt=lyric_prompt)
|
||||
|
||||
print(f"GPT Response:\n{result}")
|
||||
|
||||
# 결과에 ERROR가 포함되어 있으면 status를 failed로 업데이트
|
||||
if "ERROR:" in result:
|
||||
await _update_lyric_status(lyric_id, "failed", lyric_result=result)
|
||||
else:
|
||||
await _update_lyric_status(lyric_id, "completed", lyric_result=result)
|
||||
|
||||
|
||||
async def _task_process_async(request_body: GenerateRequest, task_id: str, project_id: int) -> None:
|
||||
"""백그라운드 작업 처리 (async 버전)"""
|
||||
customer_name = request_body.customer_name
|
||||
region = request_body.region
|
||||
detail_region_info = request_body.detail_region_info or ""
|
||||
language = request_body.language
|
||||
|
||||
print(f"customer_name: {customer_name}")
|
||||
print(f"region: {region}")
|
||||
print(f"detail_region_info: {detail_region_info}")
|
||||
print(f"language: {language}")
|
||||
|
||||
# 가사 생성 작업
|
||||
await lyric_task(task_id, project_id, customer_name, region, detail_region_info, language)
|
||||
|
||||
|
||||
def task_process(request_body: GenerateRequest, task_id: str, project_id: int) -> None:
|
||||
"""백그라운드 작업 처리 함수 (sync wrapper)"""
|
||||
asyncio.run(_task_process_async(request_body, task_id, project_id))
|
||||
|
|
@ -14,6 +14,7 @@ from sqlalchemy import select
|
|||
from app.database.session import AsyncSessionLocal
|
||||
from app.song.models import Song
|
||||
from app.utils.common import generate_task_id
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from config import prj_settings
|
||||
|
||||
|
||||
|
|
@ -99,3 +100,108 @@ async def download_and_save_song(
|
|||
song.status = "failed"
|
||||
await session.commit()
|
||||
print(f"[download_and_save_song] FAILED - task_id: {task_id}, status updated to failed")
|
||||
|
||||
|
||||
async def download_and_upload_song_to_blob(
|
||||
task_id: str,
|
||||
audio_url: str,
|
||||
store_name: str,
|
||||
) -> None:
|
||||
"""백그라운드에서 노래를 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다.
|
||||
|
||||
Args:
|
||||
task_id: 프로젝트 task_id
|
||||
audio_url: 다운로드할 오디오 URL
|
||||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
print(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
temp_file_path: Path | None = None
|
||||
|
||||
try:
|
||||
# 파일명에 사용할 수 없는 문자 제거
|
||||
safe_store_name = "".join(
|
||||
c for c in store_name if c.isalnum() or c in (" ", "_", "-")
|
||||
).strip()
|
||||
safe_store_name = safe_store_name or "song"
|
||||
file_name = f"{safe_store_name}.mp3"
|
||||
|
||||
# 임시 저장 경로 생성
|
||||
temp_dir = Path("media") / "temp" / task_id
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_file_path = temp_dir / file_name
|
||||
print(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 오디오 파일 다운로드
|
||||
print(f"[download_and_upload_song_to_blob] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(audio_url, timeout=60.0)
|
||||
response.raise_for_status()
|
||||
|
||||
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
||||
await f.write(response.content)
|
||||
print(f"[download_and_upload_song_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
||||
|
||||
# Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
upload_success = await uploader.upload_music(file_path=str(temp_file_path))
|
||||
|
||||
if not upload_success:
|
||||
raise Exception("Azure Blob Storage 업로드 실패")
|
||||
|
||||
# SAS 토큰이 제외된 public_url 사용
|
||||
blob_url = uploader.public_url
|
||||
print(f"[download_and_upload_song_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
|
||||
# Song 테이블 업데이트 (새 세션 사용)
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 여러 개 있을 경우 가장 최근 것 선택
|
||||
result = await session.execute(
|
||||
select(Song)
|
||||
.where(Song.task_id == task_id)
|
||||
.order_by(Song.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
song = result.scalar_one_or_none()
|
||||
|
||||
if song:
|
||||
song.status = "completed"
|
||||
song.song_result_url = blob_url
|
||||
await session.commit()
|
||||
print(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}, status: completed")
|
||||
else:
|
||||
print(f"[download_and_upload_song_to_blob] Song NOT FOUND in DB - task_id: {task_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
# 실패 시 Song 테이블 업데이트
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 여러 개 있을 경우 가장 최근 것 선택
|
||||
result = await session.execute(
|
||||
select(Song)
|
||||
.where(Song.task_id == task_id)
|
||||
.order_by(Song.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
song = result.scalar_one_or_none()
|
||||
|
||||
if song:
|
||||
song.status = "failed"
|
||||
await session.commit()
|
||||
print(f"[download_and_upload_song_to_blob] FAILED - task_id: {task_id}, status updated to failed")
|
||||
|
||||
finally:
|
||||
# 임시 파일 삭제
|
||||
if temp_file_path and temp_file_path.exists():
|
||||
try:
|
||||
temp_file_path.unlink()
|
||||
print(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
print(f"[download_and_upload_song_to_blob] Failed to delete temp file: {e}")
|
||||
|
||||
# 임시 디렉토리 삭제 시도
|
||||
temp_dir = Path("media") / "temp" / task_id
|
||||
if temp_dir.exists():
|
||||
try:
|
||||
temp_dir.rmdir()
|
||||
except Exception:
|
||||
pass # 디렉토리가 비어있지 않으면 무시
|
||||
|
|
|
|||
|
|
@ -1,7 +1,30 @@
|
|||
"""
|
||||
Azure Blob Storage 업로드 유틸리티
|
||||
|
||||
Azure Blob Storage에 파일을 업로드하는 비동기 함수들을 제공합니다.
|
||||
Azure Blob Storage에 파일을 업로드하는 클래스를 제공합니다.
|
||||
파일 경로 또는 바이트 데이터를 직접 업로드할 수 있습니다.
|
||||
|
||||
URL 경로 형식:
|
||||
- 음악: {BASE_URL}/{task_id}/song/{파일명}
|
||||
- 영상: {BASE_URL}/{task_id}/video/{파일명}
|
||||
- 이미지: {BASE_URL}/{task_id}/image/{파일명}
|
||||
|
||||
사용 예시:
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
|
||||
# 파일 경로로 업로드
|
||||
success = await uploader.upload_music(file_path="my_song.mp3")
|
||||
success = await uploader.upload_video(file_path="my_video.mp4")
|
||||
success = await uploader.upload_image(file_path="my_image.png")
|
||||
|
||||
# 바이트 데이터로 직접 업로드 (media 저장 없이)
|
||||
success = await uploader.upload_music_bytes(audio_bytes, "my_song") # .mp3 자동 추가
|
||||
success = await uploader.upload_video_bytes(video_bytes, "my_video") # .mp4 자동 추가
|
||||
success = await uploader.upload_image_bytes(image_bytes, "my_image.png")
|
||||
|
||||
print(uploader.public_url) # 마지막 업로드의 공개 URL
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
|
@ -9,87 +32,26 @@ from pathlib import Path
|
|||
import aiofiles
|
||||
import httpx
|
||||
|
||||
SAS_TOKEN = "sp=racwdl&st=2025-12-01T00:13:29Z&se=2026-07-31T08:28:29Z&spr=https&sv=2024-11-04&sr=c&sig=7fE2ozVBPu3Gq43%2FZDxEYdEcPLDXyNVfTf16IBasmVQ%3D"
|
||||
from config import azure_blob_settings
|
||||
|
||||
|
||||
async def upload_music_to_azure_blob(
|
||||
file_path: str = "스테이 머뭄_1.mp3",
|
||||
url: str = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp3",
|
||||
) -> bool:
|
||||
"""음악 파일을 Azure Blob Storage에 업로드합니다.
|
||||
class AzureBlobUploader:
|
||||
"""Azure Blob Storage 업로드 클래스
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
url: Azure Blob Storage URL
|
||||
Azure Blob Storage에 음악, 영상, 이미지 파일을 업로드합니다.
|
||||
URL 형식: {BASE_URL}/{task_id}/{category}/{file_name}?{SAS_TOKEN}
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
카테고리별 경로:
|
||||
- 음악: {task_id}/song/{file_name}
|
||||
- 영상: {task_id}/video/{file_name}
|
||||
- 이미지: {task_id}/image/{file_name}
|
||||
|
||||
Attributes:
|
||||
task_id: 작업 고유 식별자
|
||||
"""
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
headers = {"Content-Type": "audio/mpeg", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with aiofiles.open(file_path, "rb") as file:
|
||||
file_content = await file.read()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(access_url, content=file_content, headers=headers, timeout=120.0)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_music_to_azure_blob] Success - Status Code: {response.status_code}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_music_to_azure_blob] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_music_to_azure_blob] Response: {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
async def upload_video_to_azure_blob(
|
||||
file_path: str = "스테이 머뭄.mp4",
|
||||
url: str = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp4",
|
||||
) -> bool:
|
||||
"""영상 파일을 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
url: Azure Blob Storage URL
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
"""
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
headers = {"Content-Type": "video/mp4", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with aiofiles.open(file_path, "rb") as file:
|
||||
file_content = await file.read()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(access_url, content=file_content, headers=headers, timeout=180.0)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_video_to_azure_blob] Success - Status Code: {response.status_code}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_video_to_azure_blob] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_video_to_azure_blob] Response: {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
async def upload_image_to_azure_blob(
|
||||
file_path: str = "스테이 머뭄.png",
|
||||
url: str = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.png",
|
||||
) -> bool:
|
||||
"""이미지 파일을 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
url: Azure Blob Storage URL
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
"""
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
extension = Path(file_path).suffix.lower()
|
||||
content_types = {
|
||||
# Content-Type 매핑
|
||||
IMAGE_CONTENT_TYPES = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
|
|
@ -97,25 +59,300 @@ async def upload_image_to_azure_blob(
|
|||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
content_type = content_types.get(extension, "image/jpeg")
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with aiofiles.open(file_path, "rb") as file:
|
||||
file_content = await file.read()
|
||||
def __init__(self, task_id: str):
|
||||
"""AzureBlobUploader 초기화
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(access_url, content=file_content, headers=headers, timeout=60.0)
|
||||
Args:
|
||||
task_id: 작업 고유 식별자
|
||||
"""
|
||||
self._task_id = task_id
|
||||
self._base_url = azure_blob_settings.AZURE_BLOB_BASE_URL
|
||||
self._sas_token = azure_blob_settings.AZURE_BLOB_SAS_TOKEN
|
||||
self._last_public_url: str = ""
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_image_to_azure_blob] Success - Status Code: {response.status_code}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_image_to_azure_blob] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_image_to_azure_blob] Response: {response.text}")
|
||||
return False
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
"""작업 고유 식별자"""
|
||||
return self._task_id
|
||||
|
||||
@property
|
||||
def public_url(self) -> str:
|
||||
"""마지막 업로드의 공개 URL (SAS 토큰 제외)"""
|
||||
return self._last_public_url
|
||||
|
||||
def _build_upload_url(self, category: str, file_name: str) -> str:
|
||||
"""업로드 URL 생성 (SAS 토큰 포함)"""
|
||||
# SAS 토큰 앞뒤의 ?, ', " 제거
|
||||
sas_token = self._sas_token.strip("?'\"")
|
||||
return f"{self._base_url}/{self._task_id}/{category}/{file_name}?{sas_token}"
|
||||
|
||||
def _build_public_url(self, category: str, file_name: str) -> str:
|
||||
"""공개 URL 생성 (SAS 토큰 제외)"""
|
||||
return f"{self._base_url}/{self._task_id}/{category}/{file_name}"
|
||||
|
||||
async def _upload_file(
|
||||
self,
|
||||
file_path: str,
|
||||
category: str,
|
||||
content_type: str,
|
||||
timeout: float,
|
||||
log_prefix: str,
|
||||
) -> bool:
|
||||
"""파일을 Azure Blob Storage에 업로드하는 내부 메서드
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
category: 카테고리 (song, video, image)
|
||||
content_type: Content-Type 헤더 값
|
||||
timeout: 요청 타임아웃 (초)
|
||||
log_prefix: 로그 접두사
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
"""
|
||||
# 파일 경로에서 파일명 추출
|
||||
file_name = Path(file_path).name
|
||||
|
||||
upload_url = self._build_upload_url(category, file_name)
|
||||
self._last_public_url = self._build_public_url(category, file_name)
|
||||
print(f"[{log_prefix}] Upload URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with aiofiles.open(file_path, "rb") as file:
|
||||
file_content = await file.read()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
upload_url, content=file_content, headers=headers, timeout=timeout
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[{log_prefix}] Success - Status Code: {response.status_code}")
|
||||
print(f"[{log_prefix}] Public URL: {self._last_public_url}")
|
||||
return True
|
||||
else:
|
||||
print(f"[{log_prefix}] Failed - Status Code: {response.status_code}")
|
||||
print(f"[{log_prefix}] Response: {response.text}")
|
||||
return False
|
||||
|
||||
async def upload_music(self, file_path: str) -> bool:
|
||||
"""음악 파일을 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/song/{파일명}
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
success = await uploader.upload_music(file_path="my_song.mp3")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/song/my_song.mp3
|
||||
"""
|
||||
return await self._upload_file(
|
||||
file_path=file_path,
|
||||
category="song",
|
||||
content_type="audio/mpeg",
|
||||
timeout=120.0,
|
||||
log_prefix="upload_music",
|
||||
)
|
||||
|
||||
async def upload_music_bytes(self, file_content: bytes, file_name: str) -> bool:
|
||||
"""음악 바이트 데이터를 Azure Blob Storage에 직접 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/song/{파일명}
|
||||
|
||||
Args:
|
||||
file_content: 업로드할 파일 바이트 데이터
|
||||
file_name: 저장할 파일명 (확장자가 없으면 .mp3 추가)
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
success = await uploader.upload_music_bytes(audio_bytes, "my_song")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/song/my_song.mp3
|
||||
"""
|
||||
# 확장자가 없으면 .mp3 추가
|
||||
if not Path(file_name).suffix:
|
||||
file_name = f"{file_name}.mp3"
|
||||
|
||||
upload_url = self._build_upload_url("song", file_name)
|
||||
self._last_public_url = self._build_public_url("song", file_name)
|
||||
print(f"[upload_music_bytes] Upload URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": "audio/mpeg", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
upload_url, content=file_content, headers=headers, timeout=120.0
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_music_bytes] Success - Status Code: {response.status_code}")
|
||||
print(f"[upload_music_bytes] Public URL: {self._last_public_url}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_music_bytes] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_music_bytes] Response: {response.text}")
|
||||
return False
|
||||
|
||||
async def upload_video(self, file_path: str) -> bool:
|
||||
"""영상 파일을 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/video/{파일명}
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
success = await uploader.upload_video(file_path="my_video.mp4")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/video/my_video.mp4
|
||||
"""
|
||||
return await self._upload_file(
|
||||
file_path=file_path,
|
||||
category="video",
|
||||
content_type="video/mp4",
|
||||
timeout=180.0,
|
||||
log_prefix="upload_video",
|
||||
)
|
||||
|
||||
async def upload_video_bytes(self, file_content: bytes, file_name: str) -> bool:
|
||||
"""영상 바이트 데이터를 Azure Blob Storage에 직접 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/video/{파일명}
|
||||
|
||||
Args:
|
||||
file_content: 업로드할 파일 바이트 데이터
|
||||
file_name: 저장할 파일명 (확장자가 없으면 .mp4 추가)
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
success = await uploader.upload_video_bytes(video_bytes, "my_video")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/video/my_video.mp4
|
||||
"""
|
||||
# 확장자가 없으면 .mp4 추가
|
||||
if not Path(file_name).suffix:
|
||||
file_name = f"{file_name}.mp4"
|
||||
|
||||
upload_url = self._build_upload_url("video", file_name)
|
||||
self._last_public_url = self._build_public_url("video", file_name)
|
||||
print(f"[upload_video_bytes] Upload URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": "video/mp4", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
upload_url, content=file_content, headers=headers, timeout=180.0
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_video_bytes] Success - Status Code: {response.status_code}")
|
||||
print(f"[upload_video_bytes] Public URL: {self._last_public_url}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_video_bytes] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_video_bytes] Response: {response.text}")
|
||||
return False
|
||||
|
||||
async def upload_image(self, file_path: str) -> bool:
|
||||
"""이미지 파일을 Azure Blob Storage에 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/image/{파일명}
|
||||
|
||||
Args:
|
||||
file_path: 업로드할 파일 경로
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
success = await uploader.upload_image(file_path="my_image.png")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/image/my_image.png
|
||||
"""
|
||||
extension = Path(file_path).suffix.lower()
|
||||
content_type = self.IMAGE_CONTENT_TYPES.get(extension, "image/jpeg")
|
||||
|
||||
return await self._upload_file(
|
||||
file_path=file_path,
|
||||
category="image",
|
||||
content_type=content_type,
|
||||
timeout=60.0,
|
||||
log_prefix="upload_image",
|
||||
)
|
||||
|
||||
async def upload_image_bytes(self, file_content: bytes, file_name: str) -> bool:
|
||||
"""이미지 바이트 데이터를 Azure Blob Storage에 직접 업로드합니다.
|
||||
|
||||
URL 경로: {task_id}/image/{파일명}
|
||||
|
||||
Args:
|
||||
file_content: 업로드할 파일 바이트 데이터
|
||||
file_name: 저장할 파일명
|
||||
|
||||
Returns:
|
||||
bool: 업로드 성공 여부
|
||||
|
||||
Example:
|
||||
uploader = AzureBlobUploader(task_id="task-123")
|
||||
with open("my_image.png", "rb") as f:
|
||||
content = f.read()
|
||||
success = await uploader.upload_image_bytes(content, "my_image.png")
|
||||
print(uploader.public_url) # {BASE_URL}/task-123/image/my_image.png
|
||||
"""
|
||||
extension = Path(file_name).suffix.lower()
|
||||
content_type = self.IMAGE_CONTENT_TYPES.get(extension, "image/jpeg")
|
||||
|
||||
upload_url = self._build_upload_url("image", file_name)
|
||||
self._last_public_url = self._build_public_url("image", file_name)
|
||||
print(f"[upload_image_bytes] Upload URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
upload_url, content=file_content, headers=headers, timeout=60.0
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"[upload_image_bytes] Success - Status Code: {response.status_code}")
|
||||
print(f"[upload_image_bytes] Public URL: {self._last_public_url}")
|
||||
return True
|
||||
else:
|
||||
print(f"[upload_image_bytes] Failed - Status Code: {response.status_code}")
|
||||
print(f"[upload_image_bytes] Response: {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
# 사용 예시:
|
||||
# import asyncio
|
||||
# asyncio.run(upload_video_to_azure_blob())
|
||||
# asyncio.run(upload_image_to_azure_blob())
|
||||
#
|
||||
# async def main():
|
||||
# uploader = AzureBlobUploader(task_id="task-123")
|
||||
#
|
||||
# # 음악 업로드 -> {BASE_URL}/task-123/song/my_song.mp3
|
||||
# await uploader.upload_music("my_song.mp3")
|
||||
# print(uploader.public_url)
|
||||
#
|
||||
# # 영상 업로드 -> {BASE_URL}/task-123/video/my_video.mp4
|
||||
# await uploader.upload_video("my_video.mp4")
|
||||
# print(uploader.public_url)
|
||||
#
|
||||
# # 이미지 업로드 -> {BASE_URL}/task-123/image/my_image.png
|
||||
# await uploader.upload_image("my_image.png")
|
||||
# print(uploader.public_url)
|
||||
#
|
||||
# asyncio.run(main())
|
||||
|
|
|
|||
16
config.py
16
config.py
|
|
@ -127,6 +127,21 @@ class CrawlerSettings(BaseSettings):
|
|||
model_config = _base_config
|
||||
|
||||
|
||||
class AzureBlobSettings(BaseSettings):
|
||||
"""Azure Blob Storage 설정"""
|
||||
|
||||
AZURE_BLOB_SAS_TOKEN: str = Field(
|
||||
default="",
|
||||
description="Azure Blob Storage SAS 토큰",
|
||||
)
|
||||
AZURE_BLOB_BASE_URL: str = Field(
|
||||
default="https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original",
|
||||
description="Azure Blob Storage 기본 URL",
|
||||
)
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
prj_settings = ProjectSettings()
|
||||
apikey_settings = APIKeySettings()
|
||||
db_settings = DatabaseSettings()
|
||||
|
|
@ -134,3 +149,4 @@ security_settings = SecuritySettings()
|
|||
notification_settings = NotificationSettings()
|
||||
cors_settings = CORSSettings()
|
||||
crawler_settings = CrawlerSettings()
|
||||
azure_blob_settings = AzureBlobSettings()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
SAS_TOKEN = "sp=racwdl&st=2025-12-01T00:13:29Z&se=2026-07-31T08:28:29Z&spr=https&sv=2024-11-04&sr=c&sig=7fE2ozVBPu3Gq43%2FZDxEYdEcPLDXyNVfTf16IBasmVQ%3D"
|
||||
|
||||
def upload_music_to_azure_blob(file_path = "스테이 머뭄_1.mp3", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp3"):
|
||||
URL = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/"
|
||||
|
||||
|
||||
def upload_music_to_azure_blob(
|
||||
file_path="스테이 머뭄_1.mp3",
|
||||
url="https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp3",
|
||||
):
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
headers = {
|
||||
"Content-Type": "audio/mpeg",
|
||||
"x-ms-blob-type": "BlockBlob"
|
||||
}
|
||||
headers = {"Content-Type": "audio/mpeg", "x-ms-blob-type": "BlockBlob"}
|
||||
with open(file_path, "rb") as file:
|
||||
response = requests.put(access_url, data=file, headers=headers)
|
||||
if response.status_code in [200, 201]:
|
||||
|
|
@ -17,12 +21,13 @@ def upload_music_to_azure_blob(file_path = "스테이 머뭄_1.mp3", url = "http
|
|||
print(f"Failed Status Code: {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
def upload_video_to_azure_blob(file_path = "스테이 머뭄.mp4", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp4"):
|
||||
|
||||
def upload_video_to_azure_blob(
|
||||
file_path="스테이 머뭄.mp4",
|
||||
url="https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp4",
|
||||
):
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
headers = {
|
||||
"Content-Type": "video/mp4",
|
||||
"x-ms-blob-type": "BlockBlob"
|
||||
}
|
||||
headers = {"Content-Type": "video/mp4", "x-ms-blob-type": "BlockBlob"}
|
||||
with open(file_path, "rb") as file:
|
||||
response = requests.put(access_url, data=file, headers=headers)
|
||||
|
||||
|
|
@ -33,7 +38,10 @@ def upload_video_to_azure_blob(file_path = "스테이 머뭄.mp4", url = "https:
|
|||
print(f"Response: {response.text}")
|
||||
|
||||
|
||||
def upload_image_to_azure_blob(file_path = "스테이 머뭄.png", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.png"):
|
||||
def upload_image_to_azure_blob(
|
||||
file_path="스테이 머뭄.png",
|
||||
url="https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.png",
|
||||
):
|
||||
access_url = f"{url}?{SAS_TOKEN}"
|
||||
extension = Path(file_path).suffix.lower()
|
||||
content_types = {
|
||||
|
|
@ -42,13 +50,10 @@ def upload_image_to_azure_blob(file_path = "스테이 머뭄.png", url = "https:
|
|||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp"
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
content_type = content_types.get(extension, "image/jpeg")
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"x-ms-blob-type": "BlockBlob"
|
||||
}
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
with open(file_path, "rb") as file:
|
||||
response = requests.put(access_url, data=file, headers=headers)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue