Files

184 lines
5.5 KiB
Python

"""
File Service
文件服务,处理文件上传、分片上传等功能
"""
import os
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
from app.core.logging import get_logger
from app.db.models.chunk_upload import ChunkUploadPreRequest
from app.module.shared.chunk_upload_repository import ChunkUploadRepository
from app.module.shared.chunks_saver import ChunksSaver
from app.module.shared.file_models import (
ChunkUploadRequestDto,
FileUploadResult,
)
logger = get_logger(__name__)
class FileService:
"""文件服务"""
DEFAULT_TIMEOUT_SECONDS = 120
def __init__(
self,
chunk_upload_repo: ChunkUploadRepository,
):
self.chunk_upload_repo = chunk_upload_repo
async def pre_upload(
self,
upload_path: str,
service_id: str,
db_session,
check_info: Optional[str] = None
) -> str:
"""
预上传
Args:
upload_path: 上传路径
service_id: 服务ID
check_info: 业务信息
Returns:
预上传请求ID
"""
req_id = str(uuid.uuid4())
timeout = datetime.utcnow().replace(
microsecond=0
) + timedelta(seconds=self.DEFAULT_TIMEOUT_SECONDS)
pre_request = ChunkUploadPreRequest(
id=req_id,
total_file_num=1,
uploaded_file_num=0,
upload_path=upload_path,
timeout=timeout,
service_id=service_id,
check_info=check_info,
)
await self.chunk_upload_repo.insert(pre_request, db_session)
return req_id
async def chunk_upload(
self,
upload_request: ChunkUploadRequestDto,
upload_path: str,
file_content: bytes,
db_session,
) -> FileUploadResult:
"""
分片上传
Args:
upload_request: 上传请求
upload_path: 上传路径
file_content: 文件内容
db_session: 数据库会话
Returns:
上传结果
"""
upload_request.file_size = len(file_content)
pre_request = await self.chunk_upload_repo.find_by_id(
upload_request.req_id, db_session
)
if pre_request is None:
logger.error(f"pre-upload request not found: {upload_request.req_id}")
raise ValueError("Pre-upload request not found")
if pre_request.is_upload_complete():
logger.error(f"upload already complete: {upload_request.req_id}")
raise ValueError("Upload already complete")
if pre_request.is_request_timeout():
logger.error(f"upload request timeout: {upload_request.req_id}")
raise ValueError("Upload request timeout")
saved_file_path = None
if upload_request.total_chunk_num > 1:
saved_file_path = await self._upload_chunk(
upload_request, pre_request, upload_path, file_content
)
else:
saved_file_path = await self._upload_file(
upload_request, pre_request, upload_path, file_content
)
update_count = await self.chunk_upload_repo.update(pre_request, db_session)
if update_count == 0:
logger.error(f"failed to update pre-request: {upload_request.req_id}")
raise ValueError("Failed to update pre-upload request")
is_finish = pre_request.uploaded_file_num == pre_request.total_file_num
if is_finish:
temp_dir = os.path.join(
upload_path,
ChunksSaver.TEMP_DIR_NAME_FORMAT % pre_request.id
)
try:
ChunksSaver.delete_folder(temp_dir)
except Exception as e:
logger.warning(f"failed to delete temp dir: {temp_dir}, error: {e}")
await self.chunk_upload_repo.delete_by_id(pre_request.id, db_session)
return FileUploadResult(
is_all_files_uploaded=is_finish,
check_info=pre_request.check_info,
saved_file_path=str(saved_file_path) if saved_file_path else None,
file_name=upload_request.file_name,
)
async def _upload_file(
self,
upload_request: ChunkUploadRequestDto,
pre_request: ChunkUploadPreRequest,
upload_path: str,
file_content: bytes
) -> Path:
"""上传单文件"""
saved_file = ChunksSaver.save_file(
upload_request, upload_path, file_content
)
pre_request.timeout = datetime.utcnow().replace(
microsecond=0
) + timedelta(seconds=self.DEFAULT_TIMEOUT_SECONDS)
pre_request.increment_uploaded_file_num()
return saved_file
async def _upload_chunk(
self,
upload_request: ChunkUploadRequestDto,
pre_request: ChunkUploadPreRequest,
upload_path: str,
file_content: bytes
) -> Optional[Path]:
"""上传分片"""
saved_file = ChunksSaver.save(
upload_request, pre_request.id, upload_path, file_content
)
if saved_file is not None:
pre_request.increment_uploaded_file_num()
return saved_file
pre_request.timeout = datetime.utcnow().replace(
microsecond=0
) + timedelta(seconds=self.DEFAULT_TIMEOUT_SECONDS)
return None