189 files
This commit is contained in:
@@ -18,7 +18,7 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,6 +28,9 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from ..const import DOMAIN, MAX_DOCS_PER_OBJECT, SIGNAL_DOCUMENTS_UPDATED
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .document_text import DocumentTextIndex
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOC_STORE_VERSION = 1
|
||||
@@ -60,6 +63,10 @@ class DocumentStore:
|
||||
self.hass = hass
|
||||
self._store: Store[dict[str, Any]] = Store(hass, DOC_STORE_VERSION, DOC_STORE_KEY)
|
||||
self._data: dict[str, Any] = {"documents": {}, "blobs": {}}
|
||||
# The full-text index (#171) follows the blob lifecycle: told about
|
||||
# every new blob, told when the last reference goes. Optional so the
|
||||
# store stays usable on its own (tests, tooling).
|
||||
self.text_index: DocumentTextIndex | None = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Paths
|
||||
@@ -214,6 +221,7 @@ class DocumentStore:
|
||||
await self._async_save()
|
||||
if not wrote_new and not deduped:
|
||||
_LOGGER.debug("Adopted pre-existing blob %s into registry", digest[:12])
|
||||
self.notify_blob_added(digest)
|
||||
return {
|
||||
"id": doc_id,
|
||||
"deduped": deduped,
|
||||
@@ -221,6 +229,12 @@ class DocumentStore:
|
||||
**doc,
|
||||
}
|
||||
|
||||
def notify_blob_added(self, digest: str) -> None:
|
||||
"""A blob is (now) referenced — let the text index extract it in the
|
||||
background. Idempotent: an already-extracted blob is a no-op."""
|
||||
if self.text_index is not None:
|
||||
self.text_index.schedule(digest)
|
||||
|
||||
def _store_blob_sync(self, content: bytes) -> tuple[str, bool]:
|
||||
"""Hash content and write the blob if absent. Returns (digest, wrote_new)."""
|
||||
digest = hashlib.sha256(content).hexdigest()
|
||||
@@ -263,6 +277,7 @@ class DocumentStore:
|
||||
docs: list[dict[str, Any]],
|
||||
task_id_map: dict[str, str] | None = None,
|
||||
part_id_map: dict[str, str] | None = None,
|
||||
id_map: dict[str, str] | None = None,
|
||||
) -> int:
|
||||
"""Recreate document metadata for an imported object (P6).
|
||||
|
||||
@@ -272,9 +287,17 @@ class DocumentStore:
|
||||
absent and the hygiene scan flags the doc as dangling. ``task_ids`` /
|
||||
``part_ids`` are remapped through their old→new id maps so a doc's
|
||||
task and spare-part links survive the import; ids with no mapping are
|
||||
dropped. Returns the number created.
|
||||
dropped. ``id_map`` (if given) is filled with old→new document ids
|
||||
for every doc whose export carried an ``id`` — the importer uses it
|
||||
to re-point completion photos and part ``doc_id`` links. Returns the
|
||||
number created.
|
||||
"""
|
||||
|
||||
def _remember(meta: dict[str, Any], new_id: str) -> None:
|
||||
old_id = meta.get("id")
|
||||
if id_map is not None and isinstance(old_id, str) and old_id:
|
||||
id_map[old_id] = new_id
|
||||
|
||||
def _remap(meta: dict[str, Any]) -> list[str]:
|
||||
if not task_id_map:
|
||||
return []
|
||||
@@ -298,7 +321,9 @@ class DocumentStore:
|
||||
# frontend would later window.open (matches ws_documents_add_link).
|
||||
if not isinstance(url, str) or not url.lower().startswith(("http://", "https://")):
|
||||
continue
|
||||
self.documents[uuid4().hex] = {
|
||||
new_id = uuid4().hex
|
||||
_remember(meta, new_id)
|
||||
self.documents[new_id] = {
|
||||
"object_id": object_id,
|
||||
"kind": KIND_WEBLINK,
|
||||
"url": url,
|
||||
@@ -320,7 +345,9 @@ class DocumentStore:
|
||||
blob = {"size": size, "mime": mime, "refcount": 0}
|
||||
self.blobs[digest] = blob
|
||||
blob["refcount"] += 1
|
||||
self.documents[uuid4().hex] = {
|
||||
new_id = uuid4().hex
|
||||
_remember(meta, new_id)
|
||||
self.documents[new_id] = {
|
||||
"object_id": object_id,
|
||||
"kind": KIND_FILE,
|
||||
"hash": digest,
|
||||
@@ -464,15 +491,24 @@ class DocumentStore:
|
||||
if blob["refcount"] <= 0:
|
||||
size = int(blob.get("size", 0))
|
||||
self.blobs.pop(digest, None)
|
||||
if self.text_index is not None:
|
||||
await self.text_index.async_forget(digest)
|
||||
await self.hass.async_add_executor_job(self._delete_blob_sync, digest)
|
||||
return size
|
||||
return 0
|
||||
|
||||
def _delete_blob_sync(self, digest: str) -> None:
|
||||
"""Delete a blob file and its extracted-text sidecar (executor)."""
|
||||
from .document_text import text_sidecar_path
|
||||
|
||||
try:
|
||||
self.blob_path(digest).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
_LOGGER.warning("Could not delete blob %s", digest[:12])
|
||||
try:
|
||||
text_sidecar_path(self.hass, digest).unlink(missing_ok=True)
|
||||
except (OSError, ValueError):
|
||||
_LOGGER.debug("Could not delete text sidecar %s", digest[:12])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Storage summary (backs the sensor + the panel overview)
|
||||
|
||||
Reference in New Issue
Block a user