refactor: 统一 sqlmodel_ext 用法至官方推荐模式
Some checks failed
Test / test (push) Failing after 3m47s

- 替换 Field(max_length=X) 为 StrX/TextX 类型别名(21 个 sqlmodels 文件)
- 替换 get + 404 检查为 get_exist_one()(17 个路由文件,约 50 处)
- 替换 save + session.refresh 为 save(load=...)
- 替换 session.add + commit 为 save()(dav/provider.py)
- 更新所有依赖至最新版本

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 11:13:16 +08:00
parent 9185f26b83
commit 6c96c43bea
57 changed files with 1091 additions and 761 deletions

View File

@@ -5,6 +5,7 @@ from utils.conf import appmeta
from .admin import admin_router
from .callback import callback_router
from .category import category_router
from .directory import directory_router
from .download import download_router
from .file import router as file_router
@@ -14,7 +15,6 @@ from .trash import trash_router
from .site import site_router
from .slave import slave_router
from .user import user_router
from .vas import vas_router
from .webdav import webdav_router
router = APIRouter(prefix="/v1")
@@ -24,6 +24,7 @@ router = APIRouter(prefix="/v1")
if appmeta.mode == "master":
router.include_router(admin_router)
router.include_router(callback_router)
router.include_router(category_router)
router.include_router(directory_router)
router.include_router(download_router)
router.include_router(file_router)
@@ -32,7 +33,6 @@ if appmeta.mode == "master":
router.include_router(site_router)
router.include_router(trash_router)
router.include_router(user_router)
router.include_router(vas_router)
router.include_router(webdav_router)
elif appmeta.mode == "slave":
router.include_router(slave_router)

View File

