django.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. __package__ = 'archivebox.config'
  2. import os
  3. import sys
  4. from datetime import datetime, timezone
  5. from rich.progress import Progress
  6. from rich.console import Console
  7. import django
  8. from . import CONSTANTS
  9. from .common import SHELL_CONFIG
  10. from ..misc import logging
  11. if not SHELL_CONFIG.USE_COLOR:
  12. os.environ['NO_COLOR'] = '1'
  13. if not SHELL_CONFIG.SHOW_PROGRESS:
  14. os.environ['TERM'] = 'dumb'
  15. # recreate rich console obj based on new config values
  16. STDOUT = CONSOLE = Console()
  17. STDERR = Console(stderr=True)
  18. logging.CONSOLE = CONSOLE
  19. INITIAL_STARTUP_PROGRESS = None
  20. INITIAL_STARTUP_PROGRESS_TASK = 0
  21. def bump_startup_progress_bar(advance=1):
  22. global INITIAL_STARTUP_PROGRESS
  23. global INITIAL_STARTUP_PROGRESS_TASK
  24. if INITIAL_STARTUP_PROGRESS:
  25. INITIAL_STARTUP_PROGRESS.update(INITIAL_STARTUP_PROGRESS_TASK, advance=advance) # type: ignore
  26. def setup_django_minimal():
  27. # sys.path.append(str(CONSTANTS.PACKAGE_DIR))
  28. # os.environ.setdefault('ARCHIVEBOX_DATA_DIR', str(CONSTANTS.DATA_DIR))
  29. # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
  30. # django.setup()
  31. raise Exception('dont use this anymore')
  32. DJANGO_SET_UP = False
  33. def setup_django(check_db=False, in_memory_db=False) -> None:
  34. from rich.panel import Panel
  35. global INITIAL_STARTUP_PROGRESS
  36. global INITIAL_STARTUP_PROGRESS_TASK
  37. global DJANGO_SET_UP
  38. if DJANGO_SET_UP:
  39. # raise Exception('django is already set up!')
  40. # TODO: figure out why CLI entrypoints with init_pending are running this twice sometimes
  41. return
  42. with Progress(transient=True, expand=True, console=STDERR) as INITIAL_STARTUP_PROGRESS:
  43. INITIAL_STARTUP_PROGRESS_TASK = INITIAL_STARTUP_PROGRESS.add_task("[green]Loading modules...", total=25, visible=False)
  44. from archivebox.config.permissions import IS_ROOT, ARCHIVEBOX_USER, ARCHIVEBOX_GROUP, SudoPermission
  45. # if running as root, chown the data dir to the archivebox user to make sure it's accessible to the archivebox user
  46. if IS_ROOT and ARCHIVEBOX_USER != 0:
  47. with SudoPermission(uid=0):
  48. # running as root is a special case where it's ok to be a bit slower
  49. # make sure data dir is always owned by the correct user
  50. os.system(f'chown {ARCHIVEBOX_USER}:{ARCHIVEBOX_GROUP} "{CONSTANTS.DATA_DIR}" 2>/dev/null')
  51. os.system(f'chown {ARCHIVEBOX_USER}:{ARCHIVEBOX_GROUP} "{CONSTANTS.DATA_DIR}"/* 2>/dev/null')
  52. bump_startup_progress_bar()
  53. try:
  54. from django.core.management import call_command
  55. bump_startup_progress_bar()
  56. if in_memory_db:
  57. raise Exception('dont use this anymore')
  58. # some commands (e.g. oneshot) dont store a long-lived sqlite3 db file on disk.
  59. # in those cases we create a temporary in-memory db and run the migrations
  60. # immediately to get a usable in-memory-database at startup
  61. os.environ.setdefault("ARCHIVEBOX_DATABASE_NAME", ":memory:")
  62. django.setup()
  63. bump_startup_progress_bar()
  64. call_command("migrate", interactive=False, verbosity=0)
  65. else:
  66. # Otherwise use default sqlite3 file-based database and initialize django
  67. # without running migrations automatically (user runs them manually by calling init)
  68. try:
  69. django.setup()
  70. except Exception as e:
  71. bump_startup_progress_bar(advance=1000)
  72. is_using_meta_cmd = any(ignored_subcommand in sys.argv for ignored_subcommand in ('help', 'version', '--help', '--version'))
  73. if not is_using_meta_cmd:
  74. # show error message to user only if they're not running a meta command / just trying to get help
  75. STDERR.print()
  76. STDERR.print(Panel(
  77. f'\n[red]{e.__class__.__name__}[/red]: [yellow]{e}[/yellow]\nPlease check your config and [blue]DATA_DIR[/blue] permissions.\n',
  78. title='\n\n[red][X] Error while trying to load database![/red]',
  79. subtitle='[grey53]NO WRITES CAN BE PERFORMED[/grey53]',
  80. expand=False,
  81. style='bold red',
  82. ))
  83. STDERR.print()
  84. STDERR.print_exception(show_locals=False)
  85. return
  86. bump_startup_progress_bar()
  87. from django.conf import settings
  88. # log startup message to the error log
  89. with open(settings.ERROR_LOG, "a", encoding='utf-8') as f:
  90. command = ' '.join(sys.argv)
  91. ts = datetime.now(timezone.utc).strftime('%Y-%m-%d__%H:%M:%S')
  92. f.write(f"\n> {command}; TS={ts} VERSION={CONSTANTS.VERSION} IN_DOCKER={SHELL_CONFIG.IN_DOCKER} IS_TTY={SHELL_CONFIG.IS_TTY}\n")
  93. if check_db:
  94. # make sure the data dir is owned by a non-root user
  95. if CONSTANTS.DATA_DIR.stat().st_uid == 0:
  96. STDERR.print('[red][X] Error: ArchiveBox DATA_DIR cannot be owned by root![/red]')
  97. STDERR.print(f' {CONSTANTS.DATA_DIR}')
  98. STDERR.print()
  99. STDERR.print('[violet]Hint:[/violet] Are you running archivebox in the right folder? (and as a non-root user?)')
  100. STDERR.print(' cd path/to/your/archive/data')
  101. STDERR.print(' archivebox [command]')
  102. STDERR.print()
  103. raise SystemExit(9)
  104. # Create cache table in DB if needed
  105. try:
  106. from django.core.cache import cache
  107. cache.get('test', None)
  108. except django.db.utils.OperationalError:
  109. call_command("createcachetable", verbosity=0)
  110. bump_startup_progress_bar()
  111. # if archivebox gets imported multiple times, we have to close
  112. # the sqlite3 whenever we init from scratch to avoid multiple threads
  113. # sharing the same connection by accident
  114. from django.db import connections
  115. for conn in connections.all():
  116. conn.close_if_unusable_or_obsolete()
  117. sql_index_path = CONSTANTS.DATABASE_FILE
  118. assert os.access(sql_index_path, os.F_OK), (
  119. f'No database file {sql_index_path} found in: {CONSTANTS.DATA_DIR} (Are you in an ArchiveBox collection directory?)')
  120. bump_startup_progress_bar()
  121. # https://docs.pydantic.dev/logfire/integrations/django/ Logfire Debugging
  122. # if settings.DEBUG_LOGFIRE:
  123. # from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor
  124. # SQLite3Instrumentor().instrument()
  125. # import logfire
  126. # logfire.configure()
  127. # logfire.instrument_django(is_sql_commentor_enabled=True)
  128. # logfire.info(f'Started ArchiveBox v{CONSTANTS.VERSION}', argv=sys.argv)
  129. except KeyboardInterrupt:
  130. raise SystemExit(2)
  131. DJANGO_SET_UP = True
  132. INITIAL_STARTUP_PROGRESS = None
  133. INITIAL_STARTUP_PROGRESS_TASK = None