Files
disknext/routers/api/v1/admin/file_app/__init__.py
于小丘 15b2efe52a
All checks were successful
Test / test (push) Successful in 2m34s
fix: 修复 update_group_access 中 app 变量未赋值的问题
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:31:02 +08:00

451 lines
13 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.
"""
管理员文件应用管理端点
提供文件查看器应用的 CRUD、扩展名管理、用户组权限管理和 WOPI Discovery。
"""
from uuid import UUID
import aiohttp
from fastapi import APIRouter, Depends, status
from loguru import logger as l
from sqlalchemy import select
from middleware.auth import admin_required
from middleware.dependencies import SessionDep, TableViewRequestDep
from service.wopi import parse_wopi_discovery_xml
from sqlmodels import (
FileApp,
FileAppCreateRequest,
FileAppExtension,
FileAppGroupLink,
FileAppListResponse,
FileAppResponse,
FileAppUpdateRequest,
ExtensionUpdateRequest,
GroupAccessUpdateRequest,
WopiDiscoveredExtension,
WopiDiscoveryResponse,
)
from sqlmodels.file_app import FileAppType
from utils import http_exceptions
admin_file_app_router = APIRouter(
prefix="/file-app",
tags=["admin", "file_app"],
dependencies=[Depends(admin_required)],
)
@admin_file_app_router.get(
path='/list',
summary='列出所有文件应用',
)
async def list_file_apps(
session: SessionDep,
table_view: TableViewRequestDep,
) -> FileAppListResponse:
"""
列出所有文件应用端点(分页)
认证:管理员权限
"""
result = await FileApp.get_with_count(
session,
table_view=table_view,
)
apps: list[FileAppResponse] = []
for app in result.items:
extensions = await FileAppExtension.get(
session,
FileAppExtension.app_id == app.id,
fetch_mode="all",
)
group_links_result = await session.exec(
select(FileAppGroupLink).where(FileAppGroupLink.app_id == app.id)
)
group_links: list[FileAppGroupLink] = list(group_links_result.all())
apps.append(FileAppResponse.from_app(app, extensions, group_links))
return FileAppListResponse(apps=apps, total=result.count)
@admin_file_app_router.post(
path='/',
summary='创建文件应用',
status_code=status.HTTP_201_CREATED,
)
async def create_file_app(
session: SessionDep,
request: FileAppCreateRequest,
) -> FileAppResponse:
"""
创建文件应用端点
认证:管理员权限
错误处理:
- 409: app_key 已存在
"""
# 检查 app_key 唯一
existing = await FileApp.get(session, FileApp.app_key == request.app_key)
if existing:
http_exceptions.raise_conflict(f"应用标识 '{request.app_key}' 已存在")
# 创建应用
app = FileApp(
name=request.name,
app_key=request.app_key,
type=request.type,
icon=request.icon,
description=request.description,
is_enabled=request.is_enabled,
is_restricted=request.is_restricted,
iframe_url_template=request.iframe_url_template,
wopi_discovery_url=request.wopi_discovery_url,
wopi_editor_url_template=request.wopi_editor_url_template,
)
app = await app.save(session)
app_id = app.id
# 创建扩展名关联
extensions: list[FileAppExtension] = []
for i, ext in enumerate(request.extensions):
normalized = ext.lower().strip().lstrip('.')
ext_record = FileAppExtension(
app_id=app_id,
extension=normalized,
priority=i,
)
ext_record = await ext_record.save(session)
extensions.append(ext_record)
# 创建用户组关联
group_links: list[FileAppGroupLink] = []
for group_id in request.allowed_group_ids:
link = FileAppGroupLink(app_id=app_id, group_id=group_id)
session.add(link)
group_links.append(link)
if group_links:
await session.commit()
await session.refresh(app)
l.info(f"创建文件应用: {app.name} ({app.app_key})")
return FileAppResponse.from_app(app, extensions, group_links)
@admin_file_app_router.get(
path='/{app_id}',
summary='获取文件应用详情',
)
async def get_file_app(
session: SessionDep,
app_id: UUID,
) -> FileAppResponse:
"""
获取文件应用详情端点
认证:管理员权限
错误处理:
- 404: 应用不存在
"""
app = await FileApp.get_exist_one(session, app_id)
extensions = await FileAppExtension.get(
session,
FileAppExtension.app_id == app.id,
fetch_mode="all",
)
group_links_result = await session.exec(
select(FileAppGroupLink).where(FileAppGroupLink.app_id == app.id)
)
group_links: list[FileAppGroupLink] = list(group_links_result.all())
return FileAppResponse.from_app(app, extensions, group_links)
@admin_file_app_router.patch(
path='/{app_id}',
summary='更新文件应用',
)
async def update_file_app(
session: SessionDep,
app_id: UUID,
request: FileAppUpdateRequest,
) -> FileAppResponse:
"""
更新文件应用端点
认证:管理员权限
错误处理:
- 404: 应用不存在
- 409: 新 app_key 已被其他应用使用
"""
app = await FileApp.get_exist_one(session, app_id)
# 检查 app_key 唯一性
if request.app_key is not None and request.app_key != app.app_key:
existing = await FileApp.get(session, FileApp.app_key == request.app_key)
if existing:
http_exceptions.raise_conflict(f"应用标识 '{request.app_key}' 已存在")
# 更新非 None 字段
update_data = request.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(app, key, value)
app = await app.save(session)
extensions = await FileAppExtension.get(
session,
FileAppExtension.app_id == app.id,
fetch_mode="all",
)
group_links_result = await session.exec(
select(FileAppGroupLink).where(FileAppGroupLink.app_id == app.id)
)
group_links: list[FileAppGroupLink] = list(group_links_result.all())
l.info(f"更新文件应用: {app.name} ({app.app_key})")
return FileAppResponse.from_app(app, extensions, group_links)
@admin_file_app_router.delete(
path='/{app_id}',
summary='删除文件应用',
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_file_app(
session: SessionDep,
app_id: UUID,
) -> None:
"""
删除文件应用端点(级联删除扩展名、用户偏好和用户组关联)
认证:管理员权限
错误处理:
- 404: 应用不存在
"""
app = await FileApp.get_exist_one(session, app_id)
app_name = app.app_key
await FileApp.delete(session, app)
l.info(f"删除文件应用: {app_name}")
@admin_file_app_router.put(
path='/{app_id}/extensions',
summary='全量替换扩展名列表',
)
async def update_extensions(
session: SessionDep,
app_id: UUID,
request: ExtensionUpdateRequest,
) -> FileAppResponse:
"""
全量替换扩展名列表端点
先删除旧的扩展名关联,再创建新的。
认证:管理员权限
错误处理:
- 404: 应用不存在
"""
app = await FileApp.get_exist_one(session, app_id)
# 保留旧扩展名的 wopi_action_urlDiscovery 填充的值)
old_extensions: list[FileAppExtension] = await FileAppExtension.get(
session,
FileAppExtension.app_id == app_id,
fetch_mode="all",
)
old_url_map: dict[str, str] = {
ext.extension: ext.wopi_action_url
for ext in old_extensions
if ext.wopi_action_url
}
for old_ext in old_extensions:
await FileAppExtension.delete(session, old_ext, commit=False)
await session.flush()
# 创建新的扩展名(保留已有的 wopi_action_url
new_extensions: list[FileAppExtension] = []
for i, ext in enumerate(request.extensions):
normalized = ext.lower().strip().lstrip('.')
ext_record = FileAppExtension(
app_id=app_id,
extension=normalized,
priority=i,
wopi_action_url=old_url_map.get(normalized),
)
session.add(ext_record)
new_extensions.append(ext_record)
await session.commit()
# refresh commit 后过期的对象
await session.refresh(app)
for ext_record in new_extensions:
await session.refresh(ext_record)
group_links_result = await session.exec(
select(FileAppGroupLink).where(FileAppGroupLink.app_id == app_id)
)
group_links: list[FileAppGroupLink] = list(group_links_result.all())
l.info(f"更新文件应用 {app.app_key} 的扩展名: {request.extensions}")
return FileAppResponse.from_app(app, new_extensions, group_links)
@admin_file_app_router.put(
path='/{app_id}/groups',
summary='全量替换允许的用户组',
)
async def update_group_access(
session: SessionDep,
app_id: UUID,
request: GroupAccessUpdateRequest,
) -> FileAppResponse:
"""
全量替换允许的用户组端点
先删除旧的关联,再创建新的。
认证:管理员权限
错误处理:
- 404: 应用不存在
"""
app = await FileApp.get_exist_one(session, app_id)
# 删除旧的用户组关联
old_links_result = await session.exec(
select(FileAppGroupLink).where(FileAppGroupLink.app_id == app_id)
)
old_links: list[FileAppGroupLink] = list(old_links_result.all())
for old_link in old_links:
await session.delete(old_link)
# 创建新的用户组关联
new_links: list[FileAppGroupLink] = []
for group_id in request.group_ids:
link = FileAppGroupLink(app_id=app_id, group_id=group_id)
session.add(link)
new_links.append(link)
await session.commit()
await session.refresh(app)
extensions = await FileAppExtension.get(
session,
FileAppExtension.app_id == app_id,
fetch_mode="all",
)
l.info(f"更新文件应用 {app.app_key} 的用户组权限: {request.group_ids}")
return FileAppResponse.from_app(app, extensions, new_links)
@admin_file_app_router.post(
path='/{app_id}/discover',
summary='执行 WOPI Discovery',
)
async def discover_wopi(
session: SessionDep,
app_id: UUID,
) -> WopiDiscoveryResponse:
"""
从 WOPI 服务端获取 Discovery XML 并自动配置扩展名和 URL 模板。
流程:
1. 验证 FileApp 存在且为 WOPI 类型
2. 使用 FileApp.wopi_discovery_url 获取 Discovery XML
3. 解析 XML提取扩展名和动作 URL
4. 全量替换 FileAppExtension 记录(带 wopi_action_url
认证:管理员权限
错误处理:
- 404: 应用不存在
- 400: 非 WOPI 类型 / discovery URL 未配置 / XML 解析失败
- 502: WOPI 服务端不可达或返回无效响应
"""
app = await FileApp.get_exist_one(session, app_id)
if app.type != FileAppType.WOPI:
http_exceptions.raise_bad_request("仅 WOPI 类型应用支持自动发现")
if not app.wopi_discovery_url:
http_exceptions.raise_bad_request("未配置 WOPI Discovery URL")
# commit 后对象会过期,先保存需要的值
discovery_url = app.wopi_discovery_url
app_key = app.app_key
# 获取 Discovery XML
try:
async with aiohttp.ClientSession() as client:
async with client.get(
discovery_url,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status != 200:
http_exceptions.raise_bad_gateway(
f"WOPI 服务端返回 HTTP {resp.status}"
)
xml_content = await resp.text()
except aiohttp.ClientError as e:
http_exceptions.raise_bad_gateway(f"无法连接 WOPI 服务端: {e}")
# 解析 XML
try:
action_urls, app_names = parse_wopi_discovery_xml(xml_content)
except ValueError as e:
http_exceptions.raise_bad_request(str(e))
if not action_urls:
return WopiDiscoveryResponse(app_names=app_names)
# 全量替换扩展名
old_extensions: list[FileAppExtension] = await FileAppExtension.get(
session,
FileAppExtension.app_id == app_id,
fetch_mode="all",
)
for old_ext in old_extensions:
await FileAppExtension.delete(session, old_ext, commit=False)
await session.flush()
new_extensions: list[FileAppExtension] = []
discovered: list[WopiDiscoveredExtension] = []
for i, (ext, action_url) in enumerate(sorted(action_urls.items())):
ext_record = FileAppExtension(
app_id=app_id,
extension=ext,
priority=i,
wopi_action_url=action_url,
)
session.add(ext_record)
new_extensions.append(ext_record)
discovered.append(WopiDiscoveredExtension(extension=ext, action_url=action_url))
await session.commit()
l.info(
f"WOPI Discovery 完成: 应用 {app_key}, "
f"发现 {len(discovered)} 个扩展名"
)
return WopiDiscoveryResponse(
discovered_extensions=discovered,
app_names=app_names,
applied_count=len(discovered),
)