backfill_document_content.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """显式、幂等地回填既有PDF/DOCX正文及search_text。
  2. 仅允许显式配置并连接既有 ``dms_test``:
  3. ``python -m dms.backfill_document_content``。
  4. """
  5. from __future__ import annotations
  6. import os
  7. from flask import Flask
  8. from sqlalchemy import select, text
  9. from sqlalchemy.engine import make_url
  10. from dms import init_dms
  11. from dms.common.enums import ContentExtractStatus
  12. from dms.extensions import db
  13. from dms.models import Document
  14. from dms.services.document_content_service import (
  15. build_search_text,
  16. extract_document_content,
  17. )
  18. from dms.services.recycle_bin_service import _verify_file
  19. def _database_gate() -> None:
  20. """安全闸门:必须显式配置 DMS_DATABASE_URL,且只允许连接 dms / dms_test。"""
  21. configured = os.environ.get("DMS_DATABASE_URL", "").strip()
  22. if not configured:
  23. raise RuntimeError("必须显式配置DMS_DATABASE_URL")
  24. try:
  25. configured_database = make_url(configured).database
  26. except Exception as exc:
  27. raise RuntimeError("DMS_DATABASE_URL不是有效数据库连接配置") from exc
  28. allowed = {"dms", "dms_test"}
  29. if configured_database not in allowed:
  30. raise RuntimeError(
  31. f"正文回填命令只允许连接 {' 或 '.join(sorted(allowed))},"
  32. f"当前指向 {configured_database!r}"
  33. )
  34. database_name = db.session.scalar(text("SELECT DATABASE()"))
  35. if database_name not in allowed:
  36. raise RuntimeError(
  37. f"正文回填命令实际连接的库 {database_name!r} 不在允许列表内"
  38. )
  39. def backfill() -> dict[str, int]:
  40. _database_gate()
  41. result = {"processed": 0, "skipped": 0, "failed": 0}
  42. document_ids = db.session.scalars(
  43. select(Document.id).order_by(Document.id.asc())
  44. ).all()
  45. for document_id in document_ids:
  46. document = db.session.get(Document, document_id)
  47. assert document is not None
  48. extension = document.file_extension.lower().lstrip(".")
  49. if extension not in {"pdf", "docx"}:
  50. if document.content_extract_status != ContentExtractStatus.UNSUPPORTED:
  51. document.content_text = None
  52. document.content_extract_status = (
  53. ContentExtractStatus.UNSUPPORTED.value
  54. )
  55. document.content_extracted_at = None
  56. document.search_text = build_search_text(
  57. document.document_name,
  58. document.summary,
  59. document.tags,
  60. None,
  61. )
  62. db.session.commit()
  63. result["processed"] += 1
  64. else:
  65. result["skipped"] += 1
  66. continue
  67. if document.content_extract_status in {
  68. ContentExtractStatus.SUCCESS.value,
  69. ContentExtractStatus.EMPTY.value,
  70. } and document.content_extracted_at is not None:
  71. result["skipped"] += 1
  72. continue
  73. verified = None
  74. try:
  75. verified = _verify_file(document)
  76. extraction = extract_document_content(verified.path, extension)
  77. document.content_text = extraction.text
  78. document.content_extract_status = extraction.status
  79. document.content_extracted_at = extraction.extracted_at
  80. document.search_text = build_search_text(
  81. document.document_name,
  82. document.summary,
  83. document.tags,
  84. extraction.text,
  85. )
  86. db.session.commit()
  87. result["processed"] += 1
  88. if extraction.status == ContentExtractStatus.FAILED.value:
  89. result["failed"] += 1
  90. except Exception:
  91. db.session.rollback()
  92. document = db.session.get(Document, document_id)
  93. if document is not None:
  94. document.content_text = None
  95. document.content_extract_status = ContentExtractStatus.FAILED.value
  96. document.content_extracted_at = None
  97. document.search_text = build_search_text(
  98. document.document_name,
  99. document.summary,
  100. document.tags,
  101. None,
  102. )
  103. db.session.commit()
  104. result["processed"] += 1
  105. result["failed"] += 1
  106. finally:
  107. if verified is not None:
  108. verified.close()
  109. return result
  110. def main() -> None:
  111. app = Flask("dms-content-backfill")
  112. init_dms(app)
  113. with app.app_context():
  114. outcome = backfill()
  115. print(
  116. "正文回填完成:"
  117. f"处理{outcome['processed']}个,跳过{outcome['skipped']}个,"
  118. f"失败{outcome['failed']}个。"
  119. )
  120. if __name__ == "__main__":
  121. main()