Refactor import statements for ResponseBase in API routers

- Updated import statements in the following files to import ResponseBase directly from models instead of models.response:
  - routers/api/v1/share/__init__.py
  - routers/api/v1/site/__init__.py
  - routers/api/v1/slave/__init__.py
  - routers/api/v1/tag/__init__.py
  - routers/api/v1/user/__init__.py
  - routers/api/v1/vas/__init__.py
  - routers/api/v1/webdav/__init__.py

Enhance user registration and related endpoints in user router

- Changed return type annotations from models.response.ResponseBase to models.ResponseBase in multiple functions.
- Updated return statements to reflect the new import structure.
- Improved documentation for clarity.

Add PhysicalFile model and storage service implementation

- Introduced PhysicalFile model to represent actual files on disk with reference counting logic.
- Created storage service module with local storage implementation, including file operations and error handling.
- Defined exceptions for storage operations to improve error handling.
- Implemented naming rule parser for generating file and directory names based on templates.

Update dependency management in uv.lock

- Added aiofiles version 25.1.0 to the project dependencies.
This commit is contained in:
2025-12-23 12:20:06 +08:00
parent 96bf447426
commit 446d219aca
26 changed files with 2155 additions and 399 deletions

View File

@@ -12,7 +12,7 @@ from .admin import admin_vas_router
from .callback import callback_router
from .directory import directory_router
from .download import download_router
from .file import file_router
from .file import file_router, file_upload_router
from .object import object_router
from .share import share_router
from .site import site_router
@@ -36,6 +36,7 @@ router.include_router(callback_router)
router.include_router(directory_router)
router.include_router(download_router)
router.include_router(file_router)
router.include_router(file_upload_router)
router.include_router(object_router)
router.include_router(share_router)
router.include_router(site_router)

View File

