Files
disknext/routers/api/v1/admin/policy/__init__.py
于小丘 209cb24ab4 feat: add models for physical files, policies, and user management
- Implement PhysicalFile model to manage physical file references and reference counting.
- Create Policy model with associated options and group links for storage policies.
- Introduce Redeem and Report models for handling redeem codes and reports.
- Add Settings model for site configuration and user settings management.
- Develop Share model for sharing objects with unique codes and associated metadata.
- Implement SourceLink model for managing download links associated with objects.
- Create StoragePack model for managing user storage packages.
- Add Tag model for user-defined tags with manual and automatic types.
- Implement Task model for managing background tasks with status tracking.
- Develop User model with comprehensive user management features including authentication.
- Introduce UserAuthn model for managing WebAuthn credentials.
- Create WebDAV model for managing WebDAV accounts associated with users.
2026-02-10 19:07:48 +08:00

349 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from sqlmodel import Field
from middleware.auth import admin_required
from middleware.dependencies import SessionDep, TableViewRequestDep
from sqlmodels import (
Policy, PolicyBase, PolicyType, PolicySummary, ResponseBase,
ListResponse, Object, )
from sqlmodels.base import SQLModelBase
from service.storage import DirectoryCreationError, LocalStorageService
admin_policy_router = APIRouter(
prefix='/policy',
tags=['admin', 'admin_policy']
)
class PolicyTestPathRequest(SQLModelBase):
"""测试本地路径请求 DTO"""
path: str = Field(max_length=512)
"""要测试的本地路径"""
class PolicyTestSlaveRequest(SQLModelBase):
"""测试从机通信请求 DTO"""
server: str = Field(max_length=255)
"""从机服务器地址"""
secret: str
"""从机通信密钥"""
class PolicyCreateRequest(PolicyBase):
"""创建存储策略请求 DTO继承 PolicyBase 中的所有字段"""
pass
@admin_policy_router.get(
path='/list',
summary='列出存储策略',
description='List all storage policies',
dependencies=[Depends(admin_required)]
)
async def router_policy_list(
session: SessionDep,
table_view: TableViewRequestDep,
) -> ListResponse[PolicySummary]:
"""
获取所有存储策略列表。
:param session: 数据库会话
:param table_view: 分页排序参数依赖
:return: 分页策略列表
"""
result = await Policy.get_with_count(session, table_view=table_view)
return ListResponse(
items=[PolicySummary.model_validate(p, from_attributes=True) for p in result.items],
count=result.count,
)
@admin_policy_router.post(
path='/test/path',
summary='测试本地路径可用性',
description='Test local path availability',
dependencies=[Depends(admin_required)]
)
async def router_policy_test_path(
request: PolicyTestPathRequest,
) -> ResponseBase:
"""
测试本地存储路径是否可用。
:param request: 测试请求
:return: 测试结果
"""
import aiofiles.os
from pathlib import Path
path = Path(request.path).resolve()
# 检查路径是否存在
is_exists = await aiofiles.os.path.exists(str(path))
# 检查是否可写
is_writable = False
if is_exists:
test_file = path / ".write_test"
try:
async with aiofiles.open(str(test_file), 'w') as f:
await f.write("test")
await aiofiles.os.remove(str(test_file))
is_writable = True
except Exception:
pass
return ResponseBase(data={
"path": str(path),
"exists": is_exists,
"writable": is_writable,
})
@admin_policy_router.post(
path='/test/slave',
summary='测试从机通信',
description='Test slave node communication',
dependencies=[Depends(admin_required)]
)
async def router_policy_test_slave(
request: PolicyTestSlaveRequest,
) -> ResponseBase:
"""
测试从机RPC通信。
:param request: 测试请求
:return: 测试结果
"""
import aiohttp
try:
async with aiohttp.ClientSession() as client:
async with client.get(
f"{request.server}/api/slave/ping",
headers={"Authorization": request.secret},
timeout=aiohttp.ClientTimeout(total=10)
) as resp:
if resp.status == 200:
return ResponseBase(data={"connected": True})
else:
return ResponseBase(
code=400,
msg=f"从机响应错误HTTP {resp.status}"
)
except Exception as e:
return ResponseBase(code=400, msg=f"连接失败: {str(e)}")
@admin_policy_router.post(
path='/',
summary='创建存储策略',
description='创建新的存储策略。对于本地存储策略,会自动创建物理目录。',
dependencies=[Depends(admin_required)]
)
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',
summary='创建跨域策略',
description='Create CORS policy for S3 storage',
dependencies=[Depends(admin_required)]
)
async def router_policy_add_cors() -> ResponseBase:
"""
创建CORS配置S3相关
此端点用于S3存储的跨域配置。
"""
# TODO: 实现S3 CORS配置
raise HTTPException(status_code=501, detail="S3 CORS配置暂未实现")
@admin_policy_router.post(
path='/scf',
summary='创建COS回调函数',
description='Create COS callback function',
dependencies=[Depends(admin_required)]
)
async def router_policy_add_scf() -> ResponseBase:
"""
创建COS回调函数。
此端点用于腾讯云COS的云函数回调配置。
"""
# TODO: 实现COS SCF配置
raise HTTPException(status_code=501, detail="COS回调函数配置暂未实现")
@admin_policy_router.get(
path='/{policy_id}/oauth',
summary='获取 OneDrive OAuth URL',
description='Get OneDrive OAuth URL',
dependencies=[Depends(admin_required)]
)
async def router_policy_onddrive_oauth(
session: SessionDep,
policy_id: UUID,
) -> ResponseBase:
"""
获取OneDrive OAuth授权URL。
:param session: 数据库会话
:param policy_id: 存储策略UUID
:return: OAuth URL
"""
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
# TODO: 实现OneDrive OAuth
raise HTTPException(status_code=501, detail="OneDrive OAuth暂未实现")
@admin_policy_router.get(
path='/{policy_id}',
summary='获取存储策略',
description='Get storage policy by ID',
dependencies=[Depends(admin_required)]
)
async def router_policy_get_policy(
session: SessionDep,
policy_id: UUID,
) -> ResponseBase:
"""
获取存储策略详情。
:param session: 数据库会话
:param policy_id: 存储策略UUID
:return: 策略详情
"""
policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
# 获取使用此策略的用户组
groups = await policy.awaitable_attrs.groups
# 统计使用此策略的对象数量
object_count = await Object.count(session, Object.policy_id == policy_id)
return ResponseBase(data={
"id": str(policy.id),
"name": policy.name,
"type": policy.type.value,
"server": policy.server,
"bucket_name": policy.bucket_name,
"is_private": policy.is_private,
"base_url": policy.base_url,
"max_size": policy.max_size,
"auto_rename": policy.auto_rename,
"dir_name_rule": policy.dir_name_rule,
"file_name_rule": policy.file_name_rule,
"is_origin_link_enable": policy.is_origin_link_enable,
"options": policy.options.model_dump() if policy.options else None,
"groups": [{"id": str(g.id), "name": g.name} for g in groups],
"object_count": object_count,
})
@admin_policy_router.delete(
path='/{policy_id}',
summary='删除存储策略',
description='Delete storage policy by ID',
dependencies=[Depends(admin_required)]
)
async def router_policy_delete_policy(
session: SessionDep,
policy_id: UUID,
) -> ResponseBase:
"""
删除存储策略。
注意: 如果有文件使用此策略,会拒绝删除。
:param session: 数据库会话
:param policy_id: 存储策略UUID
:return: 删除结果
"""
policy = await Policy.get(session, Policy.id == policy_id)
if not policy:
raise HTTPException(status_code=404, detail="存储策略不存在")
# 检查是否有文件使用此策略
file_count = await Object.count(session, Object.policy_id == policy_id)
if file_count > 0:
raise HTTPException(
status_code=400,
detail=f"无法删除,还有 {file_count} 个文件使用此策略"
)
policy_name = policy.name
await Policy.delete(session, policy)
l.info(f"管理员删除了存储策略: {policy_name}")
return ResponseBase(data={"deleted": True})