feat: migrate ORM base to sqlmodel-ext, add file viewers and WOPI integration
All checks were successful
Test / test (push) Successful in 1m45s
All checks were successful
Test / test (push) Successful in 1m45s
- Migrate SQLModel base classes, mixins, and database management to external sqlmodel-ext package; remove sqlmodels/base/, sqlmodels/mixin/, and sqlmodels/database.py - Add file viewer/editor system with WOPI protocol support for collaborative editing (OnlyOffice, Collabora) - Add enterprise edition license verification module (ee/) - Add Dockerfile multi-stage build with Cython compilation support - Add new dependencies: sqlmodel-ext, cryptography, whatthepatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
409
sqlmodels/file_app.py
Normal file
409
sqlmodels/file_app.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
文件查看器应用模块
|
||||
|
||||
提供文件预览应用选择器系统的数据模型和 DTO。
|
||||
类似 Android 的"使用什么应用打开"机制:
|
||||
- 管理员注册应用(内置/iframe/WOPI)
|
||||
- 用户按扩展名查询可用查看器
|
||||
- 用户可设置"始终使用"偏好
|
||||
- 支持用户组级别的访问控制
|
||||
|
||||
架构:
|
||||
FileApp (应用注册表)
|
||||
├── FileAppExtension (扩展名关联)
|
||||
├── FileAppGroupLink (用户组访问控制)
|
||||
└── UserFileAppDefault (用户默认偏好)
|
||||
"""
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||
|
||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
|
||||
|
||||
# ==================== 枚举 ====================
|
||||
|
||||
class FileAppType(StrEnum):
|
||||
"""文件应用类型"""
|
||||
|
||||
BUILTIN = "builtin"
|
||||
"""前端内置查看器(如 pdf.js, Monaco)"""
|
||||
|
||||
IFRAME = "iframe"
|
||||
"""iframe 内嵌第三方服务"""
|
||||
|
||||
WOPI = "wopi"
|
||||
"""WOPI 协议(OnlyOffice / Collabora)"""
|
||||
|
||||
|
||||
# ==================== Link 表 ====================
|
||||
|
||||
class FileAppGroupLink(SQLModelBase, table=True):
|
||||
"""应用-用户组访问控制关联表"""
|
||||
|
||||
app_id: UUID = Field(foreign_key="fileapp.id", primary_key=True, ondelete="CASCADE")
|
||||
"""关联的应用UUID"""
|
||||
|
||||
group_id: UUID = Field(foreign_key="group.id", primary_key=True, ondelete="CASCADE")
|
||||
"""关联的用户组UUID"""
|
||||
|
||||
|
||||
# ==================== DTO 模型 ====================
|
||||
|
||||
class FileAppSummary(SQLModelBase):
|
||||
"""查看器列表项 DTO,用于选择器弹窗"""
|
||||
|
||||
id: UUID
|
||||
"""应用UUID"""
|
||||
|
||||
name: str
|
||||
"""应用名称"""
|
||||
|
||||
app_key: str
|
||||
"""应用唯一标识"""
|
||||
|
||||
type: FileAppType
|
||||
"""应用类型"""
|
||||
|
||||
icon: str | None = None
|
||||
"""图标名称/URL"""
|
||||
|
||||
description: str | None = None
|
||||
"""应用描述"""
|
||||
|
||||
iframe_url_template: str | None = None
|
||||
"""iframe URL 模板"""
|
||||
|
||||
wopi_editor_url_template: str | None = None
|
||||
"""WOPI 编辑器 URL 模板"""
|
||||
|
||||
|
||||
class FileViewersResponse(SQLModelBase):
|
||||
"""查看器查询响应 DTO"""
|
||||
|
||||
viewers: list[FileAppSummary] = []
|
||||
"""可用查看器列表(已按 priority 排序)"""
|
||||
|
||||
default_viewer_id: UUID | None = None
|
||||
"""用户默认查看器UUID(如果已设置"始终使用")"""
|
||||
|
||||
|
||||
class SetDefaultViewerRequest(SQLModelBase):
|
||||
"""设置默认查看器请求 DTO"""
|
||||
|
||||
extension: str = Field(max_length=20)
|
||||
"""文件扩展名(小写,无点号)"""
|
||||
|
||||
app_id: UUID
|
||||
"""应用UUID"""
|
||||
|
||||
|
||||
class UserFileAppDefaultResponse(SQLModelBase):
|
||||
"""用户默认查看器响应 DTO"""
|
||||
|
||||
id: UUID
|
||||
"""记录UUID"""
|
||||
|
||||
extension: str
|
||||
"""扩展名"""
|
||||
|
||||
app: FileAppSummary
|
||||
"""关联的应用摘要"""
|
||||
|
||||
|
||||
class FileAppCreateRequest(SQLModelBase):
|
||||
"""管理员创建应用请求 DTO"""
|
||||
|
||||
name: str = Field(max_length=100)
|
||||
"""应用名称"""
|
||||
|
||||
app_key: str = Field(max_length=50)
|
||||
"""应用唯一标识"""
|
||||
|
||||
type: FileAppType
|
||||
"""应用类型"""
|
||||
|
||||
icon: str | None = Field(default=None, max_length=255)
|
||||
"""图标名称/URL"""
|
||||
|
||||
description: str | None = Field(default=None, max_length=500)
|
||||
"""应用描述"""
|
||||
|
||||
is_enabled: bool = True
|
||||
"""是否启用"""
|
||||
|
||||
is_restricted: bool = False
|
||||
"""是否限制用户组访问"""
|
||||
|
||||
iframe_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""iframe URL 模板"""
|
||||
|
||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||
"""WOPI 发现端点 URL"""
|
||||
|
||||
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""WOPI 编辑器 URL 模板"""
|
||||
|
||||
extensions: list[str] = []
|
||||
"""关联的扩展名列表"""
|
||||
|
||||
allowed_group_ids: list[UUID] = []
|
||||
"""允许访问的用户组UUID列表"""
|
||||
|
||||
|
||||
class FileAppUpdateRequest(SQLModelBase):
|
||||
"""管理员更新应用请求 DTO(所有字段可选)"""
|
||||
|
||||
name: str | None = Field(default=None, max_length=100)
|
||||
"""应用名称"""
|
||||
|
||||
app_key: str | None = Field(default=None, max_length=50)
|
||||
"""应用唯一标识"""
|
||||
|
||||
type: FileAppType | None = None
|
||||
"""应用类型"""
|
||||
|
||||
icon: str | None = Field(default=None, max_length=255)
|
||||
"""图标名称/URL"""
|
||||
|
||||
description: str | None = Field(default=None, max_length=500)
|
||||
"""应用描述"""
|
||||
|
||||
is_enabled: bool | None = None
|
||||
"""是否启用"""
|
||||
|
||||
is_restricted: bool | None = None
|
||||
"""是否限制用户组访问"""
|
||||
|
||||
iframe_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""iframe URL 模板"""
|
||||
|
||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||
"""WOPI 发现端点 URL"""
|
||||
|
||||
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""WOPI 编辑器 URL 模板"""
|
||||
|
||||
|
||||
class FileAppResponse(SQLModelBase):
|
||||
"""管理员应用详情响应 DTO"""
|
||||
|
||||
id: UUID
|
||||
"""应用UUID"""
|
||||
|
||||
name: str
|
||||
"""应用名称"""
|
||||
|
||||
app_key: str
|
||||
"""应用唯一标识"""
|
||||
|
||||
type: FileAppType
|
||||
"""应用类型"""
|
||||
|
||||
icon: str | None = None
|
||||
"""图标名称/URL"""
|
||||
|
||||
description: str | None = None
|
||||
"""应用描述"""
|
||||
|
||||
is_enabled: bool = True
|
||||
"""是否启用"""
|
||||
|
||||
is_restricted: bool = False
|
||||
"""是否限制用户组访问"""
|
||||
|
||||
iframe_url_template: str | None = None
|
||||
"""iframe URL 模板"""
|
||||
|
||||
wopi_discovery_url: str | None = None
|
||||
"""WOPI 发现端点 URL"""
|
||||
|
||||
wopi_editor_url_template: str | None = None
|
||||
"""WOPI 编辑器 URL 模板"""
|
||||
|
||||
extensions: list[str] = []
|
||||
"""关联的扩展名列表"""
|
||||
|
||||
allowed_group_ids: list[UUID] = []
|
||||
"""允许访问的用户组UUID列表"""
|
||||
|
||||
@classmethod
|
||||
def from_app(
|
||||
cls,
|
||||
app: "FileApp",
|
||||
extensions: list["FileAppExtension"],
|
||||
group_links: list[FileAppGroupLink],
|
||||
) -> "FileAppResponse":
|
||||
"""从 ORM 对象构建 DTO"""
|
||||
return cls(
|
||||
id=app.id,
|
||||
name=app.name,
|
||||
app_key=app.app_key,
|
||||
type=app.type,
|
||||
icon=app.icon,
|
||||
description=app.description,
|
||||
is_enabled=app.is_enabled,
|
||||
is_restricted=app.is_restricted,
|
||||
iframe_url_template=app.iframe_url_template,
|
||||
wopi_discovery_url=app.wopi_discovery_url,
|
||||
wopi_editor_url_template=app.wopi_editor_url_template,
|
||||
extensions=[ext.extension for ext in extensions],
|
||||
allowed_group_ids=[link.group_id for link in group_links],
|
||||
)
|
||||
|
||||
|
||||
class FileAppListResponse(SQLModelBase):
|
||||
"""管理员应用列表响应 DTO"""
|
||||
|
||||
apps: list[FileAppResponse] = []
|
||||
"""应用列表"""
|
||||
|
||||
total: int = 0
|
||||
"""总数"""
|
||||
|
||||
|
||||
class ExtensionUpdateRequest(SQLModelBase):
|
||||
"""扩展名全量替换请求 DTO"""
|
||||
|
||||
extensions: list[str]
|
||||
"""扩展名列表(小写,无点号)"""
|
||||
|
||||
|
||||
class GroupAccessUpdateRequest(SQLModelBase):
|
||||
"""用户组权限全量替换请求 DTO"""
|
||||
|
||||
group_ids: list[UUID]
|
||||
"""允许访问的用户组UUID列表"""
|
||||
|
||||
|
||||
class WopiSessionResponse(SQLModelBase):
|
||||
"""WOPI 会话响应 DTO"""
|
||||
|
||||
wopi_src: str
|
||||
"""WOPI 源 URL"""
|
||||
|
||||
access_token: str
|
||||
"""WOPI 访问令牌"""
|
||||
|
||||
access_token_ttl: int
|
||||
"""令牌过期时间戳(毫秒,WOPI 规范要求)"""
|
||||
|
||||
editor_url: str
|
||||
"""完整的编辑器 URL"""
|
||||
|
||||
|
||||
# ==================== 数据库模型 ====================
|
||||
|
||||
class FileApp(SQLModelBase, UUIDTableBaseMixin):
|
||||
"""文件查看器应用注册表"""
|
||||
|
||||
name: str = Field(max_length=100)
|
||||
"""应用名称"""
|
||||
|
||||
app_key: str = Field(max_length=50, unique=True, index=True)
|
||||
"""应用唯一标识,前端路由用"""
|
||||
|
||||
type: FileAppType
|
||||
"""应用类型"""
|
||||
|
||||
icon: str | None = Field(default=None, max_length=255)
|
||||
"""图标名称/URL"""
|
||||
|
||||
description: str | None = Field(default=None, max_length=500)
|
||||
"""应用描述"""
|
||||
|
||||
is_enabled: bool = True
|
||||
"""是否启用"""
|
||||
|
||||
is_restricted: bool = False
|
||||
"""是否限制用户组访问"""
|
||||
|
||||
iframe_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""iframe URL 模板,支持 {file_url} 占位符"""
|
||||
|
||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||
"""WOPI 客户端发现端点 URL"""
|
||||
|
||||
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
|
||||
"""WOPI 编辑器 URL 模板,支持 {wopi_src} {access_token} {access_token_ttl}"""
|
||||
|
||||
# 关系
|
||||
extensions: list["FileAppExtension"] = Relationship(
|
||||
back_populates="app",
|
||||
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
||||
)
|
||||
|
||||
user_defaults: list["UserFileAppDefault"] = Relationship(
|
||||
back_populates="app",
|
||||
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
||||
)
|
||||
|
||||
allowed_groups: list["Group"] = Relationship(
|
||||
link_model=FileAppGroupLink,
|
||||
)
|
||||
|
||||
def to_summary(self) -> FileAppSummary:
|
||||
"""转换为摘要 DTO"""
|
||||
return FileAppSummary(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
app_key=self.app_key,
|
||||
type=self.type,
|
||||
icon=self.icon,
|
||||
description=self.description,
|
||||
iframe_url_template=self.iframe_url_template,
|
||||
wopi_editor_url_template=self.wopi_editor_url_template,
|
||||
)
|
||||
|
||||
|
||||
class FileAppExtension(SQLModelBase, TableBaseMixin):
|
||||
"""扩展名关联表"""
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("app_id", "extension", name="uq_fileappextension_app_extension"),
|
||||
)
|
||||
|
||||
app_id: UUID = Field(foreign_key="fileapp.id", index=True, ondelete="CASCADE")
|
||||
"""关联的应用UUID"""
|
||||
|
||||
extension: str = Field(max_length=20, index=True)
|
||||
"""扩展名(小写,无点号)"""
|
||||
|
||||
priority: int = Field(default=0, ge=0)
|
||||
"""排序优先级(越小越优先)"""
|
||||
|
||||
# 关系
|
||||
app: FileApp = Relationship(back_populates="extensions")
|
||||
|
||||
|
||||
class UserFileAppDefault(SQLModelBase, UUIDTableBaseMixin):
|
||||
"""用户"始终使用"偏好"""
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "extension", name="uq_userfileappdefault_user_extension"),
|
||||
)
|
||||
|
||||
user_id: UUID = Field(foreign_key="user.id", index=True, ondelete="CASCADE")
|
||||
"""用户UUID"""
|
||||
|
||||
extension: str = Field(max_length=20)
|
||||
"""扩展名(小写,无点号)"""
|
||||
|
||||
app_id: UUID = Field(foreign_key="fileapp.id", index=True, ondelete="CASCADE")
|
||||
"""关联的应用UUID"""
|
||||
|
||||
# 关系
|
||||
app: FileApp = Relationship(back_populates="user_defaults")
|
||||
|
||||
def to_response(self) -> UserFileAppDefaultResponse:
|
||||
"""转换为响应 DTO(需预加载 app 关系)"""
|
||||
return UserFileAppDefaultResponse(
|
||||
id=self.id,
|
||||
extension=self.extension,
|
||||
app=self.app.to_summary(),
|
||||
)
|
||||
Reference in New Issue
Block a user