backfill_document_content.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  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. configured = os.environ.get("DMS_DATABASE_URL", "").strip()
  21. if not configured:
  22. raise RuntimeError("必须显式配置DMS_DATABASE_URL并指向dms_test")
  23. try:
  24. configured_database = make_url(configured).database
  25. except Exception as exc:
  26. raise RuntimeError("DMS_DATABASE_URL不是有效数据库连接配置") from exc
  27. if configured_database != "dms_test":
  28. raise RuntimeError("正文回填命令只允许连接dms_test")
  29. database_name = db.session.scalar(text("SELECT DATABASE()"))
  30. if database_name != "dms_test":
  31. raise RuntimeError("正文回填命令只允许连接dms_test")
  32. def backfill() -> dict[str, int]:
  33. _database_gate()
  34. result = {"processed": 0, "skipped": 0, "failed": 0}
  35. document_ids = db.session.scalars(
  36. select(Document.id).order_by(Document.id.asc())
  37. ).all()
  38. for document_id in document_ids:
  39. document = db.session.get(Document, document_id)
  40. assert document is not None
  41. extension = document.file_extension.lower().lstrip(".")
  42. if extension not in {"pdf", "docx"}:
  43. if document.content_extract_status != ContentExtractStatus.UNSUPPORTED:
  44. document.content_text = None
  45. document.content_extract_status = (
  46. ContentExtractStatus.UNSUPPORTED.value
  47. )
  48. document.content_extracted_at = None
  49. document.search_text = build_search_text(
  50. document.document_name,
  51. document.summary,
  52. document.tags,
  53. None,
  54. )
  55. db.session.commit()
  56. result["processed"] += 1
  57. else:
  58. result["skipped"] += 1
  59. continue
  60. if document.content_extract_status in {
  61. ContentExtractStatus.SUCCESS.value,
  62. ContentExtractStatus.EMPTY.value,
  63. } and document.content_extracted_at is not None:
  64. result["skipped"] += 1
  65. continue
  66. verified = None
  67. try:
  68. verified = _verify_file(document)
  69. extraction = extract_document_content(verified.path, extension)
  70. document.content_text = extraction.text
  71. document.content_extract_status = extraction.status
  72. document.content_extracted_at = extraction.extracted_at
  73. document.search_text = build_search_text(
  74. document.document_name,
  75. document.summary,
  76. document.tags,
  77. extraction.text,
  78. )
  79. db.session.commit()
  80. result["processed"] += 1
  81. if extraction.status == ContentExtractStatus.FAILED.value:
  82. result["failed"] += 1
  83. except Exception:
  84. db.session.rollback()
  85. document = db.session.get(Document, document_id)
  86. if document is not None:
  87. document.content_text = None
  88. document.content_extract_status = ContentExtractStatus.FAILED.value
  89. document.content_extracted_at = None
  90. document.search_text = build_search_text(
  91. document.document_name,
  92. document.summary,
  93. document.tags,
  94. None,
  95. )
  96. db.session.commit()
  97. result["processed"] += 1
  98. result["failed"] += 1
  99. finally:
  100. if verified is not None:
  101. verified.close()
  102. return result
  103. def main() -> None:
  104. app = Flask("dms-content-backfill")
  105. init_dms(app)
  106. with app.app_context():
  107. outcome = backfill()
  108. print(
  109. "正文回填完成:"
  110. f"处理{outcome['processed']}个,跳过{outcome['skipped']}个,"
  111. f"失败{outcome['failed']}个。"
  112. )
  113. if __name__ == "__main__":
  114. main()