archivebox_init.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #!/usr/bin/env python3
  2. __package__ = 'archivebox.cli'
  3. import os
  4. import sys
  5. from pathlib import Path
  6. from rich import print
  7. import rich_click as click
  8. from archivebox.misc.util import docstring, enforce_types
  9. @enforce_types
  10. def init(force: bool=False, quick: bool=False, install: bool=False, setup: bool=False) -> None:
  11. """Initialize a new ArchiveBox collection in the current directory"""
  12. install = install or setup
  13. from archivebox.config import CONSTANTS, VERSION, DATA_DIR
  14. from archivebox.config.common import SERVER_CONFIG
  15. from archivebox.config.collection import write_config_file
  16. from archivebox.index import load_main_index, write_main_index, fix_invalid_folder_locations, get_invalid_folders
  17. from archivebox.index.schema import Link
  18. from archivebox.index.json import parse_json_main_index, parse_json_links_details
  19. from archivebox.index.sql import apply_migrations
  20. # if os.access(out_dir / CONSTANTS.JSON_INDEX_FILENAME, os.F_OK):
  21. # print("[red]:warning: This folder contains a JSON index. It is deprecated, and will no longer be kept up to date automatically.[/red]", file=sys.stderr)
  22. # print("[red] You can run `archivebox list --json --with-headers > static_index.json` to manually generate it.[/red]", file=sys.stderr)
  23. is_empty = not len(set(os.listdir(DATA_DIR)) - CONSTANTS.ALLOWED_IN_DATA_DIR)
  24. existing_index = os.path.isfile(CONSTANTS.DATABASE_FILE)
  25. if is_empty and not existing_index:
  26. print(f'[turquoise4][+] Initializing a new ArchiveBox v{VERSION} collection...[/turquoise4]')
  27. print('[green]----------------------------------------------------------------------[/green]')
  28. elif existing_index:
  29. # TODO: properly detect and print the existing version in current index as well
  30. print(f'[green][*] Verifying and updating existing ArchiveBox collection to v{VERSION}...[/green]')
  31. print('[green]----------------------------------------------------------------------[/green]')
  32. else:
  33. if force:
  34. print('[red][!] This folder appears to already have files in it, but no index.sqlite3 is present.[/red]')
  35. print('[red] Because --force was passed, ArchiveBox will initialize anyway (which may overwrite existing files).[/red]')
  36. else:
  37. print(
  38. ("[red][X] This folder appears to already have files in it, but no index.sqlite3 present.[/red]\n\n"
  39. " You must run init in a completely empty directory, or an existing data folder.\n\n"
  40. " [violet]Hint:[/violet] To import an existing data folder make sure to cd into the folder first, \n"
  41. " then run and run 'archivebox init' to pick up where you left off.\n\n"
  42. " (Always make sure your data folder is backed up first before updating ArchiveBox)"
  43. )
  44. )
  45. raise SystemExit(2)
  46. if existing_index:
  47. print('\n[green][*] Verifying archive folder structure...[/green]')
  48. else:
  49. print('\n[green][+] Building archive folder structure...[/green]')
  50. print(f' + ./{CONSTANTS.ARCHIVE_DIR.relative_to(DATA_DIR)}, ./{CONSTANTS.SOURCES_DIR.relative_to(DATA_DIR)}, ./{CONSTANTS.LOGS_DIR.relative_to(DATA_DIR)}...')
  51. Path(CONSTANTS.SOURCES_DIR).mkdir(exist_ok=True)
  52. Path(CONSTANTS.ARCHIVE_DIR).mkdir(exist_ok=True)
  53. Path(CONSTANTS.LOGS_DIR).mkdir(exist_ok=True)
  54. print(f' + ./{CONSTANTS.CONFIG_FILE.relative_to(DATA_DIR)}...')
  55. # create the .archivebox_id file with a unique ID for this collection
  56. from archivebox.config.paths import _get_collection_id
  57. _get_collection_id(DATA_DIR, force_create=True)
  58. # create the ArchiveBox.conf file
  59. write_config_file({'SECRET_KEY': SERVER_CONFIG.SECRET_KEY})
  60. if os.access(CONSTANTS.DATABASE_FILE, os.F_OK):
  61. print('\n[green][*] Verifying main SQL index and running any migrations needed...[/green]')
  62. else:
  63. print('\n[green][+] Building main SQL index and running initial migrations...[/green]')
  64. from archivebox.config.django import setup_django
  65. setup_django()
  66. for migration_line in apply_migrations(DATA_DIR):
  67. sys.stdout.write(f' {migration_line}\n')
  68. assert os.path.isfile(CONSTANTS.DATABASE_FILE) and os.access(CONSTANTS.DATABASE_FILE, os.R_OK)
  69. print()
  70. print(f' √ ./{CONSTANTS.DATABASE_FILE.relative_to(DATA_DIR)}')
  71. # from django.contrib.auth.models import User
  72. # if SHELL_CONFIG.IS_TTY and not User.objects.filter(is_superuser=True).exclude(username='system').exists():
  73. # print('{green}[+] Creating admin user account...{reset}'.format(**SHELL_CONFIG.ANSI))
  74. # call_command("createsuperuser", interactive=True)
  75. print()
  76. print('[dodger_blue3][*] Checking links from indexes and archive folders (safe to Ctrl+C)...[/dodger_blue3]')
  77. from core.models import Snapshot
  78. all_links = Snapshot.objects.none()
  79. pending_links: dict[str, Link] = {}
  80. if existing_index:
  81. all_links = load_main_index(DATA_DIR, warn=False)
  82. print(f' √ Loaded {all_links.count()} links from existing main index.')
  83. if quick:
  84. print(' > Skipping full snapshot directory check (quick mode)')
  85. else:
  86. try:
  87. # Links in data folders that dont match their timestamp
  88. fixed, cant_fix = fix_invalid_folder_locations(DATA_DIR)
  89. if fixed:
  90. print(f' [yellow]√ Fixed {len(fixed)} data directory locations that didn\'t match their link timestamps.[/yellow]')
  91. if cant_fix:
  92. print(f' [red]! Could not fix {len(cant_fix)} data directory locations due to conflicts with existing folders.[/red]')
  93. # Links in JSON index but not in main index
  94. orphaned_json_links = {
  95. link.url: link
  96. for link in parse_json_main_index(DATA_DIR)
  97. if not all_links.filter(url=link.url).exists()
  98. }
  99. if orphaned_json_links:
  100. pending_links.update(orphaned_json_links)
  101. print(f' [yellow]√ Added {len(orphaned_json_links)} orphaned links from existing JSON index...[/yellow]')
  102. # Links in data dir indexes but not in main index
  103. orphaned_data_dir_links = {
  104. link.url: link
  105. for link in parse_json_links_details(DATA_DIR)
  106. if not all_links.filter(url=link.url).exists()
  107. }
  108. if orphaned_data_dir_links:
  109. pending_links.update(orphaned_data_dir_links)
  110. print(f' [yellow]√ Added {len(orphaned_data_dir_links)} orphaned links from existing archive directories.[/yellow]')
  111. # Links in invalid/duplicate data dirs
  112. invalid_folders = {
  113. folder: link
  114. for folder, link in get_invalid_folders(all_links, DATA_DIR).items()
  115. }
  116. if invalid_folders:
  117. print(f' [red]! Skipped adding {len(invalid_folders)} invalid link data directories.[/red]')
  118. print(' X ' + '\n X '.join(f'./{Path(folder).relative_to(DATA_DIR)} {link}' for folder, link in invalid_folders.items()))
  119. print()
  120. print(' [violet]Hint:[/violet] For more information about the link data directories that were skipped, run:')
  121. print(' archivebox status')
  122. print(' archivebox list --status=invalid')
  123. except (KeyboardInterrupt, SystemExit):
  124. print(file=sys.stderr)
  125. print('[yellow]:stop_sign: Stopped checking archive directories due to Ctrl-C/SIGTERM[/yellow]', file=sys.stderr)
  126. print(' Your archive data is safe, but you should re-run `archivebox init` to finish the process later.', file=sys.stderr)
  127. print(file=sys.stderr)
  128. print(' [violet]Hint:[/violet] In the future you can run a quick init without checking dirs like so:', file=sys.stderr)
  129. print(' archivebox init --quick', file=sys.stderr)
  130. raise SystemExit(1)
  131. write_main_index(list(pending_links.values()), DATA_DIR)
  132. print('\n[green]----------------------------------------------------------------------[/green]')
  133. from django.contrib.auth.models import User
  134. if (SERVER_CONFIG.ADMIN_USERNAME and SERVER_CONFIG.ADMIN_PASSWORD) and not User.objects.filter(username=SERVER_CONFIG.ADMIN_USERNAME).exists():
  135. print('[green][+] Found ADMIN_USERNAME and ADMIN_PASSWORD configuration options, creating new admin user.[/green]')
  136. User.objects.create_superuser(username=SERVER_CONFIG.ADMIN_USERNAME, password=SERVER_CONFIG.ADMIN_PASSWORD)
  137. if existing_index:
  138. print('[green][√] Done. Verified and updated the existing ArchiveBox collection.[/green]')
  139. else:
  140. print(f'[green][√] Done. A new ArchiveBox collection was initialized ({len(all_links) + len(pending_links)} links).[/green]')
  141. CONSTANTS.PERSONAS_DIR.mkdir(parents=True, exist_ok=True)
  142. CONSTANTS.DEFAULT_TMP_DIR.mkdir(parents=True, exist_ok=True)
  143. CONSTANTS.DEFAULT_LIB_DIR.mkdir(parents=True, exist_ok=True)
  144. from archivebox.config.common import STORAGE_CONFIG
  145. STORAGE_CONFIG.TMP_DIR.mkdir(parents=True, exist_ok=True)
  146. STORAGE_CONFIG.LIB_DIR.mkdir(parents=True, exist_ok=True)
  147. if install:
  148. from archivebox.cli.archivebox_install import install as install_method
  149. install_method()
  150. if Snapshot.objects.count() < 25: # hide the hints for experienced users
  151. print()
  152. print(' [violet]Hint:[/violet] To view your archive index, run:')
  153. print(' archivebox server # then visit [deep_sky_blue4][link=http://127.0.0.1:8000]http://127.0.0.1:8000[/link][/deep_sky_blue4]')
  154. print()
  155. print(' To add new links, you can run:')
  156. print(" archivebox add < ~/some/path/to/list_of_links.txt")
  157. print()
  158. print(' For more usage and examples, run:')
  159. print(' archivebox help')
  160. @click.command()
  161. @click.option('--force', '-f', is_flag=True, help='Ignore unrecognized files in current directory and initialize anyway')
  162. @click.option('--quick', '-q', is_flag=True, help='Run any updates or migrations without rechecking all snapshot dirs')
  163. @click.option('--install', '-s', is_flag=True, help='Automatically install dependencies and extras used for archiving')
  164. @click.option('--setup', '-s', is_flag=True, help='DEPRECATED: equivalent to --install')
  165. @docstring(init.__doc__)
  166. def main(**kwargs) -> None:
  167. init(**kwargs)
  168. if __name__ == '__main__':
  169. main()