Files
disknext/sqlmodels/uri.py
于小丘 ccadfe57cd
All checks were successful
Test / test (push) Successful in 1m45s
feat: migrate ORM base to sqlmodel-ext, add file viewers and WOPI integration
- 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>
2026-02-14 14:23:17 +08:00

259 lines
6.6 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 enum import StrEnum
from urllib.parse import urlparse, parse_qs, urlencode, quote, unquote
from sqlmodel_ext import SQLModelBase
class FileSystemNamespace(StrEnum):
"""文件系统命名空间"""
MY = "my"
"""用户个人空间"""
SHARE = "share"
"""分享空间"""
TRASH = "trash"
"""回收站"""
class DiskNextURI(SQLModelBase):
"""
DiskNext 文件 URI
URI 格式: disknext://[fs_id[:password]@]namespace[/path][?query]
fs_id 可省略:
- my/trash 命名空间省略时默认当前用户
- share 命名空间必须提供 fs_idShare.code
"""
fs_id: str | None = None
"""文件系统标识符,可省略"""
namespace: FileSystemNamespace
"""命名空间"""
path: str = "/"
"""路径"""
password: str | None = None
"""访问密码(用于有密码的分享)"""
query: dict[str, str] | None = None
"""查询参数"""
# === 属性 ===
@property
def path_parts(self) -> list[str]:
"""路径分割为列表(过滤空串)"""
return [p for p in self.path.split("/") if p]
@property
def is_root(self) -> bool:
"""是否指向根目录"""
return self.path.strip("/") == ""
# === 核心方法 ===
def id(self, default_id: str | None = None) -> str | None:
"""
获取 fs_id省略时返回 default_id
参考 Cloudreve URI.ID(defaultUid) 方法
:param default_id: 默认值(通常为当前用户 ID
:return: fs_id 或 default_id
"""
return self.fs_id if self.fs_id else default_id
# === 类方法 ===
@classmethod
def parse(cls, uri: str) -> "DiskNextURI":
"""
解析 URI 字符串
实现方式:替换 disknext:// 为 http:// 后用 urllib.parse.urlparse 解析
- hostname → namespace
- username → fs_id
- password → password
- path → path
- query → query dict
:param uri: URI 字符串,如 "disknext://my/docs/readme.md"
:return: DiskNextURI 实例
:raises ValueError: URI 格式无效
"""
if not uri.startswith("disknext://"):
raise ValueError(f"URI 必须以 disknext:// 开头: {uri}")
# 替换协议为 http:// 以利用 urllib.parse 解析
http_uri = "http://" + uri[len("disknext://"):]
parsed = urlparse(http_uri)
# 解析 namespace
hostname = parsed.hostname
if not hostname:
raise ValueError(f"URI 缺少命名空间: {uri}")
try:
namespace = FileSystemNamespace(hostname)
except ValueError:
raise ValueError(f"无效的命名空间 '{hostname}',有效值: {[e.value for e in FileSystemNamespace]}")
# 解析 fs_id 和 password
fs_id = unquote(parsed.username) if parsed.username else None
password = unquote(parsed.password) if parsed.password else None
# 解析 path
path = unquote(parsed.path) if parsed.path else "/"
if not path:
path = "/"
# 解析 query
query: dict[str, str] | None = None
if parsed.query:
raw_query = parse_qs(parsed.query, keep_blank_values=True)
query = {k: v[0] for k, v in raw_query.items()}
return cls(
fs_id=fs_id,
namespace=namespace,
path=path,
password=password,
query=query,
)
@classmethod
def build(
cls,
namespace: FileSystemNamespace,
path: str = "/",
fs_id: str | None = None,
password: str | None = None,
) -> "DiskNextURI":
"""
构建 URI 实例
:param namespace: 命名空间
:param path: 路径
:param fs_id: 文件系统标识符
:param password: 访问密码
:return: DiskNextURI 实例
"""
# 确保 path 以 / 开头
if not path.startswith("/"):
path = "/" + path
return cls(
fs_id=fs_id,
namespace=namespace,
path=path,
password=password,
)
# === 实例方法 ===
def to_string(self) -> str:
"""
序列化为 URI 字符串
:return: URI 字符串,如 "disknext://my/docs/readme.md"
"""
result = "disknext://"
# fs_id 和 password
if self.fs_id:
result += quote(self.fs_id, safe="")
if self.password:
result += ":" + quote(self.password, safe="")
result += "@"
# namespace
result += self.namespace.value
# path
result += self.path
# query
if self.query:
result += "?" + urlencode(self.query)
return result
def join(self, *elements: str) -> "DiskNextURI":
"""
拼接路径元素,返回新 URI
:param elements: 路径元素
:return: 新的 DiskNextURI 实例
"""
base = self.path.rstrip("/")
for element in elements:
element = element.strip("/")
if element:
base += "/" + element
if not base:
base = "/"
return DiskNextURI(
fs_id=self.fs_id,
namespace=self.namespace,
path=base,
password=self.password,
query=self.query,
)
def dir_uri(self) -> "DiskNextURI":
"""
返回父目录的 URI
:return: 父目录的 DiskNextURI 实例
"""
parts = self.path_parts
if not parts:
# 已经是根目录
return self.root()
parent_path = "/" + "/".join(parts[:-1])
if not parent_path.endswith("/"):
parent_path += "/"
return DiskNextURI(
fs_id=self.fs_id,
namespace=self.namespace,
path=parent_path,
password=self.password,
)
def root(self) -> "DiskNextURI":
"""
返回根目录的 URI保留 namespace 和 fs_id
:return: 根目录的 DiskNextURI 实例
"""
return DiskNextURI(
fs_id=self.fs_id,
namespace=self.namespace,
path="/",
password=self.password,
)
def name(self) -> str:
"""
返回路径的最后一段(文件名或目录名)
:return: 文件名或目录名,根目录返回空字符串
"""
parts = self.path_parts
return parts[-1] if parts else ""
def __str__(self) -> str:
return self.to_string()
def __repr__(self) -> str:
return f"DiskNextURI({self.to_string()!r})"