archive.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/env python3
  2. """
  3. ArchiveBox command line application.
  4. ./archive and ./bin/archivebox both point to this file,
  5. but you can also run it directly using `python3 archive.py`
  6. Usage & Documentation:
  7. https://github.com/pirate/ArchiveBox/Wiki
  8. """
  9. __package__ = 'legacy'
  10. import os
  11. import sys
  12. import shutil
  13. from typing import List, Optional
  14. from .schema import Link
  15. from .links import links_after_timestamp
  16. from .index import write_links_index, load_links_index
  17. from .archive_methods import archive_link
  18. from .config import (
  19. ONLY_NEW,
  20. VERSION,
  21. ANSI,
  22. REPO_DIR,
  23. PYTHON_DIR,
  24. LEGACY_DIR,
  25. TEMPLATES_DIR,
  26. OUTPUT_DIR,
  27. SOURCES_DIR,
  28. ARCHIVE_DIR,
  29. DATABASE_DIR,
  30. USE_CURL,
  31. USE_WGET,
  32. USE_CHROME,
  33. FETCH_GIT,
  34. FETCH_MEDIA,
  35. DJANGO_BINARY,
  36. CURL_BINARY,
  37. GIT_BINARY,
  38. WGET_BINARY,
  39. YOUTUBEDL_BINARY,
  40. CHROME_BINARY,
  41. DJANGO_VERSION,
  42. CURL_VERSION,
  43. GIT_VERSION,
  44. WGET_VERSION,
  45. YOUTUBEDL_VERSION,
  46. CHROME_VERSION,
  47. )
  48. from .util import (
  49. enforce_types,
  50. handle_stdin_import,
  51. handle_file_import,
  52. )
  53. from .logs import (
  54. log_archiving_started,
  55. log_archiving_paused,
  56. log_archiving_finished,
  57. )
  58. __AUTHOR__ = 'Nick Sweeting <[email protected]>'
  59. __VERSION__ = VERSION
  60. __DESCRIPTION__ = 'ArchiveBox: The self-hosted internet archive.'
  61. __DOCUMENTATION__ = 'https://github.com/pirate/ArchiveBox/wiki'
  62. def print_help():
  63. print('ArchiveBox: The self-hosted internet archive.\n')
  64. print("Documentation:")
  65. print(" https://github.com/pirate/ArchiveBox/wiki\n")
  66. print("UI Usage:")
  67. print(" Open output/index.html to view your archive.\n")
  68. print("CLI Usage:")
  69. print(" mkdir data; cd data/")
  70. print(" archivebox init\n")
  71. print(" echo 'https://example.com/some/page' | archivebox add")
  72. print(" archivebox add https://example.com/some/other/page")
  73. print(" archivebox add --depth=1 ~/Downloads/bookmarks_export.html")
  74. print(" archivebox add --depth=1 https://example.com/feed.rss")
  75. print(" archivebox update --resume=15109948213.123")
  76. def print_version():
  77. print('ArchiveBox v{}'.format(__VERSION__))
  78. print()
  79. print('[i] Folder locations:')
  80. print(' REPO_DIR: ', REPO_DIR)
  81. print(' PYTHON_DIR: ', PYTHON_DIR)
  82. print(' LEGACY_DIR: ', LEGACY_DIR)
  83. print(' TEMPLATES_DIR: ', TEMPLATES_DIR)
  84. print()
  85. print(' OUTPUT_DIR: ', OUTPUT_DIR)
  86. print(' SOURCES_DIR: ', SOURCES_DIR)
  87. print(' ARCHIVE_DIR: ', ARCHIVE_DIR)
  88. print(' DATABASE_DIR: ', DATABASE_DIR)
  89. print()
  90. print(
  91. '[√] Django:'.ljust(14),
  92. 'python3 {} --version\n'.format(DJANGO_BINARY),
  93. ' '*13, DJANGO_VERSION, '\n',
  94. )
  95. print(
  96. '[{}] CURL:'.format('√' if USE_CURL else 'X').ljust(14),
  97. '{} --version\n'.format(shutil.which(CURL_BINARY)),
  98. ' '*13, CURL_VERSION, '\n',
  99. )
  100. print(
  101. '[{}] GIT:'.format('√' if FETCH_GIT else 'X').ljust(14),
  102. '{} --version\n'.format(shutil.which(GIT_BINARY)),
  103. ' '*13, GIT_VERSION, '\n',
  104. )
  105. print(
  106. '[{}] WGET:'.format('√' if USE_WGET else 'X').ljust(14),
  107. '{} --version\n'.format(shutil.which(WGET_BINARY)),
  108. ' '*13, WGET_VERSION, '\n',
  109. )
  110. print(
  111. '[{}] YOUTUBEDL:'.format('√' if FETCH_MEDIA else 'X').ljust(14),
  112. '{} --version\n'.format(shutil.which(YOUTUBEDL_BINARY)),
  113. ' '*13, YOUTUBEDL_VERSION, '\n',
  114. )
  115. print(
  116. '[{}] CHROME:'.format('√' if USE_CHROME else 'X').ljust(14),
  117. '{} --version\n'.format(shutil.which(CHROME_BINARY)),
  118. ' '*13, CHROME_VERSION, '\n',
  119. )
  120. def main(args=None) -> None:
  121. if args is None:
  122. args = sys.argv
  123. if set(args).intersection(('-h', '--help', 'help')) or len(args) > 2:
  124. print_help()
  125. raise SystemExit(0)
  126. if set(args).intersection(('--version', 'version')):
  127. print_version()
  128. raise SystemExit(0)
  129. ### Handle CLI arguments
  130. # ./archive bookmarks.html
  131. # ./archive 1523422111.234
  132. import_path, resume = None, None
  133. if len(args) == 2:
  134. # if the argument is a string, it's a import_path file to import
  135. # if it's a number, it's a timestamp to resume archiving from
  136. if args[1].replace('.', '').isdigit():
  137. import_path, resume = None, args[1]
  138. else:
  139. import_path, resume = args[1], None
  140. ### Set up output folder
  141. if not os.path.exists(OUTPUT_DIR):
  142. print('{green}[+] Created a new archive directory: {}{reset}'.format(OUTPUT_DIR, **ANSI))
  143. os.makedirs(OUTPUT_DIR)
  144. os.makedirs(SOURCES_DIR)
  145. os.makedirs(ARCHIVE_DIR)
  146. os.makedirs(DATABASE_DIR)
  147. else:
  148. not_empty = len(set(os.listdir(OUTPUT_DIR)) - {'.DS_Store', '.venv', 'venv', 'virtualenv', '.virtualenv'})
  149. index_exists = os.path.exists(os.path.join(OUTPUT_DIR, 'index.json'))
  150. if not_empty and not index_exists:
  151. print(
  152. ("{red}[X] Could not find index.json in the OUTPUT_DIR: {reset}{}\n\n"
  153. " If you're trying to update an existing archive, you must set OUTPUT_DIR to or run archivebox from inside the archive folder you're trying to update.\n"
  154. " If you're trying to create a new archive, you must run archivebox inside a completely empty directory."
  155. "\n\n"
  156. " {lightred}Hint:{reset} To import a data folder created by an older version of ArchiveBox, \n"
  157. " just cd into the folder and run the archivebox command to pick up where you left off.\n\n"
  158. " (Always make sure your data folder is backed up first before updating ArchiveBox)"
  159. ).format(OUTPUT_DIR, **ANSI)
  160. )
  161. raise SystemExit(1)
  162. ### Handle ingesting urls piped in through stdin
  163. # (.e.g if user does cat example_urls.txt | ./archive)
  164. if not sys.stdin.isatty():
  165. stdin_raw_text = sys.stdin.read()
  166. if stdin_raw_text and import_path:
  167. print(
  168. '[X] You should pass either a path as an argument, '
  169. 'or pass a list of links via stdin, but not both.\n'
  170. )
  171. print_help()
  172. raise SystemExit(1)
  173. import_path = handle_stdin_import(stdin_raw_text)
  174. ### Handle ingesting url from a remote file/feed
  175. # (e.g. if an RSS feed URL is used as the import path)
  176. if import_path:
  177. import_path = handle_file_import(import_path)
  178. ### Run the main archive update process
  179. update_archive_data(import_path=import_path, resume=resume)
  180. @enforce_types
  181. def update_archive_data(import_path: Optional[str]=None, resume: Optional[float]=None) -> List[Link]:
  182. """The main ArchiveBox entrancepoint. Everything starts here."""
  183. # Step 1: Load list of links from the existing index
  184. # merge in and dedupe new links from import_path
  185. all_links, new_links = load_links_index(out_dir=OUTPUT_DIR, import_path=import_path)
  186. # Step 2: Write updated index with deduped old and new links back to disk
  187. write_links_index(links=list(all_links), out_dir=OUTPUT_DIR)
  188. # Step 3: Run the archive methods for each link
  189. links = new_links if ONLY_NEW else all_links
  190. log_archiving_started(len(links), resume)
  191. idx: int = 0
  192. link: Optional[Link] = None
  193. try:
  194. for idx, link in enumerate(links_after_timestamp(links, resume)):
  195. archive_link(link, link_dir=link.link_dir)
  196. except KeyboardInterrupt:
  197. log_archiving_paused(len(links), idx, link.timestamp if link else '0')
  198. raise SystemExit(0)
  199. except:
  200. print()
  201. raise
  202. log_archiving_finished(len(links))
  203. # Step 4: Re-write links index with updated titles, icons, and resources
  204. all_links, _ = load_links_index(out_dir=OUTPUT_DIR)
  205. write_links_index(links=list(all_links), out_dir=OUTPUT_DIR, finished=True)
  206. return all_links
  207. if __name__ == '__main__':
  208. main(sys.argv)