paperless-ngx/src/documents/sanity_checker.py

167 lines
6.1 KiB
Python
Raw Normal View History

2020-11-25 16:04:58 +01:00
import hashlib
import logging
import uuid
from collections import defaultdict
from pathlib import Path
from typing import Final
2020-11-25 16:04:58 +01:00
from celery import states
2020-11-25 16:04:58 +01:00
from django.conf import settings
from django.utils import timezone
from tqdm import tqdm
2020-11-25 16:04:58 +01:00
from documents.models import Document
from documents.models import PaperlessTask
2020-11-25 16:04:58 +01:00
class SanityCheckMessages:
def __init__(self):
self._messages: dict[int, list[dict]] = defaultdict(list)
self.has_error = False
self.has_warning = False
2020-11-25 16:04:58 +01:00
def error(self, doc_pk, message):
self._messages[doc_pk].append({"level": logging.ERROR, "message": message})
self.has_error = True
2020-11-25 16:04:58 +01:00
def warning(self, doc_pk, message):
self._messages[doc_pk].append({"level": logging.WARNING, "message": message})
self.has_warning = True
2020-11-25 16:04:58 +01:00
def info(self, doc_pk, message):
self._messages[doc_pk].append({"level": logging.INFO, "message": message})
2020-11-25 16:04:58 +01:00
def log_messages(self):
logger = logging.getLogger("paperless.sanity_checker")
2020-11-25 16:04:58 +01:00
if len(self._messages) == 0:
logger.info("Sanity checker detected no issues.")
else:
# Query once
all_docs = Document.global_objects.all()
for doc_pk in self._messages:
if doc_pk is not None:
doc = all_docs.get(pk=doc_pk)
logger.info(
f"Detected following issue(s) with document #{doc.pk},"
f" titled {doc.title}",
)
for msg in self._messages[doc_pk]:
logger.log(msg["level"], msg["message"])
def __len__(self):
return len(self._messages)
def __getitem__(self, item):
return self._messages[item]
2020-11-25 16:04:58 +01:00
class SanityCheckFailedException(Exception):
pass
2020-11-25 16:04:58 +01:00
def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
paperless_task = PaperlessTask.objects.create(
task_id=uuid.uuid4(),
type=PaperlessTask.TaskType.SCHEDULED_TASK
if scheduled
else PaperlessTask.TaskType.MANUAL_TASK,
task_name=PaperlessTask.TaskName.CHECK_SANITY,
status=states.STARTED,
date_created=timezone.now(),
date_started=timezone.now(),
)
messages = SanityCheckMessages()
2020-11-25 16:04:58 +01:00
present_files = {
x.resolve() for x in Path(settings.MEDIA_ROOT).glob("**/*") if not x.is_dir()
}
2020-11-25 16:04:58 +01:00
lockfile = Path(settings.MEDIA_LOCK).resolve()
if lockfile in present_files:
present_files.remove(lockfile)
for doc in tqdm(Document.global_objects.all(), disable=not progress):
2020-12-02 01:18:11 +01:00
# Check sanity of the thumbnail
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()
if not thumbnail_path.exists() or not thumbnail_path.is_file():
messages.error(doc.pk, "Thumbnail of document does not exist.")
2020-11-25 16:04:58 +01:00
else:
if thumbnail_path in present_files:
present_files.remove(thumbnail_path)
2020-11-25 16:04:58 +01:00
try:
_ = thumbnail_path.read_bytes()
2020-11-25 16:04:58 +01:00
except OSError as e:
messages.error(doc.pk, f"Cannot read thumbnail file of document: {e}")
2020-11-25 16:04:58 +01:00
2020-12-02 01:18:11 +01:00
# Check sanity of the original file
# TODO: extract method
source_path: Final[Path] = Path(doc.source_path).resolve()
if not source_path.exists() or not source_path.is_file():
messages.error(doc.pk, "Original of document does not exist.")
2020-11-25 16:04:58 +01:00
else:
if source_path in present_files:
present_files.remove(source_path)
2020-11-25 16:04:58 +01:00
try:
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
2020-11-25 16:04:58 +01:00
except OSError as e:
messages.error(doc.pk, f"Cannot read original file of document: {e}")
else:
if checksum != doc.checksum:
messages.error(
doc.pk,
"Checksum mismatch. "
f"Stored: {doc.checksum}, actual: {checksum}.",
)
2020-11-25 16:04:58 +01:00
2021-02-10 00:52:18 +01:00
# Check sanity of the archive file.
if doc.archive_checksum is not None and doc.archive_filename is None:
messages.error(
doc.pk,
"Document has an archive file checksum, but no archive filename.",
)
elif doc.archive_checksum is None and doc.archive_filename is not None:
messages.error(
doc.pk,
"Document has an archive file, but its checksum is missing.",
)
2021-02-10 00:52:18 +01:00
elif doc.has_archive_version:
archive_path: Final[Path] = Path(doc.archive_path).resolve()
if not archive_path.exists() or not archive_path.is_file():
messages.error(doc.pk, "Archived version of document does not exist.")
else:
if archive_path in present_files:
present_files.remove(archive_path)
2020-12-02 01:18:11 +01:00
try:
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest()
2020-12-02 01:18:11 +01:00
except OSError as e:
messages.error(
doc.pk,
f"Cannot read archive file of document : {e}",
)
2020-12-02 01:18:11 +01:00
else:
if checksum != doc.archive_checksum:
messages.error(
doc.pk,
"Checksum mismatch of archived document. "
f"Stored: {doc.archive_checksum}, "
f"actual: {checksum}.",
)
2020-12-02 01:18:11 +01:00
# other document checks
2020-11-25 16:04:58 +01:00
if not doc.content:
2022-06-01 08:08:03 -07:00
messages.info(doc.pk, "Document contains no OCR data")
2020-11-25 16:04:58 +01:00
for extra_file in present_files:
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
2020-11-25 16:04:58 +01:00
paperless_task.status = states.SUCCESS if not messages.has_error else states.FAILURE
# result is concatenated messages
paperless_task.result = f"{len(messages)} issues found."
if messages.has_error:
paperless_task.result += " Check logs for details."
paperless_task.date_done = timezone.now()
paperless_task.save(update_fields=["status", "result", "date_done"])
2020-11-25 16:04:58 +01:00
return messages