File size: 6,728 Bytes
b4f2238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """Module for storage."""
from __future__ import annotations
import logging
from typing import Any, Final
from django.apps import apps as django_apps
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.db.utils import OperationalError, ProgrammingError
from django.utils.deconstruct import deconstructible
from apps.core.utils.path import Path
logger = logging.getLogger(__name__)
# 用户自定义模板目录名称
USER_CUSTOM_TEMPLATE_DIR = "0-用户自定义模板"
PRIVATE_DOCX_ROOT_SETTING: Final[str] = "DOCUMENTS_PRIVATE_DOCX_TEMPLATES_ROOT"
_PRIVATE_DOCX_ROOT_DEFAULT_SENTINEL: Final[str] = "__DOCX_PRIVATE_ROOT_NOT_SET__"
def get_public_docx_templates_root() -> Path:
"""获取仓库内公用 docx_templates 根目录。"""
base_path = Path(str(settings.BASE_DIR)).parent / "apps" / "documents" / "docx_templates"
return Path(str(base_path))
def get_configured_private_docx_templates_root() -> str:
"""获取私有模板根目录配置值(优先系统配置,其次环境变量)。"""
configured = str(getattr(settings, PRIVATE_DOCX_ROOT_SETTING, "") or "").strip()
if not django_apps.ready:
return configured
try:
from apps.core.interfaces import ServiceLocator
system_config_service = ServiceLocator.get_system_config_service()
runtime_config = str(
system_config_service.get_value(PRIVATE_DOCX_ROOT_SETTING, _PRIVATE_DOCX_ROOT_DEFAULT_SENTINEL) or ""
).strip()
if runtime_config != _PRIVATE_DOCX_ROOT_DEFAULT_SENTINEL:
return runtime_config
except (OperationalError, ProgrammingError):
logger.debug("系统配置表未就绪,回退环境变量配置", exc_info=True)
except Exception:
logger.warning("读取系统配置失败,回退环境变量配置", exc_info=True)
return configured
def get_private_docx_templates_root() -> Path | None:
"""获取可选私有 docx_templates 根目录(未配置则为 None)。"""
configured = get_configured_private_docx_templates_root()
if not configured:
return None
return Path(configured).expanduser()
def get_docx_templates_source() -> str:
"""返回当前活动模板来源:public/private。"""
return "private" if get_private_docx_templates_root() else "public"
def get_docx_templates_root() -> Path:
"""获取当前活动 docx_templates 根目录。"""
private_root = get_private_docx_templates_root()
if private_root is not None:
return private_root
return get_public_docx_templates_root()
def resolve_docx_template_path(file_path: str) -> Path:
"""解析模板路径(支持绝对路径和相对活动根目录路径)。"""
normalized = file_path.strip()
candidate = Path(normalized)
if candidate.is_absolute():
return candidate
root = get_docx_templates_root().resolve()
resolved = (root / candidate).resolve()
try:
resolved.relative_to(root)
except ValueError as exc:
raise ValueError("模板路径越界,必须位于当前 docx_templates 根目录内") from exc
return resolved
@deconstructible
class DocumentTemplateStorage(FileSystemStorage):
"""
文书模板智能存储类
统一管理所有文书模板文件到 docx_templates 目录
外部上传的文件统一放到 0-用户自定义模板 子目录
"""
def __init__(self) -> None:
# 使用 Path 处理路径,跨平台兼容
self.docx_templates_root = get_docx_templates_root()
self.user_custom_dir = self.docx_templates_root / USER_CUSTOM_TEMPLATE_DIR
super().__init__(location=str(self.docx_templates_root))
def save(self, name: str | None, content: Any, max_length: int | None = None) -> str:
"""
智能保存逻辑:
1. 检查文件名是否表示已在docx_templates目录中的文件(通过特殊前缀标记)
2. 如果是,直接返回相对路径(不复制)
3. 如果不是,保存到 0-用户自定义模板 目录
注意:通过 _EXISTING_: 前缀标记已存在于docx_templates中的文件
"""
# 确保目录存在
self.docx_templates_root.mkdir(parents=True, exist_ok=True)
self.user_custom_dir.mkdir(parents=True, exist_ok=True)
# 检查是否是已存在于docx_templates中的文件(通过特殊前缀标记)
if name and name.startswith("_EXISTING_:"):
# 提取实际的相对路径
relative_path = name[len("_EXISTING_:") :]
# 验证文件确实存在
full_path = self.docx_templates_root / relative_path
if full_path.exists():
return relative_path
# 文件不存在,继续正常保存流程
# 不在目录中,保存到用户自定义模板目录
if name and not name.lower().endswith(".docx"):
logger.warning("上传的文件 %s 不是docx格式", name)
# 清理文件名(移除可能的前缀)
clean_name = Path(name).name if name else "unnamed.docx"
# 将文件名添加到用户自定义模板目录
name = f"{USER_CUSTOM_TEMPLATE_DIR}/{clean_name}"
return super().save(name, content, max_length)
def url(self, name: str | None) -> str:
"""返回文件URL - 返回绝对路径供下载"""
# 由于是本地文件存储,不提供 HTTP URL
# 返回空字符串让 Django 不生成链接
return ""
def path(self, name: str) -> str:
"""返回文件的绝对路径"""
return str(self.docx_templates_root / name)
def exists(self, name: str) -> bool:
"""检查文件是否存在"""
return bool((self.docx_templates_root / name).exists())
def size(self, name: str) -> int:
"""返回文件大小"""
return int((self.docx_templates_root / name).stat().st_size)
# 创建存储实例
document_template_storage = DocumentTemplateStorage()
def list_docx_templates_files() -> list[tuple[str, str]]:
"""
列出当前活动 docx_templates 目录下所有的 docx 文件
Returns:
list of (relative_path, display_name) tuples
"""
root = get_docx_templates_root()
if not root.exists():
return []
files: list[tuple[str, str]] = []
for docx_file in root.rglob("*.docx"):
relative_path = docx_file.relative_to(root).as_posix()
# 跳过用户自定义模板目录中的文件(这些是上传的)
if relative_path.startswith(USER_CUSTOM_TEMPLATE_DIR):
continue
files.append((relative_path, relative_path))
files.sort(key=lambda x: x[0])
return files
|