Add Redis-based one-time download token support
Integrate Redis as a backend for one-time download token validation, with in-memory fallback. Added RedisManager for connection lifecycle, TokenStore for atomic token usage checks, and related configuration via environment variables. Updated download flow to ensure tokens are single-use, and improved API robustness for batch operations. Updated dependencies to include redis and cachetools.
This commit is contained in:
@@ -32,7 +32,8 @@ async def router_directory_get(
|
||||
"""
|
||||
获取目录内容
|
||||
|
||||
路径必须以用户名开头,如 /api/directory/admin 或 /api/directory/admin/docs
|
||||
路径必须以用户名或 `.crash` 开头,如 /api/directory/admin 或 /api/directory/admin/docs
|
||||
`.crash` 代表回收站,也就意味着用户名禁止为 `.crash`
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
|
||||
@@ -33,6 +33,7 @@ from models import (
|
||||
User,
|
||||
)
|
||||
from service.storage import LocalStorageService
|
||||
from service.redis.token_store import TokenStore
|
||||
from utils.JWT import create_download_token, DOWNLOAD_TOKEN_TTL
|
||||
from utils import http_exceptions
|
||||
|
||||
@@ -370,7 +371,7 @@ async def create_download_token_endpoint(
|
||||
@_download_router.get(
|
||||
path='/{token}',
|
||||
summary='下载文件',
|
||||
description='使用下载令牌下载文件。',
|
||||
description='使用下载令牌下载文件(一次性令牌,仅可使用一次)。',
|
||||
)
|
||||
async def download_file(
|
||||
session: SessionDep,
|
||||
@@ -380,13 +381,19 @@ async def download_file(
|
||||
下载文件端点
|
||||
|
||||
验证 JWT 令牌后返回文件内容。
|
||||
令牌为一次性使用,下载后即失效。
|
||||
"""
|
||||
# 验证令牌
|
||||
result = verify_download_token(token)
|
||||
if not result:
|
||||
raise HTTPException(status_code=401, detail="下载令牌无效或已过期")
|
||||
|
||||
file_id, owner_id = result
|
||||
jti, file_id, owner_id = result
|
||||
|
||||
# 检查并标记令牌已使用(原子操作)
|
||||
is_first_use = await TokenStore.mark_used(jti, DOWNLOAD_TOKEN_TTL)
|
||||
if not is_first_use:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
# 获取文件对象
|
||||
file_obj = await Object.get(session, Object.id == file_id)
|
||||
|
||||
@@ -193,7 +193,10 @@ async def router_object_delete(
|
||||
user_id = user.id
|
||||
deleted_count = 0
|
||||
|
||||
for obj_id in request.ids:
|
||||
# 处理单个 UUID 或 UUID 列表
|
||||
ids = request.ids if isinstance(request.ids, list) else [request.ids]
|
||||
|
||||
for obj_id in ids:
|
||||
obj = await Object.get(session, Object.id == obj_id)
|
||||
if not obj or obj.owner_id != user_id:
|
||||
continue
|
||||
@@ -212,7 +215,7 @@ async def router_object_delete(
|
||||
return ResponseBase(
|
||||
data={
|
||||
"deleted": deleted_count,
|
||||
"total": len(request.ids),
|
||||
"total": len(ids),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -248,7 +251,10 @@ async def router_object_move(
|
||||
|
||||
moved_count = 0
|
||||
|
||||
for src_id in request.src_ids:
|
||||
# 处理单个 UUID 或 UUID 列表
|
||||
src_ids = request.src_ids if isinstance(request.src_ids, list) else [request.src_ids]
|
||||
|
||||
for src_id in src_ids:
|
||||
src = await Object.get(session, Object.id == src_id)
|
||||
if not src or src.owner_id != user_id:
|
||||
continue
|
||||
@@ -290,7 +296,7 @@ async def router_object_move(
|
||||
return ResponseBase(
|
||||
data={
|
||||
"moved": moved_count,
|
||||
"total": len(request.src_ids),
|
||||
"total": len(src_ids),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user