Files
disknext/service/storage/naming_rule.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

145 lines
4.1 KiB
Python

"""
命名规则解析器
将包含占位符的规则模板转换为实际的文件名/目录路径。
支持的占位符:
- {date}: 当前日期 YYYY-MM-DD
- {timestamp}: Unix 时间戳
- {year}: 年份 YYYY
- {month}: 月份 MM
- {day}: 日期 DD
- {hour}: 小时 HH
- {minute}: 分钟 MM
- {randomkey16}: 16位随机字符串
- {originname}: 原始文件名(不含扩展名)
- {ext}: 文件扩展名(不含点)
- {uid}: 用户UUID
- {uuid}: 新生成的UUID
"""
import re
import secrets
import string
from datetime import datetime
from uuid import UUID, uuid4
from sqlmodel_ext import SQLModelBase
class NamingContext(SQLModelBase):
"""
命名上下文
包含生成文件名/目录名所需的所有信息。
"""
user_id: UUID
"""用户UUID"""
original_filename: str
"""原始文件名(包含扩展名)"""
timestamp: datetime | None = None
"""时间戳,默认为当前时间"""
class NamingRuleParser:
"""
命名规则解析器
将包含占位符的规则模板转换为实际的文件名/目录路径。
使用示例::
context = NamingContext(
user_id=UUID("..."),
original_filename="document.pdf",
)
dir_path = NamingRuleParser.parse("{date}/{randomkey16}", context)
# -> "2025-12-23/a1b2c3d4e5f6g7h8"
file_name = NamingRuleParser.parse("{randomkey16}_{originname}.{ext}", context)
# -> "x9y8z7w6v5u4t3s2_document.pdf"
"""
# 支持的占位符正则
_PLACEHOLDER_PATTERN = re.compile(r'\{(\w+)\}')
# 随机字符集
_RANDOM_CHARS = string.ascii_lowercase + string.digits
@classmethod
def parse(cls, rule: str, context: NamingContext) -> str:
"""
解析命名规则,替换所有占位符
:param rule: 命名规则模板,如 "{date}/{randomkey16}"
:param context: 命名上下文
:return: 解析后的实际路径/文件名
"""
timestamp = context.timestamp or datetime.now()
# 解析原始文件名
origin_name, ext = cls._split_filename(context.original_filename)
# 占位符替换映射
replacements: dict[str, str] = {
'date': timestamp.strftime('%Y-%m-%d'),
'timestamp': str(int(timestamp.timestamp())),
'year': timestamp.strftime('%Y'),
'month': timestamp.strftime('%m'),
'day': timestamp.strftime('%d'),
'hour': timestamp.strftime('%H'),
'minute': timestamp.strftime('%M'),
'randomkey16': cls._generate_random_key(16),
'originname': origin_name,
'ext': ext,
'uid': str(context.user_id),
'uuid': str(uuid4()),
}
def replace_placeholder(match: re.Match[str]) -> str:
placeholder = match.group(1)
return replacements.get(placeholder, match.group(0))
return cls._PLACEHOLDER_PATTERN.sub(replace_placeholder, rule)
@classmethod
def _split_filename(cls, filename: str) -> tuple[str, str]:
"""
分离文件名和扩展名
:param filename: 完整文件名
:return: (文件名不含扩展名, 扩展名不含点)
"""
if '.' in filename:
parts = filename.rsplit('.', 1)
return parts[0], parts[1]
return filename, ''
@classmethod
def _generate_random_key(cls, length: int) -> str:
"""
生成随机字符串
:param length: 字符串长度
:return: 随机字符串
"""
return ''.join(secrets.choice(cls._RANDOM_CHARS) for _ in range(length))
@classmethod
def validate_rule(cls, rule: str) -> bool:
"""
验证命名规则是否有效
:param rule: 命名规则模板
:return: 是否有效
"""
valid_placeholders = {
'date', 'timestamp', 'year', 'month', 'day', 'hour', 'minute',
'randomkey16', 'originname', 'ext', 'uid', 'uuid',
}
placeholders = cls._PLACEHOLDER_PATTERN.findall(rule)
return all(p in valid_placeholders for p in placeholders)