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