feat: add permanent public URL support for remote storage (OSS/S3)
This commit is contained in:
@@ -499,6 +499,51 @@ async def get_file_url(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/files/{file_id}/permanent-url", response_model=ApiResponse)
|
||||||
|
async def get_permanent_file_url(
|
||||||
|
file_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
storage_service: FileStorageService = Depends(get_file_storage_service),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取文件的永久公开 URL(无过期时间)。
|
||||||
|
|
||||||
|
- 本地存储:返回 API 永久访问地址(基于 FILE_LOCAL_SERVER_URL 配置)
|
||||||
|
- 远程存储(OSS/S3):返回 bucket 公读地址(需 bucket 已配置公共读权限)
|
||||||
|
"""
|
||||||
|
file_metadata = db.query(FileMetadata).filter(FileMetadata.id == file_id).first()
|
||||||
|
if not file_metadata:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="The file does not exist")
|
||||||
|
|
||||||
|
if file_metadata.status != "completed":
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"File upload not completed, status: {file_metadata.status}")
|
||||||
|
|
||||||
|
file_key = file_metadata.file_key
|
||||||
|
storage = storage_service.storage
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(storage, LocalStorage):
|
||||||
|
url = f"{settings.FILE_LOCAL_SERVER_URL}/storage/permanent/{file_id}"
|
||||||
|
else:
|
||||||
|
url = await storage.get_permanent_url(file_key)
|
||||||
|
if not url:
|
||||||
|
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
detail="Permanent URL not supported for current storage backend")
|
||||||
|
|
||||||
|
api_logger.info(f"Generated permanent URL: file_id={file_id}")
|
||||||
|
return success(
|
||||||
|
data={"url": url, "expires_in": None, "permanent": True, "file_name": file_metadata.file_name},
|
||||||
|
msg="Permanent file URL generated successfully"
|
||||||
|
)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
api_logger.error(f"Failed to generate permanent URL: {e}")
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to generate permanent URL: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/public/{file_id}", response_model=Any)
|
@router.get("/public/{file_id}", response_model=Any)
|
||||||
async def public_download_file(
|
async def public_download_file(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -641,14 +686,18 @@ async def permanent_download_file(
|
|||||||
media_type=file_metadata.content_type or "application/octet-stream"
|
media_type=file_metadata.content_type or "application/octet-stream"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# For remote storage, redirect to presigned URL with long expiration
|
# For remote storage, use permanent public URL (requires bucket public read)
|
||||||
try:
|
try:
|
||||||
# Use a very long expiration (7 days max for most cloud providers)
|
permanent_url = await storage.get_permanent_url(file_key)
|
||||||
|
if permanent_url:
|
||||||
|
api_logger.info(f"Redirecting to permanent public URL: file_key={file_key}")
|
||||||
|
return RedirectResponse(url=permanent_url, status_code=status.HTTP_302_FOUND)
|
||||||
|
# Fallback: long-lived presigned URL
|
||||||
presigned_url = await storage_service.get_file_url(file_key, expires=604800)
|
presigned_url = await storage_service.get_file_url(file_key, expires=604800)
|
||||||
presigned_url = _match_scheme(request, presigned_url)
|
presigned_url = _match_scheme(request, presigned_url)
|
||||||
return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
api_logger.error(f"Failed to get presigned URL: {e}")
|
api_logger.error(f"Failed to get permanent URL: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Failed to retrieve file: {str(e)}"
|
detail=f"Failed to retrieve file: {str(e)}"
|
||||||
|
|||||||
@@ -121,3 +121,18 @@ class StorageBackend(ABC):
|
|||||||
URL for accessing the file.
|
URL for accessing the file.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def get_permanent_url(self, file_key: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get a permanent public URL for the file (no expiration).
|
||||||
|
|
||||||
|
Returns None by default; remote storage backends should override this
|
||||||
|
if the bucket is configured for public read access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_key: Unique identifier for the file in the storage system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A permanent public URL, or None if not supported.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|||||||
@@ -261,3 +261,13 @@ class OSSStorage(StorageBackend):
|
|||||||
logger.error(f"Failed to generate presigned URL for {file_key}: {e}")
|
logger.error(f"Failed to generate presigned URL for {file_key}: {e}")
|
||||||
# Return a basic URL format as fallback
|
# Return a basic URL format as fallback
|
||||||
return f"https://{self.bucket_name}.{self.endpoint.replace('https://', '').replace('http://', '')}/{file_key}"
|
return f"https://{self.bucket_name}.{self.endpoint.replace('https://', '').replace('http://', '')}/{file_key}"
|
||||||
|
|
||||||
|
async def get_permanent_url(self, file_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Get a permanent public URL for the file (requires bucket public read).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A permanent URL in the format: https://{bucket}.{endpoint}/{file_key}
|
||||||
|
"""
|
||||||
|
host = self.endpoint.replace("https://", "").replace("http://", "")
|
||||||
|
return f"https://{self.bucket_name}.{host}/{file_key}"
|
||||||
|
|||||||
@@ -378,3 +378,12 @@ class S3Storage(StorageBackend):
|
|||||||
logger.error(f"Failed to generate presigned URL for {file_key}: {e}")
|
logger.error(f"Failed to generate presigned URL for {file_key}: {e}")
|
||||||
# Return a basic URL format as fallback
|
# Return a basic URL format as fallback
|
||||||
return f"https://{self.bucket_name}.s3.{self.region}.amazonaws.com/{file_key}"
|
return f"https://{self.bucket_name}.s3.{self.region}.amazonaws.com/{file_key}"
|
||||||
|
|
||||||
|
async def get_permanent_url(self, file_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Get a permanent public URL for the file (requires bucket public read).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A permanent URL in the format: https://{bucket}.s3.{region}.amazonaws.com/{file_key}
|
||||||
|
"""
|
||||||
|
return f"https://{self.bucket_name}.s3.{self.region}.amazonaws.com/{file_key}"
|
||||||
|
|||||||
Reference in New Issue
Block a user