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:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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="下载会话功能暂未实现")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user