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 archivebox.misc import logging
  9. from . import CONSTANTS
  10. from .common import SHELL_CONFIG
  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=True)
  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