@@ -16,6 +16,12 @@ from sqlmodels.setting import (
from sqlmodels.setting import SettingsType
from utils import http_exceptions
from utils.conf import appmeta
try:
from ee.service import get_cached_license
except ImportError:
get_cached_license = None
from .file import admin_file_router
from .file_app import admin_file_app_router
from .group import admin_group_router
@@ -24,7 +30,6 @@ from .share import admin_share_router
from .task import admin_task_router
from .user import admin_user_router
from .theme import admin_theme_router
from .vas import admin_vas_router
class Aria2TestRequest(SQLModelBase):
@@ -50,7 +55,6 @@ admin_router.include_router(admin_policy_router)
admin_router.include_router(admin_share_router)
admin_router.include_router(admin_task_router)
admin_router.include_router(admin_theme_router)
admin_router.include_router(admin_vas_router)
# 离线下载 /api/admin/aria2
admin_aria2_router = APIRouter(
@@ -159,14 +163,24 @@ async def router_admin_get_summary(session: SessionDep) -> AdminSummaryResponse:
if site_url_setting and site_url_setting.value:
site_urls.append(site_url_setting.value)
# 许可证信息(从设置读取或使用默认值
license_info = LicenseInfo(
expired_at=now + timedelta(days=365),
signed_at=now,
root_domains=[],
domains=[],
vol_domains=[],
)
# 许可证信息(Pro 版本从缓存读取CE 版本永不过期
if appmeta.IsPro and get_cached_license:
payload = get_cached_license()
license_info = LicenseInfo(
expired_at=payload.expires_at,
signed_at=payload.issued_at,
root_domains=[],
domains=[payload.domain],
vol_domains=[],
)
else:
license_info = LicenseInfo(
expired_at=datetime.max,
signed_at=now,
root_domains=[],
domains=[],
vol_domains=[],
)
# 版本信息
version_info = VersionInfo(
@@ -225,11 +239,11 @@ async def router_admin_update_settings(
if existing:
existing.value = item.value
await existing.save(session)
existing = await existing.save(session)
updated_count += 1
else:
new_setting = Setting(type=item.type, name=item.name, value=item.value)
await new_setting.save(session)
new_setting = await new_setting.save(session)
created_count += 1
l.info(f"管理员更新了 {updated_count} 个设置项,新建了 {created_count} 个设置项")

View File

@@ -54,7 +54,7 @@ async def _set_ban_recursive(
obj.banned_by = None
obj.ban_reason = None
await obj.save(session)
obj = await obj.save(session)
count += 1
return count
@@ -131,9 +131,7 @@ async def router_admin_preview_file(
:param file_id: 文件UUID
:return: 文件内容
"""
file_obj = await Object.get(session, Object.id == file_id)
if not file_obj:
raise HTTPException(status_code=404, detail="文件不存在")
file_obj = await Object.get_exist_one(session, file_id)
if not file_obj.is_file:
raise HTTPException(status_code=400, detail="对象不是文件")
@@ -182,9 +180,7 @@ async def router_admin_ban_file(
:param claims: 当前管理员 JWT claims
:return: 封禁结果
"""
file_obj = await Object.get(session, Object.id == file_id)
if not file_obj:
raise HTTPException(status_code=404, detail="文件不存在")
file_obj = await Object.get_exist_one(session, file_id)
count = await _set_ban_recursive(session, file_obj, request.ban, claims.sub, request.reason)
@@ -212,9 +208,7 @@ async def router_admin_delete_file(
:param delete_physical: 是否同时删除物理文件
:return: 删除结果
"""
file_obj = await Object.get(session, Object.id == file_id)
if not file_obj:
raise HTTPException(status_code=404, detail="文件不存在")
file_obj = await Object.get_exist_one(session, file_id)
if not file_obj.is_file:
raise HTTPException(status_code=400, detail="对象不是文件")

View File

@@ -151,9 +151,7 @@ async def get_file_app(
错误处理:
- 404: 应用不存在
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
app = await FileApp.get_exist_one(session, app_id)
extensions = await FileAppExtension.get(
session,
@@ -186,9 +184,7 @@ async def update_file_app(
- 404: 应用不存在
- 409: 新 app_key 已被其他应用使用
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
app = await FileApp.get_exist_one(session, app_id)
# 检查 app_key 唯一性
if request.app_key is not None and request.app_key != app.app_key:
@@ -235,9 +231,7 @@ async def delete_file_app(
错误处理:
- 404: 应用不存在
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
app = await FileApp.get_exist_one(session, app_id)
app_name = app.app_key
await FileApp.delete(session, app)
@@ -263,9 +257,7 @@ async def update_extensions(
错误处理:
- 404: 应用不存在
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
app = await FileApp.get_exist_one(session, app_id)
# 保留旧扩展名的 wopi_action_urlDiscovery 填充的值)
old_extensions: list[FileAppExtension] = await FileAppExtension.get(
@@ -330,9 +322,7 @@ async def update_group_access(
错误处理:
- 404: 应用不存在
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
await FileApp.get_exist_one(session, app_id)
# 删除旧的用户组关联
old_links_result = await session.exec(
@@ -387,9 +377,7 @@ async def discover_wopi(
- 400: 非 WOPI 类型 / discovery URL 未配置 / XML 解析失败
- 502: WOPI 服务端不可达或返回无效响应
"""
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
if not app:
http_exceptions.raise_not_found("应用不存在")
app = await FileApp.get_exist_one(session, app_id)
if app.type != FileAppType.WOPI:
http_exceptions.raise_bad_request("仅 WOPI 类型应用支持自动发现")

View File

@@ -63,10 +63,7 @@ async def router_admin_get_group(
:param group_id: 用户组UUID
:return: 用户组详情
"""
group = await Group.get(session, Group.id == group_id, load=[Group.options, Group.policies])
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
group = await Group.get_exist_one(session, group_id, load=[Group.options, Group.policies])
# 直接访问已加载的关系,无需额外查询
policies = group.policies
@@ -94,9 +91,7 @@ async def router_admin_get_group_members(
:return: 分页成员列表
"""
# 验证组存在
group = await Group.get(session, Group.id == group_id)
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
await Group.get_exist_one(session, group_id)
result = await User.get_with_count(session, User.group_id == group_id, table_view=table_view)
@@ -138,10 +133,11 @@ async def router_admin_create_group(
speed_limit=request.speed_limit,
)
group = await group.save(session)
group_id_val: UUID = group.id
# 创建选项
options = GroupOptions(
group_id=group.id,
group_id=group_id_val,
share_download=request.share_download,
share_free=request.share_free,
relocate=request.relocate,
@@ -154,11 +150,11 @@ async def router_admin_create_group(
aria2=request.aria2,
redirected_source=request.redirected_source,
)
await options.save(session)
options = await options.save(session)
# 关联存储策略
for policy_id in request.policy_ids:
link = GroupPolicyLink(group_id=group.id, policy_id=policy_id)
link = GroupPolicyLink(group_id=group_id_val, policy_id=policy_id)
session.add(link)
await session.commit()
@@ -185,9 +181,7 @@ async def router_admin_update_group(
:param request: 更新请求
:return: 更新结果
"""
group = await Group.get(session, Group.id == group_id, load=Group.options)
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
group = await Group.get_exist_one(session, group_id, load=Group.options)
# 检查名称唯一性(如果要更新名称)
if request.name and request.name != group.name:
@@ -217,7 +211,7 @@ async def router_admin_update_group(
if options_data:
for key, value in options_data.items():
setattr(group.options, key, value)
await group.options.save(session)
group.options = await group.options.save(session)
# 更新策略关联
if request.policy_ids is not None:
@@ -255,9 +249,7 @@ async def router_admin_delete_group(
:param group_id: 用户组UUID
:return: 删除结果
"""
group = await Group.get(session, Group.id == group_id)
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
group = await Group.get_exist_one(session, group_id)
# 检查是否有用户属于该组
user_count = await User.count(session, User.group_id == group_id)

View File

@@ -332,7 +332,7 @@ async def router_policy_add_policy(
s3_path_style=request.s3_path_style,
s3_region=request.s3_region,
)
await options.save(session)
options = await options.save(session)
@admin_policy_router.post(
path='/cors',
@@ -383,9 +383,7 @@ async def router_policy_onddrive_oauth(
:param policy_id: 存储策略UUID
:return: OAuth URL
"""
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id)
# TODO: 实现OneDrive OAuth
raise HTTPException(status_code=501, detail="OneDrive OAuth暂未实现")
@@ -408,9 +406,7 @@ async def router_policy_get_policy(
:param policy_id: 存储策略UUID
:return: 策略详情
"""
policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id, load=Policy.options)
# 获取使用此策略的用户组
groups = await policy.awaitable_attrs.groups
@@ -459,9 +455,7 @@ async def router_policy_delete_policy(
:param policy_id: 存储策略UUID
:return: 删除结果
"""
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id)
# 检查是否有文件使用此策略
file_count = await Object.count(session, Object.policy_id == policy_id)
@@ -503,9 +497,7 @@ async def router_policy_update_policy(
:param policy_id: 存储策略UUID
:param request: 更新请求
"""
policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id, load=Policy.options)
# 检查名称唯一性(如果要更新名称)
if request.name and request.name != policy.name:
@@ -529,10 +521,10 @@ async def router_policy_update_policy(
if policy.options:
for key, value in options_data.items():
setattr(policy.options, key, value)
await policy.options.save(session)
policy.options = await policy.options.save(session)
else:
options = PolicyOptions(policy_id=policy.id, **options_data)
await options.save(session)
options = await options.save(session)
l.info(f"管理员更新了存储策略: {policy_id}")

View File

@@ -155,9 +155,7 @@ async def router_admin_delete_share(
:param share_id: 分享ID
:return: 删除结果
"""
share = await Share.get(session, Share.id == share_id)
if not share:
raise HTTPException(status_code=404, detail="分享不存在")
share = await Share.get_exist_one(session, share_id)
await Share.delete(session, share)

View File

@@ -150,9 +150,7 @@ async def router_admin_delete_task(
:param task_id: 任务ID
:return: 删除结果
"""
task = await Task.get(session, Task.id == task_id)
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
task = await Task.get_exist_one(session, task_id)
await Task.delete(session, task)

View File

@@ -71,7 +71,7 @@ async def router_admin_theme_create(
name=request.name,
**request.colors.model_dump(),
)
await preset.save(session)
preset = await preset.save(session)
l.info(f"管理员创建了主题预设: {request.name}")
@@ -101,11 +101,7 @@ async def router_admin_theme_update(
- 404: 预设不存在
- 409: 名称已被其他预设使用
"""
preset: ThemePreset | None = await ThemePreset.get(
session, ThemePreset.id == preset_id
)
if not preset:
http_exceptions.raise_not_found("主题预设不存在")
preset = await ThemePreset.get_exist_one(session, preset_id)
# 检查名称唯一性(排除自身)
if request.name is not None and request.name != preset.name:
@@ -120,7 +116,7 @@ async def router_admin_theme_update(
for key, value in color_data.items():
setattr(preset, key, value)
await preset.save(session)
preset = await preset.save(session)
l.info(f"管理员更新了主题预设: {preset.name}")
@@ -147,11 +143,7 @@ async def router_admin_theme_delete(
副作用:
- 关联用户的 theme_preset_id 会被数据库 SET NULL
"""
preset: ThemePreset | None = await ThemePreset.get(
session, ThemePreset.id == preset_id
)
if not preset:
http_exceptions.raise_not_found("主题预设不存在")
preset = await ThemePreset.get_exist_one(session, preset_id)
await preset.delete(session)
l.info(f"管理员删除了主题预设: {preset.name}")
@@ -180,11 +172,7 @@ async def router_admin_theme_set_default(
逻辑:
- 事务中先清除所有旧默认,再设新默认
"""
preset: ThemePreset | None = await ThemePreset.get(
session, ThemePreset.id == preset_id
)
if not preset:
http_exceptions.raise_not_found("主题预设不存在")
preset = await ThemePreset.get_exist_one(session, preset_id)
# 清除所有旧默认
await session.execute(
@@ -195,5 +183,5 @@ async def router_admin_theme_set_default(
# 设新默认
preset.is_default = True
await preset.save(session)
preset = await preset.save(session)
l.info(f"管理员将主题预设 '{preset.name}' 设为默认")

View File

@@ -128,8 +128,9 @@ async def router_admin_create_user(
is_verified=True,
user_id=user.id,
)
await identity.save(session)
identity = await identity.save(session)
user = await User.get(session, User.id == user.id, load=User.group)
return user.to_public()
@@ -153,9 +154,7 @@ async def router_admin_update_user(
:param request: 更新请求
:return: 更新结果
"""
user = await User.get(session, User.id == user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user = await User.get_exist_one(session, user_id)
# 默认管理员不允许更改用户组(通过 Setting 中的 default_admin_id 识别)
default_admin_setting = await Setting.get(
@@ -252,9 +251,7 @@ async def router_admin_calibrate_storage(
:param user_id: 用户UUID
:return: 校准结果
"""
user = await User.get(session, User.id == user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user = await User.get_exist_one(session, user_id)
previous_storage = user.storage

View File

@@ -1,81 +0,0 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from sqlmodels import (
ResponseBase,
)
admin_vas_router = APIRouter(
prefix='/vas',
tags=['admin', 'admin_vas']
)
@admin_vas_router.get(
path='/list',
summary='获取增值服务列表',
description='Get VAS list (orders and storage packs)',
dependencies=[Depends(admin_required)]
)
async def router_admin_get_vas_list(
session: SessionDep,
user_id: UUID | None = None,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
"""
获取增值服务列表(订单和存储包)。
:param session: 数据库会话
:param user_id: 按用户筛选
:param page: 页码
:param page_size: 每页数量
:return: 增值服务列表
"""
# TODO: 实现增值服务列表
# 需要查询 Order 和 StoragePack 模型
raise HTTPException(status_code=501, detail="增值服务管理暂未实现")
@admin_vas_router.get(
path='/{vas_id}',
summary='获取增值服务详情',
description='Get VAS detail by ID',
dependencies=[Depends(admin_required)]
)
async def router_admin_get_vas(
session: SessionDep,
vas_id: UUID,
) -> ResponseBase:
"""
获取增值服务详情。
:param session: 数据库会话
:param vas_id: 增值服务UUID
:return: 增值服务详情
"""
# TODO: 实现增值服务详情
raise HTTPException(status_code=501, detail="增值服务管理暂未实现")
@admin_vas_router.delete(
path='/{vas_id}',
summary='删除增值服务',
description='Delete VAS by ID',
dependencies=[Depends(admin_required)]
)
async def router_admin_delete_vas(
session: SessionDep,
vas_id: UUID,
) -> ResponseBase:
"""
删除增值服务。
:param session: 数据库会话
:param vas_id: 增值服务UUID
:return: 删除结果
"""
# TODO: 实现增值服务删除
raise HTTPException(status_code=501, detail="增值服务管理暂未实现")

View File

@@ -16,18 +16,12 @@ oauth_router = APIRouter(
tags=["callback", "oauth"],
)
pay_router = APIRouter(
prefix='/callback/pay',
tags=["callback", "pay"],
)
upload_router = APIRouter(
prefix='/callback/upload',
tags=["callback", "upload"],
)
callback_router.include_router(oauth_router)
callback_router.include_router(pay_router)
callback_router.include_router(upload_router)
@oauth_router.post(
@@ -38,7 +32,7 @@ callback_router.include_router(upload_router)
def router_callback_qq() -> ResponseBase:
"""
Handle QQ OAuth callback and return user information.
Returns:
ResponseBase: A model containing the response data for the QQ OAuth callback.
"""
@@ -55,11 +49,11 @@ async def router_callback_github(
GitHub OAuth 回调处理
- 错误响应示例:
- {
'error': 'bad_verification_code',
'error_description': 'The code passed is incorrect or expired.',
'error': 'bad_verification_code',
'error_description': 'The code passed is incorrect or expired.',
'error_uri': 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code'
}
Returns:
PlainTextResponse: A response containing the user information from GitHub.
"""
@@ -77,81 +71,6 @@ async def router_callback_github(
l.error(f"GitHub OAuth 回调异常: {e}")
return PlainTextResponse("认证过程中发生错误,请重试", status_code=500)
@pay_router.post(
path='/alipay',
summary='支付宝支付回调',
description='Handle Alipay payment callback and return payment status.',
)
def router_callback_alipay() -> ResponseBase:
"""
Handle Alipay payment callback and return payment status.
Returns:
ResponseBase: A model containing the response data for the Alipay payment callback.
"""
http_exceptions.raise_not_implemented()
@pay_router.post(
path='/wechat',
summary='微信支付回调',
description='Handle WeChat Pay payment callback and return payment status.',
)
def router_callback_wechat() -> ResponseBase:
"""
Handle WeChat Pay payment callback and return payment status.
Returns:
ResponseBase: A model containing the response data for the WeChat Pay payment callback.
"""
http_exceptions.raise_not_implemented()
@pay_router.post(
path='/stripe',
summary='Stripe支付回调',
description='Handle Stripe payment callback and return payment status.',
)
def router_callback_stripe() -> ResponseBase:
"""
Handle Stripe payment callback and return payment status.
Returns:
ResponseBase: A model containing the response data for the Stripe payment callback.
"""
http_exceptions.raise_not_implemented()
@pay_router.get(
path='/easypay',
summary='易支付回调',
description='Handle EasyPay payment callback and return payment status.',
)
def router_callback_easypay() -> PlainTextResponse:
"""
Handle EasyPay payment callback and return payment status.
Returns:
PlainTextResponse: A response containing the payment status for the EasyPay payment callback.
"""
http_exceptions.raise_not_implemented()
# return PlainTextResponse("success", status_code=200)
@pay_router.get(
path='/custom/{order_no}/{id}',
summary='自定义支付回调',
description='Handle custom payment callback and return payment status.',
)
def router_callback_custom(order_no: str, id: str) -> ResponseBase:
"""
Handle custom payment callback and return payment status.
Args:
order_no (str): The order number for the payment.
id (str): The ID associated with the payment.
Returns:
ResponseBase: A model containing the response data for the custom payment callback.
"""
http_exceptions.raise_not_implemented()
@upload_router.post(
path='/remote/{session_id}/{key}',
summary='远程上传回调',
@@ -160,11 +79,11 @@ def router_callback_custom(order_no: str, id: str) -> ResponseBase:
def router_callback_remote(session_id: str, key: str) -> ResponseBase:
"""
Handle remote upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
key (str): The key for the uploaded file.
Returns:
ResponseBase: A model containing the response data for the remote upload callback.
"""
@@ -178,15 +97,15 @@ def router_callback_remote(session_id: str, key: str) -> ResponseBase:
def router_callback_qiniu(session_id: str) -> ResponseBase:
"""
Handle Qiniu Cloud upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the Qiniu Cloud upload callback.
"""
http_exceptions.raise_not_implemented()
@upload_router.post(
path='/tencent/{session_id}',
summary='腾讯云上传回调',
@@ -195,16 +114,16 @@ def router_callback_qiniu(session_id: str) -> ResponseBase:
def router_callback_tencent(session_id: str) -> ResponseBase:
"""
Handle Tencent Cloud upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the Tencent Cloud upload callback.
"""
http_exceptions.raise_not_implemented()
@upload_router.post(
@upload_router.post(
path='/aliyun/{session_id}',
summary='阿里云上传回调',
description='Handle Aliyun upload callback and return upload status.',
@@ -212,16 +131,16 @@ def router_callback_tencent(session_id: str) -> ResponseBase:
def router_callback_aliyun(session_id: str) -> ResponseBase:
"""
Handle Aliyun upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the Aliyun upload callback.
"""
http_exceptions.raise_not_implemented()
@upload_router.post(
@upload_router.post(
path='/upyun/{session_id}',
summary='又拍云上传回调',
description='Handle Upyun upload callback and return upload status.',
@@ -229,10 +148,10 @@ def router_callback_aliyun(session_id: str) -> ResponseBase:
def router_callback_upyun(session_id: str) -> ResponseBase:
"""
Handle Upyun upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the Upyun upload callback.
"""
@@ -246,10 +165,10 @@ def router_callback_upyun(session_id: str) -> ResponseBase:
def router_callback_aws(session_id: str) -> ResponseBase:
"""
Handle AWS S3 upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the AWS S3 upload callback.
"""
@@ -263,10 +182,10 @@ def router_callback_aws(session_id: str) -> ResponseBase:
def router_callback_onedrive_finish(session_id: str) -> ResponseBase:
"""
Handle OneDrive upload completion callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseBase: A model containing the response data for the OneDrive upload completion callback.
"""
@@ -280,7 +199,7 @@ def router_callback_onedrive_finish(session_id: str) -> ResponseBase:
def router_callback_onedrive_auth() -> ResponseBase:
"""
Handle OneDrive authorization callback and return authorization status.
Returns:
ResponseBase: A model containing the response data for the OneDrive authorization callback.
"""
@@ -294,8 +213,8 @@ def router_callback_onedrive_auth() -> ResponseBase:
def router_callback_google_auth() -> ResponseBase:
"""
Handle Google OAuth completion callback and return authorization status.
Returns:
ResponseBase: A model containing the response data for the Google OAuth completion callback.
"""
http_exceptions.raise_not_implemented()
http_exceptions.raise_not_implemented()

View File

@@ -0,0 +1,100 @@
"""
文件分类筛选端点
按文件类型分类(图片/视频/音频/文档)查询用户的所有文件,
跨目录搜索,支持分页。扩展名映射从数据库 Setting 表读取。
"""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from middleware.auth import auth_required
from middleware.dependencies import SessionDep, TableViewRequestDep
from sqlmodels import (
FileCategory,
ListResponse,
Object,
ObjectResponse,
ObjectType,
Setting,
SettingsType,
User,
)
category_router = APIRouter(
prefix="/category",
tags=["category"],
)
@category_router.get(
path="/{category}",
summary="按分类获取文件列表",
)
async def router_category_list(
session: SessionDep,
user: Annotated[User, Depends(auth_required)],
category: FileCategory,
table_view: TableViewRequestDep,
) -> ListResponse[ObjectResponse]:
"""
按文件类型分类查询用户的所有文件
跨所有目录搜索,返回分页结果。
扩展名配置从数据库 Setting 表读取type=file_category
认证:
- JWT token in Authorization header
路径参数:
- category: 文件分类image / video / audio / document
查询参数:
- offset: 分页偏移量默认0
- limit: 每页数量默认20最大100
- desc: 是否降序默认true
- order: 排序字段created_at / updated_at
响应:
- ListResponse[ObjectResponse]: 分页文件列表
错误处理:
- HTTPException 422: category 参数无效
- HTTPException 404: 该分类未配置扩展名
"""
# 从数据库读取该分类的扩展名配置
setting = await Setting.get(
session,
(Setting.type == SettingsType.FILE_CATEGORY) & (Setting.name == category.value),
)
if not setting or not setting.value:
raise HTTPException(status_code=404, detail=f"分类 {category.value} 未配置扩展名")
extensions = [ext.strip() for ext in setting.value.split(",") if ext.strip()]
if not extensions:
raise HTTPException(status_code=404, detail=f"分类 {category.value} 扩展名列表为空")
result = await Object.get_by_category(
session,
user.id,
extensions,
table_view=table_view,
)
items = [
ObjectResponse(
id=obj.id,
name=obj.name,
type=ObjectType.FILE,
size=obj.size,
mime_type=obj.mime_type,
thumb=False,
created_at=obj.created_at,
updated_at=obj.updated_at,
source_enabled=False,
)
for obj in result.items
]
return ListResponse(count=result.count, items=items)

View File

@@ -206,4 +206,4 @@ async def router_directory_create(
parent_id=parent_id,
policy_id=policy_id,
)
await new_folder.save(session)
new_folder = await new_folder.save(session)

View File

@@ -184,9 +184,7 @@ async def create_upload_session(
# 确定存储策略
policy_id = request.policy_id or parent.policy_id
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id)
# 校验用户组是否有权使用该策略(仅当用户显式指定 policy_id 时)
if request.policy_id:
@@ -711,9 +709,7 @@ async def create_empty_file(
# 确定存储策略
policy_id = request.policy_id or parent.policy_id
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id)
# 生成存储路径并创建空文件
storage_path: str | None = None
@@ -942,7 +938,7 @@ async def file_get(
# 递增下载次数
link.downloads += 1
await link.save(session)
link = await link.save(session)
if policy.type == PolicyType.LOCAL:
storage_service = LocalStorageService(policy)
@@ -996,7 +992,7 @@ async def file_source_redirect(
# 递增下载次数
link.downloads += 1
await link.save(session)
link = await link.save(session)
if policy.type == PolicyType.LOCAL:
storage_service = LocalStorageService(policy)

View File

@@ -112,9 +112,7 @@ async def router_object_create(
# 确定存储策略
policy_id = request.policy_id or parent.policy_id
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
policy = await Policy.get_exist_one(session, policy_id)
parent_id = parent.id
@@ -149,7 +147,7 @@ async def router_object_create(
owner_id=user_id,
policy_id=policy_id,
)
await file_object.save(session)
file_object = await file_object.save(session)
l.info(f"创建空白文件: {request.name}")
@@ -474,7 +472,7 @@ async def router_object_rename(
# 更新名称
obj.name = new_name
await obj.save(session)
obj = await obj.save(session)
l.info(f"用户 {user_id} 将对象 {obj.id} 重命名为 {new_name}")
@@ -682,7 +680,7 @@ async def router_object_switch_policy(
dest_policy_id=dest_policy_id,
object_id=obj_id,
)
await task_props.save(session)
task_props = await task_props.save(session)
if obj_is_file:
# 文件:后台迁移
@@ -698,7 +696,7 @@ async def router_object_switch_policy(
# 目录:先更新目录自身的 policy_id
obj = await Object.get(session, Object.id == obj_id)
obj.policy_id = dest_policy_id
await obj.save(session)
obj = await obj.save(session)
if request.is_migrate_existing:
# 后台迁移所有已有文件
@@ -715,7 +713,7 @@ async def router_object_switch_policy(
task = await Task.get(session, Task.id == task_id)
task.status = TaskStatus.COMPLETED
task.progress = 100
await task.save(session)
task = await task.save(session)
# 重新获取 task 以读取最新状态
task = await Task.get(session, Task.id == task_id)
@@ -850,7 +848,7 @@ async def router_patch_object_metadata(
)
if existing:
existing.value = patch.value
await existing.save(session)
existing = await existing.save(session)
else:
entry = ObjectMetadata(
object_id=object_id,
@@ -858,6 +856,6 @@ async def router_patch_object_metadata(
value=patch.value,
is_public=True,
)
await entry.save(session)
entry = await entry.save(session)
l.info(f"用户 {user.id} 更新了对象 {object_id}{len(request.patches)} 条元数据")

View File

@@ -102,7 +102,7 @@ async def router_create_custom_property(
options=request.options,
default_value=request.default_value,
)
await definition.save(session)
definition = await definition.save(session)
l.info(f"用户 {user.id} 创建了自定义属性: {request.name}")
@@ -128,12 +128,7 @@ async def router_update_custom_property(
- 404: 属性定义不存在
- 403: 无权操作此属性
"""
definition = await CustomPropertyDefinition.get(
session,
CustomPropertyDefinition.id == id,
)
if not definition:
raise HTTPException(status_code=404, detail="自定义属性不存在")
definition = await CustomPropertyDefinition.get_exist_one(session, id)
if definition.owner_id != user.id:
raise HTTPException(status_code=403, detail="无权操作此属性")
@@ -163,12 +158,7 @@ async def router_delete_custom_property(
- 404: 属性定义不存在
- 403: 无权操作此属性
"""
definition = await CustomPropertyDefinition.get(
session,
CustomPropertyDefinition.id == id,
)
if not definition:
raise HTTPException(status_code=404, detail="自定义属性不存在")
definition = await CustomPropertyDefinition.get_exist_one(session, id)
if definition.owner_id != user.id:
raise HTTPException(status_code=403, detail="无权操作此属性")

View File

@@ -45,12 +45,7 @@ async def router_share_get(
4. 返回分享详情(含文件树和分享者信息)
"""
# 1. 查询分享(预加载 user 和 object
share = await Share.get(
session, Share.id == id,
load=[Share.user, Share.object],
)
if not share:
http_exceptions.raise_not_found(detail="分享不存在或已被取消")
share = await Share.get_exist_one(session, id, load=[Share.user, Share.object])
# 2. 检查过期
now = datetime.now()
@@ -474,16 +469,29 @@ def router_share_update(id: str) -> ResponseBase:
path='/{id}',
summary='删除分享',
description='Delete a share by ID.',
dependencies=[Depends(auth_required)]
status_code=204,
)
def router_share_delete(id: str) -> ResponseBase:
async def router_share_delete(
session: SessionDep,
user: Annotated[User, Depends(auth_required)],
id: UUID,
) -> None:
"""
Delete a share by ID.
Args:
id (str): The ID of the share to be deleted.
Returns:
ResponseBase: A model containing the response data for the deleted share.
删除分享
认证:需要 JWT token
流程:
1. 通过分享ID查找分享
2. 验证分享属于当前用户
3. 删除分享记录
"""
http_exceptions.raise_not_implemented()
share = await Share.get_exist_one(session, id)
if share.user_id != user.id:
http_exceptions.raise_forbidden(detail="无权删除此分享")
user_id = user.id
share_code = share.code
await Share.delete(session, share)
l.info(f"用户 {user_id} 删除了分享: {share_code}")

View File

@@ -234,7 +234,7 @@ async def router_user_register(
group_id=default_group.id,
)
new_user_id = new_user.id
await new_user.save(session)
new_user = await new_user.save(session)
# 7. 创建 AuthIdentity
hashed_password = Password.hash(request.credential) if request.credential else None
@@ -246,7 +246,7 @@ async def router_user_register(
is_verified=False,
user_id=new_user_id,
)
await identity.save(session)
identity = await identity.save(session)
# 8. 创建用户根目录(使用用户组关联的第一个存储策略)
await session.refresh(default_group, ['policies'])
@@ -494,9 +494,24 @@ async def router_user_storage(
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
# [TODO] 总空间加上用户购买的额外空间
# 查询用户所有未过期容量包的 size 总和
from datetime import datetime
from sqlalchemy import func, select, and_, or_
total: int = group.max_storage
now = datetime.now()
stmt = select(func.coalesce(func.sum(sqlmodels.StoragePack.size), 0)).where(
and_(
sqlmodels.StoragePack.user_id == user.id,
or_(
sqlmodels.StoragePack.expired_time.is_(None),
sqlmodels.StoragePack.expired_time > now,
),
)
)
result = await session.exec(stmt)
active_packs_total: int = result.scalar_one()
total: int = group.max_storage + active_packs_total
used: int = user.storage
free: int = max(0, total - used)
@@ -638,7 +653,7 @@ async def router_user_authn_finish(
is_verified=True,
user_id=user.id,
)
await identity.save(session)
identity = await identity.save(session)
return authn.to_detail_response()

View File

@@ -218,7 +218,7 @@ async def router_user_settings_avatar(
# 更新用户头像字段
user.avatar = "file"
await user.save(session)
user = await user.save(session)
@user_settings_router.put(
@@ -252,7 +252,7 @@ async def router_user_settings_avatar_gravatar(
await delete_avatar_files(session, user.id)
user.avatar = "gravatar"
await user.save(session)
user = await user.save(session)
@user_settings_router.delete(
@@ -279,7 +279,7 @@ async def router_user_settings_avatar_delete(
await delete_avatar_files(session, user.id)
user.avatar = "default"
await user.save(session)
user = await user.save(session)
@user_settings_router.patch(
@@ -321,7 +321,7 @@ async def router_user_settings_theme(
user.color_error = request.theme_colors.error
user.color_neutral = request.theme_colors.neutral
await user.save(session)
user = await user.save(session)
@user_settings_router.patch(
@@ -358,7 +358,7 @@ async def router_user_settings_change_password(
http_exceptions.raise_forbidden("当前密码错误")
email_identity.credential = Password.hash(request.new_password)
await email_identity.save(session)
email_identity = await email_identity.save(session)
@user_settings_router.patch(
@@ -392,7 +392,7 @@ async def router_user_settings_patch(
http_exceptions.raise_bad_request(f"设置项 {option.value} 不允许为空")
setattr(user, option.value, value)
await user.save(session)
user = await user.save(session)
@user_settings_router.get(
@@ -454,7 +454,7 @@ async def router_user_settings_2fa_enable(
extra: dict = orjson.loads(email_identity.extra_data) if email_identity.extra_data else {}
extra["two_factor"] = secret
email_identity.extra_data = orjson.dumps(extra).decode('utf-8')
await email_identity.save(session)
email_identity = await email_identity.save(session)
# ==================== 认证身份管理 ====================

View File

@@ -79,9 +79,7 @@ async def set_default_viewer(
if existing:
existing.app_id = request.app_id
existing = await existing.save(session)
# 重新加载 app 关系
await session.refresh(existing, attribute_names=["app"])
existing = await existing.save(session, load=UserFileAppDefault.app)
return existing.to_response()
else:
new_default = UserFileAppDefault(
@@ -89,9 +87,7 @@ async def set_default_viewer(
extension=normalized_ext,
app_id=request.app_id,
)
new_default = await new_default.save(session)
# 重新加载 app 关系
await session.refresh(new_default, attribute_names=["app"])
new_default = await new_default.save(session, load=UserFileAppDefault.app)
return new_default.to_response()

View File

@@ -1,106 +0,0 @@
from fastapi import APIRouter, Depends
from middleware.auth import auth_required
from sqlmodels import ResponseBase
from utils import http_exceptions
vas_router = APIRouter(
prefix="/vas",
tags=["vas"]
)
@vas_router.get(
path='/pack',
summary='获取容量包及配额信息',
description='Get information about storage packs and quotas.',
dependencies=[Depends(auth_required)]
)
def router_vas_pack() -> ResponseBase:
"""
Get information about storage packs and quotas.
Returns:
ResponseBase: A model containing the response data for storage packs and quotas.
"""
http_exceptions.raise_not_implemented()
@vas_router.get(
path='/product',
summary='获取商品信息,同时返回支付信息',
description='Get product information along with payment details.',
dependencies=[Depends(auth_required)]
)
def router_vas_product() -> ResponseBase:
"""
Get product information along with payment details.
Returns:
ResponseBase: A model containing the response data for products and payment information.
"""
http_exceptions.raise_not_implemented()
@vas_router.post(
path='/order',
summary='新建支付订单',
description='Create an order for a product.',
dependencies=[Depends(auth_required)]
)
def router_vas_order() -> ResponseBase:
"""
Create an order for a product.
Returns:
ResponseBase: A model containing the response data for the created order.
"""
http_exceptions.raise_not_implemented()
@vas_router.get(
path='/order/{id}',
summary='查询订单状态',
description='Get information about a specific payment order by ID.',
dependencies=[Depends(auth_required)]
)
def router_vas_order_get(id: str) -> ResponseBase:
"""
Get information about a specific payment order by ID.
Args:
id (str): The ID of the order to retrieve information for.
Returns:
ResponseBase: A model containing the response data for the specified order.
"""
http_exceptions.raise_not_implemented()
@vas_router.get(
path='/redeem',
summary='获取兑换码信息',
description='Get information about a specific redemption code.',
dependencies=[Depends(auth_required)]
)
def router_vas_redeem(code: str) -> ResponseBase:
"""
Get information about a specific redemption code.
Args:
code (str): The redemption code to retrieve information for.
Returns:
ResponseBase: A model containing the response data for the specified redemption code.
"""
http_exceptions.raise_not_implemented()
@vas_router.post(
path='/redeem',
summary='执行兑换',
description='Redeem a redemption code for a product or service.',
dependencies=[Depends(auth_required)]
)
def router_vas_redeem_post() -> ResponseBase:
"""
Redeem a redemption code for a product or service.
Returns:
ResponseBase: A model containing the response data for the redeemed code.
"""
http_exceptions.raise_not_implemented()

View File

@@ -175,8 +175,7 @@ async def _finalize_upload(
obj = await Object.get(session, Object.id == object_id)
if obj:
obj.sqlmodel_update({'size': size, 'physical_file_id': pf.id})
session.add(obj)
await session.commit()
obj = await obj.save(session)
# 更新用户存储用量
if size > 0:
@@ -193,8 +192,7 @@ async def _move_object(
obj = await Object.get(session, Object.id == object_id)
if obj:
obj.sqlmodel_update({'parent_id': new_parent_id, 'name': new_name})
session.add(obj)
await session.commit()
obj = await obj.save(session)
async def _copy_object_recursive(