|
2 | 2 | Utility functions for dbbackup. |
3 | 3 | """ |
4 | 4 |
|
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +import copy |
5 | 8 | import gzip |
| 9 | +import json |
6 | 10 | import logging |
7 | 11 | import os |
8 | 12 | import re |
|
12 | 16 | from datetime import datetime |
13 | 17 | from functools import wraps |
14 | 18 | from getpass import getpass |
| 19 | +from importlib import import_module |
15 | 20 | from shutil import copyfileobj |
16 | 21 |
|
17 | 22 | from django.core.mail import EmailMultiAlternatives |
@@ -431,3 +436,93 @@ def filename_generate(extension, database_name="", servername=None, content_type |
431 | 436 | filename = REG_FILENAME_CLEAN.sub("-", filename) |
432 | 437 | filename = filename.removeprefix("-") |
433 | 438 | return filename |
| 439 | + |
| 440 | + |
| 441 | +def _get_function_from_path(func_or_path): |
| 442 | + """ |
| 443 | + Load a callable from a dotted path or return the callable itself. |
| 444 | +
|
| 445 | + :param func_or_path: Dotted path to callable or callable itself |
| 446 | + :type func_or_path: str | callable |
| 447 | +
|
| 448 | + :returns: Callable object |
| 449 | + :rtype: callable |
| 450 | + """ |
| 451 | + if callable(func_or_path): |
| 452 | + return func_or_path |
| 453 | + |
| 454 | + path = func_or_path |
| 455 | + module_path, func_name = path.rsplit(".", 1) |
| 456 | + try: |
| 457 | + module = import_module(module_path) |
| 458 | + except ImportError as e: |
| 459 | + msg = f"Could not import module '{module_path}': {e}" |
| 460 | + raise ImportError(msg) from e |
| 461 | + func = getattr(module, func_name) |
| 462 | + if not callable(func): |
| 463 | + msg = f"The object at '{path}' is not callable." |
| 464 | + raise TypeError(msg) |
| 465 | + return func |
| 466 | + |
| 467 | + |
| 468 | +def get_user_metadata(metadata: dict | None = None) -> dict: |
| 469 | + """ |
| 470 | + Get user generated metadata from the user's custom metadata setter. |
| 471 | +
|
| 472 | + :returns: Custom metadata dictionary |
| 473 | + :rtype: dict |
| 474 | + """ |
| 475 | + user_metadata = {} |
| 476 | + setter_setting = settings.BACKUP_METADATA_SETTER |
| 477 | + if setter_setting: |
| 478 | + setter_function = _get_function_from_path(setter_setting) |
| 479 | + try: |
| 480 | + # We pass a copy to avoid side effects |
| 481 | + user_metadata = setter_function(copy.deepcopy(metadata)) |
| 482 | + except Exception: |
| 483 | + logger = logging.getLogger("dbbackup") |
| 484 | + logger.exception("Error loading custom metadata: %s") |
| 485 | + |
| 486 | + if user_metadata is None: |
| 487 | + user_metadata = {} |
| 488 | + |
| 489 | + if not isinstance(user_metadata, dict): |
| 490 | + msg = "DBBACKUP_BACKUP_METADATA_SETTER must return a dictionary." |
| 491 | + raise ValueError(msg) |
| 492 | + |
| 493 | + # Validate that we can serialize the provided data |
| 494 | + try: |
| 495 | + json.dumps(user_metadata) |
| 496 | + except Exception as e: |
| 497 | + msg = f"Custom metadata is not JSON serializable: {e}" |
| 498 | + raise ValueError(msg) from e |
| 499 | + return user_metadata |
| 500 | + |
| 501 | + |
| 502 | +def validate_user_metadata(metadata) -> bool | None: |
| 503 | + """ |
| 504 | + Validate custom metadata using a callable defined in `settings.py`. |
| 505 | + Raise a CommandError to provide custom feedback if validation fails. |
| 506 | +
|
| 507 | + :param metadata: Metadata dictionary to validate |
| 508 | + :type metadata: dict |
| 509 | +
|
| 510 | + :returns: True if validation passes (or None), False otherwise |
| 511 | + :rtype: Optional[bool] |
| 512 | + """ |
| 513 | + validator_setting = settings.RESTORE_METADATA_VALIDATOR |
| 514 | + if validator_setting: |
| 515 | + validator_function = _get_function_from_path(validator_setting) |
| 516 | + try: |
| 517 | + # We pass a copy to avoid side effects |
| 518 | + user_metadata = validator_function(copy.deepcopy(metadata)) |
| 519 | + except Exception as e: |
| 520 | + msg = f"Error during custom metadata validation: {e}" |
| 521 | + raise ValueError(msg) from e |
| 522 | + if user_metadata is None: |
| 523 | + return None |
| 524 | + if not isinstance(user_metadata, bool): |
| 525 | + msg = "DBBACKUP_RESTORE_METADATA_VALIDATOR must return a boolean or None." |
| 526 | + raise TypeError(msg) |
| 527 | + return user_metadata |
| 528 | + return True |
0 commit comments