@@ -1,11 +1,57 @@
from fastapi import APIRouter, Depends
from loguru import logger
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from sqlmodel import Field
from middleware.auth import AdminRequired
from middleware.dependencies import SessionDep
from models import User
from models import Policy, PolicyOptions, PolicyType, User
from models.base import SQLModelBase
from models import ResponseBase
from models.user import UserPublic
from models.response import ResponseBase
from service.storage import DirectoryCreationError, LocalStorageService
class PolicyCreateRequest(SQLModelBase):
"""创建存储策略请求 DTO"""
name: str = Field(max_length=255)
"""策略名称"""
type: PolicyType
"""策略类型"""
server: str | None = Field(default=None, max_length=255)
"""服务器地址/本地路径(本地存储必填)"""
bucket_name: str | None = Field(default=None, max_length=255)
"""存储桶名称S3必填"""
is_private: bool = True
"""是否为私有空间"""
base_url: str | None = Field(default=None, max_length=255)
"""访问文件的基础URL"""
access_key: str | None = None
"""Access Key"""
secret_key: str | None = None
"""Secret Key"""
max_size: int = Field(default=0, ge=0)
"""允许上传的最大文件尺寸字节0表示不限制"""
auto_rename: bool = False
"""是否自动重命名"""
dir_name_rule: str | None = Field(default=None, max_length=255)
"""目录命名规则"""
file_name_rule: str | None = Field(default=None, max_length=255)
"""文件命名规则"""
is_origin_link_enable: bool = False
"""是否开启源链接访问"""
# 管理员根目录 /api/admin
admin_router = APIRouter(
@@ -464,11 +510,72 @@ def router_policy_test_slave() -> ResponseBase:
@admin_policy_router.post(
path='/',
summary='创建存储策略',
description='',
description='创建新的存储策略。对于本地存储策略,会自动创建物理目录。',
dependencies=[Depends(AdminRequired)]
)
def router_policy_add_policy() -> ResponseBase:
pass
async def router_policy_add_policy(
session: SessionDep,
request: PolicyCreateRequest,
) -> ResponseBase:
"""
创建存储策略端点
功能:
- 创建新的存储策略配置
- 对于 LOCAL 类型,自动创建物理目录
认证:
- 需要管理员权限
:param session: 数据库会话
:param request: 创建请求
:return: 创建结果
"""
# 验证本地存储策略必须指定 server 路径
if request.type == PolicyType.LOCAL:
if not request.server:
raise HTTPException(status_code=400, detail="本地存储策略必须指定 server 路径")
# 检查策略名称是否已存在
existing = await Policy.get(session, Policy.name == request.name)
if existing:
raise HTTPException(status_code=409, detail="策略名称已存在")
# 创建策略对象
policy = Policy(
name=request.name,
type=request.type,
server=request.server,
bucket_name=request.bucket_name,
is_private=request.is_private,
base_url=request.base_url,
access_key=request.access_key,
secret_key=request.secret_key,
max_size=request.max_size,
auto_rename=request.auto_rename,
dir_name_rule=request.dir_name_rule,
file_name_rule=request.file_name_rule,
is_origin_link_enable=request.is_origin_link_enable,
)
# 对于本地存储策略,创建物理目录
if policy.type == PolicyType.LOCAL:
try:
storage_service = LocalStorageService(policy)
await storage_service.ensure_base_directory()
l.info(f"已为本地存储策略 '{policy.name}' 创建目录: {policy.server}")
except DirectoryCreationError as e:
raise HTTPException(status_code=500, detail=f"创建存储目录失败: {e}")
# 保存到数据库
policy = await policy.save(session)
return ResponseBase(data={
"id": str(policy.id),
"name": policy.name,
"type": policy.type.value,
"server": policy.server,
})
@admin_policy_router.post(
path='/cors',

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, Query
from fastapi.responses import PlainTextResponse, RedirectResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
import service.oauth
callback_router = APIRouter(

View File

@@ -12,7 +12,7 @@ from models import (
ObjectType,
PolicyResponse,
User,
response,
ResponseBase,
)
directory_router = APIRouter(
@@ -63,7 +63,6 @@ async def router_directory_get(
ObjectResponse(
id=child.id,
name=child.name,
path=f"/{child.name}", # TODO: 完整路径
thumb=False,
size=child.size,
type=ObjectType.FOLDER if child.is_folder else ObjectType.FILE,
@@ -97,7 +96,7 @@ async def router_directory_create(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: DirectoryCreateRequest
) -> response.ResponseBase:
) -> ResponseBase:
"""
创建目录
@@ -111,6 +110,7 @@ async def router_directory_create(
if not name:
raise HTTPException(status_code=400, detail="目录名称不能为空")
# [TODO] 进一步验证名称合法性
if "/" in name or "\\" in name:
raise HTTPException(status_code=400, detail="目录名称不能包含斜杠")
@@ -146,7 +146,7 @@ async def router_directory_create(
new_folder_name = new_folder.name
await new_folder.save(session)
return response.ResponseBase(
return ResponseBase(
data={
"id": new_folder_id,
"name": new_folder_name,

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
download_router = APIRouter(
prefix="/download",

View File

@@ -1,7 +1,37 @@
from fastapi import APIRouter, Depends, UploadFile
"""
文件操作路由
提供文件上传、下载、创建等核心功能。
路由前缀:
- /file - 文件操作
- /file/upload - 上传相关操作
"""
from datetime import datetime, timedelta
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import FileResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
from loguru import logger as l
from middleware.auth import AuthRequired, SignRequired
from middleware.dependencies import SessionDep
from models import (
CreateFileRequest,
CreateUploadSessionRequest,
Object,
ObjectType,
PhysicalFile,
Policy,
PolicyType,
UploadChunkResponse,
UploadSession,
UploadSessionResponse,
User,
)
from models import ResponseBase
from service.storage import LocalStorageService, StorageFileNotFoundError
file_router = APIRouter(
prefix="/file",
@@ -13,370 +43,614 @@ file_upload_router = APIRouter(
tags=["file"]
)
@file_router.get(
path='/get/{id}/{name}',
summary='文件外链(直接输出文件数据)',
description='Get file external link endpoint.',
)
def router_file_get(id: str, name: str) -> FileResponse:
"""
Get file external link endpoint.
Args:
id (str): The ID of the file.
name (str): The name of the file.
Returns:
FileResponse: A response containing the file data.
"""
pass
@file_router.get(
path='/source/{id}/{name}',
summary='文件外链(301跳转)',
description='Get file external link with 301 redirect endpoint.',
)
def router_file_source(id: str, name: str) -> ResponseBase:
"""
Get file external link with 301 redirect endpoint.
Args:
id (str): The ID of the file.
name (str): The name of the file.
Returns:
ResponseBase: A model containing the response data for the file with a redirect.
"""
pass
@file_upload_router.get(
path='/download/{id}',
summary='下载文件',
description='Download file endpoint.',
)
def router_file_download(id: str) -> ResponseBase:
"""
Download file endpoint.
Args:
id (str): The ID of the file to download.
Returns:
ResponseBase: A model containing the response data for the file download.
"""
pass
@file_upload_router.get(
path='/archive/{sessionID}/archive.zip',
summary='打包并下载文件',
description='Archive and download files endpoint.',
)
def router_file_archive_download(sessionID: str) -> ResponseBase:
"""
Archive and download files endpoint.
Args:
sessionID (str): The session ID for the archive.
Returns:
ResponseBase: A model containing the response data for the archived files download.
"""
pass
@file_upload_router.post(
path='/{sessionID}/{index}',
summary='文件上传',
description='File upload endpoint.',
)
def router_file_upload(sessionID: str, index: int, file: UploadFile) -> ResponseBase:
"""
File upload endpoint.
Args:
sessionID (str): The session ID for the upload.
index (int): The index of the file being uploaded.
Returns:
ResponseBase: A model containing the response data.
"""
pass
# ==================== 上传会话管理 ====================
@file_upload_router.put(
path='/',
summary='创建上传会话',
description='Create an upload session endpoint.',
dependencies=[Depends(SignRequired)],
description='创建文件上传会话返回会话ID用于后续分片上传。',
)
def router_file_upload_session() -> ResponseBase:
async def create_upload_session(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: CreateUploadSessionRequest,
) -> UploadSessionResponse:
"""
Create an upload session endpoint.
Returns:
ResponseBase: A model containing the response data for the upload session.
创建上传会话端点
流程:
1. 验证父目录存在且属于当前用户
2. 确定存储策略(使用指定的或继承父目录的)
3. 验证文件大小限制
4. 创建上传会话并生成存储路径
5. 返回会话信息
:param session: 数据库会话
:param user: 当前登录用户
:param request: 创建请求
:return: 上传会话信息
"""
pass
# 验证文件名
if not request.file_name or '/' in request.file_name or '\\' in request.file_name:
raise HTTPException(status_code=400, detail="无效的文件名")
# 验证父目录
parent = await Object.get(session, Object.id == request.parent_id)
if not parent or parent.owner_id != user.id:
raise HTTPException(status_code=404, detail="父目录不存在")
if not parent.is_folder:
raise HTTPException(status_code=400, detail="父对象不是目录")
# 确定存储策略
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="存储策略不存在")
# 验证文件大小限制
if policy.max_size > 0 and request.file_size > policy.max_size:
raise HTTPException(
status_code=400,
detail=f"文件大小超过限制 ({policy.max_size} bytes)"
)
# 检查是否已存在同名文件
existing = await Object.get(
session,
(Object.owner_id == user.id) &
(Object.parent_id == parent.id) &
(Object.name == request.file_name)
)
if existing:
raise HTTPException(status_code=409, detail="同名文件已存在")
# 计算分片信息
options = await policy.awaitable_attrs.options
chunk_size = options.chunk_size if options else 52428800 # 默认 50MB
total_chunks = max(1, (request.file_size + chunk_size - 1) // chunk_size) if request.file_size > 0 else 1
# 生成存储路径
storage_path: str | None = None
if policy.type == PolicyType.LOCAL:
storage_service = LocalStorageService(policy)
dir_path, storage_name, full_path = await storage_service.generate_file_path(
user_id=user.id,
original_filename=request.file_name,
)
storage_path = full_path
else:
# S3 后续实现
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# 创建上传会话
upload_session = UploadSession(
file_name=request.file_name,
file_size=request.file_size,
chunk_size=chunk_size,
total_chunks=total_chunks,
storage_path=storage_path,
expires_at=datetime.now() + timedelta(hours=24), # 24小时过期
owner_id=user.id,
parent_id=request.parent_id,
policy_id=policy_id,
)
upload_session = await upload_session.save(session)
l.info(f"创建上传会话: {upload_session.id}, 文件: {request.file_name}, 大小: {request.file_size}")
return UploadSessionResponse(
id=upload_session.id,
file_name=upload_session.file_name,
file_size=upload_session.file_size,
chunk_size=upload_session.chunk_size,
total_chunks=upload_session.total_chunks,
uploaded_chunks=0,
expires_at=upload_session.expires_at,
)
@file_upload_router.post(
path='/{session_id}/{chunk_index}',
summary='上传文件分片',
description='上传指定分片分片索引从0开始。',
)
async def upload_chunk(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
session_id: UUID,
chunk_index: int,
file: UploadFile = File(...),
) -> UploadChunkResponse:
"""
上传文件分片端点
流程:
1. 验证上传会话
2. 写入分片数据
3. 更新会话进度
4. 如果所有分片上传完成,创建 Object 记录
:param session: 数据库会话
:param user: 当前登录用户
:param session_id: 上传会话UUID
:param chunk_index: 分片索引从0开始
:param file: 上传的文件分片
:return: 上传进度信息
"""
# 获取上传会话
upload_session = await UploadSession.get(session, UploadSession.id == session_id)
if not upload_session or upload_session.owner_id != user.id:
raise HTTPException(status_code=404, detail="上传会话不存在")
if upload_session.is_expired:
raise HTTPException(status_code=400, detail="上传会话已过期")
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
if chunk_index < 0 or chunk_index >= upload_session.total_chunks:
raise HTTPException(status_code=400, detail="无效的分片索引")
# 获取策略
policy = await Policy.get(session, Policy.id == upload_session.policy_id)
if not policy:
raise HTTPException(status_code=500, detail="存储策略不存在")
# 读取分片内容
content = await file.read()
# 写入分片
if policy.type == PolicyType.LOCAL:
if not upload_session.storage_path:
raise HTTPException(status_code=500, detail="存储路径丢失")
storage_service = LocalStorageService(policy)
offset = chunk_index * upload_session.chunk_size
await storage_service.write_file_chunk(
upload_session.storage_path,
content,
offset,
)
else:
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# 更新会话进度
upload_session.uploaded_chunks += 1
upload_session.uploaded_size += len(content)
upload_session = await upload_session.save(session)
# 检查是否完成
is_complete = upload_session.is_complete
file_object_id: UUID | None = None
if is_complete:
# 创建 PhysicalFile 记录
physical_file = PhysicalFile(
storage_path=upload_session.storage_path,
size=upload_session.uploaded_size,
policy_id=upload_session.policy_id,
reference_count=1,
)
physical_file = await physical_file.save(session)
# 创建 Object 记录
file_object = Object(
name=upload_session.file_name,
type=ObjectType.FILE,
size=upload_session.uploaded_size,
physical_file_id=physical_file.id,
upload_session_id=str(upload_session.id),
parent_id=upload_session.parent_id,
owner_id=user_id,
policy_id=upload_session.policy_id,
)
file_object = await file_object.save(session)
file_object_id = file_object.id
# 删除上传会话
await UploadSession.delete(session, upload_session)
l.info(f"文件上传完成: {file_object.name}, size={file_object.size}, id={file_object.id}")
return UploadChunkResponse(
uploaded_chunks=upload_session.uploaded_chunks if not is_complete else upload_session.total_chunks,
total_chunks=upload_session.total_chunks,
is_complete=is_complete,
object_id=file_object_id,
)
@file_upload_router.delete(
path='/{sessionID}',
path='/{session_id}',
summary='删除上传会话',
description='Delete an upload session endpoint.',
dependencies=[Depends(SignRequired)]
description='取消上传并删除会话及已上传的临时文件。',
)
def router_file_upload_session_delete(sessionID: str) -> ResponseBase:
async def delete_upload_session(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
session_id: UUID,
) -> ResponseBase:
"""
Delete an upload session endpoint.
Args:
sessionID (str): The session ID to delete.
Returns:
ResponseBase: A model containing the response data for the deletion.
删除上传会话端点
:param session: 数据库会话
:param user: 当前登录用户
:param session_id: 上传会话UUID
:return: 删除结果
"""
pass
upload_session = await UploadSession.get(session, UploadSession.id == session_id)
if not upload_session or upload_session.owner_id != user.id:
raise HTTPException(status_code=404, detail="上传会话不存在")
# 删除临时文件
policy = await Policy.get(session, Policy.id == upload_session.policy_id)
if policy and policy.type == PolicyType.LOCAL and upload_session.storage_path:
storage_service = LocalStorageService(policy)
await storage_service.delete_file(upload_session.storage_path)
# 删除会话记录
await UploadSession.delete(session, upload_session)
l.info(f"删除上传会话: {session_id}")
return ResponseBase(data={"deleted": True})
@file_upload_router.delete(
path='/',
summary='清除所有上传会话',
description='Clear all upload sessions endpoint.',
dependencies=[Depends(SignRequired)]
description='清除当前用户的所有上传会话。',
)
def router_file_upload_session_clear() -> ResponseBase:
async def clear_upload_sessions(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
) -> ResponseBase:
"""
Clear all upload sessions endpoint.
Returns:
ResponseBase: A model containing the response data for clearing all sessions.
"""
pass
清除所有上传会话端点
@file_router.put(
path='/update/{id}',
summary='更新文件',
description='Update file information endpoint.',
dependencies=[Depends(SignRequired)]
:param session: 数据库会话
:param user: 当前登录用户
:return: 清除结果
"""
# 获取所有会话
sessions = await UploadSession.get(
session,
UploadSession.owner_id == user.id,
fetch_mode="all"
)
deleted_count = 0
for upload_session in sessions:
# 删除临时文件
policy = await Policy.get(session, Policy.id == upload_session.policy_id)
if policy and policy.type == PolicyType.LOCAL and upload_session.storage_path:
storage_service = LocalStorageService(policy)
await storage_service.delete_file(upload_session.storage_path)
await UploadSession.delete(session, upload_session)
deleted_count += 1
l.info(f"清除用户 {user.id} 的所有上传会话,共 {deleted_count}")
return ResponseBase(data={"deleted": deleted_count})
# ==================== 文件下载 ====================
@file_upload_router.get(
path='/download/{file_id}',
summary='下载文件',
description='下载指定文件。',
)
def router_file_update(id: str) -> ResponseBase:
async def download_file(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
file_id: UUID,
) -> FileResponse:
"""
Update file information endpoint.
Args:
id (str): The ID of the file to update.
Returns:
ResponseBase: A model containing the response data for the file update.
下载文件端点
:param session: 数据库会话
:param user: 当前登录用户
:param file_id: 文件UUID
:return: 文件响应
"""
pass
file_obj = await Object.get(session, Object.id == file_id)
if not file_obj or file_obj.owner_id != user.id:
raise HTTPException(status_code=404, detail="文件不存在")
if not file_obj.is_file:
raise HTTPException(status_code=400, detail="对象不是文件")
if not file_obj.source_name:
raise HTTPException(status_code=500, detail="文件存储路径丢失")
# 获取策略
policy = await Policy.get(session, Policy.id == file_obj.policy_id)
if not policy:
raise HTTPException(status_code=500, detail="存储策略不存在")
if policy.type == PolicyType.LOCAL:
storage_service = LocalStorageService(policy)
if not await storage_service.file_exists(file_obj.source_name):
raise HTTPException(status_code=404, detail="物理文件不存在")
return FileResponse(
path=file_obj.source_name,
filename=file_obj.name,
media_type="application/octet-stream",
)
else:
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# ==================== 创建空白文件 ====================
@file_router.post(
path='/create',
summary='创建空白文件',
description='Create a blank file endpoint.',
dependencies=[Depends(SignRequired)]
description='在指定目录下创建空白文件。',
)
def router_file_create() -> ResponseBase:
async def create_empty_file(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: CreateFileRequest,
) -> ResponseBase:
"""
Create a blank file endpoint.
Returns:
ResponseBase: A model containing the response data for the file creation.
创建空白文件端点
:param session: 数据库会话
:param user: 当前登录用户
:param request: 创建请求
:return: 创建结果
"""
pass
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
# 验证文件名
if not request.name or '/' in request.name or '\\' in request.name:
raise HTTPException(status_code=400, detail="无效的文件名")
# 验证父目录
parent = await Object.get(session, Object.id == request.parent_id)
if not parent or parent.owner_id != user_id:
raise HTTPException(status_code=404, detail="父目录不存在")
if not parent.is_folder:
raise HTTPException(status_code=400, detail="父对象不是目录")
# 检查是否已存在同名文件
existing = await Object.get(
session,
(Object.owner_id == user_id) &
(Object.parent_id == parent.id) &
(Object.name == request.name)
)
if existing:
raise HTTPException(status_code=409, detail="同名文件已存在")
# 确定存储策略
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="存储策略不存在")
# 生成存储路径并创建空文件
storage_path: str | None = None
if policy.type == PolicyType.LOCAL:
storage_service = LocalStorageService(policy)
dir_path, storage_name, full_path = await storage_service.generate_file_path(
user_id=user_id,
original_filename=request.name,
)
await storage_service.create_empty_file(full_path)
storage_path = full_path
else:
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# 创建 PhysicalFile 记录
physical_file = PhysicalFile(
storage_path=storage_path,
size=0,
policy_id=policy_id,
reference_count=1,
)
physical_file = await physical_file.save(session)
# 创建 Object 记录
file_object = Object(
name=request.name,
type=ObjectType.FILE,
size=0,
physical_file_id=physical_file.id,
parent_id=request.parent_id,
owner_id=user_id,
policy_id=policy_id,
)
file_object = await file_object.save(session)
l.info(f"创建空白文件: {file_object.name}, id={file_object.id}")
return ResponseBase(data={
"id": str(file_object.id),
"name": file_object.name,
"size": file_object.size,
})
# ==================== 文件外链(保留原有端点结构) ====================
@file_router.get(
path='/get/{id}/{name}',
summary='文件外链(直接输出文件数据)',
description='通过外链直接获取文件内容。',
)
async def router_file_get(
session: SessionDep,
id: str,
name: str,
) -> FileResponse:
"""
文件外链端点(直接输出)
TODO: 实现签名验证和权限控制
"""
raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.get(
path='/source/{id}/{name}',
summary='文件外链(301跳转)',
description='通过外链获取文件重定向地址。',
)
async def router_file_source_redirect(id: str, name: str) -> ResponseBase:
"""
文件外链端点301跳转
TODO: 实现签名验证和重定向
"""
raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.put(
path='/download/{id}',
summary='创建文件下载会话',
description='Create a file download session endpoint.',
dependencies=[Depends(SignRequired)]
path='/update/{id}',
summary='更新文件',
description='更新文件内容。',
dependencies=[Depends(AuthRequired)]
)
def router_file_download(id: str) -> ResponseBase:
"""
Create a file download session endpoint.
Args:
id (str): The ID of the file to download.
Returns:
ResponseBase: A model containing the response data for the file download session.
"""
pass
async def router_file_update(id: str) -> ResponseBase:
"""更新文件内容"""
raise HTTPException(status_code=501, detail="更新文件功能暂未实现")
@file_router.get(
path='/preview/{id}',
summary='预览文件',
description='Preview file endpoint.',
dependencies=[Depends(SignRequired)]
description='获取文件预览。',
dependencies=[Depends(AuthRequired)]
)
def router_file_preview(id: str) -> ResponseBase:
"""
Preview file endpoint.
Args:
id (str): The ID of the file to preview.
Returns:
ResponseBase: A model containing the response data for the file preview.
"""
pass
async def router_file_preview(id: str) -> ResponseBase:
"""预览文件"""
raise HTTPException(status_code=501, detail="预览功能暂未实现")
@file_router.get(
path='/content/{id}',
summary='获取文本文件内容',
description='Get text file content endpoint.',
dependencies=[Depends(SignRequired)]
description='获取文本文件内容。',
dependencies=[Depends(AuthRequired)]
)
def router_file_content(id: str) -> ResponseBase:
"""
Get text file content endpoint.
Args:
id (str): The ID of the text file.
Returns:
ResponseBase: A model containing the response data for the text file content.
"""
pass
async def router_file_content(id: str) -> ResponseBase:
"""获取文本文件内容"""
raise HTTPException(status_code=501, detail="文本内容功能暂未实现")
@file_router.get(
path='/doc/{id}',
summary='获取Office文档预览地址',
description='Get Office document preview URL endpoint.',
dependencies=[Depends(SignRequired)]
description='获取Office文档在线预览地址。',
dependencies=[Depends(AuthRequired)]
)
def router_file_doc(id: str) -> ResponseBase:
"""
Get Office document preview URL endpoint.
Args:
id (str): The ID of the Office document.
Returns:
ResponseBase: A model containing the response data for the Office document preview URL.
"""
pass
async def router_file_doc(id: str) -> ResponseBase:
"""获取Office文档预览地址"""
raise HTTPException(status_code=501, detail="Office预览功能暂未实现")
@file_router.get(
path='/thumb/{id}',
summary='获取文件缩略图',
description='Get file thumbnail endpoint.',
dependencies=[Depends(SignRequired)]
description='获取文件缩略图。',
dependencies=[Depends(AuthRequired)]
)
def router_file_thumb(id: str) -> ResponseBase:
"""
Get file thumbnail endpoint.
Args:
id (str): The ID of the file to get the thumbnail for.
Returns:
ResponseBase: A model containing the response data for the file thumbnail.
"""
pass
async def router_file_thumb(id: str) -> ResponseBase:
"""获取文件缩略图"""
raise HTTPException(status_code=501, detail="缩略图功能暂未实现")
@file_router.post(
path='/source/{id}',
summary='取得文件外链',
description='Get file external link endpoint.',
dependencies=[Depends(SignRequired)]
description='获取文件的外链地址。',
dependencies=[Depends(AuthRequired)]
)
def router_file_source(id: str) -> ResponseBase:
"""
Get file external link endpoint.
Args:
id (str): The ID of the file to get the external link for.
Returns:
ResponseBase: A model containing the response data for the file external link.
"""
pass
async def router_file_source(id: str) -> ResponseBase:
"""获取文件外链"""
raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.post(
path='/archive',
summary='打包要下载的文件',
description='Archive files for download endpoint.',
dependencies=[Depends(SignRequired)]
description='将多个文件打包下载。',
dependencies=[Depends(AuthRequired)]
)
def router_file_archive(id: str) -> ResponseBase:
"""
Archive files for download endpoint.
Args:
id (str): The ID of the file to archive.
Returns:
ResponseBase: A model containing the response data for the archived files.
"""
pass
async def router_file_archive() -> ResponseBase:
"""打包文件"""
raise HTTPException(status_code=501, detail="打包功能暂未实现")
@file_router.post(
path='/compress',
summary='创建文件压缩任务',
description='Create file compression task endpoint.',
dependencies=[Depends(SignRequired)]
description='创建文件压缩任务。',
dependencies=[Depends(AuthRequired)]
)
def router_file_compress(id: str) -> ResponseBase:
"""
Create file compression task endpoint.
Args:
id (str): The ID of the file to compress.
Returns:
ResponseBase: A model containing the response data for the file compression task.
"""
pass
async def router_file_compress() -> ResponseBase:
"""创建压缩任务"""
raise HTTPException(status_code=501, detail="压缩功能暂未实现")
@file_router.post(
path='/decompress',
summary='创建文件解压任务',
description='Create file extraction task endpoint.',
dependencies=[Depends(SignRequired)]
description='创建文件解压任务。',
dependencies=[Depends(AuthRequired)]
)
def router_file_decompress(id: str) -> ResponseBase:
"""
Create file extraction task endpoint.
Args:
id (str): The ID of the file to decompress.
Returns:
ResponseBase: A model containing the response data for the file extraction task.
"""
pass
async def router_file_decompress() -> ResponseBase:
"""创建解压任务"""
raise HTTPException(status_code=501, detail="解压功能暂未实现")
@file_router.post(
path='/relocate',
summary='创建文件转移任务',
description='Create file relocation task endpoint.',
dependencies=[Depends(SignRequired)]
description='创建文件转移任务。',
dependencies=[Depends(AuthRequired)]
)
def router_file_relocate(id: str) -> ResponseBase:
"""
Create file relocation task endpoint.
Args:
id (str): The ID of the file to relocate.
Returns:
ResponseBase: A model containing the response data for the file relocation task.
"""
pass
async def router_file_relocate() -> ResponseBase:
"""创建转移任务"""
raise HTTPException(status_code=501, detail="转移功能暂未实现")
@file_router.get(
path='/search/{type}/{keyword}',
summary='搜索文件',
description='Search files by keyword endpoint.',
dependencies=[Depends(SignRequired)]
description='按关键字搜索文件。',
dependencies=[Depends(AuthRequired)]
)
def router_file_search(type: str, keyword: str) -> ResponseBase:
"""
Search files by keyword endpoint.
Args:
type (str): The type of search (e.g., 'name', 'content').
keyword (str): The keyword to search for.
Returns:
ResponseBase: A model containing the response data for the file search.
"""
pass
async def router_file_search(type: str, keyword: str) -> ResponseBase:
"""搜索文件"""
raise HTTPException(status_code=501, detail="搜索功能暂未实现")
@file_upload_router.get(
path='/archive/{sessionID}/archive.zip',
summary='打包并下载文件',
description='获取打包后的文件。',
)
async def router_file_archive_download(sessionID: str) -> ResponseBase:
"""打包下载"""
raise HTTPException(status_code=501, detail="打包下载功能暂未实现")
@file_router.put(
path='/download/{id}',
summary='创建文件下载会话',
description='创建文件下载会话。',
dependencies=[Depends(AuthRequired)]
)
async def router_file_download_session(id: str) -> ResponseBase:
"""创建下载会话"""
raise HTTPException(status_code=501, detail="下载会话功能暂未实现")

View File

@@ -1,11 +1,35 @@
"""
对象操作路由
提供文件和目录对象的管理功能:删除、移动、复制、重命名等。
路由前缀:/object
"""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from sqlmodel.ext.asyncio.session import AsyncSession
from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep
from models import Object, ObjectDeleteRequest, ObjectMoveRequest, User
from models.response import ResponseBase
from models import (
Object,
ObjectCopyRequest,
ObjectDeleteRequest,
ObjectMoveRequest,
ObjectPropertyDetailResponse,
ObjectPropertyResponse,
ObjectRenameRequest,
ObjectType,
PhysicalFile,
Policy,
PolicyType,
User,
)
from models import ResponseBase
from service.storage import LocalStorageService
object_router = APIRouter(
prefix="/object",
@@ -13,10 +37,137 @@ object_router = APIRouter(
)
async def _delete_object_recursive(
session: AsyncSession,
obj: Object,
user_id: UUID,
) -> int:
"""
递归删除对象(软删除)
对于文件:
- 减少 PhysicalFile 引用计数
- 只有引用计数为0时才移动物理文件到回收站
对于目录:
- 递归处理所有子对象
:param session: 数据库会话
:param obj: 要删除的对象
:param user_id: 用户UUID
:return: 删除的对象数量
"""
deleted_count = 0
if obj.is_folder:
# 递归删除子对象
children = await Object.get_children(session, user_id, obj.id)
for child in children:
deleted_count += await _delete_object_recursive(session, child, user_id)
# 如果是文件,处理物理文件引用
if obj.is_file and obj.physical_file_id:
physical_file = await PhysicalFile.get(session, PhysicalFile.id == obj.physical_file_id)
if physical_file:
# 减少引用计数
new_count = physical_file.decrement_reference()
if physical_file.can_be_deleted:
# 引用计数为0移动物理文件到回收站
policy = await Policy.get(session, Policy.id == physical_file.policy_id)
if policy and policy.type == PolicyType.LOCAL:
try:
storage_service = LocalStorageService(policy)
await storage_service.move_to_trash(
source_path=physical_file.storage_path,
user_id=user_id,
object_id=obj.id,
)
l.debug(f"物理文件已移动到回收站: {obj.name}")
except Exception as e:
l.warning(f"移动物理文件到回收站失败: {obj.name}, 错误: {e}")
# 删除 PhysicalFile 记录
await PhysicalFile.delete(session, physical_file)
l.debug(f"物理文件记录已删除: {physical_file.storage_path}")
else:
# 还有其他引用,只更新引用计数
await physical_file.save(session)
l.debug(f"物理文件仍有 {new_count} 个引用,不删除: {physical_file.storage_path}")
# 删除数据库记录
await Object.delete(session, obj)
deleted_count += 1
return deleted_count
async def _copy_object_recursive(
session: AsyncSession,
src: Object,
dst_parent_id: UUID,
user_id: UUID,
) -> tuple[int, list[UUID]]:
"""
递归复制对象
对于文件:
- 增加 PhysicalFile 引用计数
- 创建新的 Object 记录指向同一 PhysicalFile
对于目录:
- 创建新目录
- 递归复制所有子对象
:param session: 数据库会话
:param src: 源对象
:param dst_parent_id: 目标父目录UUID
:param user_id: 用户UUID
:return: (复制数量, 新对象UUID列表)
"""
copied_count = 0
new_ids: list[UUID] = []
# 创建新的 Object 记录
new_obj = Object(
name=src.name,
type=src.type,
size=src.size,
password=src.password,
parent_id=dst_parent_id,
owner_id=user_id,
policy_id=src.policy_id,
physical_file_id=src.physical_file_id,
)
# 如果是文件,增加物理文件引用计数
if src.is_file and src.physical_file_id:
physical_file = await PhysicalFile.get(session, PhysicalFile.id == src.physical_file_id)
if physical_file:
physical_file.increment_reference()
await physical_file.save(session)
new_obj = await new_obj.save(session)
copied_count += 1
new_ids.append(new_obj.id)
# 如果是目录,递归复制子对象
if src.is_folder:
children = await Object.get_children(session, user_id, src.id)
for child in children:
child_count, child_ids = await _copy_object_recursive(
session, child, new_obj.id, user_id
)
copied_count += child_count
new_ids.extend(child_ids)
return copied_count, new_ids
@object_router.delete(
path='/',
summary='删除对象',
description='删除一个或多个对象(文件或目录)',
description='删除一个或多个对象(文件或目录),文件会移动到用户回收站。',
)
async def router_object_delete(
session: SessionDep,
@@ -24,22 +175,39 @@ async def router_object_delete(
request: ObjectDeleteRequest,
) -> ResponseBase:
"""
删除对象端点
删除对象端点(软删除)
流程:
1. 验证对象存在且属于当前用户
2. 对于文件,减少物理文件引用计数
3. 如果引用计数为0移动物理文件到 .trash 目录
4. 对于目录,递归处理子对象
5. 从数据库中删除记录
:param session: 数据库会话
:param user: 当前登录用户
:param request: 删除请求包含待删除对象的UUID列表
:return: 删除结果
"""
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
deleted_count = 0
for obj_id in request.ids:
obj = await Object.get(session, Object.id == obj_id)
if obj and obj.owner_id == user.id:
# TODO: 递归删除子对象(如果是目录)
# TODO: 更新用户存储空间
await obj.delete(session)
deleted_count += 1
if not obj or obj.owner_id != user_id:
continue
# 不能删除根目录
if obj.parent_id is None:
l.warning(f"尝试删除根目录被阻止: {obj.name}")
continue
# 递归删除(包含引用计数逻辑)
count = await _delete_object_recursive(session, obj, user_id)
deleted_count += count
l.info(f"用户 {user_id} 删除了 {deleted_count} 个对象")
return ResponseBase(
data={
@@ -67,9 +235,12 @@ async def router_object_move(
:param request: 移动请求包含源对象UUID列表和目标目录UUID
:return: 移动结果
"""
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
# 验证目标目录
dst = await Object.get(session, Object.id == request.dst_id)
if not dst or dst.owner_id != user.id:
if not dst or dst.owner_id != user_id:
raise HTTPException(status_code=404, detail="目标目录不存在")
if not dst.is_folder:
@@ -79,17 +250,33 @@ async def router_object_move(
for src_id in request.src_ids:
src = await Object.get(session, Object.id == src_id)
if not src or src.owner_id != user.id:
if not src or src.owner_id != user_id:
continue
# 不能移动根目录
if src.parent_id is None:
continue
# 检查是否移动到自身或子目录(防止循环引用)
if src.id == dst.id:
continue
# 检查是否将目录移动到其子目录中(循环检测)
if src.is_folder:
current = dst
is_cycle = False
while current and current.parent_id:
if current.parent_id == src.id:
is_cycle = True
break
current = await Object.get(session, Object.id == current.parent_id)
if is_cycle:
continue
# 检查目标目录下是否存在同名对象
existing = await Object.get(
session,
(Object.owner_id == user.id) &
(Object.owner_id == user_id) &
(Object.parent_id == dst.id) &
(Object.name == src.name)
)
@@ -107,50 +294,279 @@ async def router_object_move(
}
)
@object_router.post(
path='/copy',
summary='复制对象',
description='Copy an object endpoint.',
dependencies=[Depends(AuthRequired)]
description='复制一个或多个对象到目标目录。文件复制仅增加物理文件引用计数,不复制物理文件。',
)
def router_object_copy() -> ResponseBase:
async def router_object_copy(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: ObjectCopyRequest,
) -> ResponseBase:
"""
Copy an object endpoint.
Returns:
ResponseBase: A model containing the response data for the object copy.
复制对象端点
流程:
1. 验证目标目录存在且属于当前用户
2. 对于每个源对象:
- 验证源对象存在且属于当前用户
- 检查目标目录下是否存在同名对象
- 如果是文件:增加 PhysicalFile 引用计数,创建新 Object
- 如果是目录:递归复制所有子对象
3. 返回复制结果
:param session: 数据库会话
:param user: 当前登录用户
:param request: 复制请求
:return: 复制结果
"""
pass
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
# 验证目标目录
dst = await Object.get(session, Object.id == request.dst_id)
if not dst or dst.owner_id != user_id:
raise HTTPException(status_code=404, detail="目标目录不存在")
if not dst.is_folder:
raise HTTPException(status_code=400, detail="目标不是有效文件夹")
copied_count = 0
new_ids: list[UUID] = []
for src_id in request.src_ids:
src = await Object.get(session, Object.id == src_id)
if not src or src.owner_id != user_id:
continue
# 不能复制根目录
if src.parent_id is None:
continue
# 不能复制到自身
if src.id == dst.id:
continue
# 不能将目录复制到其子目录中
if src.is_folder:
current = dst
is_cycle = False
while current and current.parent_id:
if current.parent_id == src.id:
is_cycle = True
break
current = await Object.get(session, Object.id == current.parent_id)
if is_cycle:
continue
# 检查目标目录下是否存在同名对象
existing = await Object.get(
session,
(Object.owner_id == user_id) &
(Object.parent_id == dst.id) &
(Object.name == src.name)
)
if existing:
continue # 跳过重名对象
# 递归复制
count, ids = await _copy_object_recursive(session, src, dst.id, user_id)
copied_count += count
new_ids.extend(ids)
l.info(f"用户 {user_id} 复制了 {copied_count} 个对象")
return ResponseBase(
data={
"copied": copied_count,
"total": len(request.src_ids),
"new_ids": new_ids,
}
)
@object_router.post(
path='/rename',
summary='重命名对象',
description='Rename an object endpoint.',
dependencies=[Depends(AuthRequired)]
description='重命名对象(文件或目录)。',
)
def router_object_rename() -> ResponseBase:
async def router_object_rename(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: ObjectRenameRequest,
) -> ResponseBase:
"""
Rename an object endpoint.
Returns:
ResponseBase: A model containing the response data for the object rename.
重命名对象端点
流程:
1. 验证对象存在且属于当前用户
2. 验证新名称格式(不含非法字符)
3. 检查同目录下是否存在同名对象
4. 更新 name 字段
5. 返回更新结果
:param session: 数据库会话
:param user: 当前登录用户
:param request: 重命名请求
:return: 重命名结果
"""
pass
# 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id
# 验证对象存在
obj = await Object.get(session, Object.id == request.id)
if not obj:
raise HTTPException(status_code=404, detail="对象不存在")
if obj.owner_id != user_id:
raise HTTPException(status_code=403, detail="无权操作此对象")
# 不能重命名根目录
if obj.parent_id is None:
raise HTTPException(status_code=400, detail="无法重命名根目录")
# 验证新名称格式
new_name = request.new_name.strip()
if not new_name:
raise HTTPException(status_code=400, detail="名称不能为空")
if '/' in new_name or '\\' in new_name:
raise HTTPException(status_code=400, detail="名称不能包含斜杠")
# 如果名称没有变化,直接返回成功
if obj.name == new_name:
return ResponseBase(data={"success": True})
# 检查同目录下是否存在同名对象
existing = await Object.get(
session,
(Object.owner_id == user_id) &
(Object.parent_id == obj.parent_id) &
(Object.name == new_name)
)
if existing:
raise HTTPException(status_code=409, detail="同名对象已存在")
# 更新名称
obj.name = new_name
await obj.save(session)
l.info(f"用户 {user_id} 将对象 {obj.id} 重命名为 {new_name}")
return ResponseBase(data={"success": True})
@object_router.get(
path='/property/{id}',
summary='获取对象属性',
description='Get object properties endpoint.',
dependencies=[Depends(AuthRequired)]
summary='获取对象基本属性',
description='获取对象的基本属性信息(名称、类型、大小、创建/修改时间等)。',
)
def router_object_property(id: str) -> ResponseBase:
async def router_object_property(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
id: UUID,
) -> ObjectPropertyResponse:
"""
Get object properties endpoint.
Args:
id (str): The ID of the object to retrieve properties for.
Returns:
ResponseBase: A model containing the response data for the object properties.
获取对象基本属性端点
:param session: 数据库会话
:param user: 当前登录用户
:param id: 对象UUID
:return: 对象基本属性
"""
pass
obj = await Object.get(session, Object.id == id)
if not obj:
raise HTTPException(status_code=404, detail="对象不存在")
if obj.owner_id != user.id:
raise HTTPException(status_code=403, detail="无权查看此对象")
return ObjectPropertyResponse(
id=obj.id,
name=obj.name,
type=obj.type,
size=obj.size,
created_at=obj.created_at,
updated_at=obj.updated_at,
parent_id=obj.parent_id,
)
@object_router.get(
path='/property/{id}/detail',
summary='获取对象详细属性',
description='获取对象的详细属性信息,包括元数据、分享统计、存储信息等。',
)
async def router_object_property_detail(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
id: UUID,
) -> ObjectPropertyDetailResponse:
"""
获取对象详细属性端点
:param session: 数据库会话
:param user: 当前登录用户
:param id: 对象UUID
:return: 对象详细属性
"""
obj = await Object.get(
session,
Object.id == id,
load=Object.file_metadata,
)
if not obj:
raise HTTPException(status_code=404, detail="对象不存在")
if obj.owner_id != user.id:
raise HTTPException(status_code=403, detail="无权查看此对象")
# 获取策略名称
policy = await Policy.get(session, Policy.id == obj.policy_id)
policy_name = policy.name if policy else None
# 获取分享统计
from models import Share
shares = await Share.get(
session,
Share.object_id == obj.id,
fetch_mode="all"
)
share_count = len(shares)
total_views = sum(s.views for s in shares)
total_downloads = sum(s.downloads for s in shares)
# 获取物理文件引用计数
reference_count = 1
if obj.physical_file_id:
physical_file = await PhysicalFile.get(session, PhysicalFile.id == obj.physical_file_id)
if physical_file:
reference_count = physical_file.reference_count
# 构建响应
response = ObjectPropertyDetailResponse(
id=obj.id,
name=obj.name,
type=obj.type,
size=obj.size,
created_at=obj.created_at,
updated_at=obj.updated_at,
parent_id=obj.parent_id,
policy_name=policy_name,
share_count=share_count,
total_views=total_views,
total_downloads=total_downloads,
reference_count=reference_count,
)
# 添加文件元数据
if obj.file_metadata:
response.mime_type = obj.file_metadata.mime_type
response.width = obj.file_metadata.width
response.height = obj.file_metadata.height
response.duration = obj.file_metadata.duration
response.checksum_md5 = obj.file_metadata.checksum_md5
return response

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
share_router = APIRouter(
prefix='/share',

View File

@@ -3,7 +3,7 @@ from sqlalchemy import and_
import json
from middleware.dependencies import SessionDep
from models.response import ResponseBase
from models import ResponseBase
from models.setting import Setting
site_router = APIRouter(

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
slave_router = APIRouter(
prefix="/slave",

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
tag_router = APIRouter(
prefix='/tag',

View File

@@ -96,7 +96,7 @@ async def router_user_session(
async def router_user_register(
session: SessionDep,
request: models.RegisterRequest,
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
用户注册端点
@@ -157,7 +157,7 @@ async def router_user_register(
policy_id=default_policy.id,
).save(session)
return models.response.ResponseBase(
return models.ResponseBase(
data={
"user_id": new_user_id,
"username": new_user_username,
@@ -172,7 +172,7 @@ async def router_user_register(
)
def router_user_email_code(
reason: Literal['register', 'reset'] = 'register',
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
Send a verification code email.
@@ -186,7 +186,7 @@ def router_user_email_code(
summary='初始化QQ登录',
description='Initialize QQ login for a user.',
)
def router_user_qq() -> models.response.ResponseBase:
def router_user_qq() -> models.ResponseBase:
"""
Initialize QQ login for a user.
@@ -200,7 +200,7 @@ def router_user_qq() -> models.response.ResponseBase:
summary='WebAuthn登录初始化',
description='Initialize WebAuthn login for a user.',
)
async def router_user_authn(username: str) -> models.response.ResponseBase:
async def router_user_authn(username: str) -> models.ResponseBase:
pass
@@ -209,7 +209,7 @@ async def router_user_authn(username: str) -> models.response.ResponseBase:
summary='WebAuthn登录',
description='Finish WebAuthn login for a user.',
)
def router_user_authn_finish(username: str) -> models.response.ResponseBase:
def router_user_authn_finish(username: str) -> models.ResponseBase:
"""
Finish WebAuthn login for a user.
@@ -226,7 +226,7 @@ def router_user_authn_finish(username: str) -> models.response.ResponseBase:
summary='获取用户主页展示用分享',
description='Get user profile for display.',
)
def router_user_profile(id: str) -> models.response.ResponseBase:
def router_user_profile(id: str) -> models.ResponseBase:
"""
Get user profile for display.
@@ -243,7 +243,7 @@ def router_user_profile(id: str) -> models.response.ResponseBase:
summary='获取用户头像',
description='Get user avatar by ID and size.',
)
def router_user_avatar(id: str, size: int = 128) -> models.response.ResponseBase:
def router_user_avatar(id: str, size: int = 128) -> models.ResponseBase:
"""
Get user avatar by ID and size.
@@ -265,17 +265,17 @@ def router_user_avatar(id: str, size: int = 128) -> models.response.ResponseBase
summary='获取用户信息',
description='Get user information.',
dependencies=[Depends(dependency=AuthRequired)],
response_model=models.response.ResponseBase,
response_model=models.ResponseBase,
)
async def router_user_me(
session: SessionDep,
user: Annotated[models.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
获取用户信息.
:return: response.ResponseBase containing user information.
:rtype: response.ResponseBase
:return: ResponseBase containing user information.
:rtype: ResponseBase
"""
# 加载 group 及其 options 关系
group = await models.Group.get(
@@ -302,7 +302,7 @@ async def router_user_me(
tags=[tag.name for tag in user_tags] if user_tags else [],
)
return models.response.ResponseBase(data=user_response.model_dump())
return models.ResponseBase(data=user_response.model_dump())
@user_router.get(
path='/storage',
@@ -313,7 +313,7 @@ async def router_user_me(
async def router_user_storage(
session: SessionDep,
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
获取用户存储空间信息。
@@ -330,7 +330,7 @@ async def router_user_storage(
used: int = user.storage
free: int = max(0, total - used)
return models.response.ResponseBase(
return models.ResponseBase(
data={
"used": used,
"free": free,
@@ -347,7 +347,7 @@ async def router_user_storage(
async def router_user_authn_start(
session: SessionDep,
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
Initialize WebAuthn login for a user.
@@ -378,7 +378,7 @@ async def router_user_authn_start(
user_display_name=user.nick or user.username,
)
return models.response.ResponseBase(data=options_to_json_dict(options))
return models.ResponseBase(data=options_to_json_dict(options))
@user_router.put(
path='/authn/finish',
@@ -386,7 +386,7 @@ async def router_user_authn_start(
description='Finish WebAuthn login for a user.',
dependencies=[Depends(AuthRequired)],
)
def router_user_authn_finish() -> models.response.ResponseBase:
def router_user_authn_finish() -> models.ResponseBase:
"""
Finish WebAuthn login for a user.
@@ -400,7 +400,7 @@ def router_user_authn_finish() -> models.response.ResponseBase:
summary='获取用户可选存储策略',
description='Get user selectable storage policies.',
)
def router_user_settings_policies() -> models.response.ResponseBase:
def router_user_settings_policies() -> models.ResponseBase:
"""
Get user selectable storage policies.
@@ -415,7 +415,7 @@ def router_user_settings_policies() -> models.response.ResponseBase:
description='Get user selectable nodes.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_nodes() -> models.response.ResponseBase:
def router_user_settings_nodes() -> models.ResponseBase:
"""
Get user selectable nodes.
@@ -430,7 +430,7 @@ def router_user_settings_nodes() -> models.response.ResponseBase:
description='Get user task queue.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_tasks() -> models.response.ResponseBase:
def router_user_settings_tasks() -> models.ResponseBase:
"""
Get user task queue.
@@ -445,14 +445,14 @@ def router_user_settings_tasks() -> models.response.ResponseBase:
description='Get current user settings.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings() -> models.response.ResponseBase:
def router_user_settings() -> models.ResponseBase:
"""
Get current user settings.
Returns:
dict: A dictionary containing the current user settings.
"""
return models.response.ResponseBase(data=models.UserSettingResponse().model_dump())
return models.ResponseBase(data=models.UserSettingResponse().model_dump())
@user_settings_router.post(
path='/avatar',
@@ -460,7 +460,7 @@ def router_user_settings() -> models.response.ResponseBase:
description='Upload user avatar from file.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_avatar() -> models.response.ResponseBase:
def router_user_settings_avatar() -> models.ResponseBase:
"""
Upload user avatar from file.
@@ -475,7 +475,7 @@ def router_user_settings_avatar() -> models.response.ResponseBase:
description='Set user avatar to Gravatar.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_avatar_gravatar() -> models.response.ResponseBase:
def router_user_settings_avatar_gravatar() -> models.ResponseBase:
"""
Set user avatar to Gravatar.
@@ -490,7 +490,7 @@ def router_user_settings_avatar_gravatar() -> models.response.ResponseBase:
description='Update user settings.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_patch(option: str) -> models.response.ResponseBase:
def router_user_settings_patch(option: str) -> models.ResponseBase:
"""
Update user settings.
@@ -510,7 +510,7 @@ def router_user_settings_patch(option: str) -> models.response.ResponseBase:
)
async def router_user_settings_2fa(
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
Get two-factor authentication initialization information.
@@ -518,7 +518,7 @@ async def router_user_settings_2fa(
dict: A dictionary containing two-factor authentication setup information.
"""
return models.response.ResponseBase(
return models.ResponseBase(
data=await Password.generate_totp(user.username)
)
@@ -533,7 +533,7 @@ async def router_user_settings_2fa_enable(
user: Annotated[models.user.User, Depends(AuthRequired)],
setup_token: str,
code: str,
) -> models.response.ResponseBase:
) -> models.ResponseBase:
"""
Enable two-factor authentication for the user.
@@ -559,6 +559,6 @@ async def router_user_settings_2fa_enable(
user.two_factor = secret
user = await user.save(session)
return models.response.ResponseBase(
return models.ResponseBase(
data={"message": "Two-factor authentication enabled successfully"}
)

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
vas_router = APIRouter(
prefix="/vas",

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, Request
from middleware.auth import SignRequired
from models.response import ResponseBase
from models import ResponseBase
# WebDAV 管理路由
webdav_router = APIRouter(