fc / apps /documents /storage.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
6.73 kB
"""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