From 16776b076adfd9c4da2d5904332e937adf07c056 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 Jan 2021 08:19:26 -0500 Subject: [PATCH] add v0.5.3 build --- archivebox-0.5.3/.pc/.quilt_patches | 1 + archivebox-0.5.3/.pc/.quilt_series | 1 + archivebox-0.5.3/.pc/.version | 1 + archivebox-0.5.3/.pc/applied-patches | 0 archivebox-0.5.3/MANIFEST.in | 4 + archivebox-0.5.3/PKG-INFO | 591 +++++++++ archivebox-0.5.3/README.md | 545 ++++++++ archivebox-0.5.3/archivebox.egg-info/PKG-INFO | 591 +++++++++ .../archivebox.egg-info/SOURCES.txt | 129 ++ .../archivebox.egg-info/dependency_links.txt | 1 + .../archivebox.egg-info/entry_points.txt | 3 + .../archivebox.egg-info/requires.txt | 26 + .../archivebox.egg-info/top_level.txt | 1 + archivebox-0.5.3/archivebox/.flake8 | 6 + archivebox-0.5.3/archivebox/LICENSE | 21 + archivebox-0.5.3/archivebox/README.md | 545 ++++++++ archivebox-0.5.3/archivebox/__init__.py | 1 + archivebox-0.5.3/archivebox/__main__.py | 11 + archivebox-0.5.3/archivebox/cli/__init__.py | 144 +++ .../archivebox/cli/archivebox_add.py | 113 ++ .../archivebox/cli/archivebox_config.py | 61 + .../archivebox/cli/archivebox_help.py | 32 + .../archivebox/cli/archivebox_init.py | 40 + .../archivebox/cli/archivebox_list.py | 139 ++ .../archivebox/cli/archivebox_manage.py | 24 + .../archivebox/cli/archivebox_oneshot.py | 70 + .../archivebox/cli/archivebox_remove.py | 79 ++ .../archivebox/cli/archivebox_schedule.py | 96 ++ .../archivebox/cli/archivebox_server.py | 59 + .../archivebox/cli/archivebox_shell.py | 34 + .../archivebox/cli/archivebox_status.py | 32 + .../archivebox/cli/archivebox_update.py | 132 ++ .../archivebox/cli/archivebox_version.py | 40 + archivebox-0.5.3/archivebox/cli/tests.py | 227 ++++ archivebox-0.5.3/archivebox/config.py | 1081 ++++++++++++++++ archivebox-0.5.3/archivebox/config_stubs.py | 113 ++ archivebox-0.5.3/archivebox/core/__init__.py | 1 + archivebox-0.5.3/archivebox/core/admin.py | 257 ++++ archivebox-0.5.3/archivebox/core/apps.py | 5 + archivebox-0.5.3/archivebox/core/forms.py | 67 + .../core/management/commands/archivebox.py | 18 + .../core/migrations/0001_initial.py | 27 + .../migrations/0002_auto_20200625_1521.py | 18 + .../migrations/0003_auto_20200630_1034.py | 38 + .../migrations/0004_auto_20200713_1552.py | 19 + .../migrations/0005_auto_20200728_0326.py | 28 + .../migrations/0006_auto_20201012_1520.py | 70 + .../core/migrations/0007_archiveresult.py | 97 ++ .../migrations/0008_auto_20210105_1421.py | 18 + .../archivebox/core/migrations/__init__.py | 0 archivebox-0.5.3/archivebox/core/mixins.py | 23 + archivebox-0.5.3/archivebox/core/models.py | 194 +++ archivebox-0.5.3/archivebox/core/settings.py | 165 +++ .../archivebox/core/templatetags/__init__.py | 0 .../archivebox/core/templatetags/core_tags.py | 47 + archivebox-0.5.3/archivebox/core/tests.py | 3 + archivebox-0.5.3/archivebox/core/urls.py | 36 + archivebox-0.5.3/archivebox/core/views.py | 173 +++ .../archivebox/core/welcome_message.py | 5 + archivebox-0.5.3/archivebox/core/wsgi.py | 16 + .../archivebox/extractors/__init__.py | 182 +++ .../archivebox/extractors/archive_org.py | 112 ++ archivebox-0.5.3/archivebox/extractors/dom.py | 69 + .../archivebox/extractors/favicon.py | 64 + archivebox-0.5.3/archivebox/extractors/git.py | 90 ++ .../archivebox/extractors/headers.py | 69 + .../archivebox/extractors/media.py | 81 ++ .../archivebox/extractors/mercury.py | 104 ++ archivebox-0.5.3/archivebox/extractors/pdf.py | 68 + .../archivebox/extractors/readability.py | 124 ++ .../archivebox/extractors/screenshot.py | 67 + .../archivebox/extractors/singlefile.py | 90 ++ .../archivebox/extractors/title.py | 130 ++ .../archivebox/extractors/wget.py | 184 +++ archivebox-0.5.3/archivebox/index/__init__.py | 617 +++++++++ archivebox-0.5.3/archivebox/index/csv.py | 37 + archivebox-0.5.3/archivebox/index/html.py | 164 +++ archivebox-0.5.3/archivebox/index/json.py | 154 +++ archivebox-0.5.3/archivebox/index/schema.py | 448 +++++++ archivebox-0.5.3/archivebox/index/sql.py | 106 ++ archivebox-0.5.3/archivebox/logging_util.py | 569 +++++++++ archivebox-0.5.3/archivebox/main.py | 1131 +++++++++++++++++ archivebox-0.5.3/archivebox/manage.py | 29 + archivebox-0.5.3/archivebox/mypy.ini | 3 + archivebox-0.5.3/archivebox/package.json | 21 + .../archivebox/parsers/__init__.py | 203 +++ .../archivebox/parsers/generic_html.py | 53 + .../archivebox/parsers/generic_json.py | 65 + .../archivebox/parsers/generic_rss.py | 49 + .../archivebox/parsers/generic_txt.py | 61 + .../archivebox/parsers/medium_rss.py | 35 + .../archivebox/parsers/netscape_html.py | 39 + .../archivebox/parsers/pinboard_rss.py | 47 + .../archivebox/parsers/pocket_api.py | 113 ++ .../archivebox/parsers/pocket_html.py | 38 + .../archivebox/parsers/shaarli_rss.py | 50 + .../archivebox/parsers/wallabag_atom.py | 57 + .../archivebox/search/__init__.py | 108 ++ .../archivebox/search/backends/__init__.py | 0 .../archivebox/search/backends/ripgrep.py | 45 + .../archivebox/search/backends/sonic.py | 28 + archivebox-0.5.3/archivebox/search/utils.py | 44 + archivebox-0.5.3/archivebox/system.py | 163 +++ .../themes/admin/actions_as_select.html | 1 + .../archivebox/themes/admin/app_index.html | 18 + .../archivebox/themes/admin/base.html | 246 ++++ .../themes/admin/grid_change_list.html | 91 ++ .../archivebox/themes/admin/login.html | 100 ++ .../themes/admin/snapshots_grid.html | 162 +++ .../archivebox/themes/default/add_links.html | 71 ++ .../archivebox/themes/default/base.html | 284 +++++ .../themes/default/core/snapshot_list.html | 51 + .../themes/default/link_details.html | 488 +++++++ .../archivebox/themes/default/main_index.html | 255 ++++ .../themes/default/main_index_minimal.html | 24 + .../themes/default/main_index_row.html | 22 + .../archivebox/themes/default/static/add.css | 62 + .../themes/default/static/admin.css | 234 ++++ .../themes/default/static/archive.png | Bin 0 -> 17730 bytes .../themes/default/static/bootstrap.min.css | 6 + .../themes/default/static/external.png | Bin 0 -> 1647 bytes .../default/static/jquery.dataTables.min.css | 1 + .../default/static/jquery.dataTables.min.js | 166 +++ .../themes/default/static/jquery.min.js | 2 + .../themes/default/static/sort_asc.png | Bin 0 -> 158 bytes .../themes/default/static/sort_both.png | Bin 0 -> 201 bytes .../themes/default/static/sort_desc.png | Bin 0 -> 157 bytes .../themes/default/static/spinner.gif | Bin 0 -> 10949 bytes .../archivebox/themes/legacy/main_index.html | 215 ++++ .../themes/legacy/main_index_row.html | 16 + archivebox-0.5.3/archivebox/util.py | 318 +++++ .../archivebox/vendor/__init__.py | 0 archivebox-0.5.3/build/lib/archivebox/.flake8 | 6 + archivebox-0.5.3/build/lib/archivebox/LICENSE | 21 + .../build/lib/archivebox/README.md | 545 ++++++++ .../build/lib/archivebox/__init__.py | 1 + .../build/lib/archivebox/__main__.py | 11 + .../build/lib/archivebox/cli/__init__.py | 144 +++ .../lib/archivebox/cli/archivebox_add.py | 113 ++ .../lib/archivebox/cli/archivebox_config.py | 61 + .../lib/archivebox/cli/archivebox_help.py | 32 + .../lib/archivebox/cli/archivebox_init.py | 40 + .../lib/archivebox/cli/archivebox_list.py | 139 ++ .../lib/archivebox/cli/archivebox_manage.py | 24 + .../lib/archivebox/cli/archivebox_oneshot.py | 70 + .../lib/archivebox/cli/archivebox_remove.py | 79 ++ .../lib/archivebox/cli/archivebox_schedule.py | 96 ++ .../lib/archivebox/cli/archivebox_server.py | 59 + .../lib/archivebox/cli/archivebox_shell.py | 34 + .../lib/archivebox/cli/archivebox_status.py | 32 + .../lib/archivebox/cli/archivebox_update.py | 132 ++ .../lib/archivebox/cli/archivebox_version.py | 40 + .../build/lib/archivebox/cli/tests.py | 227 ++++ .../build/lib/archivebox/config.py | 1081 ++++++++++++++++ .../build/lib/archivebox/config_stubs.py | 113 ++ .../build/lib/archivebox/core/__init__.py | 1 + .../build/lib/archivebox/core/admin.py | 257 ++++ .../build/lib/archivebox/core/apps.py | 5 + .../build/lib/archivebox/core/forms.py | 67 + .../core/management/commands/archivebox.py | 18 + .../core/migrations/0001_initial.py | 27 + .../migrations/0002_auto_20200625_1521.py | 18 + .../migrations/0003_auto_20200630_1034.py | 38 + .../migrations/0004_auto_20200713_1552.py | 19 + .../migrations/0005_auto_20200728_0326.py | 28 + .../migrations/0006_auto_20201012_1520.py | 70 + .../core/migrations/0007_archiveresult.py | 97 ++ .../migrations/0008_auto_20210105_1421.py | 18 + .../archivebox/core/migrations/__init__.py | 0 .../build/lib/archivebox/core/mixins.py | 23 + .../build/lib/archivebox/core/models.py | 194 +++ .../build/lib/archivebox/core/settings.py | 165 +++ .../archivebox/core/templatetags/__init__.py | 0 .../archivebox/core/templatetags/core_tags.py | 47 + .../build/lib/archivebox/core/tests.py | 3 + .../build/lib/archivebox/core/urls.py | 36 + .../build/lib/archivebox/core/views.py | 173 +++ .../lib/archivebox/core/welcome_message.py | 5 + .../build/lib/archivebox/core/wsgi.py | 16 + .../lib/archivebox/extractors/__init__.py | 182 +++ .../lib/archivebox/extractors/archive_org.py | 112 ++ .../build/lib/archivebox/extractors/dom.py | 69 + .../lib/archivebox/extractors/favicon.py | 64 + .../build/lib/archivebox/extractors/git.py | 90 ++ .../lib/archivebox/extractors/headers.py | 69 + .../build/lib/archivebox/extractors/media.py | 81 ++ .../lib/archivebox/extractors/mercury.py | 104 ++ .../build/lib/archivebox/extractors/pdf.py | 68 + .../lib/archivebox/extractors/readability.py | 124 ++ .../lib/archivebox/extractors/screenshot.py | 67 + .../lib/archivebox/extractors/singlefile.py | 90 ++ .../build/lib/archivebox/extractors/title.py | 130 ++ .../build/lib/archivebox/extractors/wget.py | 184 +++ .../build/lib/archivebox/index/__init__.py | 617 +++++++++ .../build/lib/archivebox/index/csv.py | 37 + .../build/lib/archivebox/index/html.py | 164 +++ .../build/lib/archivebox/index/json.py | 154 +++ .../build/lib/archivebox/index/schema.py | 448 +++++++ .../build/lib/archivebox/index/sql.py | 106 ++ .../build/lib/archivebox/logging_util.py | 569 +++++++++ archivebox-0.5.3/build/lib/archivebox/main.py | 1131 +++++++++++++++++ .../build/lib/archivebox/manage.py | 29 + .../build/lib/archivebox/mypy.ini | 3 + .../build/lib/archivebox/package.json | 21 + .../build/lib/archivebox/parsers/__init__.py | 203 +++ .../lib/archivebox/parsers/generic_html.py | 53 + .../lib/archivebox/parsers/generic_json.py | 65 + .../lib/archivebox/parsers/generic_rss.py | 49 + .../lib/archivebox/parsers/generic_txt.py | 61 + .../lib/archivebox/parsers/medium_rss.py | 35 + .../lib/archivebox/parsers/netscape_html.py | 39 + .../lib/archivebox/parsers/pinboard_rss.py | 47 + .../lib/archivebox/parsers/pocket_api.py | 113 ++ .../lib/archivebox/parsers/pocket_html.py | 38 + .../lib/archivebox/parsers/shaarli_rss.py | 50 + .../lib/archivebox/parsers/wallabag_atom.py | 57 + .../build/lib/archivebox/search/__init__.py | 108 ++ .../archivebox/search/backends/__init__.py | 0 .../lib/archivebox/search/backends/ripgrep.py | 45 + .../lib/archivebox/search/backends/sonic.py | 28 + .../build/lib/archivebox/search/utils.py | 44 + .../build/lib/archivebox/system.py | 163 +++ .../themes/admin/actions_as_select.html | 1 + .../archivebox/themes/admin/app_index.html | 18 + .../lib/archivebox/themes/admin/base.html | 246 ++++ .../themes/admin/grid_change_list.html | 91 ++ .../lib/archivebox/themes/admin/login.html | 100 ++ .../themes/admin/snapshots_grid.html | 162 +++ .../archivebox/themes/default/add_links.html | 71 ++ .../lib/archivebox/themes/default/base.html | 284 +++++ .../themes/default/core/snapshot_list.html | 51 + .../themes/default/link_details.html | 488 +++++++ .../archivebox/themes/default/main_index.html | 255 ++++ .../themes/default/main_index_minimal.html | 24 + .../themes/default/main_index_row.html | 22 + .../archivebox/themes/default/static/add.css | 62 + .../themes/default/static/admin.css | 234 ++++ .../themes/default/static/archive.png | Bin 0 -> 17730 bytes .../themes/default/static/bootstrap.min.css | 6 + .../themes/default/static/external.png | Bin 0 -> 1647 bytes .../default/static/jquery.dataTables.min.css | 1 + .../default/static/jquery.dataTables.min.js | 166 +++ .../themes/default/static/jquery.min.js | 2 + .../themes/default/static/sort_asc.png | Bin 0 -> 158 bytes .../themes/default/static/sort_both.png | Bin 0 -> 201 bytes .../themes/default/static/sort_desc.png | Bin 0 -> 157 bytes .../themes/default/static/spinner.gif | Bin 0 -> 10949 bytes .../archivebox/themes/legacy/main_index.html | 215 ++++ .../themes/legacy/main_index_row.html | 16 + archivebox-0.5.3/build/lib/archivebox/util.py | 318 +++++ .../build/lib/archivebox/vendor/__init__.py | 0 .../archivebox/installed-by-dh_installdocs | 0 .../debian/archivebox.debhelper.log | 19 + .../debian/archivebox.postinst.debhelper | 10 + .../debian/archivebox.prerm.debhelper | 10 + archivebox-0.5.3/debian/archivebox.substvars | 3 + .../debian/archivebox/DEBIAN/control | 30 + .../debian/archivebox/DEBIAN/md5sums | 126 ++ .../debian/archivebox/DEBIAN/postinst | 12 + .../debian/archivebox/DEBIAN/prerm | 12 + .../debian/archivebox/usr/bin/archivebox | 12 + .../archivebox-0.5.3.egg-info/PKG-INFO | 591 +++++++++ .../dependency_links.txt | 1 + .../entry_points.txt | 3 + .../archivebox-0.5.3.egg-info/requires.txt | 15 + .../archivebox-0.5.3.egg-info/top_level.txt | 1 + .../python3/dist-packages/archivebox/.flake8 | 6 + .../python3/dist-packages/archivebox/LICENSE | 21 + .../dist-packages/archivebox/README.md | 545 ++++++++ .../dist-packages/archivebox/__init__.py | 1 + .../dist-packages/archivebox/__main__.py | 11 + .../dist-packages/archivebox/cli/__init__.py | 144 +++ .../archivebox/cli/archivebox_add.py | 113 ++ .../archivebox/cli/archivebox_config.py | 61 + .../archivebox/cli/archivebox_help.py | 32 + .../archivebox/cli/archivebox_init.py | 40 + .../archivebox/cli/archivebox_list.py | 139 ++ .../archivebox/cli/archivebox_manage.py | 24 + .../archivebox/cli/archivebox_oneshot.py | 70 + .../archivebox/cli/archivebox_remove.py | 79 ++ .../archivebox/cli/archivebox_schedule.py | 96 ++ .../archivebox/cli/archivebox_server.py | 59 + .../archivebox/cli/archivebox_shell.py | 34 + .../archivebox/cli/archivebox_status.py | 32 + .../archivebox/cli/archivebox_update.py | 132 ++ .../archivebox/cli/archivebox_version.py | 40 + .../dist-packages/archivebox/cli/tests.py | 227 ++++ .../dist-packages/archivebox/config.py | 1081 ++++++++++++++++ .../dist-packages/archivebox/config_stubs.py | 113 ++ .../dist-packages/archivebox/core/__init__.py | 1 + .../dist-packages/archivebox/core/admin.py | 257 ++++ .../dist-packages/archivebox/core/apps.py | 5 + .../dist-packages/archivebox/core/forms.py | 67 + .../core/management/commands/archivebox.py | 18 + .../core/migrations/0001_initial.py | 27 + .../migrations/0002_auto_20200625_1521.py | 18 + .../migrations/0003_auto_20200630_1034.py | 38 + .../migrations/0004_auto_20200713_1552.py | 19 + .../migrations/0005_auto_20200728_0326.py | 28 + .../migrations/0006_auto_20201012_1520.py | 70 + .../core/migrations/0007_archiveresult.py | 97 ++ .../migrations/0008_auto_20210105_1421.py | 18 + .../archivebox/core/migrations/__init__.py | 0 .../dist-packages/archivebox/core/mixins.py | 23 + .../dist-packages/archivebox/core/models.py | 194 +++ .../dist-packages/archivebox/core/settings.py | 165 +++ .../archivebox/core/templatetags/__init__.py | 0 .../archivebox/core/templatetags/core_tags.py | 47 + .../dist-packages/archivebox/core/tests.py | 3 + .../dist-packages/archivebox/core/urls.py | 36 + .../dist-packages/archivebox/core/views.py | 173 +++ .../archivebox/core/welcome_message.py | 5 + .../dist-packages/archivebox/core/wsgi.py | 16 + .../archivebox/extractors/__init__.py | 182 +++ .../archivebox/extractors/archive_org.py | 112 ++ .../archivebox/extractors/dom.py | 69 + .../archivebox/extractors/favicon.py | 64 + .../archivebox/extractors/git.py | 90 ++ .../archivebox/extractors/headers.py | 69 + .../archivebox/extractors/media.py | 81 ++ .../archivebox/extractors/mercury.py | 104 ++ .../archivebox/extractors/pdf.py | 68 + .../archivebox/extractors/readability.py | 124 ++ .../archivebox/extractors/screenshot.py | 67 + .../archivebox/extractors/singlefile.py | 90 ++ .../archivebox/extractors/title.py | 130 ++ .../archivebox/extractors/wget.py | 184 +++ .../archivebox/index/__init__.py | 617 +++++++++ .../dist-packages/archivebox/index/csv.py | 37 + .../dist-packages/archivebox/index/html.py | 164 +++ .../dist-packages/archivebox/index/json.py | 154 +++ .../dist-packages/archivebox/index/schema.py | 448 +++++++ .../dist-packages/archivebox/index/sql.py | 106 ++ .../dist-packages/archivebox/logging_util.py | 569 +++++++++ .../python3/dist-packages/archivebox/main.py | 1131 +++++++++++++++++ .../dist-packages/archivebox/manage.py | 29 + .../python3/dist-packages/archivebox/mypy.ini | 3 + .../dist-packages/archivebox/package.json | 21 + .../archivebox/parsers/__init__.py | 203 +++ .../archivebox/parsers/generic_html.py | 53 + .../archivebox/parsers/generic_json.py | 65 + .../archivebox/parsers/generic_rss.py | 49 + .../archivebox/parsers/generic_txt.py | 61 + .../archivebox/parsers/medium_rss.py | 35 + .../archivebox/parsers/netscape_html.py | 39 + .../archivebox/parsers/pinboard_rss.py | 47 + .../archivebox/parsers/pocket_api.py | 113 ++ .../archivebox/parsers/pocket_html.py | 38 + .../archivebox/parsers/shaarli_rss.py | 50 + .../archivebox/parsers/wallabag_atom.py | 57 + .../archivebox/search/__init__.py | 108 ++ .../archivebox/search/backends/__init__.py | 0 .../archivebox/search/backends/ripgrep.py | 45 + .../archivebox/search/backends/sonic.py | 28 + .../dist-packages/archivebox/search/utils.py | 44 + .../dist-packages/archivebox/system.py | 163 +++ .../themes/admin/actions_as_select.html | 1 + .../archivebox/themes/admin/app_index.html | 18 + .../archivebox/themes/admin/base.html | 246 ++++ .../themes/admin/grid_change_list.html | 91 ++ .../archivebox/themes/admin/login.html | 100 ++ .../themes/admin/snapshots_grid.html | 162 +++ .../archivebox/themes/default/add_links.html | 71 ++ .../archivebox/themes/default/base.html | 284 +++++ .../themes/default/core/snapshot_list.html | 51 + .../themes/default/link_details.html | 488 +++++++ .../archivebox/themes/default/main_index.html | 255 ++++ .../themes/default/main_index_minimal.html | 24 + .../themes/default/main_index_row.html | 22 + .../archivebox/themes/default/static/add.css | 62 + .../themes/default/static/admin.css | 234 ++++ .../themes/default/static/archive.png | Bin 0 -> 17730 bytes .../themes/default/static/bootstrap.min.css | 6 + .../themes/default/static/external.png | Bin 0 -> 1647 bytes .../default/static/jquery.dataTables.min.css | 1 + .../default/static/jquery.dataTables.min.js | 166 +++ .../themes/default/static/jquery.min.js | 2 + .../themes/default/static/sort_asc.png | Bin 0 -> 158 bytes .../themes/default/static/sort_both.png | Bin 0 -> 201 bytes .../themes/default/static/sort_desc.png | Bin 0 -> 157 bytes .../themes/default/static/spinner.gif | Bin 0 -> 10949 bytes .../archivebox/themes/legacy/main_index.html | 215 ++++ .../themes/legacy/main_index_row.html | 16 + .../python3/dist-packages/archivebox/util.py | 318 +++++ .../archivebox/vendor/__init__.py | 0 .../share/doc/archivebox/changelog.Debian.gz | Bin 0 -> 165 bytes archivebox-0.5.3/debian/changelog | 5 + archivebox-0.5.3/debian/compat | 1 + archivebox-0.5.3/debian/control | 34 + archivebox-0.5.3/debian/files | 2 + archivebox-0.5.3/debian/rules | 21 + archivebox-0.5.3/debian/source/format | 1 + archivebox-0.5.3/debian/source/options | 1 + archivebox-0.5.3/setup.cfg | 4 + archivebox-0.5.3/setup.py | 123 ++ archivebox_0.5.3-1.debian.tar.xz | Bin 0 -> 1556 bytes archivebox_0.5.3-1.dsc | 40 + archivebox_0.5.3-1_all.deb | Bin 0 -> 194244 bytes archivebox_0.5.3-1_amd64.buildinfo | 200 +++ archivebox_0.5.3-1_amd64.changes | 25 + ...vebox_0.5.3-1_source.archivebox-ppa.upload | 5 + archivebox_0.5.3-1_source.buildinfo | 220 ++++ archivebox_0.5.3-1_source.changes | 48 + archivebox_0.5.3.orig.tar.gz | Bin 0 -> 258683 bytes 404 files changed, 42976 insertions(+) create mode 100644 archivebox-0.5.3/.pc/.quilt_patches create mode 100644 archivebox-0.5.3/.pc/.quilt_series create mode 100644 archivebox-0.5.3/.pc/.version create mode 100644 archivebox-0.5.3/.pc/applied-patches create mode 100644 archivebox-0.5.3/MANIFEST.in create mode 100644 archivebox-0.5.3/PKG-INFO create mode 100644 archivebox-0.5.3/README.md create mode 100644 archivebox-0.5.3/archivebox.egg-info/PKG-INFO create mode 100644 archivebox-0.5.3/archivebox.egg-info/SOURCES.txt create mode 100644 archivebox-0.5.3/archivebox.egg-info/dependency_links.txt create mode 100644 archivebox-0.5.3/archivebox.egg-info/entry_points.txt create mode 100644 archivebox-0.5.3/archivebox.egg-info/requires.txt create mode 100644 archivebox-0.5.3/archivebox.egg-info/top_level.txt create mode 100644 archivebox-0.5.3/archivebox/.flake8 create mode 100644 archivebox-0.5.3/archivebox/LICENSE create mode 100644 archivebox-0.5.3/archivebox/README.md create mode 100644 archivebox-0.5.3/archivebox/__init__.py create mode 100755 archivebox-0.5.3/archivebox/__main__.py create mode 100644 archivebox-0.5.3/archivebox/cli/__init__.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_add.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_config.py create mode 100755 archivebox-0.5.3/archivebox/cli/archivebox_help.py create mode 100755 archivebox-0.5.3/archivebox/cli/archivebox_init.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_list.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_manage.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_oneshot.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_remove.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_schedule.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_server.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_shell.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_status.py create mode 100644 archivebox-0.5.3/archivebox/cli/archivebox_update.py create mode 100755 archivebox-0.5.3/archivebox/cli/archivebox_version.py create mode 100755 archivebox-0.5.3/archivebox/cli/tests.py create mode 100644 archivebox-0.5.3/archivebox/config.py create mode 100644 archivebox-0.5.3/archivebox/config_stubs.py create mode 100644 archivebox-0.5.3/archivebox/core/__init__.py create mode 100644 archivebox-0.5.3/archivebox/core/admin.py create mode 100644 archivebox-0.5.3/archivebox/core/apps.py create mode 100644 archivebox-0.5.3/archivebox/core/forms.py create mode 100644 archivebox-0.5.3/archivebox/core/management/commands/archivebox.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0001_initial.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0002_auto_20200625_1521.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0003_auto_20200630_1034.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0004_auto_20200713_1552.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0005_auto_20200728_0326.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0006_auto_20201012_1520.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0007_archiveresult.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/0008_auto_20210105_1421.py create mode 100644 archivebox-0.5.3/archivebox/core/migrations/__init__.py create mode 100644 archivebox-0.5.3/archivebox/core/mixins.py create mode 100644 archivebox-0.5.3/archivebox/core/models.py create mode 100644 archivebox-0.5.3/archivebox/core/settings.py create mode 100644 archivebox-0.5.3/archivebox/core/templatetags/__init__.py create mode 100644 archivebox-0.5.3/archivebox/core/templatetags/core_tags.py create mode 100644 archivebox-0.5.3/archivebox/core/tests.py create mode 100644 archivebox-0.5.3/archivebox/core/urls.py create mode 100644 archivebox-0.5.3/archivebox/core/views.py create mode 100644 archivebox-0.5.3/archivebox/core/welcome_message.py create mode 100644 archivebox-0.5.3/archivebox/core/wsgi.py create mode 100644 archivebox-0.5.3/archivebox/extractors/__init__.py create mode 100644 archivebox-0.5.3/archivebox/extractors/archive_org.py create mode 100644 archivebox-0.5.3/archivebox/extractors/dom.py create mode 100644 archivebox-0.5.3/archivebox/extractors/favicon.py create mode 100644 archivebox-0.5.3/archivebox/extractors/git.py create mode 100644 archivebox-0.5.3/archivebox/extractors/headers.py create mode 100644 archivebox-0.5.3/archivebox/extractors/media.py create mode 100644 archivebox-0.5.3/archivebox/extractors/mercury.py create mode 100644 archivebox-0.5.3/archivebox/extractors/pdf.py create mode 100644 archivebox-0.5.3/archivebox/extractors/readability.py create mode 100644 archivebox-0.5.3/archivebox/extractors/screenshot.py create mode 100644 archivebox-0.5.3/archivebox/extractors/singlefile.py create mode 100644 archivebox-0.5.3/archivebox/extractors/title.py create mode 100644 archivebox-0.5.3/archivebox/extractors/wget.py create mode 100644 archivebox-0.5.3/archivebox/index/__init__.py create mode 100644 archivebox-0.5.3/archivebox/index/csv.py create mode 100644 archivebox-0.5.3/archivebox/index/html.py create mode 100644 archivebox-0.5.3/archivebox/index/json.py create mode 100644 archivebox-0.5.3/archivebox/index/schema.py create mode 100644 archivebox-0.5.3/archivebox/index/sql.py create mode 100644 archivebox-0.5.3/archivebox/logging_util.py create mode 100644 archivebox-0.5.3/archivebox/main.py create mode 100755 archivebox-0.5.3/archivebox/manage.py create mode 100644 archivebox-0.5.3/archivebox/mypy.ini create mode 100644 archivebox-0.5.3/archivebox/package.json create mode 100644 archivebox-0.5.3/archivebox/parsers/__init__.py create mode 100644 archivebox-0.5.3/archivebox/parsers/generic_html.py create mode 100644 archivebox-0.5.3/archivebox/parsers/generic_json.py create mode 100644 archivebox-0.5.3/archivebox/parsers/generic_rss.py create mode 100644 archivebox-0.5.3/archivebox/parsers/generic_txt.py create mode 100644 archivebox-0.5.3/archivebox/parsers/medium_rss.py create mode 100644 archivebox-0.5.3/archivebox/parsers/netscape_html.py create mode 100644 archivebox-0.5.3/archivebox/parsers/pinboard_rss.py create mode 100644 archivebox-0.5.3/archivebox/parsers/pocket_api.py create mode 100644 archivebox-0.5.3/archivebox/parsers/pocket_html.py create mode 100644 archivebox-0.5.3/archivebox/parsers/shaarli_rss.py create mode 100644 archivebox-0.5.3/archivebox/parsers/wallabag_atom.py create mode 100644 archivebox-0.5.3/archivebox/search/__init__.py create mode 100644 archivebox-0.5.3/archivebox/search/backends/__init__.py create mode 100644 archivebox-0.5.3/archivebox/search/backends/ripgrep.py create mode 100644 archivebox-0.5.3/archivebox/search/backends/sonic.py create mode 100644 archivebox-0.5.3/archivebox/search/utils.py create mode 100644 archivebox-0.5.3/archivebox/system.py create mode 100644 archivebox-0.5.3/archivebox/themes/admin/actions_as_select.html create mode 100644 archivebox-0.5.3/archivebox/themes/admin/app_index.html create mode 100644 archivebox-0.5.3/archivebox/themes/admin/base.html create mode 100644 archivebox-0.5.3/archivebox/themes/admin/grid_change_list.html create mode 100644 archivebox-0.5.3/archivebox/themes/admin/login.html create mode 100644 archivebox-0.5.3/archivebox/themes/admin/snapshots_grid.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/add_links.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/base.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/core/snapshot_list.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/link_details.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/main_index.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/main_index_minimal.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/main_index_row.html create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/add.css create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/admin.css create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/archive.png create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/bootstrap.min.css create mode 100755 archivebox-0.5.3/archivebox/themes/default/static/external.png create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/jquery.dataTables.min.css create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/jquery.dataTables.min.js create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/jquery.min.js create mode 100755 archivebox-0.5.3/archivebox/themes/default/static/sort_asc.png create mode 100755 archivebox-0.5.3/archivebox/themes/default/static/sort_both.png create mode 100755 archivebox-0.5.3/archivebox/themes/default/static/sort_desc.png create mode 100644 archivebox-0.5.3/archivebox/themes/default/static/spinner.gif create mode 100644 archivebox-0.5.3/archivebox/themes/legacy/main_index.html create mode 100644 archivebox-0.5.3/archivebox/themes/legacy/main_index_row.html create mode 100644 archivebox-0.5.3/archivebox/util.py create mode 100644 archivebox-0.5.3/archivebox/vendor/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/.flake8 create mode 100644 archivebox-0.5.3/build/lib/archivebox/LICENSE create mode 100644 archivebox-0.5.3/build/lib/archivebox/README.md create mode 100644 archivebox-0.5.3/build/lib/archivebox/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/__main__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_add.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_config.py create mode 100755 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_help.py create mode 100755 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_init.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_list.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_manage.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_oneshot.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_remove.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_schedule.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_server.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_shell.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_status.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_update.py create mode 100755 archivebox-0.5.3/build/lib/archivebox/cli/archivebox_version.py create mode 100755 archivebox-0.5.3/build/lib/archivebox/cli/tests.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/config.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/config_stubs.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/admin.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/apps.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/forms.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/management/commands/archivebox.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0001_initial.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0002_auto_20200625_1521.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0003_auto_20200630_1034.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0004_auto_20200713_1552.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0005_auto_20200728_0326.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0006_auto_20201012_1520.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0007_archiveresult.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/0008_auto_20210105_1421.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/migrations/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/mixins.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/models.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/settings.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/templatetags/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/templatetags/core_tags.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/tests.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/urls.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/views.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/welcome_message.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/core/wsgi.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/archive_org.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/dom.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/favicon.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/git.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/headers.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/media.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/mercury.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/pdf.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/readability.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/screenshot.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/singlefile.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/title.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/extractors/wget.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/csv.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/html.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/json.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/schema.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/index/sql.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/logging_util.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/main.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/manage.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/mypy.ini create mode 100644 archivebox-0.5.3/build/lib/archivebox/package.json create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/generic_html.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/generic_json.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/generic_rss.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/generic_txt.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/medium_rss.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/netscape_html.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/pinboard_rss.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/pocket_api.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/pocket_html.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/shaarli_rss.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/parsers/wallabag_atom.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/search/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/search/backends/__init__.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/search/backends/ripgrep.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/search/backends/sonic.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/search/utils.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/system.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/actions_as_select.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/app_index.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/base.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/grid_change_list.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/login.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/admin/snapshots_grid.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/add_links.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/base.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/core/snapshot_list.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/link_details.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/main_index.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_minimal.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_row.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/add.css create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/admin.css create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/archive.png create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/bootstrap.min.css create mode 100755 archivebox-0.5.3/build/lib/archivebox/themes/default/static/external.png create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.dataTables.min.css create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.dataTables.min.js create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.min.js create mode 100755 archivebox-0.5.3/build/lib/archivebox/themes/default/static/sort_asc.png create mode 100755 archivebox-0.5.3/build/lib/archivebox/themes/default/static/sort_both.png create mode 100755 archivebox-0.5.3/build/lib/archivebox/themes/default/static/sort_desc.png create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/default/static/spinner.gif create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/legacy/main_index.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/themes/legacy/main_index_row.html create mode 100644 archivebox-0.5.3/build/lib/archivebox/util.py create mode 100644 archivebox-0.5.3/build/lib/archivebox/vendor/__init__.py create mode 100644 archivebox-0.5.3/debian/.debhelper/generated/archivebox/installed-by-dh_installdocs create mode 100644 archivebox-0.5.3/debian/archivebox.debhelper.log create mode 100644 archivebox-0.5.3/debian/archivebox.postinst.debhelper create mode 100644 archivebox-0.5.3/debian/archivebox.prerm.debhelper create mode 100644 archivebox-0.5.3/debian/archivebox.substvars create mode 100644 archivebox-0.5.3/debian/archivebox/DEBIAN/control create mode 100644 archivebox-0.5.3/debian/archivebox/DEBIAN/md5sums create mode 100755 archivebox-0.5.3/debian/archivebox/DEBIAN/postinst create mode 100755 archivebox-0.5.3/debian/archivebox/DEBIAN/prerm create mode 100755 archivebox-0.5.3/debian/archivebox/usr/bin/archivebox create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/PKG-INFO create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/dependency_links.txt create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/entry_points.txt create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/requires.txt create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/top_level.txt create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/.flake8 create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/LICENSE create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/README.md create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__main__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_add.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_config.py create mode 100755 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_help.py create mode 100755 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_init.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_list.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_manage.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_oneshot.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_remove.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_schedule.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_server.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_shell.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_status.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_update.py create mode 100755 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_version.py create mode 100755 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/tests.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config_stubs.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/admin.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/apps.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/forms.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/management/commands/archivebox.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0001_initial.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0002_auto_20200625_1521.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0003_auto_20200630_1034.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0004_auto_20200713_1552.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0005_auto_20200728_0326.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0006_auto_20201012_1520.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0007_archiveresult.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0008_auto_20210105_1421.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/mixins.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/models.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/settings.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/core_tags.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/tests.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/urls.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/views.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/welcome_message.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/wsgi.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/archive_org.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/dom.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/favicon.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/git.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/headers.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/media.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/mercury.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/pdf.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/readability.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/screenshot.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/singlefile.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/title.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/wget.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/csv.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/html.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/json.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/schema.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/sql.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/logging_util.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/main.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/manage.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/mypy.ini create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/package.json create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_html.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_json.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_rss.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_txt.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/medium_rss.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/netscape_html.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pinboard_rss.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_api.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_html.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/shaarli_rss.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/wallabag_atom.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/ripgrep.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/sonic.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/utils.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/system.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/actions_as_select.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/app_index.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/base.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/grid_change_list.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/login.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/snapshots_grid.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/add_links.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/base.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/core/snapshot_list.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/link_details.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_minimal.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_row.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/add.css create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/admin.css create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/archive.png create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/bootstrap.min.css create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/external.png create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.css create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.js create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.min.js create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_asc.png create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_both.png create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_desc.png create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/spinner.gif create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index_row.html create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/util.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/vendor/__init__.py create mode 100644 archivebox-0.5.3/debian/archivebox/usr/share/doc/archivebox/changelog.Debian.gz create mode 100644 archivebox-0.5.3/debian/changelog create mode 100644 archivebox-0.5.3/debian/compat create mode 100644 archivebox-0.5.3/debian/control create mode 100644 archivebox-0.5.3/debian/files create mode 100755 archivebox-0.5.3/debian/rules create mode 100644 archivebox-0.5.3/debian/source/format create mode 100644 archivebox-0.5.3/debian/source/options create mode 100644 archivebox-0.5.3/setup.cfg create mode 100755 archivebox-0.5.3/setup.py create mode 100644 archivebox_0.5.3-1.debian.tar.xz create mode 100644 archivebox_0.5.3-1.dsc create mode 100644 archivebox_0.5.3-1_all.deb create mode 100644 archivebox_0.5.3-1_amd64.buildinfo create mode 100644 archivebox_0.5.3-1_amd64.changes create mode 100644 archivebox_0.5.3-1_source.archivebox-ppa.upload create mode 100644 archivebox_0.5.3-1_source.buildinfo create mode 100644 archivebox_0.5.3-1_source.changes create mode 100644 archivebox_0.5.3.orig.tar.gz diff --git a/archivebox-0.5.3/.pc/.quilt_patches b/archivebox-0.5.3/.pc/.quilt_patches new file mode 100644 index 0000000..6857a8d --- /dev/null +++ b/archivebox-0.5.3/.pc/.quilt_patches @@ -0,0 +1 @@ +debian/patches diff --git a/archivebox-0.5.3/.pc/.quilt_series b/archivebox-0.5.3/.pc/.quilt_series new file mode 100644 index 0000000..c206706 --- /dev/null +++ b/archivebox-0.5.3/.pc/.quilt_series @@ -0,0 +1 @@ +series diff --git a/archivebox-0.5.3/.pc/.version b/archivebox-0.5.3/.pc/.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/archivebox-0.5.3/.pc/.version @@ -0,0 +1 @@ +2 diff --git a/archivebox-0.5.3/.pc/applied-patches b/archivebox-0.5.3/.pc/applied-patches new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/MANIFEST.in b/archivebox-0.5.3/MANIFEST.in new file mode 100644 index 0000000..c9ae153 --- /dev/null +++ b/archivebox-0.5.3/MANIFEST.in @@ -0,0 +1,4 @@ +graft archivebox +global-exclude .DS_Store +global-exclude __pycache__ +global-exclude *.pyc diff --git a/archivebox-0.5.3/PKG-INFO b/archivebox-0.5.3/PKG-INFO new file mode 100644 index 0000000..b6534de --- /dev/null +++ b/archivebox-0.5.3/PKG-INFO @@ -0,0 +1,591 @@ +Metadata-Version: 2.1 +Name: archivebox +Version: 0.5.3 +Summary: The self-hosted internet archive. +Home-page: https://github.com/ArchiveBox/ArchiveBox +Author: Nick Sweeting +Author-email: git@nicksweeting.com +License: MIT +Project-URL: Source, https://github.com/ArchiveBox/ArchiveBox +Project-URL: Documentation, https://github.com/ArchiveBox/ArchiveBox/wiki +Project-URL: Bug Tracker, https://github.com/ArchiveBox/ArchiveBox/issues +Project-URL: Changelog, https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog +Project-URL: Roadmap, https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap +Project-URL: Community, https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community +Project-URL: Donate, https://github.com/ArchiveBox/ArchiveBox/wiki/Donations +Description:
+ +

ArchiveBox
The open-source self-hosted web archive.

+ + ▶️ Quickstart | + Demo | + Github | + Documentation | + Info & Motivation | + Community | + Roadmap + +
+        "Your own personal internet archive" (网站存档 / 爬虫)
+        
+ + + + + + + + + + +
+
+ + ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + + Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + + The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + + ### Quickstart + + It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + + ```bash + pip3 install archivebox + archivebox --version + # install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + + mkdir ~/archivebox && cd ~/archivebox # this can be anywhere + archivebox init + + archivebox add 'https://example.com' + archivebox add --depth=1 'https://example.com' + archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all + archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ + archivebox help # to see more options + ``` + + *(click to expand the sections below for full setup instructions)* + +
+ Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + + First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

+ This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + + ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml + docker-compose run archivebox init + docker-compose run archivebox --version + + # start the webserver and open the UI (optional) + docker-compose run archivebox manage createsuperuser + docker-compose up -d + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker-compose run archivebox add 'https://example.com' + docker-compose run archivebox status + docker-compose run archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with docker on any platform + + First make sure you have Docker installed: https://docs.docker.com/get-docker/
+ ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + docker run -v $PWD:/data -it archivebox/archivebox init + docker run -v $PWD:/data -it archivebox/archivebox --version + + # start the webserver and open the UI (optional) + docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser + docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' + docker run -v $PWD:/data -it archivebox/archivebox status + docker run -v $PWD:/data -it archivebox/archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with apt on Ubuntu >=20.04 + + ```bash + sudo add-apt-repository -u ppa:archivebox/archivebox + sudo apt install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + + For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: + ```bash + deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + ``` + (you may need to install some other dependencies manually however) + +
+ +
+ Get ArchiveBox with brew on macOS >=10.13 + + ```bash + brew install archivebox/archivebox/archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with pip on any platform + + ```bash + pip3 install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + # Install any missing extras like wget/git/chrome/etc. manually as needed + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
+ + --- + +
+ +
+ + DEMO: archivebox.zervice.io/ + For more information, see the full Quickstart guide, Usage, and Configuration docs. +
+ + --- + + + # Overview + + ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + + To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + + The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + + At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
+ CLI Screenshot + Desktop index screenshot + Desktop details page Screenshot + Desktop details page Screenshot
+ Demo | Usage | Screenshots +
+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . +

+ + + ## Key Features + + - [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally + - [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) + - [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** + - Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC + - ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) + - **Doesn't require a constantly-running daemon**, proxy, or native app + - Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) + - Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. + - Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + + ## Input formats + + ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + + ```bash + echo 'http://example.com' | archivebox add + archivebox add 'https://example.com/some/page' + archivebox add < ~/Downloads/firefox_bookmarks_export.html + archivebox add < any_text_with_urls_in_it.txt + archivebox add --depth=1 'https://example.com/some/downloads.html' + archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' + ``` + + - Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) + - RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format + - Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + + See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + + It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + + ## Output formats + + All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + + The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + + ```bash + ls ./archive// + ``` + + - **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details + - **Title:** `title` title of the site + - **Favicon:** `favicon.ico` favicon of the site + - **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file + - **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile + - **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present + - **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving + - **PDF:** `output.pdf` Printed PDF of site using headless chrome + - **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome + - **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome + - **Readability:** `article.html/json` Article text extraction using Readability + - **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org + - **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl + - **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links + - _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + + It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + + ## Dependencies + + You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + + If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + + ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + + ## Caveats + + If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. + ```bash + # don't do this: + archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' + archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + + # without first disabling share the URL with 3rd party APIs: + archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org + archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL + archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google + ``` + + Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. + ```bash + # visiting an archived page with malicious JS: + https://127.0.0.1:8000/archive/1602401954/example.com/index.html + + # example.com/index.js can now make a request to read everything: + https://127.0.0.1:8000/index.html + https://127.0.0.1:8000/archive/* + # then example.com/index.js can send it off to some evil server + ``` + + Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: + ```bash + archivebox add 'https://example.com#2020-10-24' + ... + archivebox add 'https://example.com#2020-10-25' + ``` + + --- + +
+ +
+ + --- + + # Background & Motivation + + Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + + Whether it's to resist censorship by saving articles before they get taken down or edited, or + just to save a collection of early 2010's flash games you love to play, having the tools to + archive internet content enables to you save the stuff you care most about before it disappears. + +
+
+ Image from WTF is Link Rot?...
+
+ + The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. + I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + + Because modern websites are complicated and often rely on dynamic content, + ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + + All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + + ## Comparison to Other Projects + + ▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + + comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + + #### User Interface & Intended Purpose + + ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + + #### Private Local Archives vs Centralized Public Archives + + Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + + #### Storage Requirements + + Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + + ## Learn more + + Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + + - [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ + - Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. + - Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + + --- + + # Documentation + + + + We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + + You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + + ## Getting Started + + - [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) + - [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) + - [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + + ## Reference + + - [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) + - [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) + - [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) + - [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) + - [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) + - [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) + - [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) + - [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) + - [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) + - [Python API](https://docs.archivebox.io/en/latest/modules.html) + - REST API (coming soon...) + + ## More Info + + - [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) + - [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) + - [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) + - [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) + - [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + + --- + + # ArchiveBox Development + + All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + + ### Setup the dev environment + + First, install the system dependencies from the "Bare Metal" section above. + Then you can clone the ArchiveBox repo and install + ```python3 + git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox + git checkout master # or the branch you want to test + git submodule update --init --recursive + git pull --recurse-submodules + + # Install ArchiveBox + python dependencies + python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] + # or with pipenv: pipenv install --dev && pipenv shell + + # Install node dependencies + npm install + + # Optional: install extractor dependencies manually or with helper script + ./bin/setup.sh + + # Optional: develop via docker by mounting the code dir into the container + # if you edit e.g. ./archivebox/core/models.py on the docker host, runserver + # inside the container will reload and pick up your changes + docker build . -t archivebox + docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload + ``` + + ### Common development tasks + + See the `./bin/` folder and read the source of the bash scripts within. + You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + + #### Run the linters + + ```bash + ./bin/lint.sh + ``` + (uses `flake8` and `mypy`) + + #### Run the integration tests + + ```bash + ./bin/test.sh + ``` + (uses `pytest -s`) + + #### Make migrations or enter a django shell + + ```bash + cd archivebox/ + ./manage.py makemigrations + + cd data/ + archivebox shell + ``` + (uses `pytest -s`) + + #### Build the docs, pip package, and docker image + + ```bash + ./bin/build.sh + + # or individually: + ./bin/build_docs.sh + ./bin/build_pip.sh + ./bin/build_deb.sh + ./bin/build_brew.sh + ./bin/build_docker.sh + ``` + + #### Roll a release + + ```bash + ./bin/release.sh + ``` + (bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + + --- + +
+

+ +
+ This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

+ +
+ Sponsor us on Github +
+
+ +
+ + + + +

+ +
+ +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: Topic :: System :: Archiving +Classifier: Topic :: System :: Archiving :: Backup +Classifier: Topic :: System :: Recovery Tools +Classifier: Topic :: Sociology :: History +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: End Users/Desktop +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: Legal Industry +Classifier: Intended Audience :: System Administrators +Classifier: Environment :: Console +Classifier: Environment :: Web Environment +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Framework :: Django +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Provides-Extra: dev diff --git a/archivebox-0.5.3/README.md b/archivebox-0.5.3/README.md new file mode 100644 index 0000000..2e35783 --- /dev/null +++ b/archivebox-0.5.3/README.md @@ -0,0 +1,545 @@ +
+ +

ArchiveBox
The open-source self-hosted web archive.

+ +▶️ Quickstart | +Demo | +Github | +Documentation | +Info & Motivation | +Community | +Roadmap + +
+"Your own personal internet archive" (网站存档 / 爬虫)
+
+ + + + + + + + + + +
+
+ +ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + +Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + +The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + +### Quickstart + +It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + +```bash +pip3 install archivebox +archivebox --version +# install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + +mkdir ~/archivebox && cd ~/archivebox # this can be anywhere +archivebox init + +archivebox add 'https://example.com' +archivebox add --depth=1 'https://example.com' +archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all +archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ +archivebox help # to see more options +``` + +*(click to expand the sections below for full setup instructions)* + +
+Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + +First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

+This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml +docker-compose run archivebox init +docker-compose run archivebox --version + +# start the webserver and open the UI (optional) +docker-compose run archivebox manage createsuperuser +docker-compose up -d +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker-compose run archivebox add 'https://example.com' +docker-compose run archivebox status +docker-compose run archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with docker on any platform + +First make sure you have Docker installed: https://docs.docker.com/get-docker/
+```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +docker run -v $PWD:/data -it archivebox/archivebox init +docker run -v $PWD:/data -it archivebox/archivebox --version + +# start the webserver and open the UI (optional) +docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser +docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' +docker run -v $PWD:/data -it archivebox/archivebox status +docker run -v $PWD:/data -it archivebox/archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with apt on Ubuntu >=20.04 + +```bash +sudo add-apt-repository -u ppa:archivebox/archivebox +sudo apt install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: +```bash +deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +``` +(you may need to install some other dependencies manually however) + +
+ +
+Get ArchiveBox with brew on macOS >=10.13 + +```bash +brew install archivebox/archivebox/archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with pip on any platform + +```bash +pip3 install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version +# Install any missing extras like wget/git/chrome/etc. manually as needed + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
+ +--- + +
+ +
+ +DEMO: archivebox.zervice.io/ +For more information, see the full Quickstart guide, Usage, and Configuration docs. +
+ +--- + + +# Overview + +ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + +To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + +The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + +At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
+CLI Screenshot +Desktop index screenshot +Desktop details page Screenshot +Desktop details page Screenshot
+Demo | Usage | Screenshots +
+. . . . . . . . . . . . . . . . . . . . . . . . . . . . +

+ + +## Key Features + +- [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally +- [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) +- [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) +- Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** +- Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC +- ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) +- **Doesn't require a constantly-running daemon**, proxy, or native app +- Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) +- Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. +- Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + +## Input formats + +ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + +```bash +echo 'http://example.com' | archivebox add +archivebox add 'https://example.com/some/page' +archivebox add < ~/Downloads/firefox_bookmarks_export.html +archivebox add < any_text_with_urls_in_it.txt +archivebox add --depth=1 'https://example.com/some/downloads.html' +archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' +``` + +- Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) +- RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format +- Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + +See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + +It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + +## Output formats + +All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + +The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + +```bash + ls ./archive// +``` + +- **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details +- **Title:** `title` title of the site +- **Favicon:** `favicon.ico` favicon of the site +- **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file +- **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile +- **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present +- **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving +- **PDF:** `output.pdf` Printed PDF of site using headless chrome +- **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome +- **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome +- **Readability:** `article.html/json` Article text extraction using Readability +- **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org +- **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl +- **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links +- _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + +It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + +## Dependencies + +You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + +If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + +ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + +## Caveats + +If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. +```bash +# don't do this: +archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' +archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + +# without first disabling share the URL with 3rd party APIs: +archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org +archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL +archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google +``` + +Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. +```bash +# visiting an archived page with malicious JS: +https://127.0.0.1:8000/archive/1602401954/example.com/index.html + +# example.com/index.js can now make a request to read everything: +https://127.0.0.1:8000/index.html +https://127.0.0.1:8000/archive/* +# then example.com/index.js can send it off to some evil server +``` + +Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: +```bash +archivebox add 'https://example.com#2020-10-24' +... +archivebox add 'https://example.com#2020-10-25' +``` + +--- + +
+ +
+ +--- + +# Background & Motivation + +Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + +Whether it's to resist censorship by saving articles before they get taken down or edited, or +just to save a collection of early 2010's flash games you love to play, having the tools to +archive internet content enables to you save the stuff you care most about before it disappears. + +
+
+ Image from WTF is Link Rot?...
+
+ +The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. +I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + +Because modern websites are complicated and often rely on dynamic content, +ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + +All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + +## Comparison to Other Projects + +▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + +comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + +#### User Interface & Intended Purpose + +ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + +#### Private Local Archives vs Centralized Public Archives + +Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + +#### Storage Requirements + +Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + +## Learn more + +Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + +- [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ +- Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. +- Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + +--- + +# Documentation + + + +We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + +You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + +## Getting Started + +- [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) +- [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) +- [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + +## Reference + +- [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) +- [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) +- [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) +- [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) +- [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) +- [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) +- [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) +- [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) +- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) +- [Python API](https://docs.archivebox.io/en/latest/modules.html) +- REST API (coming soon...) + +## More Info + +- [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) +- [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) +- [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) +- [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) +- [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + +--- + +# ArchiveBox Development + +All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + +### Setup the dev environment + +First, install the system dependencies from the "Bare Metal" section above. +Then you can clone the ArchiveBox repo and install +```python3 +git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox +git checkout master # or the branch you want to test +git submodule update --init --recursive +git pull --recurse-submodules + +# Install ArchiveBox + python dependencies +python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] +# or with pipenv: pipenv install --dev && pipenv shell + +# Install node dependencies +npm install + +# Optional: install extractor dependencies manually or with helper script +./bin/setup.sh + +# Optional: develop via docker by mounting the code dir into the container +# if you edit e.g. ./archivebox/core/models.py on the docker host, runserver +# inside the container will reload and pick up your changes +docker build . -t archivebox +docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload +``` + +### Common development tasks + +See the `./bin/` folder and read the source of the bash scripts within. +You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + +#### Run the linters + +```bash +./bin/lint.sh +``` +(uses `flake8` and `mypy`) + +#### Run the integration tests + +```bash +./bin/test.sh +``` +(uses `pytest -s`) + +#### Make migrations or enter a django shell + +```bash +cd archivebox/ +./manage.py makemigrations + +cd data/ +archivebox shell +``` +(uses `pytest -s`) + +#### Build the docs, pip package, and docker image + +```bash +./bin/build.sh + +# or individually: +./bin/build_docs.sh +./bin/build_pip.sh +./bin/build_deb.sh +./bin/build_brew.sh +./bin/build_docker.sh +``` + +#### Roll a release + +```bash +./bin/release.sh +``` +(bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + +--- + +
+

+ +
+This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

+ +
+Sponsor us on Github +
+
+ +
+ + + + +

+ +
diff --git a/archivebox-0.5.3/archivebox.egg-info/PKG-INFO b/archivebox-0.5.3/archivebox.egg-info/PKG-INFO new file mode 100644 index 0000000..b6534de --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/PKG-INFO @@ -0,0 +1,591 @@ +Metadata-Version: 2.1 +Name: archivebox +Version: 0.5.3 +Summary: The self-hosted internet archive. +Home-page: https://github.com/ArchiveBox/ArchiveBox +Author: Nick Sweeting +Author-email: git@nicksweeting.com +License: MIT +Project-URL: Source, https://github.com/ArchiveBox/ArchiveBox +Project-URL: Documentation, https://github.com/ArchiveBox/ArchiveBox/wiki +Project-URL: Bug Tracker, https://github.com/ArchiveBox/ArchiveBox/issues +Project-URL: Changelog, https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog +Project-URL: Roadmap, https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap +Project-URL: Community, https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community +Project-URL: Donate, https://github.com/ArchiveBox/ArchiveBox/wiki/Donations +Description:
+ +

ArchiveBox
The open-source self-hosted web archive.

+ + ▶️ Quickstart | + Demo | + Github | + Documentation | + Info & Motivation | + Community | + Roadmap + +
+        "Your own personal internet archive" (网站存档 / 爬虫)
+        
+ + + + + + + + + + +
+
+ + ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + + Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + + The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + + ### Quickstart + + It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + + ```bash + pip3 install archivebox + archivebox --version + # install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + + mkdir ~/archivebox && cd ~/archivebox # this can be anywhere + archivebox init + + archivebox add 'https://example.com' + archivebox add --depth=1 'https://example.com' + archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all + archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ + archivebox help # to see more options + ``` + + *(click to expand the sections below for full setup instructions)* + +
+ Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + + First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

+ This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + + ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml + docker-compose run archivebox init + docker-compose run archivebox --version + + # start the webserver and open the UI (optional) + docker-compose run archivebox manage createsuperuser + docker-compose up -d + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker-compose run archivebox add 'https://example.com' + docker-compose run archivebox status + docker-compose run archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with docker on any platform + + First make sure you have Docker installed: https://docs.docker.com/get-docker/
+ ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + docker run -v $PWD:/data -it archivebox/archivebox init + docker run -v $PWD:/data -it archivebox/archivebox --version + + # start the webserver and open the UI (optional) + docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser + docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' + docker run -v $PWD:/data -it archivebox/archivebox status + docker run -v $PWD:/data -it archivebox/archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with apt on Ubuntu >=20.04 + + ```bash + sudo add-apt-repository -u ppa:archivebox/archivebox + sudo apt install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + + For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: + ```bash + deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + ``` + (you may need to install some other dependencies manually however) + +
+ +
+ Get ArchiveBox with brew on macOS >=10.13 + + ```bash + brew install archivebox/archivebox/archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
+ +
+ Get ArchiveBox with pip on any platform + + ```bash + pip3 install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + # Install any missing extras like wget/git/chrome/etc. manually as needed + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
+ + --- + +
+ +
+ + DEMO: archivebox.zervice.io/ + For more information, see the full Quickstart guide, Usage, and Configuration docs. +
+ + --- + + + # Overview + + ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + + To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + + The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + + At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
+ CLI Screenshot + Desktop index screenshot + Desktop details page Screenshot + Desktop details page Screenshot
+ Demo | Usage | Screenshots +
+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . +

+ + + ## Key Features + + - [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally + - [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) + - [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** + - Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC + - ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) + - **Doesn't require a constantly-running daemon**, proxy, or native app + - Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) + - Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. + - Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + + ## Input formats + + ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + + ```bash + echo 'http://example.com' | archivebox add + archivebox add 'https://example.com/some/page' + archivebox add < ~/Downloads/firefox_bookmarks_export.html + archivebox add < any_text_with_urls_in_it.txt + archivebox add --depth=1 'https://example.com/some/downloads.html' + archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' + ``` + + - Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) + - RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format + - Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + + See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + + It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + + ## Output formats + + All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + + The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + + ```bash + ls ./archive// + ``` + + - **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details + - **Title:** `title` title of the site + - **Favicon:** `favicon.ico` favicon of the site + - **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file + - **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile + - **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present + - **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving + - **PDF:** `output.pdf` Printed PDF of site using headless chrome + - **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome + - **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome + - **Readability:** `article.html/json` Article text extraction using Readability + - **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org + - **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl + - **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links + - _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + + It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + + ## Dependencies + + You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + + If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + + ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + + ## Caveats + + If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. + ```bash + # don't do this: + archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' + archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + + # without first disabling share the URL with 3rd party APIs: + archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org + archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL + archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google + ``` + + Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. + ```bash + # visiting an archived page with malicious JS: + https://127.0.0.1:8000/archive/1602401954/example.com/index.html + + # example.com/index.js can now make a request to read everything: + https://127.0.0.1:8000/index.html + https://127.0.0.1:8000/archive/* + # then example.com/index.js can send it off to some evil server + ``` + + Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: + ```bash + archivebox add 'https://example.com#2020-10-24' + ... + archivebox add 'https://example.com#2020-10-25' + ``` + + --- + +
+ +
+ + --- + + # Background & Motivation + + Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + + Whether it's to resist censorship by saving articles before they get taken down or edited, or + just to save a collection of early 2010's flash games you love to play, having the tools to + archive internet content enables to you save the stuff you care most about before it disappears. + +
+
+ Image from WTF is Link Rot?...
+
+ + The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. + I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + + Because modern websites are complicated and often rely on dynamic content, + ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + + All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + + ## Comparison to Other Projects + + ▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + + comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + + #### User Interface & Intended Purpose + + ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + + #### Private Local Archives vs Centralized Public Archives + + Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + + #### Storage Requirements + + Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + + ## Learn more + + Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + + - [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ + - Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. + - Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + + --- + + # Documentation + + + + We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + + You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + + ## Getting Started + + - [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) + - [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) + - [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + + ## Reference + + - [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) + - [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) + - [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) + - [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) + - [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) + - [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) + - [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) + - [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) + - [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) + - [Python API](https://docs.archivebox.io/en/latest/modules.html) + - REST API (coming soon...) + + ## More Info + + - [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) + - [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) + - [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) + - [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) + - [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + + --- + + # ArchiveBox Development + + All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + + ### Setup the dev environment + + First, install the system dependencies from the "Bare Metal" section above. + Then you can clone the ArchiveBox repo and install + ```python3 + git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox + git checkout master # or the branch you want to test + git submodule update --init --recursive + git pull --recurse-submodules + + # Install ArchiveBox + python dependencies + python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] + # or with pipenv: pipenv install --dev && pipenv shell + + # Install node dependencies + npm install + + # Optional: install extractor dependencies manually or with helper script + ./bin/setup.sh + + # Optional: develop via docker by mounting the code dir into the container + # if you edit e.g. ./archivebox/core/models.py on the docker host, runserver + # inside the container will reload and pick up your changes + docker build . -t archivebox + docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload + ``` + + ### Common development tasks + + See the `./bin/` folder and read the source of the bash scripts within. + You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + + #### Run the linters + + ```bash + ./bin/lint.sh + ``` + (uses `flake8` and `mypy`) + + #### Run the integration tests + + ```bash + ./bin/test.sh + ``` + (uses `pytest -s`) + + #### Make migrations or enter a django shell + + ```bash + cd archivebox/ + ./manage.py makemigrations + + cd data/ + archivebox shell + ``` + (uses `pytest -s`) + + #### Build the docs, pip package, and docker image + + ```bash + ./bin/build.sh + + # or individually: + ./bin/build_docs.sh + ./bin/build_pip.sh + ./bin/build_deb.sh + ./bin/build_brew.sh + ./bin/build_docker.sh + ``` + + #### Roll a release + + ```bash + ./bin/release.sh + ``` + (bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + + --- + +
+

+ +
+ This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

+ +
+ Sponsor us on Github +
+
+ +
+ + + + +

+ +
+ +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: Topic :: System :: Archiving +Classifier: Topic :: System :: Archiving :: Backup +Classifier: Topic :: System :: Recovery Tools +Classifier: Topic :: Sociology :: History +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: End Users/Desktop +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: Legal Industry +Classifier: Intended Audience :: System Administrators +Classifier: Environment :: Console +Classifier: Environment :: Web Environment +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Framework :: Django +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Provides-Extra: dev diff --git a/archivebox-0.5.3/archivebox.egg-info/SOURCES.txt b/archivebox-0.5.3/archivebox.egg-info/SOURCES.txt new file mode 100644 index 0000000..6d08f24 --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/SOURCES.txt @@ -0,0 +1,129 @@ +MANIFEST.in +README.md +setup.cfg +setup.py +archivebox/.flake8 +archivebox/LICENSE +archivebox/README.md +archivebox/__init__.py +archivebox/__main__.py +archivebox/config.py +archivebox/config_stubs.py +archivebox/logging_util.py +archivebox/main.py +archivebox/manage.py +archivebox/mypy.ini +archivebox/package.json +archivebox/system.py +archivebox/util.py +archivebox.egg-info/PKG-INFO +archivebox.egg-info/SOURCES.txt +archivebox.egg-info/dependency_links.txt +archivebox.egg-info/entry_points.txt +archivebox.egg-info/requires.txt +archivebox.egg-info/top_level.txt +archivebox/cli/__init__.py +archivebox/cli/archivebox_add.py +archivebox/cli/archivebox_config.py +archivebox/cli/archivebox_help.py +archivebox/cli/archivebox_init.py +archivebox/cli/archivebox_list.py +archivebox/cli/archivebox_manage.py +archivebox/cli/archivebox_oneshot.py +archivebox/cli/archivebox_remove.py +archivebox/cli/archivebox_schedule.py +archivebox/cli/archivebox_server.py +archivebox/cli/archivebox_shell.py +archivebox/cli/archivebox_status.py +archivebox/cli/archivebox_update.py +archivebox/cli/archivebox_version.py +archivebox/cli/tests.py +archivebox/core/__init__.py +archivebox/core/admin.py +archivebox/core/apps.py +archivebox/core/forms.py +archivebox/core/mixins.py +archivebox/core/models.py +archivebox/core/settings.py +archivebox/core/tests.py +archivebox/core/urls.py +archivebox/core/views.py +archivebox/core/welcome_message.py +archivebox/core/wsgi.py +archivebox/core/management/commands/archivebox.py +archivebox/core/migrations/0001_initial.py +archivebox/core/migrations/0002_auto_20200625_1521.py +archivebox/core/migrations/0003_auto_20200630_1034.py +archivebox/core/migrations/0004_auto_20200713_1552.py +archivebox/core/migrations/0005_auto_20200728_0326.py +archivebox/core/migrations/0006_auto_20201012_1520.py +archivebox/core/migrations/0007_archiveresult.py +archivebox/core/migrations/0008_auto_20210105_1421.py +archivebox/core/migrations/__init__.py +archivebox/core/templatetags/__init__.py +archivebox/core/templatetags/core_tags.py +archivebox/extractors/__init__.py +archivebox/extractors/archive_org.py +archivebox/extractors/dom.py +archivebox/extractors/favicon.py +archivebox/extractors/git.py +archivebox/extractors/headers.py +archivebox/extractors/media.py +archivebox/extractors/mercury.py +archivebox/extractors/pdf.py +archivebox/extractors/readability.py +archivebox/extractors/screenshot.py +archivebox/extractors/singlefile.py +archivebox/extractors/title.py +archivebox/extractors/wget.py +archivebox/index/__init__.py +archivebox/index/csv.py +archivebox/index/html.py +archivebox/index/json.py +archivebox/index/schema.py +archivebox/index/sql.py +archivebox/parsers/__init__.py +archivebox/parsers/generic_html.py +archivebox/parsers/generic_json.py +archivebox/parsers/generic_rss.py +archivebox/parsers/generic_txt.py +archivebox/parsers/medium_rss.py +archivebox/parsers/netscape_html.py +archivebox/parsers/pinboard_rss.py +archivebox/parsers/pocket_api.py +archivebox/parsers/pocket_html.py +archivebox/parsers/shaarli_rss.py +archivebox/parsers/wallabag_atom.py +archivebox/search/__init__.py +archivebox/search/utils.py +archivebox/search/backends/__init__.py +archivebox/search/backends/ripgrep.py +archivebox/search/backends/sonic.py +archivebox/themes/admin/actions_as_select.html +archivebox/themes/admin/app_index.html +archivebox/themes/admin/base.html +archivebox/themes/admin/grid_change_list.html +archivebox/themes/admin/login.html +archivebox/themes/admin/snapshots_grid.html +archivebox/themes/default/add_links.html +archivebox/themes/default/base.html +archivebox/themes/default/link_details.html +archivebox/themes/default/main_index.html +archivebox/themes/default/main_index_minimal.html +archivebox/themes/default/main_index_row.html +archivebox/themes/default/core/snapshot_list.html +archivebox/themes/default/static/add.css +archivebox/themes/default/static/admin.css +archivebox/themes/default/static/archive.png +archivebox/themes/default/static/bootstrap.min.css +archivebox/themes/default/static/external.png +archivebox/themes/default/static/jquery.dataTables.min.css +archivebox/themes/default/static/jquery.dataTables.min.js +archivebox/themes/default/static/jquery.min.js +archivebox/themes/default/static/sort_asc.png +archivebox/themes/default/static/sort_both.png +archivebox/themes/default/static/sort_desc.png +archivebox/themes/default/static/spinner.gif +archivebox/themes/legacy/main_index.html +archivebox/themes/legacy/main_index_row.html +archivebox/vendor/__init__.py \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox.egg-info/dependency_links.txt b/archivebox-0.5.3/archivebox.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/archivebox-0.5.3/archivebox.egg-info/entry_points.txt b/archivebox-0.5.3/archivebox.egg-info/entry_points.txt new file mode 100644 index 0000000..14fdb7e --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +archivebox = archivebox.cli:main + diff --git a/archivebox-0.5.3/archivebox.egg-info/requires.txt b/archivebox-0.5.3/archivebox.egg-info/requires.txt new file mode 100644 index 0000000..64d30de --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/requires.txt @@ -0,0 +1,26 @@ +atomicwrites==1.4.0 +croniter==0.3.34 +dateparser +django-extensions==3.0.3 +django==3.1.3 +ipython +mypy-extensions==0.4.3 +python-crontab==2.5.1 +requests==2.24.0 +w3lib==1.22.0 +youtube-dl + +[dev] +bottle +django-stubs +flake8 +ipdb +mypy +pytest +recommonmark +setuptools +sphinx +sphinx-rtd-theme +stdeb +twine +wheel diff --git a/archivebox-0.5.3/archivebox.egg-info/top_level.txt b/archivebox-0.5.3/archivebox.egg-info/top_level.txt new file mode 100644 index 0000000..74056b6 --- /dev/null +++ b/archivebox-0.5.3/archivebox.egg-info/top_level.txt @@ -0,0 +1 @@ +archivebox diff --git a/archivebox-0.5.3/archivebox/.flake8 b/archivebox-0.5.3/archivebox/.flake8 new file mode 100644 index 0000000..dd6ba8e --- /dev/null +++ b/archivebox-0.5.3/archivebox/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = D100,D101,D102,D103,D104,D105,D202,D203,D205,D400,E131,E241,E252,E266,E272,E701,E731,W293,W503,W291,W391 +select = F,E9,W +max-line-length = 130 +max-complexity = 10 +exclude = migrations,tests,node_modules,vendor,static,venv,.venv,.venv2,.docker-venv diff --git a/archivebox-0.5.3/archivebox/LICENSE b/archivebox-0.5.3/archivebox/LICENSE new file mode 100644 index 0000000..ea201f9 --- /dev/null +++ b/archivebox-0.5.3/archivebox/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Nick Sweeting + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/archivebox-0.5.3/archivebox/README.md b/archivebox-0.5.3/archivebox/README.md new file mode 100644 index 0000000..2e35783 --- /dev/null +++ b/archivebox-0.5.3/archivebox/README.md @@ -0,0 +1,545 @@ +
+ +

ArchiveBox
The open-source self-hosted web archive.

+ +▶️ Quickstart | +Demo | +Github | +Documentation | +Info & Motivation | +Community | +Roadmap + +
+"Your own personal internet archive" (网站存档 / 爬虫)
+
+ + + + + + + + + + +
+
+ +ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + +Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + +The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + +### Quickstart + +It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + +```bash +pip3 install archivebox +archivebox --version +# install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + +mkdir ~/archivebox && cd ~/archivebox # this can be anywhere +archivebox init + +archivebox add 'https://example.com' +archivebox add --depth=1 'https://example.com' +archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all +archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ +archivebox help # to see more options +``` + +*(click to expand the sections below for full setup instructions)* + +
+Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + +First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

+This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml +docker-compose run archivebox init +docker-compose run archivebox --version + +# start the webserver and open the UI (optional) +docker-compose run archivebox manage createsuperuser +docker-compose up -d +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker-compose run archivebox add 'https://example.com' +docker-compose run archivebox status +docker-compose run archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with docker on any platform + +First make sure you have Docker installed: https://docs.docker.com/get-docker/
+```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +docker run -v $PWD:/data -it archivebox/archivebox init +docker run -v $PWD:/data -it archivebox/archivebox --version + +# start the webserver and open the UI (optional) +docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser +docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' +docker run -v $PWD:/data -it archivebox/archivebox status +docker run -v $PWD:/data -it archivebox/archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with apt on Ubuntu >=20.04 + +```bash +sudo add-apt-repository -u ppa:archivebox/archivebox +sudo apt install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: +```bash +deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +``` +(you may need to install some other dependencies manually however) + +
+ +
+Get ArchiveBox with brew on macOS >=10.13 + +```bash +brew install archivebox/archivebox/archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
+ +
+Get ArchiveBox with pip on any platform + +```bash +pip3 install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version +# Install any missing extras like wget/git/chrome/etc. manually as needed + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
+ +--- + +
+ +
+ +DEMO: archivebox.zervice.io/ +For more information, see the full Quickstart guide, Usage, and Configuration docs. +
+ +--- + + +# Overview + +ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + +To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + +The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + +At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
+CLI Screenshot +Desktop index screenshot +Desktop details page Screenshot +Desktop details page Screenshot
+Demo | Usage | Screenshots +
+. . . . . . . . . . . . . . . . . . . . . . . . . . . . +

+ + +## Key Features + +- [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally +- [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) +- [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) +- Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** +- Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC +- ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) +- **Doesn't require a constantly-running daemon**, proxy, or native app +- Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) +- Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. +- Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + +## Input formats + +ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + +```bash +echo 'http://example.com' | archivebox add +archivebox add 'https://example.com/some/page' +archivebox add < ~/Downloads/firefox_bookmarks_export.html +archivebox add < any_text_with_urls_in_it.txt +archivebox add --depth=1 'https://example.com/some/downloads.html' +archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' +``` + +- Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) +- RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format +- Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + +See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + +It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + +## Output formats + +All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + +The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + +```bash + ls ./archive// +``` + +- **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details +- **Title:** `title` title of the site +- **Favicon:** `favicon.ico` favicon of the site +- **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file +- **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile +- **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present +- **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving +- **PDF:** `output.pdf` Printed PDF of site using headless chrome +- **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome +- **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome +- **Readability:** `article.html/json` Article text extraction using Readability +- **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org +- **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl +- **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links +- _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + +It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + +## Dependencies + +You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + +If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + +ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + +## Caveats + +If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. +```bash +# don't do this: +archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' +archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + +# without first disabling share the URL with 3rd party APIs: +archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org +archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL +archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google +``` + +Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. +```bash +# visiting an archived page with malicious JS: +https://127.0.0.1:8000/archive/1602401954/example.com/index.html + +# example.com/index.js can now make a request to read everything: +https://127.0.0.1:8000/index.html +https://127.0.0.1:8000/archive/* +# then example.com/index.js can send it off to some evil server +``` + +Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: +```bash +archivebox add 'https://example.com#2020-10-24' +... +archivebox add 'https://example.com#2020-10-25' +``` + +--- + +
+ +
+ +--- + +# Background & Motivation + +Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + +Whether it's to resist censorship by saving articles before they get taken down or edited, or +just to save a collection of early 2010's flash games you love to play, having the tools to +archive internet content enables to you save the stuff you care most about before it disappears. + +
+
+ Image from WTF is Link Rot?...
+
+ +The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. +I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + +Because modern websites are complicated and often rely on dynamic content, +ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + +All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + +## Comparison to Other Projects + +▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + +comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + +#### User Interface & Intended Purpose + +ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + +#### Private Local Archives vs Centralized Public Archives + +Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + +#### Storage Requirements + +Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + +## Learn more + +Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + +- [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ +- Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. +- Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + +--- + +# Documentation + + + +We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + +You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + +## Getting Started + +- [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) +- [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) +- [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + +## Reference + +- [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) +- [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) +- [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) +- [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) +- [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) +- [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) +- [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) +- [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) +- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) +- [Python API](https://docs.archivebox.io/en/latest/modules.html) +- REST API (coming soon...) + +## More Info + +- [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) +- [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) +- [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) +- [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) +- [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + +--- + +# ArchiveBox Development + +All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + +### Setup the dev environment + +First, install the system dependencies from the "Bare Metal" section above. +Then you can clone the ArchiveBox repo and install +```python3 +git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox +git checkout master # or the branch you want to test +git submodule update --init --recursive +git pull --recurse-submodules + +# Install ArchiveBox + python dependencies +python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] +# or with pipenv: pipenv install --dev && pipenv shell + +# Install node dependencies +npm install + +# Optional: install extractor dependencies manually or with helper script +./bin/setup.sh + +# Optional: develop via docker by mounting the code dir into the container +# if you edit e.g. ./archivebox/core/models.py on the docker host, runserver +# inside the container will reload and pick up your changes +docker build . -t archivebox +docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload +``` + +### Common development tasks + +See the `./bin/` folder and read the source of the bash scripts within. +You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + +#### Run the linters + +```bash +./bin/lint.sh +``` +(uses `flake8` and `mypy`) + +#### Run the integration tests + +```bash +./bin/test.sh +``` +(uses `pytest -s`) + +#### Make migrations or enter a django shell + +```bash +cd archivebox/ +./manage.py makemigrations + +cd data/ +archivebox shell +``` +(uses `pytest -s`) + +#### Build the docs, pip package, and docker image + +```bash +./bin/build.sh + +# or individually: +./bin/build_docs.sh +./bin/build_pip.sh +./bin/build_deb.sh +./bin/build_brew.sh +./bin/build_docker.sh +``` + +#### Roll a release + +```bash +./bin/release.sh +``` +(bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + +--- + +
+

+ +
+This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

+ +
+Sponsor us on Github +
+
+ +
+ + + + +

+ +
diff --git a/archivebox-0.5.3/archivebox/__init__.py b/archivebox-0.5.3/archivebox/__init__.py new file mode 100644 index 0000000..b0c00b6 --- /dev/null +++ b/archivebox-0.5.3/archivebox/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox' diff --git a/archivebox-0.5.3/archivebox/__main__.py b/archivebox-0.5.3/archivebox/__main__.py new file mode 100755 index 0000000..8afaa27 --- /dev/null +++ b/archivebox-0.5.3/archivebox/__main__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox' + +import sys + +from .cli import main + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/__init__.py b/archivebox-0.5.3/archivebox/cli/__init__.py new file mode 100644 index 0000000..f9a55ef --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/__init__.py @@ -0,0 +1,144 @@ +__package__ = 'archivebox.cli' +__command__ = 'archivebox' + +import os +import sys +import argparse + +from typing import Optional, Dict, List, IO, Union +from pathlib import Path + +from ..config import OUTPUT_DIR + +from importlib import import_module + +CLI_DIR = Path(__file__).resolve().parent + +# these common commands will appear sorted before any others for ease-of-use +meta_cmds = ('help', 'version') +main_cmds = ('init', 'info', 'config') +archive_cmds = ('add', 'remove', 'update', 'list', 'status') + +fake_db = ("oneshot",) + +display_first = (*meta_cmds, *main_cmds, *archive_cmds) + +# every imported command module must have these properties in order to be valid +required_attrs = ('__package__', '__command__', 'main') + +# basic checks to make sure imported files are valid subcommands +is_cli_module = lambda fname: fname.startswith('archivebox_') and fname.endswith('.py') +is_valid_cli_module = lambda module, subcommand: ( + all(hasattr(module, attr) for attr in required_attrs) + and module.__command__.split(' ')[-1] == subcommand +) + + +def list_subcommands() -> Dict[str, str]: + """find and import all valid archivebox_.py files in CLI_DIR""" + + COMMANDS = [] + for filename in os.listdir(CLI_DIR): + if is_cli_module(filename): + subcommand = filename.replace('archivebox_', '').replace('.py', '') + module = import_module('.archivebox_{}'.format(subcommand), __package__) + assert is_valid_cli_module(module, subcommand) + COMMANDS.append((subcommand, module.main.__doc__)) + globals()[subcommand] = module.main + + display_order = lambda cmd: ( + display_first.index(cmd[0]) + if cmd[0] in display_first else + 100 + len(cmd[0]) + ) + + return dict(sorted(COMMANDS, key=display_order)) + + +def run_subcommand(subcommand: str, + subcommand_args: List[str]=None, + stdin: Optional[IO]=None, + pwd: Union[Path, str, None]=None) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + + if subcommand not in meta_cmds: + from ..config import setup_django + setup_django(in_memory_db=subcommand in fake_db, check_db=subcommand in archive_cmds) + + module = import_module('.archivebox_{}'.format(subcommand), __package__) + module.main(args=subcommand_args, stdin=stdin, pwd=pwd) # type: ignore + + +SUBCOMMANDS = list_subcommands() + +class NotProvided: + pass + + +def main(args: Optional[List[str]]=NotProvided, stdin: Optional[IO]=NotProvided, pwd: Optional[str]=None) -> None: + args = sys.argv[1:] if args is NotProvided else args + stdin = sys.stdin if stdin is NotProvided else stdin + + subcommands = list_subcommands() + parser = argparse.ArgumentParser( + prog=__command__, + description='ArchiveBox: The self-hosted internet archive', + add_help=False, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--help', '-h', + action='store_true', + help=subcommands['help'], + ) + group.add_argument( + '--version', + action='store_true', + help=subcommands['version'], + ) + group.add_argument( + "subcommand", + type=str, + help= "The name of the subcommand to run", + nargs='?', + choices=subcommands.keys(), + default=None, + ) + parser.add_argument( + "subcommand_args", + help="Arguments for the subcommand", + nargs=argparse.REMAINDER, + ) + command = parser.parse_args(args or ()) + + if command.version: + command.subcommand = 'version' + elif command.help or command.subcommand is None: + command.subcommand = 'help' + + if command.subcommand not in ('help', 'version', 'status'): + from ..logging_util import log_cli_command + + log_cli_command( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR + ) + + run_subcommand( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR, + ) + + +__all__ = ( + 'SUBCOMMANDS', + 'list_subcommands', + 'run_subcommand', + *SUBCOMMANDS.keys(), +) + + diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_add.py b/archivebox-0.5.3/archivebox/cli/archivebox_add.py new file mode 100644 index 0000000..41c7554 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_add.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox add' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import add +from ..util import docstring +from ..config import OUTPUT_DIR, ONLY_NEW +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(add.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=add.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--update-all', #'-n', + action='store_true', + default=not ONLY_NEW, # when ONLY_NEW=True we skip updating old links + help="Also retry previously skipped/failed links when adding new links", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Add the links to the main index without archiving them", + ) + parser.add_argument( + 'urls', + nargs='*', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--depth", + action="store", + default=0, + choices=[0, 1], + type=int, + help="Recursively archive all linked pages up to this many hops away" + ) + parser.add_argument( + "--overwrite", + default=False, + action="store_true", + help="Re-archive URLs from scratch, overwriting any existing files" + ) + parser.add_argument( + "--init", #'-i', + action='store_true', + help="Init/upgrade the curent data directory before adding", + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + urls = command.urls + stdin_urls = accept_stdin(stdin) + if (stdin_urls and urls) or (not stdin and not urls): + stderr( + '[X] You must pass URLs/paths to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + add( + urls=stdin_urls or urls, + depth=command.depth, + update_all=command.update_all, + index_only=command.index_only, + overwrite=command.overwrite, + init=command.init, + extractors=command.extract, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) + + +# TODO: Implement these +# +# parser.add_argument( +# '--mirror', #'-m', +# action='store_true', +# help='Archive an entire site (finding all linked pages below it on the same domain)', +# ) +# parser.add_argument( +# '--crawler', #'-r', +# choices=('depth_first', 'breadth_first'), +# help='Controls which crawler to use in order to find outlinks in a given page', +# default=None, +# ) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_config.py b/archivebox-0.5.3/archivebox/cli/archivebox_config.py new file mode 100644 index 0000000..f81286c --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_config.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox config' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import config +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(config.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=config.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--get', #'-g', + action='store_true', + help="Get the value for the given config KEYs", + ) + group.add_argument( + '--set', #'-s', + action='store_true', + help="Set the given KEY=VALUE config values", + ) + group.add_argument( + '--reset', #'-s', + action='store_true', + help="Reset the given KEY config values to their defaults", + ) + parser.add_argument( + 'config_options', + nargs='*', + type=str, + help='KEY or KEY=VALUE formatted config values to get or set', + ) + command = parser.parse_args(args or ()) + config_options_str = accept_stdin(stdin) + + config( + config_options_str=config_options_str, + config_options=command.config_options, + get=command.get, + set=command.set, + reset=command.reset, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_help.py b/archivebox-0.5.3/archivebox/cli/archivebox_help.py new file mode 100755 index 0000000..46f17cb --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_help.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox help' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import help +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(help.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=help.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + help(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_init.py b/archivebox-0.5.3/archivebox/cli/archivebox_init.py new file mode 100755 index 0000000..6255ef2 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_init.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox init' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import init +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(init.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=init.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--force', # '-f', + action='store_true', + help='Ignore unrecognized files in current directory and initialize anyway', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + init( + force=command.force, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_list.py b/archivebox-0.5.3/archivebox/cli/archivebox_list.py new file mode 100644 index 0000000..3838cf6 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_list.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox list' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import list_all +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(list_all.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=list_all.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--csv', #'-c', + type=str, + help="Print the output in CSV format with the given columns, e.g.: timestamp,url,extension", + default=None, + ) + group.add_argument( + '--json', #'-j', + action='store_true', + help="Print the output in JSON format with all columns included.", + ) + group.add_argument( + '--html', + action='store_true', + help="Print the output in HTML format" + ) + parser.add_argument( + '--with-headers', + action='store_true', + help='Include the headers in the output document' + ) + parser.add_argument( + '--sort', #'-s', + type=str, + help="List the links sorted using the given key, e.g. timestamp or updated.", + default=None, + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'List only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='List only URLs matching these filter patterns.' + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + if command.with_headers and not (command.json or command.html or command.csv): + stderr( + '[X] --with-headers can only be used with --json, --html or --csv options.\n', + color='red', + ) + raise SystemExit(2) + + matching_folders = list_all( + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + sort=command.sort, + csv=command.csv, + json=command.json, + html=command.html, + with_headers=command.with_headers, + out_dir=pwd or OUTPUT_DIR, + ) + raise SystemExit(not matching_folders) + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_manage.py b/archivebox-0.5.3/archivebox/cli/archivebox_manage.py new file mode 100644 index 0000000..f05604e --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox manage' + +import sys + +from typing import Optional, List, IO + +from ..main import manage +from ..util import docstring +from ..config import OUTPUT_DIR + + +@docstring(manage.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + manage( + args=args, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_oneshot.py b/archivebox-0.5.3/archivebox/cli/archivebox_oneshot.py new file mode 100644 index 0000000..af68bac --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_oneshot.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox oneshot' + +import sys +import argparse + +from pathlib import Path +from typing import List, Optional, IO + +from ..main import oneshot +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(oneshot.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=oneshot.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'url', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + parser.add_argument( + '--out-dir', + type=str, + default=OUTPUT_DIR, + help= "Path to save the single archive folder to, e.g. ./example.com_archive" + ) + command = parser.parse_args(args or ()) + url = command.url + stdin_url = accept_stdin(stdin) + if (stdin_url and url) or (not stdin and not url): + stderr( + '[X] You must pass a URL/path to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + + oneshot( + url=stdin_url or url, + out_dir=Path(command.out_dir).resolve(), + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_remove.py b/archivebox-0.5.3/archivebox/cli/archivebox_remove.py new file mode 100644 index 0000000..cb073e9 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_remove.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox remove' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import remove +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(remove.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=remove.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--yes', # '-y', + action='store_true', + help='Remove links instantly without prompting to confirm.', + ) + parser.add_argument( + '--delete', # '-r', + action='store_true', + help=( + "In addition to removing the link from the index, " + "also delete its archived content and metadata folder." + ), + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only URLs bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only URLs bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex','tag'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + help='URLs matching this filter pattern will be removed from the index.' + ) + command = parser.parse_args(args or ()) + filter_str = accept_stdin(stdin) + + remove( + filter_str=filter_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + before=command.before, + after=command.after, + yes=command.yes, + delete=command.delete, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_schedule.py b/archivebox-0.5.3/archivebox/cli/archivebox_schedule.py new file mode 100644 index 0000000..ec5e914 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_schedule.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox schedule' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import schedule +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(schedule.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=schedule.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help=("Don't warn about storage space."), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--add', # '-a', + action='store_true', + help='Add a new scheduled ArchiveBox update job to cron', + ) + parser.add_argument( + '--every', # '-e', + type=str, + default=None, + help='Run ArchiveBox once every [timeperiod] (hour/day/month/year or cron format e.g. "0 0 * * *")', + ) + parser.add_argument( + '--depth', # '-d', + type=int, + default=0, + help='Depth to archive to [0] or 1, see "add" command help for more info.', + ) + group.add_argument( + '--clear', # '-c' + action='store_true', + help=("Stop all ArchiveBox scheduled runs (remove cron jobs)"), + ) + group.add_argument( + '--show', # '-s' + action='store_true', + help=("Print a list of currently active ArchiveBox cron jobs"), + ) + group.add_argument( + '--foreground', '-f', + action='store_true', + help=("Launch ArchiveBox scheduler as a long-running foreground task " + "instead of using cron."), + ) + group.add_argument( + '--run-all', # '-a', + action='store_true', + help=("Run all the scheduled jobs once immediately, independent of " + "their configured schedules, can be used together with --foreground"), + ) + parser.add_argument( + 'import_path', + nargs='?', + type=str, + default=None, + help=("Check this path and import any new links on every run " + "(can be either local file or remote URL)"), + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + schedule( + add=command.add, + show=command.show, + clear=command.clear, + foreground=command.foreground, + run_all=command.run_all, + quiet=command.quiet, + every=command.every, + depth=command.depth, + import_path=command.import_path, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_server.py b/archivebox-0.5.3/archivebox/cli/archivebox_server.py new file mode 100644 index 0000000..dbacf7e --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_server.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox server' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import server +from ..util import docstring +from ..config import OUTPUT_DIR, BIND_ADDR +from ..logging_util import SmartFormatter, reject_stdin + +@docstring(server.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=server.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'runserver_args', + nargs='*', + type=str, + default=[BIND_ADDR], + help='Arguments to pass to Django runserver' + ) + parser.add_argument( + '--reload', + action='store_true', + help='Enable auto-reloading when code or templates change', + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable DEBUG=True mode with more verbose errors', + ) + parser.add_argument( + '--init', + action='store_true', + help='Run archivebox init before starting the server', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + server( + runserver_args=command.runserver_args, + reload=command.reload, + debug=command.debug, + init=command.init, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_shell.py b/archivebox-0.5.3/archivebox/cli/archivebox_shell.py new file mode 100644 index 0000000..bcd5fdd --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_shell.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox shell' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import shell +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(shell.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=shell.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + shell( + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_status.py b/archivebox-0.5.3/archivebox/cli/archivebox_status.py new file mode 100644 index 0000000..2bef19c --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_status.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox status' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import status +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(status.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=status.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + status(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_update.py b/archivebox-0.5.3/archivebox/cli/archivebox_update.py new file mode 100644 index 0000000..6748096 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_update.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox update' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import update +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(update.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=update.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--only-new', #'-n', + action='store_true', + help="Don't attempt to retry previously skipped/failed links when updating", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Update the main index without archiving any content", + ) + parser.add_argument( + '--resume', #'-r', + type=float, + help='Resume the update process from a given timestamp', + default=None, + ) + parser.add_argument( + '--overwrite', #'-x', + action='store_true', + help='Ignore existing archived content and overwrite with new versions (DANGEROUS)', + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="Update only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="Update only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'Update only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='Update only URLs matching these filter patterns.' + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + update( + resume=command.resume, + only_new=command.only_new, + index_only=command.index_only, + overwrite=command.overwrite, + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + out_dir=pwd or OUTPUT_DIR, + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/archivebox_version.py b/archivebox-0.5.3/archivebox/cli/archivebox_version.py new file mode 100755 index 0000000..e7922f3 --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/archivebox_version.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox version' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import version +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(version.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=version.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Only print ArchiveBox version number and nothing else.', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + version( + quiet=command.quiet, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/archivebox/cli/tests.py b/archivebox-0.5.3/archivebox/cli/tests.py new file mode 100755 index 0000000..4d7016a --- /dev/null +++ b/archivebox-0.5.3/archivebox/cli/tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' + + +import os +import sys +import shutil +import unittest +from pathlib import Path + +from contextlib import contextmanager + +TEST_CONFIG = { + 'USE_COLOR': 'False', + 'SHOW_PROGRESS': 'False', + + 'OUTPUT_DIR': 'data.tests', + + 'SAVE_ARCHIVE_DOT_ORG': 'False', + 'SAVE_TITLE': 'False', + + 'USE_CURL': 'False', + 'USE_WGET': 'False', + 'USE_GIT': 'False', + 'USE_CHROME': 'False', + 'USE_YOUTUBEDL': 'False', +} + +OUTPUT_DIR = 'data.tests' +os.environ.update(TEST_CONFIG) + +from ..main import init +from ..index import load_main_index +from ..config import ( + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, +) + +from . import ( + archivebox_init, + archivebox_add, + archivebox_remove, +) + +HIDE_CLI_OUTPUT = True + +test_urls = ''' +https://example1.com/what/is/happening.html?what=1#how-about-this=1 +https://example2.com/what/is/happening/?what=1#how-about-this=1 +HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f +https://example4.com/what/is/happening.html +https://example5.com/ +https://example6.com + +http://example7.com +[https://example8.com/what/is/this.php?what=1] +[and http://example9.com?what=1&other=3#and-thing=2] +https://example10.com#and-thing=2 " +abcdef +sdflkf[what](https://subb.example12.com/who/what.php?whoami=1#whatami=2)?am=hi +example13.bada +and example14.badb +htt://example15.badc +''' + +stdout = sys.stdout +stderr = sys.stderr + + +@contextmanager +def output_hidden(show_failing=True): + if not HIDE_CLI_OUTPUT: + yield + return + + sys.stdout = open('stdout.txt', 'w+') + sys.stderr = open('stderr.txt', 'w+') + try: + yield + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + except: + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + if show_failing: + with open('stdout.txt', 'r') as f: + print(f.read()) + with open('stderr.txt', 'r') as f: + print(f.read()) + raise + finally: + os.remove('stdout.txt') + os.remove('stderr.txt') + + +class TestInit(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_basic_init(self): + with output_hidden(): + archivebox_init.main([]) + + assert (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + assert len(load_main_index(out_dir=OUTPUT_DIR)) == 0 + + def test_conflicting_init(self): + with open(Path(OUTPUT_DIR) / 'test_conflict.txt', 'w+') as f: + f.write('test') + + try: + with output_hidden(show_failing=False): + archivebox_init.main([]) + assert False, 'Init should have exited with an exception' + except SystemExit: + pass + + assert not (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + try: + load_main_index(out_dir=OUTPUT_DIR) + assert False, 'load_main_index should raise an exception when no index is present' + except: + pass + + def test_no_dirty_state(self): + with output_hidden(): + init() + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + with output_hidden(): + init() + + +class TestAdd(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_add_arg_url(self): + with output_hidden(): + archivebox_add.main(['https://getpocket.com/users/nikisweeting/feed/all']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 30 + + def test_add_arg_file(self): + test_file = Path(OUTPUT_DIR) / 'test.txt' + with open(test_file, 'w+') as f: + f.write(test_urls) + + with output_hidden(): + archivebox_add.main([test_file]) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + os.remove(test_file) + + def test_add_stdin_url(self): + with output_hidden(): + archivebox_add.main([], stdin=test_urls) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + + +class TestRemove(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + archivebox_add.main([], stdin=test_urls) + + # def tearDown(self): + # shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + + def test_remove_exact(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', 'https://example5.com/']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 11 + + def test_remove_regex(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=regex', r'http(s)?:\/\/(.+\.)?(example\d\.com)']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 4 + + def test_remove_domain(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=domain', 'example5.com', 'example6.com']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 10 + + def test_remove_none(self): + try: + with output_hidden(show_failing=False): + archivebox_remove.main(['--yes', '--delete', 'https://doesntexist.com']) + assert False, 'Should raise if no URLs match' + except: + pass + + +if __name__ == '__main__': + if '--verbose' in sys.argv or '-v' in sys.argv: + HIDE_CLI_OUTPUT = False + + unittest.main() diff --git a/archivebox-0.5.3/archivebox/config.py b/archivebox-0.5.3/archivebox/config.py new file mode 100644 index 0000000..9a3f9a7 --- /dev/null +++ b/archivebox-0.5.3/archivebox/config.py @@ -0,0 +1,1081 @@ +""" +ArchiveBox config definitons (including defaults and dynamic config options). + +Config Usage Example: + + archivebox config --set MEDIA_TIMEOUT=600 + env MEDIA_TIMEOUT=600 USE_COLOR=False ... archivebox [subcommand] ... + +Config Precedence Order: + + 1. cli args (--update-all / --index-only / etc.) + 2. shell environment vars (env USE_COLOR=False archivebox add '...') + 3. config file (echo "SAVE_FAVICON=False" >> ArchiveBox.conf) + 4. defaults (defined below in Python) + +Documentation: + + https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + +""" + +__package__ = 'archivebox' + +import os +import io +import re +import sys +import json +import getpass +import shutil +import django + +from hashlib import md5 +from pathlib import Path +from typing import Optional, Type, Tuple, Dict, Union, List +from subprocess import run, PIPE, DEVNULL +from configparser import ConfigParser +from collections import defaultdict + +from .config_stubs import ( + SimpleConfigValueDict, + ConfigValue, + ConfigDict, + ConfigDefaultValue, + ConfigDefaultDict, +) + +############################### Config Schema ################################## + +CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { + 'SHELL_CONFIG': { + 'IS_TTY': {'type': bool, 'default': lambda _: sys.stdout.isatty()}, + 'USE_COLOR': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'SHOW_PROGRESS': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'IN_DOCKER': {'type': bool, 'default': False}, + # TODO: 'SHOW_HINTS': {'type: bool, 'default': True}, + }, + + 'GENERAL_CONFIG': { + 'OUTPUT_DIR': {'type': str, 'default': None}, + 'CONFIG_FILE': {'type': str, 'default': None}, + 'ONLY_NEW': {'type': bool, 'default': True}, + 'TIMEOUT': {'type': int, 'default': 60}, + 'MEDIA_TIMEOUT': {'type': int, 'default': 3600}, + 'OUTPUT_PERMISSIONS': {'type': str, 'default': '755'}, + 'RESTRICT_FILE_NAMES': {'type': str, 'default': 'windows'}, + 'URL_BLACKLIST': {'type': str, 'default': r'\.(css|js|otf|ttf|woff|woff2|gstatic\.com|googleapis\.com/css)(\?.*)?$'}, # to avoid downloading code assets as their own pages + }, + + 'SERVER_CONFIG': { + 'SECRET_KEY': {'type': str, 'default': None}, + 'BIND_ADDR': {'type': str, 'default': lambda c: ['127.0.0.1:8000', '0.0.0.0:8000'][c['IN_DOCKER']]}, + 'ALLOWED_HOSTS': {'type': str, 'default': '*'}, + 'DEBUG': {'type': bool, 'default': False}, + 'PUBLIC_INDEX': {'type': bool, 'default': True}, + 'PUBLIC_SNAPSHOTS': {'type': bool, 'default': True}, + 'PUBLIC_ADD_VIEW': {'type': bool, 'default': False}, + 'FOOTER_INFO': {'type': str, 'default': 'Content is hosted for personal archiving purposes only. Contact server owner for any takedown requests.'}, + 'ACTIVE_THEME': {'type': str, 'default': 'default'}, + }, + + 'ARCHIVE_METHOD_TOGGLES': { + 'SAVE_TITLE': {'type': bool, 'default': True, 'aliases': ('FETCH_TITLE',)}, + 'SAVE_FAVICON': {'type': bool, 'default': True, 'aliases': ('FETCH_FAVICON',)}, + 'SAVE_WGET': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET',)}, + 'SAVE_WGET_REQUISITES': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET_REQUISITES',)}, + 'SAVE_SINGLEFILE': {'type': bool, 'default': True, 'aliases': ('FETCH_SINGLEFILE',)}, + 'SAVE_READABILITY': {'type': bool, 'default': True, 'aliases': ('FETCH_READABILITY',)}, + 'SAVE_MERCURY': {'type': bool, 'default': True, 'aliases': ('FETCH_MERCURY',)}, + 'SAVE_PDF': {'type': bool, 'default': True, 'aliases': ('FETCH_PDF',)}, + 'SAVE_SCREENSHOT': {'type': bool, 'default': True, 'aliases': ('FETCH_SCREENSHOT',)}, + 'SAVE_DOM': {'type': bool, 'default': True, 'aliases': ('FETCH_DOM',)}, + 'SAVE_HEADERS': {'type': bool, 'default': True, 'aliases': ('FETCH_HEADERS',)}, + 'SAVE_WARC': {'type': bool, 'default': True, 'aliases': ('FETCH_WARC',)}, + 'SAVE_GIT': {'type': bool, 'default': True, 'aliases': ('FETCH_GIT',)}, + 'SAVE_MEDIA': {'type': bool, 'default': True, 'aliases': ('FETCH_MEDIA',)}, + 'SAVE_ARCHIVE_DOT_ORG': {'type': bool, 'default': True, 'aliases': ('SUBMIT_ARCHIVE_DOT_ORG',)}, + }, + + 'ARCHIVE_METHOD_OPTIONS': { + 'RESOLUTION': {'type': str, 'default': '1440,2000', 'aliases': ('SCREENSHOT_RESOLUTION',)}, + 'GIT_DOMAINS': {'type': str, 'default': 'github.com,bitbucket.org,gitlab.com'}, + 'CHECK_SSL_VALIDITY': {'type': bool, 'default': True}, + + 'CURL_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) curl/{CURL_VERSION}'}, + 'WGET_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) wget/{WGET_VERSION}'}, + 'CHROME_USER_AGENT': {'type': str, 'default': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}, + + 'COOKIES_FILE': {'type': str, 'default': None}, + 'CHROME_USER_DATA_DIR': {'type': str, 'default': None}, + + 'CHROME_HEADLESS': {'type': bool, 'default': True}, + 'CHROME_SANDBOX': {'type': bool, 'default': lambda c: not c['IN_DOCKER']}, + 'YOUTUBEDL_ARGS': {'type': list, 'default': ['--write-description', + '--write-info-json', + '--write-annotations', + '--write-thumbnail', + '--no-call-home', + '--user-agent', + '--all-subs', + '--extract-audio', + '--keep-video', + '--ignore-errors', + '--geo-bypass', + '--audio-format', 'mp3', + '--audio-quality', '320K', + '--embed-thumbnail', + '--add-metadata']}, + + 'WGET_ARGS': {'type': list, 'default': ['--no-verbose', + '--adjust-extension', + '--convert-links', + '--force-directories', + '--backup-converted', + '--span-hosts', + '--no-parent', + '-e', 'robots=off', + ]}, + 'CURL_ARGS': {'type': list, 'default': ['--silent', + '--location', + '--compressed' + ]}, + 'GIT_ARGS': {'type': list, 'default': ['--recursive']}, + }, + + 'SEARCH_BACKEND_CONFIG' : { + 'USE_INDEXING_BACKEND': {'type': bool, 'default': True}, + 'USE_SEARCHING_BACKEND': {'type': bool, 'default': True}, + 'SEARCH_BACKEND_ENGINE': {'type': str, 'default': 'ripgrep'}, + 'SEARCH_BACKEND_HOST_NAME': {'type': str, 'default': 'localhost'}, + 'SEARCH_BACKEND_PORT': {'type': int, 'default': 1491}, + 'SEARCH_BACKEND_PASSWORD': {'type': str, 'default': 'SecretPassword'}, + # SONIC + 'SONIC_COLLECTION': {'type': str, 'default': 'archivebox'}, + 'SONIC_BUCKET': {'type': str, 'default': 'snapshots'}, + }, + + 'DEPENDENCY_CONFIG': { + 'USE_CURL': {'type': bool, 'default': True}, + 'USE_WGET': {'type': bool, 'default': True}, + 'USE_SINGLEFILE': {'type': bool, 'default': True}, + 'USE_READABILITY': {'type': bool, 'default': True}, + 'USE_MERCURY': {'type': bool, 'default': True}, + 'USE_GIT': {'type': bool, 'default': True}, + 'USE_CHROME': {'type': bool, 'default': True}, + 'USE_NODE': {'type': bool, 'default': True}, + 'USE_YOUTUBEDL': {'type': bool, 'default': True}, + 'USE_RIPGREP': {'type': bool, 'default': True}, + + 'CURL_BINARY': {'type': str, 'default': 'curl'}, + 'GIT_BINARY': {'type': str, 'default': 'git'}, + 'WGET_BINARY': {'type': str, 'default': 'wget'}, + 'SINGLEFILE_BINARY': {'type': str, 'default': 'single-file'}, + 'READABILITY_BINARY': {'type': str, 'default': 'readability-extractor'}, + 'MERCURY_BINARY': {'type': str, 'default': 'mercury-parser'}, + 'YOUTUBEDL_BINARY': {'type': str, 'default': 'youtube-dl'}, + 'NODE_BINARY': {'type': str, 'default': 'node'}, + 'RIPGREP_BINARY': {'type': str, 'default': 'rg'}, + 'CHROME_BINARY': {'type': str, 'default': None}, + + 'POCKET_CONSUMER_KEY': {'type': str, 'default': None}, + 'POCKET_ACCESS_TOKENS': {'type': dict, 'default': {}}, + }, +} + + +########################## Backwards-Compatibility ############################# + + +# for backwards compatibility with old config files, check old/deprecated names for each key +CONFIG_ALIASES = { + alias: key + for section in CONFIG_SCHEMA.values() + for key, default in section.items() + for alias in default.get('aliases', ()) +} +USER_CONFIG = {key for section in CONFIG_SCHEMA.values() for key in section.keys()} + +def get_real_name(key: str) -> str: + """get the current canonical name for a given deprecated config key""" + return CONFIG_ALIASES.get(key.upper().strip(), key.upper().strip()) + + + +################################ Constants ##################################### + +PACKAGE_DIR_NAME = 'archivebox' +TEMPLATES_DIR_NAME = 'themes' + +ARCHIVE_DIR_NAME = 'archive' +SOURCES_DIR_NAME = 'sources' +LOGS_DIR_NAME = 'logs' +STATIC_DIR_NAME = 'static' +SQL_INDEX_FILENAME = 'index.sqlite3' +JSON_INDEX_FILENAME = 'index.json' +HTML_INDEX_FILENAME = 'index.html' +ROBOTS_TXT_FILENAME = 'robots.txt' +FAVICON_FILENAME = 'favicon.ico' +CONFIG_FILENAME = 'ArchiveBox.conf' + +DEFAULT_CLI_COLORS = { + 'reset': '\033[00;00m', + 'lightblue': '\033[01;30m', + 'lightyellow': '\033[01;33m', + 'lightred': '\033[01;35m', + 'red': '\033[01;31m', + 'green': '\033[01;32m', + 'blue': '\033[01;34m', + 'white': '\033[01;37m', + 'black': '\033[01;30m', +} +ANSI = {k: '' for k in DEFAULT_CLI_COLORS.keys()} + +COLOR_DICT = defaultdict(lambda: [(0, 0, 0), (0, 0, 0)], { + '00': [(0, 0, 0), (0, 0, 0)], + '30': [(0, 0, 0), (0, 0, 0)], + '31': [(255, 0, 0), (128, 0, 0)], + '32': [(0, 200, 0), (0, 128, 0)], + '33': [(255, 255, 0), (128, 128, 0)], + '34': [(0, 0, 255), (0, 0, 128)], + '35': [(255, 0, 255), (128, 0, 128)], + '36': [(0, 255, 255), (0, 128, 128)], + '37': [(255, 255, 255), (255, 255, 255)], +}) + +STATICFILE_EXTENSIONS = { + # 99.999% of the time, URLs ending in these extensions are static files + # that can be downloaded as-is, not html pages that need to be rendered + 'gif', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'wbmp', 'ico', 'jng', 'bmp', + 'svg', 'svgz', 'webp', 'ps', 'eps', 'ai', + 'mp3', 'mp4', 'm4a', 'mpeg', 'mpg', 'mkv', 'mov', 'webm', 'm4v', + 'flv', 'wmv', 'avi', 'ogg', 'ts', 'm3u8', + 'pdf', 'txt', 'rtf', 'rtfd', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', + 'atom', 'rss', 'css', 'js', 'json', + 'dmg', 'iso', 'img', + 'rar', 'war', 'hqx', 'zip', 'gz', 'bz2', '7z', + + # Less common extensions to consider adding later + # jar, swf, bin, com, exe, dll, deb + # ear, hqx, eot, wmlc, kml, kmz, cco, jardiff, jnlp, run, msi, msp, msm, + # pl pm, prc pdb, rar, rpm, sea, sit, tcl tk, der, pem, crt, xpi, xspf, + # ra, mng, asx, asf, 3gpp, 3gp, mid, midi, kar, jad, wml, htc, mml + + # These are always treated as pages, not as static files, never add them: + # html, htm, shtml, xhtml, xml, aspx, php, cgi +} + + + +############################## Derived Config ################################## + + +DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { + 'TERM_WIDTH': {'default': lambda c: lambda: shutil.get_terminal_size((100, 10)).columns}, + 'USER': {'default': lambda c: getpass.getuser() or os.getlogin()}, + 'ANSI': {'default': lambda c: DEFAULT_CLI_COLORS if c['USE_COLOR'] else {k: '' for k in DEFAULT_CLI_COLORS.keys()}}, + + 'PACKAGE_DIR': {'default': lambda c: Path(__file__).resolve().parent}, + 'TEMPLATES_DIR': {'default': lambda c: c['PACKAGE_DIR'] / TEMPLATES_DIR_NAME}, + + 'OUTPUT_DIR': {'default': lambda c: Path(c['OUTPUT_DIR']).resolve() if c['OUTPUT_DIR'] else Path(os.curdir).resolve()}, + 'ARCHIVE_DIR': {'default': lambda c: c['OUTPUT_DIR'] / ARCHIVE_DIR_NAME}, + 'SOURCES_DIR': {'default': lambda c: c['OUTPUT_DIR'] / SOURCES_DIR_NAME}, + 'LOGS_DIR': {'default': lambda c: c['OUTPUT_DIR'] / LOGS_DIR_NAME}, + 'CONFIG_FILE': {'default': lambda c: Path(c['CONFIG_FILE']).resolve() if c['CONFIG_FILE'] else c['OUTPUT_DIR'] / CONFIG_FILENAME}, + 'COOKIES_FILE': {'default': lambda c: c['COOKIES_FILE'] and Path(c['COOKIES_FILE']).resolve()}, + 'CHROME_USER_DATA_DIR': {'default': lambda c: find_chrome_data_dir() if c['CHROME_USER_DATA_DIR'] is None else (Path(c['CHROME_USER_DATA_DIR']).resolve() if c['CHROME_USER_DATA_DIR'] else None)}, # None means unset, so we autodetect it with find_chrome_Data_dir(), but emptystring '' means user manually set it to '', and we should store it as None + 'URL_BLACKLIST_PTN': {'default': lambda c: c['URL_BLACKLIST'] and re.compile(c['URL_BLACKLIST'] or '', re.IGNORECASE | re.UNICODE | re.MULTILINE)}, + + 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0]}, + 'VERSION': {'default': lambda c: json.loads((Path(c['PACKAGE_DIR']) / 'package.json').read_text().strip())['version']}, + 'GIT_SHA': {'default': lambda c: c['VERSION'].split('+')[-1] or 'unknown'}, + + 'PYTHON_BINARY': {'default': lambda c: sys.executable}, + 'PYTHON_ENCODING': {'default': lambda c: sys.stdout.encoding.upper()}, + 'PYTHON_VERSION': {'default': lambda c: '{}.{}.{}'.format(*sys.version_info[:3])}, + + 'DJANGO_BINARY': {'default': lambda c: django.__file__.replace('__init__.py', 'bin/django-admin.py')}, + 'DJANGO_VERSION': {'default': lambda c: '{}.{}.{} {} ({})'.format(*django.VERSION)}, + + 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['SAVE_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, + 'CURL_VERSION': {'default': lambda c: bin_version(c['CURL_BINARY']) if c['USE_CURL'] else None}, + 'CURL_USER_AGENT': {'default': lambda c: c['CURL_USER_AGENT'].format(**c)}, + 'CURL_ARGS': {'default': lambda c: c['CURL_ARGS'] or []}, + 'SAVE_FAVICON': {'default': lambda c: c['USE_CURL'] and c['SAVE_FAVICON']}, + 'SAVE_ARCHIVE_DOT_ORG': {'default': lambda c: c['USE_CURL'] and c['SAVE_ARCHIVE_DOT_ORG']}, + + 'USE_WGET': {'default': lambda c: c['USE_WGET'] and (c['SAVE_WGET'] or c['SAVE_WARC'])}, + 'WGET_VERSION': {'default': lambda c: bin_version(c['WGET_BINARY']) if c['USE_WGET'] else None}, + 'WGET_AUTO_COMPRESSION': {'default': lambda c: wget_supports_compression(c) if c['USE_WGET'] else False}, + 'WGET_USER_AGENT': {'default': lambda c: c['WGET_USER_AGENT'].format(**c)}, + 'SAVE_WGET': {'default': lambda c: c['USE_WGET'] and c['SAVE_WGET']}, + 'SAVE_WARC': {'default': lambda c: c['USE_WGET'] and c['SAVE_WARC']}, + 'WGET_ARGS': {'default': lambda c: c['WGET_ARGS'] or []}, + + 'RIPGREP_VERSION': {'default': lambda c: bin_version(c['RIPGREP_BINARY']) if c['USE_RIPGREP'] else None}, + + 'USE_SINGLEFILE': {'default': lambda c: c['USE_SINGLEFILE'] and c['SAVE_SINGLEFILE']}, + 'SINGLEFILE_VERSION': {'default': lambda c: bin_version(c['SINGLEFILE_BINARY']) if c['USE_SINGLEFILE'] else None}, + + 'USE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['SAVE_READABILITY']}, + 'READABILITY_VERSION': {'default': lambda c: bin_version(c['READABILITY_BINARY']) if c['USE_READABILITY'] else None}, + + 'USE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['SAVE_MERCURY']}, + 'MERCURY_VERSION': {'default': lambda c: '1.0.0' if shutil.which(str(bin_path(c['MERCURY_BINARY']))) else None}, # mercury is unversioned + + 'USE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + 'GIT_VERSION': {'default': lambda c: bin_version(c['GIT_BINARY']) if c['USE_GIT'] else None}, + 'SAVE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + + 'USE_YOUTUBEDL': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_VERSION': {'default': lambda c: bin_version(c['YOUTUBEDL_BINARY']) if c['USE_YOUTUBEDL'] else None}, + 'SAVE_MEDIA': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_ARGS': {'default': lambda c: c['YOUTUBEDL_ARGS'] or []}, + + 'USE_CHROME': {'default': lambda c: c['USE_CHROME'] and (c['SAVE_PDF'] or c['SAVE_SCREENSHOT'] or c['SAVE_DOM'] or c['SAVE_SINGLEFILE'])}, + 'CHROME_BINARY': {'default': lambda c: c['CHROME_BINARY'] if c['CHROME_BINARY'] else find_chrome_binary()}, + 'CHROME_VERSION': {'default': lambda c: bin_version(c['CHROME_BINARY']) if c['USE_CHROME'] else None}, + + 'SAVE_PDF': {'default': lambda c: c['USE_CHROME'] and c['SAVE_PDF']}, + 'SAVE_SCREENSHOT': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SCREENSHOT']}, + 'SAVE_DOM': {'default': lambda c: c['USE_CHROME'] and c['SAVE_DOM']}, + 'SAVE_SINGLEFILE': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SINGLEFILE'] and c['USE_NODE']}, + 'SAVE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['USE_NODE']}, + 'SAVE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['USE_NODE']}, + + 'USE_NODE': {'default': lambda c: c['USE_NODE'] and (c['SAVE_READABILITY'] or c['SAVE_SINGLEFILE'] or c['SAVE_MERCURY'])}, + 'NODE_VERSION': {'default': lambda c: bin_version(c['NODE_BINARY']) if c['USE_NODE'] else None}, + + 'DEPENDENCIES': {'default': lambda c: get_dependency_info(c)}, + 'CODE_LOCATIONS': {'default': lambda c: get_code_locations(c)}, + 'EXTERNAL_LOCATIONS': {'default': lambda c: get_external_locations(c)}, + 'DATA_LOCATIONS': {'default': lambda c: get_data_locations(c)}, + 'CHROME_OPTIONS': {'default': lambda c: get_chrome_info(c)}, +} + + + +################################### Helpers #################################### + + +def load_config_val(key: str, + default: ConfigDefaultValue=None, + type: Optional[Type]=None, + aliases: Optional[Tuple[str, ...]]=None, + config: Optional[ConfigDict]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigValue: + """parse bool, int, and str key=value pairs from env""" + + + config_keys_to_check = (key, *(aliases or ())) + for key in config_keys_to_check: + if env_vars: + val = env_vars.get(key) + if val: + break + if config_file_vars: + val = config_file_vars.get(key) + if val: + break + + if type is None or val is None: + if callable(default): + assert isinstance(config, dict) + return default(config) + + return default + + elif type is bool: + if val.lower() in ('true', 'yes', '1'): + return True + elif val.lower() in ('false', 'no', '0'): + return False + else: + raise ValueError(f'Invalid configuration option {key}={val} (expected a boolean: True/False)') + + elif type is str: + if val.lower() in ('true', 'false', 'yes', 'no', '1', '0'): + raise ValueError(f'Invalid configuration option {key}={val} (expected a string)') + return val.strip() + + elif type is int: + if not val.isdigit(): + raise ValueError(f'Invalid configuration option {key}={val} (expected an integer)') + return int(val) + + elif type is list or type is dict: + return json.loads(val) + + raise Exception('Config values can only be str, bool, int or json') + + +def load_config_file(out_dir: str=None) -> Optional[Dict[str, str]]: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + if config_path.exists(): + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + # flatten into one namespace + config_file_vars = { + key.upper(): val + for section, options in config_file.items() + for key, val in options.items() + } + # print('[i] Loaded config file', os.path.abspath(config_path)) + # print(config_file_vars) + return config_file_vars + return None + + +def write_config_file(config: Dict[str, str], out_dir: str=None) -> ConfigDict: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + from .system import atomic_write + + CONFIG_HEADER = ( + """# This is the config file for your ArchiveBox collection. + # + # You can add options here manually in INI format, or automatically by running: + # archivebox config --set KEY=VALUE + # + # If you modify this file manually, make sure to update your archive after by running: + # archivebox init + # + # A list of all possible config with documentation and examples can be found here: + # https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + + """) + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + + if not config_path.exists(): + atomic_write(config_path, CONFIG_HEADER) + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + + with open(config_path, 'r') as old: + atomic_write(f'{config_path}.bak', old.read()) + + find_section = lambda key: [name for name, opts in CONFIG_SCHEMA.items() if key in opts][0] + + # Set up sections in empty config file + for key, val in config.items(): + section = find_section(key) + if section in config_file: + existing_config = dict(config_file[section]) + else: + existing_config = {} + config_file[section] = {**existing_config, key: val} + + # always make sure there's a SECRET_KEY defined for Django + existing_secret_key = None + if 'SERVER_CONFIG' in config_file and 'SECRET_KEY' in config_file['SERVER_CONFIG']: + existing_secret_key = config_file['SERVER_CONFIG']['SECRET_KEY'] + + if (not existing_secret_key) or ('not a valid secret' in existing_secret_key): + from django.utils.crypto import get_random_string + chars = 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.' + random_secret_key = get_random_string(50, chars) + if 'SERVER_CONFIG' in config_file: + config_file['SERVER_CONFIG']['SECRET_KEY'] = random_secret_key + else: + config_file['SERVER_CONFIG'] = {'SECRET_KEY': random_secret_key} + + with open(config_path, 'w+') as new: + config_file.write(new) + + try: + # validate the config by attempting to re-parse it + CONFIG = load_all_config() + return { + key.upper(): CONFIG.get(key.upper()) + for key in config.keys() + } + except: + # something went horribly wrong, rever to the previous version + with open(f'{config_path}.bak', 'r') as old: + atomic_write(config_path, old.read()) + + if Path(f'{config_path}.bak').exists(): + os.remove(f'{config_path}.bak') + + return {} + + + +def load_config(defaults: ConfigDefaultDict, + config: Optional[ConfigDict]=None, + out_dir: Optional[str]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigDict: + + env_vars = env_vars or os.environ + config_file_vars = config_file_vars or load_config_file(out_dir=out_dir) + + extended_config: ConfigDict = config.copy() if config else {} + for key, default in defaults.items(): + try: + extended_config[key] = load_config_val( + key, + default=default['default'], + type=default.get('type'), + aliases=default.get('aliases'), + config=extended_config, + env_vars=env_vars, + config_file_vars=config_file_vars, + ) + except KeyboardInterrupt: + raise SystemExit(0) + except Exception as e: + stderr() + stderr(f'[X] Error while loading configuration value: {key}', color='red', config=extended_config) + stderr(' {}: {}'.format(e.__class__.__name__, e)) + stderr() + stderr(' Check your config for mistakes and try again (your archive data is unaffected).') + stderr() + stderr(' For config documentation and examples see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration') + stderr() + raise + raise SystemExit(2) + + return extended_config + +# def write_config(config: ConfigDict): + +# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: + + +# Logging Helpers +def stdout(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stdout.write(prefix + ''.join(strs)) + +def stderr(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stderr.write(prefix + ''.join(strs)) + +def hint(text: Union[Tuple[str, ...], List[str], str], prefix=' ', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if isinstance(text, str): + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text, **ansi)) + else: + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text[0], **ansi)) + for line in text[1:]: + stderr('{} {}'.format(prefix, line)) + + +# Dependency Metadata Helpers +def bin_version(binary: Optional[str]) -> Optional[str]: + """check the presence and return valid version line of a specified binary""" + + abspath = bin_path(binary) + if not binary or not abspath: + return None + + try: + version_str = run([abspath, "--version"], stdout=PIPE).stdout.strip().decode() + # take first 3 columns of first line of version info + return ' '.join(version_str.split('\n')[0].strip().split()[:3]) + except OSError: + pass + # stderr(f'[X] Unable to find working version of dependency: {binary}', color='red') + # stderr(' Make sure it\'s installed, then confirm it\'s working by running:') + # stderr(f' {binary} --version') + # stderr() + # stderr(' If you don\'t want to install it, you can disable it via config. See here for more info:') + # stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Install') + return None + +def bin_path(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + + node_modules_bin = Path('.') / 'node_modules' / '.bin' / binary + if node_modules_bin.exists(): + return str(node_modules_bin.resolve()) + + return shutil.which(str(Path(binary).expanduser())) or shutil.which(str(binary)) or binary + +def bin_hash(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + abs_path = bin_path(binary) + if abs_path is None or not Path(abs_path).exists(): + return None + + file_hash = md5() + with io.open(abs_path, mode='rb') as f: + for chunk in iter(lambda: f.read(io.DEFAULT_BUFFER_SIZE), b''): + file_hash.update(chunk) + + return f'md5:{file_hash.hexdigest()}' + +def find_chrome_binary() -> Optional[str]: + """find any installed chrome binaries in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_executable_paths = ( + 'chromium-browser', + 'chromium', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + 'chrome', + 'google-chrome', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'google-chrome-stable', + 'google-chrome-beta', + 'google-chrome-canary', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'google-chrome-unstable', + 'google-chrome-dev', + ) + for name in default_executable_paths: + full_path_exists = shutil.which(name) + if full_path_exists: + return name + + return None + +def find_chrome_data_dir() -> Optional[str]: + """find any installed chrome user data directories in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_profile_paths = ( + '~/.config/chromium', + '~/Library/Application Support/Chromium', + '~/AppData/Local/Chromium/User Data', + '~/.config/chrome', + '~/.config/google-chrome', + '~/Library/Application Support/Google/Chrome', + '~/AppData/Local/Google/Chrome/User Data', + '~/.config/google-chrome-stable', + '~/.config/google-chrome-beta', + '~/Library/Application Support/Google/Chrome Canary', + '~/AppData/Local/Google/Chrome SxS/User Data', + '~/.config/google-chrome-unstable', + '~/.config/google-chrome-dev', + ) + for path in default_profile_paths: + full_path = Path(path).resolve() + if full_path.exists(): + return full_path + return None + +def wget_supports_compression(config): + try: + cmd = [ + config['WGET_BINARY'], + "--compression=auto", + "--help", + ] + return not run(cmd, stdout=DEVNULL, stderr=DEVNULL).returncode + except (FileNotFoundError, OSError): + return False + +def get_code_locations(config: ConfigDict) -> SimpleConfigValueDict: + return { + 'PACKAGE_DIR': { + 'path': (config['PACKAGE_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['PACKAGE_DIR'] / '__main__.py').exists(), + }, + 'TEMPLATES_DIR': { + 'path': (config['TEMPLATES_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['TEMPLATES_DIR'] / config['ACTIVE_THEME'] / 'static').exists(), + }, + # 'NODE_MODULES_DIR': { + # 'path': , + # 'enabled': , + # 'is_valid': (...).exists(), + # }, + } + +def get_external_locations(config: ConfigDict) -> ConfigValue: + abspath = lambda path: None if path is None else Path(path).resolve() + return { + 'CHROME_USER_DATA_DIR': { + 'path': abspath(config['CHROME_USER_DATA_DIR']), + 'enabled': config['USE_CHROME'] and config['CHROME_USER_DATA_DIR'], + 'is_valid': False if config['CHROME_USER_DATA_DIR'] is None else (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(), + }, + 'COOKIES_FILE': { + 'path': abspath(config['COOKIES_FILE']), + 'enabled': config['USE_WGET'] and config['COOKIES_FILE'], + 'is_valid': False if config['COOKIES_FILE'] is None else Path(config['COOKIES_FILE']).exists(), + }, + } + +def get_data_locations(config: ConfigDict) -> ConfigValue: + return { + 'OUTPUT_DIR': { + 'path': config['OUTPUT_DIR'].resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + 'SOURCES_DIR': { + 'path': config['SOURCES_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['SOURCES_DIR'].exists(), + }, + 'LOGS_DIR': { + 'path': config['LOGS_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['LOGS_DIR'].exists(), + }, + 'ARCHIVE_DIR': { + 'path': config['ARCHIVE_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['ARCHIVE_DIR'].exists(), + }, + 'CONFIG_FILE': { + 'path': config['CONFIG_FILE'].resolve(), + 'enabled': True, + 'is_valid': config['CONFIG_FILE'].exists(), + }, + 'SQL_INDEX': { + 'path': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + } + +def get_dependency_info(config: ConfigDict) -> ConfigValue: + return { + 'ARCHIVEBOX_BINARY': { + 'path': bin_path(config['ARCHIVEBOX_BINARY']), + 'version': config['VERSION'], + 'hash': bin_hash(config['ARCHIVEBOX_BINARY']), + 'enabled': True, + 'is_valid': True, + }, + 'PYTHON_BINARY': { + 'path': bin_path(config['PYTHON_BINARY']), + 'version': config['PYTHON_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'DJANGO_BINARY': { + 'path': bin_path(config['DJANGO_BINARY']), + 'version': config['DJANGO_VERSION'], + 'hash': bin_hash(config['DJANGO_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'CURL_BINARY': { + 'path': bin_path(config['CURL_BINARY']), + 'version': config['CURL_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': config['USE_CURL'], + 'is_valid': bool(config['CURL_VERSION']), + }, + 'WGET_BINARY': { + 'path': bin_path(config['WGET_BINARY']), + 'version': config['WGET_VERSION'], + 'hash': bin_hash(config['WGET_BINARY']), + 'enabled': config['USE_WGET'], + 'is_valid': bool(config['WGET_VERSION']), + }, + 'NODE_BINARY': { + 'path': bin_path(config['NODE_BINARY']), + 'version': config['NODE_VERSION'], + 'hash': bin_hash(config['NODE_BINARY']), + 'enabled': config['USE_NODE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'SINGLEFILE_BINARY': { + 'path': bin_path(config['SINGLEFILE_BINARY']), + 'version': config['SINGLEFILE_VERSION'], + 'hash': bin_hash(config['SINGLEFILE_BINARY']), + 'enabled': config['USE_SINGLEFILE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'READABILITY_BINARY': { + 'path': bin_path(config['READABILITY_BINARY']), + 'version': config['READABILITY_VERSION'], + 'hash': bin_hash(config['READABILITY_BINARY']), + 'enabled': config['USE_READABILITY'], + 'is_valid': bool(config['READABILITY_VERSION']), + }, + 'MERCURY_BINARY': { + 'path': bin_path(config['MERCURY_BINARY']), + 'version': config['MERCURY_VERSION'], + 'hash': bin_hash(config['MERCURY_BINARY']), + 'enabled': config['USE_MERCURY'], + 'is_valid': bool(config['MERCURY_VERSION']), + }, + 'GIT_BINARY': { + 'path': bin_path(config['GIT_BINARY']), + 'version': config['GIT_VERSION'], + 'hash': bin_hash(config['GIT_BINARY']), + 'enabled': config['USE_GIT'], + 'is_valid': bool(config['GIT_VERSION']), + }, + 'YOUTUBEDL_BINARY': { + 'path': bin_path(config['YOUTUBEDL_BINARY']), + 'version': config['YOUTUBEDL_VERSION'], + 'hash': bin_hash(config['YOUTUBEDL_BINARY']), + 'enabled': config['USE_YOUTUBEDL'], + 'is_valid': bool(config['YOUTUBEDL_VERSION']), + }, + 'CHROME_BINARY': { + 'path': bin_path(config['CHROME_BINARY']), + 'version': config['CHROME_VERSION'], + 'hash': bin_hash(config['CHROME_BINARY']), + 'enabled': config['USE_CHROME'], + 'is_valid': bool(config['CHROME_VERSION']), + }, + 'RIPGREP_BINARY': { + 'path': bin_path(config['RIPGREP_BINARY']), + 'version': config['RIPGREP_VERSION'], + 'hash': bin_hash(config['RIPGREP_BINARY']), + 'enabled': config['USE_RIPGREP'], + 'is_valid': bool(config['RIPGREP_VERSION']), + }, + # TODO: add an entry for the sonic search backend? + # 'SONIC_BINARY': { + # 'path': bin_path(config['SONIC_BINARY']), + # 'version': config['SONIC_VERSION'], + # 'hash': bin_hash(config['SONIC_BINARY']), + # 'enabled': config['USE_SONIC'], + # 'is_valid': bool(config['SONIC_VERSION']), + # }, + } + +def get_chrome_info(config: ConfigDict) -> ConfigValue: + return { + 'TIMEOUT': config['TIMEOUT'], + 'RESOLUTION': config['RESOLUTION'], + 'CHECK_SSL_VALIDITY': config['CHECK_SSL_VALIDITY'], + 'CHROME_BINARY': config['CHROME_BINARY'], + 'CHROME_HEADLESS': config['CHROME_HEADLESS'], + 'CHROME_SANDBOX': config['CHROME_SANDBOX'], + 'CHROME_USER_AGENT': config['CHROME_USER_AGENT'], + 'CHROME_USER_DATA_DIR': config['CHROME_USER_DATA_DIR'], + } + + +# ****************************************************************************** +# ****************************************************************************** +# ******************************** Load Config ********************************* +# ******* (compile the defaults, configs, and metadata all into CONFIG) ******** +# ****************************************************************************** +# ****************************************************************************** + + +def load_all_config(): + CONFIG: ConfigDict = {} + for section_name, section_config in CONFIG_SCHEMA.items(): + CONFIG = load_config(section_config, CONFIG) + + return load_config(DYNAMIC_CONFIG_SCHEMA, CONFIG) + +# add all final config values in CONFIG to globals in this file +CONFIG = load_all_config() +globals().update(CONFIG) +# this lets us do: from .config import DEBUG, MEDIA_TIMEOUT, ... + + +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** + + + +########################### System Environment Setup ########################### + + +# Set timezone to UTC and umask to OUTPUT_PERMISSIONS +os.environ["TZ"] = 'UTC' +os.umask(0o777 - int(OUTPUT_PERMISSIONS, base=8)) # noqa: F821 + +# add ./node_modules/.bin to $PATH so we can use node scripts in extractors +NODE_BIN_PATH = str((Path(CONFIG["OUTPUT_DIR"]).absolute() / 'node_modules' / '.bin')) +sys.path.append(NODE_BIN_PATH) + + + + +########################### Config Validity Checkers ########################### + + +def check_system_config(config: ConfigDict=CONFIG) -> None: + ### Check system environment + if config['USER'] == 'root': + stderr('[!] ArchiveBox should never be run as root!', color='red') + stderr(' For more information, see the security overview documentation:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#do-not-run-as-root') + raise SystemExit(2) + + ### Check Python environment + if sys.version_info[:3] < (3, 6, 0): + stderr(f'[X] Python version is not new enough: {config["PYTHON_VERSION"]} (>3.6 is required)', color='red') + stderr(' See https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') + raise SystemExit(2) + + if config['PYTHON_ENCODING'] not in ('UTF-8', 'UTF8'): + stderr(f'[X] Your system is running python3 scripts with a bad locale setting: {config["PYTHON_ENCODING"]} (it should be UTF-8).', color='red') + stderr(' To fix it, add the line "export PYTHONIOENCODING=UTF-8" to your ~/.bashrc file (without quotes)') + stderr(' Or if you\'re using ubuntu/debian, run "dpkg-reconfigure locales"') + stderr('') + stderr(' Confirm that it\'s fixed by opening a new shell and running:') + stderr(' python3 -c "import sys; print(sys.stdout.encoding)" # should output UTF-8') + raise SystemExit(2) + + # stderr('[i] Using Chrome binary: {}'.format(shutil.which(CHROME_BINARY) or CHROME_BINARY)) + # stderr('[i] Using Chrome data dir: {}'.format(os.path.abspath(CHROME_USER_DATA_DIR))) + if config['CHROME_USER_DATA_DIR'] is not None: + if not (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(): + stderr('[X] Could not find profile "Default" in CHROME_USER_DATA_DIR.', color='red') + stderr(f' {config["CHROME_USER_DATA_DIR"]}') + stderr(' Make sure you set it to a Chrome user data directory containing a Default profile folder.') + stderr(' For more info see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#CHROME_USER_DATA_DIR') + if '/Default' in str(config['CHROME_USER_DATA_DIR']): + stderr() + stderr(' Try removing /Default from the end e.g.:') + stderr(' CHROME_USER_DATA_DIR="{}"'.format(config['CHROME_USER_DATA_DIR'].split('/Default')[0])) + raise SystemExit(2) + + +def check_dependencies(config: ConfigDict=CONFIG, show_help: bool=True) -> None: + invalid_dependencies = [ + (name, info) for name, info in config['DEPENDENCIES'].items() + if info['enabled'] and not info['is_valid'] + ] + if invalid_dependencies and show_help: + stderr(f'[!] Warning: Missing {len(invalid_dependencies)} recommended dependencies', color='lightyellow') + for dependency, info in invalid_dependencies: + stderr( + ' ! {}: {} ({})'.format( + dependency, + info['path'] or 'unable to find binary', + info['version'] or 'unable to detect version', + ) + ) + if dependency in ('SINGLEFILE_BINARY', 'READABILITY_BINARY', 'MERCURY_BINARY'): + hint(('npm install --prefix . "git+https://github.com/ArchiveBox/ArchiveBox.git"', + f'or archivebox config --set SAVE_{dependency.rsplit("_", 1)[0]}=False to silence this warning', + ''), prefix=' ') + stderr('') + + if config['TIMEOUT'] < 5: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' You must allow *at least* 5 seconds for indexing and archive methods to run succesfully.') + stderr(' (Setting it to somewhere between 30 and 3000 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + elif config['USE_CHROME'] and config['TIMEOUT'] < 15: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' Chrome will fail to archive all sites if set to less than ~15 seconds.') + stderr(' (Setting it to somewhere between 30 and 300 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + if config['USE_YOUTUBEDL'] and config['MEDIA_TIMEOUT'] < 20: + stderr(f'[!] Warning: MEDIA_TIMEOUT is set too low! (currently set to MEDIA_TIMEOUT={config["MEDIA_TIMEOUT"]} seconds)', color='red') + stderr(' Youtube-dl will fail to archive all media if set to less than ~20 seconds.') + stderr(' (Setting it somewhere over 60 seconds is recommended)') + stderr() + stderr(' If you want to disable media archiving entirely, set SAVE_MEDIA=False instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#save_media') + stderr() + +def check_data_folder(out_dir: Union[str, Path, None]=None, config: ConfigDict=CONFIG) -> None: + output_dir = out_dir or config['OUTPUT_DIR'] + assert isinstance(output_dir, (str, Path)) + + sql_index_exists = (Path(output_dir) / SQL_INDEX_FILENAME).exists() + if not sql_index_exists: + stderr('[X] No archivebox index found in the current directory.', color='red') + stderr(f' {output_dir}', color='lightyellow') + stderr() + stderr(' {lightred}Hint{reset}: Are you running archivebox in the right folder?'.format(**config['ANSI'])) + stderr(' cd path/to/your/archive/folder') + stderr(' archivebox [command]') + stderr() + stderr(' {lightred}Hint{reset}: To create a new archive collection or import existing data in this folder, run:'.format(**config['ANSI'])) + stderr(' archivebox init') + raise SystemExit(2) + + from .index.sql import list_migrations + + pending_migrations = [name for status, name in list_migrations() if not status] + + if (not sql_index_exists) or pending_migrations: + if sql_index_exists: + pending_operation = f'apply the {len(pending_migrations)} pending migrations' + else: + pending_operation = 'generate the new SQL main index' + + stderr('[X] This collection was created with an older version of ArchiveBox and must be upgraded first.', color='lightyellow') + stderr(f' {output_dir}') + stderr() + stderr(f' To upgrade it to the latest version and {pending_operation} run:') + stderr(' archivebox init') + raise SystemExit(3) + + sources_dir = Path(output_dir) / SOURCES_DIR_NAME + if not sources_dir.exists(): + sources_dir.mkdir() + + + +def setup_django(out_dir: Path=None, check_db=False, config: ConfigDict=CONFIG, in_memory_db=False) -> None: + check_system_config() + + output_dir = out_dir or Path(config['OUTPUT_DIR']) + + assert isinstance(output_dir, Path) and isinstance(config['PACKAGE_DIR'], Path) + + try: + import django + sys.path.append(str(config['PACKAGE_DIR'])) + os.environ.setdefault('OUTPUT_DIR', str(output_dir)) + assert (config['PACKAGE_DIR'] / 'core' / 'settings.py').exists(), 'settings.py was not found at archivebox/core/settings.py' + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + + if in_memory_db: + # Put the db in memory and run migrations in case any command requires it + from django.core.management import call_command + os.environ.setdefault("ARCHIVEBOX_DATABASE_NAME", ":memory:") + django.setup() + call_command("migrate", interactive=False, verbosity=0) + else: + django.setup() + + if check_db: + sql_index_path = Path(output_dir) / SQL_INDEX_FILENAME + assert sql_index_path.exists(), ( + f'No database file {SQL_INDEX_FILENAME} found in OUTPUT_DIR: {config["OUTPUT_DIR"]}') + except KeyboardInterrupt: + raise SystemExit(2) diff --git a/archivebox-0.5.3/archivebox/config_stubs.py b/archivebox-0.5.3/archivebox/config_stubs.py new file mode 100644 index 0000000..988f58a --- /dev/null +++ b/archivebox-0.5.3/archivebox/config_stubs.py @@ -0,0 +1,113 @@ +from pathlib import Path +from typing import Optional, Dict, Union, Tuple, Callable, Pattern, Type, Any, List +from mypy_extensions import TypedDict + + + +SimpleConfigValue = Union[str, bool, int, None, Pattern, Dict[str, Any]] +SimpleConfigValueDict = Dict[str, SimpleConfigValue] +SimpleConfigValueGetter = Callable[[], SimpleConfigValue] +ConfigValue = Union[SimpleConfigValue, SimpleConfigValueDict, SimpleConfigValueGetter] + + +class BaseConfig(TypedDict): + pass + +class ConfigDict(BaseConfig, total=False): + """ + # Regenerate by pasting this quine into `archivebox shell` 🥚 + from archivebox.config import ConfigDict, CONFIG_DEFAULTS + print('class ConfigDict(BaseConfig, total=False):') + print(' ' + '"'*3 + ConfigDict.__doc__ + '"'*3) + for section, configs in CONFIG_DEFAULTS.items(): + for key, attrs in configs.items(): + Type, default = attrs['type'], attrs['default'] + if default is None: + print(f' {key}: Optional[{Type.__name__}]') + else: + print(f' {key}: {Type.__name__}') + print() + """ + IS_TTY: bool + USE_COLOR: bool + SHOW_PROGRESS: bool + IN_DOCKER: bool + + PACKAGE_DIR: Path + OUTPUT_DIR: Path + CONFIG_FILE: Path + ONLY_NEW: bool + TIMEOUT: int + MEDIA_TIMEOUT: int + OUTPUT_PERMISSIONS: str + RESTRICT_FILE_NAMES: str + URL_BLACKLIST: str + + SECRET_KEY: Optional[str] + BIND_ADDR: str + ALLOWED_HOSTS: str + DEBUG: bool + PUBLIC_INDEX: bool + PUBLIC_SNAPSHOTS: bool + FOOTER_INFO: str + ACTIVE_THEME: str + + SAVE_TITLE: bool + SAVE_FAVICON: bool + SAVE_WGET: bool + SAVE_WGET_REQUISITES: bool + SAVE_SINGLEFILE: bool + SAVE_READABILITY: bool + SAVE_MERCURY: bool + SAVE_PDF: bool + SAVE_SCREENSHOT: bool + SAVE_DOM: bool + SAVE_WARC: bool + SAVE_GIT: bool + SAVE_MEDIA: bool + SAVE_ARCHIVE_DOT_ORG: bool + + RESOLUTION: str + GIT_DOMAINS: str + CHECK_SSL_VALIDITY: bool + CURL_USER_AGENT: str + WGET_USER_AGENT: str + CHROME_USER_AGENT: str + COOKIES_FILE: Union[str, Path, None] + CHROME_USER_DATA_DIR: Union[str, Path, None] + CHROME_HEADLESS: bool + CHROME_SANDBOX: bool + + USE_CURL: bool + USE_WGET: bool + USE_SINGLEFILE: bool + USE_READABILITY: bool + USE_MERCURY: bool + USE_GIT: bool + USE_CHROME: bool + USE_YOUTUBEDL: bool + CURL_BINARY: str + GIT_BINARY: str + WGET_BINARY: str + SINGLEFILE_BINARY: str + READABILITY_BINARY: str + MERCURY_BINARY: str + YOUTUBEDL_BINARY: str + CHROME_BINARY: Optional[str] + + YOUTUBEDL_ARGS: List[str] + WGET_ARGS: List[str] + CURL_ARGS: List[str] + GIT_ARGS: List[str] + + +ConfigDefaultValueGetter = Callable[[ConfigDict], ConfigValue] +ConfigDefaultValue = Union[ConfigValue, ConfigDefaultValueGetter] + +ConfigDefault = TypedDict('ConfigDefault', { + 'default': ConfigDefaultValue, + 'type': Optional[Type], + 'aliases': Optional[Tuple[str, ...]], +}, total=False) + +ConfigDefaultDict = Dict[str, ConfigDefault] diff --git a/archivebox-0.5.3/archivebox/core/__init__.py b/archivebox-0.5.3/archivebox/core/__init__.py new file mode 100644 index 0000000..3e1d607 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox.core' diff --git a/archivebox-0.5.3/archivebox/core/admin.py b/archivebox-0.5.3/archivebox/core/admin.py new file mode 100644 index 0000000..832bea3 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/admin.py @@ -0,0 +1,257 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.contrib import admin +from django.urls import path +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.shortcuts import render, redirect +from django.contrib.auth import get_user_model +from django import forms + +from core.models import Snapshot, Tag +from core.forms import AddLinkForm, TagField + +from core.mixins import SearchResultsAdminMixin + +from index.html import snapshot_icons +from util import htmldecode, urldecode, ansi_to_html +from logging_util import printable_filesize +from main import add, remove +from config import OUTPUT_DIR +from extractors import archive_links + +# TODO: https://stackoverflow.com/questions/40760880/add-custom-button-to-django-admin-panel + +def update_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], out_dir=OUTPUT_DIR) +update_snapshots.short_description = "Archive" + +def update_titles(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, methods=('title','favicon'), out_dir=OUTPUT_DIR) +update_titles.short_description = "Pull title" + +def overwrite_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, out_dir=OUTPUT_DIR) +overwrite_snapshots.short_description = "Re-archive (overwrite)" + +def verify_snapshots(modeladmin, request, queryset): + for snapshot in queryset: + print(snapshot.timestamp, snapshot.url, snapshot.is_archived, snapshot.archive_size, len(snapshot.history)) + +verify_snapshots.short_description = "Check" + +def delete_snapshots(modeladmin, request, queryset): + remove(snapshots=queryset, yes=True, delete=True, out_dir=OUTPUT_DIR) + +delete_snapshots.short_description = "Delete" + + +class SnapshotAdminForm(forms.ModelForm): + tags = TagField(required=False) + + class Meta: + model = Snapshot + fields = "__all__" + + def save(self, commit=True): + # Based on: https://stackoverflow.com/a/49933068/3509554 + + # Get the unsave instance + instance = forms.ModelForm.save(self, False) + tags = self.cleaned_data.pop("tags") + + #update save_m2m + def new_save_m2m(): + instance.save_tags(tags) + + # Do we need to save all changes now? + self.save_m2m = new_save_m2m + if commit: + instance.save() + + return instance + + +class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): + list_display = ('added', 'title_str', 'url_str', 'files', 'size') + sort_fields = ('title_str', 'url_str', 'added') + readonly_fields = ('id', 'url', 'timestamp', 'num_outputs', 'is_archived', 'url_hash', 'added', 'updated') + search_fields = ['url', 'timestamp', 'title', 'tags__name'] + fields = (*readonly_fields, 'title', 'tags') + list_filter = ('added', 'updated', 'tags') + ordering = ['-added'] + actions = [delete_snapshots, overwrite_snapshots, update_snapshots, update_titles, verify_snapshots] + actions_template = 'admin/actions_as_select.html' + form = SnapshotAdminForm + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('grid/', self.admin_site.admin_view(self.grid_view),name='grid') + ] + return custom_urls + urls + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return ', '.join(obj.tags.values_list('name', flat=True)) + + def id_str(self, obj): + return format_html( + '{}', + obj.url_hash[:8], + ) + + def title_str(self, obj): + canon = obj.as_link().canonical_outputs() + tags = ''.join( + format_html('{} ', tag.id, tag) + for tag in obj.tags.all() + if str(tag).strip() + ) + return format_html( + '' + '' + '' + '' + '{}' + '', + obj.archive_path, + obj.archive_path, canon['favicon_path'], + obj.archive_path, + 'fetched' if obj.latest_title or obj.title else 'pending', + urldecode(htmldecode(obj.latest_title or obj.title or ''))[:128] or 'Pending...' + ) + mark_safe(f' {tags}') + + def files(self, obj): + return snapshot_icons(obj) + + def size(self, obj): + archive_size = obj.archive_size + if archive_size: + size_txt = printable_filesize(archive_size) + if archive_size > 52428800: + size_txt = mark_safe(f'{size_txt}') + else: + size_txt = mark_safe('...') + return format_html( + '{}', + obj.archive_path, + size_txt, + ) + + def url_str(self, obj): + return format_html( + '{}', + obj.url, + obj.url.split('://www.', 1)[-1].split('://', 1)[-1][:64], + ) + + def grid_view(self, request): + + # cl = self.get_changelist_instance(request) + + # Save before monkey patching to restore for changelist list view + saved_change_list_template = self.change_list_template + saved_list_per_page = self.list_per_page + saved_list_max_show_all = self.list_max_show_all + + # Monkey patch here plus core_tags.py + self.change_list_template = 'admin/grid_change_list.html' + self.list_per_page = 20 + self.list_max_show_all = self.list_per_page + + # Call monkey patched view + rendered_response = self.changelist_view(request) + + # Restore values + self.change_list_template = saved_change_list_template + self.list_per_page = saved_list_per_page + self.list_max_show_all = saved_list_max_show_all + + return rendered_response + + + id_str.short_description = 'ID' + title_str.short_description = 'Title' + url_str.short_description = 'Original URL' + + id_str.admin_order_field = 'id' + title_str.admin_order_field = 'title' + url_str.admin_order_field = 'url' + +class TagAdmin(admin.ModelAdmin): + list_display = ('slug', 'name', 'id') + sort_fields = ('id', 'name', 'slug') + readonly_fields = ('id',) + search_fields = ('id', 'name', 'slug') + fields = (*readonly_fields, 'name', 'slug') + + +class ArchiveBoxAdmin(admin.AdminSite): + site_header = 'ArchiveBox' + index_title = 'Links' + site_title = 'Index' + + def get_urls(self): + return [ + path('core/snapshot/add/', self.add_view, name='Add'), + ] + super().get_urls() + + def add_view(self, request): + if not request.user.is_authenticated: + return redirect(f'/admin/login/?next={request.path}') + + request.current_app = self.name + context = { + **self.each_context(request), + 'title': 'Add URLs', + } + + if request.method == 'GET': + context['form'] = AddLinkForm() + + elif request.method == 'POST': + form = AddLinkForm(request.POST) + if form.is_valid(): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + else: + context["form"] = form + + return render(template_name='add_links.html', request=request, context=context) + +admin.site = ArchiveBoxAdmin() +admin.site.register(get_user_model()) +admin.site.register(Snapshot, SnapshotAdmin) +admin.site.register(Tag, TagAdmin) +admin.site.disable_action('delete_selected') diff --git a/archivebox-0.5.3/archivebox/core/apps.py b/archivebox-0.5.3/archivebox/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/archivebox-0.5.3/archivebox/core/forms.py b/archivebox-0.5.3/archivebox/core/forms.py new file mode 100644 index 0000000..86b29bb --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/forms.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.core' + +from django import forms + +from ..util import URL_REGEX +from ..vendor.taggit_utils import edit_string_for_tags, parse_tags + +CHOICES = ( + ('0', 'depth = 0 (archive just these URLs)'), + ('1', 'depth = 1 (archive these URLs and all URLs one hop away)'), +) + +from ..extractors import get_default_archive_methods + +ARCHIVE_METHODS = [ + (name, name) + for name, _, _ in get_default_archive_methods() +] + + +class AddLinkForm(forms.Form): + url = forms.RegexField(label="URLs (one per line)", regex=URL_REGEX, min_length='6', strip=True, widget=forms.Textarea, required=True) + depth = forms.ChoiceField(label="Archive depth", choices=CHOICES, widget=forms.RadioSelect, initial='0') + archive_methods = forms.MultipleChoiceField( + required=False, + widget=forms.SelectMultiple, + choices=ARCHIVE_METHODS, + ) +class TagWidgetMixin: + def format_value(self, value): + if value is not None and not isinstance(value, str): + value = edit_string_for_tags(value) + return super().format_value(value) + +class TagWidget(TagWidgetMixin, forms.TextInput): + pass + +class TagField(forms.CharField): + widget = TagWidget + + def clean(self, value): + value = super().clean(value) + try: + return parse_tags(value) + except ValueError: + raise forms.ValidationError( + "Please provide a comma-separated list of tags." + ) + + def has_changed(self, initial_value, data_value): + # Always return False if the field is disabled since self.bound_data + # always uses the initial value in this case. + if self.disabled: + return False + + try: + data_value = self.clean(data_value) + except forms.ValidationError: + pass + + if initial_value is None: + initial_value = [] + + initial_value = [tag.name for tag in initial_value] + initial_value.sort() + + return initial_value != data_value diff --git a/archivebox-0.5.3/archivebox/core/management/commands/archivebox.py b/archivebox-0.5.3/archivebox/core/management/commands/archivebox.py new file mode 100644 index 0000000..a68b5d9 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/management/commands/archivebox.py @@ -0,0 +1,18 @@ +__package__ = 'archivebox' + +from django.core.management.base import BaseCommand + + +from .cli import run_subcommand + + +class Command(BaseCommand): + help = 'Run an ArchiveBox CLI subcommand (e.g. add, remove, list, etc)' + + def add_arguments(self, parser): + parser.add_argument('subcommand', type=str, help='The subcommand you want to run') + parser.add_argument('command_args', nargs='*', help='Arguments to pass to the subcommand') + + + def handle(self, *args, **kwargs): + run_subcommand(kwargs['subcommand'], args=kwargs['command_args']) diff --git a/archivebox-0.5.3/archivebox/core/migrations/0001_initial.py b/archivebox-0.5.3/archivebox/core/migrations/0001_initial.py new file mode 100644 index 0000000..73ac78e --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2019-05-01 03:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Snapshot', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('url', models.URLField(unique=True)), + ('timestamp', models.CharField(default=None, max_length=32, null=True, unique=True)), + ('title', models.CharField(default=None, max_length=128, null=True)), + ('tags', models.CharField(default=None, max_length=256, null=True)), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(default=None, null=True)), + ], + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0002_auto_20200625_1521.py b/archivebox-0.5.3/archivebox/core/migrations/0002_auto_20200625_1521.py new file mode 100644 index 0000000..4811282 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0002_auto_20200625_1521.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-06-25 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0003_auto_20200630_1034.py b/archivebox-0.5.3/archivebox/core/migrations/0003_auto_20200630_1034.py new file mode 100644 index 0000000..61fd472 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0003_auto_20200630_1034.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.7 on 2020-06-30 10:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20200625_1521'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='added', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(db_index=True, default=None, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(db_index=True, default=None, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(db_index=True, default=None, null=True), + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0004_auto_20200713_1552.py b/archivebox-0.5.3/archivebox/core/migrations/0004_auto_20200713_1552.py new file mode 100644 index 0000000..6983662 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0004_auto_20200713_1552.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-07-13 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_auto_20200630_1034'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, unique=True), + preserve_default=False, + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0005_auto_20200728_0326.py b/archivebox-0.5.3/archivebox/core/migrations/0005_auto_20200728_0326.py new file mode 100644 index 0000000..f367aeb --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0005_auto_20200728_0326.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-07-28 03:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20200713_1552'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(blank=True, db_index=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(blank=True, db_index=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0006_auto_20201012_1520.py b/archivebox-0.5.3/archivebox/core/migrations/0006_auto_20201012_1520.py new file mode 100644 index 0000000..694c990 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0006_auto_20201012_1520.py @@ -0,0 +1,70 @@ +# Generated by Django 3.0.8 on 2020-10-12 15:20 + +from django.db import migrations, models +from django.utils.text import slugify + +def forwards_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags + tag_set = ( + set(tag.strip() for tag in (snapshot.tags_old or '').split(',')) + ) + tag_set.discard("") + + for tag in tag_set: + to_add, _ = TagModel.objects.get_or_create(name=tag, slug=slugify(tag)) + snapshot.tags.add(to_add) + + +def reverse_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags.values_list("name", flat=True) + snapshot.tags_old = ",".join([tag for tag in tags]) + snapshot.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20200728_0326'), + ] + + operations = [ + migrations.RenameField( + model_name='snapshot', + old_name='tags', + new_name='tags_old', + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), + ('slug', models.SlugField(max_length=100, unique=True, verbose_name='slug')), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + }, + ), + migrations.AddField( + model_name='snapshot', + name='tags', + field=models.ManyToManyField(to='core.Tag'), + ), + migrations.RunPython(forwards_func, reverse_func), + migrations.RemoveField( + model_name='snapshot', + name='tags_old', + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0007_archiveresult.py b/archivebox-0.5.3/archivebox/core/migrations/0007_archiveresult.py new file mode 100644 index 0000000..a780376 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0007_archiveresult.py @@ -0,0 +1,97 @@ +# Generated by Django 3.0.8 on 2020-11-04 12:25 + +import json +from pathlib import Path + +from django.db import migrations, models +import django.db.models.deletion + +from config import CONFIG +from index.json import to_json + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +def forwards_func(apps, schema_editor): + from core.models import EXTRACTORS + + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + + snapshots = Snapshot.objects.all() + for snapshot in snapshots: + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + + try: + with open(out_dir / "index.json", "r") as f: + fs_index = json.load(f) + except Exception as e: + continue + + history = fs_index["history"] + + for extractor in history: + for result in history[extractor]: + ArchiveResult.objects.create(extractor=extractor, snapshot=snapshot, cmd=result["cmd"], cmd_version=result["cmd_version"], + start_ts=result["start_ts"], end_ts=result["end_ts"], status=result["status"], pwd=result["pwd"], output=result["output"]) + + +def verify_json_index_integrity(snapshot): + results = snapshot.archiveresult_set.all() + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + with open(out_dir / "index.json", "r") as f: + index = json.load(f) + + history = index["history"] + index_results = [result for extractor in history for result in history[extractor]] + flattened_results = [result["start_ts"] for result in index_results] + + missing_results = [result for result in results if result.start_ts.isoformat() not in flattened_results] + + for missing in missing_results: + index["history"][missing.extractor].append({"cmd": missing.cmd, "cmd_version": missing.cmd_version, "end_ts": missing.end_ts.isoformat(), + "start_ts": missing.start_ts.isoformat(), "pwd": missing.pwd, "output": missing.output, + "schema": "ArchiveResult", "status": missing.status}) + + json_index = to_json(index) + with open(out_dir / "index.json", "w") as f: + f.write(json_index) + + +def reverse_func(apps, schema_editor): + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + for snapshot in Snapshot.objects.all(): + verify_json_index_integrity(snapshot) + + ArchiveResult.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20201012_1520'), + ] + + operations = [ + migrations.CreateModel( + name='ArchiveResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cmd', JSONField()), + ('pwd', models.CharField(max_length=256)), + ('cmd_version', models.CharField(max_length=32)), + ('status', models.CharField(choices=[('succeeded', 'succeeded'), ('failed', 'failed'), ('skipped', 'skipped')], max_length=16)), + ('output', models.CharField(max_length=512)), + ('start_ts', models.DateTimeField()), + ('end_ts', models.DateTimeField()), + ('extractor', models.CharField(choices=[('title', 'title'), ('favicon', 'favicon'), ('wget', 'wget'), ('singlefile', 'singlefile'), ('pdf', 'pdf'), ('screenshot', 'screenshot'), ('dom', 'dom'), ('readability', 'readability'), ('mercury', 'mercury'), ('git', 'git'), ('media', 'media'), ('headers', 'headers'), ('archive_org', 'archive_org')], max_length=32)), + ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Snapshot')), + ], + ), + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/0008_auto_20210105_1421.py b/archivebox-0.5.3/archivebox/core/migrations/0008_auto_20210105_1421.py new file mode 100644 index 0000000..e5b3387 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/migrations/0008_auto_20210105_1421.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-01-05 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_archiveresult'), + ] + + operations = [ + migrations.AlterField( + model_name='archiveresult', + name='cmd_version', + field=models.CharField(blank=True, default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/archivebox/core/migrations/__init__.py b/archivebox-0.5.3/archivebox/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/archivebox/core/mixins.py b/archivebox-0.5.3/archivebox/core/mixins.py new file mode 100644 index 0000000..538ca1e --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/mixins.py @@ -0,0 +1,23 @@ +from django.contrib import messages + +from archivebox.search import query_search_index + +class SearchResultsAdminMixin(object): + def get_search_results(self, request, queryset, search_term): + ''' Enhances the search queryset with results from the search backend. + ''' + qs, use_distinct = \ + super(SearchResultsAdminMixin, self).get_search_results( + request, queryset, search_term) + + search_term = search_term.strip() + if not search_term: + return qs, use_distinct + try: + qsearch = query_search_index(search_term) + except Exception as err: + messages.add_message(request, messages.WARNING, f'Error from the search backend, only showing results from default admin search fields - Error: {err}') + else: + qs = queryset & qsearch + finally: + return qs, use_distinct diff --git a/archivebox-0.5.3/archivebox/core/models.py b/archivebox-0.5.3/archivebox/core/models.py new file mode 100644 index 0000000..13d75b6 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/models.py @@ -0,0 +1,194 @@ +__package__ = 'archivebox.core' + +import uuid + +from django.db import models, transaction +from django.utils.functional import cached_property +from django.utils.text import slugify +from django.db.models import Case, When, Value, IntegerField + +from ..util import parse_date +from ..index.schema import Link +from ..extractors import get_default_archive_methods, ARCHIVE_METHODS_INDEXING_PRECEDENCE + +EXTRACTORS = [(extractor[0], extractor[0]) for extractor in get_default_archive_methods()] +STATUS_CHOICES = [ + ("succeeded", "succeeded"), + ("failed", "failed"), + ("skipped", "skipped") +] + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +class Tag(models.Model): + """ + Based on django-taggit model + """ + name = models.CharField(verbose_name="name", unique=True, blank=False, max_length=100) + slug = models.SlugField(verbose_name="slug", unique=True, max_length=100) + + class Meta: + verbose_name = "Tag" + verbose_name_plural = "Tags" + + def __str__(self): + return self.name + + def slugify(self, tag, i=None): + slug = slugify(tag) + if i is not None: + slug += "_%d" % i + return slug + + def save(self, *args, **kwargs): + if self._state.adding and not self.slug: + self.slug = self.slugify(self.name) + + with transaction.atomic(): + slugs = set( + type(self) + ._default_manager.filter(slug__startswith=self.slug) + .values_list("slug", flat=True) + ) + + i = None + while True: + slug = self.slugify(self.name, i) + if slug not in slugs: + self.slug = slug + return super().save(*args, **kwargs) + i = 1 if i is None else i+1 + else: + return super().save(*args, **kwargs) + + +class Snapshot(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + url = models.URLField(unique=True) + timestamp = models.CharField(max_length=32, unique=True, db_index=True) + + title = models.CharField(max_length=128, null=True, blank=True, db_index=True) + + added = models.DateTimeField(auto_now_add=True, db_index=True) + updated = models.DateTimeField(null=True, blank=True, db_index=True) + tags = models.ManyToManyField(Tag) + + keys = ('url', 'timestamp', 'title', 'tags', 'updated') + + def __repr__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + def __str__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + @classmethod + def from_json(cls, info: dict): + info = {k: v for k, v in info.items() if k in cls.keys} + return cls(**info) + + def as_json(self, *args) -> dict: + args = args or self.keys + return { + key: getattr(self, key) + if key != 'tags' else self.tags_str() + for key in args + } + + def as_link(self) -> Link: + return Link.from_json(self.as_json()) + + def as_link_with_details(self) -> Link: + from ..index import load_link_details + return load_link_details(self.as_link()) + + def tags_str(self) -> str: + return ','.join(self.tags.order_by('name').values_list('name', flat=True)) + + @cached_property + def bookmarked(self): + return parse_date(self.timestamp) + + @cached_property + def is_archived(self): + return self.as_link().is_archived + + @cached_property + def num_outputs(self): + return self.archiveresult_set.filter(status='succeeded').count() + + @cached_property + def url_hash(self): + return self.as_link().url_hash + + @cached_property + def base_url(self): + return self.as_link().base_url + + @cached_property + def link_dir(self): + return self.as_link().link_dir + + @cached_property + def archive_path(self): + return self.as_link().archive_path + + @cached_property + def archive_size(self): + return self.as_link().archive_size + + @cached_property + def history(self): + # TODO: use ArchiveResult for this instead of json + return self.as_link_with_details().history + + @cached_property + def latest_title(self): + if ('title' in self.history + and self.history['title'] + and (self.history['title'][-1].status == 'succeeded') + and self.history['title'][-1].output.strip()): + return self.history['title'][-1].output.strip() + return None + + def save_tags(self, tags=()): + tags_id = [] + for tag in tags: + tags_id.append(Tag.objects.get_or_create(name=tag)[0].id) + self.tags.clear() + self.tags.add(*tags_id) + + +class ArchiveResultManager(models.Manager): + def indexable(self, sorted: bool = True): + INDEXABLE_METHODS = [ r[0] for r in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = self.get_queryset().filter(extractor__in=INDEXABLE_METHODS,status='succeeded') + + if sorted: + precedence = [ When(extractor=method, then=Value(precedence)) for method, precedence in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = qs.annotate(indexing_precedence=Case(*precedence, default=Value(1000),output_field=IntegerField())).order_by('indexing_precedence') + return qs + + +class ArchiveResult(models.Model): + snapshot = models.ForeignKey(Snapshot, on_delete=models.CASCADE) + cmd = JSONField() + pwd = models.CharField(max_length=256) + cmd_version = models.CharField(max_length=32, default=None, null=True, blank=True) + output = models.CharField(max_length=512) + start_ts = models.DateTimeField() + end_ts = models.DateTimeField() + status = models.CharField(max_length=16, choices=STATUS_CHOICES) + extractor = models.CharField(choices=EXTRACTORS, max_length=32) + + objects = ArchiveResultManager() + + def __str__(self): + return self.extractor diff --git a/archivebox-0.5.3/archivebox/core/settings.py b/archivebox-0.5.3/archivebox/core/settings.py new file mode 100644 index 0000000..e8ed6b1 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/settings.py @@ -0,0 +1,165 @@ +__package__ = 'archivebox.core' + +import os +import sys + +from pathlib import Path +from django.utils.crypto import get_random_string + +from ..config import ( # noqa: F401 + DEBUG, + SECRET_KEY, + ALLOWED_HOSTS, + PACKAGE_DIR, + ACTIVE_THEME, + TEMPLATES_DIR_NAME, + SQL_INDEX_FILENAME, + OUTPUT_DIR, +) + + +IS_MIGRATING = 'makemigrations' in sys.argv[:3] or 'migrate' in sys.argv[:3] +IS_TESTING = 'test' in sys.argv[:3] or 'PYTEST_CURRENT_TEST' in os.environ +IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3] + +################################################################################ +### Django Core Settings +################################################################################ + +WSGI_APPLICATION = 'core.wsgi.application' +ROOT_URLCONF = 'core.urls' + +LOGIN_URL = '/accounts/login/' +LOGOUT_REDIRECT_URL = '/' +PASSWORD_RESET_URL = '/accounts/password_reset/' +APPEND_SLASH = True + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'core', + + 'django_extensions', +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + + +################################################################################ +### Staticfile and Template Settings +################################################################################ + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME / 'static'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default' / 'static'), +] + +TEMPLATE_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME), +] + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': TEMPLATE_DIRS, + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + + +################################################################################ +### External Service Settings +################################################################################ + +DATABASE_FILE = Path(OUTPUT_DIR) / SQL_INDEX_FILENAME +DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", DATABASE_FILE) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': DATABASE_NAME, + } +} + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + + +################################################################################ +### Security Settings +################################################################################ + +SECRET_KEY = SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.') + +ALLOWED_HOSTS = ALLOWED_HOSTS.split(',') + +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True + +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = False +SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_AGE = 1209600 # 2 weeks +SESSION_EXPIRE_AT_BROWSER_CLOSE = False +SESSION_SAVE_EVERY_REQUEST = True + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + + +################################################################################ +### Shell Settings +################################################################################ + +SHELL_PLUS = 'ipython' +SHELL_PLUS_PRINT_SQL = False +IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner'] +IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell' +if IS_SHELL: + os.environ['PYTHONSTARTUP'] = str(Path(PACKAGE_DIR) / 'core' / 'welcome_message.py') + + +################################################################################ +### Internationalization & Localization Settings +################################################################################ + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = False +USE_L10N = False +USE_TZ = False + +DATETIME_FORMAT = 'Y-m-d g:iA' +SHORT_DATETIME_FORMAT = 'Y-m-d h:iA' diff --git a/archivebox-0.5.3/archivebox/core/templatetags/__init__.py b/archivebox-0.5.3/archivebox/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/archivebox/core/templatetags/core_tags.py b/archivebox-0.5.3/archivebox/core/templatetags/core_tags.py new file mode 100644 index 0000000..25f0685 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/templatetags/core_tags.py @@ -0,0 +1,47 @@ +from django import template +from django.urls import reverse +from django.contrib.admin.templatetags.base import InclusionAdminNode +from django.templatetags.static import static + + +from typing import Union + +from core.models import ArchiveResult + +register = template.Library() + +@register.simple_tag +def snapshot_image(snapshot): + result = ArchiveResult.objects.filter(snapshot=snapshot, extractor='screenshot', status='succeeded').first() + if result: + return reverse('LinkAssets', args=[f'{str(snapshot.timestamp)}/{result.output}']) + + return static('archive.png') + +@register.filter +def file_size(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + +def result_list(cl): + """ + Monkey patched result + """ + num_sorted_fields = 0 + return { + 'cl': cl, + 'num_sorted_fields': num_sorted_fields, + 'results': cl.result_list, + } + +@register.tag(name='snapshots_grid') +def result_list_tag(parser, token): + return InclusionAdminNode( + parser, token, + func=result_list, + template_name='snapshots_grid.html', + takes_context=False, + ) diff --git a/archivebox-0.5.3/archivebox/core/tests.py b/archivebox-0.5.3/archivebox/core/tests.py new file mode 100644 index 0000000..4d66077 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/tests.py @@ -0,0 +1,3 @@ +#from django.test import TestCase + +# Create your tests here. diff --git a/archivebox-0.5.3/archivebox/core/urls.py b/archivebox-0.5.3/archivebox/core/urls.py new file mode 100644 index 0000000..b8e4baf --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/urls.py @@ -0,0 +1,36 @@ +from django.contrib import admin + +from django.urls import path, include +from django.views import static +from django.conf import settings +from django.views.generic.base import RedirectView + +from core.views import MainIndex, LinkDetails, PublicArchiveView, AddView + + +# print('DEBUG', settings.DEBUG) + +urlpatterns = [ + path('robots.txt', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'robots.txt'}), + path('favicon.ico', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'favicon.ico'}), + + path('docs/', RedirectView.as_view(url='https://github.com/ArchiveBox/ArchiveBox/wiki'), name='Docs'), + + path('archive/', RedirectView.as_view(url='/')), + path('archive/', LinkDetails.as_view(), name='LinkAssets'), + + path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')), + path('add/', AddView.as_view()), + + path('accounts/login/', RedirectView.as_view(url='/admin/login/')), + path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')), + + + path('accounts/', include('django.contrib.auth.urls')), + path('admin/', admin.site.urls), + + path('index.html', RedirectView.as_view(url='/')), + path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}), + path('', MainIndex.as_view(), name='Home'), + path('public/', PublicArchiveView.as_view(), name='public-index'), +] diff --git a/archivebox-0.5.3/archivebox/core/views.py b/archivebox-0.5.3/archivebox/core/views.py new file mode 100644 index 0000000..b46e364 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/views.py @@ -0,0 +1,173 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.shortcuts import render, redirect + +from django.http import HttpResponse +from django.views import View, static +from django.views.generic.list import ListView +from django.views.generic import FormView +from django.contrib.auth.mixins import UserPassesTestMixin + +from core.models import Snapshot +from core.forms import AddLinkForm + +from ..config import ( + OUTPUT_DIR, + PUBLIC_INDEX, + PUBLIC_SNAPSHOTS, + PUBLIC_ADD_VIEW, + VERSION, + FOOTER_INFO, +) +from main import add +from ..util import base_url, ansi_to_html +from ..index.html import snapshot_icons + + +class MainIndex(View): + template = 'main_index.html' + + def get(self, request): + if request.user.is_authenticated: + return redirect('/admin/core/snapshot/') + + if PUBLIC_INDEX: + return redirect('public-index') + + return redirect(f'/admin/login/?next={request.path}') + + +class LinkDetails(View): + def get(self, request, path): + # missing trailing slash -> redirect to index + if '/' not in path: + return redirect(f'{path}/index.html') + + if not request.user.is_authenticated and not PUBLIC_SNAPSHOTS: + return redirect(f'/admin/login/?next={request.path}') + + try: + slug, archivefile = path.split('/', 1) + except (IndexError, ValueError): + slug, archivefile = path.split('/', 1)[0], 'index.html' + + all_pages = list(Snapshot.objects.all()) + + # slug is a timestamp + by_ts = {page.timestamp: page for page in all_pages} + try: + # print('SERVING STATICFILE', by_ts[slug].link_dir, request.path, path) + response = static.serve(request, archivefile, document_root=by_ts[slug].link_dir, show_indexes=True) + response["Link"] = f'<{by_ts[slug].url}>; rel="canonical"' + return response + except KeyError: + pass + + # slug is a hash + by_hash = {page.url_hash: page for page in all_pages} + try: + timestamp = by_hash[slug].timestamp + return redirect(f'/archive/{timestamp}/{archivefile}') + except KeyError: + pass + + # slug is a URL + by_url = {page.base_url: page for page in all_pages} + try: + # TODO: add multiple snapshot support by showing index of all snapshots + # for given url instead of redirecting to timestamp index + timestamp = by_url[base_url(path)].timestamp + return redirect(f'/archive/{timestamp}/index.html') + except KeyError: + pass + + return HttpResponse( + 'No archived link matches the given timestamp or hash.', + content_type="text/plain", + status=404, + ) + +class PublicArchiveView(ListView): + template = 'snapshot_list.html' + model = Snapshot + paginate_by = 100 + ordering = ['title'] + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + query = self.request.GET.get('q') + if query: + qs = qs.filter(title__icontains=query) + for snapshot in qs: + snapshot.icons = snapshot_icons(snapshot) + return qs + + def get(self, *args, **kwargs): + if PUBLIC_INDEX or self.request.user.is_authenticated: + response = super().get(*args, **kwargs) + return response + else: + return redirect(f'/admin/login/?next={self.request.path}') + + +class AddView(UserPassesTestMixin, FormView): + template_name = "add_links.html" + form_class = AddLinkForm + + def get_initial(self): + """Prefill the AddLinkForm with the 'url' GET parameter""" + if self.request.method == 'GET': + url = self.request.GET.get('url', None) + if url: + return {'url': url} + else: + return super().get_initial() + + def test_func(self): + return PUBLIC_ADD_VIEW or self.request.user.is_authenticated + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'title': "Add URLs", + # We can't just call request.build_absolute_uri in the template, because it would include query parameters + 'absolute_add_path': self.request.build_absolute_uri(self.request.path), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def form_valid(self, form): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + extractors = ','.join(form.cleaned_data["archive_methods"]) + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + if extractors: + input_kwargs.update({"extractors": extractors}) + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context = self.get_context_data() + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + return render(template_name=self.template_name, request=self.request, context=context) diff --git a/archivebox-0.5.3/archivebox/core/welcome_message.py b/archivebox-0.5.3/archivebox/core/welcome_message.py new file mode 100644 index 0000000..ed5d2d7 --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/welcome_message.py @@ -0,0 +1,5 @@ +from archivebox.logging_util import log_shell_welcome_msg + + +if __name__ == '__main__': + log_shell_welcome_msg() diff --git a/archivebox-0.5.3/archivebox/core/wsgi.py b/archivebox-0.5.3/archivebox/core/wsgi.py new file mode 100644 index 0000000..f933afa --- /dev/null +++ b/archivebox-0.5.3/archivebox/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for archivebox project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.settings') + +application = get_wsgi_application() diff --git a/archivebox-0.5.3/archivebox/extractors/__init__.py b/archivebox-0.5.3/archivebox/extractors/__init__.py new file mode 100644 index 0000000..a4acef0 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/__init__.py @@ -0,0 +1,182 @@ +__package__ = 'archivebox.extractors' + +import os +from pathlib import Path + +from typing import Optional, List, Iterable, Union +from datetime import datetime +from django.db.models import QuerySet + +from ..index.schema import Link +from ..index.sql import write_link_to_sql_index +from ..index import ( + load_link_details, + write_link_details, +) +from ..util import enforce_types +from ..logging_util import ( + log_archiving_started, + log_archiving_paused, + log_archiving_finished, + log_link_archiving_started, + log_link_archiving_finished, + log_archive_method_started, + log_archive_method_finished, +) +from ..search import write_search_index + +from .title import should_save_title, save_title +from .favicon import should_save_favicon, save_favicon +from .wget import should_save_wget, save_wget +from .singlefile import should_save_singlefile, save_singlefile +from .readability import should_save_readability, save_readability +from .mercury import should_save_mercury, save_mercury +from .pdf import should_save_pdf, save_pdf +from .screenshot import should_save_screenshot, save_screenshot +from .dom import should_save_dom, save_dom +from .git import should_save_git, save_git +from .media import should_save_media, save_media +from .archive_org import should_save_archive_dot_org, save_archive_dot_org +from .headers import should_save_headers, save_headers + + +def get_default_archive_methods(): + return [ + ('title', should_save_title, save_title), + ('favicon', should_save_favicon, save_favicon), + ('wget', should_save_wget, save_wget), + ('singlefile', should_save_singlefile, save_singlefile), + ('pdf', should_save_pdf, save_pdf), + ('screenshot', should_save_screenshot, save_screenshot), + ('dom', should_save_dom, save_dom), + ('readability', should_save_readability, save_readability), #keep readability below wget and singlefile, as it depends on them + ('mercury', should_save_mercury, save_mercury), + ('git', should_save_git, save_git), + ('media', should_save_media, save_media), + ('headers', should_save_headers, save_headers), + ('archive_org', should_save_archive_dot_org, save_archive_dot_org), + ] + +ARCHIVE_METHODS_INDEXING_PRECEDENCE = [('readability', 1), ('singlefile', 2), ('dom', 3), ('wget', 4)] + +@enforce_types +def ignore_methods(to_ignore: List[str]): + ARCHIVE_METHODS = get_default_archive_methods() + methods = filter(lambda x: x[0] not in to_ignore, ARCHIVE_METHODS) + methods = map(lambda x: x[0], methods) + return list(methods) + +@enforce_types +def archive_link(link: Link, overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> Link: + """download the DOM, PDF, and a screenshot into a folder named after the link's timestamp""" + + # TODO: Remove when the input is changed to be a snapshot. Suboptimal approach. + from core.models import Snapshot, ArchiveResult + try: + snapshot = Snapshot.objects.get(url=link.url) # TODO: This will be unnecessary once everything is a snapshot + except Snapshot.DoesNotExist: + snapshot = write_link_to_sql_index(link) + + ARCHIVE_METHODS = get_default_archive_methods() + + if methods: + ARCHIVE_METHODS = [ + method for method in ARCHIVE_METHODS + if method[0] in methods + ] + + out_dir = out_dir or Path(link.link_dir) + try: + is_new = not Path(out_dir).exists() + if is_new: + os.makedirs(out_dir) + + link = load_link_details(link, out_dir=out_dir) + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + log_link_archiving_started(link, out_dir, is_new) + link = link.overwrite(updated=datetime.now()) + stats = {'skipped': 0, 'succeeded': 0, 'failed': 0} + + for method_name, should_run, method_function in ARCHIVE_METHODS: + try: + if method_name not in link.history: + link.history[method_name] = [] + + if should_run(link, out_dir) or overwrite: + log_archive_method_started(method_name) + + result = method_function(link=link, out_dir=out_dir) + + link.history[method_name].append(result) + + stats[result.status] += 1 + log_archive_method_finished(result) + write_search_index(link=link, texts=result.index_texts) + ArchiveResult.objects.create(snapshot=snapshot, extractor=method_name, cmd=result.cmd, cmd_version=result.cmd_version, + output=result.output, pwd=result.pwd, start_ts=result.start_ts, end_ts=result.end_ts, status=result.status) + + else: + # print('{black} X {}{reset}'.format(method_name, **ANSI)) + stats['skipped'] += 1 + except Exception as e: + raise Exception('Exception in archive_methods.save_{}(Link(url={}))'.format( + method_name, + link.url, + )) from e + + # print(' ', stats) + + try: + latest_title = link.history['title'][-1].output.strip() + if latest_title and len(latest_title) >= len(link.title or ''): + link = link.overwrite(title=latest_title) + except Exception: + pass + + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + + log_link_archiving_finished(link, link.link_dir, is_new, stats) + + except KeyboardInterrupt: + try: + write_link_details(link, out_dir=link.link_dir) + except: + pass + raise + + except Exception as err: + print(' ! Failed to archive link: {}: {}'.format(err.__class__.__name__, err)) + raise + + return link + +@enforce_types +def archive_links(all_links: Union[Iterable[Link], QuerySet], overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> List[Link]: + + if type(all_links) is QuerySet: + num_links: int = all_links.count() + get_link = lambda x: x.as_link() + all_links = all_links.iterator() + else: + num_links: int = len(all_links) + get_link = lambda x: x + + if num_links == 0: + return [] + + log_archiving_started(num_links) + idx: int = 0 + try: + for link in all_links: + idx += 1 + to_archive = get_link(link) + archive_link(to_archive, overwrite=overwrite, methods=methods, out_dir=Path(link.link_dir)) + except KeyboardInterrupt: + log_archiving_paused(num_links, idx, link.timestamp) + raise SystemExit(0) + except BaseException: + print() + raise + + log_archiving_finished(num_links) + return all_links diff --git a/archivebox-0.5.3/archivebox/extractors/archive_org.py b/archivebox-0.5.3/archivebox/extractors/archive_org.py new file mode 100644 index 0000000..f5598d6 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/archive_org.py @@ -0,0 +1,112 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional, List, Dict, Tuple +from collections import defaultdict + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + TIMEOUT, + CURL_ARGS, + CHECK_SSL_VALIDITY, + SAVE_ARCHIVE_DOT_ORG, + CURL_BINARY, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_archive_dot_org(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "archive.org.txt").exists(): + # if open(path, 'r').read().strip() != 'None': + return False + + return SAVE_ARCHIVE_DOT_ORG + +@enforce_types +def save_archive_dot_org(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """submit site to archive.org for archiving via their service, save returned archive url""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'archive.org.txt' + archive_org_url = None + submit_url = 'https://web.archive.org/save/{}'.format(link.url) + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + submit_url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + content_location, errors = parse_archive_dot_org_response(result.stdout) + if content_location: + archive_org_url = content_location[0] + elif len(errors) == 1 and 'RobotAccessControlException' in errors[0]: + archive_org_url = None + # raise ArchiveError('Archive.org denied by {}/robots.txt'.format(domain(link.url))) + elif errors: + raise ArchiveError(', '.join(errors)) + else: + raise ArchiveError('Failed to find "content-location" URL header in Archive.org response.') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + if output and not isinstance(output, Exception): + # instead of writing None when archive.org rejects the url write the + # url to resubmit it to archive.org. This is so when the user visits + # the URL in person, it will attempt to re-archive it, and it'll show the + # nicer error message explaining why the url was rejected if it fails. + archive_org_url = archive_org_url or submit_url + with open(str(out_dir / output), 'w', encoding='utf-8') as f: + f.write(archive_org_url) + chmod_file('archive.org.txt', cwd=str(out_dir)) + output = archive_org_url + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) + +@enforce_types +def parse_archive_dot_org_response(response: bytes) -> Tuple[List[str], List[str]]: + # Parse archive.org response headers + headers: Dict[str, List[str]] = defaultdict(list) + + # lowercase all the header names and store in dict + for header in response.splitlines(): + if b':' not in header or not header.strip(): + continue + name, val = header.decode().split(':', 1) + headers[name.lower().strip()].append(val.strip()) + + # Get successful archive url in "content-location" header or any errors + content_location = headers.get('content-location', headers['location']) + errors = headers['x-archive-wayback-runtime-error'] + return content_location, errors + diff --git a/archivebox-0.5.3/archivebox/extractors/dom.py b/archivebox-0.5.3/archivebox/extractors/dom.py new file mode 100644 index 0000000..babbe71 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/dom.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file, atomic_write +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_DOM, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_dom(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / 'output.html').exists(): + return False + + return SAVE_DOM + +@enforce_types +def save_dom(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print HTML of site to file using chrome --dump-html""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.html' + output_path = out_dir / output + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--dump-dom', + link.url + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + atomic_write(output_path, result.stdout) + + if result.returncode: + hints = result.stderr.decode() + raise ArchiveError('Failed to save DOM', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/favicon.py b/archivebox-0.5.3/archivebox/extractors/favicon.py new file mode 100644 index 0000000..5e7c1fb --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/favicon.py @@ -0,0 +1,64 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import chmod_file, run +from ..util import enforce_types, domain +from ..config import ( + TIMEOUT, + SAVE_FAVICON, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CHECK_SSL_VALIDITY, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_favicon(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if (Path(out_dir) / 'favicon.ico').exists(): + return False + + return SAVE_FAVICON + +@enforce_types +def save_favicon(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download site favicon from google's favicon api""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'favicon.ico' + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + '--output', str(output), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + 'https://www.google.com/s2/favicons?domain={}'.format(domain(link.url)), + ] + status = 'pending' + timer = TimedProgress(timeout, prefix=' ') + try: + run(cmd, cwd=str(out_dir), timeout=timeout) + chmod_file(output, cwd=str(out_dir)) + status = 'succeeded' + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/git.py b/archivebox-0.5.3/archivebox/extractors/git.py new file mode 100644 index 0000000..fd20d4b --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/git.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + domain, + extension, + without_query, + without_fragment, +) +from ..config import ( + TIMEOUT, + SAVE_GIT, + GIT_BINARY, + GIT_ARGS, + GIT_VERSION, + GIT_DOMAINS, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_git(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if (out_dir / "git").exists(): + return False + + is_clonable_url = ( + (domain(link.url) in GIT_DOMAINS) + or (extension(link.url) == 'git') + ) + if not is_clonable_url: + return False + + return SAVE_GIT + + +@enforce_types +def save_git(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using git""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'git' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + GIT_BINARY, + 'clone', + *GIT_ARGS, + *([] if CHECK_SSL_VALIDITY else ['-c', 'http.sslVerify=false']), + without_query(without_fragment(link.url)), + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + if result.returncode == 128: + # ignore failed re-download when the folder already exists + pass + elif result.returncode > 0: + hints = 'Got git response code: {}.'.format(result.returncode) + raise ArchiveError('Failed to save git clone', hints) + + chmod_file(output, cwd=str(out_dir)) + + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=GIT_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/headers.py b/archivebox-0.5.3/archivebox/extractors/headers.py new file mode 100644 index 0000000..4e69dec --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/headers.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import atomic_write +from ..util import ( + enforce_types, + get_headers, +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + CURL_ARGS, + CURL_USER_AGENT, + CURL_VERSION, + CHECK_SSL_VALIDITY, + SAVE_HEADERS +) +from ..logging_util import TimedProgress + +@enforce_types +def should_save_headers(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + + output = Path(out_dir or link.link_dir) / 'headers.json' + return not output.exists() and SAVE_HEADERS + + +@enforce_types +def save_headers(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """Download site headers""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() + output: ArchiveOutput = 'headers.json' + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + try: + json_headers = get_headers(link.url, timeout=timeout) + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "headers.json"), json_headers) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/media.py b/archivebox-0.5.3/archivebox/extractors/media.py new file mode 100644 index 0000000..3792fd2 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/media.py @@ -0,0 +1,81 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + MEDIA_TIMEOUT, + SAVE_MEDIA, + YOUTUBEDL_ARGS, + YOUTUBEDL_BINARY, + YOUTUBEDL_VERSION, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_media(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + + if is_static_file(link.url): + return False + + if (out_dir / "media").exists(): + return False + + return SAVE_MEDIA + +@enforce_types +def save_media(link: Link, out_dir: Optional[Path]=None, timeout: int=MEDIA_TIMEOUT) -> ArchiveResult: + """Download playlists or individual video, audio, and subtitles using youtube-dl""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'media' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + YOUTUBEDL_BINARY, + *YOUTUBEDL_ARGS, + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + chmod_file(output, cwd=str(out_dir)) + if result.returncode: + if (b'ERROR: Unsupported URL' in result.stderr + or b'HTTP Error 404' in result.stderr + or b'HTTP Error 403' in result.stderr + or b'URL could be a direct video link' in result.stderr + or b'Unable to extract container ID' in result.stderr): + # These happen too frequently on non-media pages to warrant printing to console + pass + else: + hints = ( + 'Got youtube-dl response code: {}.'.format(result.returncode), + *result.stderr.decode().split('\n'), + ) + raise ArchiveError('Failed to save media', hints) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=YOUTUBEDL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/mercury.py b/archivebox-0.5.3/archivebox/extractors/mercury.py new file mode 100644 index 0000000..741c329 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/mercury.py @@ -0,0 +1,104 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from subprocess import CompletedProcess +from typing import Optional, List +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + is_static_file, + +) +from ..config import ( + TIMEOUT, + SAVE_MERCURY, + DEPENDENCIES, + MERCURY_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def ShellError(cmd: List[str], result: CompletedProcess, lines: int=20) -> ArchiveError: + # parse out last line of stderr + return ArchiveError( + f'Got {cmd[0]} response code: {result.returncode}).', + *( + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', lines)[-lines:] + if line.strip() + ), + ) + + +@enforce_types +def should_save_mercury(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'mercury' + return SAVE_MERCURY and MERCURY_VERSION and (not output.exists()) + + +@enforce_types +def save_mercury(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @postlight/mercury-parser""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "mercury" + output = str(output_folder) + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + # Get plain text version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url, + "--format=text" + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_text = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + # Get HTML version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_json = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "content.html"), article_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), article_text["content"]) + atomic_write(str(output_folder / "article.json"), article_json) + + # Check for common failure cases + if (result.returncode > 0): + raise ShellError(cmd, result) + except (ArchiveError, Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=MERCURY_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/pdf.py b/archivebox-0.5.3/archivebox/extractors/pdf.py new file mode 100644 index 0000000..1b0201e --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/pdf.py @@ -0,0 +1,68 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_PDF, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_pdf(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "output.pdf").exists(): + return False + + return SAVE_PDF + + +@enforce_types +def save_pdf(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print PDF of site to file using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.pdf' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--print-to-pdf', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save PDF', hints) + + chmod_file('output.pdf', cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/readability.py b/archivebox-0.5.3/archivebox/extractors/readability.py new file mode 100644 index 0000000..9da620b --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/readability.py @@ -0,0 +1,124 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from tempfile import NamedTemporaryFile + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + download_url, + is_static_file, + +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + SAVE_READABILITY, + DEPENDENCIES, + READABILITY_VERSION, +) +from ..logging_util import TimedProgress + +@enforce_types +def get_html(link: Link, path: Path) -> str: + """ + Try to find wget, singlefile and then dom files. + If none is found, download the url again. + """ + canonical = link.canonical_outputs() + abs_path = path.absolute() + sources = [canonical["singlefile_path"], canonical["wget_path"], canonical["dom_path"]] + document = None + for source in sources: + try: + with open(abs_path / source, "r") as f: + document = f.read() + break + except (FileNotFoundError, TypeError): + continue + if document is None: + return download_url(link.url) + else: + return document + +@enforce_types +def should_save_readability(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'readability' + return SAVE_READABILITY and READABILITY_VERSION and (not output.exists()) + + +@enforce_types +def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @mozilla/readability""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "readability" + output = str(output_folder) + + # Readability Docs: https://github.com/mozilla/readability + + status = 'succeeded' + # fake command to show the user so they have something to try debugging if get_html fails + cmd = [ + CURL_BINARY, + link.url + ] + readability_content = None + timer = TimedProgress(timeout, prefix=' ') + try: + document = get_html(link, out_dir) + temp_doc = NamedTemporaryFile(delete=False) + temp_doc.write(document.encode("utf-8")) + temp_doc.close() + + cmd = [ + DEPENDENCIES['READABILITY_BINARY']['path'], + temp_doc.name + ] + + result = run(cmd, cwd=out_dir, timeout=timeout) + result_json = json.loads(result.stdout) + output_folder.mkdir(exist_ok=True) + readability_content = result_json.pop("textContent") + atomic_write(str(output_folder / "content.html"), result_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), readability_content) + atomic_write(str(output_folder / "article.json"), result_json) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got readability response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('Readability was not able to archive the page', hints) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=READABILITY_VERSION, + output=output, + status=status, + index_texts= [readability_content] if readability_content else [], + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/screenshot.py b/archivebox-0.5.3/archivebox/extractors/screenshot.py new file mode 100644 index 0000000..325584e --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/screenshot.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SCREENSHOT, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_screenshot(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "screenshot.png").exists(): + return False + + return SAVE_SCREENSHOT + +@enforce_types +def save_screenshot(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """take screenshot of site using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'screenshot.png' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--screenshot', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save screenshot', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/singlefile.py b/archivebox-0.5.3/archivebox/extractors/singlefile.py new file mode 100644 index 0000000..2e5c389 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/singlefile.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SINGLEFILE, + DEPENDENCIES, + SINGLEFILE_VERSION, + CHROME_BINARY, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_singlefile(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + output = out_dir / 'singlefile.html' + return SAVE_SINGLEFILE and SINGLEFILE_VERSION and (not output.exists()) + + +@enforce_types +def save_singlefile(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using single-file""" + + out_dir = out_dir or Path(link.link_dir) + output = str(out_dir.absolute() / "singlefile.html") + + browser_args = chrome_args(TIMEOUT=0) + + # SingleFile CLI Docs: https://github.com/gildas-lormeau/SingleFile/tree/master/cli + browser_args = '--browser-args={}'.format(json.dumps(browser_args[1:])) + cmd = [ + DEPENDENCIES['SINGLEFILE_BINARY']['path'], + '--browser-executable-path={}'.format(CHROME_BINARY), + browser_args, + link.url, + output + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got single-file response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('SingleFile was not able to archive the page', hints) + chmod_file(output) + except (Exception, OSError) as err: + status = 'failed' + # TODO: Make this prettier. This is necessary to run the command (escape JSON internal quotes). + cmd[2] = browser_args.replace('"', "\\\"") + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=SINGLEFILE_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/title.py b/archivebox-0.5.3/archivebox/extractors/title.py new file mode 100644 index 0000000..28cb128 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/title.py @@ -0,0 +1,130 @@ +__package__ = 'archivebox.extractors' + +import re +from html.parser import HTMLParser +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..util import ( + enforce_types, + is_static_file, + download_url, + htmldecode, +) +from ..config import ( + TIMEOUT, + CHECK_SSL_VALIDITY, + SAVE_TITLE, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +HTML_TITLE_REGEX = re.compile( + r'' # start matching text after tag + r'(.[^<>]+)', # get everything up to these symbols + re.IGNORECASE | re.MULTILINE | re.DOTALL | re.UNICODE, +) + + +class TitleParser(HTMLParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.title_tag = "" + self.title_og = "" + self.inside_title_tag = False + + @property + def title(self): + return self.title_tag or self.title_og or None + + def handle_starttag(self, tag, attrs): + if tag.lower() == "title" and not self.title_tag: + self.inside_title_tag = True + elif tag.lower() == "meta" and not self.title_og: + attrs = dict(attrs) + if attrs.get("property") == "og:title" and attrs.get("content"): + self.title_og = attrs.get("content") + + def handle_data(self, data): + if self.inside_title_tag and data: + self.title_tag += data.strip() + + def handle_endtag(self, tag): + if tag.lower() == "title": + self.inside_title_tag = False + + +@enforce_types +def should_save_title(link: Link, out_dir: Optional[str]=None) -> bool: + # if link already has valid title, skip it + if link.title and not link.title.lower().startswith('http'): + return False + + if is_static_file(link.url): + return False + + return SAVE_TITLE + +def extract_title_with_regex(html): + match = re.search(HTML_TITLE_REGEX, html) + output = htmldecode(match.group(1).strip()) if match else None + return output + +@enforce_types +def save_title(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """try to guess the page's title from its content""" + + from core.models import Snapshot + + output: ArchiveOutput = None + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + html = download_url(link.url, timeout=timeout) + try: + # try using relatively strict html parser first + parser = TitleParser() + parser.feed(html) + output = parser.title + if output is None: + raise + except Exception: + # fallback to regex that can handle broken/malformed html + output = extract_title_with_regex(html) + + # if title is better than the one in the db, update db with new title + if isinstance(output, str) and output: + if not link.title or len(output) >= len(link.title): + Snapshot.objects.filter(url=link.url, + timestamp=link.timestamp)\ + .update(title=output) + else: + raise ArchiveError('Unable to detect page title') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/archivebox/extractors/wget.py b/archivebox-0.5.3/archivebox/extractors/wget.py new file mode 100644 index 0000000..331f636 --- /dev/null +++ b/archivebox-0.5.3/archivebox/extractors/wget.py @@ -0,0 +1,184 @@ +__package__ = 'archivebox.extractors' + +import re +from pathlib import Path + +from typing import Optional +from datetime import datetime + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + without_scheme, + without_fragment, + without_query, + path, + domain, + urldecode, +) +from ..config import ( + WGET_ARGS, + TIMEOUT, + SAVE_WGET, + SAVE_WARC, + WGET_BINARY, + WGET_VERSION, + RESTRICT_FILE_NAMES, + CHECK_SSL_VALIDITY, + SAVE_WGET_REQUISITES, + WGET_AUTO_COMPRESSION, + WGET_USER_AGENT, + COOKIES_FILE, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_wget(link: Link, out_dir: Optional[Path]=None) -> bool: + output_path = wget_output_path(link) + out_dir = out_dir or Path(link.link_dir) + if output_path and (out_dir / output_path).exists(): + return False + + return SAVE_WGET + + +@enforce_types +def save_wget(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using wget""" + + out_dir = out_dir or link.link_dir + if SAVE_WARC: + warc_dir = out_dir / "warc" + warc_dir.mkdir(exist_ok=True) + warc_path = warc_dir / str(int(datetime.now().timestamp())) + + # WGET CLI Docs: https://www.gnu.org/software/wget/manual/wget.html + output: ArchiveOutput = None + cmd = [ + WGET_BINARY, + # '--server-response', # print headers for better error parsing + *WGET_ARGS, + '--timeout={}'.format(timeout), + *(['--restrict-file-names={}'.format(RESTRICT_FILE_NAMES)] if RESTRICT_FILE_NAMES else []), + *(['--warc-file={}'.format(str(warc_path))] if SAVE_WARC else []), + *(['--page-requisites'] if SAVE_WGET_REQUISITES else []), + *(['--user-agent={}'.format(WGET_USER_AGENT)] if WGET_USER_AGENT else []), + *(['--load-cookies', COOKIES_FILE] if COOKIES_FILE else []), + *(['--compression=auto'] if WGET_AUTO_COMPRESSION else []), + *([] if SAVE_WARC else ['--timestamping']), + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate', '--no-hsts']), + link.url, + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + output = wget_output_path(link) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + files_downloaded = ( + int(output_tail[-1].strip().split(' ', 2)[1] or 0) + if 'Downloaded:' in output_tail[-1] + else 0 + ) + hints = ( + 'Got wget response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0 and files_downloaded < 1) or output is None: + if b'403: Forbidden' in result.stderr: + raise ArchiveError('403 Forbidden (try changing WGET_USER_AGENT)', hints) + if b'404: Not Found' in result.stderr: + raise ArchiveError('404 Not Found', hints) + if b'ERROR 500: Internal Server Error' in result.stderr: + raise ArchiveError('500 Internal Server Error', hints) + raise ArchiveError('Wget failed or got an error from the server', hints) + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=WGET_VERSION, + output=output, + status=status, + **timer.stats, + ) + + +@enforce_types +def wget_output_path(link: Link) -> Optional[str]: + """calculate the path to the wgetted .html file, since wget may + adjust some paths to be different than the base_url path. + + See docs on wget --adjust-extension (-E) + """ + if is_static_file(link.url): + return without_scheme(without_fragment(link.url)) + + # Wget downloads can save in a number of different ways depending on the url: + # https://example.com + # > example.com/index.html + # https://example.com?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + # https://www.example.com/?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc + # > example.com/abc.html + # https://example.com/abc/ + # > example.com/abc/index.html + # https://example.com/abc?v=zzVa_tX1OiI.html + # > example.com/abc?v=zzVa_tX1OiI.html + # https://example.com/abc/?v=zzVa_tX1OiI.html + # > example.com/abc/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc/test.html + # > example.com/abc/test.html + # https://example.com/abc/test?v=zzVa_tX1OiI + # > example.com/abc/test?v=zzVa_tX1OiI.html + # https://example.com/abc/test/?v=zzVa_tX1OiI + # > example.com/abc/test/index.html?v=zzVa_tX1OiI.html + + # There's also lots of complexity around how the urlencoding and renaming + # is done for pages with query and hash fragments or extensions like shtml / htm / php / etc + + # Since the wget algorithm for -E (appending .html) is incredibly complex + # and there's no way to get the computed output path from wget + # in order to avoid having to reverse-engineer how they calculate it, + # we just look in the output folder read the filename wget used from the filesystem + full_path = without_fragment(without_query(path(link.url))).strip('/') + search_dir = Path(link.link_dir) / domain(link.url).replace(":", "+") / urldecode(full_path) + for _ in range(4): + if search_dir.exists(): + if search_dir.is_dir(): + html_files = [ + f for f in search_dir.iterdir() + if re.search(".+\\.[Ss]?[Hh][Tt][Mm][Ll]?$", str(f), re.I | re.M) + ] + if html_files: + return str(html_files[0].relative_to(link.link_dir)) + + # Move up one directory level + search_dir = search_dir.parent + + if str(search_dir) == link.link_dir: + break + + return None diff --git a/archivebox-0.5.3/archivebox/index/__init__.py b/archivebox-0.5.3/archivebox/index/__init__.py new file mode 100644 index 0000000..8eab1d3 --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/__init__.py @@ -0,0 +1,617 @@ +__package__ = 'archivebox.index' + +import os +import shutil +import json as pyjson +from pathlib import Path + +from itertools import chain +from typing import List, Tuple, Dict, Optional, Iterable +from collections import OrderedDict +from contextlib import contextmanager +from urllib.parse import urlparse +from django.db.models import QuerySet, Q + +from ..util import ( + scheme, + enforce_types, + ExtendedEncoder, +) +from ..config import ( + ARCHIVE_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + OUTPUT_DIR, + TIMEOUT, + URL_BLACKLIST_PTN, + stderr, + OUTPUT_PERMISSIONS +) +from ..logging_util import ( + TimedProgress, + log_indexing_process_started, + log_indexing_process_finished, + log_indexing_started, + log_indexing_finished, + log_parsing_finished, + log_deduping_finished, +) + +from .schema import Link, ArchiveResult +from .html import ( + write_html_link_details, +) +from .json import ( + parse_json_link_details, + write_json_link_details, +) +from .sql import ( + write_sql_main_index, + write_sql_link_details, +) + +from ..search import search_backend_enabled, query_search_index + +### Link filtering and checking + +@enforce_types +def merge_links(a: Link, b: Link) -> Link: + """deterministially merge two links, favoring longer field values over shorter, + and "cleaner" values over worse ones. + """ + assert a.base_url == b.base_url, f'Cannot merge two links with different URLs ({a.base_url} != {b.base_url})' + + # longest url wins (because a fuzzy url will always be shorter) + url = a.url if len(a.url) > len(b.url) else b.url + + # best title based on length and quality + possible_titles = [ + title + for title in (a.title, b.title) + if title and title.strip() and '://' not in title + ] + title = None + if len(possible_titles) == 2: + title = max(possible_titles, key=lambda t: len(t)) + elif len(possible_titles) == 1: + title = possible_titles[0] + + # earliest valid timestamp + timestamp = ( + a.timestamp + if float(a.timestamp or 0) < float(b.timestamp or 0) else + b.timestamp + ) + + # all unique, truthy tags + tags_set = ( + set(tag.strip() for tag in (a.tags or '').split(',')) + | set(tag.strip() for tag in (b.tags or '').split(',')) + ) + tags = ','.join(tags_set) or None + + # all unique source entries + sources = list(set(a.sources + b.sources)) + + # all unique history entries for the combined archive methods + all_methods = set(list(a.history.keys()) + list(a.history.keys())) + history = { + method: (a.history.get(method) or []) + (b.history.get(method) or []) + for method in all_methods + } + for method in all_methods: + deduped_jsons = { + pyjson.dumps(result, sort_keys=True, cls=ExtendedEncoder) + for result in history[method] + } + history[method] = list(reversed(sorted( + (ArchiveResult.from_json(pyjson.loads(result)) for result in deduped_jsons), + key=lambda result: result.start_ts, + ))) + + return Link( + url=url, + timestamp=timestamp, + title=title, + tags=tags, + sources=sources, + history=history, + ) + + +@enforce_types +def validate_links(links: Iterable[Link]) -> List[Link]: + timer = TimedProgress(TIMEOUT * 4) + try: + links = archivable_links(links) # remove chrome://, about:, mailto: etc. + links = sorted_links(links) # deterministically sort the links based on timestamp, url + links = fix_duplicate_links(links) # merge/dedupe duplicate timestamps & urls + finally: + timer.end() + + return list(links) + +@enforce_types +def archivable_links(links: Iterable[Link]) -> Iterable[Link]: + """remove chrome://, about:// or other schemed links that cant be archived""" + for link in links: + try: + urlparse(link.url) + except ValueError: + continue + if scheme(link.url) not in ('http', 'https', 'ftp'): + continue + if URL_BLACKLIST_PTN and URL_BLACKLIST_PTN.search(link.url): + continue + + yield link + + +@enforce_types +def fix_duplicate_links(sorted_links: Iterable[Link]) -> Iterable[Link]: + """ + ensures that all non-duplicate links have monotonically increasing timestamps + """ + # from core.models import Snapshot + + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in sorted_links: + if link.url in unique_urls: + # merge with any other links that share the same url + link = merge_links(unique_urls[link.url], link) + unique_urls[link.url] = link + + return unique_urls.values() + + +@enforce_types +def sorted_links(links: Iterable[Link]) -> Iterable[Link]: + sort_func = lambda link: (link.timestamp.split('.', 1)[0], link.url) + return sorted(links, key=sort_func, reverse=True) + + +@enforce_types +def links_after_timestamp(links: Iterable[Link], resume: Optional[float]=None) -> Iterable[Link]: + if not resume: + yield from links + return + + for link in links: + try: + if float(link.timestamp) <= resume: + yield link + except (ValueError, TypeError): + print('Resume value and all timestamp values must be valid numbers.') + + +@enforce_types +def lowest_uniq_timestamp(used_timestamps: OrderedDict, timestamp: str) -> str: + """resolve duplicate timestamps by appending a decimal 1234, 1234 -> 1234.1, 1234.2""" + + timestamp = timestamp.split('.')[0] + nonce = 0 + + # first try 152323423 before 152323423.0 + if timestamp not in used_timestamps: + return timestamp + + new_timestamp = '{}.{}'.format(timestamp, nonce) + while new_timestamp in used_timestamps: + nonce += 1 + new_timestamp = '{}.{}'.format(timestamp, nonce) + + return new_timestamp + + + +### Main Links Index + +@contextmanager +@enforce_types +def timed_index_update(out_path: Path): + log_indexing_started(out_path) + timer = TimedProgress(TIMEOUT * 2, prefix=' ') + try: + yield + finally: + timer.end() + + assert out_path.exists(), f'Failed to write index file: {out_path}' + log_indexing_finished(out_path) + + +@enforce_types +def write_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + """Writes links to sqlite3 file for a given list of links""" + + log_indexing_process_started(len(links)) + + try: + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + + except (KeyboardInterrupt, SystemExit): + stderr('[!] Warning: Still writing index to disk...', color='lightyellow') + stderr(' Run archivebox init to fix any inconsistencies from an ungraceful exit.') + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + raise SystemExit(0) + + log_indexing_process_finished() + +@enforce_types +def load_main_index(out_dir: Path=OUTPUT_DIR, warn: bool=True) -> List[Link]: + """parse and load existing index with any new links from import_path merged in""" + from core.models import Snapshot + try: + return Snapshot.objects.all() + + except (KeyboardInterrupt, SystemExit): + raise SystemExit(0) + +@enforce_types +def load_main_index_meta(out_dir: Path=OUTPUT_DIR) -> Optional[dict]: + index_path = out_dir / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + meta_dict = pyjson.load(f) + meta_dict.pop('links') + return meta_dict + + return None + + +@enforce_types +def parse_links_from_source(source_path: str, root_url: Optional[str]=None) -> Tuple[List[Link], List[Link]]: + + from ..parsers import parse_links + + new_links: List[Link] = [] + + # parse and validate the import file + raw_links, parser_name = parse_links(source_path, root_url=root_url) + new_links = validate_links(raw_links) + + if parser_name: + num_parsed = len(raw_links) + log_parsing_finished(num_parsed, parser_name) + + return new_links + +@enforce_types +def fix_duplicate_links_in_index(snapshots: QuerySet, links: Iterable[Link]) -> Iterable[Link]: + """ + Given a list of in-memory Links, dedupe and merge them with any conflicting Snapshots in the DB. + """ + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in links: + index_link = snapshots.filter(url=link.url) + if index_link: + link = merge_links(index_link[0].as_link(), link) + + unique_urls[link.url] = link + + return unique_urls.values() + +@enforce_types +def dedupe_links(snapshots: QuerySet, + new_links: List[Link]) -> List[Link]: + """ + The validation of links happened at a different stage. This method will + focus on actual deduplication and timestamp fixing. + """ + + # merge existing links in out_dir and new links + dedup_links = fix_duplicate_links_in_index(snapshots, new_links) + + new_links = [ + link for link in new_links + if not snapshots.filter(url=link.url).exists() + ] + + dedup_links_dict = {link.url: link for link in dedup_links} + + # Replace links in new_links with the dedup version + for i in range(len(new_links)): + if new_links[i].url in dedup_links_dict.keys(): + new_links[i] = dedup_links_dict[new_links[i].url] + log_deduping_finished(len(new_links)) + + return new_links + +### Link Details Index + +@enforce_types +def write_link_details(link: Link, out_dir: Optional[str]=None, skip_sql_index: bool=False) -> None: + out_dir = out_dir or link.link_dir + + write_json_link_details(link, out_dir=out_dir) + write_html_link_details(link, out_dir=out_dir) + if not skip_sql_index: + write_sql_link_details(link) + + +@enforce_types +def load_link_details(link: Link, out_dir: Optional[str]=None) -> Link: + """check for an existing link archive in the given directory, + and load+merge it into the given link dict + """ + out_dir = out_dir or link.link_dir + + existing_link = parse_json_link_details(out_dir) + if existing_link: + return merge_links(existing_link, link) + + return link + + + +LINK_FILTERS = { + 'exact': lambda pattern: Q(url=pattern), + 'substring': lambda pattern: Q(url__icontains=pattern), + 'regex': lambda pattern: Q(url__iregex=pattern), + 'domain': lambda pattern: Q(url__istartswith=f"http://{pattern}") | Q(url__istartswith=f"https://{pattern}") | Q(url__istartswith=f"ftp://{pattern}"), + 'tag': lambda pattern: Q(tags__name=pattern), +} + +@enforce_types +def q_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + q_filter = Q() + for pattern in filter_patterns: + try: + q_filter = q_filter | LINK_FILTERS[filter_type](pattern) + except KeyError: + stderr() + stderr( + f'[X] Got invalid pattern for --filter-type={filter_type}:', + color='red', + ) + stderr(f' {pattern}') + raise SystemExit(2) + return snapshots.filter(q_filter) + +def search_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='search') -> QuerySet: + if not search_backend_enabled(): + stderr() + stderr( + '[X] The search backend is not enabled, set config.USE_SEARCHING_BACKEND = True', + color='red', + ) + raise SystemExit(2) + from core.models import Snapshot + + qsearch = Snapshot.objects.none() + for pattern in filter_patterns: + try: + qsearch |= query_search_index(pattern) + except: + raise SystemExit(2) + + return snapshots & qsearch + +@enforce_types +def snapshot_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + if filter_type != 'search': + return q_filter(snapshots, filter_patterns, filter_type) + else: + return search_filter(snapshots, filter_patterns, filter_type) + + +def get_indexed_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links without checking archive status or data directory validity""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in links + } + +def get_archived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are archived with a valid data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_archived, links) + } + +def get_unarchived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are unarchived with no data directory or an empty data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_unarchived, links) + } + +def get_present_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that actually exist in the archive/ folder""" + + all_folders = {} + + for entry in (out_dir / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(entry.path) + except Exception: + pass + + all_folders[entry.name] = link + + return all_folders + +def get_valid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs with a valid index matched to the main index and archived content""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_valid, links) + } + +def get_invalid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that are invalid for any reason: corrupted/duplicate/orphaned/unrecognized""" + duplicate = get_duplicate_folders(snapshots, out_dir=OUTPUT_DIR) + orphaned = get_orphaned_folders(snapshots, out_dir=OUTPUT_DIR) + corrupted = get_corrupted_folders(snapshots, out_dir=OUTPUT_DIR) + unrecognized = get_unrecognized_folders(snapshots, out_dir=OUTPUT_DIR) + return {**duplicate, **orphaned, **corrupted, **unrecognized} + + +def get_duplicate_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that conflict with other directories that have the same link URL or timestamp""" + by_url = {} + by_timestamp = {} + duplicate_folders = {} + + data_folders = ( + str(entry) + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir() + if entry.is_dir() and not snapshots.filter(timestamp=entry.name).exists() + ) + + for path in chain(snapshots.iterator(), data_folders): + link = None + if type(path) is not str: + path = path.as_link().link_dir + + try: + link = parse_json_link_details(path) + except Exception: + pass + + if link: + # link folder has same timestamp as different link folder + by_timestamp[link.timestamp] = by_timestamp.get(link.timestamp, 0) + 1 + if by_timestamp[link.timestamp] > 1: + duplicate_folders[path] = link + + # link folder has same url as different link folder + by_url[link.url] = by_url.get(link.url, 0) + 1 + if by_url[link.url] > 1: + duplicate_folders[path] = link + return duplicate_folders + +def get_orphaned_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that contain a valid index but aren't listed in the main index""" + orphaned_folders = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if link and not snapshots.filter(timestamp=entry.name).exists(): + # folder is a valid link data dir with index details, but it's not in the main index + orphaned_folders[str(entry)] = link + + return orphaned_folders + +def get_corrupted_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain a valid index and aren't listed in the main index""" + corrupted = {} + for snapshot in snapshots.iterator(): + link = snapshot.as_link() + if is_corrupt(link): + corrupted[link.link_dir] = link + return corrupted + +def get_unrecognized_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain recognizable archive data and aren't listed in the main index""" + unrecognized_folders: Dict[str, Optional[Link]] = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + index_exists = (entry / "index.json").exists() + link = None + try: + link = parse_json_link_details(str(entry)) + except KeyError: + # Try to fix index + if index_exists: + try: + # Last attempt to repair the detail index + link_guessed = parse_json_link_details(str(entry), guess=True) + write_json_link_details(link_guessed, out_dir=str(entry)) + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if index_exists and link is None: + # index exists but it's corrupted or unparseable + unrecognized_folders[str(entry)] = link + + elif not index_exists: + # link details index doesn't exist and the folder isn't in the main index + timestamp = entry.name + if not snapshots.filter(timestamp=timestamp).exists(): + unrecognized_folders[str(entry)] = link + + return unrecognized_folders + + +def is_valid(link: Link) -> bool: + dir_exists = Path(link.link_dir).exists() + index_exists = (Path(link.link_dir) / "index.json").exists() + if not dir_exists: + # unarchived links are not included in the valid list + return False + if dir_exists and not index_exists: + return False + if dir_exists and index_exists: + try: + parsed_link = parse_json_link_details(link.link_dir, guess=True) + return link.url == parsed_link.url + except Exception: + pass + return False + +def is_corrupt(link: Link) -> bool: + if not Path(link.link_dir).exists(): + # unarchived links are not considered corrupt + return False + + if is_valid(link): + return False + + return True + +def is_archived(link: Link) -> bool: + return is_valid(link) and link.is_archived + +def is_unarchived(link: Link) -> bool: + if not Path(link.link_dir).exists(): + return True + return not link.is_archived + + +def fix_invalid_folder_locations(out_dir: Path=OUTPUT_DIR) -> Tuple[List[str], List[str]]: + fixed = [] + cant_fix = [] + for entry in os.scandir(out_dir / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if not link: + continue + + if not entry.path.endswith(f'/{link.timestamp}'): + dest = out_dir / ARCHIVE_DIR_NAME / link.timestamp + if dest.exists(): + cant_fix.append(entry.path) + else: + shutil.move(entry.path, dest) + fixed.append(dest) + timestamp = entry.path.rsplit('/', 1)[-1] + assert link.link_dir == entry.path + assert link.timestamp == timestamp + write_json_link_details(link, out_dir=entry.path) + + return fixed, cant_fix diff --git a/archivebox-0.5.3/archivebox/index/csv.py b/archivebox-0.5.3/archivebox/index/csv.py new file mode 100644 index 0000000..804e646 --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/csv.py @@ -0,0 +1,37 @@ +__package__ = 'archivebox.index' + +from typing import List, Optional, Any + +from ..util import enforce_types +from .schema import Link + + +@enforce_types +def links_to_csv(links: List[Link], + cols: Optional[List[str]]=None, + header: bool=True, + separator: str=',', + ljust: int=0) -> str: + + cols = cols or ['timestamp', 'is_archived', 'url'] + + header_str = '' + if header: + header_str = separator.join(col.ljust(ljust) for col in cols) + + row_strs = ( + link.to_csv(cols=cols, ljust=ljust, separator=separator) + for link in links + ) + + return '\n'.join((header_str, *row_strs)) + + +@enforce_types +def to_csv(obj: Any, cols: List[str], separator: str=',', ljust: int=0) -> str: + from .json import to_json + + return separator.join( + to_json(getattr(obj, col), indent=None).ljust(ljust) + for col in cols + ) diff --git a/archivebox-0.5.3/archivebox/index/html.py b/archivebox-0.5.3/archivebox/index/html.py new file mode 100644 index 0000000..a62e2c7 --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/html.py @@ -0,0 +1,164 @@ +__package__ = 'archivebox.index' + +from datetime import datetime +from typing import List, Optional, Iterator, Mapping +from pathlib import Path + +from django.utils.html import format_html +from collections import defaultdict + +from .schema import Link +from ..system import atomic_write +from ..logging_util import printable_filesize +from ..util import ( + enforce_types, + ts_to_date, + urlencode, + htmlencode, + urldecode, +) +from ..config import ( + OUTPUT_DIR, + VERSION, + GIT_SHA, + FOOTER_INFO, + HTML_INDEX_FILENAME, +) + +MAIN_INDEX_TEMPLATE = 'main_index.html' +MINIMAL_INDEX_TEMPLATE = 'main_index_minimal.html' +LINK_DETAILS_TEMPLATE = 'link_details.html' +TITLE_LOADING_MSG = 'Not yet archived...' + + +### Main Links Index + +@enforce_types +def parse_html_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[str]: + """parse an archive index html file and return the list of urls""" + + index_path = Path(out_dir) / HTML_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + for line in f: + if 'class="link-url"' in line: + yield line.split('"')[1] + return () + +@enforce_types +def generate_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = main_index_template(links) + else: + output = main_index_template(links, template=MINIMAL_INDEX_TEMPLATE) + return output + +@enforce_types +def main_index_template(links: List[Link], template: str=MAIN_INDEX_TEMPLATE) -> str: + """render the template for the entire main index""" + + return render_django_template(template, { + 'version': VERSION, + 'git_sha': GIT_SHA, + 'num_links': str(len(links)), + 'date_updated': datetime.now().strftime('%Y-%m-%d'), + 'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'), + 'links': [link._asdict(extended=True) for link in links], + 'FOOTER_INFO': FOOTER_INFO, + }) + + +### Link Details Index + +@enforce_types +def write_html_link_details(link: Link, out_dir: Optional[str]=None) -> None: + out_dir = out_dir or link.link_dir + + rendered_html = link_details_template(link) + atomic_write(str(Path(out_dir) / HTML_INDEX_FILENAME), rendered_html) + + +@enforce_types +def link_details_template(link: Link) -> str: + + from ..extractors.wget import wget_output_path + + link_info = link._asdict(extended=True) + + return render_django_template(LINK_DETAILS_TEMPLATE, { + **link_info, + **link_info['canonical'], + 'title': htmlencode( + link.title + or (link.base_url if link.is_archived else TITLE_LOADING_MSG) + ), + 'url_str': htmlencode(urldecode(link.base_url)), + 'archive_url': urlencode( + wget_output_path(link) + or (link.domain if link.is_archived else '') + ) or 'about:blank', + 'extension': link.extension or 'html', + 'tags': link.tags or 'untagged', + 'size': printable_filesize(link.archive_size) if link.archive_size else 'pending', + 'status': 'archived' if link.is_archived else 'not yet archived', + 'status_color': 'success' if link.is_archived else 'danger', + 'oldest_archive_date': ts_to_date(link.oldest_archive_date), + }) + +@enforce_types +def render_django_template(template: str, context: Mapping[str, str]) -> str: + """render a given html template string with the given template content""" + from django.template.loader import render_to_string + + return render_to_string(template, context) + + +def snapshot_icons(snapshot) -> str: + from core.models import EXTRACTORS + + archive_results = snapshot.archiveresult_set.filter(status="succeeded") + link = snapshot.as_link() + path = link.archive_path + canon = link.canonical_outputs() + output = "" + output_template = '<a href="/{}/{}" class="exists-{}" title="{}">{} </a>' + icons = { + "singlefile": "❶", + "wget": "🆆", + "dom": "🅷", + "pdf": "📄", + "screenshot": "💻", + "media": "📼", + "git": "🅶", + "archive_org": "🏛", + "readability": "🆁", + "mercury": "🅼", + "warc": "📦" + } + exclude = ["favicon", "title", "headers", "archive_org"] + # Missing specific entry for WARC + + extractor_items = defaultdict(lambda: None) + for extractor, _ in EXTRACTORS: + for result in archive_results: + if result.extractor == extractor: + extractor_items[extractor] = result + + for extractor, _ in EXTRACTORS: + if extractor not in exclude: + exists = extractor_items[extractor] is not None + output += output_template.format(path, canon[f"{extractor}_path"], str(exists), + extractor, icons.get(extractor, "?")) + if extractor == "wget": + # warc isn't technically it's own extractor, so we have to add it after wget + exists = list((Path(path) / canon["warc_path"]).glob("*.warc.gz")) + output += output_template.format(exists[0] if exists else '#', canon["warc_path"], str(bool(exists)), "warc", icons.get("warc", "?")) + + if extractor == "archive_org": + # The check for archive_org is different, so it has to be handled separately + target_path = Path(path) / "archive.org.txt" + exists = target_path.exists() + output += '<a href="{}" class="exists-{}" title="{}">{}</a> '.format(canon["archive_org_path"], str(exists), + "archive_org", icons.get("archive_org", "?")) + + return format_html(f'<span class="files-icons" style="font-size: 1.1em; opacity: 0.8">{output}<span>') diff --git a/archivebox-0.5.3/archivebox/index/json.py b/archivebox-0.5.3/archivebox/index/json.py new file mode 100644 index 0000000..f24b969 --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/json.py @@ -0,0 +1,154 @@ +__package__ = 'archivebox.index' + +import os +import sys +import json as pyjson +from pathlib import Path + +from datetime import datetime +from typing import List, Optional, Iterator, Any, Union + +from .schema import Link +from ..system import atomic_write +from ..util import enforce_types +from ..config import ( + VERSION, + OUTPUT_DIR, + FOOTER_INFO, + GIT_SHA, + DEPENDENCIES, + JSON_INDEX_FILENAME, + ARCHIVE_DIR_NAME, + ANSI +) + + +MAIN_INDEX_HEADER = { + 'info': 'This is an index of site data archived by ArchiveBox: The self-hosted web archive.', + 'schema': 'archivebox.index.json', + 'copyright_info': FOOTER_INFO, + 'meta': { + 'project': 'ArchiveBox', + 'version': VERSION, + 'git_sha': GIT_SHA, + 'website': 'https://ArchiveBox.io', + 'docs': 'https://github.com/ArchiveBox/ArchiveBox/wiki', + 'source': 'https://github.com/ArchiveBox/ArchiveBox', + 'issues': 'https://github.com/ArchiveBox/ArchiveBox/issues', + 'dependencies': DEPENDENCIES, + }, +} + +@enforce_types +def generate_json_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = { + **MAIN_INDEX_HEADER, + 'num_links': len(links), + 'updated': datetime.now(), + 'last_run_cmd': sys.argv, + 'links': links, + } + else: + output = links + return to_json(output, indent=4, sort_keys=True) + + +@enforce_types +def parse_json_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + """parse an archive index json file and return the list of links""" + + index_path = Path(out_dir) / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + links = pyjson.load(f)['links'] + for link_json in links: + try: + yield Link.from_json(link_json) + except KeyError: + try: + detail_index_path = Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / link_json['timestamp'] + yield parse_json_link_details(str(detail_index_path)) + except KeyError: + # as a last effort, try to guess the missing values out of existing ones + try: + yield Link.from_json(link_json, guess=True) + except KeyError: + print(" {lightyellow}! Failed to load the index.json from {}".format(detail_index_path, **ANSI)) + continue + return () + +### Link Details Index + +@enforce_types +def write_json_link_details(link: Link, out_dir: Optional[str]=None) -> None: + """write a json file with some info about the link""" + + out_dir = out_dir or link.link_dir + path = Path(out_dir) / JSON_INDEX_FILENAME + atomic_write(str(path), link._asdict(extended=True)) + + +@enforce_types +def parse_json_link_details(out_dir: Union[Path, str], guess: Optional[bool]=False) -> Optional[Link]: + """load the json link index from a given directory""" + existing_index = Path(out_dir) / JSON_INDEX_FILENAME + if existing_index.exists(): + with open(existing_index, 'r', encoding='utf-8') as f: + try: + link_json = pyjson.load(f) + return Link.from_json(link_json, guess) + except pyjson.JSONDecodeError: + pass + return None + + +@enforce_types +def parse_json_links_details(out_dir: Union[Path, str]) -> Iterator[Link]: + """read through all the archive data folders and return the parsed links""" + + for entry in os.scandir(Path(out_dir) / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if link: + yield link + + + +### Helpers + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + + +@enforce_types +def to_json(obj: Any, indent: Optional[int]=4, sort_keys: bool=True, cls=ExtendedEncoder) -> str: + return pyjson.dumps(obj, indent=indent, sort_keys=sort_keys, cls=ExtendedEncoder) + diff --git a/archivebox-0.5.3/archivebox/index/schema.py b/archivebox-0.5.3/archivebox/index/schema.py new file mode 100644 index 0000000..bc3a25d --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/schema.py @@ -0,0 +1,448 @@ +""" + +WARNING: THIS FILE IS ALL LEGACY CODE TO BE REMOVED. + +DO NOT ADD ANY NEW FEATURES TO THIS FILE, NEW CODE GOES HERE: core/models.py + +""" + +__package__ = 'archivebox.index' + +from pathlib import Path + +from datetime import datetime, timedelta + +from typing import List, Dict, Any, Optional, Union + +from dataclasses import dataclass, asdict, field, fields + + +from ..system import get_dir_size + +from ..config import OUTPUT_DIR, ARCHIVE_DIR_NAME + +class ArchiveError(Exception): + def __init__(self, message, hints=None): + super().__init__(message) + self.hints = hints + +LinkDict = Dict[str, Any] + +ArchiveOutput = Union[str, Exception, None] + +@dataclass(frozen=True) +class ArchiveResult: + cmd: List[str] + pwd: Optional[str] + cmd_version: Optional[str] + output: ArchiveOutput + status: str + start_ts: datetime + end_ts: datetime + index_texts: Union[List[str], None] = None + schema: str = 'ArchiveResult' + + def __post_init__(self): + self.typecheck() + + def _asdict(self): + return asdict(self) + + def typecheck(self) -> None: + assert self.schema == self.__class__.__name__ + assert isinstance(self.status, str) and self.status + assert isinstance(self.start_ts, datetime) + assert isinstance(self.end_ts, datetime) + assert isinstance(self.cmd, list) + assert all(isinstance(arg, str) and arg for arg in self.cmd) + assert self.pwd is None or isinstance(self.pwd, str) and self.pwd + assert self.cmd_version is None or isinstance(self.cmd_version, str) and self.cmd_version + assert self.output is None or isinstance(self.output, (str, Exception)) + if isinstance(self.output, str): + assert self.output + + @classmethod + def guess_ts(_cls, dict_info): + from ..util import parse_date + parsed_timestamp = parse_date(dict_info["timestamp"]) + start_ts = parsed_timestamp + end_ts = parsed_timestamp + timedelta(seconds=int(dict_info["duration"])) + return start_ts, end_ts + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + if guess: + keys = info.keys() + if "start_ts" not in keys: + info["start_ts"], info["end_ts"] = cls.guess_ts(json_info) + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + if "pwd" not in keys: + info["pwd"] = str(Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / json_info["timestamp"]) + if "cmd_version" not in keys: + info["cmd_version"] = "Undefined" + if "cmd" not in keys: + info["cmd"] = [] + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + info['cmd_version'] = info.get('cmd_version') + if type(info["cmd"]) is str: + info["cmd"] = [info["cmd"]] + return cls(**info) + + def to_dict(self, *keys) -> dict: + if keys: + return {k: v for k, v in asdict(self).items() if k in keys} + return asdict(self) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, csv_col=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def duration(self) -> int: + return (self.end_ts - self.start_ts).seconds + +@dataclass(frozen=True) +class Link: + timestamp: str + url: str + title: Optional[str] + tags: Optional[str] + sources: List[str] + history: Dict[str, List[ArchiveResult]] = field(default_factory=lambda: {}) + updated: Optional[datetime] = None + schema: str = 'Link' + + + def __str__(self) -> str: + return f'[{self.timestamp}] {self.url} "{self.title}"' + + def __post_init__(self): + self.typecheck() + + def overwrite(self, **kwargs): + """pure functional version of dict.update that returns a new instance""" + return Link(**{**self._asdict(), **kwargs}) + + def __eq__(self, other): + if not isinstance(other, Link): + return NotImplemented + return self.url == other.url + + def __gt__(self, other): + if not isinstance(other, Link): + return NotImplemented + if not self.timestamp or not other.timestamp: + return + return float(self.timestamp) > float(other.timestamp) + + def typecheck(self) -> None: + from ..config import stderr, ANSI + try: + assert self.schema == self.__class__.__name__ + assert isinstance(self.timestamp, str) and self.timestamp + assert self.timestamp.replace('.', '').isdigit() + assert isinstance(self.url, str) and '://' in self.url + assert self.updated is None or isinstance(self.updated, datetime) + assert self.title is None or (isinstance(self.title, str) and self.title) + assert self.tags is None or isinstance(self.tags, str) + assert isinstance(self.sources, list) + assert all(isinstance(source, str) and source for source in self.sources) + assert isinstance(self.history, dict) + for method, results in self.history.items(): + assert isinstance(method, str) and method + assert isinstance(results, list) + assert all(isinstance(result, ArchiveResult) for result in results) + except Exception: + stderr('{red}[X] Error while loading link! [{}] {} "{}"{reset}'.format(self.timestamp, self.url, self.title, **ANSI)) + raise + + def _asdict(self, extended=False): + info = { + 'schema': 'Link', + 'url': self.url, + 'title': self.title or None, + 'timestamp': self.timestamp, + 'updated': self.updated or None, + 'tags': self.tags or None, + 'sources': self.sources or [], + 'history': self.history or {}, + } + if extended: + info.update({ + 'link_dir': self.link_dir, + 'archive_path': self.archive_path, + + 'hash': self.url_hash, + 'base_url': self.base_url, + 'scheme': self.scheme, + 'domain': self.domain, + 'path': self.path, + 'basename': self.basename, + 'extension': self.extension, + 'is_static': self.is_static, + + 'bookmarked_date': self.bookmarked_date, + 'updated_date': self.updated_date, + 'oldest_archive_date': self.oldest_archive_date, + 'newest_archive_date': self.newest_archive_date, + + 'is_archived': self.is_archived, + 'num_outputs': self.num_outputs, + 'num_failures': self.num_failures, + + 'latest': self.latest_outputs(), + 'canonical': self.canonical_outputs(), + }) + return info + + def as_snapshot(self): + from core.models import Snapshot + return Snapshot.objects.get(url=self.url) + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + info['updated'] = parse_date(info.get('updated')) + info['sources'] = info.get('sources') or [] + + json_history = info.get('history') or {} + cast_history = {} + + for method, method_history in json_history.items(): + cast_history[method] = [] + for json_result in method_history: + assert isinstance(json_result, dict), 'Items in Link["history"][method] must be dicts' + cast_result = ArchiveResult.from_json(json_result, guess) + cast_history[method].append(cast_result) + + info['history'] = cast_history + return cls(**info) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, cols=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def link_dir(self) -> str: + from ..config import CONFIG + return str(Path(CONFIG['ARCHIVE_DIR']) / self.timestamp) + + @property + def archive_path(self) -> str: + from ..config import ARCHIVE_DIR_NAME + return '{}/{}'.format(ARCHIVE_DIR_NAME, self.timestamp) + + @property + def archive_size(self) -> float: + try: + return get_dir_size(self.archive_path)[0] + except Exception: + return 0 + + ### URL Helpers + @property + def url_hash(self): + from ..util import hashurl + + return hashurl(self.url) + + @property + def scheme(self) -> str: + from ..util import scheme + return scheme(self.url) + + @property + def extension(self) -> str: + from ..util import extension + return extension(self.url) + + @property + def domain(self) -> str: + from ..util import domain + return domain(self.url) + + @property + def path(self) -> str: + from ..util import path + return path(self.url) + + @property + def basename(self) -> str: + from ..util import basename + return basename(self.url) + + @property + def base_url(self) -> str: + from ..util import base_url + return base_url(self.url) + + ### Pretty Printing Helpers + @property + def bookmarked_date(self) -> Optional[str]: + from ..util import ts_to_date + + max_ts = (datetime.now() + timedelta(days=30)).timestamp() + + if self.timestamp and self.timestamp.replace('.', '').isdigit(): + if 0 < float(self.timestamp) < max_ts: + return ts_to_date(datetime.fromtimestamp(float(self.timestamp))) + else: + return str(self.timestamp) + return None + + + @property + def updated_date(self) -> Optional[str]: + from ..util import ts_to_date + return ts_to_date(self.updated) if self.updated else None + + @property + def archive_dates(self) -> List[datetime]: + return [ + result.start_ts + for method in self.history.keys() + for result in self.history[method] + ] + + @property + def oldest_archive_date(self) -> Optional[datetime]: + return min(self.archive_dates, default=None) + + @property + def newest_archive_date(self) -> Optional[datetime]: + return max(self.archive_dates, default=None) + + ### Archive Status Helpers + @property + def num_outputs(self) -> int: + return self.as_snapshot().num_outputs + + @property + def num_failures(self) -> int: + return sum(1 + for method in self.history.keys() + for result in self.history[method] + if result.status == 'failed') + + @property + def is_static(self) -> bool: + from ..util import is_static_file + return is_static_file(self.url) + + @property + def is_archived(self) -> bool: + from ..config import ARCHIVE_DIR + from ..util import domain + + output_paths = ( + domain(self.url), + 'output.pdf', + 'screenshot.png', + 'output.html', + 'media', + 'singlefile.html' + ) + + return any( + (Path(ARCHIVE_DIR) / self.timestamp / path).exists() + for path in output_paths + ) + + def latest_outputs(self, status: str=None) -> Dict[str, ArchiveOutput]: + """get the latest output that each archive method produced for link""" + + ARCHIVE_METHODS = ( + 'title', 'favicon', 'wget', 'warc', 'singlefile', 'pdf', + 'screenshot', 'dom', 'git', 'media', 'archive_org', + ) + latest: Dict[str, ArchiveOutput] = {} + for archive_method in ARCHIVE_METHODS: + # get most recent succesful result in history for each archive method + history = self.history.get(archive_method) or [] + history = list(filter(lambda result: result.output, reversed(history))) + if status is not None: + history = list(filter(lambda result: result.status == status, history)) + + history = list(history) + if history: + latest[archive_method] = history[0].output + else: + latest[archive_method] = None + return latest + + + def canonical_outputs(self) -> Dict[str, Optional[str]]: + """predict the expected output paths that should be present after archiving""" + + from ..extractors.wget import wget_output_path + canonical = { + 'index_path': 'index.html', + 'favicon_path': 'favicon.ico', + 'google_favicon_path': 'https://www.google.com/s2/favicons?domain={}'.format(self.domain), + 'wget_path': wget_output_path(self), + 'warc_path': 'warc', + 'singlefile_path': 'singlefile.html', + 'readability_path': 'readability/content.html', + 'mercury_path': 'mercury/content.html', + 'pdf_path': 'output.pdf', + 'screenshot_path': 'screenshot.png', + 'dom_path': 'output.html', + 'archive_org_path': 'https://web.archive.org/web/{}'.format(self.base_url), + 'git_path': 'git', + 'media_path': 'media', + } + if self.is_static: + # static binary files like PDF and images are handled slightly differently. + # they're just downloaded once and aren't archived separately multiple times, + # so the wget, screenshot, & pdf urls should all point to the same file + + static_path = wget_output_path(self) + canonical.update({ + 'title': self.basename, + 'wget_path': static_path, + 'pdf_path': static_path, + 'screenshot_path': static_path, + 'dom_path': static_path, + 'singlefile_path': static_path, + 'readability_path': static_path, + 'mercury_path': static_path, + }) + return canonical + diff --git a/archivebox-0.5.3/archivebox/index/sql.py b/archivebox-0.5.3/archivebox/index/sql.py new file mode 100644 index 0000000..1e99f67 --- /dev/null +++ b/archivebox-0.5.3/archivebox/index/sql.py @@ -0,0 +1,106 @@ +__package__ = 'archivebox.index' + +from io import StringIO +from pathlib import Path +from typing import List, Tuple, Iterator +from django.db.models import QuerySet +from django.db import transaction + +from .schema import Link +from ..util import enforce_types +from ..config import OUTPUT_DIR + + +### Main Links Index + +@enforce_types +def parse_sql_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + from core.models import Snapshot + + return ( + Link.from_json(page.as_json(*Snapshot.keys)) + for page in Snapshot.objects.all() + ) + +@enforce_types +def remove_from_sql_main_index(snapshots: QuerySet, out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + snapshots.delete() + +@enforce_types +def write_link_to_sql_index(link: Link): + from core.models import Snapshot + info = {k: v for k, v in link._asdict().items() if k in Snapshot.keys} + tags = info.pop("tags") + if tags is None: + tags = [] + + try: + info["timestamp"] = Snapshot.objects.get(url=link.url).timestamp + except Snapshot.DoesNotExist: + while Snapshot.objects.filter(timestamp=info["timestamp"]).exists(): + info["timestamp"] = str(float(info["timestamp"]) + 1.0) + + snapshot, _ = Snapshot.objects.update_or_create(url=link.url, defaults=info) + snapshot.save_tags(tags) + return snapshot + + +@enforce_types +def write_sql_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + for link in links: + write_link_to_sql_index(link) + + +@enforce_types +def write_sql_link_details(link: Link, out_dir: Path=OUTPUT_DIR) -> None: + from core.models import Snapshot + + with transaction.atomic(): + try: + snap = Snapshot.objects.get(url=link.url) + except Snapshot.DoesNotExist: + snap = write_link_to_sql_index(link) + snap.title = link.title + + tag_set = ( + set(tag.strip() for tag in (link.tags or '').split(',')) + ) + tag_list = list(tag_set) or [] + + snap.save() + snap.save_tags(tag_list) + + + +@enforce_types +def list_migrations(out_dir: Path=OUTPUT_DIR) -> List[Tuple[bool, str]]: + from django.core.management import call_command + out = StringIO() + call_command("showmigrations", list=True, stdout=out) + out.seek(0) + migrations = [] + for line in out.readlines(): + if line.strip() and ']' in line: + status_str, name_str = line.strip().split(']', 1) + is_applied = 'X' in status_str + migration_name = name_str.strip() + migrations.append((is_applied, migration_name)) + + return migrations + +@enforce_types +def apply_migrations(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.core.management import call_command + null, out = StringIO(), StringIO() + call_command("makemigrations", interactive=False, stdout=null) + call_command("migrate", interactive=False, stdout=out) + out.seek(0) + + return [line.strip() for line in out.readlines() if line.strip()] + +@enforce_types +def get_admins(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.contrib.auth.models import User + return User.objects.filter(is_superuser=True) diff --git a/archivebox-0.5.3/archivebox/logging_util.py b/archivebox-0.5.3/archivebox/logging_util.py new file mode 100644 index 0000000..f2b8673 --- /dev/null +++ b/archivebox-0.5.3/archivebox/logging_util.py @@ -0,0 +1,569 @@ +__package__ = 'archivebox' + +import re +import os +import sys +import time +import argparse +from math import log +from multiprocessing import Process +from pathlib import Path + +from datetime import datetime +from dataclasses import dataclass +from typing import Optional, List, Dict, Union, IO, TYPE_CHECKING + +if TYPE_CHECKING: + from .index.schema import Link, ArchiveResult + +from .util import enforce_types +from .config import ( + ConfigDict, + OUTPUT_DIR, + PYTHON_ENCODING, + ANSI, + IS_TTY, + TERM_WIDTH, + SHOW_PROGRESS, + SOURCES_DIR_NAME, + stderr, +) + +@dataclass +class RuntimeStats: + """mutable stats counter for logging archiving timing info to CLI output""" + + skipped: int = 0 + succeeded: int = 0 + failed: int = 0 + + parse_start_ts: Optional[datetime] = None + parse_end_ts: Optional[datetime] = None + + index_start_ts: Optional[datetime] = None + index_end_ts: Optional[datetime] = None + + archiving_start_ts: Optional[datetime] = None + archiving_end_ts: Optional[datetime] = None + +# globals are bad, mmkay +_LAST_RUN_STATS = RuntimeStats() + + + +class SmartFormatter(argparse.HelpFormatter): + """Patched formatter that prints newlines in argparse help strings""" + def _split_lines(self, text, width): + if '\n' in text: + return text.splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) + + +def reject_stdin(caller: str, stdin: Optional[IO]=sys.stdin) -> None: + """Tell the user they passed stdin to a command that doesn't accept it""" + + if stdin and not stdin.isatty(): + stdin_raw_text = stdin.read().strip() + if stdin_raw_text: + stderr(f'[X] The "{caller}" command does not accept stdin.', color='red') + stderr(f' Run archivebox "{caller} --help" to see usage and examples.') + stderr() + raise SystemExit(1) + + +def accept_stdin(stdin: Optional[IO]=sys.stdin) -> Optional[str]: + """accept any standard input and return it as a string or None""" + if not stdin: + return None + elif stdin and not stdin.isatty(): + stdin_str = stdin.read().strip() + return stdin_str or None + return None + + +class TimedProgress: + """Show a progress bar and measure elapsed time until .end() is called""" + + def __init__(self, seconds, prefix=''): + self.SHOW_PROGRESS = SHOW_PROGRESS + if self.SHOW_PROGRESS: + self.p = Process(target=progress_bar, args=(seconds, prefix)) + self.p.start() + + self.stats = {'start_ts': datetime.now(), 'end_ts': None} + + def end(self): + """immediately end progress, clear the progressbar line, and save end_ts""" + + end_ts = datetime.now() + self.stats['end_ts'] = end_ts + + if self.SHOW_PROGRESS: + # terminate if we havent already terminated + try: + # kill the progress bar subprocess + try: + self.p.close() # must be closed *before* its terminnated + except: + pass + self.p.terminate() + self.p.join() + + + # clear whole terminal line + try: + sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + except (IOError, BrokenPipeError): + # ignore when the parent proc has stopped listening to our stdout + pass + except ValueError: + pass + + +@enforce_types +def progress_bar(seconds: int, prefix: str='') -> None: + """show timer in the form of progress bar, with percentage and seconds remaining""" + chunk = '█' if PYTHON_ENCODING == 'UTF-8' else '#' + last_width = TERM_WIDTH() + chunks = last_width - len(prefix) - 20 # number of progress chunks to show (aka max bar width) + try: + for s in range(seconds * chunks): + max_width = TERM_WIDTH() + if max_width < last_width: + # when the terminal size is shrunk, we have to write a newline + # otherwise the progress bar will keep wrapping incorrectly + sys.stdout.write('\r\n') + sys.stdout.flush() + chunks = max_width - len(prefix) - 20 + pct_complete = s / chunks / seconds * 100 + log_pct = (log(pct_complete or 1, 10) / 2) * 100 # everyone likes faster progress bars ;) + bar_width = round(log_pct/(100/chunks)) + last_width = max_width + + # ████████████████████ 0.9% (1/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['green' if pct_complete < 80 else 'lightyellow'], + (chunk * bar_width).ljust(chunks), + ANSI['reset'], + round(pct_complete, 1), + round(s/chunks), + seconds, + )) + sys.stdout.flush() + time.sleep(1 / chunks) + + # ██████████████████████████████████ 100.0% (60/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['red'], + chunk * chunks, + ANSI['reset'], + 100.0, + seconds, + seconds, + )) + sys.stdout.flush() + # uncomment to have it disappear when it hits 100% instead of staying full red: + # time.sleep(0.5) + # sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + # sys.stdout.flush() + except (KeyboardInterrupt, BrokenPipeError): + print() + pass + + +def log_cli_command(subcommand: str, subcommand_args: List[str], stdin: Optional[str], pwd: str): + from .config import VERSION, ANSI + cmd = ' '.join(('archivebox', subcommand, *subcommand_args)) + stderr('{black}[i] [{now}] ArchiveBox v{VERSION}: {cmd}{reset}'.format( + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + VERSION=VERSION, + cmd=cmd, + **ANSI, + )) + stderr('{black} > {pwd}{reset}'.format(pwd=pwd, **ANSI)) + stderr() + +### Parsing Stage + + +def log_importing_started(urls: Union[str, List[str]], depth: int, index_only: bool): + _LAST_RUN_STATS.parse_start_ts = datetime.now() + print('{green}[+] [{}] Adding {} links to index (crawl depth={}){}...{reset}'.format( + _LAST_RUN_STATS.parse_start_ts.strftime('%Y-%m-%d %H:%M:%S'), + len(urls) if isinstance(urls, list) else len(urls.split('\n')), + depth, + ' (index only)' if index_only else '', + **ANSI, + )) + +def log_source_saved(source_file: str): + print(' > Saved verbatim input to {}/{}'.format(SOURCES_DIR_NAME, source_file.rsplit('/', 1)[-1])) + +def log_parsing_finished(num_parsed: int, parser_name: str): + _LAST_RUN_STATS.parse_end_ts = datetime.now() + print(' > Parsed {} URLs from input ({})'.format(num_parsed, parser_name)) + +def log_deduping_finished(num_new_links: int): + print(' > Found {} new URLs not already in index'.format(num_new_links)) + + +def log_crawl_started(new_links): + print() + print('{green}[*] Starting crawl of {} sites 1 hop out from starting point{reset}'.format(len(new_links), **ANSI)) + +### Indexing Stage + +def log_indexing_process_started(num_links: int): + start_ts = datetime.now() + _LAST_RUN_STATS.index_start_ts = start_ts + print() + print('{black}[*] [{}] Writing {} links to main index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + + +def log_indexing_process_finished(): + end_ts = datetime.now() + _LAST_RUN_STATS.index_end_ts = end_ts + + +def log_indexing_started(out_path: str): + if IS_TTY: + sys.stdout.write(f' > {out_path}') + + +def log_indexing_finished(out_path: str): + print(f'\r √ {out_path}') + + +### Archiving Stage + +def log_archiving_started(num_links: int, resume: Optional[float]=None): + start_ts = datetime.now() + _LAST_RUN_STATS.archiving_start_ts = start_ts + print() + if resume: + print('{green}[▶] [{}] Resuming archive updating for {} pages starting from {}...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + resume, + **ANSI, + )) + else: + print('{green}[▶] [{}] Starting archiving of {} snapshots in index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + +def log_archiving_paused(num_links: int, idx: int, timestamp: str): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + print() + print('\n{lightyellow}[X] [{now}] Downloading paused on link {timestamp} ({idx}/{total}){reset}'.format( + **ANSI, + now=end_ts.strftime('%Y-%m-%d %H:%M:%S'), + idx=idx+1, + timestamp=timestamp, + total=num_links, + )) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print(' Continue archiving where you left off by running:') + print(' archivebox update --resume={}'.format(timestamp)) + +def log_archiving_finished(num_links: int): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + assert _LAST_RUN_STATS.archiving_start_ts is not None + seconds = end_ts.timestamp() - _LAST_RUN_STATS.archiving_start_ts.timestamp() + if seconds > 60: + duration = '{0:.2f} min'.format(seconds / 60) + else: + duration = '{0:.2f} sec'.format(seconds) + + print() + print('{}[√] [{}] Update of {} pages complete ({}){}'.format( + ANSI['green'], + end_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + duration, + ANSI['reset'], + )) + print(' - {} links skipped'.format(_LAST_RUN_STATS.skipped)) + print(' - {} links updated'.format(_LAST_RUN_STATS.succeeded + _LAST_RUN_STATS.failed)) + print(' - {} links had errors'.format(_LAST_RUN_STATS.failed)) + print() + print(' {lightred}Hint:{reset} To manage your archive in a Web UI, run:'.format(**ANSI)) + print(' archivebox server 0.0.0.0:8000') + + +def log_link_archiving_started(link: "Link", link_dir: str, is_new: bool): + # [*] [2019-03-22 13:46:45] "Log Structured Merge Trees - ben stopford" + # http://www.benstopford.com/2015/02/14/log-structured-merge-trees/ + # > output/archive/1478739709 + + print('\n[{symbol_color}{symbol}{reset}] [{symbol_color}{now}{reset}] "{title}"'.format( + symbol_color=ANSI['green' if is_new else 'black'], + symbol='+' if is_new else '√', + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + title=link.title or link.base_url, + **ANSI, + )) + print(' {blue}{url}{reset}'.format(url=link.url, **ANSI)) + print(' {} {}'.format( + '>' if is_new else '√', + pretty_path(link_dir), + )) + +def log_link_archiving_finished(link: "Link", link_dir: str, is_new: bool, stats: dict): + total = sum(stats.values()) + + if stats['failed'] > 0 : + _LAST_RUN_STATS.failed += 1 + elif stats['skipped'] == total: + _LAST_RUN_STATS.skipped += 1 + else: + _LAST_RUN_STATS.succeeded += 1 + + +def log_archive_method_started(method: str): + print(' > {}'.format(method)) + + +def log_archive_method_finished(result: "ArchiveResult"): + """quote the argument with whitespace in a command so the user can + copy-paste the outputted string directly to run the cmd + """ + # Prettify CMD string and make it safe to copy-paste by quoting arguments + quoted_cmd = ' '.join( + '"{}"'.format(arg) if ' ' in arg else arg + for arg in result.cmd + ) + + if result.status == 'failed': + if result.output.__class__.__name__ == 'TimeoutExpired': + duration = (result.end_ts - result.start_ts).seconds + hint_header = [ + '{lightyellow}Extractor timed out after {}s.{reset}'.format(duration, **ANSI), + ] + else: + hint_header = [ + '{lightyellow}Extractor failed:{reset}'.format(**ANSI), + ' {reset}{} {red}{}{reset}'.format( + result.output.__class__.__name__.replace('ArchiveError', ''), + result.output, + **ANSI, + ), + ] + + # Prettify error output hints string and limit to five lines + hints = getattr(result.output, 'hints', None) or () + if hints: + hints = hints if isinstance(hints, (list, tuple)) else hints.split('\n') + hints = ( + ' {}{}{}'.format(ANSI['lightyellow'], line.strip(), ANSI['reset']) + for line in hints[:5] if line.strip() + ) + + + # Collect and prefix output lines with indentation + output_lines = [ + *hint_header, + *hints, + '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), + *([' cd {};'.format(result.pwd)] if result.pwd else []), + ' {}'.format(quoted_cmd), + ] + print('\n'.join( + ' {}'.format(line) + for line in output_lines + if line + )) + print() + + +def log_list_started(filter_patterns: Optional[List[str]], filter_type: str): + print('{green}[*] Finding links in the archive index matching these {} patterns:{reset}'.format( + filter_type, + **ANSI, + )) + print(' {}'.format(' '.join(filter_patterns or ()))) + +def log_list_finished(links): + from .index.csv import links_to_csv + print() + print('---------------------------------------------------------------------------------------------------') + print(links_to_csv(links, cols=['timestamp', 'is_archived', 'num_outputs', 'url'], header=True, ljust=16, separator=' | ')) + print('---------------------------------------------------------------------------------------------------') + print() + + +def log_removal_started(links: List["Link"], yes: bool, delete: bool): + print('{lightyellow}[i] Found {} matching URLs to remove.{reset}'.format(len(links), **ANSI)) + if delete: + file_counts = [link.num_outputs for link in links if Path(link.link_dir).exists()] + print( + f' {len(links)} Links will be de-listed from the main index, and their archived content folders will be deleted from disk.\n' + f' ({len(file_counts)} data folders with {sum(file_counts)} archived files will be deleted!)' + ) + else: + print( + ' Matching links will be de-listed from the main index, but their archived content folders will remain in place on disk.\n' + ' (Pass --delete if you also want to permanently delete the data folders)' + ) + + if not yes: + print() + print('{lightyellow}[?] Do you want to proceed with removing these {} links?{reset}'.format(len(links), **ANSI)) + try: + assert input(' y/[n]: ').lower() == 'y' + except (KeyboardInterrupt, EOFError, AssertionError): + raise SystemExit(0) + +def log_removal_finished(all_links: int, to_remove: int): + if all_links == 0: + print() + print('{red}[X] No matching links found.{reset}'.format(**ANSI)) + else: + print() + print('{red}[√] Removed {} out of {} links from the archive index.{reset}'.format( + to_remove, + all_links, + **ANSI, + )) + print(' Index now contains {} links.'.format(all_links - to_remove)) + + +def log_shell_welcome_msg(): + from .cli import list_subcommands + + print('{green}# ArchiveBox Imports{reset}'.format(**ANSI)) + print('{green}from core.models import Snapshot, User{reset}'.format(**ANSI)) + print('{green}from archivebox import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) + print() + print('[i] Welcome to the ArchiveBox Shell!') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Shell-Usage') + print() + print(' {lightred}Hint:{reset} Example use:'.format(**ANSI)) + print(' print(Snapshot.objects.filter(is_archived=True).count())') + print(' Snapshot.objects.get(url="https://example.com").as_json()') + print(' add("https://example.com/some/new/url")') + + + +### Helpers + +@enforce_types +def pretty_path(path: Union[Path, str]) -> str: + """convert paths like .../ArchiveBox/archivebox/../output/abc into output/abc""" + pwd = Path('.').resolve() + # parent = os.path.abspath(os.path.join(pwd, os.path.pardir)) + return str(path).replace(str(pwd) + '/', './') + + +@enforce_types +def printable_filesize(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + + +@enforce_types +def printable_folders(folders: Dict[str, Optional["Link"]], + with_headers: bool=False) -> str: + return '\n'.join( + f'{folder} {link and link.url} "{link and link.title}"' + for folder, link in folders.items() + ) + + + +@enforce_types +def printable_config(config: ConfigDict, prefix: str='') -> str: + return f'\n{prefix}'.join( + f'{key}={val}' + for key, val in config.items() + if not (isinstance(val, dict) or callable(val)) + ) + + +@enforce_types +def printable_folder_status(name: str, folder: Dict) -> str: + if folder['enabled']: + if folder['is_valid']: + color, symbol, note = 'green', '√', 'valid' + else: + color, symbol, note, num_files = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, num_files = 'lightyellow', '-', 'disabled', '-' + + if folder['path']: + if Path(folder['path']).exists(): + num_files = ( + f'{len(os.listdir(folder["path"]))} files' + if Path(folder['path']).is_dir() else + printable_filesize(Path(folder['path']).stat().st_size) + ) + else: + num_files = 'missing' + + path = str(folder['path']).replace(str(OUTPUT_DIR), '.') if folder['path'] else '' + if path and ' ' in path: + path = f'"{path}"' + + # if path is just a plain dot, replace it back with the full path for clarity + if path == '.': + path = str(OUTPUT_DIR) + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + num_files.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) + + +@enforce_types +def printable_dependency_version(name: str, dependency: Dict) -> str: + version = None + if dependency['enabled']: + if dependency['is_valid']: + color, symbol, note, version = 'green', '√', 'valid', '' + + parsed_version_num = re.search(r'[\d\.]+', dependency['version']) + if parsed_version_num: + version = f'v{parsed_version_num[0]}' + + if not version: + color, symbol, note, version = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, version = 'lightyellow', '-', 'disabled', '-' + + path = str(dependency["path"]).replace(str(OUTPUT_DIR), '.') if dependency["path"] else '' + if path and ' ' in path: + path = f'"{path}"' + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + version.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) diff --git a/archivebox-0.5.3/archivebox/main.py b/archivebox-0.5.3/archivebox/main.py new file mode 100644 index 0000000..eb8cd6a --- /dev/null +++ b/archivebox-0.5.3/archivebox/main.py @@ -0,0 +1,1131 @@ +__package__ = 'archivebox' + +import os +import sys +import shutil +import platform +from pathlib import Path +from datetime import date + +from typing import Dict, List, Optional, Iterable, IO, Union +from crontab import CronTab, CronSlices +from django.db.models import QuerySet + +from .cli import ( + list_subcommands, + run_subcommand, + display_first, + meta_cmds, + main_cmds, + archive_cmds, +) +from .parsers import ( + save_text_as_source, + save_file_as_source, + parse_links_memory, +) +from .index.schema import Link +from .util import enforce_types # type: ignore +from .system import get_dir_size, dedupe_cron_jobs, CRON_COMMENT +from .index import ( + load_main_index, + parse_links_from_source, + dedupe_links, + write_main_index, + snapshot_filter, + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, + fix_invalid_folder_locations, + write_link_details, +) +from .index.json import ( + parse_json_main_index, + parse_json_links_details, + generate_json_index_from_links, +) +from .index.sql import ( + get_admins, + apply_migrations, + remove_from_sql_main_index, +) +from .index.html import ( + generate_index_from_links, +) +from .index.csv import links_to_csv +from .extractors import archive_links, archive_link, ignore_methods +from .config import ( + stderr, + hint, + ConfigDict, + ANSI, + IS_TTY, + IN_DOCKER, + USER, + ARCHIVEBOX_BINARY, + ONLY_NEW, + OUTPUT_DIR, + SOURCES_DIR, + ARCHIVE_DIR, + LOGS_DIR, + CONFIG_FILE, + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + SQL_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, + check_dependencies, + check_data_folder, + write_config_file, + VERSION, + CODE_LOCATIONS, + EXTERNAL_LOCATIONS, + DATA_LOCATIONS, + DEPENDENCIES, + load_all_config, + CONFIG, + USER_CONFIG, + get_real_name, +) +from .logging_util import ( + TERM_WIDTH, + TimedProgress, + log_importing_started, + log_crawl_started, + log_removal_started, + log_removal_finished, + log_list_started, + log_list_finished, + printable_config, + printable_folders, + printable_filesize, + printable_folder_status, + printable_dependency_version, +) + +from .search import flush_search_index, index_links + +ALLOWED_IN_OUTPUT_DIR = { + 'lost+found', + '.DS_Store', + '.venv', + 'venv', + 'virtualenv', + '.virtualenv', + 'node_modules', + 'package-lock.json', + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, +} + +@enforce_types +def help(out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox help message and usage""" + + all_subcommands = list_subcommands() + COMMANDS_HELP_TEXT = '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in meta_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in main_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in archive_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd not in display_first + ) + + + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('''{green}ArchiveBox v{}: The self-hosted internet archive.{reset} + +{lightred}Active data directory:{reset} + {} + +{lightred}Usage:{reset} + archivebox [command] [--help] [--version] [...args] + +{lightred}Commands:{reset} + {} + +{lightred}Example Use:{reset} + mkdir my-archive; cd my-archive/ + archivebox init + archivebox status + + archivebox add https://example.com/some/page + archivebox add --depth=1 ~/Downloads/bookmarks_export.html + + archivebox list --sort=timestamp --csv=timestamp,url,is_archived + archivebox schedule --every=day https://example.com/some/feed.rss + archivebox update --resume=15109948213.123 + +{lightred}Documentation:{reset} + https://github.com/ArchiveBox/ArchiveBox/wiki +'''.format(VERSION, out_dir, COMMANDS_HELP_TEXT, **ANSI)) + + else: + print('{green}Welcome to ArchiveBox v{}!{reset}'.format(VERSION, **ANSI)) + print() + if IN_DOCKER: + print('When using Docker, you need to mount a volume to use as your data dir:') + print(' docker run -v /some/path:/data archivebox ...') + print() + print('To import an existing archive (from a previous version of ArchiveBox):') + print(' 1. cd into your data dir OUTPUT_DIR (usually ArchiveBox/output) and run:') + print(' 2. archivebox init') + print() + print('To start a new archive:') + print(' 1. Create an empty directory, then cd into it and run:') + print(' 2. archivebox init') + print() + print('For more information, see the documentation here:') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki') + + +@enforce_types +def version(quiet: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox version and dependency information""" + + if quiet: + print(VERSION) + else: + print('ArchiveBox v{}'.format(VERSION)) + p = platform.uname() + print(sys.implementation.name.title(), p.system, platform.platform(), p.machine, '(in Docker)' if IN_DOCKER else '(not in Docker)') + print() + + print('{white}[i] Dependency versions:{reset}'.format(**ANSI)) + for name, dependency in DEPENDENCIES.items(): + print(printable_dependency_version(name, dependency)) + + print() + print('{white}[i] Source-code locations:{reset}'.format(**ANSI)) + for name, folder in CODE_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + print('{white}[i] Secrets locations:{reset}'.format(**ANSI)) + for name, folder in EXTERNAL_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + if DATA_LOCATIONS['OUTPUT_DIR']['is_valid']: + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + for name, folder in DATA_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + else: + print() + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + + print() + check_dependencies() + + +@enforce_types +def run(subcommand: str, + subcommand_args: Optional[List[str]], + stdin: Optional[IO]=None, + out_dir: Path=OUTPUT_DIR) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + run_subcommand( + subcommand=subcommand, + subcommand_args=subcommand_args, + stdin=stdin, + pwd=out_dir, + ) + + +@enforce_types +def init(force: bool=False, out_dir: Path=OUTPUT_DIR) -> None: + """Initialize a new ArchiveBox collection in the current directory""" + from core.models import Snapshot + Path(out_dir).mkdir(exist_ok=True) + is_empty = not len(set(os.listdir(out_dir)) - ALLOWED_IN_OUTPUT_DIR) + + if (Path(out_dir) / JSON_INDEX_FILENAME).exists(): + stderr("[!] This folder contains a JSON index. It is deprecated, and will no longer be kept up to date automatically.", color="lightyellow") + stderr(" You can run `archivebox list --json --with-headers > index.json` to manually generate it.", color="lightyellow") + + existing_index = (Path(out_dir) / SQL_INDEX_FILENAME).exists() + + if is_empty and not existing_index: + print('{green}[+] Initializing a new ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + elif existing_index: + print('{green}[*] Updating existing ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + else: + if force: + stderr('[!] This folder appears to already have files in it, but no index.sqlite3 is present.', color='lightyellow') + stderr(' Because --force was passed, ArchiveBox will initialize anyway (which may overwrite existing files).') + else: + stderr( + ("{red}[X] This folder appears to already have files in it, but no index.sqlite3 present.{reset}\n\n" + " You must run init in a completely empty directory, or an existing data folder.\n\n" + " {lightred}Hint:{reset} To import an existing data folder make sure to cd into the folder first, \n" + " then run and run 'archivebox init' to pick up where you left off.\n\n" + " (Always make sure your data folder is backed up first before updating ArchiveBox)" + ).format(out_dir, **ANSI) + ) + raise SystemExit(2) + + if existing_index: + print('\n{green}[*] Verifying archive folder structure...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building archive folder structure...{reset}'.format(**ANSI)) + + Path(SOURCES_DIR).mkdir(exist_ok=True) + print(f' √ {SOURCES_DIR}') + + Path(ARCHIVE_DIR).mkdir(exist_ok=True) + print(f' √ {ARCHIVE_DIR}') + + Path(LOGS_DIR).mkdir(exist_ok=True) + print(f' √ {LOGS_DIR}') + + write_config_file({}, out_dir=out_dir) + print(f' √ {CONFIG_FILE}') + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('\n{green}[*] Verifying main SQL index and running migrations...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building main SQL index and running migrations...{reset}'.format(**ANSI)) + + DATABASE_FILE = Path(out_dir) / SQL_INDEX_FILENAME + print(f' √ {DATABASE_FILE}') + print() + for migration_line in apply_migrations(out_dir): + print(f' {migration_line}') + + + assert DATABASE_FILE.exists() + + # from django.contrib.auth.models import User + # if IS_TTY and not User.objects.filter(is_superuser=True).exists(): + # print('{green}[+] Creating admin user account...{reset}'.format(**ANSI)) + # call_command("createsuperuser", interactive=True) + + print() + print('{green}[*] Collecting links from any existing indexes and archive folders...{reset}'.format(**ANSI)) + + all_links = Snapshot.objects.none() + pending_links: Dict[str, Link] = {} + + if existing_index: + all_links = load_main_index(out_dir=out_dir, warn=False) + print(' √ Loaded {} links from existing main index.'.format(all_links.count())) + + # Links in data folders that dont match their timestamp + fixed, cant_fix = fix_invalid_folder_locations(out_dir=out_dir) + if fixed: + print(' {lightyellow}√ Fixed {} data directory locations that didn\'t match their link timestamps.{reset}'.format(len(fixed), **ANSI)) + if cant_fix: + print(' {lightyellow}! Could not fix {} data directory locations due to conflicts with existing folders.{reset}'.format(len(cant_fix), **ANSI)) + + # Links in JSON index but not in main index + orphaned_json_links = { + link.url: link + for link in parse_json_main_index(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_json_links: + pending_links.update(orphaned_json_links) + print(' {lightyellow}√ Added {} orphaned links from existing JSON index...{reset}'.format(len(orphaned_json_links), **ANSI)) + + # Links in data dir indexes but not in main index + orphaned_data_dir_links = { + link.url: link + for link in parse_json_links_details(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_data_dir_links: + pending_links.update(orphaned_data_dir_links) + print(' {lightyellow}√ Added {} orphaned links from existing archive directories.{reset}'.format(len(orphaned_data_dir_links), **ANSI)) + + # Links in invalid/duplicate data dirs + invalid_folders = { + folder: link + for folder, link in get_invalid_folders(all_links, out_dir=out_dir).items() + } + if invalid_folders: + print(' {lightyellow}! Skipped adding {} invalid link data directories.{reset}'.format(len(invalid_folders), **ANSI)) + print(' X ' + '\n X '.join(f'{folder} {link}' for folder, link in invalid_folders.items())) + print() + print(' {lightred}Hint:{reset} For more information about the link data directories that were skipped, run:'.format(**ANSI)) + print(' archivebox status') + print(' archivebox list --status=invalid') + + + write_main_index(list(pending_links.values()), out_dir=out_dir) + + print('\n{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + if existing_index: + print('{green}[√] Done. Verified and updated the existing ArchiveBox collection.{reset}'.format(**ANSI)) + else: + print('{green}[√] Done. A new ArchiveBox collection was initialized ({} links).{reset}'.format(len(all_links), **ANSI)) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print() + print(' To add new links, you can run:') + print(" archivebox add ~/some/path/or/url/to/list_of_links.txt") + print() + print(' For more usage and examples, run:') + print(' archivebox help') + + json_index = Path(out_dir) / JSON_INDEX_FILENAME + html_index = Path(out_dir) / HTML_INDEX_FILENAME + index_name = f"{date.today()}_index_old" + if json_index.exists(): + json_index.rename(f"{index_name}.json") + if html_index.exists(): + html_index.rename(f"{index_name}.html") + + + +@enforce_types +def status(out_dir: Path=OUTPUT_DIR) -> None: + """Print out some info and statistics about the archive collection""" + + check_data_folder(out_dir=out_dir) + + from core.models import Snapshot + from django.contrib.auth import get_user_model + User = get_user_model() + + print('{green}[*] Scanning archive main index...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {out_dir}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(out_dir, recursive=False, pattern='index.') + size = printable_filesize(num_bytes) + print(f' Index size: {size} across {num_files} files') + print() + + links = load_main_index(out_dir=out_dir) + num_sql_links = links.count() + num_link_details = sum(1 for link in parse_json_links_details(out_dir=out_dir)) + print(f' > SQL Main Index: {num_sql_links} links'.ljust(36), f'(found in {SQL_INDEX_FILENAME})') + print(f' > JSON Link Details: {num_link_details} links'.ljust(36), f'(found in {ARCHIVE_DIR_NAME}/*/index.json)') + print() + print('{green}[*] Scanning archive data directories...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {ARCHIVE_DIR}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(ARCHIVE_DIR) + size = printable_filesize(num_bytes) + print(f' Size: {size} across {num_files} files in {num_dirs} directories') + print(ANSI['black']) + num_indexed = len(get_indexed_folders(links, out_dir=out_dir)) + num_archived = len(get_archived_folders(links, out_dir=out_dir)) + num_unarchived = len(get_unarchived_folders(links, out_dir=out_dir)) + print(f' > indexed: {num_indexed}'.ljust(36), f'({get_indexed_folders.__doc__})') + print(f' > archived: {num_archived}'.ljust(36), f'({get_archived_folders.__doc__})') + print(f' > unarchived: {num_unarchived}'.ljust(36), f'({get_unarchived_folders.__doc__})') + + num_present = len(get_present_folders(links, out_dir=out_dir)) + num_valid = len(get_valid_folders(links, out_dir=out_dir)) + print() + print(f' > present: {num_present}'.ljust(36), f'({get_present_folders.__doc__})') + print(f' > valid: {num_valid}'.ljust(36), f'({get_valid_folders.__doc__})') + + duplicate = get_duplicate_folders(links, out_dir=out_dir) + orphaned = get_orphaned_folders(links, out_dir=out_dir) + corrupted = get_corrupted_folders(links, out_dir=out_dir) + unrecognized = get_unrecognized_folders(links, out_dir=out_dir) + num_invalid = len({**duplicate, **orphaned, **corrupted, **unrecognized}) + print(f' > invalid: {num_invalid}'.ljust(36), f'({get_invalid_folders.__doc__})') + print(f' > duplicate: {len(duplicate)}'.ljust(36), f'({get_duplicate_folders.__doc__})') + print(f' > orphaned: {len(orphaned)}'.ljust(36), f'({get_orphaned_folders.__doc__})') + print(f' > corrupted: {len(corrupted)}'.ljust(36), f'({get_corrupted_folders.__doc__})') + print(f' > unrecognized: {len(unrecognized)}'.ljust(36), f'({get_unrecognized_folders.__doc__})') + + print(ANSI['reset']) + + if num_indexed: + print(' {lightred}Hint:{reset} You can list link data directories by status like so:'.format(**ANSI)) + print(' archivebox list --status=<status> (e.g. indexed, corrupted, archived, etc.)') + + if orphaned: + print(' {lightred}Hint:{reset} To automatically import orphaned data directories into the main index, run:'.format(**ANSI)) + print(' archivebox init') + + if num_invalid: + print(' {lightred}Hint:{reset} You may need to manually remove or fix some invalid data directories, afterwards make sure to run:'.format(**ANSI)) + print(' archivebox init') + + print() + print('{green}[*] Scanning recent archive changes and user logins:{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {LOGS_DIR}/*', ANSI['reset']) + users = get_admins().values_list('username', flat=True) + print(f' UI users {len(users)}: {", ".join(users)}') + last_login = User.objects.order_by('last_login').last() + if last_login: + print(f' Last UI login: {last_login.username} @ {str(last_login.last_login)[:16]}') + last_updated = Snapshot.objects.order_by('updated').last() + if last_updated: + print(f' Last changes: {str(last_updated.updated)[:16]}') + + if not users: + print() + print(' {lightred}Hint:{reset} You can create an admin user by running:'.format(**ANSI)) + print(' archivebox manage createsuperuser') + + print() + for snapshot in links.order_by('-updated')[:10]: + if not snapshot.updated: + continue + print( + ANSI['black'], + ( + f' > {str(snapshot.updated)[:16]} ' + f'[{snapshot.num_outputs} {("X", "√")[snapshot.is_archived]} {printable_filesize(snapshot.archive_size)}] ' + f'"{snapshot.title}": {snapshot.url}' + )[:TERM_WIDTH()], + ANSI['reset'], + ) + print(ANSI['black'], ' ...', ANSI['reset']) + + +@enforce_types +def oneshot(url: str, extractors: str="", out_dir: Path=OUTPUT_DIR): + """ + Create a single URL archive folder with an index.json and index.html, and all the archive method outputs. + You can run this to archive single pages without needing to create a whole collection with archivebox init. + """ + oneshot_link, _ = parse_links_memory([url]) + if len(oneshot_link) > 1: + stderr( + '[X] You should pass a single url to the oneshot command', + color='red' + ) + raise SystemExit(2) + + methods = extractors.split(",") if extractors else ignore_methods(['title']) + archive_link(oneshot_link[0], out_dir=out_dir, methods=methods) + return oneshot_link + +@enforce_types +def add(urls: Union[str, List[str]], + depth: int=0, + update_all: bool=not ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + init: bool=False, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Add a new URL or list of URLs to your archive""" + + assert depth in (0, 1), 'Depth must be 0 or 1 (depth >1 is not supported yet)' + + extractors = extractors.split(",") if extractors else [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # Load list of links from the existing index + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] + all_links = load_main_index(out_dir=out_dir) + + log_importing_started(urls=urls, depth=depth, index_only=index_only) + if isinstance(urls, str): + # save verbatim stdin to sources + write_ahead_log = save_text_as_source(urls, filename='{ts}-import.txt', out_dir=out_dir) + elif isinstance(urls, list): + # save verbatim args to sources + write_ahead_log = save_text_as_source('\n'.join(urls), filename='{ts}-import.txt', out_dir=out_dir) + + new_links += parse_links_from_source(write_ahead_log, root_url=None) + + # If we're going one level deeper, download each link and look for more links + new_links_depth = [] + if new_links and depth == 1: + log_crawl_started(new_links) + for new_link in new_links: + downloaded_file = save_file_as_source(new_link.url, filename=f'{new_link.timestamp}-crawl-{new_link.domain}.txt', out_dir=out_dir) + new_links_depth += parse_links_from_source(downloaded_file, root_url=new_link.url) + + imported_links = list({link.url: link for link in (new_links + new_links_depth)}.values()) + new_links = dedupe_links(all_links, imported_links) + + write_main_index(links=new_links, out_dir=out_dir) + all_links = load_main_index(out_dir=out_dir) + + if index_only: + return all_links + + # Run the archive methods for each link + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + if update_all: + archive_links(all_links, overwrite=overwrite, **archive_kwargs) + elif overwrite: + archive_links(imported_links, overwrite=True, **archive_kwargs) + elif new_links: + archive_links(new_links, overwrite=False, **archive_kwargs) + + return all_links + +@enforce_types +def remove(filter_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + snapshots: Optional[QuerySet]=None, + after: Optional[float]=None, + before: Optional[float]=None, + yes: bool=False, + delete: bool=False, + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Remove the specified URLs from the archive""" + + check_data_folder(out_dir=out_dir) + + if snapshots is None: + if filter_str and filter_patterns: + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif not (filter_str or filter_patterns): + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin.', + color='red', + ) + stderr() + hint(('To remove all urls you can run:', + 'archivebox remove --filter-type=regex ".*"')) + stderr() + raise SystemExit(2) + elif filter_str: + filter_patterns = [ptn.strip() for ptn in filter_str.split('\n')] + + list_kwargs = { + "filter_patterns": filter_patterns, + "filter_type": filter_type, + "after": after, + "before": before, + } + if snapshots: + list_kwargs["snapshots"] = snapshots + + log_list_started(filter_patterns, filter_type) + timer = TimedProgress(360, prefix=' ') + try: + snapshots = list_links(**list_kwargs) + finally: + timer.end() + + + if not snapshots.exists(): + log_removal_finished(0, 0) + raise SystemExit(1) + + + log_links = [link.as_link() for link in snapshots] + log_list_finished(log_links) + log_removal_started(log_links, yes=yes, delete=delete) + + timer = TimedProgress(360, prefix=' ') + try: + for snapshot in snapshots: + if delete: + shutil.rmtree(snapshot.as_link().link_dir, ignore_errors=True) + finally: + timer.end() + + to_remove = snapshots.count() + + flush_search_index(snapshots=snapshots) + remove_from_sql_main_index(snapshots=snapshots, out_dir=out_dir) + all_snapshots = load_main_index(out_dir=out_dir) + log_removal_finished(all_snapshots.count(), to_remove) + + return all_snapshots + +@enforce_types +def update(resume: Optional[float]=None, + only_new: bool=ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: Optional[str]=None, + status: Optional[str]=None, + after: Optional[str]=None, + before: Optional[str]=None, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Import any new links from subscriptions and retry any previously failed/skipped links""" + + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] # TODO: Remove input argument: only_new + + extractors = extractors.split(",") if extractors else [] + + # Step 1: Filter for selected_links + matching_snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + matching_folders = list_folders( + links=matching_snapshots, + status=status, + out_dir=out_dir, + ) + all_links = [link for link in matching_folders.values() if link] + + if index_only: + for link in all_links: + write_link_details(link, out_dir=out_dir, skip_sql_index=True) + index_links(all_links, out_dir=out_dir) + return all_links + + # Step 2: Run the archive methods for each link + to_archive = new_links if only_new else all_links + if resume: + to_archive = [ + link for link in to_archive + if link.timestamp >= str(resume) + ] + if not to_archive: + stderr('') + stderr(f'[√] Nothing found to resume after {resume}', color='green') + return all_links + + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + + archive_links(to_archive, overwrite=overwrite, **archive_kwargs) + + # Step 4: Re-write links index with updated titles, icons, and resources + all_links = load_main_index(out_dir=out_dir) + return all_links + +@enforce_types +def list_all(filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + status: Optional[str]=None, + after: Optional[float]=None, + before: Optional[float]=None, + sort: Optional[str]=None, + csv: Optional[str]=None, + json: bool=False, + html: bool=False, + with_headers: bool=False, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + """List, filter, and export information about archive entries""" + + check_data_folder(out_dir=out_dir) + + if filter_patterns and filter_patterns_str: + stderr( + '[X] You should either pass filter patterns as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif filter_patterns_str: + filter_patterns = filter_patterns_str.split('\n') + + snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + if sort: + snapshots = snapshots.order_by(sort) + + folders = list_folders( + links=snapshots, + status=status, + out_dir=out_dir, + ) + + if json: + output = generate_json_index_from_links(folders.values(), with_headers) + elif html: + output = generate_index_from_links(folders.values(), with_headers) + elif csv: + output = links_to_csv(folders.values(), cols=csv.split(','), header=with_headers) + else: + output = printable_folders(folders, with_headers=with_headers) + print(output) + return folders + + +@enforce_types +def list_links(snapshots: Optional[QuerySet]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + after: Optional[float]=None, + before: Optional[float]=None, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + + check_data_folder(out_dir=out_dir) + + if snapshots: + all_snapshots = snapshots + else: + all_snapshots = load_main_index(out_dir=out_dir) + + if after is not None: + all_snapshots = all_snapshots.filter(timestamp__lt=after) + if before is not None: + all_snapshots = all_snapshots.filter(timestamp__gt=before) + if filter_patterns: + all_snapshots = snapshot_filter(all_snapshots, filter_patterns, filter_type) + return all_snapshots + +@enforce_types +def list_folders(links: List[Link], + status: str, + out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + + check_data_folder(out_dir=out_dir) + + STATUS_FUNCTIONS = { + "indexed": get_indexed_folders, + "archived": get_archived_folders, + "unarchived": get_unarchived_folders, + "present": get_present_folders, + "valid": get_valid_folders, + "invalid": get_invalid_folders, + "duplicate": get_duplicate_folders, + "orphaned": get_orphaned_folders, + "corrupted": get_corrupted_folders, + "unrecognized": get_unrecognized_folders, + } + + try: + return STATUS_FUNCTIONS[status](links, out_dir=out_dir) + except KeyError: + raise ValueError('Status not recognized.') + + +@enforce_types +def config(config_options_str: Optional[str]=None, + config_options: Optional[List[str]]=None, + get: bool=False, + set: bool=False, + reset: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Get and set your ArchiveBox project configuration values""" + + check_data_folder(out_dir=out_dir) + + if config_options and config_options_str: + stderr( + '[X] You should either pass config values as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif config_options_str: + config_options = config_options_str.split('\n') + + config_options = config_options or [] + + no_args = not (get or set or reset or config_options) + + matching_config: ConfigDict = {} + if get or no_args: + if config_options: + config_options = [get_real_name(key) for key in config_options] + matching_config = {key: CONFIG[key] for key in config_options if key in CONFIG} + failed_config = [key for key in config_options if key not in CONFIG] + if failed_config: + stderr() + stderr('[X] These options failed to get', color='red') + stderr(' {}'.format('\n '.join(config_options))) + raise SystemExit(1) + else: + matching_config = CONFIG + + print(printable_config(matching_config)) + raise SystemExit(not matching_config) + elif set: + new_config = {} + failed_options = [] + for line in config_options: + if line.startswith('#') or not line.strip(): + continue + if '=' not in line: + stderr('[X] Config KEY=VALUE must have an = sign in it', color='red') + stderr(f' {line}') + raise SystemExit(2) + + raw_key, val = line.split('=', 1) + raw_key = raw_key.upper().strip() + key = get_real_name(raw_key) + if key != raw_key: + stderr(f'[i] Note: The config option {raw_key} has been renamed to {key}, please use the new name going forwards.', color='lightyellow') + + if key in CONFIG: + new_config[key] = val.strip() + else: + failed_options.append(line) + + if new_config: + before = CONFIG + matching_config = write_config_file(new_config, out_dir=OUTPUT_DIR) + after = load_all_config() + print(printable_config(matching_config)) + + side_effect_changes: ConfigDict = {} + for key, val in after.items(): + if key in USER_CONFIG and (before[key] != after[key]) and (key not in matching_config): + side_effect_changes[key] = after[key] + + if side_effect_changes: + stderr() + stderr('[i] Note: This change also affected these other options that depended on it:', color='lightyellow') + print(' {}'.format(printable_config(side_effect_changes, prefix=' '))) + if failed_options: + stderr() + stderr('[X] These options failed to set (check for typos):', color='red') + stderr(' {}'.format('\n '.join(failed_options))) + raise SystemExit(bool(failed_options)) + elif reset: + stderr('[X] This command is not implemented yet.', color='red') + stderr(' Please manually remove the relevant lines from your config file:') + stderr(f' {CONFIG_FILE}') + raise SystemExit(2) + else: + stderr('[X] You must pass either --get or --set, or no arguments to get the whole config.', color='red') + stderr(' archivebox config') + stderr(' archivebox config --get SOME_KEY') + stderr(' archivebox config --set SOME_KEY=SOME_VALUE') + raise SystemExit(2) + + +@enforce_types +def schedule(add: bool=False, + show: bool=False, + clear: bool=False, + foreground: bool=False, + run_all: bool=False, + quiet: bool=False, + every: Optional[str]=None, + depth: int=0, + import_path: Optional[str]=None, + out_dir: Path=OUTPUT_DIR): + """Set ArchiveBox to regularly import URLs at specific times using cron""" + + check_data_folder(out_dir=out_dir) + + (Path(out_dir) / LOGS_DIR_NAME).mkdir(exist_ok=True) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + + if clear: + print(cron.remove_all(comment=CRON_COMMENT)) + cron.write() + raise SystemExit(0) + + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if every or add: + every = every or 'day' + quoted = lambda s: f'"{s}"' if s and ' ' in str(s) else str(s) + cmd = [ + 'cd', + quoted(out_dir), + '&&', + quoted(ARCHIVEBOX_BINARY), + *(['add', f'--depth={depth}', f'"{import_path}"'] if import_path else ['update']), + '>', + quoted(Path(LOGS_DIR) / 'archivebox.log'), + '2>&1', + + ] + new_job = cron.new(command=' '.join(cmd), comment=CRON_COMMENT) + + if every in ('minute', 'hour', 'day', 'month', 'year'): + set_every = getattr(new_job.every(), every) + set_every() + elif CronSlices.is_valid(every): + new_job.setall(every) + else: + stderr('{red}[X] Got invalid timeperiod for cron task.{reset}'.format(**ANSI)) + stderr(' It must be one of minute/hour/day/month') + stderr(' or a quoted cron-format schedule like:') + stderr(' archivebox init --every=day https://example.com/some/rss/feed.xml') + stderr(' archivebox init --every="0/5 * * * *" https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + cron = dedupe_cron_jobs(cron) + cron.write() + + total_runs = sum(j.frequency_per_year() for j in cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + print() + print('{green}[√] Scheduled new ArchiveBox cron job for user: {} ({} jobs are active).{reset}'.format(USER, len(existing_jobs), **ANSI)) + print('\n'.join(f' > {cmd}' if str(cmd) == str(new_job) else f' {cmd}' for cmd in existing_jobs)) + if total_runs > 60 and not quiet: + stderr() + stderr('{lightyellow}[!] With the current cron config, ArchiveBox is estimated to run >{} times per year.{reset}'.format(total_runs, **ANSI)) + stderr(' Congrats on being an enthusiastic internet archiver! 👌') + stderr() + stderr(' Make sure you have enough storage space available to hold all the data.') + stderr(' Using a compressed/deduped filesystem like ZFS is recommended if you plan on archiving a lot.') + stderr('') + elif show: + if existing_jobs: + print('\n'.join(str(cmd) for cmd in existing_jobs)) + else: + stderr('{red}[X] There are no ArchiveBox cron jobs scheduled for your user ({}).{reset}'.format(USER, **ANSI)) + stderr(' To schedule a new job, run:') + stderr(' archivebox schedule --every=[timeperiod] https://example.com/some/rss/feed.xml') + raise SystemExit(0) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if foreground or run_all: + if not existing_jobs: + stderr('{red}[X] You must schedule some jobs first before running in foreground mode.{reset}'.format(**ANSI)) + stderr(' archivebox schedule --every=hour https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + print('{green}[*] Running {} ArchiveBox jobs in foreground task scheduler...{reset}'.format(len(existing_jobs), **ANSI)) + if run_all: + try: + for job in existing_jobs: + sys.stdout.write(f' > {job.command.split("/archivebox ")[0].split(" && ")[0]}\n') + sys.stdout.write(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + sys.stdout.flush() + job.run() + sys.stdout.write(f'\r √ {job.command.split("/archivebox ")[-1]}\n') + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + if foreground: + try: + for job in existing_jobs: + print(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + for result in cron.run_scheduler(): + print(result) + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + +@enforce_types +def server(runserver_args: Optional[List[str]]=None, + reload: bool=False, + debug: bool=False, + init: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Run the ArchiveBox HTTP server""" + + runserver_args = runserver_args or [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # setup config for django runserver + from . import config + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + from django.contrib.auth.models import User + + admin_user = User.objects.filter(is_superuser=True).order_by('date_joined').only('username').last() + + print('{green}[+] Starting ArchiveBox webserver...{reset}'.format(**ANSI)) + if admin_user: + hint('The admin username is{lightblue} {}{reset}\n'.format(admin_user.username, **ANSI)) + else: + print('{lightyellow}[!] No admin users exist yet, you will not be able to edit links in the UI.{reset}'.format(**ANSI)) + print() + print(' To create an admin user, run:') + print(' archivebox manage createsuperuser') + print() + + # fallback to serving staticfiles insecurely with django when DEBUG=False + if not config.DEBUG: + runserver_args.append('--insecure') # TODO: serve statics w/ nginx instead + + # toggle autoreloading when archivebox code changes (it's on by default) + if not reload: + runserver_args.append('--noreload') + + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + + call_command("runserver", *runserver_args) + + +@enforce_types +def manage(args: Optional[List[str]]=None, out_dir: Path=OUTPUT_DIR) -> None: + """Run an ArchiveBox Django management command""" + + check_data_folder(out_dir=out_dir) + from django.core.management import execute_from_command_line + + if (args and "createsuperuser" in args) and (IN_DOCKER and not IS_TTY): + stderr('[!] Warning: you need to pass -it to use interactive commands in docker', color='lightyellow') + stderr(' docker run -it archivebox manage {}'.format(' '.join(args or ['...'])), color='lightyellow') + stderr() + + execute_from_command_line([f'{ARCHIVEBOX_BINARY} manage', *(args or ['help'])]) + + +@enforce_types +def shell(out_dir: Path=OUTPUT_DIR) -> None: + """Enter an interactive ArchiveBox Django shell""" + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + call_command("shell_plus") + diff --git a/archivebox-0.5.3/archivebox/manage.py b/archivebox-0.5.3/archivebox/manage.py new file mode 100755 index 0000000..1a9b297 --- /dev/null +++ b/archivebox-0.5.3/archivebox/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + # if you're a developer working on archivebox, still prefer the archivebox + # versions of ./manage.py commands whenever possible. When that's not possible + # (e.g. makemigrations), you can comment out this check temporarily + + if not ('makemigrations' in sys.argv or 'migrate' in sys.argv): + print("[X] Don't run ./manage.py directly (unless you are a developer running makemigrations):") + print() + print(' Hint: Use these archivebox CLI commands instead of the ./manage.py equivalents:') + print(' archivebox init (migrates the databse to latest version)') + print(' archivebox server (runs the Django web server)') + print(' archivebox shell (opens an iPython Django shell with all models imported)') + print(' archivebox manage [cmd] (any other management commands)') + raise SystemExit(2) + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/archivebox-0.5.3/archivebox/mypy.ini b/archivebox-0.5.3/archivebox/mypy.ini new file mode 100644 index 0000000..b1b4489 --- /dev/null +++ b/archivebox-0.5.3/archivebox/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +plugins = + mypy_django_plugin.main diff --git a/archivebox-0.5.3/archivebox/package.json b/archivebox-0.5.3/archivebox/package.json new file mode 100644 index 0000000..7f8bf66 --- /dev/null +++ b/archivebox-0.5.3/archivebox/package.json @@ -0,0 +1,21 @@ +{ + "name": "archivebox", + "version": "0.5.3", + "description": "ArchiveBox: The self-hosted internet archive", + "author": "Nick Sweeting <archivebox-npm@sweeting.me>", + "license": "MIT", + "scripts": { + "archivebox": "./bin/archive" + }, + "bin": { + "archivebox-node": "./bin/archive", + "single-file": "./node_modules/.bin/single-file", + "readability-extractor": "./node_modules/.bin/readability-extractor", + "mercury-parser": "./node_modules/.bin/mercury-parser" + }, + "dependencies": { + "@postlight/mercury-parser": "^2.2.0", + "readability-extractor": "git+https://github.com/pirate/readability-extractor.git", + "single-file": "git+https://github.com/gildas-lormeau/SingleFile.git" + } +} diff --git a/archivebox-0.5.3/archivebox/parsers/__init__.py b/archivebox-0.5.3/archivebox/parsers/__init__.py new file mode 100644 index 0000000..441c08a --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/__init__.py @@ -0,0 +1,203 @@ +""" +Everything related to parsing links from input sources. + +For a list of supported services, see the README.md. +For examples of supported import formats see tests/. +""" + +__package__ = 'archivebox.parsers' + +import re +from io import StringIO + +from typing import IO, Tuple, List, Optional +from datetime import datetime +from pathlib import Path + +from ..system import atomic_write +from ..config import ( + ANSI, + OUTPUT_DIR, + SOURCES_DIR_NAME, + TIMEOUT, +) +from ..util import ( + basename, + htmldecode, + download_url, + enforce_types, + URL_REGEX, +) +from ..index.schema import Link +from ..logging_util import TimedProgress, log_source_saved + +from .pocket_html import parse_pocket_html_export +from .pocket_api import parse_pocket_api_export +from .pinboard_rss import parse_pinboard_rss_export +from .wallabag_atom import parse_wallabag_atom_export +from .shaarli_rss import parse_shaarli_rss_export +from .medium_rss import parse_medium_rss_export +from .netscape_html import parse_netscape_html_export +from .generic_rss import parse_generic_rss_export +from .generic_json import parse_generic_json_export +from .generic_html import parse_generic_html_export +from .generic_txt import parse_generic_txt_export + +PARSERS = ( + # Specialized parsers + ('Pocket API', parse_pocket_api_export), + ('Wallabag ATOM', parse_wallabag_atom_export), + ('Pocket HTML', parse_pocket_html_export), + ('Pinboard RSS', parse_pinboard_rss_export), + ('Shaarli RSS', parse_shaarli_rss_export), + ('Medium RSS', parse_medium_rss_export), + + # General parsers + ('Netscape HTML', parse_netscape_html_export), + ('Generic RSS', parse_generic_rss_export), + ('Generic JSON', parse_generic_json_export), + ('Generic HTML', parse_generic_html_export), + + # Fallback parser + ('Plain Text', parse_generic_txt_export), +) + + +@enforce_types +def parse_links_memory(urls: List[str], root_url: Optional[str]=None): + """ + parse a list of URLS without touching the filesystem + """ + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + #urls = list(map(lambda x: x + "\n", urls)) + file = StringIO() + file.writelines(urls) + file.name = "io_string" + links, parser = run_parser_functions(file, timer, root_url=root_url) + timer.end() + + if parser is None: + return [], 'Failed to parse' + return links, parser + + +@enforce_types +def parse_links(source_file: str, root_url: Optional[str]=None) -> Tuple[List[Link], str]: + """parse a list of URLs with their metadata from an + RSS feed, bookmarks export, or text file + """ + + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + with open(source_file, 'r', encoding='utf-8') as file: + links, parser = run_parser_functions(file, timer, root_url=root_url) + + timer.end() + if parser is None: + return [], 'Failed to parse' + return links, parser + + +def run_parser_functions(to_parse: IO[str], timer, root_url: Optional[str]=None) -> Tuple[List[Link], Optional[str]]: + most_links: List[Link] = [] + best_parser_name = None + + for parser_name, parser_func in PARSERS: + try: + parsed_links = list(parser_func(to_parse, root_url=root_url)) + if not parsed_links: + raise Exception('no links found') + + # print(f'[√] Parser {parser_name} succeeded: {len(parsed_links)} links parsed') + if len(parsed_links) > len(most_links): + most_links = parsed_links + best_parser_name = parser_name + + except Exception as err: # noqa + # Parsers are tried one by one down the list, and the first one + # that succeeds is used. To see why a certain parser was not used + # due to error or format incompatibility, uncomment this line: + + # print('[!] Parser {} failed: {} {}'.format(parser_name, err.__class__.__name__, err)) + # raise + pass + timer.end() + return most_links, best_parser_name + + +@enforce_types +def save_text_as_source(raw_text: str, filename: str='{ts}-stdin.txt', out_dir: Path=OUTPUT_DIR) -> str: + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(out_dir / SOURCES_DIR_NAME / filename.format(ts=ts)) + atomic_write(source_path, raw_text) + log_source_saved(source_file=source_path) + return source_path + + +@enforce_types +def save_file_as_source(path: str, timeout: int=TIMEOUT, filename: str='{ts}-{basename}.txt', out_dir: Path=OUTPUT_DIR) -> str: + """download a given url's content into output/sources/domain-<timestamp>.txt""" + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(OUTPUT_DIR / SOURCES_DIR_NAME / filename.format(basename=basename(path), ts=ts)) + + if any(path.startswith(s) for s in ('http://', 'https://', 'ftp://')): + # Source is a URL that needs to be downloaded + print(f' > Downloading {path} contents') + timer = TimedProgress(timeout, prefix=' ') + try: + raw_source_text = download_url(path, timeout=timeout) + raw_source_text = htmldecode(raw_source_text) + timer.end() + except Exception as e: + timer.end() + print('{}[!] Failed to download {}{}\n'.format( + ANSI['red'], + path, + ANSI['reset'], + )) + print(' ', e) + raise SystemExit(1) + + else: + # Source is a path to a local file on the filesystem + with open(path, 'r') as f: + raw_source_text = f.read() + + atomic_write(source_path, raw_source_text) + + log_source_saved(source_file=source_path) + + return source_path + + +def check_url_parsing_invariants() -> None: + """Check that plain text regex URL parsing works as expected""" + + # this is last-line-of-defense to make sure the URL_REGEX isn't + # misbehaving, as the consequences could be disastrous and lead to many + # incorrect/badly parsed links being added to the archive + + test_urls = ''' + https://example1.com/what/is/happening.html?what=1#how-about-this=1 + https://example2.com/what/is/happening/?what=1#how-about-this=1 + HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f + https://example4.com/what/is/happening.html + https://example5.com/ + https://example6.com + + <test>http://example7.com</test> + [https://example8.com/what/is/this.php?what=1] + [and http://example9.com?what=1&other=3#and-thing=2] + <what>https://example10.com#and-thing=2 "</about> + abc<this["https://example11.com/what/is#and-thing=2?whoami=23&where=1"]that>def + sdflkf[what](https://example12.com/who/what.php?whoami=1#whatami=2)?am=hi + example13.bada + and example14.badb + <or>htt://example15.badc</that> + ''' + # print('\n'.join(re.findall(URL_REGEX, test_urls))) + assert len(re.findall(URL_REGEX, test_urls)) == 12 + diff --git a/archivebox-0.5.3/archivebox/parsers/generic_html.py b/archivebox-0.5.3/archivebox/parsers/generic_html.py new file mode 100644 index 0000000..74b3d1f --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/generic_html.py @@ -0,0 +1,53 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + URL_REGEX, +) +from html.parser import HTMLParser +from urllib.parse import urljoin + + +class HrefParser(HTMLParser): + def __init__(self): + super().__init__() + self.urls = [] + + def handle_starttag(self, tag, attrs): + if tag == "a": + for attr, value in attrs: + if attr == "href": + self.urls.append(value) + + +@enforce_types +def parse_generic_html_export(html_file: IO[str], root_url: Optional[str]=None, **_kwargs) -> Iterable[Link]: + """Parse Generic HTML for href tags and use only the url (support for title coming later)""" + + html_file.seek(0) + for line in html_file: + parser = HrefParser() + # example line + # <li><a href="http://example.com/ time_added="1478739709" tags="tag1,tag2">example title</a></li> + parser.feed(line) + for url in parser.urls: + if root_url: + # resolve relative urls /home.html -> https://example.com/home.html + url = urljoin(root_url, url) + + for archivable_url in re.findall(URL_REGEX, url): + yield Link( + url=htmldecode(archivable_url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/generic_json.py b/archivebox-0.5.3/archivebox/parsers/generic_json.py new file mode 100644 index 0000000..e6ed677 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/generic_json.py @@ -0,0 +1,65 @@ +__package__ = 'archivebox.parsers' + +import json + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_generic_json_export(json_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)""" + + json_file.seek(0) + links = json.load(json_file) + json_date = lambda s: datetime.strptime(s, '%Y-%m-%dT%H:%M:%S%z') + + for link in links: + # example line + # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}] + if link: + # Parse URL + url = link.get('href') or link.get('url') or link.get('URL') + if not url: + raise Exception('JSON must contain URL in each entry [{"url": "http://...", ...}, ...]') + + # Parse the timestamp + ts_str = str(datetime.now().timestamp()) + if link.get('timestamp'): + # chrome/ff histories use a very precise timestamp + ts_str = str(link['timestamp'] / 10000000) + elif link.get('time'): + ts_str = str(json_date(link['time'].split(',', 1)[0]).timestamp()) + elif link.get('created_at'): + ts_str = str(json_date(link['created_at']).timestamp()) + elif link.get('created'): + ts_str = str(json_date(link['created']).timestamp()) + elif link.get('date'): + ts_str = str(json_date(link['date']).timestamp()) + elif link.get('bookmarked'): + ts_str = str(json_date(link['bookmarked']).timestamp()) + elif link.get('saved'): + ts_str = str(json_date(link['saved']).timestamp()) + + # Parse the title + title = None + if link.get('title'): + title = link['title'].strip() + elif link.get('description'): + title = link['description'].replace(' — Readability', '').strip() + elif link.get('name'): + title = link['name'].strip() + + yield Link( + url=htmldecode(url), + timestamp=ts_str, + title=htmldecode(title) or None, + tags=htmldecode(link.get('tags')) or '', + sources=[json_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/generic_rss.py b/archivebox-0.5.3/archivebox/parsers/generic_rss.py new file mode 100644 index 0000000..2831844 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/generic_rss.py @@ -0,0 +1,49 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + +@enforce_types +def parse_generic_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse RSS XML-format files into links""" + + rss_file.seek(0) + items = rss_file.read().split('<item>') + items = items[1:] if items else [] + for item in items: + # example item: + # <item> + # <title><![CDATA[How JavaScript works: inside the V8 engine]]> + # Unread + # https://blog.sessionstack.com/how-javascript-works-inside + # https://blog.sessionstack.com/how-javascript-works-inside + # Mon, 21 Aug 2017 14:21:58 -0500 + # + + trailing_removed = item.split('', 1)[0] + leading_removed = trailing_removed.split('', 1)[-1].strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r for r in rows if r.strip().startswith('<{}>'.format(key))][0] + + url = str_between(get_row('link'), '', '') + ts_str = str_between(get_row('pubDate'), '', '') + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z") + title = str_between(get_row('title'), ' Iterable[Link]: + """Parse raw links from each line in a text file""" + + text_file.seek(0) + for line in text_file.readlines(): + if not line.strip(): + continue + + # if the line is a local file path that resolves, then we can archive it + try: + if Path(line).exists(): + yield Link( + url=line, + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + except (OSError, PermissionError): + # nvm, not a valid path... + pass + + # otherwise look for anything that looks like a URL in the line + for url in re.findall(URL_REGEX, line): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + + # look inside the URL for any sub-urls, e.g. for archive.org links + # https://web.archive.org/web/20200531203453/https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + # -> https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + for url in re.findall(URL_REGEX, line[1:]): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/medium_rss.py b/archivebox-0.5.3/archivebox/parsers/medium_rss.py new file mode 100644 index 0000000..8f14f77 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/medium_rss.py @@ -0,0 +1,35 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_medium_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Medium RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.find("channel").findall("item") # type: ignore + for item in items: + url = item.find("link").text # type: ignore + title = item.find("title").text.strip() # type: ignore + ts_str = item.find("pubDate").text # type: ignore + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z") # type: ignore + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/netscape_html.py b/archivebox-0.5.3/archivebox/parsers/netscape_html.py new file mode 100644 index 0000000..a063023 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/netscape_html.py @@ -0,0 +1,39 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_netscape_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse netscape-format bookmarks export files (produced by all browsers)""" + + html_file.seek(0) + pattern = re.compile("]*>(.+)", re.UNICODE | re.IGNORECASE) + for line in html_file: + # example line + #
example bookmark title + + match = pattern.search(line) + if match: + url = match.group(1) + time = datetime.fromtimestamp(float(match.group(2))) + title = match.group(3).strip() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[html_file.name], + ) + diff --git a/archivebox-0.5.3/archivebox/parsers/pinboard_rss.py b/archivebox-0.5.3/archivebox/parsers/pinboard_rss.py new file mode 100644 index 0000000..98ff14a --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/pinboard_rss.py @@ -0,0 +1,47 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pinboard_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pinboard RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.findall("{http://purl.org/rss/1.0/}item") + for item in items: + find = lambda p: item.find(p).text.strip() if item.find(p) else None # type: ignore + + url = find("{http://purl.org/rss/1.0/}link") + tags = find("{http://purl.org/dc/elements/1.1/}subject") + title = find("{http://purl.org/rss/1.0/}title") + ts_str = find("{http://purl.org/dc/elements/1.1/}date") + + # Pinboard includes a colon in its date stamp timezone offsets, which + # Python can't parse. Remove it: + if ts_str and ts_str[-3:-2] == ":": + ts_str = ts_str[:-3]+ts_str[-2:] + + if ts_str: + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + else: + time = datetime.now() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=htmldecode(tags) or None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/pocket_api.py b/archivebox-0.5.3/archivebox/parsers/pocket_api.py new file mode 100644 index 0000000..bf3a292 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/pocket_api.py @@ -0,0 +1,113 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from configparser import ConfigParser + +from pathlib import Path +from ..vendor.pocket import Pocket + +from ..index.schema import Link +from ..util import enforce_types +from ..system import atomic_write +from ..config import ( + SOURCES_DIR, + POCKET_CONSUMER_KEY, + POCKET_ACCESS_TOKENS, +) + + +COUNT_PER_PAGE = 500 +API_DB_PATH = Path(SOURCES_DIR) / 'pocket_api.db' + +# search for broken protocols that sometimes come from the Pocket API +_BROKEN_PROTOCOL_RE = re.compile('^(http[s]?)(:/(?!/))') + + +def get_pocket_articles(api: Pocket, since=None, page=0): + body, headers = api.get( + state='archive', + sort='oldest', + since=since, + count=COUNT_PER_PAGE, + offset=page * COUNT_PER_PAGE, + ) + + articles = body['list'].values() if isinstance(body['list'], dict) else body['list'] + returned_count = len(articles) + + yield from articles + + if returned_count == COUNT_PER_PAGE: + yield from get_pocket_articles(api, since=since, page=page + 1) + else: + api.last_since = body['since'] + + +def link_from_article(article: dict, sources: list): + url: str = article['resolved_url'] or article['given_url'] + broken_protocol = _BROKEN_PROTOCOL_RE.match(url) + if broken_protocol: + url = url.replace(f'{broken_protocol.group(1)}:/', f'{broken_protocol.group(1)}://') + title = article['resolved_title'] or article['given_title'] or url + + return Link( + url=url, + timestamp=article['time_read'], + title=title, + tags=article.get('tags'), + sources=sources + ) + + +def write_since(username: str, since: str): + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + since_file = ConfigParser() + since_file.optionxform = str + since_file.read(API_DB_PATH) + + since_file[username] = { + 'since': since + } + + with open(API_DB_PATH, 'w+') as new: + since_file.write(new) + + +def read_since(username: str) -> Optional[str]: + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(API_DB_PATH) + + return config_file.get(username, 'since', fallback=None) + + +@enforce_types +def should_parse_as_pocket_api(text: str) -> bool: + return text.startswith('pocket://') + + +@enforce_types +def parse_pocket_api_export(input_buffer: IO[str], **_kwargs) -> Iterable[Link]: + """Parse bookmarks from the Pocket API""" + + input_buffer.seek(0) + pattern = re.compile(r"^pocket:\/\/(\w+)") + for line in input_buffer: + if should_parse_as_pocket_api(line): + + username = pattern.search(line).group(1) + api = Pocket(POCKET_CONSUMER_KEY, POCKET_ACCESS_TOKENS[username]) + api.last_since = None + + for article in get_pocket_articles(api, since=read_since(username)): + yield link_from_article(article, sources=[line]) + + write_since(username, api.last_since) diff --git a/archivebox-0.5.3/archivebox/parsers/pocket_html.py b/archivebox-0.5.3/archivebox/parsers/pocket_html.py new file mode 100644 index 0000000..653f21b --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/pocket_html.py @@ -0,0 +1,38 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pocket_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)""" + + html_file.seek(0) + pattern = re.compile("^\\s*
  • (.+)
  • ", re.UNICODE) + for line in html_file: + # example line + #
  • example title
  • + match = pattern.search(line) + if match: + url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url + time = datetime.fromtimestamp(float(match.group(2))) + tags = match.group(3) + title = match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/shaarli_rss.py b/archivebox-0.5.3/archivebox/parsers/shaarli_rss.py new file mode 100644 index 0000000..4a925f4 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/shaarli_rss.py @@ -0,0 +1,50 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_shaarli_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Shaarli-specific RSS XML-format files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # Aktuelle Trojaner-Welle: Emotet lauert in gefälschten Rechnungsmails | heise online + # + # https://demo.shaarli.org/?cEV4vw + # 2019-01-30T06:06:01+00:00 + # 2019-01-30T06:06:01+00:00 + #

    Permalink

    ]]>
    + #
    + + trailing_removed = entry.split('
    ', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '', '').strip() + url = str_between(get_row('link'), '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/parsers/wallabag_atom.py b/archivebox-0.5.3/archivebox/parsers/wallabag_atom.py new file mode 100644 index 0000000..0d77869 --- /dev/null +++ b/archivebox-0.5.3/archivebox/parsers/wallabag_atom.py @@ -0,0 +1,57 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_wallabag_atom_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Wallabag Atom files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # <![CDATA[Orient Ray vs Mako: Is There Much Difference? - iknowwatches.com]]> + # + # https://iknowwatches.com/orient-ray-vs-mako/ + # wallabag:wallabag.drycat.fr:milosh:entry:14041 + # 2020-10-18T09:14:02+02:00 + # 2020-10-18T09:13:56+02:00 + # + # + # + + trailing_removed = entry.split('', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '<![CDATA[', ']]>').strip() + url = str_between(get_row('link rel="via"'), '', '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + try: + tags = str_between(get_row('category'), 'label="', '" />') + except: + tags = None + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/archivebox/search/__init__.py b/archivebox-0.5.3/archivebox/search/__init__.py new file mode 100644 index 0000000..6191ede --- /dev/null +++ b/archivebox-0.5.3/archivebox/search/__init__.py @@ -0,0 +1,108 @@ +from typing import List, Union +from pathlib import Path +from importlib import import_module + +from django.db.models import QuerySet + +from archivebox.index.schema import Link +from archivebox.util import enforce_types +from archivebox.config import stderr, OUTPUT_DIR, USE_INDEXING_BACKEND, USE_SEARCHING_BACKEND, SEARCH_BACKEND_ENGINE + +from .utils import get_indexable_content, log_index_started + +def indexing_enabled(): + return USE_INDEXING_BACKEND + +def search_backend_enabled(): + return USE_SEARCHING_BACKEND + +def get_backend(): + return f'search.backends.{SEARCH_BACKEND_ENGINE}' + +def import_backend(): + backend_string = get_backend() + try: + backend = import_module(backend_string) + except Exception as err: + raise Exception("Could not load '%s' as a backend: %s" % (backend_string, err)) + return backend + +@enforce_types +def write_search_index(link: Link, texts: Union[List[str], None]=None, out_dir: Path=OUTPUT_DIR, skip_text_index: bool=False) -> None: + if not indexing_enabled(): + return + + if not skip_text_index and texts: + from core.models import Snapshot + + snap = Snapshot.objects.filter(url=link.url).first() + backend = import_backend() + if snap: + try: + backend.index(snapshot_id=str(snap.id), texts=texts) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def query_search_index(query: str, out_dir: Path=OUTPUT_DIR) -> QuerySet: + from core.models import Snapshot + + if search_backend_enabled(): + backend = import_backend() + try: + snapshot_ids = backend.search(query) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + raise + else: + # TODO preserve ordering from backend + qsearch = Snapshot.objects.filter(pk__in=snapshot_ids) + return qsearch + + return Snapshot.objects.none() + +@enforce_types +def flush_search_index(snapshots: QuerySet): + if not indexing_enabled() or not snapshots: + return + backend = import_backend() + snapshot_ids=(str(pk) for pk in snapshots.values_list('pk',flat=True)) + try: + backend.flush(snapshot_ids) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def index_links(links: Union[List[Link],None], out_dir: Path=OUTPUT_DIR): + if not links: + return + + from core.models import Snapshot, ArchiveResult + + for link in links: + snap = Snapshot.objects.filter(url=link.url).first() + if snap: + results = ArchiveResult.objects.indexable().filter(snapshot=snap) + log_index_started(link.url) + try: + texts = get_indexable_content(results) + except Exception as err: + stderr() + stderr( + f'[X] An Exception ocurred reading the indexable content={err}:', + color='red', + ) + else: + write_search_index(link, texts, out_dir=out_dir) diff --git a/archivebox-0.5.3/archivebox/search/backends/__init__.py b/archivebox-0.5.3/archivebox/search/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/archivebox/search/backends/ripgrep.py b/archivebox-0.5.3/archivebox/search/backends/ripgrep.py new file mode 100644 index 0000000..840d2d2 --- /dev/null +++ b/archivebox-0.5.3/archivebox/search/backends/ripgrep.py @@ -0,0 +1,45 @@ +import re +from subprocess import run, PIPE +from typing import List, Generator + +from archivebox.config import ARCHIVE_DIR, RIPGREP_VERSION +from archivebox.util import enforce_types + +RG_IGNORE_EXTENSIONS = ('css','js','orig','svg') + +RG_ADD_TYPE = '--type-add' +RG_IGNORE_ARGUMENTS = f"ignore:*.{{{','.join(RG_IGNORE_EXTENSIONS)}}}" +RG_DEFAULT_ARGUMENTS = "-ilTignore" # Case insensitive(i), matching files results(l) +RG_REGEX_ARGUMENT = '-e' + +TIMESTAMP_REGEX = r'\/([\d]+\.[\d]+)\/' + +ts_regex = re.compile(TIMESTAMP_REGEX) + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + return + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + return + +@enforce_types +def search(text: str) -> List[str]: + if not RIPGREP_VERSION: + raise Exception("ripgrep binary not found, install ripgrep to use this search backend") + + from core.models import Snapshot + + rg_cmd = ['rg', RG_ADD_TYPE, RG_IGNORE_ARGUMENTS, RG_DEFAULT_ARGUMENTS, RG_REGEX_ARGUMENT, text, str(ARCHIVE_DIR)] + rg = run(rg_cmd, stdout=PIPE, stderr=PIPE, timeout=60) + file_paths = [p.decode() for p in rg.stdout.splitlines()] + timestamps = set() + for path in file_paths: + ts = ts_regex.findall(path) + if ts: + timestamps.add(ts[0]) + + snap_ids = [str(id) for id in Snapshot.objects.filter(timestamp__in=timestamps).values_list('pk', flat=True)] + + return snap_ids diff --git a/archivebox-0.5.3/archivebox/search/backends/sonic.py b/archivebox-0.5.3/archivebox/search/backends/sonic.py new file mode 100644 index 0000000..f0beadd --- /dev/null +++ b/archivebox-0.5.3/archivebox/search/backends/sonic.py @@ -0,0 +1,28 @@ +from typing import List, Generator + +from sonic import IngestClient, SearchClient + +from archivebox.util import enforce_types +from archivebox.config import SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD, SONIC_BUCKET, SONIC_COLLECTION + +MAX_SONIC_TEXT_LENGTH = 20000 + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for text in texts: + chunks = [text[i:i+MAX_SONIC_TEXT_LENGTH] for i in range(0, len(text), MAX_SONIC_TEXT_LENGTH)] + for chunk in chunks: + ingestcl.push(SONIC_COLLECTION, SONIC_BUCKET, snapshot_id, str(chunk)) + +@enforce_types +def search(text: str) -> List[str]: + with SearchClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as querycl: + snap_ids = querycl.query(SONIC_COLLECTION, SONIC_BUCKET, text) + return snap_ids + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for id in snapshot_ids: + ingestcl.flush_object(SONIC_COLLECTION, SONIC_BUCKET, str(id)) diff --git a/archivebox-0.5.3/archivebox/search/utils.py b/archivebox-0.5.3/archivebox/search/utils.py new file mode 100644 index 0000000..55c97e7 --- /dev/null +++ b/archivebox-0.5.3/archivebox/search/utils.py @@ -0,0 +1,44 @@ +from django.db.models import QuerySet + +from archivebox.util import enforce_types +from archivebox.config import ANSI + +def log_index_started(url): + print('{green}[*] Indexing url: {} in the search index {reset}'.format(url, **ANSI)) + print( ) + +def get_file_result_content(res, extra_path, use_pwd=False): + if use_pwd: + fpath = f'{res.pwd}/{res.output}' + else: + fpath = f'{res.output}' + + if extra_path: + fpath = f'{fpath}/{extra_path}' + + with open(fpath, 'r') as file: + data = file.read() + if data: + return [data] + return [] + + +# This should be abstracted by a plugin interface for extractors +@enforce_types +def get_indexable_content(results: QuerySet): + if not results: + return [] + # Only use the first method available + res, method = results.first(), results.first().extractor + if method not in ('readability', 'singlefile', 'dom', 'wget'): + return [] + # This should come from a plugin interface + + if method == 'readability': + return get_file_result_content(res, 'content.txt') + elif method == 'singlefile': + return get_file_result_content(res, '') + elif method == 'dom': + return get_file_result_content(res,'',use_pwd=True) + elif method == 'wget': + return get_file_result_content(res,'',use_pwd=True) diff --git a/archivebox-0.5.3/archivebox/system.py b/archivebox-0.5.3/archivebox/system.py new file mode 100644 index 0000000..b27c5e4 --- /dev/null +++ b/archivebox-0.5.3/archivebox/system.py @@ -0,0 +1,163 @@ +__package__ = 'archivebox' + + +import os +import shutil + +from json import dump +from pathlib import Path +from typing import Optional, Union, Set, Tuple +from subprocess import run as subprocess_run + +from crontab import CronTab +from atomicwrites import atomic_write as lib_atomic_write + +from .util import enforce_types, ExtendedEncoder +from .config import OUTPUT_PERMISSIONS + + + +def run(*args, input=None, capture_output=True, text=False, **kwargs): + """Patched of subprocess.run to fix blocking io making timeout=innefective""" + + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + + if capture_output: + if ('stdout' in kwargs) or ('stderr' in kwargs): + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + + return subprocess_run(*args, input=input, capture_output=capture_output, text=text, **kwargs) + + +@enforce_types +def atomic_write(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True) -> None: + """Safe atomic write to filesystem by writing to temp file + atomic rename""" + + mode = 'wb+' if isinstance(contents, bytes) else 'w' + + # print('\n> Atomic Write:', mode, path, len(contents), f'overwrite={overwrite}') + try: + with lib_atomic_write(path, mode=mode, overwrite=overwrite) as f: + if isinstance(contents, dict): + dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) + elif isinstance(contents, (bytes, str)): + f.write(contents) + except OSError as e: + print(f"[X] OSError: Failed to write {path} with fcntl.F_FULLFSYNC. ({e})") + print(" For data integrity, ArchiveBox requires a filesystem that supports atomic writes.") + print(" Filesystems and network drives that don't implement FSYNC are incompatible and require workarounds.") + raise SystemExit(1) + os.chmod(path, int(OUTPUT_PERMISSIONS, base=8)) + +@enforce_types +def chmod_file(path: str, cwd: str='.', permissions: str=OUTPUT_PERMISSIONS) -> None: + """chmod -R /""" + + root = Path(cwd) / path + if not root.exists(): + raise Exception('Failed to chmod: {} does not exist (did the previous step fail?)'.format(path)) + + if not root.is_dir(): + os.chmod(root, int(OUTPUT_PERMISSIONS, base=8)) + else: + for subpath in Path(path).glob('**/*'): + os.chmod(subpath, int(OUTPUT_PERMISSIONS, base=8)) + + +@enforce_types +def copy_and_overwrite(from_path: Union[str, Path], to_path: Union[str, Path]): + """copy a given file or directory to a given path, overwriting the destination""" + if Path(from_path).is_dir(): + shutil.rmtree(to_path, ignore_errors=True) + shutil.copytree(from_path, to_path) + else: + with open(from_path, 'rb') as src: + contents = src.read() + atomic_write(to_path, contents) + + +@enforce_types +def get_dir_size(path: Union[str, Path], recursive: bool=True, pattern: Optional[str]=None) -> Tuple[int, int, int]: + """get the total disk size of a given directory, optionally summing up + recursively and limiting to a given filter list + """ + num_bytes, num_dirs, num_files = 0, 0, 0 + for entry in os.scandir(path): + if (pattern is not None) and (pattern not in entry.path): + continue + if entry.is_dir(follow_symlinks=False): + if not recursive: + continue + num_dirs += 1 + bytes_inside, dirs_inside, files_inside = get_dir_size(entry.path) + num_bytes += bytes_inside + num_dirs += dirs_inside + num_files += files_inside + else: + num_bytes += entry.stat(follow_symlinks=False).st_size + num_files += 1 + return num_bytes, num_dirs, num_files + + +CRON_COMMENT = 'archivebox_schedule' + + +@enforce_types +def dedupe_cron_jobs(cron: CronTab) -> CronTab: + deduped: Set[Tuple[str, str]] = set() + + for job in list(cron): + unique_tuple = (str(job.slices), job.command) + if unique_tuple not in deduped: + deduped.add(unique_tuple) + cron.remove(job) + + for schedule, command in deduped: + job = cron.new(command=command, comment=CRON_COMMENT) + job.setall(schedule) + job.enable() + + return cron + + +class suppress_output(object): + ''' + A context manager for doing a "deep suppression" of stdout and stderr in + Python, i.e. will suppress all print, even if the print originates in a + compiled C/Fortran sub-function. + This will not suppress raised exceptions, since exceptions are printed + to stderr just before a script exits, and after the context manager has + exited (at least, I think that is why it lets exceptions through). + + with suppress_stdout_stderr(): + rogue_function() + ''' + def __init__(self, stdout=True, stderr=True): + # Open a pair of null files + # Save the actual stdout (1) and stderr (2) file descriptors. + self.stdout, self.stderr = stdout, stderr + if stdout: + self.null_stdout = os.open(os.devnull, os.O_RDWR) + self.real_stdout = os.dup(1) + if stderr: + self.null_stderr = os.open(os.devnull, os.O_RDWR) + self.real_stderr = os.dup(2) + + def __enter__(self): + # Assign the null pointers to stdout and stderr. + if self.stdout: + os.dup2(self.null_stdout, 1) + if self.stderr: + os.dup2(self.null_stderr, 2) + + def __exit__(self, *_): + # Re-assign the real stdout/stderr back to (1) and (2) + if self.stdout: + os.dup2(self.real_stdout, 1) + os.close(self.null_stdout) + if self.stderr: + os.dup2(self.real_stderr, 2) + os.close(self.null_stderr) diff --git a/archivebox-0.5.3/archivebox/themes/admin/actions_as_select.html b/archivebox-0.5.3/archivebox/themes/admin/actions_as_select.html new file mode 100644 index 0000000..86a7719 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/actions_as_select.html @@ -0,0 +1 @@ +actions_as_select diff --git a/archivebox-0.5.3/archivebox/themes/admin/app_index.html b/archivebox-0.5.3/archivebox/themes/admin/app_index.html new file mode 100644 index 0000000..6868b49 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/app_index.html @@ -0,0 +1,18 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/archivebox/themes/admin/base.html b/archivebox-0.5.3/archivebox/themes/admin/base.html new file mode 100644 index 0000000..d8ad8d0 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/base.html @@ -0,0 +1,246 @@ +{% load i18n static %} +{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block title %}{% endblock %} | ArchiveBox + +{% block extrastyle %}{% endblock %} +{% if LANGUAGE_BIDI %}{% endif %} +{% block extrahead %}{% endblock %} +{% block responsive %} + + + {% if LANGUAGE_BIDI %}{% endif %} +{% endblock %} +{% block blockbots %}{% endblock %} + + +{% load i18n %} + + + + + + + + + +
    + + {% if not is_popup %} + + + + {% block breadcrumbs %} + + {% endblock %} + {% endif %} + + {% block messages %} + {% if messages %} +
      {% for message in messages %} + {{ message|capfirst }} + {% endfor %}
    + {% endif %} + {% endblock messages %} + + +
    + {% block pretitle %}{% endblock %} + {% block content_title %}{# {% if title %}

    {{ title }}

    {% endif %} #}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
    +
    + + + {% block footer %}{% endblock %} +
    + + + + + diff --git a/archivebox-0.5.3/archivebox/themes/admin/grid_change_list.html b/archivebox-0.5.3/archivebox/themes/admin/grid_change_list.html new file mode 100644 index 0000000..6894efd --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/grid_change_list.html @@ -0,0 +1,91 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + {{ block.super }} + + {% if cl.formset %} + + {% endif %} + {% if cl.formset or action_form %} + + {% endif %} + {{ media.css }} + {% if not actions_on_top and not actions_on_bottom %} + + {% endif %} +{% endblock %} + +{% block extrahead %} +{{ block.super }} +{{ media.js }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block coltype %}{% endblock %} + +{% block content %} +
    + {% block object-tools %} +
      + {% block object-tools-items %} + {% change_list_object_tools %} + {% endblock %} +
    + {% endblock %} + {% if cl.formset and cl.formset.errors %} +

    + {% if cl.formset.total_error_count == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} +

    + {{ cl.formset.non_form_errors }} + {% endif %} +
    +
    + {% block search %}{% search_form cl %}{% endblock %} + {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %} + +
    {% csrf_token %} + {% if cl.formset %} +
    {{ cl.formset.management_form }}
    + {% endif %} + + {% block result_list %} + {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% comment %} + Table grid + {% result_list cl %} + {% endcomment %} + {% snapshots_grid cl %} + {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% endblock %} + {% block pagination %}{% pagination cl %}{% endblock %} +
    +
    + {% block filters %} + {% if cl.has_filters %} +
    +

    {% translate 'Filter' %}

    + {% if cl.has_active_filters %}

    + ✖ {% translate "Clear all filters" %} +

    {% endif %} + {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} +
    + {% endif %} + {% endblock %} +
    +
    +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox/themes/admin/login.html b/archivebox-0.5.3/archivebox/themes/admin/login.html new file mode 100644 index 0000000..98283f8 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/login.html @@ -0,0 +1,100 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} + +{% block extrastyle %}{{ block.super }} +{{ form.media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} login{% endblock %} + +{% block branding %}

    ArchiveBox Admin

    {% endblock %} + +{% block usertools %} +
    + Back to Main Index +{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block content_title %} +
    + Log in to add, edit, and remove links from your archive. +


    +
    +{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors %} +

    +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

    +{% endif %} + +{% if form.non_field_errors %} +{% for error in form.non_field_errors %} +

    + {{ error }} +

    +{% endfor %} +{% endif %} + +
    + +{% if user.is_authenticated %} +

    +{% blocktrans trimmed %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? +{% endblocktrans %} +

    +{% endif %} + +
    +
    {% csrf_token %} +
    + {{ form.username.errors }} + {{ form.username.label_tag }} {{ form.username }} +
    +
    + {{ form.password.errors }} + {{ form.password.label_tag }} {{ form.password }} + +
    + {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + + {% endif %} +
    + +
    +
    + +
    +

    +
    +
    + If you forgot your password, reset it here or run:
    +
    +archivebox manage changepassword USERNAME
    +
    + +

    +
    +
    + To create a new admin user, run the following: +
    +archivebox manage createsuperuser
    +
    +
    +
    + + (cd into your archive folder before running commands) +
    + + +
    +{% endblock %} diff --git a/archivebox-0.5.3/archivebox/themes/admin/snapshots_grid.html b/archivebox-0.5.3/archivebox/themes/admin/snapshots_grid.html new file mode 100644 index 0000000..a7a2d4f --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/admin/snapshots_grid.html @@ -0,0 +1,162 @@ +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + + +{% endblock %} + +{% block content %} +
    + {% for obj in results %} +
    + + + + + +
    + {% if obj.tags_str %} +

    {{obj.tags_str}}

    + {% endif %} + {% if obj.title %} + +

    {{obj.title|truncatechars:55 }}

    +
    + {% endif %} + {% comment %}

    TEXT If needed.

    {% endcomment %} +
    +
    + +
    +
    + {% endfor %} +
    + +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox/themes/default/add_links.html b/archivebox-0.5.3/archivebox/themes/default/add_links.html new file mode 100644 index 0000000..0b384f5 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/add_links.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block body %} +
    +

    + {% if stdout %} +

    Add new URLs to your archive: results

    +
    +                {{ stdout | safe }}
    +                

    +
    +
    +
    +   Add more URLs ➕ +
    + {% else %} +
    {% csrf_token %} +

    Add new URLs to your archive

    +
    + {{ form.as_p }} +
    + +
    +
    +


    + + {% if absolute_add_path %} +
    +

    Bookmark this link to quickly add to your archive: + Add to ArchiveBox

    +
    + {% endif %} + + {% endif %} +
    +{% endblock %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/archivebox/themes/default/base.html b/archivebox-0.5.3/archivebox/themes/default/base.html new file mode 100644 index 0000000..a70430e --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/base.html @@ -0,0 +1,284 @@ +{% load static %} + + + + + + Archived Sites + + + + + + {% block extra_head %} + {% endblock %} + + + + + + +
    +
    + +
    +
    + {% block body %} + {% endblock %} +
    + + + + diff --git a/archivebox-0.5.3/archivebox/themes/default/core/snapshot_list.html b/archivebox-0.5.3/archivebox/themes/default/core/snapshot_list.html new file mode 100644 index 0000000..ce2b2fa --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/core/snapshot_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% load static %} + +{% block body %} +
    +
    + + + +
    + + + + + + + + + + + {% for link in object_list %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSnapshot ({{object_list|length}})FilesOriginal URL
    +
    + + {% if page_obj.has_previous %} + « first + previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next + last » + {% endif %} + + + {% if page_obj.has_next %} + next + last » + {% endif %} + +
    +
    +{% endblock %} diff --git a/archivebox-0.5.3/archivebox/themes/default/link_details.html b/archivebox-0.5.3/archivebox/themes/default/link_details.html new file mode 100644 index 0000000..b1edcfe --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/link_details.html @@ -0,0 +1,488 @@ + + + + {{title}} + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    Added
    + {{bookmarked_date}} +
    +
    +
    First Archived
    + {{oldest_archive_date}} +
    +
    +
    Last Checked
    + {{updated_date}} +
    +
    +
    +
    +
    Type
    +
    {{extension}}
    +
    +
    +
    Tags
    +
    {{tags}}
    +
    +
    +
    Status
    +
    {{status}}
    +
    +
    +
    Saved
    + ✅ {{num_outputs}} +
    +
    +
    Errors
    + ❌ {{num_failures}} +
    +
    +
    Size
    + {{size}} +
    +
    +
    +
    +
    🗃 Files
    + JSON | + WARC | + Media | + Git | + Favicon | + See all... +
    +
    +
    +
    +
    +
    + +
    + + + +

    Wget > WARC

    +

    archive/{{domain}}

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > SingleFile

    +

    archive/singlefile.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Archive.Org

    +

    web.archive.org/web/...

    +
    +
    +
    +
    +
    + +
    + + + +

    Original

    +

    {{domain}}

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > PDF

    +

    archive/output.pdf

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > Screenshot

    +

    archive/screenshot.png

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > HTML

    +

    archive/output.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Readability

    +

    archive/readability/...

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    mercury

    +

    archive/mercury/...

    +
    +
    +
    +
    +
    +
    + + + + + + + + diff --git a/archivebox-0.5.3/archivebox/themes/default/main_index.html b/archivebox-0.5.3/archivebox/themes/default/main_index.html new file mode 100644 index 0000000..95af196 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/main_index.html @@ -0,0 +1,255 @@ +{% load static %} + + + + + Archived Sites + + + + + + + + + +
    +
    + +
    +
    + + + + + + + + + + + + {% for link in links %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/archivebox/themes/default/main_index_minimal.html b/archivebox-0.5.3/archivebox/themes/default/main_index_minimal.html new file mode 100644 index 0000000..dcfaa23 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/main_index_minimal.html @@ -0,0 +1,24 @@ + + + + Archived Sites + + + + + + + + + + + + + + {% for link in links %} + {% include "main_index_row.html" with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox/themes/default/main_index_row.html b/archivebox-0.5.3/archivebox/themes/default/main_index_row.html new file mode 100644 index 0000000..5e21a8c --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/main_index_row.html @@ -0,0 +1,22 @@ +{% load static %} + + + {% if link.bookmarked_date %} {{ link.bookmarked_date }} {% else %} {{ link.added }} {% endif %} + + {% if link.is_archived %} + + {% else %} + + {% endif %} + + {{link.title|default:'Loading...'}} + {% if link.tags_str != None %} {{link.tags_str|default:''}} {% else %} {{ link.tags|default:'' }} {% endif %} + + + + 📄 + {% if link.icons %} {{link.icons}} {% else %} {{ link.num_outputs}} {% endif %} + + + {{link.url}} + \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox/themes/default/static/add.css b/archivebox-0.5.3/archivebox/themes/default/static/add.css new file mode 100644 index 0000000..b128bf4 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/static/add.css @@ -0,0 +1,62 @@ +.dashboard #content { + width: 100%; + margin-right: 0px; + margin-left: 0px; +} +#submit { + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 10px; + border-radius: 4px; + background-color: #f5dd5d; + color: #333; + font-size: 18px; + font-weight: 800; +} +#add-form button[role="submit"]:hover { + background-color: #e5cd4d; +} +#add-form label { + display: block; + font-size: 16px; +} +#add-form textarea { + width: 100%; + min-height: 300px; +} +#delay-warning div { + border: 1px solid red; + border-radius: 4px; + margin: 10px; + padding: 10px; + font-size: 15px; + background-color: #f5dd5d; +} +#stdout { + background-color: #ded; + padding: 10px 10px; + border-radius: 4px; + white-space: normal; +} +ul#id_depth { + list-style-type: none; + padding: 0; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} diff --git a/archivebox-0.5.3/archivebox/themes/default/static/admin.css b/archivebox-0.5.3/archivebox/themes/default/static/admin.css new file mode 100644 index 0000000..181c06d --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/static/admin.css @@ -0,0 +1,234 @@ +#logo { + height: 30px; + vertical-align: -6px; + padding-right: 5px; +} +#site-name:hover a { + opacity: 0.9; +} +#site-name .loader { + height: 25px; + width: 25px; + display: inline-block; + border-width: 3px; + vertical-align: -3px; + margin-right: 5px; + margin-top: 2px; +} +#branding h1, #branding h1 a:link, #branding h1 a:visited { + color: mintcream; +} +#header { + background: #aa1e55; + padding: 6px 14px; +} +#content { + padding: 8px 8px; +} +#user-tools { + font-size: 13px; + +} + +div.breadcrumbs { + background: #772948; + color: #f5dd5d; + padding: 6px 15px; +} + +body.model-snapshot.change-list div.breadcrumbs, +body.model-snapshot.change-list #content .object-tools { + display: none; +} + +.module h2, .module caption, .inline-group h2 { + background: #772948; +} + +#content .object-tools { + margin-top: -35px; + margin-right: -10px; + float: right; +} + +#content .object-tools a:link, #content .object-tools a:visited { + border-radius: 0px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; +} + +#content .object-tools a.addlink { + background-blend-mode: difference; +} + +#content #changelist #toolbar { + padding: 0px; + background: none; + margin-bottom: 10px; + border-top: 0px; + border-bottom: 0px; +} + +#content #changelist #toolbar form input[type="submit"] { + border-color: #aa1e55; +} + +#content #changelist-filter li.selected a { + color: #aa1e55; +} + + +/*#content #changelist .actions { + position: fixed; + bottom: 0px; + z-index: 800; +}*/ +#content #changelist .actions { + float: right; + margin-top: -34px; + padding: 0px; + background: none; + margin-right: 0px; + width: auto; +} + +#content #changelist .actions .button { + border-radius: 2px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; + margin-right: 4px; + box-shadow: 4px 4px 4px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.08); +} +#content #changelist .actions .button:hover { + border: 1px solid rgba(0,0,0,0.2); + opacity: 0.9; +} +#content #changelist .actions .button[name=verify_snapshots], #content #changelist .actions .button[name=update_titles] { + background-color: #dedede; + color: #333; +} +#content #changelist .actions .button[name=update_snapshots] { + background-color:lightseagreen; + color: #333; +} +#content #changelist .actions .button[name=overwrite_snapshots] { + background-color: #ffaa31; + color: #333; +} +#content #changelist .actions .button[name=delete_snapshots] { + background-color: #f91f74; + color: rgb(255 248 252 / 64%); +} + + +#content #changelist-filter h2 { + border-radius: 4px 4px 0px 0px; +} + +@media (min-width: 767px) { + #content #changelist-filter { + top: 35px; + width: 110px; + margin-bottom: 35px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered div.xfull { + margin-right: 115px; + } +} + +@media (max-width: 1127px) { + #content #changelist .actions { + position: fixed; + bottom: 6px; + left: 10px; + float: left; + z-index: 1000; + } +} + +#content a img.favicon { + height: 20px; + width: 20px; + vertical-align: -5px; + padding-right: 6px; +} + +#content td, #content th { + vertical-align: middle; + padding: 4px; +} + +#content #changelist table input { + vertical-align: -2px; +} + +#content thead th .text a { + padding: 8px 4px; +} + +#content th.field-added, #content td.field-updated { + word-break: break-word; + min-width: 128px; + white-space: normal; +} + +#content th.field-title_str { + min-width: 300px; +} + +#content td.field-files { + white-space: nowrap; +} +#content td.field-files .exists-True { + opacity: 1; +} +#content td.field-files .exists-False { + opacity: 0.1; + filter: grayscale(100%); +} +#content td.field-size { + white-space: nowrap; +} + +#content td.field-url_str { + word-break: break-all; + min-width: 200px; +} + +#content tr b.status-pending { + font-weight: 200; + opacity: 0.6; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.tags > a > .tag { + float: right; + border-radius: 5px; + background-color: #bfdfff; + padding: 2px 5px; + margin-left: 4px; + margin-top: 1px; +} diff --git a/archivebox-0.5.3/archivebox/themes/default/static/archive.png b/archivebox-0.5.3/archivebox/themes/default/static/archive.png new file mode 100644 index 0000000000000000000000000000000000000000..307b45013382851ef1026e111921bd94ba55af1d GIT binary patch literal 17730 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:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/archivebox-0.5.3/archivebox/themes/default/static/external.png b/archivebox-0.5.3/archivebox/themes/default/static/external.png new file mode 100755 index 0000000000000000000000000000000000000000..7e1a5f02aebccd4dcc6b1b0e3040c66ee84270a8 GIT binary patch literal 1647 zcmV-#29WuQP)gwIy-SzeLx3{-TOH0Pa z#^mJWg@uK_zP?jaQ@FUeMn*<ssP45C&jS0Z#=KL|K+q*ZaT6T^=ZwNzx>f$;{MezMmFr z-hyp&pfeg-EEbE!VzF2(7K_DVk;{Eq7SipHWdCbiob5_l5zgwP+;_-S8WPHi#`iyW z(v(2RfaCG2w8fhTLg82%;|yPEGBw{8Lj0lBOs#fPD z0xTl};WJsQGjbahmJlz6v?G;inIw(73980_q z`d%=(mLkC*iCv-ZJo+UDa)Y~&b%mbIJ28$M5`4gFr46&Z?!L`zQe3aQrAs1=ee8qz9E1kOAI z6T=WD0&f#W;B*SpfpMZrVGb}#)F{jY#)t}qxxfffpD-WTPgEzw0DFnrgg9UyQJD}6 z>>=tB;(=kJDq#sQNYo@O1BQr-gr&d$QID`3=qIWXQUJX~EkYWgkGMW<5f)MbJ;Zhb zMpj4%bcyYE@!4@ZcN3DaUi$(S%LCX|c`(_%u&m@q9Sl#B_}VnWH7FfA&d zc6|ygYJl5|&L@^11ICj-aeIMJoyUwA)e2a(@&~sUSUF%F{}r}?rv0YbO`wvrMBv8@ zTeblf0A7%`vLtZP3buL*tWmA!9}!kh!B$FxM@mhR5~qj}_R|F~7iz8-usZQy^q!y) zdD)?kbL8=7d8b(Xl(3!ne8Oho@3!1BvKsE(oa0_nEX~>1xFKsOHsMEEV&W;%DP7je zvrxR`sQZcm%hq`G9CPd~+c9v}{~?kWeqR;)vP15z=)95oXMFLs=CN?6{*oqJiI3_F zyiY!+_{8Af?RYZXah`Kludr9&ro3cf6WH_7$&$Z4EqEh1d}K9.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} diff --git a/archivebox-0.5.3/archivebox/themes/default/static/jquery.dataTables.min.js b/archivebox-0.5.3/archivebox/themes/default/static/jquery.dataTables.min.js new file mode 100644 index 0000000..07af1c3 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/static/jquery.dataTables.min.js @@ -0,0 +1,166 @@ +/*! + DataTables 1.10.19 + ©2008-2018 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Z(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Z(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Z(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=n.defaults.oLanguage,c=b.sDecimal;c&&Da(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&(d&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(d&&"Loading..."===b.sLoadingRecords)&&F(a, +a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Da(a)}}function fb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%": +"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1, +overflow:"hidden"}).append(h("
    ").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("
    ").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,n.__browser);a.oScroll.iBarWidth=n.__browser.barWidth} +function ib(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=n.defaults.column,d=a.aoColumns.length,c=h.extend({},n.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},n.models.oSearch,c[d]);ka(a,d,h(b).data())}function ka(a,b,c){var b=a.aoColumns[b], +d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(gb(c),J(n.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=S(g),i=b.mRender? +S(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return N(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone, +b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function $(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],m);else if("string"=== +typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d, +1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(m.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(m.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,m=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!mb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:m;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("",{valign:"top",colSpan:V(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,m,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,m,i]);d=h(a.nTBody);d.children().detach(); +d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&nb(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;P(a);a._drawHold=!1}function ob(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore= +a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,m,l,q,k=0;k")[0];m=f[k+1];if("'"==m||'"'==m){l="";for(q=2;f[k+q]!=m;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(m=l.split("."),i.id=m[0].substr(1,m[0].length-1),i.className=m[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=pb(a);else if("f"==j&& +d.bFilter)g=qb(a);else if("r"==j&&d.bProcessing)g=rb(a);else if("t"==j)g=sb(a);else if("i"==j&&d.bInfo)g=tb(a);else if("p"==j&&d.bPaginate)g=ub(a);else if(0!==n.ext.feature.length){i=n.ext.feature;q=0;for(m=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_", +g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ra(a,h(this).val());P(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a=== +c&&h("select",i).val(d)});return i[0]}function ub(a){var b=a.sPaginationType,c=n.ext.pager[b],d="function"===typeof c,e=function(a){P(a)},b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]} +function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");r(a,null,"processing",[a,b])}function sb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),m=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden", +position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", +{"class":f.sScrollFootInner}).append(m.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:la,sName:"scrolling"});return i[0]}function la(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth, +f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,m=j.children("table"),j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),n=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),Q,L,R,w,Ua=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!== +L&&a.scrollBarVis!==k)a.scrollBarVis=L,$(a);else{a.scrollBarVis=L;p.children("thead, tfoot").remove();u&&(R=u.clone().prependTo(p),Q=u.find("tr"),R=R.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=aa(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},R);f=p.outerWidth();if(""===c){r.width="100%";if(U&&(p.find("tbody").height()>j.offsetHeight|| +"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=v(d),f=p.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Ua.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ua[b]},o);h(L).height(0);u&&(I(C,R),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},R),I(function(a,b){a.style.width=y[b]},Q),h(R).height(0));I(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.childNodes[0].style.height= +"0";a.childNodes[0].style.overflow="hidden";a.style.width=Ua[b]},L);u&&I(function(a,b){a.innerHTML='
    '+A[b]+"
    ";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=y[b]},R);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(Q-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else Q="100%";q.width=v(Q); +g.width=v(Q);u&&(a.nScrollFoot.style.width=v(Q));!e&&U&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();m[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(n[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0, +f=b.length,g,j;e").appendTo(j.find("tbody"));j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");m=ra(a,j.find("thead")[0]);for(n=0;n").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(n=0;n").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a, +b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function X(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var m=[];f=function(a){a.length&& +!h.isArray(a[0])?m.push(a):h.merge(m,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,n=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=X(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=n.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ba(a,b)));for(var f,g=n.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!==k&&h.extend(a.oPreviousSearch,Cb(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Na(a,b){var c=a.renderer,d=n.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"=== +typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Lb.numbers_length,d=Math.floor(c/2);b<=c?c=Y(0,b):a<=d?(c=Y(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=Y(b-(c-2),b):(c=Y(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function Da(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Ya)},"html-num":function(b){return za(b, +a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Ya)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Mb(a){return function(){var b=[ya(this[n.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return n.ext.internal[a].apply(this,b)}}var n=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)}; +this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&la(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a, +b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data(): +c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]}; +this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust(); +(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in n.ext.internal)e&&(this[e]=Mb(e));this.each(function(){var e={},g=1").appendTo(q)); +p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){p.nTFoot=b[0];ea(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Ya=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Ob=function(a){var b=parseInt(a,10);return!isNaN(b)&& +isFinite(a)?b:null},Pb=function(a,b){Za[b]||(Za[b]=RegExp(Qa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Za[b],"."):a},$a=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Pb(a,b));c&&d&&(a=a.replace(Ya,""));return!isNaN(parseFloat(a))&&isFinite(a)},Qb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:$a(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb= +/<.*?>/g,Oa=n.util.throttle,Sb=[],w=Array.prototype,ac=function(a){var b,c,d=n.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof +s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=V(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e); +c._detailsShow&&c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Ub(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Ub(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){db(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Vb=function(a,b, +c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Vb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc): +"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var n=h.map(g,function(a,b){return a.bVisible?b:null});return[n[n.length+b]]}return[aa(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)}, +1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Vb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData}, +1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData, +i,m,l;if(a!==k&&g.bVisible!==a){if(a){var n=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(m=j.length;id;return!0};n.isDataTable= +n.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof n.Api)return!0;h.each(n.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};n.tables=n.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(n.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};n.camelToHungarian=J;o("$()",function(a,b){var c= +this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){oa(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a= +this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT"); +h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable), +(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,n.settings);-1!==c&&n.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=S(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]: +a._);return a.replace("%d",c)});n.version="1.10.19";n.settings=[];n.models={};n.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};n.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};n.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null, +sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};n.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1, +bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+ +a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"}, +oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({}, +n.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Z(n.defaults);n.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null}; +Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[], +aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button", +iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal: +this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{}, +classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); +h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled", +sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"", +sJUIHeader:"",sJUIFooter:""});var Lb=n.ext.pager;h.extend(Lb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e, +f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,n=0,o=function(b,d){var k,s,u,r,v=function(b){Ta(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{m=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":m=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m= +j.sNext;l=r+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":n,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(m).appendTo(b);Wa(u,{action:r},v);n++}}}},s;try{s=h(b).find(H.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+ +s+"]").focus()}}});h.extend(n.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)|| +"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(n.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," ").replace(Aa,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Pb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return M(a)? +"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Da("");h.extend(!0,n.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc: +c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]== +"asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var eb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g,"""):a};n.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return eb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g, +a)+f+(e||"")}}},text:function(){return{display:eb,filter:eb}}};h.extend(n.ext.internal,{_fnExternApiFunc:Mb,_fnBuildAjax:sa,_fnAjaxUpdate:mb,_fnAjaxParameters:vb,_fnAjaxUpdateDraw:wb,_fnAjaxDataSrc:ta,_fnAddColumn:Ea,_fnColumnOptions:ka,_fnAdjustColumnSizing:$,_fnVisibleToColumnIndex:aa,_fnColumnIndexToVisible:ba,_fnVisbleColumns:V,_fnGetColumns:ma,_fnColumnTypes:Ga,_fnApplyColumnDefs:jb,_fnHungarianMap:Z,_fnCamelToHungarian:J,_fnLanguageCompat:Ca,_fnBrowserDetect:hb,_fnAddData:O,_fnAddTr:na,_fnNodeToDataIndex:function(a, +b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:kb,_fnSplitObjNotation:Ja,_fnGetObjectDataFn:S,_fnSetObjectDataFn:N,_fnGetDataMaster:Ka,_fnClearTable:oa,_fnDeleteIndex:pa,_fnInvalidate:da,_fnGetRowElements:Ia,_fnCreateTr:Ha,_fnBuildHead:lb,_fnDrawHead:fa,_fnDraw:P,_fnReDraw:T,_fnAddOptionsHtml:ob,_fnDetectHeader:ea,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:qb,_fnFilterComplete:ga,_fnFilterCustom:zb, +_fnFilterColumn:yb,_fnFilter:xb,_fnFilterCreateSearch:Pa,_fnEscapeRegex:Qa,_fnFilterData:Ab,_fnFeatureHtmlInfo:tb,_fnUpdateInfo:Db,_fnInfoMacros:Eb,_fnInitialise:ha,_fnInitComplete:ua,_fnLengthChange:Ra,_fnFeatureHtmlLength:pb,_fnFeatureHtmlPaginate:ub,_fnPageChange:Ta,_fnFeatureHtmlProcessing:rb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:sb,_fnScrollDraw:la,_fnApplyToChildren:I,_fnCalculateColumnWidths:Fa,_fnThrottle:Oa,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:v, +_fnSortFlatten:X,_fnSort:nb,_fnSortAria:Jb,_fnSortListener:Va,_fnSortAttachListener:Ma,_fnSortingClasses:wa,_fnSortData:Ib,_fnSaveState:xa,_fnLoadState:Kb,_fnSettingsFromNode:ya,_fnLog:K,_fnMap:F,_fnBindAction:Wa,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Sa,_fnRenderer:Na,_fnDataSource:y,_fnRowAttributes:La,_fnExtend:Xa,_fnCalculateEnd:function(){}});h.fn.dataTable=n;n.$=h;h.fn.dataTableSettings=n.settings;h.fn.dataTableExt=n.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()}; +h.each(n,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); diff --git a/archivebox-0.5.3/archivebox/themes/default/static/jquery.min.js b/archivebox-0.5.3/archivebox/themes/default/static/jquery.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/default/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + +
    +
    + +
    +
    + + + + + + + + + + $rows +
    BookmarkedSnapshot ($num_links)FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/archivebox/themes/legacy/main_index_row.html b/archivebox-0.5.3/archivebox/themes/legacy/main_index_row.html new file mode 100644 index 0000000..9112eac --- /dev/null +++ b/archivebox-0.5.3/archivebox/themes/legacy/main_index_row.html @@ -0,0 +1,16 @@ + + $bookmarked_date + + + + $title + $tags + + + + 📄 + $num_outputs + + + $url + diff --git a/archivebox-0.5.3/archivebox/util.py b/archivebox-0.5.3/archivebox/util.py new file mode 100644 index 0000000..5530ab4 --- /dev/null +++ b/archivebox-0.5.3/archivebox/util.py @@ -0,0 +1,318 @@ +__package__ = 'archivebox' + +import re +import requests +import json as pyjson + +from typing import List, Optional, Any +from pathlib import Path +from inspect import signature +from functools import wraps +from hashlib import sha256 +from urllib.parse import urlparse, quote, unquote +from html import escape, unescape +from datetime import datetime +from dateparser import parse as dateparser +from requests.exceptions import RequestException, ReadTimeout + +from .vendor.base32_crockford import encode as base32_encode # type: ignore +from w3lib.encoding import html_body_declared_encoding, http_content_type_encoding + +try: + import chardet + detect_encoding = lambda rawdata: chardet.detect(rawdata)["encoding"] +except ImportError: + detect_encoding = lambda rawdata: "utf-8" + +### Parsing Helpers + +# All of these are (str) -> str +# shortcuts to: https://docs.python.org/3/library/urllib.parse.html#url-parsing +scheme = lambda url: urlparse(url).scheme.lower() +without_scheme = lambda url: urlparse(url)._replace(scheme='').geturl().strip('//') +without_query = lambda url: urlparse(url)._replace(query='').geturl().strip('//') +without_fragment = lambda url: urlparse(url)._replace(fragment='').geturl().strip('//') +without_path = lambda url: urlparse(url)._replace(path='', fragment='', query='').geturl().strip('//') +path = lambda url: urlparse(url).path +basename = lambda url: urlparse(url).path.rsplit('/', 1)[-1] +domain = lambda url: urlparse(url).netloc +query = lambda url: urlparse(url).query +fragment = lambda url: urlparse(url).fragment +extension = lambda url: basename(url).rsplit('.', 1)[-1].lower() if '.' in basename(url) else '' +base_url = lambda url: without_scheme(url) # uniq base url used to dedupe links + +without_www = lambda url: url.replace('://www.', '://', 1) +without_trailing_slash = lambda url: url[:-1] if url[-1] == '/' else url.replace('/?', '?') +hashurl = lambda url: base32_encode(int(sha256(base_url(url).encode('utf-8')).hexdigest(), 16))[:20] + +urlencode = lambda s: s and quote(s, encoding='utf-8', errors='replace') +urldecode = lambda s: s and unquote(s) +htmlencode = lambda s: s and escape(s, quote=True) +htmldecode = lambda s: s and unescape(s) + +short_ts = lambda ts: str(parse_date(ts).timestamp()).split('.')[0] +ts_to_date = lambda ts: ts and parse_date(ts).strftime('%Y-%m-%d %H:%M') +ts_to_iso = lambda ts: ts and parse_date(ts).isoformat() + + +URL_REGEX = re.compile( + r'http[s]?://' # start matching from allowed schemes + r'(?:[a-zA-Z]|[0-9]' # followed by allowed alphanum characters + r'|[$-_@.&+]|[!*\(\),]' # or allowed symbols + r'|(?:%[0-9a-fA-F][0-9a-fA-F]))' # or allowed unicode bytes + r'[^\]\[\(\)<>"\'\s]+', # stop parsing at these symbols + re.IGNORECASE, +) + +COLOR_REGEX = re.compile(r'\[(?P\d+)(;(?P\d+)(;(?P\d+))?)?m') + +def is_static_file(url: str): + # TODO: the proper way is with MIME type detection + ext, not only extension + from .config import STATICFILE_EXTENSIONS + return extension(url).lower() in STATICFILE_EXTENSIONS + + +def enforce_types(func): + """ + Enforce function arg and kwarg types at runtime using its python3 type hints + """ + # TODO: check return type as well + + @wraps(func) + def typechecked_function(*args, **kwargs): + sig = signature(func) + + def check_argument_type(arg_key, arg_val): + try: + annotation = sig.parameters[arg_key].annotation + except KeyError: + annotation = None + + if annotation is not None and annotation.__class__ is type: + if not isinstance(arg_val, annotation): + raise TypeError( + '{}(..., {}: {}) got unexpected {} argument {}={}'.format( + func.__name__, + arg_key, + annotation.__name__, + type(arg_val).__name__, + arg_key, + str(arg_val)[:64], + ) + ) + + # check args + for arg_val, arg_key in zip(args, sig.parameters): + check_argument_type(arg_key, arg_val) + + # check kwargs + for arg_key, arg_val in kwargs.items(): + check_argument_type(arg_key, arg_val) + + return func(*args, **kwargs) + + return typechecked_function + + +def docstring(text: Optional[str]): + """attach the given docstring to the decorated function""" + def decorator(func): + if text: + func.__doc__ = text + return func + return decorator + + +@enforce_types +def str_between(string: str, start: str, end: str=None) -> str: + """(12345, , ) -> 12345""" + + content = string.split(start, 1)[-1] + if end is not None: + content = content.rsplit(end, 1)[0] + + return content + + +@enforce_types +def parse_date(date: Any) -> Optional[datetime]: + """Parse unix timestamps, iso format, and human-readable strings""" + + if date is None: + return None + + if isinstance(date, datetime): + return date + + if isinstance(date, (float, int)): + date = str(date) + + if isinstance(date, str): + return dateparser(date) + + raise ValueError('Tried to parse invalid date! {}'.format(date)) + + +@enforce_types +def download_url(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the text""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + ) + + content_type = response.headers.get('Content-Type', '') + encoding = http_content_type_encoding(content_type) or html_body_declared_encoding(response.text) + + if encoding is not None: + response.encoding = encoding + + return response.text + +@enforce_types +def get_headers(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the headers""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + + try: + response = requests.head( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + allow_redirects=True, + ) + if response.status_code >= 400: + raise RequestException + except ReadTimeout: + raise + except RequestException: + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + stream=True + ) + + return pyjson.dumps(dict(response.headers), indent=4) + + +@enforce_types +def chrome_args(**options) -> List[str]: + """helper to build up a chrome shell command with arguments""" + + from .config import CHROME_OPTIONS + + options = {**CHROME_OPTIONS, **options} + + cmd_args = [options['CHROME_BINARY']] + + if options['CHROME_HEADLESS']: + cmd_args += ('--headless',) + + if not options['CHROME_SANDBOX']: + # assume this means we are running inside a docker container + # in docker, GPU support is limited, sandboxing is unecessary, + # and SHM is limited to 64MB by default (which is too low to be usable). + cmd_args += ( + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + ) + + + if not options['CHECK_SSL_VALIDITY']: + cmd_args += ('--disable-web-security', '--ignore-certificate-errors') + + if options['CHROME_USER_AGENT']: + cmd_args += ('--user-agent={}'.format(options['CHROME_USER_AGENT']),) + + if options['RESOLUTION']: + cmd_args += ('--window-size={}'.format(options['RESOLUTION']),) + + if options['TIMEOUT']: + cmd_args += ('--timeout={}'.format((options['TIMEOUT']) * 1000),) + + if options['CHROME_USER_DATA_DIR']: + cmd_args.append('--user-data-dir={}'.format(options['CHROME_USER_DATA_DIR'])) + + return cmd_args + + +def ansi_to_html(text): + """ + Based on: https://stackoverflow.com/questions/19212665/python-converting-ansi-color-codes-to-html + """ + from .config import COLOR_DICT + + TEMPLATE = '
    ' + text = text.replace('[m', '
    ') + + def single_sub(match): + argsdict = match.groupdict() + if argsdict['arg_3'] is None: + if argsdict['arg_2'] is None: + _, color = 0, argsdict['arg_1'] + else: + _, color = argsdict['arg_1'], argsdict['arg_2'] + else: + _, color = argsdict['arg_3'], argsdict['arg_2'] + + return TEMPLATE.format(COLOR_DICT[color][0]) + + return COLOR_REGEX.sub(single_sub, text) + + +class AttributeDict(dict): + """Helper to allow accessing dict values via Example.key or Example['key']""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Recursively convert nested dicts to AttributeDicts (optional): + # for key, val in self.items(): + # if isinstance(val, dict) and type(val) is not AttributeDict: + # self[key] = AttributeDict(val) + + def __getattr__(self, attr: str) -> Any: + return dict.__getitem__(self, attr) + + def __setattr__(self, attr: str, value: Any) -> None: + return dict.__setitem__(self, attr, value) + + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif isinstance(obj, Path): + return str(obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + diff --git a/archivebox-0.5.3/archivebox/vendor/__init__.py b/archivebox-0.5.3/archivebox/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/build/lib/archivebox/.flake8 b/archivebox-0.5.3/build/lib/archivebox/.flake8 new file mode 100644 index 0000000..dd6ba8e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = D100,D101,D102,D103,D104,D105,D202,D203,D205,D400,E131,E241,E252,E266,E272,E701,E731,W293,W503,W291,W391 +select = F,E9,W +max-line-length = 130 +max-complexity = 10 +exclude = migrations,tests,node_modules,vendor,static,venv,.venv,.venv2,.docker-venv diff --git a/archivebox-0.5.3/build/lib/archivebox/LICENSE b/archivebox-0.5.3/build/lib/archivebox/LICENSE new file mode 100644 index 0000000..ea201f9 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Nick Sweeting + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/archivebox-0.5.3/build/lib/archivebox/README.md b/archivebox-0.5.3/build/lib/archivebox/README.md new file mode 100644 index 0000000..2e35783 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/README.md @@ -0,0 +1,545 @@ +
    + +

    ArchiveBox
    The open-source self-hosted web archive.

    + +▶️ Quickstart | +Demo | +Github | +Documentation | +Info & Motivation | +Community | +Roadmap + +
    +"Your own personal internet archive" (网站存档 / 爬虫)
    +
    + + + + + + + + + + +
    +
    + +ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + +Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + +The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + +### Quickstart + +It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + +```bash +pip3 install archivebox +archivebox --version +# install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + +mkdir ~/archivebox && cd ~/archivebox # this can be anywhere +archivebox init + +archivebox add 'https://example.com' +archivebox add --depth=1 'https://example.com' +archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all +archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ +archivebox help # to see more options +``` + +*(click to expand the sections below for full setup instructions)* + +
    +Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + +First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

    +This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml +docker-compose run archivebox init +docker-compose run archivebox --version + +# start the webserver and open the UI (optional) +docker-compose run archivebox manage createsuperuser +docker-compose up -d +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker-compose run archivebox add 'https://example.com' +docker-compose run archivebox status +docker-compose run archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with docker on any platform + +First make sure you have Docker installed: https://docs.docker.com/get-docker/
    +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +docker run -v $PWD:/data -it archivebox/archivebox init +docker run -v $PWD:/data -it archivebox/archivebox --version + +# start the webserver and open the UI (optional) +docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser +docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' +docker run -v $PWD:/data -it archivebox/archivebox status +docker run -v $PWD:/data -it archivebox/archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with apt on Ubuntu >=20.04 + +```bash +sudo add-apt-repository -u ppa:archivebox/archivebox +sudo apt install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: +```bash +deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +``` +(you may need to install some other dependencies manually however) + +
    + +
    +Get ArchiveBox with brew on macOS >=10.13 + +```bash +brew install archivebox/archivebox/archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with pip on any platform + +```bash +pip3 install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version +# Install any missing extras like wget/git/chrome/etc. manually as needed + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
    + +--- + +
    + +
    + +DEMO: archivebox.zervice.io/ +For more information, see the full Quickstart guide, Usage, and Configuration docs. +
    + +--- + + +# Overview + +ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + +To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + +The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + +At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
    +CLI Screenshot +Desktop index screenshot +Desktop details page Screenshot +Desktop details page Screenshot
    +Demo | Usage | Screenshots +
    +. . . . . . . . . . . . . . . . . . . . . . . . . . . . +

    + + +## Key Features + +- [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally +- [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) +- [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) +- Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** +- Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC +- ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) +- **Doesn't require a constantly-running daemon**, proxy, or native app +- Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) +- Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. +- Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + +## Input formats + +ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + +```bash +echo 'http://example.com' | archivebox add +archivebox add 'https://example.com/some/page' +archivebox add < ~/Downloads/firefox_bookmarks_export.html +archivebox add < any_text_with_urls_in_it.txt +archivebox add --depth=1 'https://example.com/some/downloads.html' +archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' +``` + +- Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) +- RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format +- Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + +See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + +It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + +## Output formats + +All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + +The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + +```bash + ls ./archive// +``` + +- **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details +- **Title:** `title` title of the site +- **Favicon:** `favicon.ico` favicon of the site +- **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file +- **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile +- **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present +- **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving +- **PDF:** `output.pdf` Printed PDF of site using headless chrome +- **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome +- **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome +- **Readability:** `article.html/json` Article text extraction using Readability +- **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org +- **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl +- **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links +- _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + +It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + +## Dependencies + +You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + +If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + +ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + +## Caveats + +If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. +```bash +# don't do this: +archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' +archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + +# without first disabling share the URL with 3rd party APIs: +archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org +archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL +archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google +``` + +Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. +```bash +# visiting an archived page with malicious JS: +https://127.0.0.1:8000/archive/1602401954/example.com/index.html + +# example.com/index.js can now make a request to read everything: +https://127.0.0.1:8000/index.html +https://127.0.0.1:8000/archive/* +# then example.com/index.js can send it off to some evil server +``` + +Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: +```bash +archivebox add 'https://example.com#2020-10-24' +... +archivebox add 'https://example.com#2020-10-25' +``` + +--- + +
    + +
    + +--- + +# Background & Motivation + +Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + +Whether it's to resist censorship by saving articles before they get taken down or edited, or +just to save a collection of early 2010's flash games you love to play, having the tools to +archive internet content enables to you save the stuff you care most about before it disappears. + +
    +
    + Image from WTF is Link Rot?...
    +
    + +The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. +I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + +Because modern websites are complicated and often rely on dynamic content, +ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + +All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + +## Comparison to Other Projects + +▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + +comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + +#### User Interface & Intended Purpose + +ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + +#### Private Local Archives vs Centralized Public Archives + +Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + +#### Storage Requirements + +Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + +## Learn more + +Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + +- [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ +- Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. +- Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + +--- + +# Documentation + + + +We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + +You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + +## Getting Started + +- [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) +- [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) +- [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + +## Reference + +- [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) +- [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) +- [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) +- [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) +- [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) +- [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) +- [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) +- [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) +- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) +- [Python API](https://docs.archivebox.io/en/latest/modules.html) +- REST API (coming soon...) + +## More Info + +- [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) +- [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) +- [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) +- [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) +- [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + +--- + +# ArchiveBox Development + +All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + +### Setup the dev environment + +First, install the system dependencies from the "Bare Metal" section above. +Then you can clone the ArchiveBox repo and install +```python3 +git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox +git checkout master # or the branch you want to test +git submodule update --init --recursive +git pull --recurse-submodules + +# Install ArchiveBox + python dependencies +python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] +# or with pipenv: pipenv install --dev && pipenv shell + +# Install node dependencies +npm install + +# Optional: install extractor dependencies manually or with helper script +./bin/setup.sh + +# Optional: develop via docker by mounting the code dir into the container +# if you edit e.g. ./archivebox/core/models.py on the docker host, runserver +# inside the container will reload and pick up your changes +docker build . -t archivebox +docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload +``` + +### Common development tasks + +See the `./bin/` folder and read the source of the bash scripts within. +You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + +#### Run the linters + +```bash +./bin/lint.sh +``` +(uses `flake8` and `mypy`) + +#### Run the integration tests + +```bash +./bin/test.sh +``` +(uses `pytest -s`) + +#### Make migrations or enter a django shell + +```bash +cd archivebox/ +./manage.py makemigrations + +cd data/ +archivebox shell +``` +(uses `pytest -s`) + +#### Build the docs, pip package, and docker image + +```bash +./bin/build.sh + +# or individually: +./bin/build_docs.sh +./bin/build_pip.sh +./bin/build_deb.sh +./bin/build_brew.sh +./bin/build_docker.sh +``` + +#### Roll a release + +```bash +./bin/release.sh +``` +(bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + +--- + +
    +

    + +
    +This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

    + +
    +Sponsor us on Github +
    +
    + +
    + + + + +

    + +
    diff --git a/archivebox-0.5.3/build/lib/archivebox/__init__.py b/archivebox-0.5.3/build/lib/archivebox/__init__.py new file mode 100644 index 0000000..b0c00b6 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox' diff --git a/archivebox-0.5.3/build/lib/archivebox/__main__.py b/archivebox-0.5.3/build/lib/archivebox/__main__.py new file mode 100644 index 0000000..8afaa27 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/__main__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox' + +import sys + +from .cli import main + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/__init__.py b/archivebox-0.5.3/build/lib/archivebox/cli/__init__.py new file mode 100644 index 0000000..f9a55ef --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/__init__.py @@ -0,0 +1,144 @@ +__package__ = 'archivebox.cli' +__command__ = 'archivebox' + +import os +import sys +import argparse + +from typing import Optional, Dict, List, IO, Union +from pathlib import Path + +from ..config import OUTPUT_DIR + +from importlib import import_module + +CLI_DIR = Path(__file__).resolve().parent + +# these common commands will appear sorted before any others for ease-of-use +meta_cmds = ('help', 'version') +main_cmds = ('init', 'info', 'config') +archive_cmds = ('add', 'remove', 'update', 'list', 'status') + +fake_db = ("oneshot",) + +display_first = (*meta_cmds, *main_cmds, *archive_cmds) + +# every imported command module must have these properties in order to be valid +required_attrs = ('__package__', '__command__', 'main') + +# basic checks to make sure imported files are valid subcommands +is_cli_module = lambda fname: fname.startswith('archivebox_') and fname.endswith('.py') +is_valid_cli_module = lambda module, subcommand: ( + all(hasattr(module, attr) for attr in required_attrs) + and module.__command__.split(' ')[-1] == subcommand +) + + +def list_subcommands() -> Dict[str, str]: + """find and import all valid archivebox_.py files in CLI_DIR""" + + COMMANDS = [] + for filename in os.listdir(CLI_DIR): + if is_cli_module(filename): + subcommand = filename.replace('archivebox_', '').replace('.py', '') + module = import_module('.archivebox_{}'.format(subcommand), __package__) + assert is_valid_cli_module(module, subcommand) + COMMANDS.append((subcommand, module.main.__doc__)) + globals()[subcommand] = module.main + + display_order = lambda cmd: ( + display_first.index(cmd[0]) + if cmd[0] in display_first else + 100 + len(cmd[0]) + ) + + return dict(sorted(COMMANDS, key=display_order)) + + +def run_subcommand(subcommand: str, + subcommand_args: List[str]=None, + stdin: Optional[IO]=None, + pwd: Union[Path, str, None]=None) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + + if subcommand not in meta_cmds: + from ..config import setup_django + setup_django(in_memory_db=subcommand in fake_db, check_db=subcommand in archive_cmds) + + module = import_module('.archivebox_{}'.format(subcommand), __package__) + module.main(args=subcommand_args, stdin=stdin, pwd=pwd) # type: ignore + + +SUBCOMMANDS = list_subcommands() + +class NotProvided: + pass + + +def main(args: Optional[List[str]]=NotProvided, stdin: Optional[IO]=NotProvided, pwd: Optional[str]=None) -> None: + args = sys.argv[1:] if args is NotProvided else args + stdin = sys.stdin if stdin is NotProvided else stdin + + subcommands = list_subcommands() + parser = argparse.ArgumentParser( + prog=__command__, + description='ArchiveBox: The self-hosted internet archive', + add_help=False, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--help', '-h', + action='store_true', + help=subcommands['help'], + ) + group.add_argument( + '--version', + action='store_true', + help=subcommands['version'], + ) + group.add_argument( + "subcommand", + type=str, + help= "The name of the subcommand to run", + nargs='?', + choices=subcommands.keys(), + default=None, + ) + parser.add_argument( + "subcommand_args", + help="Arguments for the subcommand", + nargs=argparse.REMAINDER, + ) + command = parser.parse_args(args or ()) + + if command.version: + command.subcommand = 'version' + elif command.help or command.subcommand is None: + command.subcommand = 'help' + + if command.subcommand not in ('help', 'version', 'status'): + from ..logging_util import log_cli_command + + log_cli_command( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR + ) + + run_subcommand( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR, + ) + + +__all__ = ( + 'SUBCOMMANDS', + 'list_subcommands', + 'run_subcommand', + *SUBCOMMANDS.keys(), +) + + diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_add.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_add.py new file mode 100644 index 0000000..41c7554 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_add.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox add' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import add +from ..util import docstring +from ..config import OUTPUT_DIR, ONLY_NEW +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(add.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=add.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--update-all', #'-n', + action='store_true', + default=not ONLY_NEW, # when ONLY_NEW=True we skip updating old links + help="Also retry previously skipped/failed links when adding new links", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Add the links to the main index without archiving them", + ) + parser.add_argument( + 'urls', + nargs='*', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--depth", + action="store", + default=0, + choices=[0, 1], + type=int, + help="Recursively archive all linked pages up to this many hops away" + ) + parser.add_argument( + "--overwrite", + default=False, + action="store_true", + help="Re-archive URLs from scratch, overwriting any existing files" + ) + parser.add_argument( + "--init", #'-i', + action='store_true', + help="Init/upgrade the curent data directory before adding", + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + urls = command.urls + stdin_urls = accept_stdin(stdin) + if (stdin_urls and urls) or (not stdin and not urls): + stderr( + '[X] You must pass URLs/paths to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + add( + urls=stdin_urls or urls, + depth=command.depth, + update_all=command.update_all, + index_only=command.index_only, + overwrite=command.overwrite, + init=command.init, + extractors=command.extract, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) + + +# TODO: Implement these +# +# parser.add_argument( +# '--mirror', #'-m', +# action='store_true', +# help='Archive an entire site (finding all linked pages below it on the same domain)', +# ) +# parser.add_argument( +# '--crawler', #'-r', +# choices=('depth_first', 'breadth_first'), +# help='Controls which crawler to use in order to find outlinks in a given page', +# default=None, +# ) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_config.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_config.py new file mode 100644 index 0000000..f81286c --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_config.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox config' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import config +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(config.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=config.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--get', #'-g', + action='store_true', + help="Get the value for the given config KEYs", + ) + group.add_argument( + '--set', #'-s', + action='store_true', + help="Set the given KEY=VALUE config values", + ) + group.add_argument( + '--reset', #'-s', + action='store_true', + help="Reset the given KEY config values to their defaults", + ) + parser.add_argument( + 'config_options', + nargs='*', + type=str, + help='KEY or KEY=VALUE formatted config values to get or set', + ) + command = parser.parse_args(args or ()) + config_options_str = accept_stdin(stdin) + + config( + config_options_str=config_options_str, + config_options=command.config_options, + get=command.get, + set=command.set, + reset=command.reset, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_help.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_help.py new file mode 100755 index 0000000..46f17cb --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_help.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox help' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import help +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(help.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=help.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + help(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_init.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_init.py new file mode 100755 index 0000000..6255ef2 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_init.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox init' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import init +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(init.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=init.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--force', # '-f', + action='store_true', + help='Ignore unrecognized files in current directory and initialize anyway', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + init( + force=command.force, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_list.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_list.py new file mode 100644 index 0000000..3838cf6 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_list.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox list' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import list_all +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(list_all.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=list_all.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--csv', #'-c', + type=str, + help="Print the output in CSV format with the given columns, e.g.: timestamp,url,extension", + default=None, + ) + group.add_argument( + '--json', #'-j', + action='store_true', + help="Print the output in JSON format with all columns included.", + ) + group.add_argument( + '--html', + action='store_true', + help="Print the output in HTML format" + ) + parser.add_argument( + '--with-headers', + action='store_true', + help='Include the headers in the output document' + ) + parser.add_argument( + '--sort', #'-s', + type=str, + help="List the links sorted using the given key, e.g. timestamp or updated.", + default=None, + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'List only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='List only URLs matching these filter patterns.' + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + if command.with_headers and not (command.json or command.html or command.csv): + stderr( + '[X] --with-headers can only be used with --json, --html or --csv options.\n', + color='red', + ) + raise SystemExit(2) + + matching_folders = list_all( + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + sort=command.sort, + csv=command.csv, + json=command.json, + html=command.html, + with_headers=command.with_headers, + out_dir=pwd or OUTPUT_DIR, + ) + raise SystemExit(not matching_folders) + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_manage.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_manage.py new file mode 100644 index 0000000..f05604e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox manage' + +import sys + +from typing import Optional, List, IO + +from ..main import manage +from ..util import docstring +from ..config import OUTPUT_DIR + + +@docstring(manage.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + manage( + args=args, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_oneshot.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_oneshot.py new file mode 100644 index 0000000..af68bac --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_oneshot.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox oneshot' + +import sys +import argparse + +from pathlib import Path +from typing import List, Optional, IO + +from ..main import oneshot +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(oneshot.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=oneshot.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'url', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + parser.add_argument( + '--out-dir', + type=str, + default=OUTPUT_DIR, + help= "Path to save the single archive folder to, e.g. ./example.com_archive" + ) + command = parser.parse_args(args or ()) + url = command.url + stdin_url = accept_stdin(stdin) + if (stdin_url and url) or (not stdin and not url): + stderr( + '[X] You must pass a URL/path to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + + oneshot( + url=stdin_url or url, + out_dir=Path(command.out_dir).resolve(), + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_remove.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_remove.py new file mode 100644 index 0000000..cb073e9 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_remove.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox remove' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import remove +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(remove.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=remove.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--yes', # '-y', + action='store_true', + help='Remove links instantly without prompting to confirm.', + ) + parser.add_argument( + '--delete', # '-r', + action='store_true', + help=( + "In addition to removing the link from the index, " + "also delete its archived content and metadata folder." + ), + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only URLs bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only URLs bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex','tag'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + help='URLs matching this filter pattern will be removed from the index.' + ) + command = parser.parse_args(args or ()) + filter_str = accept_stdin(stdin) + + remove( + filter_str=filter_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + before=command.before, + after=command.after, + yes=command.yes, + delete=command.delete, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_schedule.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_schedule.py new file mode 100644 index 0000000..ec5e914 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_schedule.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox schedule' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import schedule +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(schedule.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=schedule.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help=("Don't warn about storage space."), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--add', # '-a', + action='store_true', + help='Add a new scheduled ArchiveBox update job to cron', + ) + parser.add_argument( + '--every', # '-e', + type=str, + default=None, + help='Run ArchiveBox once every [timeperiod] (hour/day/month/year or cron format e.g. "0 0 * * *")', + ) + parser.add_argument( + '--depth', # '-d', + type=int, + default=0, + help='Depth to archive to [0] or 1, see "add" command help for more info.', + ) + group.add_argument( + '--clear', # '-c' + action='store_true', + help=("Stop all ArchiveBox scheduled runs (remove cron jobs)"), + ) + group.add_argument( + '--show', # '-s' + action='store_true', + help=("Print a list of currently active ArchiveBox cron jobs"), + ) + group.add_argument( + '--foreground', '-f', + action='store_true', + help=("Launch ArchiveBox scheduler as a long-running foreground task " + "instead of using cron."), + ) + group.add_argument( + '--run-all', # '-a', + action='store_true', + help=("Run all the scheduled jobs once immediately, independent of " + "their configured schedules, can be used together with --foreground"), + ) + parser.add_argument( + 'import_path', + nargs='?', + type=str, + default=None, + help=("Check this path and import any new links on every run " + "(can be either local file or remote URL)"), + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + schedule( + add=command.add, + show=command.show, + clear=command.clear, + foreground=command.foreground, + run_all=command.run_all, + quiet=command.quiet, + every=command.every, + depth=command.depth, + import_path=command.import_path, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_server.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_server.py new file mode 100644 index 0000000..dbacf7e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_server.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox server' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import server +from ..util import docstring +from ..config import OUTPUT_DIR, BIND_ADDR +from ..logging_util import SmartFormatter, reject_stdin + +@docstring(server.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=server.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'runserver_args', + nargs='*', + type=str, + default=[BIND_ADDR], + help='Arguments to pass to Django runserver' + ) + parser.add_argument( + '--reload', + action='store_true', + help='Enable auto-reloading when code or templates change', + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable DEBUG=True mode with more verbose errors', + ) + parser.add_argument( + '--init', + action='store_true', + help='Run archivebox init before starting the server', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + server( + runserver_args=command.runserver_args, + reload=command.reload, + debug=command.debug, + init=command.init, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_shell.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_shell.py new file mode 100644 index 0000000..bcd5fdd --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_shell.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox shell' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import shell +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(shell.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=shell.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + shell( + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_status.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_status.py new file mode 100644 index 0000000..2bef19c --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_status.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox status' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import status +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(status.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=status.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + status(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_update.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_update.py new file mode 100644 index 0000000..6748096 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_update.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox update' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import update +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(update.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=update.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--only-new', #'-n', + action='store_true', + help="Don't attempt to retry previously skipped/failed links when updating", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Update the main index without archiving any content", + ) + parser.add_argument( + '--resume', #'-r', + type=float, + help='Resume the update process from a given timestamp', + default=None, + ) + parser.add_argument( + '--overwrite', #'-x', + action='store_true', + help='Ignore existing archived content and overwrite with new versions (DANGEROUS)', + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="Update only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="Update only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'Update only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='Update only URLs matching these filter patterns.' + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + update( + resume=command.resume, + only_new=command.only_new, + index_only=command.index_only, + overwrite=command.overwrite, + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + out_dir=pwd or OUTPUT_DIR, + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_version.py b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_version.py new file mode 100755 index 0000000..e7922f3 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/archivebox_version.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox version' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import version +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(version.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=version.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Only print ArchiveBox version number and nothing else.', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + version( + quiet=command.quiet, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/build/lib/archivebox/cli/tests.py b/archivebox-0.5.3/build/lib/archivebox/cli/tests.py new file mode 100755 index 0000000..4d7016a --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/cli/tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' + + +import os +import sys +import shutil +import unittest +from pathlib import Path + +from contextlib import contextmanager + +TEST_CONFIG = { + 'USE_COLOR': 'False', + 'SHOW_PROGRESS': 'False', + + 'OUTPUT_DIR': 'data.tests', + + 'SAVE_ARCHIVE_DOT_ORG': 'False', + 'SAVE_TITLE': 'False', + + 'USE_CURL': 'False', + 'USE_WGET': 'False', + 'USE_GIT': 'False', + 'USE_CHROME': 'False', + 'USE_YOUTUBEDL': 'False', +} + +OUTPUT_DIR = 'data.tests' +os.environ.update(TEST_CONFIG) + +from ..main import init +from ..index import load_main_index +from ..config import ( + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, +) + +from . import ( + archivebox_init, + archivebox_add, + archivebox_remove, +) + +HIDE_CLI_OUTPUT = True + +test_urls = ''' +https://example1.com/what/is/happening.html?what=1#how-about-this=1 +https://example2.com/what/is/happening/?what=1#how-about-this=1 +HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f +https://example4.com/what/is/happening.html +https://example5.com/ +https://example6.com + +http://example7.com +[https://example8.com/what/is/this.php?what=1] +[and http://example9.com?what=1&other=3#and-thing=2] +https://example10.com#and-thing=2 " +abcdef +sdflkf[what](https://subb.example12.com/who/what.php?whoami=1#whatami=2)?am=hi +example13.bada +and example14.badb +htt://example15.badc +''' + +stdout = sys.stdout +stderr = sys.stderr + + +@contextmanager +def output_hidden(show_failing=True): + if not HIDE_CLI_OUTPUT: + yield + return + + sys.stdout = open('stdout.txt', 'w+') + sys.stderr = open('stderr.txt', 'w+') + try: + yield + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + except: + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + if show_failing: + with open('stdout.txt', 'r') as f: + print(f.read()) + with open('stderr.txt', 'r') as f: + print(f.read()) + raise + finally: + os.remove('stdout.txt') + os.remove('stderr.txt') + + +class TestInit(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_basic_init(self): + with output_hidden(): + archivebox_init.main([]) + + assert (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + assert len(load_main_index(out_dir=OUTPUT_DIR)) == 0 + + def test_conflicting_init(self): + with open(Path(OUTPUT_DIR) / 'test_conflict.txt', 'w+') as f: + f.write('test') + + try: + with output_hidden(show_failing=False): + archivebox_init.main([]) + assert False, 'Init should have exited with an exception' + except SystemExit: + pass + + assert not (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + try: + load_main_index(out_dir=OUTPUT_DIR) + assert False, 'load_main_index should raise an exception when no index is present' + except: + pass + + def test_no_dirty_state(self): + with output_hidden(): + init() + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + with output_hidden(): + init() + + +class TestAdd(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_add_arg_url(self): + with output_hidden(): + archivebox_add.main(['https://getpocket.com/users/nikisweeting/feed/all']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 30 + + def test_add_arg_file(self): + test_file = Path(OUTPUT_DIR) / 'test.txt' + with open(test_file, 'w+') as f: + f.write(test_urls) + + with output_hidden(): + archivebox_add.main([test_file]) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + os.remove(test_file) + + def test_add_stdin_url(self): + with output_hidden(): + archivebox_add.main([], stdin=test_urls) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + + +class TestRemove(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + archivebox_add.main([], stdin=test_urls) + + # def tearDown(self): + # shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + + def test_remove_exact(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', 'https://example5.com/']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 11 + + def test_remove_regex(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=regex', r'http(s)?:\/\/(.+\.)?(example\d\.com)']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 4 + + def test_remove_domain(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=domain', 'example5.com', 'example6.com']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 10 + + def test_remove_none(self): + try: + with output_hidden(show_failing=False): + archivebox_remove.main(['--yes', '--delete', 'https://doesntexist.com']) + assert False, 'Should raise if no URLs match' + except: + pass + + +if __name__ == '__main__': + if '--verbose' in sys.argv or '-v' in sys.argv: + HIDE_CLI_OUTPUT = False + + unittest.main() diff --git a/archivebox-0.5.3/build/lib/archivebox/config.py b/archivebox-0.5.3/build/lib/archivebox/config.py new file mode 100644 index 0000000..9a3f9a7 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/config.py @@ -0,0 +1,1081 @@ +""" +ArchiveBox config definitons (including defaults and dynamic config options). + +Config Usage Example: + + archivebox config --set MEDIA_TIMEOUT=600 + env MEDIA_TIMEOUT=600 USE_COLOR=False ... archivebox [subcommand] ... + +Config Precedence Order: + + 1. cli args (--update-all / --index-only / etc.) + 2. shell environment vars (env USE_COLOR=False archivebox add '...') + 3. config file (echo "SAVE_FAVICON=False" >> ArchiveBox.conf) + 4. defaults (defined below in Python) + +Documentation: + + https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + +""" + +__package__ = 'archivebox' + +import os +import io +import re +import sys +import json +import getpass +import shutil +import django + +from hashlib import md5 +from pathlib import Path +from typing import Optional, Type, Tuple, Dict, Union, List +from subprocess import run, PIPE, DEVNULL +from configparser import ConfigParser +from collections import defaultdict + +from .config_stubs import ( + SimpleConfigValueDict, + ConfigValue, + ConfigDict, + ConfigDefaultValue, + ConfigDefaultDict, +) + +############################### Config Schema ################################## + +CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { + 'SHELL_CONFIG': { + 'IS_TTY': {'type': bool, 'default': lambda _: sys.stdout.isatty()}, + 'USE_COLOR': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'SHOW_PROGRESS': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'IN_DOCKER': {'type': bool, 'default': False}, + # TODO: 'SHOW_HINTS': {'type: bool, 'default': True}, + }, + + 'GENERAL_CONFIG': { + 'OUTPUT_DIR': {'type': str, 'default': None}, + 'CONFIG_FILE': {'type': str, 'default': None}, + 'ONLY_NEW': {'type': bool, 'default': True}, + 'TIMEOUT': {'type': int, 'default': 60}, + 'MEDIA_TIMEOUT': {'type': int, 'default': 3600}, + 'OUTPUT_PERMISSIONS': {'type': str, 'default': '755'}, + 'RESTRICT_FILE_NAMES': {'type': str, 'default': 'windows'}, + 'URL_BLACKLIST': {'type': str, 'default': r'\.(css|js|otf|ttf|woff|woff2|gstatic\.com|googleapis\.com/css)(\?.*)?$'}, # to avoid downloading code assets as their own pages + }, + + 'SERVER_CONFIG': { + 'SECRET_KEY': {'type': str, 'default': None}, + 'BIND_ADDR': {'type': str, 'default': lambda c: ['127.0.0.1:8000', '0.0.0.0:8000'][c['IN_DOCKER']]}, + 'ALLOWED_HOSTS': {'type': str, 'default': '*'}, + 'DEBUG': {'type': bool, 'default': False}, + 'PUBLIC_INDEX': {'type': bool, 'default': True}, + 'PUBLIC_SNAPSHOTS': {'type': bool, 'default': True}, + 'PUBLIC_ADD_VIEW': {'type': bool, 'default': False}, + 'FOOTER_INFO': {'type': str, 'default': 'Content is hosted for personal archiving purposes only. Contact server owner for any takedown requests.'}, + 'ACTIVE_THEME': {'type': str, 'default': 'default'}, + }, + + 'ARCHIVE_METHOD_TOGGLES': { + 'SAVE_TITLE': {'type': bool, 'default': True, 'aliases': ('FETCH_TITLE',)}, + 'SAVE_FAVICON': {'type': bool, 'default': True, 'aliases': ('FETCH_FAVICON',)}, + 'SAVE_WGET': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET',)}, + 'SAVE_WGET_REQUISITES': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET_REQUISITES',)}, + 'SAVE_SINGLEFILE': {'type': bool, 'default': True, 'aliases': ('FETCH_SINGLEFILE',)}, + 'SAVE_READABILITY': {'type': bool, 'default': True, 'aliases': ('FETCH_READABILITY',)}, + 'SAVE_MERCURY': {'type': bool, 'default': True, 'aliases': ('FETCH_MERCURY',)}, + 'SAVE_PDF': {'type': bool, 'default': True, 'aliases': ('FETCH_PDF',)}, + 'SAVE_SCREENSHOT': {'type': bool, 'default': True, 'aliases': ('FETCH_SCREENSHOT',)}, + 'SAVE_DOM': {'type': bool, 'default': True, 'aliases': ('FETCH_DOM',)}, + 'SAVE_HEADERS': {'type': bool, 'default': True, 'aliases': ('FETCH_HEADERS',)}, + 'SAVE_WARC': {'type': bool, 'default': True, 'aliases': ('FETCH_WARC',)}, + 'SAVE_GIT': {'type': bool, 'default': True, 'aliases': ('FETCH_GIT',)}, + 'SAVE_MEDIA': {'type': bool, 'default': True, 'aliases': ('FETCH_MEDIA',)}, + 'SAVE_ARCHIVE_DOT_ORG': {'type': bool, 'default': True, 'aliases': ('SUBMIT_ARCHIVE_DOT_ORG',)}, + }, + + 'ARCHIVE_METHOD_OPTIONS': { + 'RESOLUTION': {'type': str, 'default': '1440,2000', 'aliases': ('SCREENSHOT_RESOLUTION',)}, + 'GIT_DOMAINS': {'type': str, 'default': 'github.com,bitbucket.org,gitlab.com'}, + 'CHECK_SSL_VALIDITY': {'type': bool, 'default': True}, + + 'CURL_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) curl/{CURL_VERSION}'}, + 'WGET_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) wget/{WGET_VERSION}'}, + 'CHROME_USER_AGENT': {'type': str, 'default': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}, + + 'COOKIES_FILE': {'type': str, 'default': None}, + 'CHROME_USER_DATA_DIR': {'type': str, 'default': None}, + + 'CHROME_HEADLESS': {'type': bool, 'default': True}, + 'CHROME_SANDBOX': {'type': bool, 'default': lambda c: not c['IN_DOCKER']}, + 'YOUTUBEDL_ARGS': {'type': list, 'default': ['--write-description', + '--write-info-json', + '--write-annotations', + '--write-thumbnail', + '--no-call-home', + '--user-agent', + '--all-subs', + '--extract-audio', + '--keep-video', + '--ignore-errors', + '--geo-bypass', + '--audio-format', 'mp3', + '--audio-quality', '320K', + '--embed-thumbnail', + '--add-metadata']}, + + 'WGET_ARGS': {'type': list, 'default': ['--no-verbose', + '--adjust-extension', + '--convert-links', + '--force-directories', + '--backup-converted', + '--span-hosts', + '--no-parent', + '-e', 'robots=off', + ]}, + 'CURL_ARGS': {'type': list, 'default': ['--silent', + '--location', + '--compressed' + ]}, + 'GIT_ARGS': {'type': list, 'default': ['--recursive']}, + }, + + 'SEARCH_BACKEND_CONFIG' : { + 'USE_INDEXING_BACKEND': {'type': bool, 'default': True}, + 'USE_SEARCHING_BACKEND': {'type': bool, 'default': True}, + 'SEARCH_BACKEND_ENGINE': {'type': str, 'default': 'ripgrep'}, + 'SEARCH_BACKEND_HOST_NAME': {'type': str, 'default': 'localhost'}, + 'SEARCH_BACKEND_PORT': {'type': int, 'default': 1491}, + 'SEARCH_BACKEND_PASSWORD': {'type': str, 'default': 'SecretPassword'}, + # SONIC + 'SONIC_COLLECTION': {'type': str, 'default': 'archivebox'}, + 'SONIC_BUCKET': {'type': str, 'default': 'snapshots'}, + }, + + 'DEPENDENCY_CONFIG': { + 'USE_CURL': {'type': bool, 'default': True}, + 'USE_WGET': {'type': bool, 'default': True}, + 'USE_SINGLEFILE': {'type': bool, 'default': True}, + 'USE_READABILITY': {'type': bool, 'default': True}, + 'USE_MERCURY': {'type': bool, 'default': True}, + 'USE_GIT': {'type': bool, 'default': True}, + 'USE_CHROME': {'type': bool, 'default': True}, + 'USE_NODE': {'type': bool, 'default': True}, + 'USE_YOUTUBEDL': {'type': bool, 'default': True}, + 'USE_RIPGREP': {'type': bool, 'default': True}, + + 'CURL_BINARY': {'type': str, 'default': 'curl'}, + 'GIT_BINARY': {'type': str, 'default': 'git'}, + 'WGET_BINARY': {'type': str, 'default': 'wget'}, + 'SINGLEFILE_BINARY': {'type': str, 'default': 'single-file'}, + 'READABILITY_BINARY': {'type': str, 'default': 'readability-extractor'}, + 'MERCURY_BINARY': {'type': str, 'default': 'mercury-parser'}, + 'YOUTUBEDL_BINARY': {'type': str, 'default': 'youtube-dl'}, + 'NODE_BINARY': {'type': str, 'default': 'node'}, + 'RIPGREP_BINARY': {'type': str, 'default': 'rg'}, + 'CHROME_BINARY': {'type': str, 'default': None}, + + 'POCKET_CONSUMER_KEY': {'type': str, 'default': None}, + 'POCKET_ACCESS_TOKENS': {'type': dict, 'default': {}}, + }, +} + + +########################## Backwards-Compatibility ############################# + + +# for backwards compatibility with old config files, check old/deprecated names for each key +CONFIG_ALIASES = { + alias: key + for section in CONFIG_SCHEMA.values() + for key, default in section.items() + for alias in default.get('aliases', ()) +} +USER_CONFIG = {key for section in CONFIG_SCHEMA.values() for key in section.keys()} + +def get_real_name(key: str) -> str: + """get the current canonical name for a given deprecated config key""" + return CONFIG_ALIASES.get(key.upper().strip(), key.upper().strip()) + + + +################################ Constants ##################################### + +PACKAGE_DIR_NAME = 'archivebox' +TEMPLATES_DIR_NAME = 'themes' + +ARCHIVE_DIR_NAME = 'archive' +SOURCES_DIR_NAME = 'sources' +LOGS_DIR_NAME = 'logs' +STATIC_DIR_NAME = 'static' +SQL_INDEX_FILENAME = 'index.sqlite3' +JSON_INDEX_FILENAME = 'index.json' +HTML_INDEX_FILENAME = 'index.html' +ROBOTS_TXT_FILENAME = 'robots.txt' +FAVICON_FILENAME = 'favicon.ico' +CONFIG_FILENAME = 'ArchiveBox.conf' + +DEFAULT_CLI_COLORS = { + 'reset': '\033[00;00m', + 'lightblue': '\033[01;30m', + 'lightyellow': '\033[01;33m', + 'lightred': '\033[01;35m', + 'red': '\033[01;31m', + 'green': '\033[01;32m', + 'blue': '\033[01;34m', + 'white': '\033[01;37m', + 'black': '\033[01;30m', +} +ANSI = {k: '' for k in DEFAULT_CLI_COLORS.keys()} + +COLOR_DICT = defaultdict(lambda: [(0, 0, 0), (0, 0, 0)], { + '00': [(0, 0, 0), (0, 0, 0)], + '30': [(0, 0, 0), (0, 0, 0)], + '31': [(255, 0, 0), (128, 0, 0)], + '32': [(0, 200, 0), (0, 128, 0)], + '33': [(255, 255, 0), (128, 128, 0)], + '34': [(0, 0, 255), (0, 0, 128)], + '35': [(255, 0, 255), (128, 0, 128)], + '36': [(0, 255, 255), (0, 128, 128)], + '37': [(255, 255, 255), (255, 255, 255)], +}) + +STATICFILE_EXTENSIONS = { + # 99.999% of the time, URLs ending in these extensions are static files + # that can be downloaded as-is, not html pages that need to be rendered + 'gif', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'wbmp', 'ico', 'jng', 'bmp', + 'svg', 'svgz', 'webp', 'ps', 'eps', 'ai', + 'mp3', 'mp4', 'm4a', 'mpeg', 'mpg', 'mkv', 'mov', 'webm', 'm4v', + 'flv', 'wmv', 'avi', 'ogg', 'ts', 'm3u8', + 'pdf', 'txt', 'rtf', 'rtfd', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', + 'atom', 'rss', 'css', 'js', 'json', + 'dmg', 'iso', 'img', + 'rar', 'war', 'hqx', 'zip', 'gz', 'bz2', '7z', + + # Less common extensions to consider adding later + # jar, swf, bin, com, exe, dll, deb + # ear, hqx, eot, wmlc, kml, kmz, cco, jardiff, jnlp, run, msi, msp, msm, + # pl pm, prc pdb, rar, rpm, sea, sit, tcl tk, der, pem, crt, xpi, xspf, + # ra, mng, asx, asf, 3gpp, 3gp, mid, midi, kar, jad, wml, htc, mml + + # These are always treated as pages, not as static files, never add them: + # html, htm, shtml, xhtml, xml, aspx, php, cgi +} + + + +############################## Derived Config ################################## + + +DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { + 'TERM_WIDTH': {'default': lambda c: lambda: shutil.get_terminal_size((100, 10)).columns}, + 'USER': {'default': lambda c: getpass.getuser() or os.getlogin()}, + 'ANSI': {'default': lambda c: DEFAULT_CLI_COLORS if c['USE_COLOR'] else {k: '' for k in DEFAULT_CLI_COLORS.keys()}}, + + 'PACKAGE_DIR': {'default': lambda c: Path(__file__).resolve().parent}, + 'TEMPLATES_DIR': {'default': lambda c: c['PACKAGE_DIR'] / TEMPLATES_DIR_NAME}, + + 'OUTPUT_DIR': {'default': lambda c: Path(c['OUTPUT_DIR']).resolve() if c['OUTPUT_DIR'] else Path(os.curdir).resolve()}, + 'ARCHIVE_DIR': {'default': lambda c: c['OUTPUT_DIR'] / ARCHIVE_DIR_NAME}, + 'SOURCES_DIR': {'default': lambda c: c['OUTPUT_DIR'] / SOURCES_DIR_NAME}, + 'LOGS_DIR': {'default': lambda c: c['OUTPUT_DIR'] / LOGS_DIR_NAME}, + 'CONFIG_FILE': {'default': lambda c: Path(c['CONFIG_FILE']).resolve() if c['CONFIG_FILE'] else c['OUTPUT_DIR'] / CONFIG_FILENAME}, + 'COOKIES_FILE': {'default': lambda c: c['COOKIES_FILE'] and Path(c['COOKIES_FILE']).resolve()}, + 'CHROME_USER_DATA_DIR': {'default': lambda c: find_chrome_data_dir() if c['CHROME_USER_DATA_DIR'] is None else (Path(c['CHROME_USER_DATA_DIR']).resolve() if c['CHROME_USER_DATA_DIR'] else None)}, # None means unset, so we autodetect it with find_chrome_Data_dir(), but emptystring '' means user manually set it to '', and we should store it as None + 'URL_BLACKLIST_PTN': {'default': lambda c: c['URL_BLACKLIST'] and re.compile(c['URL_BLACKLIST'] or '', re.IGNORECASE | re.UNICODE | re.MULTILINE)}, + + 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0]}, + 'VERSION': {'default': lambda c: json.loads((Path(c['PACKAGE_DIR']) / 'package.json').read_text().strip())['version']}, + 'GIT_SHA': {'default': lambda c: c['VERSION'].split('+')[-1] or 'unknown'}, + + 'PYTHON_BINARY': {'default': lambda c: sys.executable}, + 'PYTHON_ENCODING': {'default': lambda c: sys.stdout.encoding.upper()}, + 'PYTHON_VERSION': {'default': lambda c: '{}.{}.{}'.format(*sys.version_info[:3])}, + + 'DJANGO_BINARY': {'default': lambda c: django.__file__.replace('__init__.py', 'bin/django-admin.py')}, + 'DJANGO_VERSION': {'default': lambda c: '{}.{}.{} {} ({})'.format(*django.VERSION)}, + + 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['SAVE_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, + 'CURL_VERSION': {'default': lambda c: bin_version(c['CURL_BINARY']) if c['USE_CURL'] else None}, + 'CURL_USER_AGENT': {'default': lambda c: c['CURL_USER_AGENT'].format(**c)}, + 'CURL_ARGS': {'default': lambda c: c['CURL_ARGS'] or []}, + 'SAVE_FAVICON': {'default': lambda c: c['USE_CURL'] and c['SAVE_FAVICON']}, + 'SAVE_ARCHIVE_DOT_ORG': {'default': lambda c: c['USE_CURL'] and c['SAVE_ARCHIVE_DOT_ORG']}, + + 'USE_WGET': {'default': lambda c: c['USE_WGET'] and (c['SAVE_WGET'] or c['SAVE_WARC'])}, + 'WGET_VERSION': {'default': lambda c: bin_version(c['WGET_BINARY']) if c['USE_WGET'] else None}, + 'WGET_AUTO_COMPRESSION': {'default': lambda c: wget_supports_compression(c) if c['USE_WGET'] else False}, + 'WGET_USER_AGENT': {'default': lambda c: c['WGET_USER_AGENT'].format(**c)}, + 'SAVE_WGET': {'default': lambda c: c['USE_WGET'] and c['SAVE_WGET']}, + 'SAVE_WARC': {'default': lambda c: c['USE_WGET'] and c['SAVE_WARC']}, + 'WGET_ARGS': {'default': lambda c: c['WGET_ARGS'] or []}, + + 'RIPGREP_VERSION': {'default': lambda c: bin_version(c['RIPGREP_BINARY']) if c['USE_RIPGREP'] else None}, + + 'USE_SINGLEFILE': {'default': lambda c: c['USE_SINGLEFILE'] and c['SAVE_SINGLEFILE']}, + 'SINGLEFILE_VERSION': {'default': lambda c: bin_version(c['SINGLEFILE_BINARY']) if c['USE_SINGLEFILE'] else None}, + + 'USE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['SAVE_READABILITY']}, + 'READABILITY_VERSION': {'default': lambda c: bin_version(c['READABILITY_BINARY']) if c['USE_READABILITY'] else None}, + + 'USE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['SAVE_MERCURY']}, + 'MERCURY_VERSION': {'default': lambda c: '1.0.0' if shutil.which(str(bin_path(c['MERCURY_BINARY']))) else None}, # mercury is unversioned + + 'USE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + 'GIT_VERSION': {'default': lambda c: bin_version(c['GIT_BINARY']) if c['USE_GIT'] else None}, + 'SAVE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + + 'USE_YOUTUBEDL': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_VERSION': {'default': lambda c: bin_version(c['YOUTUBEDL_BINARY']) if c['USE_YOUTUBEDL'] else None}, + 'SAVE_MEDIA': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_ARGS': {'default': lambda c: c['YOUTUBEDL_ARGS'] or []}, + + 'USE_CHROME': {'default': lambda c: c['USE_CHROME'] and (c['SAVE_PDF'] or c['SAVE_SCREENSHOT'] or c['SAVE_DOM'] or c['SAVE_SINGLEFILE'])}, + 'CHROME_BINARY': {'default': lambda c: c['CHROME_BINARY'] if c['CHROME_BINARY'] else find_chrome_binary()}, + 'CHROME_VERSION': {'default': lambda c: bin_version(c['CHROME_BINARY']) if c['USE_CHROME'] else None}, + + 'SAVE_PDF': {'default': lambda c: c['USE_CHROME'] and c['SAVE_PDF']}, + 'SAVE_SCREENSHOT': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SCREENSHOT']}, + 'SAVE_DOM': {'default': lambda c: c['USE_CHROME'] and c['SAVE_DOM']}, + 'SAVE_SINGLEFILE': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SINGLEFILE'] and c['USE_NODE']}, + 'SAVE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['USE_NODE']}, + 'SAVE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['USE_NODE']}, + + 'USE_NODE': {'default': lambda c: c['USE_NODE'] and (c['SAVE_READABILITY'] or c['SAVE_SINGLEFILE'] or c['SAVE_MERCURY'])}, + 'NODE_VERSION': {'default': lambda c: bin_version(c['NODE_BINARY']) if c['USE_NODE'] else None}, + + 'DEPENDENCIES': {'default': lambda c: get_dependency_info(c)}, + 'CODE_LOCATIONS': {'default': lambda c: get_code_locations(c)}, + 'EXTERNAL_LOCATIONS': {'default': lambda c: get_external_locations(c)}, + 'DATA_LOCATIONS': {'default': lambda c: get_data_locations(c)}, + 'CHROME_OPTIONS': {'default': lambda c: get_chrome_info(c)}, +} + + + +################################### Helpers #################################### + + +def load_config_val(key: str, + default: ConfigDefaultValue=None, + type: Optional[Type]=None, + aliases: Optional[Tuple[str, ...]]=None, + config: Optional[ConfigDict]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigValue: + """parse bool, int, and str key=value pairs from env""" + + + config_keys_to_check = (key, *(aliases or ())) + for key in config_keys_to_check: + if env_vars: + val = env_vars.get(key) + if val: + break + if config_file_vars: + val = config_file_vars.get(key) + if val: + break + + if type is None or val is None: + if callable(default): + assert isinstance(config, dict) + return default(config) + + return default + + elif type is bool: + if val.lower() in ('true', 'yes', '1'): + return True + elif val.lower() in ('false', 'no', '0'): + return False + else: + raise ValueError(f'Invalid configuration option {key}={val} (expected a boolean: True/False)') + + elif type is str: + if val.lower() in ('true', 'false', 'yes', 'no', '1', '0'): + raise ValueError(f'Invalid configuration option {key}={val} (expected a string)') + return val.strip() + + elif type is int: + if not val.isdigit(): + raise ValueError(f'Invalid configuration option {key}={val} (expected an integer)') + return int(val) + + elif type is list or type is dict: + return json.loads(val) + + raise Exception('Config values can only be str, bool, int or json') + + +def load_config_file(out_dir: str=None) -> Optional[Dict[str, str]]: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + if config_path.exists(): + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + # flatten into one namespace + config_file_vars = { + key.upper(): val + for section, options in config_file.items() + for key, val in options.items() + } + # print('[i] Loaded config file', os.path.abspath(config_path)) + # print(config_file_vars) + return config_file_vars + return None + + +def write_config_file(config: Dict[str, str], out_dir: str=None) -> ConfigDict: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + from .system import atomic_write + + CONFIG_HEADER = ( + """# This is the config file for your ArchiveBox collection. + # + # You can add options here manually in INI format, or automatically by running: + # archivebox config --set KEY=VALUE + # + # If you modify this file manually, make sure to update your archive after by running: + # archivebox init + # + # A list of all possible config with documentation and examples can be found here: + # https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + + """) + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + + if not config_path.exists(): + atomic_write(config_path, CONFIG_HEADER) + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + + with open(config_path, 'r') as old: + atomic_write(f'{config_path}.bak', old.read()) + + find_section = lambda key: [name for name, opts in CONFIG_SCHEMA.items() if key in opts][0] + + # Set up sections in empty config file + for key, val in config.items(): + section = find_section(key) + if section in config_file: + existing_config = dict(config_file[section]) + else: + existing_config = {} + config_file[section] = {**existing_config, key: val} + + # always make sure there's a SECRET_KEY defined for Django + existing_secret_key = None + if 'SERVER_CONFIG' in config_file and 'SECRET_KEY' in config_file['SERVER_CONFIG']: + existing_secret_key = config_file['SERVER_CONFIG']['SECRET_KEY'] + + if (not existing_secret_key) or ('not a valid secret' in existing_secret_key): + from django.utils.crypto import get_random_string + chars = 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.' + random_secret_key = get_random_string(50, chars) + if 'SERVER_CONFIG' in config_file: + config_file['SERVER_CONFIG']['SECRET_KEY'] = random_secret_key + else: + config_file['SERVER_CONFIG'] = {'SECRET_KEY': random_secret_key} + + with open(config_path, 'w+') as new: + config_file.write(new) + + try: + # validate the config by attempting to re-parse it + CONFIG = load_all_config() + return { + key.upper(): CONFIG.get(key.upper()) + for key in config.keys() + } + except: + # something went horribly wrong, rever to the previous version + with open(f'{config_path}.bak', 'r') as old: + atomic_write(config_path, old.read()) + + if Path(f'{config_path}.bak').exists(): + os.remove(f'{config_path}.bak') + + return {} + + + +def load_config(defaults: ConfigDefaultDict, + config: Optional[ConfigDict]=None, + out_dir: Optional[str]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigDict: + + env_vars = env_vars or os.environ + config_file_vars = config_file_vars or load_config_file(out_dir=out_dir) + + extended_config: ConfigDict = config.copy() if config else {} + for key, default in defaults.items(): + try: + extended_config[key] = load_config_val( + key, + default=default['default'], + type=default.get('type'), + aliases=default.get('aliases'), + config=extended_config, + env_vars=env_vars, + config_file_vars=config_file_vars, + ) + except KeyboardInterrupt: + raise SystemExit(0) + except Exception as e: + stderr() + stderr(f'[X] Error while loading configuration value: {key}', color='red', config=extended_config) + stderr(' {}: {}'.format(e.__class__.__name__, e)) + stderr() + stderr(' Check your config for mistakes and try again (your archive data is unaffected).') + stderr() + stderr(' For config documentation and examples see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration') + stderr() + raise + raise SystemExit(2) + + return extended_config + +# def write_config(config: ConfigDict): + +# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: + + +# Logging Helpers +def stdout(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stdout.write(prefix + ''.join(strs)) + +def stderr(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stderr.write(prefix + ''.join(strs)) + +def hint(text: Union[Tuple[str, ...], List[str], str], prefix=' ', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if isinstance(text, str): + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text, **ansi)) + else: + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text[0], **ansi)) + for line in text[1:]: + stderr('{} {}'.format(prefix, line)) + + +# Dependency Metadata Helpers +def bin_version(binary: Optional[str]) -> Optional[str]: + """check the presence and return valid version line of a specified binary""" + + abspath = bin_path(binary) + if not binary or not abspath: + return None + + try: + version_str = run([abspath, "--version"], stdout=PIPE).stdout.strip().decode() + # take first 3 columns of first line of version info + return ' '.join(version_str.split('\n')[0].strip().split()[:3]) + except OSError: + pass + # stderr(f'[X] Unable to find working version of dependency: {binary}', color='red') + # stderr(' Make sure it\'s installed, then confirm it\'s working by running:') + # stderr(f' {binary} --version') + # stderr() + # stderr(' If you don\'t want to install it, you can disable it via config. See here for more info:') + # stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Install') + return None + +def bin_path(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + + node_modules_bin = Path('.') / 'node_modules' / '.bin' / binary + if node_modules_bin.exists(): + return str(node_modules_bin.resolve()) + + return shutil.which(str(Path(binary).expanduser())) or shutil.which(str(binary)) or binary + +def bin_hash(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + abs_path = bin_path(binary) + if abs_path is None or not Path(abs_path).exists(): + return None + + file_hash = md5() + with io.open(abs_path, mode='rb') as f: + for chunk in iter(lambda: f.read(io.DEFAULT_BUFFER_SIZE), b''): + file_hash.update(chunk) + + return f'md5:{file_hash.hexdigest()}' + +def find_chrome_binary() -> Optional[str]: + """find any installed chrome binaries in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_executable_paths = ( + 'chromium-browser', + 'chromium', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + 'chrome', + 'google-chrome', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'google-chrome-stable', + 'google-chrome-beta', + 'google-chrome-canary', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'google-chrome-unstable', + 'google-chrome-dev', + ) + for name in default_executable_paths: + full_path_exists = shutil.which(name) + if full_path_exists: + return name + + return None + +def find_chrome_data_dir() -> Optional[str]: + """find any installed chrome user data directories in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_profile_paths = ( + '~/.config/chromium', + '~/Library/Application Support/Chromium', + '~/AppData/Local/Chromium/User Data', + '~/.config/chrome', + '~/.config/google-chrome', + '~/Library/Application Support/Google/Chrome', + '~/AppData/Local/Google/Chrome/User Data', + '~/.config/google-chrome-stable', + '~/.config/google-chrome-beta', + '~/Library/Application Support/Google/Chrome Canary', + '~/AppData/Local/Google/Chrome SxS/User Data', + '~/.config/google-chrome-unstable', + '~/.config/google-chrome-dev', + ) + for path in default_profile_paths: + full_path = Path(path).resolve() + if full_path.exists(): + return full_path + return None + +def wget_supports_compression(config): + try: + cmd = [ + config['WGET_BINARY'], + "--compression=auto", + "--help", + ] + return not run(cmd, stdout=DEVNULL, stderr=DEVNULL).returncode + except (FileNotFoundError, OSError): + return False + +def get_code_locations(config: ConfigDict) -> SimpleConfigValueDict: + return { + 'PACKAGE_DIR': { + 'path': (config['PACKAGE_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['PACKAGE_DIR'] / '__main__.py').exists(), + }, + 'TEMPLATES_DIR': { + 'path': (config['TEMPLATES_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['TEMPLATES_DIR'] / config['ACTIVE_THEME'] / 'static').exists(), + }, + # 'NODE_MODULES_DIR': { + # 'path': , + # 'enabled': , + # 'is_valid': (...).exists(), + # }, + } + +def get_external_locations(config: ConfigDict) -> ConfigValue: + abspath = lambda path: None if path is None else Path(path).resolve() + return { + 'CHROME_USER_DATA_DIR': { + 'path': abspath(config['CHROME_USER_DATA_DIR']), + 'enabled': config['USE_CHROME'] and config['CHROME_USER_DATA_DIR'], + 'is_valid': False if config['CHROME_USER_DATA_DIR'] is None else (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(), + }, + 'COOKIES_FILE': { + 'path': abspath(config['COOKIES_FILE']), + 'enabled': config['USE_WGET'] and config['COOKIES_FILE'], + 'is_valid': False if config['COOKIES_FILE'] is None else Path(config['COOKIES_FILE']).exists(), + }, + } + +def get_data_locations(config: ConfigDict) -> ConfigValue: + return { + 'OUTPUT_DIR': { + 'path': config['OUTPUT_DIR'].resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + 'SOURCES_DIR': { + 'path': config['SOURCES_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['SOURCES_DIR'].exists(), + }, + 'LOGS_DIR': { + 'path': config['LOGS_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['LOGS_DIR'].exists(), + }, + 'ARCHIVE_DIR': { + 'path': config['ARCHIVE_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['ARCHIVE_DIR'].exists(), + }, + 'CONFIG_FILE': { + 'path': config['CONFIG_FILE'].resolve(), + 'enabled': True, + 'is_valid': config['CONFIG_FILE'].exists(), + }, + 'SQL_INDEX': { + 'path': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + } + +def get_dependency_info(config: ConfigDict) -> ConfigValue: + return { + 'ARCHIVEBOX_BINARY': { + 'path': bin_path(config['ARCHIVEBOX_BINARY']), + 'version': config['VERSION'], + 'hash': bin_hash(config['ARCHIVEBOX_BINARY']), + 'enabled': True, + 'is_valid': True, + }, + 'PYTHON_BINARY': { + 'path': bin_path(config['PYTHON_BINARY']), + 'version': config['PYTHON_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'DJANGO_BINARY': { + 'path': bin_path(config['DJANGO_BINARY']), + 'version': config['DJANGO_VERSION'], + 'hash': bin_hash(config['DJANGO_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'CURL_BINARY': { + 'path': bin_path(config['CURL_BINARY']), + 'version': config['CURL_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': config['USE_CURL'], + 'is_valid': bool(config['CURL_VERSION']), + }, + 'WGET_BINARY': { + 'path': bin_path(config['WGET_BINARY']), + 'version': config['WGET_VERSION'], + 'hash': bin_hash(config['WGET_BINARY']), + 'enabled': config['USE_WGET'], + 'is_valid': bool(config['WGET_VERSION']), + }, + 'NODE_BINARY': { + 'path': bin_path(config['NODE_BINARY']), + 'version': config['NODE_VERSION'], + 'hash': bin_hash(config['NODE_BINARY']), + 'enabled': config['USE_NODE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'SINGLEFILE_BINARY': { + 'path': bin_path(config['SINGLEFILE_BINARY']), + 'version': config['SINGLEFILE_VERSION'], + 'hash': bin_hash(config['SINGLEFILE_BINARY']), + 'enabled': config['USE_SINGLEFILE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'READABILITY_BINARY': { + 'path': bin_path(config['READABILITY_BINARY']), + 'version': config['READABILITY_VERSION'], + 'hash': bin_hash(config['READABILITY_BINARY']), + 'enabled': config['USE_READABILITY'], + 'is_valid': bool(config['READABILITY_VERSION']), + }, + 'MERCURY_BINARY': { + 'path': bin_path(config['MERCURY_BINARY']), + 'version': config['MERCURY_VERSION'], + 'hash': bin_hash(config['MERCURY_BINARY']), + 'enabled': config['USE_MERCURY'], + 'is_valid': bool(config['MERCURY_VERSION']), + }, + 'GIT_BINARY': { + 'path': bin_path(config['GIT_BINARY']), + 'version': config['GIT_VERSION'], + 'hash': bin_hash(config['GIT_BINARY']), + 'enabled': config['USE_GIT'], + 'is_valid': bool(config['GIT_VERSION']), + }, + 'YOUTUBEDL_BINARY': { + 'path': bin_path(config['YOUTUBEDL_BINARY']), + 'version': config['YOUTUBEDL_VERSION'], + 'hash': bin_hash(config['YOUTUBEDL_BINARY']), + 'enabled': config['USE_YOUTUBEDL'], + 'is_valid': bool(config['YOUTUBEDL_VERSION']), + }, + 'CHROME_BINARY': { + 'path': bin_path(config['CHROME_BINARY']), + 'version': config['CHROME_VERSION'], + 'hash': bin_hash(config['CHROME_BINARY']), + 'enabled': config['USE_CHROME'], + 'is_valid': bool(config['CHROME_VERSION']), + }, + 'RIPGREP_BINARY': { + 'path': bin_path(config['RIPGREP_BINARY']), + 'version': config['RIPGREP_VERSION'], + 'hash': bin_hash(config['RIPGREP_BINARY']), + 'enabled': config['USE_RIPGREP'], + 'is_valid': bool(config['RIPGREP_VERSION']), + }, + # TODO: add an entry for the sonic search backend? + # 'SONIC_BINARY': { + # 'path': bin_path(config['SONIC_BINARY']), + # 'version': config['SONIC_VERSION'], + # 'hash': bin_hash(config['SONIC_BINARY']), + # 'enabled': config['USE_SONIC'], + # 'is_valid': bool(config['SONIC_VERSION']), + # }, + } + +def get_chrome_info(config: ConfigDict) -> ConfigValue: + return { + 'TIMEOUT': config['TIMEOUT'], + 'RESOLUTION': config['RESOLUTION'], + 'CHECK_SSL_VALIDITY': config['CHECK_SSL_VALIDITY'], + 'CHROME_BINARY': config['CHROME_BINARY'], + 'CHROME_HEADLESS': config['CHROME_HEADLESS'], + 'CHROME_SANDBOX': config['CHROME_SANDBOX'], + 'CHROME_USER_AGENT': config['CHROME_USER_AGENT'], + 'CHROME_USER_DATA_DIR': config['CHROME_USER_DATA_DIR'], + } + + +# ****************************************************************************** +# ****************************************************************************** +# ******************************** Load Config ********************************* +# ******* (compile the defaults, configs, and metadata all into CONFIG) ******** +# ****************************************************************************** +# ****************************************************************************** + + +def load_all_config(): + CONFIG: ConfigDict = {} + for section_name, section_config in CONFIG_SCHEMA.items(): + CONFIG = load_config(section_config, CONFIG) + + return load_config(DYNAMIC_CONFIG_SCHEMA, CONFIG) + +# add all final config values in CONFIG to globals in this file +CONFIG = load_all_config() +globals().update(CONFIG) +# this lets us do: from .config import DEBUG, MEDIA_TIMEOUT, ... + + +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** + + + +########################### System Environment Setup ########################### + + +# Set timezone to UTC and umask to OUTPUT_PERMISSIONS +os.environ["TZ"] = 'UTC' +os.umask(0o777 - int(OUTPUT_PERMISSIONS, base=8)) # noqa: F821 + +# add ./node_modules/.bin to $PATH so we can use node scripts in extractors +NODE_BIN_PATH = str((Path(CONFIG["OUTPUT_DIR"]).absolute() / 'node_modules' / '.bin')) +sys.path.append(NODE_BIN_PATH) + + + + +########################### Config Validity Checkers ########################### + + +def check_system_config(config: ConfigDict=CONFIG) -> None: + ### Check system environment + if config['USER'] == 'root': + stderr('[!] ArchiveBox should never be run as root!', color='red') + stderr(' For more information, see the security overview documentation:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#do-not-run-as-root') + raise SystemExit(2) + + ### Check Python environment + if sys.version_info[:3] < (3, 6, 0): + stderr(f'[X] Python version is not new enough: {config["PYTHON_VERSION"]} (>3.6 is required)', color='red') + stderr(' See https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') + raise SystemExit(2) + + if config['PYTHON_ENCODING'] not in ('UTF-8', 'UTF8'): + stderr(f'[X] Your system is running python3 scripts with a bad locale setting: {config["PYTHON_ENCODING"]} (it should be UTF-8).', color='red') + stderr(' To fix it, add the line "export PYTHONIOENCODING=UTF-8" to your ~/.bashrc file (without quotes)') + stderr(' Or if you\'re using ubuntu/debian, run "dpkg-reconfigure locales"') + stderr('') + stderr(' Confirm that it\'s fixed by opening a new shell and running:') + stderr(' python3 -c "import sys; print(sys.stdout.encoding)" # should output UTF-8') + raise SystemExit(2) + + # stderr('[i] Using Chrome binary: {}'.format(shutil.which(CHROME_BINARY) or CHROME_BINARY)) + # stderr('[i] Using Chrome data dir: {}'.format(os.path.abspath(CHROME_USER_DATA_DIR))) + if config['CHROME_USER_DATA_DIR'] is not None: + if not (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(): + stderr('[X] Could not find profile "Default" in CHROME_USER_DATA_DIR.', color='red') + stderr(f' {config["CHROME_USER_DATA_DIR"]}') + stderr(' Make sure you set it to a Chrome user data directory containing a Default profile folder.') + stderr(' For more info see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#CHROME_USER_DATA_DIR') + if '/Default' in str(config['CHROME_USER_DATA_DIR']): + stderr() + stderr(' Try removing /Default from the end e.g.:') + stderr(' CHROME_USER_DATA_DIR="{}"'.format(config['CHROME_USER_DATA_DIR'].split('/Default')[0])) + raise SystemExit(2) + + +def check_dependencies(config: ConfigDict=CONFIG, show_help: bool=True) -> None: + invalid_dependencies = [ + (name, info) for name, info in config['DEPENDENCIES'].items() + if info['enabled'] and not info['is_valid'] + ] + if invalid_dependencies and show_help: + stderr(f'[!] Warning: Missing {len(invalid_dependencies)} recommended dependencies', color='lightyellow') + for dependency, info in invalid_dependencies: + stderr( + ' ! {}: {} ({})'.format( + dependency, + info['path'] or 'unable to find binary', + info['version'] or 'unable to detect version', + ) + ) + if dependency in ('SINGLEFILE_BINARY', 'READABILITY_BINARY', 'MERCURY_BINARY'): + hint(('npm install --prefix . "git+https://github.com/ArchiveBox/ArchiveBox.git"', + f'or archivebox config --set SAVE_{dependency.rsplit("_", 1)[0]}=False to silence this warning', + ''), prefix=' ') + stderr('') + + if config['TIMEOUT'] < 5: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' You must allow *at least* 5 seconds for indexing and archive methods to run succesfully.') + stderr(' (Setting it to somewhere between 30 and 3000 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + elif config['USE_CHROME'] and config['TIMEOUT'] < 15: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' Chrome will fail to archive all sites if set to less than ~15 seconds.') + stderr(' (Setting it to somewhere between 30 and 300 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + if config['USE_YOUTUBEDL'] and config['MEDIA_TIMEOUT'] < 20: + stderr(f'[!] Warning: MEDIA_TIMEOUT is set too low! (currently set to MEDIA_TIMEOUT={config["MEDIA_TIMEOUT"]} seconds)', color='red') + stderr(' Youtube-dl will fail to archive all media if set to less than ~20 seconds.') + stderr(' (Setting it somewhere over 60 seconds is recommended)') + stderr() + stderr(' If you want to disable media archiving entirely, set SAVE_MEDIA=False instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#save_media') + stderr() + +def check_data_folder(out_dir: Union[str, Path, None]=None, config: ConfigDict=CONFIG) -> None: + output_dir = out_dir or config['OUTPUT_DIR'] + assert isinstance(output_dir, (str, Path)) + + sql_index_exists = (Path(output_dir) / SQL_INDEX_FILENAME).exists() + if not sql_index_exists: + stderr('[X] No archivebox index found in the current directory.', color='red') + stderr(f' {output_dir}', color='lightyellow') + stderr() + stderr(' {lightred}Hint{reset}: Are you running archivebox in the right folder?'.format(**config['ANSI'])) + stderr(' cd path/to/your/archive/folder') + stderr(' archivebox [command]') + stderr() + stderr(' {lightred}Hint{reset}: To create a new archive collection or import existing data in this folder, run:'.format(**config['ANSI'])) + stderr(' archivebox init') + raise SystemExit(2) + + from .index.sql import list_migrations + + pending_migrations = [name for status, name in list_migrations() if not status] + + if (not sql_index_exists) or pending_migrations: + if sql_index_exists: + pending_operation = f'apply the {len(pending_migrations)} pending migrations' + else: + pending_operation = 'generate the new SQL main index' + + stderr('[X] This collection was created with an older version of ArchiveBox and must be upgraded first.', color='lightyellow') + stderr(f' {output_dir}') + stderr() + stderr(f' To upgrade it to the latest version and {pending_operation} run:') + stderr(' archivebox init') + raise SystemExit(3) + + sources_dir = Path(output_dir) / SOURCES_DIR_NAME + if not sources_dir.exists(): + sources_dir.mkdir() + + + +def setup_django(out_dir: Path=None, check_db=False, config: ConfigDict=CONFIG, in_memory_db=False) -> None: + check_system_config() + + output_dir = out_dir or Path(config['OUTPUT_DIR']) + + assert isinstance(output_dir, Path) and isinstance(config['PACKAGE_DIR'], Path) + + try: + import django + sys.path.append(str(config['PACKAGE_DIR'])) + os.environ.setdefault('OUTPUT_DIR', str(output_dir)) + assert (config['PACKAGE_DIR'] / 'core' / 'settings.py').exists(), 'settings.py was not found at archivebox/core/settings.py' + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + + if in_memory_db: + # Put the db in memory and run migrations in case any command requires it + from django.core.management import call_command + os.environ.setdefault("ARCHIVEBOX_DATABASE_NAME", ":memory:") + django.setup() + call_command("migrate", interactive=False, verbosity=0) + else: + django.setup() + + if check_db: + sql_index_path = Path(output_dir) / SQL_INDEX_FILENAME + assert sql_index_path.exists(), ( + f'No database file {SQL_INDEX_FILENAME} found in OUTPUT_DIR: {config["OUTPUT_DIR"]}') + except KeyboardInterrupt: + raise SystemExit(2) diff --git a/archivebox-0.5.3/build/lib/archivebox/config_stubs.py b/archivebox-0.5.3/build/lib/archivebox/config_stubs.py new file mode 100644 index 0000000..988f58a --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/config_stubs.py @@ -0,0 +1,113 @@ +from pathlib import Path +from typing import Optional, Dict, Union, Tuple, Callable, Pattern, Type, Any, List +from mypy_extensions import TypedDict + + + +SimpleConfigValue = Union[str, bool, int, None, Pattern, Dict[str, Any]] +SimpleConfigValueDict = Dict[str, SimpleConfigValue] +SimpleConfigValueGetter = Callable[[], SimpleConfigValue] +ConfigValue = Union[SimpleConfigValue, SimpleConfigValueDict, SimpleConfigValueGetter] + + +class BaseConfig(TypedDict): + pass + +class ConfigDict(BaseConfig, total=False): + """ + # Regenerate by pasting this quine into `archivebox shell` 🥚 + from archivebox.config import ConfigDict, CONFIG_DEFAULTS + print('class ConfigDict(BaseConfig, total=False):') + print(' ' + '"'*3 + ConfigDict.__doc__ + '"'*3) + for section, configs in CONFIG_DEFAULTS.items(): + for key, attrs in configs.items(): + Type, default = attrs['type'], attrs['default'] + if default is None: + print(f' {key}: Optional[{Type.__name__}]') + else: + print(f' {key}: {Type.__name__}') + print() + """ + IS_TTY: bool + USE_COLOR: bool + SHOW_PROGRESS: bool + IN_DOCKER: bool + + PACKAGE_DIR: Path + OUTPUT_DIR: Path + CONFIG_FILE: Path + ONLY_NEW: bool + TIMEOUT: int + MEDIA_TIMEOUT: int + OUTPUT_PERMISSIONS: str + RESTRICT_FILE_NAMES: str + URL_BLACKLIST: str + + SECRET_KEY: Optional[str] + BIND_ADDR: str + ALLOWED_HOSTS: str + DEBUG: bool + PUBLIC_INDEX: bool + PUBLIC_SNAPSHOTS: bool + FOOTER_INFO: str + ACTIVE_THEME: str + + SAVE_TITLE: bool + SAVE_FAVICON: bool + SAVE_WGET: bool + SAVE_WGET_REQUISITES: bool + SAVE_SINGLEFILE: bool + SAVE_READABILITY: bool + SAVE_MERCURY: bool + SAVE_PDF: bool + SAVE_SCREENSHOT: bool + SAVE_DOM: bool + SAVE_WARC: bool + SAVE_GIT: bool + SAVE_MEDIA: bool + SAVE_ARCHIVE_DOT_ORG: bool + + RESOLUTION: str + GIT_DOMAINS: str + CHECK_SSL_VALIDITY: bool + CURL_USER_AGENT: str + WGET_USER_AGENT: str + CHROME_USER_AGENT: str + COOKIES_FILE: Union[str, Path, None] + CHROME_USER_DATA_DIR: Union[str, Path, None] + CHROME_HEADLESS: bool + CHROME_SANDBOX: bool + + USE_CURL: bool + USE_WGET: bool + USE_SINGLEFILE: bool + USE_READABILITY: bool + USE_MERCURY: bool + USE_GIT: bool + USE_CHROME: bool + USE_YOUTUBEDL: bool + CURL_BINARY: str + GIT_BINARY: str + WGET_BINARY: str + SINGLEFILE_BINARY: str + READABILITY_BINARY: str + MERCURY_BINARY: str + YOUTUBEDL_BINARY: str + CHROME_BINARY: Optional[str] + + YOUTUBEDL_ARGS: List[str] + WGET_ARGS: List[str] + CURL_ARGS: List[str] + GIT_ARGS: List[str] + + +ConfigDefaultValueGetter = Callable[[ConfigDict], ConfigValue] +ConfigDefaultValue = Union[ConfigValue, ConfigDefaultValueGetter] + +ConfigDefault = TypedDict('ConfigDefault', { + 'default': ConfigDefaultValue, + 'type': Optional[Type], + 'aliases': Optional[Tuple[str, ...]], +}, total=False) + +ConfigDefaultDict = Dict[str, ConfigDefault] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/__init__.py b/archivebox-0.5.3/build/lib/archivebox/core/__init__.py new file mode 100644 index 0000000..3e1d607 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox.core' diff --git a/archivebox-0.5.3/build/lib/archivebox/core/admin.py b/archivebox-0.5.3/build/lib/archivebox/core/admin.py new file mode 100644 index 0000000..832bea3 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/admin.py @@ -0,0 +1,257 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.contrib import admin +from django.urls import path +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.shortcuts import render, redirect +from django.contrib.auth import get_user_model +from django import forms + +from core.models import Snapshot, Tag +from core.forms import AddLinkForm, TagField + +from core.mixins import SearchResultsAdminMixin + +from index.html import snapshot_icons +from util import htmldecode, urldecode, ansi_to_html +from logging_util import printable_filesize +from main import add, remove +from config import OUTPUT_DIR +from extractors import archive_links + +# TODO: https://stackoverflow.com/questions/40760880/add-custom-button-to-django-admin-panel + +def update_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], out_dir=OUTPUT_DIR) +update_snapshots.short_description = "Archive" + +def update_titles(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, methods=('title','favicon'), out_dir=OUTPUT_DIR) +update_titles.short_description = "Pull title" + +def overwrite_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, out_dir=OUTPUT_DIR) +overwrite_snapshots.short_description = "Re-archive (overwrite)" + +def verify_snapshots(modeladmin, request, queryset): + for snapshot in queryset: + print(snapshot.timestamp, snapshot.url, snapshot.is_archived, snapshot.archive_size, len(snapshot.history)) + +verify_snapshots.short_description = "Check" + +def delete_snapshots(modeladmin, request, queryset): + remove(snapshots=queryset, yes=True, delete=True, out_dir=OUTPUT_DIR) + +delete_snapshots.short_description = "Delete" + + +class SnapshotAdminForm(forms.ModelForm): + tags = TagField(required=False) + + class Meta: + model = Snapshot + fields = "__all__" + + def save(self, commit=True): + # Based on: https://stackoverflow.com/a/49933068/3509554 + + # Get the unsave instance + instance = forms.ModelForm.save(self, False) + tags = self.cleaned_data.pop("tags") + + #update save_m2m + def new_save_m2m(): + instance.save_tags(tags) + + # Do we need to save all changes now? + self.save_m2m = new_save_m2m + if commit: + instance.save() + + return instance + + +class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): + list_display = ('added', 'title_str', 'url_str', 'files', 'size') + sort_fields = ('title_str', 'url_str', 'added') + readonly_fields = ('id', 'url', 'timestamp', 'num_outputs', 'is_archived', 'url_hash', 'added', 'updated') + search_fields = ['url', 'timestamp', 'title', 'tags__name'] + fields = (*readonly_fields, 'title', 'tags') + list_filter = ('added', 'updated', 'tags') + ordering = ['-added'] + actions = [delete_snapshots, overwrite_snapshots, update_snapshots, update_titles, verify_snapshots] + actions_template = 'admin/actions_as_select.html' + form = SnapshotAdminForm + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('grid/', self.admin_site.admin_view(self.grid_view),name='grid') + ] + return custom_urls + urls + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return ', '.join(obj.tags.values_list('name', flat=True)) + + def id_str(self, obj): + return format_html( + '{}', + obj.url_hash[:8], + ) + + def title_str(self, obj): + canon = obj.as_link().canonical_outputs() + tags = ''.join( + format_html('{} ', tag.id, tag) + for tag in obj.tags.all() + if str(tag).strip() + ) + return format_html( + '' + '' + '' + '' + '{}' + '', + obj.archive_path, + obj.archive_path, canon['favicon_path'], + obj.archive_path, + 'fetched' if obj.latest_title or obj.title else 'pending', + urldecode(htmldecode(obj.latest_title or obj.title or ''))[:128] or 'Pending...' + ) + mark_safe(f' {tags}') + + def files(self, obj): + return snapshot_icons(obj) + + def size(self, obj): + archive_size = obj.archive_size + if archive_size: + size_txt = printable_filesize(archive_size) + if archive_size > 52428800: + size_txt = mark_safe(f'{size_txt}') + else: + size_txt = mark_safe('...') + return format_html( + '{}', + obj.archive_path, + size_txt, + ) + + def url_str(self, obj): + return format_html( + '{}', + obj.url, + obj.url.split('://www.', 1)[-1].split('://', 1)[-1][:64], + ) + + def grid_view(self, request): + + # cl = self.get_changelist_instance(request) + + # Save before monkey patching to restore for changelist list view + saved_change_list_template = self.change_list_template + saved_list_per_page = self.list_per_page + saved_list_max_show_all = self.list_max_show_all + + # Monkey patch here plus core_tags.py + self.change_list_template = 'admin/grid_change_list.html' + self.list_per_page = 20 + self.list_max_show_all = self.list_per_page + + # Call monkey patched view + rendered_response = self.changelist_view(request) + + # Restore values + self.change_list_template = saved_change_list_template + self.list_per_page = saved_list_per_page + self.list_max_show_all = saved_list_max_show_all + + return rendered_response + + + id_str.short_description = 'ID' + title_str.short_description = 'Title' + url_str.short_description = 'Original URL' + + id_str.admin_order_field = 'id' + title_str.admin_order_field = 'title' + url_str.admin_order_field = 'url' + +class TagAdmin(admin.ModelAdmin): + list_display = ('slug', 'name', 'id') + sort_fields = ('id', 'name', 'slug') + readonly_fields = ('id',) + search_fields = ('id', 'name', 'slug') + fields = (*readonly_fields, 'name', 'slug') + + +class ArchiveBoxAdmin(admin.AdminSite): + site_header = 'ArchiveBox' + index_title = 'Links' + site_title = 'Index' + + def get_urls(self): + return [ + path('core/snapshot/add/', self.add_view, name='Add'), + ] + super().get_urls() + + def add_view(self, request): + if not request.user.is_authenticated: + return redirect(f'/admin/login/?next={request.path}') + + request.current_app = self.name + context = { + **self.each_context(request), + 'title': 'Add URLs', + } + + if request.method == 'GET': + context['form'] = AddLinkForm() + + elif request.method == 'POST': + form = AddLinkForm(request.POST) + if form.is_valid(): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + else: + context["form"] = form + + return render(template_name='add_links.html', request=request, context=context) + +admin.site = ArchiveBoxAdmin() +admin.site.register(get_user_model()) +admin.site.register(Snapshot, SnapshotAdmin) +admin.site.register(Tag, TagAdmin) +admin.site.disable_action('delete_selected') diff --git a/archivebox-0.5.3/build/lib/archivebox/core/apps.py b/archivebox-0.5.3/build/lib/archivebox/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/archivebox-0.5.3/build/lib/archivebox/core/forms.py b/archivebox-0.5.3/build/lib/archivebox/core/forms.py new file mode 100644 index 0000000..86b29bb --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/forms.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.core' + +from django import forms + +from ..util import URL_REGEX +from ..vendor.taggit_utils import edit_string_for_tags, parse_tags + +CHOICES = ( + ('0', 'depth = 0 (archive just these URLs)'), + ('1', 'depth = 1 (archive these URLs and all URLs one hop away)'), +) + +from ..extractors import get_default_archive_methods + +ARCHIVE_METHODS = [ + (name, name) + for name, _, _ in get_default_archive_methods() +] + + +class AddLinkForm(forms.Form): + url = forms.RegexField(label="URLs (one per line)", regex=URL_REGEX, min_length='6', strip=True, widget=forms.Textarea, required=True) + depth = forms.ChoiceField(label="Archive depth", choices=CHOICES, widget=forms.RadioSelect, initial='0') + archive_methods = forms.MultipleChoiceField( + required=False, + widget=forms.SelectMultiple, + choices=ARCHIVE_METHODS, + ) +class TagWidgetMixin: + def format_value(self, value): + if value is not None and not isinstance(value, str): + value = edit_string_for_tags(value) + return super().format_value(value) + +class TagWidget(TagWidgetMixin, forms.TextInput): + pass + +class TagField(forms.CharField): + widget = TagWidget + + def clean(self, value): + value = super().clean(value) + try: + return parse_tags(value) + except ValueError: + raise forms.ValidationError( + "Please provide a comma-separated list of tags." + ) + + def has_changed(self, initial_value, data_value): + # Always return False if the field is disabled since self.bound_data + # always uses the initial value in this case. + if self.disabled: + return False + + try: + data_value = self.clean(data_value) + except forms.ValidationError: + pass + + if initial_value is None: + initial_value = [] + + initial_value = [tag.name for tag in initial_value] + initial_value.sort() + + return initial_value != data_value diff --git a/archivebox-0.5.3/build/lib/archivebox/core/management/commands/archivebox.py b/archivebox-0.5.3/build/lib/archivebox/core/management/commands/archivebox.py new file mode 100644 index 0000000..a68b5d9 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/management/commands/archivebox.py @@ -0,0 +1,18 @@ +__package__ = 'archivebox' + +from django.core.management.base import BaseCommand + + +from .cli import run_subcommand + + +class Command(BaseCommand): + help = 'Run an ArchiveBox CLI subcommand (e.g. add, remove, list, etc)' + + def add_arguments(self, parser): + parser.add_argument('subcommand', type=str, help='The subcommand you want to run') + parser.add_argument('command_args', nargs='*', help='Arguments to pass to the subcommand') + + + def handle(self, *args, **kwargs): + run_subcommand(kwargs['subcommand'], args=kwargs['command_args']) diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0001_initial.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0001_initial.py new file mode 100644 index 0000000..73ac78e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2019-05-01 03:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Snapshot', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('url', models.URLField(unique=True)), + ('timestamp', models.CharField(default=None, max_length=32, null=True, unique=True)), + ('title', models.CharField(default=None, max_length=128, null=True)), + ('tags', models.CharField(default=None, max_length=256, null=True)), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(default=None, null=True)), + ], + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0002_auto_20200625_1521.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0002_auto_20200625_1521.py new file mode 100644 index 0000000..4811282 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0002_auto_20200625_1521.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-06-25 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0003_auto_20200630_1034.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0003_auto_20200630_1034.py new file mode 100644 index 0000000..61fd472 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0003_auto_20200630_1034.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.7 on 2020-06-30 10:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20200625_1521'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='added', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(db_index=True, default=None, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(db_index=True, default=None, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(db_index=True, default=None, null=True), + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0004_auto_20200713_1552.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0004_auto_20200713_1552.py new file mode 100644 index 0000000..6983662 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0004_auto_20200713_1552.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-07-13 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_auto_20200630_1034'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, unique=True), + preserve_default=False, + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0005_auto_20200728_0326.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0005_auto_20200728_0326.py new file mode 100644 index 0000000..f367aeb --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0005_auto_20200728_0326.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-07-28 03:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20200713_1552'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(blank=True, db_index=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(blank=True, db_index=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0006_auto_20201012_1520.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0006_auto_20201012_1520.py new file mode 100644 index 0000000..694c990 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0006_auto_20201012_1520.py @@ -0,0 +1,70 @@ +# Generated by Django 3.0.8 on 2020-10-12 15:20 + +from django.db import migrations, models +from django.utils.text import slugify + +def forwards_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags + tag_set = ( + set(tag.strip() for tag in (snapshot.tags_old or '').split(',')) + ) + tag_set.discard("") + + for tag in tag_set: + to_add, _ = TagModel.objects.get_or_create(name=tag, slug=slugify(tag)) + snapshot.tags.add(to_add) + + +def reverse_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags.values_list("name", flat=True) + snapshot.tags_old = ",".join([tag for tag in tags]) + snapshot.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20200728_0326'), + ] + + operations = [ + migrations.RenameField( + model_name='snapshot', + old_name='tags', + new_name='tags_old', + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), + ('slug', models.SlugField(max_length=100, unique=True, verbose_name='slug')), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + }, + ), + migrations.AddField( + model_name='snapshot', + name='tags', + field=models.ManyToManyField(to='core.Tag'), + ), + migrations.RunPython(forwards_func, reverse_func), + migrations.RemoveField( + model_name='snapshot', + name='tags_old', + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0007_archiveresult.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0007_archiveresult.py new file mode 100644 index 0000000..a780376 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0007_archiveresult.py @@ -0,0 +1,97 @@ +# Generated by Django 3.0.8 on 2020-11-04 12:25 + +import json +from pathlib import Path + +from django.db import migrations, models +import django.db.models.deletion + +from config import CONFIG +from index.json import to_json + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +def forwards_func(apps, schema_editor): + from core.models import EXTRACTORS + + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + + snapshots = Snapshot.objects.all() + for snapshot in snapshots: + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + + try: + with open(out_dir / "index.json", "r") as f: + fs_index = json.load(f) + except Exception as e: + continue + + history = fs_index["history"] + + for extractor in history: + for result in history[extractor]: + ArchiveResult.objects.create(extractor=extractor, snapshot=snapshot, cmd=result["cmd"], cmd_version=result["cmd_version"], + start_ts=result["start_ts"], end_ts=result["end_ts"], status=result["status"], pwd=result["pwd"], output=result["output"]) + + +def verify_json_index_integrity(snapshot): + results = snapshot.archiveresult_set.all() + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + with open(out_dir / "index.json", "r") as f: + index = json.load(f) + + history = index["history"] + index_results = [result for extractor in history for result in history[extractor]] + flattened_results = [result["start_ts"] for result in index_results] + + missing_results = [result for result in results if result.start_ts.isoformat() not in flattened_results] + + for missing in missing_results: + index["history"][missing.extractor].append({"cmd": missing.cmd, "cmd_version": missing.cmd_version, "end_ts": missing.end_ts.isoformat(), + "start_ts": missing.start_ts.isoformat(), "pwd": missing.pwd, "output": missing.output, + "schema": "ArchiveResult", "status": missing.status}) + + json_index = to_json(index) + with open(out_dir / "index.json", "w") as f: + f.write(json_index) + + +def reverse_func(apps, schema_editor): + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + for snapshot in Snapshot.objects.all(): + verify_json_index_integrity(snapshot) + + ArchiveResult.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20201012_1520'), + ] + + operations = [ + migrations.CreateModel( + name='ArchiveResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cmd', JSONField()), + ('pwd', models.CharField(max_length=256)), + ('cmd_version', models.CharField(max_length=32)), + ('status', models.CharField(choices=[('succeeded', 'succeeded'), ('failed', 'failed'), ('skipped', 'skipped')], max_length=16)), + ('output', models.CharField(max_length=512)), + ('start_ts', models.DateTimeField()), + ('end_ts', models.DateTimeField()), + ('extractor', models.CharField(choices=[('title', 'title'), ('favicon', 'favicon'), ('wget', 'wget'), ('singlefile', 'singlefile'), ('pdf', 'pdf'), ('screenshot', 'screenshot'), ('dom', 'dom'), ('readability', 'readability'), ('mercury', 'mercury'), ('git', 'git'), ('media', 'media'), ('headers', 'headers'), ('archive_org', 'archive_org')], max_length=32)), + ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Snapshot')), + ], + ), + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/0008_auto_20210105_1421.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0008_auto_20210105_1421.py new file mode 100644 index 0000000..e5b3387 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/migrations/0008_auto_20210105_1421.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-01-05 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_archiveresult'), + ] + + operations = [ + migrations.AlterField( + model_name='archiveresult', + name='cmd_version', + field=models.CharField(blank=True, default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/migrations/__init__.py b/archivebox-0.5.3/build/lib/archivebox/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/build/lib/archivebox/core/mixins.py b/archivebox-0.5.3/build/lib/archivebox/core/mixins.py new file mode 100644 index 0000000..538ca1e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/mixins.py @@ -0,0 +1,23 @@ +from django.contrib import messages + +from archivebox.search import query_search_index + +class SearchResultsAdminMixin(object): + def get_search_results(self, request, queryset, search_term): + ''' Enhances the search queryset with results from the search backend. + ''' + qs, use_distinct = \ + super(SearchResultsAdminMixin, self).get_search_results( + request, queryset, search_term) + + search_term = search_term.strip() + if not search_term: + return qs, use_distinct + try: + qsearch = query_search_index(search_term) + except Exception as err: + messages.add_message(request, messages.WARNING, f'Error from the search backend, only showing results from default admin search fields - Error: {err}') + else: + qs = queryset & qsearch + finally: + return qs, use_distinct diff --git a/archivebox-0.5.3/build/lib/archivebox/core/models.py b/archivebox-0.5.3/build/lib/archivebox/core/models.py new file mode 100644 index 0000000..13d75b6 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/models.py @@ -0,0 +1,194 @@ +__package__ = 'archivebox.core' + +import uuid + +from django.db import models, transaction +from django.utils.functional import cached_property +from django.utils.text import slugify +from django.db.models import Case, When, Value, IntegerField + +from ..util import parse_date +from ..index.schema import Link +from ..extractors import get_default_archive_methods, ARCHIVE_METHODS_INDEXING_PRECEDENCE + +EXTRACTORS = [(extractor[0], extractor[0]) for extractor in get_default_archive_methods()] +STATUS_CHOICES = [ + ("succeeded", "succeeded"), + ("failed", "failed"), + ("skipped", "skipped") +] + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +class Tag(models.Model): + """ + Based on django-taggit model + """ + name = models.CharField(verbose_name="name", unique=True, blank=False, max_length=100) + slug = models.SlugField(verbose_name="slug", unique=True, max_length=100) + + class Meta: + verbose_name = "Tag" + verbose_name_plural = "Tags" + + def __str__(self): + return self.name + + def slugify(self, tag, i=None): + slug = slugify(tag) + if i is not None: + slug += "_%d" % i + return slug + + def save(self, *args, **kwargs): + if self._state.adding and not self.slug: + self.slug = self.slugify(self.name) + + with transaction.atomic(): + slugs = set( + type(self) + ._default_manager.filter(slug__startswith=self.slug) + .values_list("slug", flat=True) + ) + + i = None + while True: + slug = self.slugify(self.name, i) + if slug not in slugs: + self.slug = slug + return super().save(*args, **kwargs) + i = 1 if i is None else i+1 + else: + return super().save(*args, **kwargs) + + +class Snapshot(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + url = models.URLField(unique=True) + timestamp = models.CharField(max_length=32, unique=True, db_index=True) + + title = models.CharField(max_length=128, null=True, blank=True, db_index=True) + + added = models.DateTimeField(auto_now_add=True, db_index=True) + updated = models.DateTimeField(null=True, blank=True, db_index=True) + tags = models.ManyToManyField(Tag) + + keys = ('url', 'timestamp', 'title', 'tags', 'updated') + + def __repr__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + def __str__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + @classmethod + def from_json(cls, info: dict): + info = {k: v for k, v in info.items() if k in cls.keys} + return cls(**info) + + def as_json(self, *args) -> dict: + args = args or self.keys + return { + key: getattr(self, key) + if key != 'tags' else self.tags_str() + for key in args + } + + def as_link(self) -> Link: + return Link.from_json(self.as_json()) + + def as_link_with_details(self) -> Link: + from ..index import load_link_details + return load_link_details(self.as_link()) + + def tags_str(self) -> str: + return ','.join(self.tags.order_by('name').values_list('name', flat=True)) + + @cached_property + def bookmarked(self): + return parse_date(self.timestamp) + + @cached_property + def is_archived(self): + return self.as_link().is_archived + + @cached_property + def num_outputs(self): + return self.archiveresult_set.filter(status='succeeded').count() + + @cached_property + def url_hash(self): + return self.as_link().url_hash + + @cached_property + def base_url(self): + return self.as_link().base_url + + @cached_property + def link_dir(self): + return self.as_link().link_dir + + @cached_property + def archive_path(self): + return self.as_link().archive_path + + @cached_property + def archive_size(self): + return self.as_link().archive_size + + @cached_property + def history(self): + # TODO: use ArchiveResult for this instead of json + return self.as_link_with_details().history + + @cached_property + def latest_title(self): + if ('title' in self.history + and self.history['title'] + and (self.history['title'][-1].status == 'succeeded') + and self.history['title'][-1].output.strip()): + return self.history['title'][-1].output.strip() + return None + + def save_tags(self, tags=()): + tags_id = [] + for tag in tags: + tags_id.append(Tag.objects.get_or_create(name=tag)[0].id) + self.tags.clear() + self.tags.add(*tags_id) + + +class ArchiveResultManager(models.Manager): + def indexable(self, sorted: bool = True): + INDEXABLE_METHODS = [ r[0] for r in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = self.get_queryset().filter(extractor__in=INDEXABLE_METHODS,status='succeeded') + + if sorted: + precedence = [ When(extractor=method, then=Value(precedence)) for method, precedence in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = qs.annotate(indexing_precedence=Case(*precedence, default=Value(1000),output_field=IntegerField())).order_by('indexing_precedence') + return qs + + +class ArchiveResult(models.Model): + snapshot = models.ForeignKey(Snapshot, on_delete=models.CASCADE) + cmd = JSONField() + pwd = models.CharField(max_length=256) + cmd_version = models.CharField(max_length=32, default=None, null=True, blank=True) + output = models.CharField(max_length=512) + start_ts = models.DateTimeField() + end_ts = models.DateTimeField() + status = models.CharField(max_length=16, choices=STATUS_CHOICES) + extractor = models.CharField(choices=EXTRACTORS, max_length=32) + + objects = ArchiveResultManager() + + def __str__(self): + return self.extractor diff --git a/archivebox-0.5.3/build/lib/archivebox/core/settings.py b/archivebox-0.5.3/build/lib/archivebox/core/settings.py new file mode 100644 index 0000000..e8ed6b1 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/settings.py @@ -0,0 +1,165 @@ +__package__ = 'archivebox.core' + +import os +import sys + +from pathlib import Path +from django.utils.crypto import get_random_string + +from ..config import ( # noqa: F401 + DEBUG, + SECRET_KEY, + ALLOWED_HOSTS, + PACKAGE_DIR, + ACTIVE_THEME, + TEMPLATES_DIR_NAME, + SQL_INDEX_FILENAME, + OUTPUT_DIR, +) + + +IS_MIGRATING = 'makemigrations' in sys.argv[:3] or 'migrate' in sys.argv[:3] +IS_TESTING = 'test' in sys.argv[:3] or 'PYTEST_CURRENT_TEST' in os.environ +IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3] + +################################################################################ +### Django Core Settings +################################################################################ + +WSGI_APPLICATION = 'core.wsgi.application' +ROOT_URLCONF = 'core.urls' + +LOGIN_URL = '/accounts/login/' +LOGOUT_REDIRECT_URL = '/' +PASSWORD_RESET_URL = '/accounts/password_reset/' +APPEND_SLASH = True + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'core', + + 'django_extensions', +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + + +################################################################################ +### Staticfile and Template Settings +################################################################################ + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME / 'static'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default' / 'static'), +] + +TEMPLATE_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME), +] + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': TEMPLATE_DIRS, + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + + +################################################################################ +### External Service Settings +################################################################################ + +DATABASE_FILE = Path(OUTPUT_DIR) / SQL_INDEX_FILENAME +DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", DATABASE_FILE) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': DATABASE_NAME, + } +} + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + + +################################################################################ +### Security Settings +################################################################################ + +SECRET_KEY = SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.') + +ALLOWED_HOSTS = ALLOWED_HOSTS.split(',') + +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True + +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = False +SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_AGE = 1209600 # 2 weeks +SESSION_EXPIRE_AT_BROWSER_CLOSE = False +SESSION_SAVE_EVERY_REQUEST = True + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + + +################################################################################ +### Shell Settings +################################################################################ + +SHELL_PLUS = 'ipython' +SHELL_PLUS_PRINT_SQL = False +IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner'] +IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell' +if IS_SHELL: + os.environ['PYTHONSTARTUP'] = str(Path(PACKAGE_DIR) / 'core' / 'welcome_message.py') + + +################################################################################ +### Internationalization & Localization Settings +################################################################################ + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = False +USE_L10N = False +USE_TZ = False + +DATETIME_FORMAT = 'Y-m-d g:iA' +SHORT_DATETIME_FORMAT = 'Y-m-d h:iA' diff --git a/archivebox-0.5.3/build/lib/archivebox/core/templatetags/__init__.py b/archivebox-0.5.3/build/lib/archivebox/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/build/lib/archivebox/core/templatetags/core_tags.py b/archivebox-0.5.3/build/lib/archivebox/core/templatetags/core_tags.py new file mode 100644 index 0000000..25f0685 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/templatetags/core_tags.py @@ -0,0 +1,47 @@ +from django import template +from django.urls import reverse +from django.contrib.admin.templatetags.base import InclusionAdminNode +from django.templatetags.static import static + + +from typing import Union + +from core.models import ArchiveResult + +register = template.Library() + +@register.simple_tag +def snapshot_image(snapshot): + result = ArchiveResult.objects.filter(snapshot=snapshot, extractor='screenshot', status='succeeded').first() + if result: + return reverse('LinkAssets', args=[f'{str(snapshot.timestamp)}/{result.output}']) + + return static('archive.png') + +@register.filter +def file_size(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + +def result_list(cl): + """ + Monkey patched result + """ + num_sorted_fields = 0 + return { + 'cl': cl, + 'num_sorted_fields': num_sorted_fields, + 'results': cl.result_list, + } + +@register.tag(name='snapshots_grid') +def result_list_tag(parser, token): + return InclusionAdminNode( + parser, token, + func=result_list, + template_name='snapshots_grid.html', + takes_context=False, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/core/tests.py b/archivebox-0.5.3/build/lib/archivebox/core/tests.py new file mode 100644 index 0000000..4d66077 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/tests.py @@ -0,0 +1,3 @@ +#from django.test import TestCase + +# Create your tests here. diff --git a/archivebox-0.5.3/build/lib/archivebox/core/urls.py b/archivebox-0.5.3/build/lib/archivebox/core/urls.py new file mode 100644 index 0000000..b8e4baf --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/urls.py @@ -0,0 +1,36 @@ +from django.contrib import admin + +from django.urls import path, include +from django.views import static +from django.conf import settings +from django.views.generic.base import RedirectView + +from core.views import MainIndex, LinkDetails, PublicArchiveView, AddView + + +# print('DEBUG', settings.DEBUG) + +urlpatterns = [ + path('robots.txt', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'robots.txt'}), + path('favicon.ico', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'favicon.ico'}), + + path('docs/', RedirectView.as_view(url='https://github.com/ArchiveBox/ArchiveBox/wiki'), name='Docs'), + + path('archive/', RedirectView.as_view(url='/')), + path('archive/', LinkDetails.as_view(), name='LinkAssets'), + + path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')), + path('add/', AddView.as_view()), + + path('accounts/login/', RedirectView.as_view(url='/admin/login/')), + path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')), + + + path('accounts/', include('django.contrib.auth.urls')), + path('admin/', admin.site.urls), + + path('index.html', RedirectView.as_view(url='/')), + path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}), + path('', MainIndex.as_view(), name='Home'), + path('public/', PublicArchiveView.as_view(), name='public-index'), +] diff --git a/archivebox-0.5.3/build/lib/archivebox/core/views.py b/archivebox-0.5.3/build/lib/archivebox/core/views.py new file mode 100644 index 0000000..b46e364 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/views.py @@ -0,0 +1,173 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.shortcuts import render, redirect + +from django.http import HttpResponse +from django.views import View, static +from django.views.generic.list import ListView +from django.views.generic import FormView +from django.contrib.auth.mixins import UserPassesTestMixin + +from core.models import Snapshot +from core.forms import AddLinkForm + +from ..config import ( + OUTPUT_DIR, + PUBLIC_INDEX, + PUBLIC_SNAPSHOTS, + PUBLIC_ADD_VIEW, + VERSION, + FOOTER_INFO, +) +from main import add +from ..util import base_url, ansi_to_html +from ..index.html import snapshot_icons + + +class MainIndex(View): + template = 'main_index.html' + + def get(self, request): + if request.user.is_authenticated: + return redirect('/admin/core/snapshot/') + + if PUBLIC_INDEX: + return redirect('public-index') + + return redirect(f'/admin/login/?next={request.path}') + + +class LinkDetails(View): + def get(self, request, path): + # missing trailing slash -> redirect to index + if '/' not in path: + return redirect(f'{path}/index.html') + + if not request.user.is_authenticated and not PUBLIC_SNAPSHOTS: + return redirect(f'/admin/login/?next={request.path}') + + try: + slug, archivefile = path.split('/', 1) + except (IndexError, ValueError): + slug, archivefile = path.split('/', 1)[0], 'index.html' + + all_pages = list(Snapshot.objects.all()) + + # slug is a timestamp + by_ts = {page.timestamp: page for page in all_pages} + try: + # print('SERVING STATICFILE', by_ts[slug].link_dir, request.path, path) + response = static.serve(request, archivefile, document_root=by_ts[slug].link_dir, show_indexes=True) + response["Link"] = f'<{by_ts[slug].url}>; rel="canonical"' + return response + except KeyError: + pass + + # slug is a hash + by_hash = {page.url_hash: page for page in all_pages} + try: + timestamp = by_hash[slug].timestamp + return redirect(f'/archive/{timestamp}/{archivefile}') + except KeyError: + pass + + # slug is a URL + by_url = {page.base_url: page for page in all_pages} + try: + # TODO: add multiple snapshot support by showing index of all snapshots + # for given url instead of redirecting to timestamp index + timestamp = by_url[base_url(path)].timestamp + return redirect(f'/archive/{timestamp}/index.html') + except KeyError: + pass + + return HttpResponse( + 'No archived link matches the given timestamp or hash.', + content_type="text/plain", + status=404, + ) + +class PublicArchiveView(ListView): + template = 'snapshot_list.html' + model = Snapshot + paginate_by = 100 + ordering = ['title'] + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + query = self.request.GET.get('q') + if query: + qs = qs.filter(title__icontains=query) + for snapshot in qs: + snapshot.icons = snapshot_icons(snapshot) + return qs + + def get(self, *args, **kwargs): + if PUBLIC_INDEX or self.request.user.is_authenticated: + response = super().get(*args, **kwargs) + return response + else: + return redirect(f'/admin/login/?next={self.request.path}') + + +class AddView(UserPassesTestMixin, FormView): + template_name = "add_links.html" + form_class = AddLinkForm + + def get_initial(self): + """Prefill the AddLinkForm with the 'url' GET parameter""" + if self.request.method == 'GET': + url = self.request.GET.get('url', None) + if url: + return {'url': url} + else: + return super().get_initial() + + def test_func(self): + return PUBLIC_ADD_VIEW or self.request.user.is_authenticated + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'title': "Add URLs", + # We can't just call request.build_absolute_uri in the template, because it would include query parameters + 'absolute_add_path': self.request.build_absolute_uri(self.request.path), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def form_valid(self, form): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + extractors = ','.join(form.cleaned_data["archive_methods"]) + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + if extractors: + input_kwargs.update({"extractors": extractors}) + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context = self.get_context_data() + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + return render(template_name=self.template_name, request=self.request, context=context) diff --git a/archivebox-0.5.3/build/lib/archivebox/core/welcome_message.py b/archivebox-0.5.3/build/lib/archivebox/core/welcome_message.py new file mode 100644 index 0000000..ed5d2d7 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/welcome_message.py @@ -0,0 +1,5 @@ +from archivebox.logging_util import log_shell_welcome_msg + + +if __name__ == '__main__': + log_shell_welcome_msg() diff --git a/archivebox-0.5.3/build/lib/archivebox/core/wsgi.py b/archivebox-0.5.3/build/lib/archivebox/core/wsgi.py new file mode 100644 index 0000000..f933afa --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for archivebox project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.settings') + +application = get_wsgi_application() diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/__init__.py b/archivebox-0.5.3/build/lib/archivebox/extractors/__init__.py new file mode 100644 index 0000000..a4acef0 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/__init__.py @@ -0,0 +1,182 @@ +__package__ = 'archivebox.extractors' + +import os +from pathlib import Path + +from typing import Optional, List, Iterable, Union +from datetime import datetime +from django.db.models import QuerySet + +from ..index.schema import Link +from ..index.sql import write_link_to_sql_index +from ..index import ( + load_link_details, + write_link_details, +) +from ..util import enforce_types +from ..logging_util import ( + log_archiving_started, + log_archiving_paused, + log_archiving_finished, + log_link_archiving_started, + log_link_archiving_finished, + log_archive_method_started, + log_archive_method_finished, +) +from ..search import write_search_index + +from .title import should_save_title, save_title +from .favicon import should_save_favicon, save_favicon +from .wget import should_save_wget, save_wget +from .singlefile import should_save_singlefile, save_singlefile +from .readability import should_save_readability, save_readability +from .mercury import should_save_mercury, save_mercury +from .pdf import should_save_pdf, save_pdf +from .screenshot import should_save_screenshot, save_screenshot +from .dom import should_save_dom, save_dom +from .git import should_save_git, save_git +from .media import should_save_media, save_media +from .archive_org import should_save_archive_dot_org, save_archive_dot_org +from .headers import should_save_headers, save_headers + + +def get_default_archive_methods(): + return [ + ('title', should_save_title, save_title), + ('favicon', should_save_favicon, save_favicon), + ('wget', should_save_wget, save_wget), + ('singlefile', should_save_singlefile, save_singlefile), + ('pdf', should_save_pdf, save_pdf), + ('screenshot', should_save_screenshot, save_screenshot), + ('dom', should_save_dom, save_dom), + ('readability', should_save_readability, save_readability), #keep readability below wget and singlefile, as it depends on them + ('mercury', should_save_mercury, save_mercury), + ('git', should_save_git, save_git), + ('media', should_save_media, save_media), + ('headers', should_save_headers, save_headers), + ('archive_org', should_save_archive_dot_org, save_archive_dot_org), + ] + +ARCHIVE_METHODS_INDEXING_PRECEDENCE = [('readability', 1), ('singlefile', 2), ('dom', 3), ('wget', 4)] + +@enforce_types +def ignore_methods(to_ignore: List[str]): + ARCHIVE_METHODS = get_default_archive_methods() + methods = filter(lambda x: x[0] not in to_ignore, ARCHIVE_METHODS) + methods = map(lambda x: x[0], methods) + return list(methods) + +@enforce_types +def archive_link(link: Link, overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> Link: + """download the DOM, PDF, and a screenshot into a folder named after the link's timestamp""" + + # TODO: Remove when the input is changed to be a snapshot. Suboptimal approach. + from core.models import Snapshot, ArchiveResult + try: + snapshot = Snapshot.objects.get(url=link.url) # TODO: This will be unnecessary once everything is a snapshot + except Snapshot.DoesNotExist: + snapshot = write_link_to_sql_index(link) + + ARCHIVE_METHODS = get_default_archive_methods() + + if methods: + ARCHIVE_METHODS = [ + method for method in ARCHIVE_METHODS + if method[0] in methods + ] + + out_dir = out_dir or Path(link.link_dir) + try: + is_new = not Path(out_dir).exists() + if is_new: + os.makedirs(out_dir) + + link = load_link_details(link, out_dir=out_dir) + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + log_link_archiving_started(link, out_dir, is_new) + link = link.overwrite(updated=datetime.now()) + stats = {'skipped': 0, 'succeeded': 0, 'failed': 0} + + for method_name, should_run, method_function in ARCHIVE_METHODS: + try: + if method_name not in link.history: + link.history[method_name] = [] + + if should_run(link, out_dir) or overwrite: + log_archive_method_started(method_name) + + result = method_function(link=link, out_dir=out_dir) + + link.history[method_name].append(result) + + stats[result.status] += 1 + log_archive_method_finished(result) + write_search_index(link=link, texts=result.index_texts) + ArchiveResult.objects.create(snapshot=snapshot, extractor=method_name, cmd=result.cmd, cmd_version=result.cmd_version, + output=result.output, pwd=result.pwd, start_ts=result.start_ts, end_ts=result.end_ts, status=result.status) + + else: + # print('{black} X {}{reset}'.format(method_name, **ANSI)) + stats['skipped'] += 1 + except Exception as e: + raise Exception('Exception in archive_methods.save_{}(Link(url={}))'.format( + method_name, + link.url, + )) from e + + # print(' ', stats) + + try: + latest_title = link.history['title'][-1].output.strip() + if latest_title and len(latest_title) >= len(link.title or ''): + link = link.overwrite(title=latest_title) + except Exception: + pass + + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + + log_link_archiving_finished(link, link.link_dir, is_new, stats) + + except KeyboardInterrupt: + try: + write_link_details(link, out_dir=link.link_dir) + except: + pass + raise + + except Exception as err: + print(' ! Failed to archive link: {}: {}'.format(err.__class__.__name__, err)) + raise + + return link + +@enforce_types +def archive_links(all_links: Union[Iterable[Link], QuerySet], overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> List[Link]: + + if type(all_links) is QuerySet: + num_links: int = all_links.count() + get_link = lambda x: x.as_link() + all_links = all_links.iterator() + else: + num_links: int = len(all_links) + get_link = lambda x: x + + if num_links == 0: + return [] + + log_archiving_started(num_links) + idx: int = 0 + try: + for link in all_links: + idx += 1 + to_archive = get_link(link) + archive_link(to_archive, overwrite=overwrite, methods=methods, out_dir=Path(link.link_dir)) + except KeyboardInterrupt: + log_archiving_paused(num_links, idx, link.timestamp) + raise SystemExit(0) + except BaseException: + print() + raise + + log_archiving_finished(num_links) + return all_links diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/archive_org.py b/archivebox-0.5.3/build/lib/archivebox/extractors/archive_org.py new file mode 100644 index 0000000..f5598d6 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/archive_org.py @@ -0,0 +1,112 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional, List, Dict, Tuple +from collections import defaultdict + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + TIMEOUT, + CURL_ARGS, + CHECK_SSL_VALIDITY, + SAVE_ARCHIVE_DOT_ORG, + CURL_BINARY, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_archive_dot_org(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "archive.org.txt").exists(): + # if open(path, 'r').read().strip() != 'None': + return False + + return SAVE_ARCHIVE_DOT_ORG + +@enforce_types +def save_archive_dot_org(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """submit site to archive.org for archiving via their service, save returned archive url""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'archive.org.txt' + archive_org_url = None + submit_url = 'https://web.archive.org/save/{}'.format(link.url) + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + submit_url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + content_location, errors = parse_archive_dot_org_response(result.stdout) + if content_location: + archive_org_url = content_location[0] + elif len(errors) == 1 and 'RobotAccessControlException' in errors[0]: + archive_org_url = None + # raise ArchiveError('Archive.org denied by {}/robots.txt'.format(domain(link.url))) + elif errors: + raise ArchiveError(', '.join(errors)) + else: + raise ArchiveError('Failed to find "content-location" URL header in Archive.org response.') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + if output and not isinstance(output, Exception): + # instead of writing None when archive.org rejects the url write the + # url to resubmit it to archive.org. This is so when the user visits + # the URL in person, it will attempt to re-archive it, and it'll show the + # nicer error message explaining why the url was rejected if it fails. + archive_org_url = archive_org_url or submit_url + with open(str(out_dir / output), 'w', encoding='utf-8') as f: + f.write(archive_org_url) + chmod_file('archive.org.txt', cwd=str(out_dir)) + output = archive_org_url + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) + +@enforce_types +def parse_archive_dot_org_response(response: bytes) -> Tuple[List[str], List[str]]: + # Parse archive.org response headers + headers: Dict[str, List[str]] = defaultdict(list) + + # lowercase all the header names and store in dict + for header in response.splitlines(): + if b':' not in header or not header.strip(): + continue + name, val = header.decode().split(':', 1) + headers[name.lower().strip()].append(val.strip()) + + # Get successful archive url in "content-location" header or any errors + content_location = headers.get('content-location', headers['location']) + errors = headers['x-archive-wayback-runtime-error'] + return content_location, errors + diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/dom.py b/archivebox-0.5.3/build/lib/archivebox/extractors/dom.py new file mode 100644 index 0000000..babbe71 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/dom.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file, atomic_write +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_DOM, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_dom(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / 'output.html').exists(): + return False + + return SAVE_DOM + +@enforce_types +def save_dom(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print HTML of site to file using chrome --dump-html""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.html' + output_path = out_dir / output + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--dump-dom', + link.url + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + atomic_write(output_path, result.stdout) + + if result.returncode: + hints = result.stderr.decode() + raise ArchiveError('Failed to save DOM', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/favicon.py b/archivebox-0.5.3/build/lib/archivebox/extractors/favicon.py new file mode 100644 index 0000000..5e7c1fb --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/favicon.py @@ -0,0 +1,64 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import chmod_file, run +from ..util import enforce_types, domain +from ..config import ( + TIMEOUT, + SAVE_FAVICON, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CHECK_SSL_VALIDITY, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_favicon(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if (Path(out_dir) / 'favicon.ico').exists(): + return False + + return SAVE_FAVICON + +@enforce_types +def save_favicon(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download site favicon from google's favicon api""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'favicon.ico' + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + '--output', str(output), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + 'https://www.google.com/s2/favicons?domain={}'.format(domain(link.url)), + ] + status = 'pending' + timer = TimedProgress(timeout, prefix=' ') + try: + run(cmd, cwd=str(out_dir), timeout=timeout) + chmod_file(output, cwd=str(out_dir)) + status = 'succeeded' + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/git.py b/archivebox-0.5.3/build/lib/archivebox/extractors/git.py new file mode 100644 index 0000000..fd20d4b --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/git.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + domain, + extension, + without_query, + without_fragment, +) +from ..config import ( + TIMEOUT, + SAVE_GIT, + GIT_BINARY, + GIT_ARGS, + GIT_VERSION, + GIT_DOMAINS, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_git(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if (out_dir / "git").exists(): + return False + + is_clonable_url = ( + (domain(link.url) in GIT_DOMAINS) + or (extension(link.url) == 'git') + ) + if not is_clonable_url: + return False + + return SAVE_GIT + + +@enforce_types +def save_git(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using git""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'git' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + GIT_BINARY, + 'clone', + *GIT_ARGS, + *([] if CHECK_SSL_VALIDITY else ['-c', 'http.sslVerify=false']), + without_query(without_fragment(link.url)), + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + if result.returncode == 128: + # ignore failed re-download when the folder already exists + pass + elif result.returncode > 0: + hints = 'Got git response code: {}.'.format(result.returncode) + raise ArchiveError('Failed to save git clone', hints) + + chmod_file(output, cwd=str(out_dir)) + + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=GIT_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/headers.py b/archivebox-0.5.3/build/lib/archivebox/extractors/headers.py new file mode 100644 index 0000000..4e69dec --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/headers.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import atomic_write +from ..util import ( + enforce_types, + get_headers, +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + CURL_ARGS, + CURL_USER_AGENT, + CURL_VERSION, + CHECK_SSL_VALIDITY, + SAVE_HEADERS +) +from ..logging_util import TimedProgress + +@enforce_types +def should_save_headers(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + + output = Path(out_dir or link.link_dir) / 'headers.json' + return not output.exists() and SAVE_HEADERS + + +@enforce_types +def save_headers(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """Download site headers""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() + output: ArchiveOutput = 'headers.json' + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + try: + json_headers = get_headers(link.url, timeout=timeout) + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "headers.json"), json_headers) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/media.py b/archivebox-0.5.3/build/lib/archivebox/extractors/media.py new file mode 100644 index 0000000..3792fd2 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/media.py @@ -0,0 +1,81 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + MEDIA_TIMEOUT, + SAVE_MEDIA, + YOUTUBEDL_ARGS, + YOUTUBEDL_BINARY, + YOUTUBEDL_VERSION, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_media(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + + if is_static_file(link.url): + return False + + if (out_dir / "media").exists(): + return False + + return SAVE_MEDIA + +@enforce_types +def save_media(link: Link, out_dir: Optional[Path]=None, timeout: int=MEDIA_TIMEOUT) -> ArchiveResult: + """Download playlists or individual video, audio, and subtitles using youtube-dl""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'media' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + YOUTUBEDL_BINARY, + *YOUTUBEDL_ARGS, + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + chmod_file(output, cwd=str(out_dir)) + if result.returncode: + if (b'ERROR: Unsupported URL' in result.stderr + or b'HTTP Error 404' in result.stderr + or b'HTTP Error 403' in result.stderr + or b'URL could be a direct video link' in result.stderr + or b'Unable to extract container ID' in result.stderr): + # These happen too frequently on non-media pages to warrant printing to console + pass + else: + hints = ( + 'Got youtube-dl response code: {}.'.format(result.returncode), + *result.stderr.decode().split('\n'), + ) + raise ArchiveError('Failed to save media', hints) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=YOUTUBEDL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/mercury.py b/archivebox-0.5.3/build/lib/archivebox/extractors/mercury.py new file mode 100644 index 0000000..741c329 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/mercury.py @@ -0,0 +1,104 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from subprocess import CompletedProcess +from typing import Optional, List +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + is_static_file, + +) +from ..config import ( + TIMEOUT, + SAVE_MERCURY, + DEPENDENCIES, + MERCURY_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def ShellError(cmd: List[str], result: CompletedProcess, lines: int=20) -> ArchiveError: + # parse out last line of stderr + return ArchiveError( + f'Got {cmd[0]} response code: {result.returncode}).', + *( + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', lines)[-lines:] + if line.strip() + ), + ) + + +@enforce_types +def should_save_mercury(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'mercury' + return SAVE_MERCURY and MERCURY_VERSION and (not output.exists()) + + +@enforce_types +def save_mercury(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @postlight/mercury-parser""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "mercury" + output = str(output_folder) + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + # Get plain text version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url, + "--format=text" + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_text = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + # Get HTML version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_json = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "content.html"), article_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), article_text["content"]) + atomic_write(str(output_folder / "article.json"), article_json) + + # Check for common failure cases + if (result.returncode > 0): + raise ShellError(cmd, result) + except (ArchiveError, Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=MERCURY_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/pdf.py b/archivebox-0.5.3/build/lib/archivebox/extractors/pdf.py new file mode 100644 index 0000000..1b0201e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/pdf.py @@ -0,0 +1,68 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_PDF, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_pdf(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "output.pdf").exists(): + return False + + return SAVE_PDF + + +@enforce_types +def save_pdf(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print PDF of site to file using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.pdf' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--print-to-pdf', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save PDF', hints) + + chmod_file('output.pdf', cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/readability.py b/archivebox-0.5.3/build/lib/archivebox/extractors/readability.py new file mode 100644 index 0000000..9da620b --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/readability.py @@ -0,0 +1,124 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from tempfile import NamedTemporaryFile + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + download_url, + is_static_file, + +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + SAVE_READABILITY, + DEPENDENCIES, + READABILITY_VERSION, +) +from ..logging_util import TimedProgress + +@enforce_types +def get_html(link: Link, path: Path) -> str: + """ + Try to find wget, singlefile and then dom files. + If none is found, download the url again. + """ + canonical = link.canonical_outputs() + abs_path = path.absolute() + sources = [canonical["singlefile_path"], canonical["wget_path"], canonical["dom_path"]] + document = None + for source in sources: + try: + with open(abs_path / source, "r") as f: + document = f.read() + break + except (FileNotFoundError, TypeError): + continue + if document is None: + return download_url(link.url) + else: + return document + +@enforce_types +def should_save_readability(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'readability' + return SAVE_READABILITY and READABILITY_VERSION and (not output.exists()) + + +@enforce_types +def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @mozilla/readability""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "readability" + output = str(output_folder) + + # Readability Docs: https://github.com/mozilla/readability + + status = 'succeeded' + # fake command to show the user so they have something to try debugging if get_html fails + cmd = [ + CURL_BINARY, + link.url + ] + readability_content = None + timer = TimedProgress(timeout, prefix=' ') + try: + document = get_html(link, out_dir) + temp_doc = NamedTemporaryFile(delete=False) + temp_doc.write(document.encode("utf-8")) + temp_doc.close() + + cmd = [ + DEPENDENCIES['READABILITY_BINARY']['path'], + temp_doc.name + ] + + result = run(cmd, cwd=out_dir, timeout=timeout) + result_json = json.loads(result.stdout) + output_folder.mkdir(exist_ok=True) + readability_content = result_json.pop("textContent") + atomic_write(str(output_folder / "content.html"), result_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), readability_content) + atomic_write(str(output_folder / "article.json"), result_json) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got readability response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('Readability was not able to archive the page', hints) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=READABILITY_VERSION, + output=output, + status=status, + index_texts= [readability_content] if readability_content else [], + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/screenshot.py b/archivebox-0.5.3/build/lib/archivebox/extractors/screenshot.py new file mode 100644 index 0000000..325584e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/screenshot.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SCREENSHOT, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_screenshot(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "screenshot.png").exists(): + return False + + return SAVE_SCREENSHOT + +@enforce_types +def save_screenshot(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """take screenshot of site using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'screenshot.png' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--screenshot', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save screenshot', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/singlefile.py b/archivebox-0.5.3/build/lib/archivebox/extractors/singlefile.py new file mode 100644 index 0000000..2e5c389 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/singlefile.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SINGLEFILE, + DEPENDENCIES, + SINGLEFILE_VERSION, + CHROME_BINARY, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_singlefile(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + output = out_dir / 'singlefile.html' + return SAVE_SINGLEFILE and SINGLEFILE_VERSION and (not output.exists()) + + +@enforce_types +def save_singlefile(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using single-file""" + + out_dir = out_dir or Path(link.link_dir) + output = str(out_dir.absolute() / "singlefile.html") + + browser_args = chrome_args(TIMEOUT=0) + + # SingleFile CLI Docs: https://github.com/gildas-lormeau/SingleFile/tree/master/cli + browser_args = '--browser-args={}'.format(json.dumps(browser_args[1:])) + cmd = [ + DEPENDENCIES['SINGLEFILE_BINARY']['path'], + '--browser-executable-path={}'.format(CHROME_BINARY), + browser_args, + link.url, + output + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got single-file response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('SingleFile was not able to archive the page', hints) + chmod_file(output) + except (Exception, OSError) as err: + status = 'failed' + # TODO: Make this prettier. This is necessary to run the command (escape JSON internal quotes). + cmd[2] = browser_args.replace('"', "\\\"") + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=SINGLEFILE_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/title.py b/archivebox-0.5.3/build/lib/archivebox/extractors/title.py new file mode 100644 index 0000000..28cb128 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/title.py @@ -0,0 +1,130 @@ +__package__ = 'archivebox.extractors' + +import re +from html.parser import HTMLParser +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..util import ( + enforce_types, + is_static_file, + download_url, + htmldecode, +) +from ..config import ( + TIMEOUT, + CHECK_SSL_VALIDITY, + SAVE_TITLE, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +HTML_TITLE_REGEX = re.compile( + r'' # start matching text after tag + r'(.[^<>]+)', # get everything up to these symbols + re.IGNORECASE | re.MULTILINE | re.DOTALL | re.UNICODE, +) + + +class TitleParser(HTMLParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.title_tag = "" + self.title_og = "" + self.inside_title_tag = False + + @property + def title(self): + return self.title_tag or self.title_og or None + + def handle_starttag(self, tag, attrs): + if tag.lower() == "title" and not self.title_tag: + self.inside_title_tag = True + elif tag.lower() == "meta" and not self.title_og: + attrs = dict(attrs) + if attrs.get("property") == "og:title" and attrs.get("content"): + self.title_og = attrs.get("content") + + def handle_data(self, data): + if self.inside_title_tag and data: + self.title_tag += data.strip() + + def handle_endtag(self, tag): + if tag.lower() == "title": + self.inside_title_tag = False + + +@enforce_types +def should_save_title(link: Link, out_dir: Optional[str]=None) -> bool: + # if link already has valid title, skip it + if link.title and not link.title.lower().startswith('http'): + return False + + if is_static_file(link.url): + return False + + return SAVE_TITLE + +def extract_title_with_regex(html): + match = re.search(HTML_TITLE_REGEX, html) + output = htmldecode(match.group(1).strip()) if match else None + return output + +@enforce_types +def save_title(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """try to guess the page's title from its content""" + + from core.models import Snapshot + + output: ArchiveOutput = None + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + html = download_url(link.url, timeout=timeout) + try: + # try using relatively strict html parser first + parser = TitleParser() + parser.feed(html) + output = parser.title + if output is None: + raise + except Exception: + # fallback to regex that can handle broken/malformed html + output = extract_title_with_regex(html) + + # if title is better than the one in the db, update db with new title + if isinstance(output, str) and output: + if not link.title or len(output) >= len(link.title): + Snapshot.objects.filter(url=link.url, + timestamp=link.timestamp)\ + .update(title=output) + else: + raise ArchiveError('Unable to detect page title') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/extractors/wget.py b/archivebox-0.5.3/build/lib/archivebox/extractors/wget.py new file mode 100644 index 0000000..331f636 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/extractors/wget.py @@ -0,0 +1,184 @@ +__package__ = 'archivebox.extractors' + +import re +from pathlib import Path + +from typing import Optional +from datetime import datetime + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + without_scheme, + without_fragment, + without_query, + path, + domain, + urldecode, +) +from ..config import ( + WGET_ARGS, + TIMEOUT, + SAVE_WGET, + SAVE_WARC, + WGET_BINARY, + WGET_VERSION, + RESTRICT_FILE_NAMES, + CHECK_SSL_VALIDITY, + SAVE_WGET_REQUISITES, + WGET_AUTO_COMPRESSION, + WGET_USER_AGENT, + COOKIES_FILE, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_wget(link: Link, out_dir: Optional[Path]=None) -> bool: + output_path = wget_output_path(link) + out_dir = out_dir or Path(link.link_dir) + if output_path and (out_dir / output_path).exists(): + return False + + return SAVE_WGET + + +@enforce_types +def save_wget(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using wget""" + + out_dir = out_dir or link.link_dir + if SAVE_WARC: + warc_dir = out_dir / "warc" + warc_dir.mkdir(exist_ok=True) + warc_path = warc_dir / str(int(datetime.now().timestamp())) + + # WGET CLI Docs: https://www.gnu.org/software/wget/manual/wget.html + output: ArchiveOutput = None + cmd = [ + WGET_BINARY, + # '--server-response', # print headers for better error parsing + *WGET_ARGS, + '--timeout={}'.format(timeout), + *(['--restrict-file-names={}'.format(RESTRICT_FILE_NAMES)] if RESTRICT_FILE_NAMES else []), + *(['--warc-file={}'.format(str(warc_path))] if SAVE_WARC else []), + *(['--page-requisites'] if SAVE_WGET_REQUISITES else []), + *(['--user-agent={}'.format(WGET_USER_AGENT)] if WGET_USER_AGENT else []), + *(['--load-cookies', COOKIES_FILE] if COOKIES_FILE else []), + *(['--compression=auto'] if WGET_AUTO_COMPRESSION else []), + *([] if SAVE_WARC else ['--timestamping']), + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate', '--no-hsts']), + link.url, + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + output = wget_output_path(link) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + files_downloaded = ( + int(output_tail[-1].strip().split(' ', 2)[1] or 0) + if 'Downloaded:' in output_tail[-1] + else 0 + ) + hints = ( + 'Got wget response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0 and files_downloaded < 1) or output is None: + if b'403: Forbidden' in result.stderr: + raise ArchiveError('403 Forbidden (try changing WGET_USER_AGENT)', hints) + if b'404: Not Found' in result.stderr: + raise ArchiveError('404 Not Found', hints) + if b'ERROR 500: Internal Server Error' in result.stderr: + raise ArchiveError('500 Internal Server Error', hints) + raise ArchiveError('Wget failed or got an error from the server', hints) + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=WGET_VERSION, + output=output, + status=status, + **timer.stats, + ) + + +@enforce_types +def wget_output_path(link: Link) -> Optional[str]: + """calculate the path to the wgetted .html file, since wget may + adjust some paths to be different than the base_url path. + + See docs on wget --adjust-extension (-E) + """ + if is_static_file(link.url): + return without_scheme(without_fragment(link.url)) + + # Wget downloads can save in a number of different ways depending on the url: + # https://example.com + # > example.com/index.html + # https://example.com?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + # https://www.example.com/?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc + # > example.com/abc.html + # https://example.com/abc/ + # > example.com/abc/index.html + # https://example.com/abc?v=zzVa_tX1OiI.html + # > example.com/abc?v=zzVa_tX1OiI.html + # https://example.com/abc/?v=zzVa_tX1OiI.html + # > example.com/abc/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc/test.html + # > example.com/abc/test.html + # https://example.com/abc/test?v=zzVa_tX1OiI + # > example.com/abc/test?v=zzVa_tX1OiI.html + # https://example.com/abc/test/?v=zzVa_tX1OiI + # > example.com/abc/test/index.html?v=zzVa_tX1OiI.html + + # There's also lots of complexity around how the urlencoding and renaming + # is done for pages with query and hash fragments or extensions like shtml / htm / php / etc + + # Since the wget algorithm for -E (appending .html) is incredibly complex + # and there's no way to get the computed output path from wget + # in order to avoid having to reverse-engineer how they calculate it, + # we just look in the output folder read the filename wget used from the filesystem + full_path = without_fragment(without_query(path(link.url))).strip('/') + search_dir = Path(link.link_dir) / domain(link.url).replace(":", "+") / urldecode(full_path) + for _ in range(4): + if search_dir.exists(): + if search_dir.is_dir(): + html_files = [ + f for f in search_dir.iterdir() + if re.search(".+\\.[Ss]?[Hh][Tt][Mm][Ll]?$", str(f), re.I | re.M) + ] + if html_files: + return str(html_files[0].relative_to(link.link_dir)) + + # Move up one directory level + search_dir = search_dir.parent + + if str(search_dir) == link.link_dir: + break + + return None diff --git a/archivebox-0.5.3/build/lib/archivebox/index/__init__.py b/archivebox-0.5.3/build/lib/archivebox/index/__init__.py new file mode 100644 index 0000000..8eab1d3 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/__init__.py @@ -0,0 +1,617 @@ +__package__ = 'archivebox.index' + +import os +import shutil +import json as pyjson +from pathlib import Path + +from itertools import chain +from typing import List, Tuple, Dict, Optional, Iterable +from collections import OrderedDict +from contextlib import contextmanager +from urllib.parse import urlparse +from django.db.models import QuerySet, Q + +from ..util import ( + scheme, + enforce_types, + ExtendedEncoder, +) +from ..config import ( + ARCHIVE_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + OUTPUT_DIR, + TIMEOUT, + URL_BLACKLIST_PTN, + stderr, + OUTPUT_PERMISSIONS +) +from ..logging_util import ( + TimedProgress, + log_indexing_process_started, + log_indexing_process_finished, + log_indexing_started, + log_indexing_finished, + log_parsing_finished, + log_deduping_finished, +) + +from .schema import Link, ArchiveResult +from .html import ( + write_html_link_details, +) +from .json import ( + parse_json_link_details, + write_json_link_details, +) +from .sql import ( + write_sql_main_index, + write_sql_link_details, +) + +from ..search import search_backend_enabled, query_search_index + +### Link filtering and checking + +@enforce_types +def merge_links(a: Link, b: Link) -> Link: + """deterministially merge two links, favoring longer field values over shorter, + and "cleaner" values over worse ones. + """ + assert a.base_url == b.base_url, f'Cannot merge two links with different URLs ({a.base_url} != {b.base_url})' + + # longest url wins (because a fuzzy url will always be shorter) + url = a.url if len(a.url) > len(b.url) else b.url + + # best title based on length and quality + possible_titles = [ + title + for title in (a.title, b.title) + if title and title.strip() and '://' not in title + ] + title = None + if len(possible_titles) == 2: + title = max(possible_titles, key=lambda t: len(t)) + elif len(possible_titles) == 1: + title = possible_titles[0] + + # earliest valid timestamp + timestamp = ( + a.timestamp + if float(a.timestamp or 0) < float(b.timestamp or 0) else + b.timestamp + ) + + # all unique, truthy tags + tags_set = ( + set(tag.strip() for tag in (a.tags or '').split(',')) + | set(tag.strip() for tag in (b.tags or '').split(',')) + ) + tags = ','.join(tags_set) or None + + # all unique source entries + sources = list(set(a.sources + b.sources)) + + # all unique history entries for the combined archive methods + all_methods = set(list(a.history.keys()) + list(a.history.keys())) + history = { + method: (a.history.get(method) or []) + (b.history.get(method) or []) + for method in all_methods + } + for method in all_methods: + deduped_jsons = { + pyjson.dumps(result, sort_keys=True, cls=ExtendedEncoder) + for result in history[method] + } + history[method] = list(reversed(sorted( + (ArchiveResult.from_json(pyjson.loads(result)) for result in deduped_jsons), + key=lambda result: result.start_ts, + ))) + + return Link( + url=url, + timestamp=timestamp, + title=title, + tags=tags, + sources=sources, + history=history, + ) + + +@enforce_types +def validate_links(links: Iterable[Link]) -> List[Link]: + timer = TimedProgress(TIMEOUT * 4) + try: + links = archivable_links(links) # remove chrome://, about:, mailto: etc. + links = sorted_links(links) # deterministically sort the links based on timestamp, url + links = fix_duplicate_links(links) # merge/dedupe duplicate timestamps & urls + finally: + timer.end() + + return list(links) + +@enforce_types +def archivable_links(links: Iterable[Link]) -> Iterable[Link]: + """remove chrome://, about:// or other schemed links that cant be archived""" + for link in links: + try: + urlparse(link.url) + except ValueError: + continue + if scheme(link.url) not in ('http', 'https', 'ftp'): + continue + if URL_BLACKLIST_PTN and URL_BLACKLIST_PTN.search(link.url): + continue + + yield link + + +@enforce_types +def fix_duplicate_links(sorted_links: Iterable[Link]) -> Iterable[Link]: + """ + ensures that all non-duplicate links have monotonically increasing timestamps + """ + # from core.models import Snapshot + + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in sorted_links: + if link.url in unique_urls: + # merge with any other links that share the same url + link = merge_links(unique_urls[link.url], link) + unique_urls[link.url] = link + + return unique_urls.values() + + +@enforce_types +def sorted_links(links: Iterable[Link]) -> Iterable[Link]: + sort_func = lambda link: (link.timestamp.split('.', 1)[0], link.url) + return sorted(links, key=sort_func, reverse=True) + + +@enforce_types +def links_after_timestamp(links: Iterable[Link], resume: Optional[float]=None) -> Iterable[Link]: + if not resume: + yield from links + return + + for link in links: + try: + if float(link.timestamp) <= resume: + yield link + except (ValueError, TypeError): + print('Resume value and all timestamp values must be valid numbers.') + + +@enforce_types +def lowest_uniq_timestamp(used_timestamps: OrderedDict, timestamp: str) -> str: + """resolve duplicate timestamps by appending a decimal 1234, 1234 -> 1234.1, 1234.2""" + + timestamp = timestamp.split('.')[0] + nonce = 0 + + # first try 152323423 before 152323423.0 + if timestamp not in used_timestamps: + return timestamp + + new_timestamp = '{}.{}'.format(timestamp, nonce) + while new_timestamp in used_timestamps: + nonce += 1 + new_timestamp = '{}.{}'.format(timestamp, nonce) + + return new_timestamp + + + +### Main Links Index + +@contextmanager +@enforce_types +def timed_index_update(out_path: Path): + log_indexing_started(out_path) + timer = TimedProgress(TIMEOUT * 2, prefix=' ') + try: + yield + finally: + timer.end() + + assert out_path.exists(), f'Failed to write index file: {out_path}' + log_indexing_finished(out_path) + + +@enforce_types +def write_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + """Writes links to sqlite3 file for a given list of links""" + + log_indexing_process_started(len(links)) + + try: + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + + except (KeyboardInterrupt, SystemExit): + stderr('[!] Warning: Still writing index to disk...', color='lightyellow') + stderr(' Run archivebox init to fix any inconsistencies from an ungraceful exit.') + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + raise SystemExit(0) + + log_indexing_process_finished() + +@enforce_types +def load_main_index(out_dir: Path=OUTPUT_DIR, warn: bool=True) -> List[Link]: + """parse and load existing index with any new links from import_path merged in""" + from core.models import Snapshot + try: + return Snapshot.objects.all() + + except (KeyboardInterrupt, SystemExit): + raise SystemExit(0) + +@enforce_types +def load_main_index_meta(out_dir: Path=OUTPUT_DIR) -> Optional[dict]: + index_path = out_dir / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + meta_dict = pyjson.load(f) + meta_dict.pop('links') + return meta_dict + + return None + + +@enforce_types +def parse_links_from_source(source_path: str, root_url: Optional[str]=None) -> Tuple[List[Link], List[Link]]: + + from ..parsers import parse_links + + new_links: List[Link] = [] + + # parse and validate the import file + raw_links, parser_name = parse_links(source_path, root_url=root_url) + new_links = validate_links(raw_links) + + if parser_name: + num_parsed = len(raw_links) + log_parsing_finished(num_parsed, parser_name) + + return new_links + +@enforce_types +def fix_duplicate_links_in_index(snapshots: QuerySet, links: Iterable[Link]) -> Iterable[Link]: + """ + Given a list of in-memory Links, dedupe and merge them with any conflicting Snapshots in the DB. + """ + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in links: + index_link = snapshots.filter(url=link.url) + if index_link: + link = merge_links(index_link[0].as_link(), link) + + unique_urls[link.url] = link + + return unique_urls.values() + +@enforce_types +def dedupe_links(snapshots: QuerySet, + new_links: List[Link]) -> List[Link]: + """ + The validation of links happened at a different stage. This method will + focus on actual deduplication and timestamp fixing. + """ + + # merge existing links in out_dir and new links + dedup_links = fix_duplicate_links_in_index(snapshots, new_links) + + new_links = [ + link for link in new_links + if not snapshots.filter(url=link.url).exists() + ] + + dedup_links_dict = {link.url: link for link in dedup_links} + + # Replace links in new_links with the dedup version + for i in range(len(new_links)): + if new_links[i].url in dedup_links_dict.keys(): + new_links[i] = dedup_links_dict[new_links[i].url] + log_deduping_finished(len(new_links)) + + return new_links + +### Link Details Index + +@enforce_types +def write_link_details(link: Link, out_dir: Optional[str]=None, skip_sql_index: bool=False) -> None: + out_dir = out_dir or link.link_dir + + write_json_link_details(link, out_dir=out_dir) + write_html_link_details(link, out_dir=out_dir) + if not skip_sql_index: + write_sql_link_details(link) + + +@enforce_types +def load_link_details(link: Link, out_dir: Optional[str]=None) -> Link: + """check for an existing link archive in the given directory, + and load+merge it into the given link dict + """ + out_dir = out_dir or link.link_dir + + existing_link = parse_json_link_details(out_dir) + if existing_link: + return merge_links(existing_link, link) + + return link + + + +LINK_FILTERS = { + 'exact': lambda pattern: Q(url=pattern), + 'substring': lambda pattern: Q(url__icontains=pattern), + 'regex': lambda pattern: Q(url__iregex=pattern), + 'domain': lambda pattern: Q(url__istartswith=f"http://{pattern}") | Q(url__istartswith=f"https://{pattern}") | Q(url__istartswith=f"ftp://{pattern}"), + 'tag': lambda pattern: Q(tags__name=pattern), +} + +@enforce_types +def q_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + q_filter = Q() + for pattern in filter_patterns: + try: + q_filter = q_filter | LINK_FILTERS[filter_type](pattern) + except KeyError: + stderr() + stderr( + f'[X] Got invalid pattern for --filter-type={filter_type}:', + color='red', + ) + stderr(f' {pattern}') + raise SystemExit(2) + return snapshots.filter(q_filter) + +def search_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='search') -> QuerySet: + if not search_backend_enabled(): + stderr() + stderr( + '[X] The search backend is not enabled, set config.USE_SEARCHING_BACKEND = True', + color='red', + ) + raise SystemExit(2) + from core.models import Snapshot + + qsearch = Snapshot.objects.none() + for pattern in filter_patterns: + try: + qsearch |= query_search_index(pattern) + except: + raise SystemExit(2) + + return snapshots & qsearch + +@enforce_types +def snapshot_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + if filter_type != 'search': + return q_filter(snapshots, filter_patterns, filter_type) + else: + return search_filter(snapshots, filter_patterns, filter_type) + + +def get_indexed_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links without checking archive status or data directory validity""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in links + } + +def get_archived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are archived with a valid data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_archived, links) + } + +def get_unarchived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are unarchived with no data directory or an empty data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_unarchived, links) + } + +def get_present_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that actually exist in the archive/ folder""" + + all_folders = {} + + for entry in (out_dir / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(entry.path) + except Exception: + pass + + all_folders[entry.name] = link + + return all_folders + +def get_valid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs with a valid index matched to the main index and archived content""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_valid, links) + } + +def get_invalid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that are invalid for any reason: corrupted/duplicate/orphaned/unrecognized""" + duplicate = get_duplicate_folders(snapshots, out_dir=OUTPUT_DIR) + orphaned = get_orphaned_folders(snapshots, out_dir=OUTPUT_DIR) + corrupted = get_corrupted_folders(snapshots, out_dir=OUTPUT_DIR) + unrecognized = get_unrecognized_folders(snapshots, out_dir=OUTPUT_DIR) + return {**duplicate, **orphaned, **corrupted, **unrecognized} + + +def get_duplicate_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that conflict with other directories that have the same link URL or timestamp""" + by_url = {} + by_timestamp = {} + duplicate_folders = {} + + data_folders = ( + str(entry) + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir() + if entry.is_dir() and not snapshots.filter(timestamp=entry.name).exists() + ) + + for path in chain(snapshots.iterator(), data_folders): + link = None + if type(path) is not str: + path = path.as_link().link_dir + + try: + link = parse_json_link_details(path) + except Exception: + pass + + if link: + # link folder has same timestamp as different link folder + by_timestamp[link.timestamp] = by_timestamp.get(link.timestamp, 0) + 1 + if by_timestamp[link.timestamp] > 1: + duplicate_folders[path] = link + + # link folder has same url as different link folder + by_url[link.url] = by_url.get(link.url, 0) + 1 + if by_url[link.url] > 1: + duplicate_folders[path] = link + return duplicate_folders + +def get_orphaned_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that contain a valid index but aren't listed in the main index""" + orphaned_folders = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if link and not snapshots.filter(timestamp=entry.name).exists(): + # folder is a valid link data dir with index details, but it's not in the main index + orphaned_folders[str(entry)] = link + + return orphaned_folders + +def get_corrupted_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain a valid index and aren't listed in the main index""" + corrupted = {} + for snapshot in snapshots.iterator(): + link = snapshot.as_link() + if is_corrupt(link): + corrupted[link.link_dir] = link + return corrupted + +def get_unrecognized_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain recognizable archive data and aren't listed in the main index""" + unrecognized_folders: Dict[str, Optional[Link]] = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + index_exists = (entry / "index.json").exists() + link = None + try: + link = parse_json_link_details(str(entry)) + except KeyError: + # Try to fix index + if index_exists: + try: + # Last attempt to repair the detail index + link_guessed = parse_json_link_details(str(entry), guess=True) + write_json_link_details(link_guessed, out_dir=str(entry)) + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if index_exists and link is None: + # index exists but it's corrupted or unparseable + unrecognized_folders[str(entry)] = link + + elif not index_exists: + # link details index doesn't exist and the folder isn't in the main index + timestamp = entry.name + if not snapshots.filter(timestamp=timestamp).exists(): + unrecognized_folders[str(entry)] = link + + return unrecognized_folders + + +def is_valid(link: Link) -> bool: + dir_exists = Path(link.link_dir).exists() + index_exists = (Path(link.link_dir) / "index.json").exists() + if not dir_exists: + # unarchived links are not included in the valid list + return False + if dir_exists and not index_exists: + return False + if dir_exists and index_exists: + try: + parsed_link = parse_json_link_details(link.link_dir, guess=True) + return link.url == parsed_link.url + except Exception: + pass + return False + +def is_corrupt(link: Link) -> bool: + if not Path(link.link_dir).exists(): + # unarchived links are not considered corrupt + return False + + if is_valid(link): + return False + + return True + +def is_archived(link: Link) -> bool: + return is_valid(link) and link.is_archived + +def is_unarchived(link: Link) -> bool: + if not Path(link.link_dir).exists(): + return True + return not link.is_archived + + +def fix_invalid_folder_locations(out_dir: Path=OUTPUT_DIR) -> Tuple[List[str], List[str]]: + fixed = [] + cant_fix = [] + for entry in os.scandir(out_dir / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if not link: + continue + + if not entry.path.endswith(f'/{link.timestamp}'): + dest = out_dir / ARCHIVE_DIR_NAME / link.timestamp + if dest.exists(): + cant_fix.append(entry.path) + else: + shutil.move(entry.path, dest) + fixed.append(dest) + timestamp = entry.path.rsplit('/', 1)[-1] + assert link.link_dir == entry.path + assert link.timestamp == timestamp + write_json_link_details(link, out_dir=entry.path) + + return fixed, cant_fix diff --git a/archivebox-0.5.3/build/lib/archivebox/index/csv.py b/archivebox-0.5.3/build/lib/archivebox/index/csv.py new file mode 100644 index 0000000..804e646 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/csv.py @@ -0,0 +1,37 @@ +__package__ = 'archivebox.index' + +from typing import List, Optional, Any + +from ..util import enforce_types +from .schema import Link + + +@enforce_types +def links_to_csv(links: List[Link], + cols: Optional[List[str]]=None, + header: bool=True, + separator: str=',', + ljust: int=0) -> str: + + cols = cols or ['timestamp', 'is_archived', 'url'] + + header_str = '' + if header: + header_str = separator.join(col.ljust(ljust) for col in cols) + + row_strs = ( + link.to_csv(cols=cols, ljust=ljust, separator=separator) + for link in links + ) + + return '\n'.join((header_str, *row_strs)) + + +@enforce_types +def to_csv(obj: Any, cols: List[str], separator: str=',', ljust: int=0) -> str: + from .json import to_json + + return separator.join( + to_json(getattr(obj, col), indent=None).ljust(ljust) + for col in cols + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/index/html.py b/archivebox-0.5.3/build/lib/archivebox/index/html.py new file mode 100644 index 0000000..a62e2c7 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/html.py @@ -0,0 +1,164 @@ +__package__ = 'archivebox.index' + +from datetime import datetime +from typing import List, Optional, Iterator, Mapping +from pathlib import Path + +from django.utils.html import format_html +from collections import defaultdict + +from .schema import Link +from ..system import atomic_write +from ..logging_util import printable_filesize +from ..util import ( + enforce_types, + ts_to_date, + urlencode, + htmlencode, + urldecode, +) +from ..config import ( + OUTPUT_DIR, + VERSION, + GIT_SHA, + FOOTER_INFO, + HTML_INDEX_FILENAME, +) + +MAIN_INDEX_TEMPLATE = 'main_index.html' +MINIMAL_INDEX_TEMPLATE = 'main_index_minimal.html' +LINK_DETAILS_TEMPLATE = 'link_details.html' +TITLE_LOADING_MSG = 'Not yet archived...' + + +### Main Links Index + +@enforce_types +def parse_html_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[str]: + """parse an archive index html file and return the list of urls""" + + index_path = Path(out_dir) / HTML_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + for line in f: + if 'class="link-url"' in line: + yield line.split('"')[1] + return () + +@enforce_types +def generate_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = main_index_template(links) + else: + output = main_index_template(links, template=MINIMAL_INDEX_TEMPLATE) + return output + +@enforce_types +def main_index_template(links: List[Link], template: str=MAIN_INDEX_TEMPLATE) -> str: + """render the template for the entire main index""" + + return render_django_template(template, { + 'version': VERSION, + 'git_sha': GIT_SHA, + 'num_links': str(len(links)), + 'date_updated': datetime.now().strftime('%Y-%m-%d'), + 'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'), + 'links': [link._asdict(extended=True) for link in links], + 'FOOTER_INFO': FOOTER_INFO, + }) + + +### Link Details Index + +@enforce_types +def write_html_link_details(link: Link, out_dir: Optional[str]=None) -> None: + out_dir = out_dir or link.link_dir + + rendered_html = link_details_template(link) + atomic_write(str(Path(out_dir) / HTML_INDEX_FILENAME), rendered_html) + + +@enforce_types +def link_details_template(link: Link) -> str: + + from ..extractors.wget import wget_output_path + + link_info = link._asdict(extended=True) + + return render_django_template(LINK_DETAILS_TEMPLATE, { + **link_info, + **link_info['canonical'], + 'title': htmlencode( + link.title + or (link.base_url if link.is_archived else TITLE_LOADING_MSG) + ), + 'url_str': htmlencode(urldecode(link.base_url)), + 'archive_url': urlencode( + wget_output_path(link) + or (link.domain if link.is_archived else '') + ) or 'about:blank', + 'extension': link.extension or 'html', + 'tags': link.tags or 'untagged', + 'size': printable_filesize(link.archive_size) if link.archive_size else 'pending', + 'status': 'archived' if link.is_archived else 'not yet archived', + 'status_color': 'success' if link.is_archived else 'danger', + 'oldest_archive_date': ts_to_date(link.oldest_archive_date), + }) + +@enforce_types +def render_django_template(template: str, context: Mapping[str, str]) -> str: + """render a given html template string with the given template content""" + from django.template.loader import render_to_string + + return render_to_string(template, context) + + +def snapshot_icons(snapshot) -> str: + from core.models import EXTRACTORS + + archive_results = snapshot.archiveresult_set.filter(status="succeeded") + link = snapshot.as_link() + path = link.archive_path + canon = link.canonical_outputs() + output = "" + output_template = '<a href="/{}/{}" class="exists-{}" title="{}">{} </a>' + icons = { + "singlefile": "❶", + "wget": "🆆", + "dom": "🅷", + "pdf": "📄", + "screenshot": "💻", + "media": "📼", + "git": "🅶", + "archive_org": "🏛", + "readability": "🆁", + "mercury": "🅼", + "warc": "📦" + } + exclude = ["favicon", "title", "headers", "archive_org"] + # Missing specific entry for WARC + + extractor_items = defaultdict(lambda: None) + for extractor, _ in EXTRACTORS: + for result in archive_results: + if result.extractor == extractor: + extractor_items[extractor] = result + + for extractor, _ in EXTRACTORS: + if extractor not in exclude: + exists = extractor_items[extractor] is not None + output += output_template.format(path, canon[f"{extractor}_path"], str(exists), + extractor, icons.get(extractor, "?")) + if extractor == "wget": + # warc isn't technically it's own extractor, so we have to add it after wget + exists = list((Path(path) / canon["warc_path"]).glob("*.warc.gz")) + output += output_template.format(exists[0] if exists else '#', canon["warc_path"], str(bool(exists)), "warc", icons.get("warc", "?")) + + if extractor == "archive_org": + # The check for archive_org is different, so it has to be handled separately + target_path = Path(path) / "archive.org.txt" + exists = target_path.exists() + output += '<a href="{}" class="exists-{}" title="{}">{}</a> '.format(canon["archive_org_path"], str(exists), + "archive_org", icons.get("archive_org", "?")) + + return format_html(f'<span class="files-icons" style="font-size: 1.1em; opacity: 0.8">{output}<span>') diff --git a/archivebox-0.5.3/build/lib/archivebox/index/json.py b/archivebox-0.5.3/build/lib/archivebox/index/json.py new file mode 100644 index 0000000..f24b969 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/json.py @@ -0,0 +1,154 @@ +__package__ = 'archivebox.index' + +import os +import sys +import json as pyjson +from pathlib import Path + +from datetime import datetime +from typing import List, Optional, Iterator, Any, Union + +from .schema import Link +from ..system import atomic_write +from ..util import enforce_types +from ..config import ( + VERSION, + OUTPUT_DIR, + FOOTER_INFO, + GIT_SHA, + DEPENDENCIES, + JSON_INDEX_FILENAME, + ARCHIVE_DIR_NAME, + ANSI +) + + +MAIN_INDEX_HEADER = { + 'info': 'This is an index of site data archived by ArchiveBox: The self-hosted web archive.', + 'schema': 'archivebox.index.json', + 'copyright_info': FOOTER_INFO, + 'meta': { + 'project': 'ArchiveBox', + 'version': VERSION, + 'git_sha': GIT_SHA, + 'website': 'https://ArchiveBox.io', + 'docs': 'https://github.com/ArchiveBox/ArchiveBox/wiki', + 'source': 'https://github.com/ArchiveBox/ArchiveBox', + 'issues': 'https://github.com/ArchiveBox/ArchiveBox/issues', + 'dependencies': DEPENDENCIES, + }, +} + +@enforce_types +def generate_json_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = { + **MAIN_INDEX_HEADER, + 'num_links': len(links), + 'updated': datetime.now(), + 'last_run_cmd': sys.argv, + 'links': links, + } + else: + output = links + return to_json(output, indent=4, sort_keys=True) + + +@enforce_types +def parse_json_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + """parse an archive index json file and return the list of links""" + + index_path = Path(out_dir) / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + links = pyjson.load(f)['links'] + for link_json in links: + try: + yield Link.from_json(link_json) + except KeyError: + try: + detail_index_path = Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / link_json['timestamp'] + yield parse_json_link_details(str(detail_index_path)) + except KeyError: + # as a last effort, try to guess the missing values out of existing ones + try: + yield Link.from_json(link_json, guess=True) + except KeyError: + print(" {lightyellow}! Failed to load the index.json from {}".format(detail_index_path, **ANSI)) + continue + return () + +### Link Details Index + +@enforce_types +def write_json_link_details(link: Link, out_dir: Optional[str]=None) -> None: + """write a json file with some info about the link""" + + out_dir = out_dir or link.link_dir + path = Path(out_dir) / JSON_INDEX_FILENAME + atomic_write(str(path), link._asdict(extended=True)) + + +@enforce_types +def parse_json_link_details(out_dir: Union[Path, str], guess: Optional[bool]=False) -> Optional[Link]: + """load the json link index from a given directory""" + existing_index = Path(out_dir) / JSON_INDEX_FILENAME + if existing_index.exists(): + with open(existing_index, 'r', encoding='utf-8') as f: + try: + link_json = pyjson.load(f) + return Link.from_json(link_json, guess) + except pyjson.JSONDecodeError: + pass + return None + + +@enforce_types +def parse_json_links_details(out_dir: Union[Path, str]) -> Iterator[Link]: + """read through all the archive data folders and return the parsed links""" + + for entry in os.scandir(Path(out_dir) / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if link: + yield link + + + +### Helpers + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + + +@enforce_types +def to_json(obj: Any, indent: Optional[int]=4, sort_keys: bool=True, cls=ExtendedEncoder) -> str: + return pyjson.dumps(obj, indent=indent, sort_keys=sort_keys, cls=ExtendedEncoder) + diff --git a/archivebox-0.5.3/build/lib/archivebox/index/schema.py b/archivebox-0.5.3/build/lib/archivebox/index/schema.py new file mode 100644 index 0000000..bc3a25d --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/schema.py @@ -0,0 +1,448 @@ +""" + +WARNING: THIS FILE IS ALL LEGACY CODE TO BE REMOVED. + +DO NOT ADD ANY NEW FEATURES TO THIS FILE, NEW CODE GOES HERE: core/models.py + +""" + +__package__ = 'archivebox.index' + +from pathlib import Path + +from datetime import datetime, timedelta + +from typing import List, Dict, Any, Optional, Union + +from dataclasses import dataclass, asdict, field, fields + + +from ..system import get_dir_size + +from ..config import OUTPUT_DIR, ARCHIVE_DIR_NAME + +class ArchiveError(Exception): + def __init__(self, message, hints=None): + super().__init__(message) + self.hints = hints + +LinkDict = Dict[str, Any] + +ArchiveOutput = Union[str, Exception, None] + +@dataclass(frozen=True) +class ArchiveResult: + cmd: List[str] + pwd: Optional[str] + cmd_version: Optional[str] + output: ArchiveOutput + status: str + start_ts: datetime + end_ts: datetime + index_texts: Union[List[str], None] = None + schema: str = 'ArchiveResult' + + def __post_init__(self): + self.typecheck() + + def _asdict(self): + return asdict(self) + + def typecheck(self) -> None: + assert self.schema == self.__class__.__name__ + assert isinstance(self.status, str) and self.status + assert isinstance(self.start_ts, datetime) + assert isinstance(self.end_ts, datetime) + assert isinstance(self.cmd, list) + assert all(isinstance(arg, str) and arg for arg in self.cmd) + assert self.pwd is None or isinstance(self.pwd, str) and self.pwd + assert self.cmd_version is None or isinstance(self.cmd_version, str) and self.cmd_version + assert self.output is None or isinstance(self.output, (str, Exception)) + if isinstance(self.output, str): + assert self.output + + @classmethod + def guess_ts(_cls, dict_info): + from ..util import parse_date + parsed_timestamp = parse_date(dict_info["timestamp"]) + start_ts = parsed_timestamp + end_ts = parsed_timestamp + timedelta(seconds=int(dict_info["duration"])) + return start_ts, end_ts + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + if guess: + keys = info.keys() + if "start_ts" not in keys: + info["start_ts"], info["end_ts"] = cls.guess_ts(json_info) + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + if "pwd" not in keys: + info["pwd"] = str(Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / json_info["timestamp"]) + if "cmd_version" not in keys: + info["cmd_version"] = "Undefined" + if "cmd" not in keys: + info["cmd"] = [] + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + info['cmd_version'] = info.get('cmd_version') + if type(info["cmd"]) is str: + info["cmd"] = [info["cmd"]] + return cls(**info) + + def to_dict(self, *keys) -> dict: + if keys: + return {k: v for k, v in asdict(self).items() if k in keys} + return asdict(self) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, csv_col=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def duration(self) -> int: + return (self.end_ts - self.start_ts).seconds + +@dataclass(frozen=True) +class Link: + timestamp: str + url: str + title: Optional[str] + tags: Optional[str] + sources: List[str] + history: Dict[str, List[ArchiveResult]] = field(default_factory=lambda: {}) + updated: Optional[datetime] = None + schema: str = 'Link' + + + def __str__(self) -> str: + return f'[{self.timestamp}] {self.url} "{self.title}"' + + def __post_init__(self): + self.typecheck() + + def overwrite(self, **kwargs): + """pure functional version of dict.update that returns a new instance""" + return Link(**{**self._asdict(), **kwargs}) + + def __eq__(self, other): + if not isinstance(other, Link): + return NotImplemented + return self.url == other.url + + def __gt__(self, other): + if not isinstance(other, Link): + return NotImplemented + if not self.timestamp or not other.timestamp: + return + return float(self.timestamp) > float(other.timestamp) + + def typecheck(self) -> None: + from ..config import stderr, ANSI + try: + assert self.schema == self.__class__.__name__ + assert isinstance(self.timestamp, str) and self.timestamp + assert self.timestamp.replace('.', '').isdigit() + assert isinstance(self.url, str) and '://' in self.url + assert self.updated is None or isinstance(self.updated, datetime) + assert self.title is None or (isinstance(self.title, str) and self.title) + assert self.tags is None or isinstance(self.tags, str) + assert isinstance(self.sources, list) + assert all(isinstance(source, str) and source for source in self.sources) + assert isinstance(self.history, dict) + for method, results in self.history.items(): + assert isinstance(method, str) and method + assert isinstance(results, list) + assert all(isinstance(result, ArchiveResult) for result in results) + except Exception: + stderr('{red}[X] Error while loading link! [{}] {} "{}"{reset}'.format(self.timestamp, self.url, self.title, **ANSI)) + raise + + def _asdict(self, extended=False): + info = { + 'schema': 'Link', + 'url': self.url, + 'title': self.title or None, + 'timestamp': self.timestamp, + 'updated': self.updated or None, + 'tags': self.tags or None, + 'sources': self.sources or [], + 'history': self.history or {}, + } + if extended: + info.update({ + 'link_dir': self.link_dir, + 'archive_path': self.archive_path, + + 'hash': self.url_hash, + 'base_url': self.base_url, + 'scheme': self.scheme, + 'domain': self.domain, + 'path': self.path, + 'basename': self.basename, + 'extension': self.extension, + 'is_static': self.is_static, + + 'bookmarked_date': self.bookmarked_date, + 'updated_date': self.updated_date, + 'oldest_archive_date': self.oldest_archive_date, + 'newest_archive_date': self.newest_archive_date, + + 'is_archived': self.is_archived, + 'num_outputs': self.num_outputs, + 'num_failures': self.num_failures, + + 'latest': self.latest_outputs(), + 'canonical': self.canonical_outputs(), + }) + return info + + def as_snapshot(self): + from core.models import Snapshot + return Snapshot.objects.get(url=self.url) + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + info['updated'] = parse_date(info.get('updated')) + info['sources'] = info.get('sources') or [] + + json_history = info.get('history') or {} + cast_history = {} + + for method, method_history in json_history.items(): + cast_history[method] = [] + for json_result in method_history: + assert isinstance(json_result, dict), 'Items in Link["history"][method] must be dicts' + cast_result = ArchiveResult.from_json(json_result, guess) + cast_history[method].append(cast_result) + + info['history'] = cast_history + return cls(**info) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, cols=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def link_dir(self) -> str: + from ..config import CONFIG + return str(Path(CONFIG['ARCHIVE_DIR']) / self.timestamp) + + @property + def archive_path(self) -> str: + from ..config import ARCHIVE_DIR_NAME + return '{}/{}'.format(ARCHIVE_DIR_NAME, self.timestamp) + + @property + def archive_size(self) -> float: + try: + return get_dir_size(self.archive_path)[0] + except Exception: + return 0 + + ### URL Helpers + @property + def url_hash(self): + from ..util import hashurl + + return hashurl(self.url) + + @property + def scheme(self) -> str: + from ..util import scheme + return scheme(self.url) + + @property + def extension(self) -> str: + from ..util import extension + return extension(self.url) + + @property + def domain(self) -> str: + from ..util import domain + return domain(self.url) + + @property + def path(self) -> str: + from ..util import path + return path(self.url) + + @property + def basename(self) -> str: + from ..util import basename + return basename(self.url) + + @property + def base_url(self) -> str: + from ..util import base_url + return base_url(self.url) + + ### Pretty Printing Helpers + @property + def bookmarked_date(self) -> Optional[str]: + from ..util import ts_to_date + + max_ts = (datetime.now() + timedelta(days=30)).timestamp() + + if self.timestamp and self.timestamp.replace('.', '').isdigit(): + if 0 < float(self.timestamp) < max_ts: + return ts_to_date(datetime.fromtimestamp(float(self.timestamp))) + else: + return str(self.timestamp) + return None + + + @property + def updated_date(self) -> Optional[str]: + from ..util import ts_to_date + return ts_to_date(self.updated) if self.updated else None + + @property + def archive_dates(self) -> List[datetime]: + return [ + result.start_ts + for method in self.history.keys() + for result in self.history[method] + ] + + @property + def oldest_archive_date(self) -> Optional[datetime]: + return min(self.archive_dates, default=None) + + @property + def newest_archive_date(self) -> Optional[datetime]: + return max(self.archive_dates, default=None) + + ### Archive Status Helpers + @property + def num_outputs(self) -> int: + return self.as_snapshot().num_outputs + + @property + def num_failures(self) -> int: + return sum(1 + for method in self.history.keys() + for result in self.history[method] + if result.status == 'failed') + + @property + def is_static(self) -> bool: + from ..util import is_static_file + return is_static_file(self.url) + + @property + def is_archived(self) -> bool: + from ..config import ARCHIVE_DIR + from ..util import domain + + output_paths = ( + domain(self.url), + 'output.pdf', + 'screenshot.png', + 'output.html', + 'media', + 'singlefile.html' + ) + + return any( + (Path(ARCHIVE_DIR) / self.timestamp / path).exists() + for path in output_paths + ) + + def latest_outputs(self, status: str=None) -> Dict[str, ArchiveOutput]: + """get the latest output that each archive method produced for link""" + + ARCHIVE_METHODS = ( + 'title', 'favicon', 'wget', 'warc', 'singlefile', 'pdf', + 'screenshot', 'dom', 'git', 'media', 'archive_org', + ) + latest: Dict[str, ArchiveOutput] = {} + for archive_method in ARCHIVE_METHODS: + # get most recent succesful result in history for each archive method + history = self.history.get(archive_method) or [] + history = list(filter(lambda result: result.output, reversed(history))) + if status is not None: + history = list(filter(lambda result: result.status == status, history)) + + history = list(history) + if history: + latest[archive_method] = history[0].output + else: + latest[archive_method] = None + return latest + + + def canonical_outputs(self) -> Dict[str, Optional[str]]: + """predict the expected output paths that should be present after archiving""" + + from ..extractors.wget import wget_output_path + canonical = { + 'index_path': 'index.html', + 'favicon_path': 'favicon.ico', + 'google_favicon_path': 'https://www.google.com/s2/favicons?domain={}'.format(self.domain), + 'wget_path': wget_output_path(self), + 'warc_path': 'warc', + 'singlefile_path': 'singlefile.html', + 'readability_path': 'readability/content.html', + 'mercury_path': 'mercury/content.html', + 'pdf_path': 'output.pdf', + 'screenshot_path': 'screenshot.png', + 'dom_path': 'output.html', + 'archive_org_path': 'https://web.archive.org/web/{}'.format(self.base_url), + 'git_path': 'git', + 'media_path': 'media', + } + if self.is_static: + # static binary files like PDF and images are handled slightly differently. + # they're just downloaded once and aren't archived separately multiple times, + # so the wget, screenshot, & pdf urls should all point to the same file + + static_path = wget_output_path(self) + canonical.update({ + 'title': self.basename, + 'wget_path': static_path, + 'pdf_path': static_path, + 'screenshot_path': static_path, + 'dom_path': static_path, + 'singlefile_path': static_path, + 'readability_path': static_path, + 'mercury_path': static_path, + }) + return canonical + diff --git a/archivebox-0.5.3/build/lib/archivebox/index/sql.py b/archivebox-0.5.3/build/lib/archivebox/index/sql.py new file mode 100644 index 0000000..1e99f67 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/index/sql.py @@ -0,0 +1,106 @@ +__package__ = 'archivebox.index' + +from io import StringIO +from pathlib import Path +from typing import List, Tuple, Iterator +from django.db.models import QuerySet +from django.db import transaction + +from .schema import Link +from ..util import enforce_types +from ..config import OUTPUT_DIR + + +### Main Links Index + +@enforce_types +def parse_sql_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + from core.models import Snapshot + + return ( + Link.from_json(page.as_json(*Snapshot.keys)) + for page in Snapshot.objects.all() + ) + +@enforce_types +def remove_from_sql_main_index(snapshots: QuerySet, out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + snapshots.delete() + +@enforce_types +def write_link_to_sql_index(link: Link): + from core.models import Snapshot + info = {k: v for k, v in link._asdict().items() if k in Snapshot.keys} + tags = info.pop("tags") + if tags is None: + tags = [] + + try: + info["timestamp"] = Snapshot.objects.get(url=link.url).timestamp + except Snapshot.DoesNotExist: + while Snapshot.objects.filter(timestamp=info["timestamp"]).exists(): + info["timestamp"] = str(float(info["timestamp"]) + 1.0) + + snapshot, _ = Snapshot.objects.update_or_create(url=link.url, defaults=info) + snapshot.save_tags(tags) + return snapshot + + +@enforce_types +def write_sql_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + for link in links: + write_link_to_sql_index(link) + + +@enforce_types +def write_sql_link_details(link: Link, out_dir: Path=OUTPUT_DIR) -> None: + from core.models import Snapshot + + with transaction.atomic(): + try: + snap = Snapshot.objects.get(url=link.url) + except Snapshot.DoesNotExist: + snap = write_link_to_sql_index(link) + snap.title = link.title + + tag_set = ( + set(tag.strip() for tag in (link.tags or '').split(',')) + ) + tag_list = list(tag_set) or [] + + snap.save() + snap.save_tags(tag_list) + + + +@enforce_types +def list_migrations(out_dir: Path=OUTPUT_DIR) -> List[Tuple[bool, str]]: + from django.core.management import call_command + out = StringIO() + call_command("showmigrations", list=True, stdout=out) + out.seek(0) + migrations = [] + for line in out.readlines(): + if line.strip() and ']' in line: + status_str, name_str = line.strip().split(']', 1) + is_applied = 'X' in status_str + migration_name = name_str.strip() + migrations.append((is_applied, migration_name)) + + return migrations + +@enforce_types +def apply_migrations(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.core.management import call_command + null, out = StringIO(), StringIO() + call_command("makemigrations", interactive=False, stdout=null) + call_command("migrate", interactive=False, stdout=out) + out.seek(0) + + return [line.strip() for line in out.readlines() if line.strip()] + +@enforce_types +def get_admins(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.contrib.auth.models import User + return User.objects.filter(is_superuser=True) diff --git a/archivebox-0.5.3/build/lib/archivebox/logging_util.py b/archivebox-0.5.3/build/lib/archivebox/logging_util.py new file mode 100644 index 0000000..f2b8673 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/logging_util.py @@ -0,0 +1,569 @@ +__package__ = 'archivebox' + +import re +import os +import sys +import time +import argparse +from math import log +from multiprocessing import Process +from pathlib import Path + +from datetime import datetime +from dataclasses import dataclass +from typing import Optional, List, Dict, Union, IO, TYPE_CHECKING + +if TYPE_CHECKING: + from .index.schema import Link, ArchiveResult + +from .util import enforce_types +from .config import ( + ConfigDict, + OUTPUT_DIR, + PYTHON_ENCODING, + ANSI, + IS_TTY, + TERM_WIDTH, + SHOW_PROGRESS, + SOURCES_DIR_NAME, + stderr, +) + +@dataclass +class RuntimeStats: + """mutable stats counter for logging archiving timing info to CLI output""" + + skipped: int = 0 + succeeded: int = 0 + failed: int = 0 + + parse_start_ts: Optional[datetime] = None + parse_end_ts: Optional[datetime] = None + + index_start_ts: Optional[datetime] = None + index_end_ts: Optional[datetime] = None + + archiving_start_ts: Optional[datetime] = None + archiving_end_ts: Optional[datetime] = None + +# globals are bad, mmkay +_LAST_RUN_STATS = RuntimeStats() + + + +class SmartFormatter(argparse.HelpFormatter): + """Patched formatter that prints newlines in argparse help strings""" + def _split_lines(self, text, width): + if '\n' in text: + return text.splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) + + +def reject_stdin(caller: str, stdin: Optional[IO]=sys.stdin) -> None: + """Tell the user they passed stdin to a command that doesn't accept it""" + + if stdin and not stdin.isatty(): + stdin_raw_text = stdin.read().strip() + if stdin_raw_text: + stderr(f'[X] The "{caller}" command does not accept stdin.', color='red') + stderr(f' Run archivebox "{caller} --help" to see usage and examples.') + stderr() + raise SystemExit(1) + + +def accept_stdin(stdin: Optional[IO]=sys.stdin) -> Optional[str]: + """accept any standard input and return it as a string or None""" + if not stdin: + return None + elif stdin and not stdin.isatty(): + stdin_str = stdin.read().strip() + return stdin_str or None + return None + + +class TimedProgress: + """Show a progress bar and measure elapsed time until .end() is called""" + + def __init__(self, seconds, prefix=''): + self.SHOW_PROGRESS = SHOW_PROGRESS + if self.SHOW_PROGRESS: + self.p = Process(target=progress_bar, args=(seconds, prefix)) + self.p.start() + + self.stats = {'start_ts': datetime.now(), 'end_ts': None} + + def end(self): + """immediately end progress, clear the progressbar line, and save end_ts""" + + end_ts = datetime.now() + self.stats['end_ts'] = end_ts + + if self.SHOW_PROGRESS: + # terminate if we havent already terminated + try: + # kill the progress bar subprocess + try: + self.p.close() # must be closed *before* its terminnated + except: + pass + self.p.terminate() + self.p.join() + + + # clear whole terminal line + try: + sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + except (IOError, BrokenPipeError): + # ignore when the parent proc has stopped listening to our stdout + pass + except ValueError: + pass + + +@enforce_types +def progress_bar(seconds: int, prefix: str='') -> None: + """show timer in the form of progress bar, with percentage and seconds remaining""" + chunk = '█' if PYTHON_ENCODING == 'UTF-8' else '#' + last_width = TERM_WIDTH() + chunks = last_width - len(prefix) - 20 # number of progress chunks to show (aka max bar width) + try: + for s in range(seconds * chunks): + max_width = TERM_WIDTH() + if max_width < last_width: + # when the terminal size is shrunk, we have to write a newline + # otherwise the progress bar will keep wrapping incorrectly + sys.stdout.write('\r\n') + sys.stdout.flush() + chunks = max_width - len(prefix) - 20 + pct_complete = s / chunks / seconds * 100 + log_pct = (log(pct_complete or 1, 10) / 2) * 100 # everyone likes faster progress bars ;) + bar_width = round(log_pct/(100/chunks)) + last_width = max_width + + # ████████████████████ 0.9% (1/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['green' if pct_complete < 80 else 'lightyellow'], + (chunk * bar_width).ljust(chunks), + ANSI['reset'], + round(pct_complete, 1), + round(s/chunks), + seconds, + )) + sys.stdout.flush() + time.sleep(1 / chunks) + + # ██████████████████████████████████ 100.0% (60/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['red'], + chunk * chunks, + ANSI['reset'], + 100.0, + seconds, + seconds, + )) + sys.stdout.flush() + # uncomment to have it disappear when it hits 100% instead of staying full red: + # time.sleep(0.5) + # sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + # sys.stdout.flush() + except (KeyboardInterrupt, BrokenPipeError): + print() + pass + + +def log_cli_command(subcommand: str, subcommand_args: List[str], stdin: Optional[str], pwd: str): + from .config import VERSION, ANSI + cmd = ' '.join(('archivebox', subcommand, *subcommand_args)) + stderr('{black}[i] [{now}] ArchiveBox v{VERSION}: {cmd}{reset}'.format( + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + VERSION=VERSION, + cmd=cmd, + **ANSI, + )) + stderr('{black} > {pwd}{reset}'.format(pwd=pwd, **ANSI)) + stderr() + +### Parsing Stage + + +def log_importing_started(urls: Union[str, List[str]], depth: int, index_only: bool): + _LAST_RUN_STATS.parse_start_ts = datetime.now() + print('{green}[+] [{}] Adding {} links to index (crawl depth={}){}...{reset}'.format( + _LAST_RUN_STATS.parse_start_ts.strftime('%Y-%m-%d %H:%M:%S'), + len(urls) if isinstance(urls, list) else len(urls.split('\n')), + depth, + ' (index only)' if index_only else '', + **ANSI, + )) + +def log_source_saved(source_file: str): + print(' > Saved verbatim input to {}/{}'.format(SOURCES_DIR_NAME, source_file.rsplit('/', 1)[-1])) + +def log_parsing_finished(num_parsed: int, parser_name: str): + _LAST_RUN_STATS.parse_end_ts = datetime.now() + print(' > Parsed {} URLs from input ({})'.format(num_parsed, parser_name)) + +def log_deduping_finished(num_new_links: int): + print(' > Found {} new URLs not already in index'.format(num_new_links)) + + +def log_crawl_started(new_links): + print() + print('{green}[*] Starting crawl of {} sites 1 hop out from starting point{reset}'.format(len(new_links), **ANSI)) + +### Indexing Stage + +def log_indexing_process_started(num_links: int): + start_ts = datetime.now() + _LAST_RUN_STATS.index_start_ts = start_ts + print() + print('{black}[*] [{}] Writing {} links to main index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + + +def log_indexing_process_finished(): + end_ts = datetime.now() + _LAST_RUN_STATS.index_end_ts = end_ts + + +def log_indexing_started(out_path: str): + if IS_TTY: + sys.stdout.write(f' > {out_path}') + + +def log_indexing_finished(out_path: str): + print(f'\r √ {out_path}') + + +### Archiving Stage + +def log_archiving_started(num_links: int, resume: Optional[float]=None): + start_ts = datetime.now() + _LAST_RUN_STATS.archiving_start_ts = start_ts + print() + if resume: + print('{green}[▶] [{}] Resuming archive updating for {} pages starting from {}...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + resume, + **ANSI, + )) + else: + print('{green}[▶] [{}] Starting archiving of {} snapshots in index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + +def log_archiving_paused(num_links: int, idx: int, timestamp: str): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + print() + print('\n{lightyellow}[X] [{now}] Downloading paused on link {timestamp} ({idx}/{total}){reset}'.format( + **ANSI, + now=end_ts.strftime('%Y-%m-%d %H:%M:%S'), + idx=idx+1, + timestamp=timestamp, + total=num_links, + )) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print(' Continue archiving where you left off by running:') + print(' archivebox update --resume={}'.format(timestamp)) + +def log_archiving_finished(num_links: int): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + assert _LAST_RUN_STATS.archiving_start_ts is not None + seconds = end_ts.timestamp() - _LAST_RUN_STATS.archiving_start_ts.timestamp() + if seconds > 60: + duration = '{0:.2f} min'.format(seconds / 60) + else: + duration = '{0:.2f} sec'.format(seconds) + + print() + print('{}[√] [{}] Update of {} pages complete ({}){}'.format( + ANSI['green'], + end_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + duration, + ANSI['reset'], + )) + print(' - {} links skipped'.format(_LAST_RUN_STATS.skipped)) + print(' - {} links updated'.format(_LAST_RUN_STATS.succeeded + _LAST_RUN_STATS.failed)) + print(' - {} links had errors'.format(_LAST_RUN_STATS.failed)) + print() + print(' {lightred}Hint:{reset} To manage your archive in a Web UI, run:'.format(**ANSI)) + print(' archivebox server 0.0.0.0:8000') + + +def log_link_archiving_started(link: "Link", link_dir: str, is_new: bool): + # [*] [2019-03-22 13:46:45] "Log Structured Merge Trees - ben stopford" + # http://www.benstopford.com/2015/02/14/log-structured-merge-trees/ + # > output/archive/1478739709 + + print('\n[{symbol_color}{symbol}{reset}] [{symbol_color}{now}{reset}] "{title}"'.format( + symbol_color=ANSI['green' if is_new else 'black'], + symbol='+' if is_new else '√', + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + title=link.title or link.base_url, + **ANSI, + )) + print(' {blue}{url}{reset}'.format(url=link.url, **ANSI)) + print(' {} {}'.format( + '>' if is_new else '√', + pretty_path(link_dir), + )) + +def log_link_archiving_finished(link: "Link", link_dir: str, is_new: bool, stats: dict): + total = sum(stats.values()) + + if stats['failed'] > 0 : + _LAST_RUN_STATS.failed += 1 + elif stats['skipped'] == total: + _LAST_RUN_STATS.skipped += 1 + else: + _LAST_RUN_STATS.succeeded += 1 + + +def log_archive_method_started(method: str): + print(' > {}'.format(method)) + + +def log_archive_method_finished(result: "ArchiveResult"): + """quote the argument with whitespace in a command so the user can + copy-paste the outputted string directly to run the cmd + """ + # Prettify CMD string and make it safe to copy-paste by quoting arguments + quoted_cmd = ' '.join( + '"{}"'.format(arg) if ' ' in arg else arg + for arg in result.cmd + ) + + if result.status == 'failed': + if result.output.__class__.__name__ == 'TimeoutExpired': + duration = (result.end_ts - result.start_ts).seconds + hint_header = [ + '{lightyellow}Extractor timed out after {}s.{reset}'.format(duration, **ANSI), + ] + else: + hint_header = [ + '{lightyellow}Extractor failed:{reset}'.format(**ANSI), + ' {reset}{} {red}{}{reset}'.format( + result.output.__class__.__name__.replace('ArchiveError', ''), + result.output, + **ANSI, + ), + ] + + # Prettify error output hints string and limit to five lines + hints = getattr(result.output, 'hints', None) or () + if hints: + hints = hints if isinstance(hints, (list, tuple)) else hints.split('\n') + hints = ( + ' {}{}{}'.format(ANSI['lightyellow'], line.strip(), ANSI['reset']) + for line in hints[:5] if line.strip() + ) + + + # Collect and prefix output lines with indentation + output_lines = [ + *hint_header, + *hints, + '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), + *([' cd {};'.format(result.pwd)] if result.pwd else []), + ' {}'.format(quoted_cmd), + ] + print('\n'.join( + ' {}'.format(line) + for line in output_lines + if line + )) + print() + + +def log_list_started(filter_patterns: Optional[List[str]], filter_type: str): + print('{green}[*] Finding links in the archive index matching these {} patterns:{reset}'.format( + filter_type, + **ANSI, + )) + print(' {}'.format(' '.join(filter_patterns or ()))) + +def log_list_finished(links): + from .index.csv import links_to_csv + print() + print('---------------------------------------------------------------------------------------------------') + print(links_to_csv(links, cols=['timestamp', 'is_archived', 'num_outputs', 'url'], header=True, ljust=16, separator=' | ')) + print('---------------------------------------------------------------------------------------------------') + print() + + +def log_removal_started(links: List["Link"], yes: bool, delete: bool): + print('{lightyellow}[i] Found {} matching URLs to remove.{reset}'.format(len(links), **ANSI)) + if delete: + file_counts = [link.num_outputs for link in links if Path(link.link_dir).exists()] + print( + f' {len(links)} Links will be de-listed from the main index, and their archived content folders will be deleted from disk.\n' + f' ({len(file_counts)} data folders with {sum(file_counts)} archived files will be deleted!)' + ) + else: + print( + ' Matching links will be de-listed from the main index, but their archived content folders will remain in place on disk.\n' + ' (Pass --delete if you also want to permanently delete the data folders)' + ) + + if not yes: + print() + print('{lightyellow}[?] Do you want to proceed with removing these {} links?{reset}'.format(len(links), **ANSI)) + try: + assert input(' y/[n]: ').lower() == 'y' + except (KeyboardInterrupt, EOFError, AssertionError): + raise SystemExit(0) + +def log_removal_finished(all_links: int, to_remove: int): + if all_links == 0: + print() + print('{red}[X] No matching links found.{reset}'.format(**ANSI)) + else: + print() + print('{red}[√] Removed {} out of {} links from the archive index.{reset}'.format( + to_remove, + all_links, + **ANSI, + )) + print(' Index now contains {} links.'.format(all_links - to_remove)) + + +def log_shell_welcome_msg(): + from .cli import list_subcommands + + print('{green}# ArchiveBox Imports{reset}'.format(**ANSI)) + print('{green}from core.models import Snapshot, User{reset}'.format(**ANSI)) + print('{green}from archivebox import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) + print() + print('[i] Welcome to the ArchiveBox Shell!') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Shell-Usage') + print() + print(' {lightred}Hint:{reset} Example use:'.format(**ANSI)) + print(' print(Snapshot.objects.filter(is_archived=True).count())') + print(' Snapshot.objects.get(url="https://example.com").as_json()') + print(' add("https://example.com/some/new/url")') + + + +### Helpers + +@enforce_types +def pretty_path(path: Union[Path, str]) -> str: + """convert paths like .../ArchiveBox/archivebox/../output/abc into output/abc""" + pwd = Path('.').resolve() + # parent = os.path.abspath(os.path.join(pwd, os.path.pardir)) + return str(path).replace(str(pwd) + '/', './') + + +@enforce_types +def printable_filesize(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + + +@enforce_types +def printable_folders(folders: Dict[str, Optional["Link"]], + with_headers: bool=False) -> str: + return '\n'.join( + f'{folder} {link and link.url} "{link and link.title}"' + for folder, link in folders.items() + ) + + + +@enforce_types +def printable_config(config: ConfigDict, prefix: str='') -> str: + return f'\n{prefix}'.join( + f'{key}={val}' + for key, val in config.items() + if not (isinstance(val, dict) or callable(val)) + ) + + +@enforce_types +def printable_folder_status(name: str, folder: Dict) -> str: + if folder['enabled']: + if folder['is_valid']: + color, symbol, note = 'green', '√', 'valid' + else: + color, symbol, note, num_files = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, num_files = 'lightyellow', '-', 'disabled', '-' + + if folder['path']: + if Path(folder['path']).exists(): + num_files = ( + f'{len(os.listdir(folder["path"]))} files' + if Path(folder['path']).is_dir() else + printable_filesize(Path(folder['path']).stat().st_size) + ) + else: + num_files = 'missing' + + path = str(folder['path']).replace(str(OUTPUT_DIR), '.') if folder['path'] else '' + if path and ' ' in path: + path = f'"{path}"' + + # if path is just a plain dot, replace it back with the full path for clarity + if path == '.': + path = str(OUTPUT_DIR) + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + num_files.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) + + +@enforce_types +def printable_dependency_version(name: str, dependency: Dict) -> str: + version = None + if dependency['enabled']: + if dependency['is_valid']: + color, symbol, note, version = 'green', '√', 'valid', '' + + parsed_version_num = re.search(r'[\d\.]+', dependency['version']) + if parsed_version_num: + version = f'v{parsed_version_num[0]}' + + if not version: + color, symbol, note, version = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, version = 'lightyellow', '-', 'disabled', '-' + + path = str(dependency["path"]).replace(str(OUTPUT_DIR), '.') if dependency["path"] else '' + if path and ' ' in path: + path = f'"{path}"' + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + version.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) diff --git a/archivebox-0.5.3/build/lib/archivebox/main.py b/archivebox-0.5.3/build/lib/archivebox/main.py new file mode 100644 index 0000000..eb8cd6a --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/main.py @@ -0,0 +1,1131 @@ +__package__ = 'archivebox' + +import os +import sys +import shutil +import platform +from pathlib import Path +from datetime import date + +from typing import Dict, List, Optional, Iterable, IO, Union +from crontab import CronTab, CronSlices +from django.db.models import QuerySet + +from .cli import ( + list_subcommands, + run_subcommand, + display_first, + meta_cmds, + main_cmds, + archive_cmds, +) +from .parsers import ( + save_text_as_source, + save_file_as_source, + parse_links_memory, +) +from .index.schema import Link +from .util import enforce_types # type: ignore +from .system import get_dir_size, dedupe_cron_jobs, CRON_COMMENT +from .index import ( + load_main_index, + parse_links_from_source, + dedupe_links, + write_main_index, + snapshot_filter, + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, + fix_invalid_folder_locations, + write_link_details, +) +from .index.json import ( + parse_json_main_index, + parse_json_links_details, + generate_json_index_from_links, +) +from .index.sql import ( + get_admins, + apply_migrations, + remove_from_sql_main_index, +) +from .index.html import ( + generate_index_from_links, +) +from .index.csv import links_to_csv +from .extractors import archive_links, archive_link, ignore_methods +from .config import ( + stderr, + hint, + ConfigDict, + ANSI, + IS_TTY, + IN_DOCKER, + USER, + ARCHIVEBOX_BINARY, + ONLY_NEW, + OUTPUT_DIR, + SOURCES_DIR, + ARCHIVE_DIR, + LOGS_DIR, + CONFIG_FILE, + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + SQL_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, + check_dependencies, + check_data_folder, + write_config_file, + VERSION, + CODE_LOCATIONS, + EXTERNAL_LOCATIONS, + DATA_LOCATIONS, + DEPENDENCIES, + load_all_config, + CONFIG, + USER_CONFIG, + get_real_name, +) +from .logging_util import ( + TERM_WIDTH, + TimedProgress, + log_importing_started, + log_crawl_started, + log_removal_started, + log_removal_finished, + log_list_started, + log_list_finished, + printable_config, + printable_folders, + printable_filesize, + printable_folder_status, + printable_dependency_version, +) + +from .search import flush_search_index, index_links + +ALLOWED_IN_OUTPUT_DIR = { + 'lost+found', + '.DS_Store', + '.venv', + 'venv', + 'virtualenv', + '.virtualenv', + 'node_modules', + 'package-lock.json', + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, +} + +@enforce_types +def help(out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox help message and usage""" + + all_subcommands = list_subcommands() + COMMANDS_HELP_TEXT = '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in meta_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in main_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in archive_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd not in display_first + ) + + + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('''{green}ArchiveBox v{}: The self-hosted internet archive.{reset} + +{lightred}Active data directory:{reset} + {} + +{lightred}Usage:{reset} + archivebox [command] [--help] [--version] [...args] + +{lightred}Commands:{reset} + {} + +{lightred}Example Use:{reset} + mkdir my-archive; cd my-archive/ + archivebox init + archivebox status + + archivebox add https://example.com/some/page + archivebox add --depth=1 ~/Downloads/bookmarks_export.html + + archivebox list --sort=timestamp --csv=timestamp,url,is_archived + archivebox schedule --every=day https://example.com/some/feed.rss + archivebox update --resume=15109948213.123 + +{lightred}Documentation:{reset} + https://github.com/ArchiveBox/ArchiveBox/wiki +'''.format(VERSION, out_dir, COMMANDS_HELP_TEXT, **ANSI)) + + else: + print('{green}Welcome to ArchiveBox v{}!{reset}'.format(VERSION, **ANSI)) + print() + if IN_DOCKER: + print('When using Docker, you need to mount a volume to use as your data dir:') + print(' docker run -v /some/path:/data archivebox ...') + print() + print('To import an existing archive (from a previous version of ArchiveBox):') + print(' 1. cd into your data dir OUTPUT_DIR (usually ArchiveBox/output) and run:') + print(' 2. archivebox init') + print() + print('To start a new archive:') + print(' 1. Create an empty directory, then cd into it and run:') + print(' 2. archivebox init') + print() + print('For more information, see the documentation here:') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki') + + +@enforce_types +def version(quiet: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox version and dependency information""" + + if quiet: + print(VERSION) + else: + print('ArchiveBox v{}'.format(VERSION)) + p = platform.uname() + print(sys.implementation.name.title(), p.system, platform.platform(), p.machine, '(in Docker)' if IN_DOCKER else '(not in Docker)') + print() + + print('{white}[i] Dependency versions:{reset}'.format(**ANSI)) + for name, dependency in DEPENDENCIES.items(): + print(printable_dependency_version(name, dependency)) + + print() + print('{white}[i] Source-code locations:{reset}'.format(**ANSI)) + for name, folder in CODE_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + print('{white}[i] Secrets locations:{reset}'.format(**ANSI)) + for name, folder in EXTERNAL_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + if DATA_LOCATIONS['OUTPUT_DIR']['is_valid']: + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + for name, folder in DATA_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + else: + print() + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + + print() + check_dependencies() + + +@enforce_types +def run(subcommand: str, + subcommand_args: Optional[List[str]], + stdin: Optional[IO]=None, + out_dir: Path=OUTPUT_DIR) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + run_subcommand( + subcommand=subcommand, + subcommand_args=subcommand_args, + stdin=stdin, + pwd=out_dir, + ) + + +@enforce_types +def init(force: bool=False, out_dir: Path=OUTPUT_DIR) -> None: + """Initialize a new ArchiveBox collection in the current directory""" + from core.models import Snapshot + Path(out_dir).mkdir(exist_ok=True) + is_empty = not len(set(os.listdir(out_dir)) - ALLOWED_IN_OUTPUT_DIR) + + if (Path(out_dir) / JSON_INDEX_FILENAME).exists(): + stderr("[!] This folder contains a JSON index. It is deprecated, and will no longer be kept up to date automatically.", color="lightyellow") + stderr(" You can run `archivebox list --json --with-headers > index.json` to manually generate it.", color="lightyellow") + + existing_index = (Path(out_dir) / SQL_INDEX_FILENAME).exists() + + if is_empty and not existing_index: + print('{green}[+] Initializing a new ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + elif existing_index: + print('{green}[*] Updating existing ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + else: + if force: + stderr('[!] This folder appears to already have files in it, but no index.sqlite3 is present.', color='lightyellow') + stderr(' Because --force was passed, ArchiveBox will initialize anyway (which may overwrite existing files).') + else: + stderr( + ("{red}[X] This folder appears to already have files in it, but no index.sqlite3 present.{reset}\n\n" + " You must run init in a completely empty directory, or an existing data folder.\n\n" + " {lightred}Hint:{reset} To import an existing data folder make sure to cd into the folder first, \n" + " then run and run 'archivebox init' to pick up where you left off.\n\n" + " (Always make sure your data folder is backed up first before updating ArchiveBox)" + ).format(out_dir, **ANSI) + ) + raise SystemExit(2) + + if existing_index: + print('\n{green}[*] Verifying archive folder structure...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building archive folder structure...{reset}'.format(**ANSI)) + + Path(SOURCES_DIR).mkdir(exist_ok=True) + print(f' √ {SOURCES_DIR}') + + Path(ARCHIVE_DIR).mkdir(exist_ok=True) + print(f' √ {ARCHIVE_DIR}') + + Path(LOGS_DIR).mkdir(exist_ok=True) + print(f' √ {LOGS_DIR}') + + write_config_file({}, out_dir=out_dir) + print(f' √ {CONFIG_FILE}') + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('\n{green}[*] Verifying main SQL index and running migrations...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building main SQL index and running migrations...{reset}'.format(**ANSI)) + + DATABASE_FILE = Path(out_dir) / SQL_INDEX_FILENAME + print(f' √ {DATABASE_FILE}') + print() + for migration_line in apply_migrations(out_dir): + print(f' {migration_line}') + + + assert DATABASE_FILE.exists() + + # from django.contrib.auth.models import User + # if IS_TTY and not User.objects.filter(is_superuser=True).exists(): + # print('{green}[+] Creating admin user account...{reset}'.format(**ANSI)) + # call_command("createsuperuser", interactive=True) + + print() + print('{green}[*] Collecting links from any existing indexes and archive folders...{reset}'.format(**ANSI)) + + all_links = Snapshot.objects.none() + pending_links: Dict[str, Link] = {} + + if existing_index: + all_links = load_main_index(out_dir=out_dir, warn=False) + print(' √ Loaded {} links from existing main index.'.format(all_links.count())) + + # Links in data folders that dont match their timestamp + fixed, cant_fix = fix_invalid_folder_locations(out_dir=out_dir) + if fixed: + print(' {lightyellow}√ Fixed {} data directory locations that didn\'t match their link timestamps.{reset}'.format(len(fixed), **ANSI)) + if cant_fix: + print(' {lightyellow}! Could not fix {} data directory locations due to conflicts with existing folders.{reset}'.format(len(cant_fix), **ANSI)) + + # Links in JSON index but not in main index + orphaned_json_links = { + link.url: link + for link in parse_json_main_index(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_json_links: + pending_links.update(orphaned_json_links) + print(' {lightyellow}√ Added {} orphaned links from existing JSON index...{reset}'.format(len(orphaned_json_links), **ANSI)) + + # Links in data dir indexes but not in main index + orphaned_data_dir_links = { + link.url: link + for link in parse_json_links_details(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_data_dir_links: + pending_links.update(orphaned_data_dir_links) + print(' {lightyellow}√ Added {} orphaned links from existing archive directories.{reset}'.format(len(orphaned_data_dir_links), **ANSI)) + + # Links in invalid/duplicate data dirs + invalid_folders = { + folder: link + for folder, link in get_invalid_folders(all_links, out_dir=out_dir).items() + } + if invalid_folders: + print(' {lightyellow}! Skipped adding {} invalid link data directories.{reset}'.format(len(invalid_folders), **ANSI)) + print(' X ' + '\n X '.join(f'{folder} {link}' for folder, link in invalid_folders.items())) + print() + print(' {lightred}Hint:{reset} For more information about the link data directories that were skipped, run:'.format(**ANSI)) + print(' archivebox status') + print(' archivebox list --status=invalid') + + + write_main_index(list(pending_links.values()), out_dir=out_dir) + + print('\n{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + if existing_index: + print('{green}[√] Done. Verified and updated the existing ArchiveBox collection.{reset}'.format(**ANSI)) + else: + print('{green}[√] Done. A new ArchiveBox collection was initialized ({} links).{reset}'.format(len(all_links), **ANSI)) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print() + print(' To add new links, you can run:') + print(" archivebox add ~/some/path/or/url/to/list_of_links.txt") + print() + print(' For more usage and examples, run:') + print(' archivebox help') + + json_index = Path(out_dir) / JSON_INDEX_FILENAME + html_index = Path(out_dir) / HTML_INDEX_FILENAME + index_name = f"{date.today()}_index_old" + if json_index.exists(): + json_index.rename(f"{index_name}.json") + if html_index.exists(): + html_index.rename(f"{index_name}.html") + + + +@enforce_types +def status(out_dir: Path=OUTPUT_DIR) -> None: + """Print out some info and statistics about the archive collection""" + + check_data_folder(out_dir=out_dir) + + from core.models import Snapshot + from django.contrib.auth import get_user_model + User = get_user_model() + + print('{green}[*] Scanning archive main index...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {out_dir}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(out_dir, recursive=False, pattern='index.') + size = printable_filesize(num_bytes) + print(f' Index size: {size} across {num_files} files') + print() + + links = load_main_index(out_dir=out_dir) + num_sql_links = links.count() + num_link_details = sum(1 for link in parse_json_links_details(out_dir=out_dir)) + print(f' > SQL Main Index: {num_sql_links} links'.ljust(36), f'(found in {SQL_INDEX_FILENAME})') + print(f' > JSON Link Details: {num_link_details} links'.ljust(36), f'(found in {ARCHIVE_DIR_NAME}/*/index.json)') + print() + print('{green}[*] Scanning archive data directories...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {ARCHIVE_DIR}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(ARCHIVE_DIR) + size = printable_filesize(num_bytes) + print(f' Size: {size} across {num_files} files in {num_dirs} directories') + print(ANSI['black']) + num_indexed = len(get_indexed_folders(links, out_dir=out_dir)) + num_archived = len(get_archived_folders(links, out_dir=out_dir)) + num_unarchived = len(get_unarchived_folders(links, out_dir=out_dir)) + print(f' > indexed: {num_indexed}'.ljust(36), f'({get_indexed_folders.__doc__})') + print(f' > archived: {num_archived}'.ljust(36), f'({get_archived_folders.__doc__})') + print(f' > unarchived: {num_unarchived}'.ljust(36), f'({get_unarchived_folders.__doc__})') + + num_present = len(get_present_folders(links, out_dir=out_dir)) + num_valid = len(get_valid_folders(links, out_dir=out_dir)) + print() + print(f' > present: {num_present}'.ljust(36), f'({get_present_folders.__doc__})') + print(f' > valid: {num_valid}'.ljust(36), f'({get_valid_folders.__doc__})') + + duplicate = get_duplicate_folders(links, out_dir=out_dir) + orphaned = get_orphaned_folders(links, out_dir=out_dir) + corrupted = get_corrupted_folders(links, out_dir=out_dir) + unrecognized = get_unrecognized_folders(links, out_dir=out_dir) + num_invalid = len({**duplicate, **orphaned, **corrupted, **unrecognized}) + print(f' > invalid: {num_invalid}'.ljust(36), f'({get_invalid_folders.__doc__})') + print(f' > duplicate: {len(duplicate)}'.ljust(36), f'({get_duplicate_folders.__doc__})') + print(f' > orphaned: {len(orphaned)}'.ljust(36), f'({get_orphaned_folders.__doc__})') + print(f' > corrupted: {len(corrupted)}'.ljust(36), f'({get_corrupted_folders.__doc__})') + print(f' > unrecognized: {len(unrecognized)}'.ljust(36), f'({get_unrecognized_folders.__doc__})') + + print(ANSI['reset']) + + if num_indexed: + print(' {lightred}Hint:{reset} You can list link data directories by status like so:'.format(**ANSI)) + print(' archivebox list --status=<status> (e.g. indexed, corrupted, archived, etc.)') + + if orphaned: + print(' {lightred}Hint:{reset} To automatically import orphaned data directories into the main index, run:'.format(**ANSI)) + print(' archivebox init') + + if num_invalid: + print(' {lightred}Hint:{reset} You may need to manually remove or fix some invalid data directories, afterwards make sure to run:'.format(**ANSI)) + print(' archivebox init') + + print() + print('{green}[*] Scanning recent archive changes and user logins:{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {LOGS_DIR}/*', ANSI['reset']) + users = get_admins().values_list('username', flat=True) + print(f' UI users {len(users)}: {", ".join(users)}') + last_login = User.objects.order_by('last_login').last() + if last_login: + print(f' Last UI login: {last_login.username} @ {str(last_login.last_login)[:16]}') + last_updated = Snapshot.objects.order_by('updated').last() + if last_updated: + print(f' Last changes: {str(last_updated.updated)[:16]}') + + if not users: + print() + print(' {lightred}Hint:{reset} You can create an admin user by running:'.format(**ANSI)) + print(' archivebox manage createsuperuser') + + print() + for snapshot in links.order_by('-updated')[:10]: + if not snapshot.updated: + continue + print( + ANSI['black'], + ( + f' > {str(snapshot.updated)[:16]} ' + f'[{snapshot.num_outputs} {("X", "√")[snapshot.is_archived]} {printable_filesize(snapshot.archive_size)}] ' + f'"{snapshot.title}": {snapshot.url}' + )[:TERM_WIDTH()], + ANSI['reset'], + ) + print(ANSI['black'], ' ...', ANSI['reset']) + + +@enforce_types +def oneshot(url: str, extractors: str="", out_dir: Path=OUTPUT_DIR): + """ + Create a single URL archive folder with an index.json and index.html, and all the archive method outputs. + You can run this to archive single pages without needing to create a whole collection with archivebox init. + """ + oneshot_link, _ = parse_links_memory([url]) + if len(oneshot_link) > 1: + stderr( + '[X] You should pass a single url to the oneshot command', + color='red' + ) + raise SystemExit(2) + + methods = extractors.split(",") if extractors else ignore_methods(['title']) + archive_link(oneshot_link[0], out_dir=out_dir, methods=methods) + return oneshot_link + +@enforce_types +def add(urls: Union[str, List[str]], + depth: int=0, + update_all: bool=not ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + init: bool=False, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Add a new URL or list of URLs to your archive""" + + assert depth in (0, 1), 'Depth must be 0 or 1 (depth >1 is not supported yet)' + + extractors = extractors.split(",") if extractors else [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # Load list of links from the existing index + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] + all_links = load_main_index(out_dir=out_dir) + + log_importing_started(urls=urls, depth=depth, index_only=index_only) + if isinstance(urls, str): + # save verbatim stdin to sources + write_ahead_log = save_text_as_source(urls, filename='{ts}-import.txt', out_dir=out_dir) + elif isinstance(urls, list): + # save verbatim args to sources + write_ahead_log = save_text_as_source('\n'.join(urls), filename='{ts}-import.txt', out_dir=out_dir) + + new_links += parse_links_from_source(write_ahead_log, root_url=None) + + # If we're going one level deeper, download each link and look for more links + new_links_depth = [] + if new_links and depth == 1: + log_crawl_started(new_links) + for new_link in new_links: + downloaded_file = save_file_as_source(new_link.url, filename=f'{new_link.timestamp}-crawl-{new_link.domain}.txt', out_dir=out_dir) + new_links_depth += parse_links_from_source(downloaded_file, root_url=new_link.url) + + imported_links = list({link.url: link for link in (new_links + new_links_depth)}.values()) + new_links = dedupe_links(all_links, imported_links) + + write_main_index(links=new_links, out_dir=out_dir) + all_links = load_main_index(out_dir=out_dir) + + if index_only: + return all_links + + # Run the archive methods for each link + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + if update_all: + archive_links(all_links, overwrite=overwrite, **archive_kwargs) + elif overwrite: + archive_links(imported_links, overwrite=True, **archive_kwargs) + elif new_links: + archive_links(new_links, overwrite=False, **archive_kwargs) + + return all_links + +@enforce_types +def remove(filter_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + snapshots: Optional[QuerySet]=None, + after: Optional[float]=None, + before: Optional[float]=None, + yes: bool=False, + delete: bool=False, + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Remove the specified URLs from the archive""" + + check_data_folder(out_dir=out_dir) + + if snapshots is None: + if filter_str and filter_patterns: + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif not (filter_str or filter_patterns): + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin.', + color='red', + ) + stderr() + hint(('To remove all urls you can run:', + 'archivebox remove --filter-type=regex ".*"')) + stderr() + raise SystemExit(2) + elif filter_str: + filter_patterns = [ptn.strip() for ptn in filter_str.split('\n')] + + list_kwargs = { + "filter_patterns": filter_patterns, + "filter_type": filter_type, + "after": after, + "before": before, + } + if snapshots: + list_kwargs["snapshots"] = snapshots + + log_list_started(filter_patterns, filter_type) + timer = TimedProgress(360, prefix=' ') + try: + snapshots = list_links(**list_kwargs) + finally: + timer.end() + + + if not snapshots.exists(): + log_removal_finished(0, 0) + raise SystemExit(1) + + + log_links = [link.as_link() for link in snapshots] + log_list_finished(log_links) + log_removal_started(log_links, yes=yes, delete=delete) + + timer = TimedProgress(360, prefix=' ') + try: + for snapshot in snapshots: + if delete: + shutil.rmtree(snapshot.as_link().link_dir, ignore_errors=True) + finally: + timer.end() + + to_remove = snapshots.count() + + flush_search_index(snapshots=snapshots) + remove_from_sql_main_index(snapshots=snapshots, out_dir=out_dir) + all_snapshots = load_main_index(out_dir=out_dir) + log_removal_finished(all_snapshots.count(), to_remove) + + return all_snapshots + +@enforce_types +def update(resume: Optional[float]=None, + only_new: bool=ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: Optional[str]=None, + status: Optional[str]=None, + after: Optional[str]=None, + before: Optional[str]=None, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Import any new links from subscriptions and retry any previously failed/skipped links""" + + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] # TODO: Remove input argument: only_new + + extractors = extractors.split(",") if extractors else [] + + # Step 1: Filter for selected_links + matching_snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + matching_folders = list_folders( + links=matching_snapshots, + status=status, + out_dir=out_dir, + ) + all_links = [link for link in matching_folders.values() if link] + + if index_only: + for link in all_links: + write_link_details(link, out_dir=out_dir, skip_sql_index=True) + index_links(all_links, out_dir=out_dir) + return all_links + + # Step 2: Run the archive methods for each link + to_archive = new_links if only_new else all_links + if resume: + to_archive = [ + link for link in to_archive + if link.timestamp >= str(resume) + ] + if not to_archive: + stderr('') + stderr(f'[√] Nothing found to resume after {resume}', color='green') + return all_links + + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + + archive_links(to_archive, overwrite=overwrite, **archive_kwargs) + + # Step 4: Re-write links index with updated titles, icons, and resources + all_links = load_main_index(out_dir=out_dir) + return all_links + +@enforce_types +def list_all(filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + status: Optional[str]=None, + after: Optional[float]=None, + before: Optional[float]=None, + sort: Optional[str]=None, + csv: Optional[str]=None, + json: bool=False, + html: bool=False, + with_headers: bool=False, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + """List, filter, and export information about archive entries""" + + check_data_folder(out_dir=out_dir) + + if filter_patterns and filter_patterns_str: + stderr( + '[X] You should either pass filter patterns as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif filter_patterns_str: + filter_patterns = filter_patterns_str.split('\n') + + snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + if sort: + snapshots = snapshots.order_by(sort) + + folders = list_folders( + links=snapshots, + status=status, + out_dir=out_dir, + ) + + if json: + output = generate_json_index_from_links(folders.values(), with_headers) + elif html: + output = generate_index_from_links(folders.values(), with_headers) + elif csv: + output = links_to_csv(folders.values(), cols=csv.split(','), header=with_headers) + else: + output = printable_folders(folders, with_headers=with_headers) + print(output) + return folders + + +@enforce_types +def list_links(snapshots: Optional[QuerySet]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + after: Optional[float]=None, + before: Optional[float]=None, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + + check_data_folder(out_dir=out_dir) + + if snapshots: + all_snapshots = snapshots + else: + all_snapshots = load_main_index(out_dir=out_dir) + + if after is not None: + all_snapshots = all_snapshots.filter(timestamp__lt=after) + if before is not None: + all_snapshots = all_snapshots.filter(timestamp__gt=before) + if filter_patterns: + all_snapshots = snapshot_filter(all_snapshots, filter_patterns, filter_type) + return all_snapshots + +@enforce_types +def list_folders(links: List[Link], + status: str, + out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + + check_data_folder(out_dir=out_dir) + + STATUS_FUNCTIONS = { + "indexed": get_indexed_folders, + "archived": get_archived_folders, + "unarchived": get_unarchived_folders, + "present": get_present_folders, + "valid": get_valid_folders, + "invalid": get_invalid_folders, + "duplicate": get_duplicate_folders, + "orphaned": get_orphaned_folders, + "corrupted": get_corrupted_folders, + "unrecognized": get_unrecognized_folders, + } + + try: + return STATUS_FUNCTIONS[status](links, out_dir=out_dir) + except KeyError: + raise ValueError('Status not recognized.') + + +@enforce_types +def config(config_options_str: Optional[str]=None, + config_options: Optional[List[str]]=None, + get: bool=False, + set: bool=False, + reset: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Get and set your ArchiveBox project configuration values""" + + check_data_folder(out_dir=out_dir) + + if config_options and config_options_str: + stderr( + '[X] You should either pass config values as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif config_options_str: + config_options = config_options_str.split('\n') + + config_options = config_options or [] + + no_args = not (get or set or reset or config_options) + + matching_config: ConfigDict = {} + if get or no_args: + if config_options: + config_options = [get_real_name(key) for key in config_options] + matching_config = {key: CONFIG[key] for key in config_options if key in CONFIG} + failed_config = [key for key in config_options if key not in CONFIG] + if failed_config: + stderr() + stderr('[X] These options failed to get', color='red') + stderr(' {}'.format('\n '.join(config_options))) + raise SystemExit(1) + else: + matching_config = CONFIG + + print(printable_config(matching_config)) + raise SystemExit(not matching_config) + elif set: + new_config = {} + failed_options = [] + for line in config_options: + if line.startswith('#') or not line.strip(): + continue + if '=' not in line: + stderr('[X] Config KEY=VALUE must have an = sign in it', color='red') + stderr(f' {line}') + raise SystemExit(2) + + raw_key, val = line.split('=', 1) + raw_key = raw_key.upper().strip() + key = get_real_name(raw_key) + if key != raw_key: + stderr(f'[i] Note: The config option {raw_key} has been renamed to {key}, please use the new name going forwards.', color='lightyellow') + + if key in CONFIG: + new_config[key] = val.strip() + else: + failed_options.append(line) + + if new_config: + before = CONFIG + matching_config = write_config_file(new_config, out_dir=OUTPUT_DIR) + after = load_all_config() + print(printable_config(matching_config)) + + side_effect_changes: ConfigDict = {} + for key, val in after.items(): + if key in USER_CONFIG and (before[key] != after[key]) and (key not in matching_config): + side_effect_changes[key] = after[key] + + if side_effect_changes: + stderr() + stderr('[i] Note: This change also affected these other options that depended on it:', color='lightyellow') + print(' {}'.format(printable_config(side_effect_changes, prefix=' '))) + if failed_options: + stderr() + stderr('[X] These options failed to set (check for typos):', color='red') + stderr(' {}'.format('\n '.join(failed_options))) + raise SystemExit(bool(failed_options)) + elif reset: + stderr('[X] This command is not implemented yet.', color='red') + stderr(' Please manually remove the relevant lines from your config file:') + stderr(f' {CONFIG_FILE}') + raise SystemExit(2) + else: + stderr('[X] You must pass either --get or --set, or no arguments to get the whole config.', color='red') + stderr(' archivebox config') + stderr(' archivebox config --get SOME_KEY') + stderr(' archivebox config --set SOME_KEY=SOME_VALUE') + raise SystemExit(2) + + +@enforce_types +def schedule(add: bool=False, + show: bool=False, + clear: bool=False, + foreground: bool=False, + run_all: bool=False, + quiet: bool=False, + every: Optional[str]=None, + depth: int=0, + import_path: Optional[str]=None, + out_dir: Path=OUTPUT_DIR): + """Set ArchiveBox to regularly import URLs at specific times using cron""" + + check_data_folder(out_dir=out_dir) + + (Path(out_dir) / LOGS_DIR_NAME).mkdir(exist_ok=True) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + + if clear: + print(cron.remove_all(comment=CRON_COMMENT)) + cron.write() + raise SystemExit(0) + + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if every or add: + every = every or 'day' + quoted = lambda s: f'"{s}"' if s and ' ' in str(s) else str(s) + cmd = [ + 'cd', + quoted(out_dir), + '&&', + quoted(ARCHIVEBOX_BINARY), + *(['add', f'--depth={depth}', f'"{import_path}"'] if import_path else ['update']), + '>', + quoted(Path(LOGS_DIR) / 'archivebox.log'), + '2>&1', + + ] + new_job = cron.new(command=' '.join(cmd), comment=CRON_COMMENT) + + if every in ('minute', 'hour', 'day', 'month', 'year'): + set_every = getattr(new_job.every(), every) + set_every() + elif CronSlices.is_valid(every): + new_job.setall(every) + else: + stderr('{red}[X] Got invalid timeperiod for cron task.{reset}'.format(**ANSI)) + stderr(' It must be one of minute/hour/day/month') + stderr(' or a quoted cron-format schedule like:') + stderr(' archivebox init --every=day https://example.com/some/rss/feed.xml') + stderr(' archivebox init --every="0/5 * * * *" https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + cron = dedupe_cron_jobs(cron) + cron.write() + + total_runs = sum(j.frequency_per_year() for j in cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + print() + print('{green}[√] Scheduled new ArchiveBox cron job for user: {} ({} jobs are active).{reset}'.format(USER, len(existing_jobs), **ANSI)) + print('\n'.join(f' > {cmd}' if str(cmd) == str(new_job) else f' {cmd}' for cmd in existing_jobs)) + if total_runs > 60 and not quiet: + stderr() + stderr('{lightyellow}[!] With the current cron config, ArchiveBox is estimated to run >{} times per year.{reset}'.format(total_runs, **ANSI)) + stderr(' Congrats on being an enthusiastic internet archiver! 👌') + stderr() + stderr(' Make sure you have enough storage space available to hold all the data.') + stderr(' Using a compressed/deduped filesystem like ZFS is recommended if you plan on archiving a lot.') + stderr('') + elif show: + if existing_jobs: + print('\n'.join(str(cmd) for cmd in existing_jobs)) + else: + stderr('{red}[X] There are no ArchiveBox cron jobs scheduled for your user ({}).{reset}'.format(USER, **ANSI)) + stderr(' To schedule a new job, run:') + stderr(' archivebox schedule --every=[timeperiod] https://example.com/some/rss/feed.xml') + raise SystemExit(0) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if foreground or run_all: + if not existing_jobs: + stderr('{red}[X] You must schedule some jobs first before running in foreground mode.{reset}'.format(**ANSI)) + stderr(' archivebox schedule --every=hour https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + print('{green}[*] Running {} ArchiveBox jobs in foreground task scheduler...{reset}'.format(len(existing_jobs), **ANSI)) + if run_all: + try: + for job in existing_jobs: + sys.stdout.write(f' > {job.command.split("/archivebox ")[0].split(" && ")[0]}\n') + sys.stdout.write(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + sys.stdout.flush() + job.run() + sys.stdout.write(f'\r √ {job.command.split("/archivebox ")[-1]}\n') + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + if foreground: + try: + for job in existing_jobs: + print(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + for result in cron.run_scheduler(): + print(result) + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + +@enforce_types +def server(runserver_args: Optional[List[str]]=None, + reload: bool=False, + debug: bool=False, + init: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Run the ArchiveBox HTTP server""" + + runserver_args = runserver_args or [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # setup config for django runserver + from . import config + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + from django.contrib.auth.models import User + + admin_user = User.objects.filter(is_superuser=True).order_by('date_joined').only('username').last() + + print('{green}[+] Starting ArchiveBox webserver...{reset}'.format(**ANSI)) + if admin_user: + hint('The admin username is{lightblue} {}{reset}\n'.format(admin_user.username, **ANSI)) + else: + print('{lightyellow}[!] No admin users exist yet, you will not be able to edit links in the UI.{reset}'.format(**ANSI)) + print() + print(' To create an admin user, run:') + print(' archivebox manage createsuperuser') + print() + + # fallback to serving staticfiles insecurely with django when DEBUG=False + if not config.DEBUG: + runserver_args.append('--insecure') # TODO: serve statics w/ nginx instead + + # toggle autoreloading when archivebox code changes (it's on by default) + if not reload: + runserver_args.append('--noreload') + + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + + call_command("runserver", *runserver_args) + + +@enforce_types +def manage(args: Optional[List[str]]=None, out_dir: Path=OUTPUT_DIR) -> None: + """Run an ArchiveBox Django management command""" + + check_data_folder(out_dir=out_dir) + from django.core.management import execute_from_command_line + + if (args and "createsuperuser" in args) and (IN_DOCKER and not IS_TTY): + stderr('[!] Warning: you need to pass -it to use interactive commands in docker', color='lightyellow') + stderr(' docker run -it archivebox manage {}'.format(' '.join(args or ['...'])), color='lightyellow') + stderr() + + execute_from_command_line([f'{ARCHIVEBOX_BINARY} manage', *(args or ['help'])]) + + +@enforce_types +def shell(out_dir: Path=OUTPUT_DIR) -> None: + """Enter an interactive ArchiveBox Django shell""" + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + call_command("shell_plus") + diff --git a/archivebox-0.5.3/build/lib/archivebox/manage.py b/archivebox-0.5.3/build/lib/archivebox/manage.py new file mode 100644 index 0000000..1a9b297 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + # if you're a developer working on archivebox, still prefer the archivebox + # versions of ./manage.py commands whenever possible. When that's not possible + # (e.g. makemigrations), you can comment out this check temporarily + + if not ('makemigrations' in sys.argv or 'migrate' in sys.argv): + print("[X] Don't run ./manage.py directly (unless you are a developer running makemigrations):") + print() + print(' Hint: Use these archivebox CLI commands instead of the ./manage.py equivalents:') + print(' archivebox init (migrates the databse to latest version)') + print(' archivebox server (runs the Django web server)') + print(' archivebox shell (opens an iPython Django shell with all models imported)') + print(' archivebox manage [cmd] (any other management commands)') + raise SystemExit(2) + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/archivebox-0.5.3/build/lib/archivebox/mypy.ini b/archivebox-0.5.3/build/lib/archivebox/mypy.ini new file mode 100644 index 0000000..b1b4489 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +plugins = + mypy_django_plugin.main diff --git a/archivebox-0.5.3/build/lib/archivebox/package.json b/archivebox-0.5.3/build/lib/archivebox/package.json new file mode 100644 index 0000000..7f8bf66 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/package.json @@ -0,0 +1,21 @@ +{ + "name": "archivebox", + "version": "0.5.3", + "description": "ArchiveBox: The self-hosted internet archive", + "author": "Nick Sweeting <archivebox-npm@sweeting.me>", + "license": "MIT", + "scripts": { + "archivebox": "./bin/archive" + }, + "bin": { + "archivebox-node": "./bin/archive", + "single-file": "./node_modules/.bin/single-file", + "readability-extractor": "./node_modules/.bin/readability-extractor", + "mercury-parser": "./node_modules/.bin/mercury-parser" + }, + "dependencies": { + "@postlight/mercury-parser": "^2.2.0", + "readability-extractor": "git+https://github.com/pirate/readability-extractor.git", + "single-file": "git+https://github.com/gildas-lormeau/SingleFile.git" + } +} diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/__init__.py b/archivebox-0.5.3/build/lib/archivebox/parsers/__init__.py new file mode 100644 index 0000000..441c08a --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/__init__.py @@ -0,0 +1,203 @@ +""" +Everything related to parsing links from input sources. + +For a list of supported services, see the README.md. +For examples of supported import formats see tests/. +""" + +__package__ = 'archivebox.parsers' + +import re +from io import StringIO + +from typing import IO, Tuple, List, Optional +from datetime import datetime +from pathlib import Path + +from ..system import atomic_write +from ..config import ( + ANSI, + OUTPUT_DIR, + SOURCES_DIR_NAME, + TIMEOUT, +) +from ..util import ( + basename, + htmldecode, + download_url, + enforce_types, + URL_REGEX, +) +from ..index.schema import Link +from ..logging_util import TimedProgress, log_source_saved + +from .pocket_html import parse_pocket_html_export +from .pocket_api import parse_pocket_api_export +from .pinboard_rss import parse_pinboard_rss_export +from .wallabag_atom import parse_wallabag_atom_export +from .shaarli_rss import parse_shaarli_rss_export +from .medium_rss import parse_medium_rss_export +from .netscape_html import parse_netscape_html_export +from .generic_rss import parse_generic_rss_export +from .generic_json import parse_generic_json_export +from .generic_html import parse_generic_html_export +from .generic_txt import parse_generic_txt_export + +PARSERS = ( + # Specialized parsers + ('Pocket API', parse_pocket_api_export), + ('Wallabag ATOM', parse_wallabag_atom_export), + ('Pocket HTML', parse_pocket_html_export), + ('Pinboard RSS', parse_pinboard_rss_export), + ('Shaarli RSS', parse_shaarli_rss_export), + ('Medium RSS', parse_medium_rss_export), + + # General parsers + ('Netscape HTML', parse_netscape_html_export), + ('Generic RSS', parse_generic_rss_export), + ('Generic JSON', parse_generic_json_export), + ('Generic HTML', parse_generic_html_export), + + # Fallback parser + ('Plain Text', parse_generic_txt_export), +) + + +@enforce_types +def parse_links_memory(urls: List[str], root_url: Optional[str]=None): + """ + parse a list of URLS without touching the filesystem + """ + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + #urls = list(map(lambda x: x + "\n", urls)) + file = StringIO() + file.writelines(urls) + file.name = "io_string" + links, parser = run_parser_functions(file, timer, root_url=root_url) + timer.end() + + if parser is None: + return [], 'Failed to parse' + return links, parser + + +@enforce_types +def parse_links(source_file: str, root_url: Optional[str]=None) -> Tuple[List[Link], str]: + """parse a list of URLs with their metadata from an + RSS feed, bookmarks export, or text file + """ + + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + with open(source_file, 'r', encoding='utf-8') as file: + links, parser = run_parser_functions(file, timer, root_url=root_url) + + timer.end() + if parser is None: + return [], 'Failed to parse' + return links, parser + + +def run_parser_functions(to_parse: IO[str], timer, root_url: Optional[str]=None) -> Tuple[List[Link], Optional[str]]: + most_links: List[Link] = [] + best_parser_name = None + + for parser_name, parser_func in PARSERS: + try: + parsed_links = list(parser_func(to_parse, root_url=root_url)) + if not parsed_links: + raise Exception('no links found') + + # print(f'[√] Parser {parser_name} succeeded: {len(parsed_links)} links parsed') + if len(parsed_links) > len(most_links): + most_links = parsed_links + best_parser_name = parser_name + + except Exception as err: # noqa + # Parsers are tried one by one down the list, and the first one + # that succeeds is used. To see why a certain parser was not used + # due to error or format incompatibility, uncomment this line: + + # print('[!] Parser {} failed: {} {}'.format(parser_name, err.__class__.__name__, err)) + # raise + pass + timer.end() + return most_links, best_parser_name + + +@enforce_types +def save_text_as_source(raw_text: str, filename: str='{ts}-stdin.txt', out_dir: Path=OUTPUT_DIR) -> str: + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(out_dir / SOURCES_DIR_NAME / filename.format(ts=ts)) + atomic_write(source_path, raw_text) + log_source_saved(source_file=source_path) + return source_path + + +@enforce_types +def save_file_as_source(path: str, timeout: int=TIMEOUT, filename: str='{ts}-{basename}.txt', out_dir: Path=OUTPUT_DIR) -> str: + """download a given url's content into output/sources/domain-<timestamp>.txt""" + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(OUTPUT_DIR / SOURCES_DIR_NAME / filename.format(basename=basename(path), ts=ts)) + + if any(path.startswith(s) for s in ('http://', 'https://', 'ftp://')): + # Source is a URL that needs to be downloaded + print(f' > Downloading {path} contents') + timer = TimedProgress(timeout, prefix=' ') + try: + raw_source_text = download_url(path, timeout=timeout) + raw_source_text = htmldecode(raw_source_text) + timer.end() + except Exception as e: + timer.end() + print('{}[!] Failed to download {}{}\n'.format( + ANSI['red'], + path, + ANSI['reset'], + )) + print(' ', e) + raise SystemExit(1) + + else: + # Source is a path to a local file on the filesystem + with open(path, 'r') as f: + raw_source_text = f.read() + + atomic_write(source_path, raw_source_text) + + log_source_saved(source_file=source_path) + + return source_path + + +def check_url_parsing_invariants() -> None: + """Check that plain text regex URL parsing works as expected""" + + # this is last-line-of-defense to make sure the URL_REGEX isn't + # misbehaving, as the consequences could be disastrous and lead to many + # incorrect/badly parsed links being added to the archive + + test_urls = ''' + https://example1.com/what/is/happening.html?what=1#how-about-this=1 + https://example2.com/what/is/happening/?what=1#how-about-this=1 + HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f + https://example4.com/what/is/happening.html + https://example5.com/ + https://example6.com + + <test>http://example7.com</test> + [https://example8.com/what/is/this.php?what=1] + [and http://example9.com?what=1&other=3#and-thing=2] + <what>https://example10.com#and-thing=2 "</about> + abc<this["https://example11.com/what/is#and-thing=2?whoami=23&where=1"]that>def + sdflkf[what](https://example12.com/who/what.php?whoami=1#whatami=2)?am=hi + example13.bada + and example14.badb + <or>htt://example15.badc</that> + ''' + # print('\n'.join(re.findall(URL_REGEX, test_urls))) + assert len(re.findall(URL_REGEX, test_urls)) == 12 + diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/generic_html.py b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_html.py new file mode 100644 index 0000000..74b3d1f --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_html.py @@ -0,0 +1,53 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + URL_REGEX, +) +from html.parser import HTMLParser +from urllib.parse import urljoin + + +class HrefParser(HTMLParser): + def __init__(self): + super().__init__() + self.urls = [] + + def handle_starttag(self, tag, attrs): + if tag == "a": + for attr, value in attrs: + if attr == "href": + self.urls.append(value) + + +@enforce_types +def parse_generic_html_export(html_file: IO[str], root_url: Optional[str]=None, **_kwargs) -> Iterable[Link]: + """Parse Generic HTML for href tags and use only the url (support for title coming later)""" + + html_file.seek(0) + for line in html_file: + parser = HrefParser() + # example line + # <li><a href="http://example.com/ time_added="1478739709" tags="tag1,tag2">example title</a></li> + parser.feed(line) + for url in parser.urls: + if root_url: + # resolve relative urls /home.html -> https://example.com/home.html + url = urljoin(root_url, url) + + for archivable_url in re.findall(URL_REGEX, url): + yield Link( + url=htmldecode(archivable_url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/generic_json.py b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_json.py new file mode 100644 index 0000000..e6ed677 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_json.py @@ -0,0 +1,65 @@ +__package__ = 'archivebox.parsers' + +import json + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_generic_json_export(json_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)""" + + json_file.seek(0) + links = json.load(json_file) + json_date = lambda s: datetime.strptime(s, '%Y-%m-%dT%H:%M:%S%z') + + for link in links: + # example line + # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}] + if link: + # Parse URL + url = link.get('href') or link.get('url') or link.get('URL') + if not url: + raise Exception('JSON must contain URL in each entry [{"url": "http://...", ...}, ...]') + + # Parse the timestamp + ts_str = str(datetime.now().timestamp()) + if link.get('timestamp'): + # chrome/ff histories use a very precise timestamp + ts_str = str(link['timestamp'] / 10000000) + elif link.get('time'): + ts_str = str(json_date(link['time'].split(',', 1)[0]).timestamp()) + elif link.get('created_at'): + ts_str = str(json_date(link['created_at']).timestamp()) + elif link.get('created'): + ts_str = str(json_date(link['created']).timestamp()) + elif link.get('date'): + ts_str = str(json_date(link['date']).timestamp()) + elif link.get('bookmarked'): + ts_str = str(json_date(link['bookmarked']).timestamp()) + elif link.get('saved'): + ts_str = str(json_date(link['saved']).timestamp()) + + # Parse the title + title = None + if link.get('title'): + title = link['title'].strip() + elif link.get('description'): + title = link['description'].replace(' — Readability', '').strip() + elif link.get('name'): + title = link['name'].strip() + + yield Link( + url=htmldecode(url), + timestamp=ts_str, + title=htmldecode(title) or None, + tags=htmldecode(link.get('tags')) or '', + sources=[json_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/generic_rss.py b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_rss.py new file mode 100644 index 0000000..2831844 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/generic_rss.py @@ -0,0 +1,49 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + +@enforce_types +def parse_generic_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse RSS XML-format files into links""" + + rss_file.seek(0) + items = rss_file.read().split('<item>') + items = items[1:] if items else [] + for item in items: + # example item: + # <item> + # <title><![CDATA[How JavaScript works: inside the V8 engine]]> + # Unread + # https://blog.sessionstack.com/how-javascript-works-inside + # https://blog.sessionstack.com/how-javascript-works-inside + # Mon, 21 Aug 2017 14:21:58 -0500 + # + + trailing_removed = item.split('', 1)[0] + leading_removed = trailing_removed.split('', 1)[-1].strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r for r in rows if r.strip().startswith('<{}>'.format(key))][0] + + url = str_between(get_row('link'), '', '') + ts_str = str_between(get_row('pubDate'), '', '') + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z") + title = str_between(get_row('title'), ' Iterable[Link]: + """Parse raw links from each line in a text file""" + + text_file.seek(0) + for line in text_file.readlines(): + if not line.strip(): + continue + + # if the line is a local file path that resolves, then we can archive it + try: + if Path(line).exists(): + yield Link( + url=line, + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + except (OSError, PermissionError): + # nvm, not a valid path... + pass + + # otherwise look for anything that looks like a URL in the line + for url in re.findall(URL_REGEX, line): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + + # look inside the URL for any sub-urls, e.g. for archive.org links + # https://web.archive.org/web/20200531203453/https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + # -> https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + for url in re.findall(URL_REGEX, line[1:]): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/medium_rss.py b/archivebox-0.5.3/build/lib/archivebox/parsers/medium_rss.py new file mode 100644 index 0000000..8f14f77 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/medium_rss.py @@ -0,0 +1,35 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_medium_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Medium RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.find("channel").findall("item") # type: ignore + for item in items: + url = item.find("link").text # type: ignore + title = item.find("title").text.strip() # type: ignore + ts_str = item.find("pubDate").text # type: ignore + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z") # type: ignore + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/netscape_html.py b/archivebox-0.5.3/build/lib/archivebox/parsers/netscape_html.py new file mode 100644 index 0000000..a063023 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/netscape_html.py @@ -0,0 +1,39 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_netscape_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse netscape-format bookmarks export files (produced by all browsers)""" + + html_file.seek(0) + pattern = re.compile("]*>(.+)", re.UNICODE | re.IGNORECASE) + for line in html_file: + # example line + #
    example bookmark title + + match = pattern.search(line) + if match: + url = match.group(1) + time = datetime.fromtimestamp(float(match.group(2))) + title = match.group(3).strip() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[html_file.name], + ) + diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/pinboard_rss.py b/archivebox-0.5.3/build/lib/archivebox/parsers/pinboard_rss.py new file mode 100644 index 0000000..98ff14a --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/pinboard_rss.py @@ -0,0 +1,47 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pinboard_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pinboard RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.findall("{http://purl.org/rss/1.0/}item") + for item in items: + find = lambda p: item.find(p).text.strip() if item.find(p) else None # type: ignore + + url = find("{http://purl.org/rss/1.0/}link") + tags = find("{http://purl.org/dc/elements/1.1/}subject") + title = find("{http://purl.org/rss/1.0/}title") + ts_str = find("{http://purl.org/dc/elements/1.1/}date") + + # Pinboard includes a colon in its date stamp timezone offsets, which + # Python can't parse. Remove it: + if ts_str and ts_str[-3:-2] == ":": + ts_str = ts_str[:-3]+ts_str[-2:] + + if ts_str: + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + else: + time = datetime.now() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=htmldecode(tags) or None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_api.py b/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_api.py new file mode 100644 index 0000000..bf3a292 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_api.py @@ -0,0 +1,113 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from configparser import ConfigParser + +from pathlib import Path +from ..vendor.pocket import Pocket + +from ..index.schema import Link +from ..util import enforce_types +from ..system import atomic_write +from ..config import ( + SOURCES_DIR, + POCKET_CONSUMER_KEY, + POCKET_ACCESS_TOKENS, +) + + +COUNT_PER_PAGE = 500 +API_DB_PATH = Path(SOURCES_DIR) / 'pocket_api.db' + +# search for broken protocols that sometimes come from the Pocket API +_BROKEN_PROTOCOL_RE = re.compile('^(http[s]?)(:/(?!/))') + + +def get_pocket_articles(api: Pocket, since=None, page=0): + body, headers = api.get( + state='archive', + sort='oldest', + since=since, + count=COUNT_PER_PAGE, + offset=page * COUNT_PER_PAGE, + ) + + articles = body['list'].values() if isinstance(body['list'], dict) else body['list'] + returned_count = len(articles) + + yield from articles + + if returned_count == COUNT_PER_PAGE: + yield from get_pocket_articles(api, since=since, page=page + 1) + else: + api.last_since = body['since'] + + +def link_from_article(article: dict, sources: list): + url: str = article['resolved_url'] or article['given_url'] + broken_protocol = _BROKEN_PROTOCOL_RE.match(url) + if broken_protocol: + url = url.replace(f'{broken_protocol.group(1)}:/', f'{broken_protocol.group(1)}://') + title = article['resolved_title'] or article['given_title'] or url + + return Link( + url=url, + timestamp=article['time_read'], + title=title, + tags=article.get('tags'), + sources=sources + ) + + +def write_since(username: str, since: str): + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + since_file = ConfigParser() + since_file.optionxform = str + since_file.read(API_DB_PATH) + + since_file[username] = { + 'since': since + } + + with open(API_DB_PATH, 'w+') as new: + since_file.write(new) + + +def read_since(username: str) -> Optional[str]: + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(API_DB_PATH) + + return config_file.get(username, 'since', fallback=None) + + +@enforce_types +def should_parse_as_pocket_api(text: str) -> bool: + return text.startswith('pocket://') + + +@enforce_types +def parse_pocket_api_export(input_buffer: IO[str], **_kwargs) -> Iterable[Link]: + """Parse bookmarks from the Pocket API""" + + input_buffer.seek(0) + pattern = re.compile(r"^pocket:\/\/(\w+)") + for line in input_buffer: + if should_parse_as_pocket_api(line): + + username = pattern.search(line).group(1) + api = Pocket(POCKET_CONSUMER_KEY, POCKET_ACCESS_TOKENS[username]) + api.last_since = None + + for article in get_pocket_articles(api, since=read_since(username)): + yield link_from_article(article, sources=[line]) + + write_since(username, api.last_since) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_html.py b/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_html.py new file mode 100644 index 0000000..653f21b --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/pocket_html.py @@ -0,0 +1,38 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pocket_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)""" + + html_file.seek(0) + pattern = re.compile("^\\s*
  • (.+)
  • ", re.UNICODE) + for line in html_file: + # example line + #
  • example title
  • + match = pattern.search(line) + if match: + url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url + time = datetime.fromtimestamp(float(match.group(2))) + tags = match.group(3) + title = match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/shaarli_rss.py b/archivebox-0.5.3/build/lib/archivebox/parsers/shaarli_rss.py new file mode 100644 index 0000000..4a925f4 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/shaarli_rss.py @@ -0,0 +1,50 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_shaarli_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Shaarli-specific RSS XML-format files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # Aktuelle Trojaner-Welle: Emotet lauert in gefälschten Rechnungsmails | heise online + # + # https://demo.shaarli.org/?cEV4vw + # 2019-01-30T06:06:01+00:00 + # 2019-01-30T06:06:01+00:00 + #

    Permalink

    ]]>
    + #
    + + trailing_removed = entry.split('
    ', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '', '').strip() + url = str_between(get_row('link'), '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/parsers/wallabag_atom.py b/archivebox-0.5.3/build/lib/archivebox/parsers/wallabag_atom.py new file mode 100644 index 0000000..0d77869 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/parsers/wallabag_atom.py @@ -0,0 +1,57 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_wallabag_atom_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Wallabag Atom files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # <![CDATA[Orient Ray vs Mako: Is There Much Difference? - iknowwatches.com]]> + # + # https://iknowwatches.com/orient-ray-vs-mako/ + # wallabag:wallabag.drycat.fr:milosh:entry:14041 + # 2020-10-18T09:14:02+02:00 + # 2020-10-18T09:13:56+02:00 + # + # + # + + trailing_removed = entry.split('', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '<![CDATA[', ']]>').strip() + url = str_between(get_row('link rel="via"'), '', '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + try: + tags = str_between(get_row('category'), 'label="', '" />') + except: + tags = None + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/build/lib/archivebox/search/__init__.py b/archivebox-0.5.3/build/lib/archivebox/search/__init__.py new file mode 100644 index 0000000..6191ede --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/search/__init__.py @@ -0,0 +1,108 @@ +from typing import List, Union +from pathlib import Path +from importlib import import_module + +from django.db.models import QuerySet + +from archivebox.index.schema import Link +from archivebox.util import enforce_types +from archivebox.config import stderr, OUTPUT_DIR, USE_INDEXING_BACKEND, USE_SEARCHING_BACKEND, SEARCH_BACKEND_ENGINE + +from .utils import get_indexable_content, log_index_started + +def indexing_enabled(): + return USE_INDEXING_BACKEND + +def search_backend_enabled(): + return USE_SEARCHING_BACKEND + +def get_backend(): + return f'search.backends.{SEARCH_BACKEND_ENGINE}' + +def import_backend(): + backend_string = get_backend() + try: + backend = import_module(backend_string) + except Exception as err: + raise Exception("Could not load '%s' as a backend: %s" % (backend_string, err)) + return backend + +@enforce_types +def write_search_index(link: Link, texts: Union[List[str], None]=None, out_dir: Path=OUTPUT_DIR, skip_text_index: bool=False) -> None: + if not indexing_enabled(): + return + + if not skip_text_index and texts: + from core.models import Snapshot + + snap = Snapshot.objects.filter(url=link.url).first() + backend = import_backend() + if snap: + try: + backend.index(snapshot_id=str(snap.id), texts=texts) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def query_search_index(query: str, out_dir: Path=OUTPUT_DIR) -> QuerySet: + from core.models import Snapshot + + if search_backend_enabled(): + backend = import_backend() + try: + snapshot_ids = backend.search(query) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + raise + else: + # TODO preserve ordering from backend + qsearch = Snapshot.objects.filter(pk__in=snapshot_ids) + return qsearch + + return Snapshot.objects.none() + +@enforce_types +def flush_search_index(snapshots: QuerySet): + if not indexing_enabled() or not snapshots: + return + backend = import_backend() + snapshot_ids=(str(pk) for pk in snapshots.values_list('pk',flat=True)) + try: + backend.flush(snapshot_ids) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def index_links(links: Union[List[Link],None], out_dir: Path=OUTPUT_DIR): + if not links: + return + + from core.models import Snapshot, ArchiveResult + + for link in links: + snap = Snapshot.objects.filter(url=link.url).first() + if snap: + results = ArchiveResult.objects.indexable().filter(snapshot=snap) + log_index_started(link.url) + try: + texts = get_indexable_content(results) + except Exception as err: + stderr() + stderr( + f'[X] An Exception ocurred reading the indexable content={err}:', + color='red', + ) + else: + write_search_index(link, texts, out_dir=out_dir) diff --git a/archivebox-0.5.3/build/lib/archivebox/search/backends/__init__.py b/archivebox-0.5.3/build/lib/archivebox/search/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/build/lib/archivebox/search/backends/ripgrep.py b/archivebox-0.5.3/build/lib/archivebox/search/backends/ripgrep.py new file mode 100644 index 0000000..840d2d2 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/search/backends/ripgrep.py @@ -0,0 +1,45 @@ +import re +from subprocess import run, PIPE +from typing import List, Generator + +from archivebox.config import ARCHIVE_DIR, RIPGREP_VERSION +from archivebox.util import enforce_types + +RG_IGNORE_EXTENSIONS = ('css','js','orig','svg') + +RG_ADD_TYPE = '--type-add' +RG_IGNORE_ARGUMENTS = f"ignore:*.{{{','.join(RG_IGNORE_EXTENSIONS)}}}" +RG_DEFAULT_ARGUMENTS = "-ilTignore" # Case insensitive(i), matching files results(l) +RG_REGEX_ARGUMENT = '-e' + +TIMESTAMP_REGEX = r'\/([\d]+\.[\d]+)\/' + +ts_regex = re.compile(TIMESTAMP_REGEX) + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + return + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + return + +@enforce_types +def search(text: str) -> List[str]: + if not RIPGREP_VERSION: + raise Exception("ripgrep binary not found, install ripgrep to use this search backend") + + from core.models import Snapshot + + rg_cmd = ['rg', RG_ADD_TYPE, RG_IGNORE_ARGUMENTS, RG_DEFAULT_ARGUMENTS, RG_REGEX_ARGUMENT, text, str(ARCHIVE_DIR)] + rg = run(rg_cmd, stdout=PIPE, stderr=PIPE, timeout=60) + file_paths = [p.decode() for p in rg.stdout.splitlines()] + timestamps = set() + for path in file_paths: + ts = ts_regex.findall(path) + if ts: + timestamps.add(ts[0]) + + snap_ids = [str(id) for id in Snapshot.objects.filter(timestamp__in=timestamps).values_list('pk', flat=True)] + + return snap_ids diff --git a/archivebox-0.5.3/build/lib/archivebox/search/backends/sonic.py b/archivebox-0.5.3/build/lib/archivebox/search/backends/sonic.py new file mode 100644 index 0000000..f0beadd --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/search/backends/sonic.py @@ -0,0 +1,28 @@ +from typing import List, Generator + +from sonic import IngestClient, SearchClient + +from archivebox.util import enforce_types +from archivebox.config import SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD, SONIC_BUCKET, SONIC_COLLECTION + +MAX_SONIC_TEXT_LENGTH = 20000 + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for text in texts: + chunks = [text[i:i+MAX_SONIC_TEXT_LENGTH] for i in range(0, len(text), MAX_SONIC_TEXT_LENGTH)] + for chunk in chunks: + ingestcl.push(SONIC_COLLECTION, SONIC_BUCKET, snapshot_id, str(chunk)) + +@enforce_types +def search(text: str) -> List[str]: + with SearchClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as querycl: + snap_ids = querycl.query(SONIC_COLLECTION, SONIC_BUCKET, text) + return snap_ids + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for id in snapshot_ids: + ingestcl.flush_object(SONIC_COLLECTION, SONIC_BUCKET, str(id)) diff --git a/archivebox-0.5.3/build/lib/archivebox/search/utils.py b/archivebox-0.5.3/build/lib/archivebox/search/utils.py new file mode 100644 index 0000000..55c97e7 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/search/utils.py @@ -0,0 +1,44 @@ +from django.db.models import QuerySet + +from archivebox.util import enforce_types +from archivebox.config import ANSI + +def log_index_started(url): + print('{green}[*] Indexing url: {} in the search index {reset}'.format(url, **ANSI)) + print( ) + +def get_file_result_content(res, extra_path, use_pwd=False): + if use_pwd: + fpath = f'{res.pwd}/{res.output}' + else: + fpath = f'{res.output}' + + if extra_path: + fpath = f'{fpath}/{extra_path}' + + with open(fpath, 'r') as file: + data = file.read() + if data: + return [data] + return [] + + +# This should be abstracted by a plugin interface for extractors +@enforce_types +def get_indexable_content(results: QuerySet): + if not results: + return [] + # Only use the first method available + res, method = results.first(), results.first().extractor + if method not in ('readability', 'singlefile', 'dom', 'wget'): + return [] + # This should come from a plugin interface + + if method == 'readability': + return get_file_result_content(res, 'content.txt') + elif method == 'singlefile': + return get_file_result_content(res, '') + elif method == 'dom': + return get_file_result_content(res,'',use_pwd=True) + elif method == 'wget': + return get_file_result_content(res,'',use_pwd=True) diff --git a/archivebox-0.5.3/build/lib/archivebox/system.py b/archivebox-0.5.3/build/lib/archivebox/system.py new file mode 100644 index 0000000..b27c5e4 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/system.py @@ -0,0 +1,163 @@ +__package__ = 'archivebox' + + +import os +import shutil + +from json import dump +from pathlib import Path +from typing import Optional, Union, Set, Tuple +from subprocess import run as subprocess_run + +from crontab import CronTab +from atomicwrites import atomic_write as lib_atomic_write + +from .util import enforce_types, ExtendedEncoder +from .config import OUTPUT_PERMISSIONS + + + +def run(*args, input=None, capture_output=True, text=False, **kwargs): + """Patched of subprocess.run to fix blocking io making timeout=innefective""" + + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + + if capture_output: + if ('stdout' in kwargs) or ('stderr' in kwargs): + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + + return subprocess_run(*args, input=input, capture_output=capture_output, text=text, **kwargs) + + +@enforce_types +def atomic_write(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True) -> None: + """Safe atomic write to filesystem by writing to temp file + atomic rename""" + + mode = 'wb+' if isinstance(contents, bytes) else 'w' + + # print('\n> Atomic Write:', mode, path, len(contents), f'overwrite={overwrite}') + try: + with lib_atomic_write(path, mode=mode, overwrite=overwrite) as f: + if isinstance(contents, dict): + dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) + elif isinstance(contents, (bytes, str)): + f.write(contents) + except OSError as e: + print(f"[X] OSError: Failed to write {path} with fcntl.F_FULLFSYNC. ({e})") + print(" For data integrity, ArchiveBox requires a filesystem that supports atomic writes.") + print(" Filesystems and network drives that don't implement FSYNC are incompatible and require workarounds.") + raise SystemExit(1) + os.chmod(path, int(OUTPUT_PERMISSIONS, base=8)) + +@enforce_types +def chmod_file(path: str, cwd: str='.', permissions: str=OUTPUT_PERMISSIONS) -> None: + """chmod -R /""" + + root = Path(cwd) / path + if not root.exists(): + raise Exception('Failed to chmod: {} does not exist (did the previous step fail?)'.format(path)) + + if not root.is_dir(): + os.chmod(root, int(OUTPUT_PERMISSIONS, base=8)) + else: + for subpath in Path(path).glob('**/*'): + os.chmod(subpath, int(OUTPUT_PERMISSIONS, base=8)) + + +@enforce_types +def copy_and_overwrite(from_path: Union[str, Path], to_path: Union[str, Path]): + """copy a given file or directory to a given path, overwriting the destination""" + if Path(from_path).is_dir(): + shutil.rmtree(to_path, ignore_errors=True) + shutil.copytree(from_path, to_path) + else: + with open(from_path, 'rb') as src: + contents = src.read() + atomic_write(to_path, contents) + + +@enforce_types +def get_dir_size(path: Union[str, Path], recursive: bool=True, pattern: Optional[str]=None) -> Tuple[int, int, int]: + """get the total disk size of a given directory, optionally summing up + recursively and limiting to a given filter list + """ + num_bytes, num_dirs, num_files = 0, 0, 0 + for entry in os.scandir(path): + if (pattern is not None) and (pattern not in entry.path): + continue + if entry.is_dir(follow_symlinks=False): + if not recursive: + continue + num_dirs += 1 + bytes_inside, dirs_inside, files_inside = get_dir_size(entry.path) + num_bytes += bytes_inside + num_dirs += dirs_inside + num_files += files_inside + else: + num_bytes += entry.stat(follow_symlinks=False).st_size + num_files += 1 + return num_bytes, num_dirs, num_files + + +CRON_COMMENT = 'archivebox_schedule' + + +@enforce_types +def dedupe_cron_jobs(cron: CronTab) -> CronTab: + deduped: Set[Tuple[str, str]] = set() + + for job in list(cron): + unique_tuple = (str(job.slices), job.command) + if unique_tuple not in deduped: + deduped.add(unique_tuple) + cron.remove(job) + + for schedule, command in deduped: + job = cron.new(command=command, comment=CRON_COMMENT) + job.setall(schedule) + job.enable() + + return cron + + +class suppress_output(object): + ''' + A context manager for doing a "deep suppression" of stdout and stderr in + Python, i.e. will suppress all print, even if the print originates in a + compiled C/Fortran sub-function. + This will not suppress raised exceptions, since exceptions are printed + to stderr just before a script exits, and after the context manager has + exited (at least, I think that is why it lets exceptions through). + + with suppress_stdout_stderr(): + rogue_function() + ''' + def __init__(self, stdout=True, stderr=True): + # Open a pair of null files + # Save the actual stdout (1) and stderr (2) file descriptors. + self.stdout, self.stderr = stdout, stderr + if stdout: + self.null_stdout = os.open(os.devnull, os.O_RDWR) + self.real_stdout = os.dup(1) + if stderr: + self.null_stderr = os.open(os.devnull, os.O_RDWR) + self.real_stderr = os.dup(2) + + def __enter__(self): + # Assign the null pointers to stdout and stderr. + if self.stdout: + os.dup2(self.null_stdout, 1) + if self.stderr: + os.dup2(self.null_stderr, 2) + + def __exit__(self, *_): + # Re-assign the real stdout/stderr back to (1) and (2) + if self.stdout: + os.dup2(self.real_stdout, 1) + os.close(self.null_stdout) + if self.stderr: + os.dup2(self.real_stderr, 2) + os.close(self.null_stderr) diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/actions_as_select.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/actions_as_select.html new file mode 100644 index 0000000..86a7719 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/actions_as_select.html @@ -0,0 +1 @@ +actions_as_select diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/app_index.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/app_index.html new file mode 100644 index 0000000..6868b49 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/app_index.html @@ -0,0 +1,18 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/base.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/base.html new file mode 100644 index 0000000..d8ad8d0 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/base.html @@ -0,0 +1,246 @@ +{% load i18n static %} +{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block title %}{% endblock %} | ArchiveBox + +{% block extrastyle %}{% endblock %} +{% if LANGUAGE_BIDI %}{% endif %} +{% block extrahead %}{% endblock %} +{% block responsive %} + + + {% if LANGUAGE_BIDI %}{% endif %} +{% endblock %} +{% block blockbots %}{% endblock %} + + +{% load i18n %} + + + + + + + + + +
    + + {% if not is_popup %} + + + + {% block breadcrumbs %} + + {% endblock %} + {% endif %} + + {% block messages %} + {% if messages %} +
      {% for message in messages %} + {{ message|capfirst }} + {% endfor %}
    + {% endif %} + {% endblock messages %} + + +
    + {% block pretitle %}{% endblock %} + {% block content_title %}{# {% if title %}

    {{ title }}

    {% endif %} #}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
    +
    + + + {% block footer %}{% endblock %} +
    + + + + + diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/grid_change_list.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/grid_change_list.html new file mode 100644 index 0000000..6894efd --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/grid_change_list.html @@ -0,0 +1,91 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + {{ block.super }} + + {% if cl.formset %} + + {% endif %} + {% if cl.formset or action_form %} + + {% endif %} + {{ media.css }} + {% if not actions_on_top and not actions_on_bottom %} + + {% endif %} +{% endblock %} + +{% block extrahead %} +{{ block.super }} +{{ media.js }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block coltype %}{% endblock %} + +{% block content %} +
    + {% block object-tools %} +
      + {% block object-tools-items %} + {% change_list_object_tools %} + {% endblock %} +
    + {% endblock %} + {% if cl.formset and cl.formset.errors %} +

    + {% if cl.formset.total_error_count == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} +

    + {{ cl.formset.non_form_errors }} + {% endif %} +
    +
    + {% block search %}{% search_form cl %}{% endblock %} + {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %} + +
    {% csrf_token %} + {% if cl.formset %} +
    {{ cl.formset.management_form }}
    + {% endif %} + + {% block result_list %} + {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% comment %} + Table grid + {% result_list cl %} + {% endcomment %} + {% snapshots_grid cl %} + {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% endblock %} + {% block pagination %}{% pagination cl %}{% endblock %} +
    +
    + {% block filters %} + {% if cl.has_filters %} +
    +

    {% translate 'Filter' %}

    + {% if cl.has_active_filters %}

    + ✖ {% translate "Clear all filters" %} +

    {% endif %} + {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} +
    + {% endif %} + {% endblock %} +
    +
    +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/login.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/login.html new file mode 100644 index 0000000..98283f8 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/login.html @@ -0,0 +1,100 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} + +{% block extrastyle %}{{ block.super }} +{{ form.media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} login{% endblock %} + +{% block branding %}

    ArchiveBox Admin

    {% endblock %} + +{% block usertools %} +
    + Back to Main Index +{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block content_title %} +
    + Log in to add, edit, and remove links from your archive. +


    +
    +{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors %} +

    +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

    +{% endif %} + +{% if form.non_field_errors %} +{% for error in form.non_field_errors %} +

    + {{ error }} +

    +{% endfor %} +{% endif %} + +
    + +{% if user.is_authenticated %} +

    +{% blocktrans trimmed %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? +{% endblocktrans %} +

    +{% endif %} + +
    +
    {% csrf_token %} +
    + {{ form.username.errors }} + {{ form.username.label_tag }} {{ form.username }} +
    +
    + {{ form.password.errors }} + {{ form.password.label_tag }} {{ form.password }} + +
    + {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + + {% endif %} +
    + +
    +
    + +
    +

    +
    +
    + If you forgot your password, reset it here or run:
    +
    +archivebox manage changepassword USERNAME
    +
    + +

    +
    +
    + To create a new admin user, run the following: +
    +archivebox manage createsuperuser
    +
    +
    +
    + + (cd into your archive folder before running commands) +
    + + +
    +{% endblock %} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/admin/snapshots_grid.html b/archivebox-0.5.3/build/lib/archivebox/themes/admin/snapshots_grid.html new file mode 100644 index 0000000..a7a2d4f --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/admin/snapshots_grid.html @@ -0,0 +1,162 @@ +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + + +{% endblock %} + +{% block content %} +
    + {% for obj in results %} +
    + + + + + +
    + {% if obj.tags_str %} +

    {{obj.tags_str}}

    + {% endif %} + {% if obj.title %} + +

    {{obj.title|truncatechars:55 }}

    +
    + {% endif %} + {% comment %}

    TEXT If needed.

    {% endcomment %} +
    +
    + +
    +
    + {% endfor %} +
    + +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/add_links.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/add_links.html new file mode 100644 index 0000000..0b384f5 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/add_links.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block body %} +
    +

    + {% if stdout %} +

    Add new URLs to your archive: results

    +
    +                {{ stdout | safe }}
    +                

    +
    +
    +
    +   Add more URLs ➕ +
    + {% else %} +
    {% csrf_token %} +

    Add new URLs to your archive

    +
    + {{ form.as_p }} +
    + +
    +
    +


    + + {% if absolute_add_path %} +
    +

    Bookmark this link to quickly add to your archive: + Add to ArchiveBox

    +
    + {% endif %} + + {% endif %} +
    +{% endblock %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/base.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/base.html new file mode 100644 index 0000000..a70430e --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/base.html @@ -0,0 +1,284 @@ +{% load static %} + + + + + + Archived Sites + + + + + + {% block extra_head %} + {% endblock %} + + + + + + +
    +
    + +
    +
    + {% block body %} + {% endblock %} +
    + + + + diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/core/snapshot_list.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/core/snapshot_list.html new file mode 100644 index 0000000..ce2b2fa --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/core/snapshot_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% load static %} + +{% block body %} +
    +
    + + + +
    + + + + + + + + + + + {% for link in object_list %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSnapshot ({{object_list|length}})FilesOriginal URL
    +
    + + {% if page_obj.has_previous %} + « first + previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next + last » + {% endif %} + + + {% if page_obj.has_next %} + next + last » + {% endif %} + +
    +
    +{% endblock %} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/link_details.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/link_details.html new file mode 100644 index 0000000..b1edcfe --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/link_details.html @@ -0,0 +1,488 @@ + + + + {{title}} + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    Added
    + {{bookmarked_date}} +
    +
    +
    First Archived
    + {{oldest_archive_date}} +
    +
    +
    Last Checked
    + {{updated_date}} +
    +
    +
    +
    +
    Type
    +
    {{extension}}
    +
    +
    +
    Tags
    +
    {{tags}}
    +
    +
    +
    Status
    +
    {{status}}
    +
    +
    +
    Saved
    + ✅ {{num_outputs}} +
    +
    +
    Errors
    + ❌ {{num_failures}} +
    +
    +
    Size
    + {{size}} +
    +
    +
    +
    +
    🗃 Files
    + JSON | + WARC | + Media | + Git | + Favicon | + See all... +
    +
    +
    +
    +
    +
    + +
    + + + +

    Wget > WARC

    +

    archive/{{domain}}

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > SingleFile

    +

    archive/singlefile.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Archive.Org

    +

    web.archive.org/web/...

    +
    +
    +
    +
    +
    + +
    + + + +

    Original

    +

    {{domain}}

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > PDF

    +

    archive/output.pdf

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > Screenshot

    +

    archive/screenshot.png

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > HTML

    +

    archive/output.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Readability

    +

    archive/readability/...

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    mercury

    +

    archive/mercury/...

    +
    +
    +
    +
    +
    +
    + + + + + + + + diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index.html new file mode 100644 index 0000000..95af196 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index.html @@ -0,0 +1,255 @@ +{% load static %} + + + + + Archived Sites + + + + + + + + + +
    +
    + +
    +
    + + + + + + + + + + + + {% for link in links %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_minimal.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_minimal.html new file mode 100644 index 0000000..dcfaa23 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_minimal.html @@ -0,0 +1,24 @@ + + + + Archived Sites + + + + + + + + + + + + + + {% for link in links %} + {% include "main_index_row.html" with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + \ No newline at end of file diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_row.html b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_row.html new file mode 100644 index 0000000..5e21a8c --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/main_index_row.html @@ -0,0 +1,22 @@ +{% load static %} + + + {% if link.bookmarked_date %} {{ link.bookmarked_date }} {% else %} {{ link.added }} {% endif %} + + {% if link.is_archived %} + + {% else %} + + {% endif %} + + {{link.title|default:'Loading...'}} + {% if link.tags_str != None %} {{link.tags_str|default:''}} {% else %} {{ link.tags|default:'' }} {% endif %} + + + + 📄 + {% if link.icons %} {{link.icons}} {% else %} {{ link.num_outputs}} {% endif %} + + + {{link.url}} + \ No newline at end of file diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/add.css b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/add.css new file mode 100644 index 0000000..b128bf4 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/add.css @@ -0,0 +1,62 @@ +.dashboard #content { + width: 100%; + margin-right: 0px; + margin-left: 0px; +} +#submit { + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 10px; + border-radius: 4px; + background-color: #f5dd5d; + color: #333; + font-size: 18px; + font-weight: 800; +} +#add-form button[role="submit"]:hover { + background-color: #e5cd4d; +} +#add-form label { + display: block; + font-size: 16px; +} +#add-form textarea { + width: 100%; + min-height: 300px; +} +#delay-warning div { + border: 1px solid red; + border-radius: 4px; + margin: 10px; + padding: 10px; + font-size: 15px; + background-color: #f5dd5d; +} +#stdout { + background-color: #ded; + padding: 10px 10px; + border-radius: 4px; + white-space: normal; +} +ul#id_depth { + list-style-type: none; + padding: 0; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/admin.css b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/admin.css new file mode 100644 index 0000000..181c06d --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/admin.css @@ -0,0 +1,234 @@ +#logo { + height: 30px; + vertical-align: -6px; + padding-right: 5px; +} +#site-name:hover a { + opacity: 0.9; +} +#site-name .loader { + height: 25px; + width: 25px; + display: inline-block; + border-width: 3px; + vertical-align: -3px; + margin-right: 5px; + margin-top: 2px; +} +#branding h1, #branding h1 a:link, #branding h1 a:visited { + color: mintcream; +} +#header { + background: #aa1e55; + padding: 6px 14px; +} +#content { + padding: 8px 8px; +} +#user-tools { + font-size: 13px; + +} + +div.breadcrumbs { + background: #772948; + color: #f5dd5d; + padding: 6px 15px; +} + +body.model-snapshot.change-list div.breadcrumbs, +body.model-snapshot.change-list #content .object-tools { + display: none; +} + +.module h2, .module caption, .inline-group h2 { + background: #772948; +} + +#content .object-tools { + margin-top: -35px; + margin-right: -10px; + float: right; +} + +#content .object-tools a:link, #content .object-tools a:visited { + border-radius: 0px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; +} + +#content .object-tools a.addlink { + background-blend-mode: difference; +} + +#content #changelist #toolbar { + padding: 0px; + background: none; + margin-bottom: 10px; + border-top: 0px; + border-bottom: 0px; +} + +#content #changelist #toolbar form input[type="submit"] { + border-color: #aa1e55; +} + +#content #changelist-filter li.selected a { + color: #aa1e55; +} + + +/*#content #changelist .actions { + position: fixed; + bottom: 0px; + z-index: 800; +}*/ +#content #changelist .actions { + float: right; + margin-top: -34px; + padding: 0px; + background: none; + margin-right: 0px; + width: auto; +} + +#content #changelist .actions .button { + border-radius: 2px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; + margin-right: 4px; + box-shadow: 4px 4px 4px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.08); +} +#content #changelist .actions .button:hover { + border: 1px solid rgba(0,0,0,0.2); + opacity: 0.9; +} +#content #changelist .actions .button[name=verify_snapshots], #content #changelist .actions .button[name=update_titles] { + background-color: #dedede; + color: #333; +} +#content #changelist .actions .button[name=update_snapshots] { + background-color:lightseagreen; + color: #333; +} +#content #changelist .actions .button[name=overwrite_snapshots] { + background-color: #ffaa31; + color: #333; +} +#content #changelist .actions .button[name=delete_snapshots] { + background-color: #f91f74; + color: rgb(255 248 252 / 64%); +} + + +#content #changelist-filter h2 { + border-radius: 4px 4px 0px 0px; +} + +@media (min-width: 767px) { + #content #changelist-filter { + top: 35px; + width: 110px; + margin-bottom: 35px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered div.xfull { + margin-right: 115px; + } +} + +@media (max-width: 1127px) { + #content #changelist .actions { + position: fixed; + bottom: 6px; + left: 10px; + float: left; + z-index: 1000; + } +} + +#content a img.favicon { + height: 20px; + width: 20px; + vertical-align: -5px; + padding-right: 6px; +} + +#content td, #content th { + vertical-align: middle; + padding: 4px; +} + +#content #changelist table input { + vertical-align: -2px; +} + +#content thead th .text a { + padding: 8px 4px; +} + +#content th.field-added, #content td.field-updated { + word-break: break-word; + min-width: 128px; + white-space: normal; +} + +#content th.field-title_str { + min-width: 300px; +} + +#content td.field-files { + white-space: nowrap; +} +#content td.field-files .exists-True { + opacity: 1; +} +#content td.field-files .exists-False { + opacity: 0.1; + filter: grayscale(100%); +} +#content td.field-size { + white-space: nowrap; +} + +#content td.field-url_str { + word-break: break-all; + min-width: 200px; +} + +#content tr b.status-pending { + font-weight: 200; + opacity: 0.6; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.tags > a > .tag { + float: right; + border-radius: 5px; + background-color: #bfdfff; + padding: 2px 5px; + margin-left: 4px; + margin-top: 1px; +} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/archive.png b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/archive.png new file mode 100644 index 0000000000000000000000000000000000000000..307b45013382851ef1026e111921bd94ba55af1d GIT binary patch literal 17730 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:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/external.png b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/external.png new file mode 100755 index 0000000000000000000000000000000000000000..7e1a5f02aebccd4dcc6b1b0e3040c66ee84270a8 GIT binary patch literal 1647 zcmV-#29WuQP)gwIy-SzeLx3{-TOH0Pa z#^mJWg@uK_zP?jaQ@FUeMn*<ssP45C&jS0Z#=KL|K+q*ZaT6T^=ZwNzx>f$;{MezMmFr z-hyp&pfeg-EEbE!VzF2(7K_DVk;{Eq7SipHWdCbiob5_l5zgwP+;_-S8WPHi#`iyW z(v(2RfaCG2w8fhTLg82%;|yPEGBw{8Lj0lBOs#fPD z0xTl};WJsQGjbahmJlz6v?G;inIw(73980_q z`d%=(mLkC*iCv-ZJo+UDa)Y~&b%mbIJ28$M5`4gFr46&Z?!L`zQe3aQrAs1=ee8qz9E1kOAI z6T=WD0&f#W;B*SpfpMZrVGb}#)F{jY#)t}qxxfffpD-WTPgEzw0DFnrgg9UyQJD}6 z>>=tB;(=kJDq#sQNYo@O1BQr-gr&d$QID`3=qIWXQUJX~EkYWgkGMW<5f)MbJ;Zhb zMpj4%bcyYE@!4@ZcN3DaUi$(S%LCX|c`(_%u&m@q9Sl#B_}VnWH7FfA&d zc6|ygYJl5|&L@^11ICj-aeIMJoyUwA)e2a(@&~sUSUF%F{}r}?rv0YbO`wvrMBv8@ zTeblf0A7%`vLtZP3buL*tWmA!9}!kh!B$FxM@mhR5~qj}_R|F~7iz8-usZQy^q!y) zdD)?kbL8=7d8b(Xl(3!ne8Oho@3!1BvKsE(oa0_nEX~>1xFKsOHsMEEV&W;%DP7je zvrxR`sQZcm%hq`G9CPd~+c9v}{~?kWeqR;)vP15z=)95oXMFLs=CN?6{*oqJiI3_F zyiY!+_{8Af?RYZXah`Kludr9&ro3cf6WH_7$&$Z4EqEh1d}K9.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.dataTables.min.js b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.dataTables.min.js new file mode 100644 index 0000000..07af1c3 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.dataTables.min.js @@ -0,0 +1,166 @@ +/*! + DataTables 1.10.19 + ©2008-2018 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Z(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Z(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Z(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=n.defaults.oLanguage,c=b.sDecimal;c&&Da(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&(d&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(d&&"Loading..."===b.sLoadingRecords)&&F(a, +a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Da(a)}}function fb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%": +"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1, +overflow:"hidden"}).append(h("
    ").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("
    ").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,n.__browser);a.oScroll.iBarWidth=n.__browser.barWidth} +function ib(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=n.defaults.column,d=a.aoColumns.length,c=h.extend({},n.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},n.models.oSearch,c[d]);ka(a,d,h(b).data())}function ka(a,b,c){var b=a.aoColumns[b], +d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(gb(c),J(n.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=S(g),i=b.mRender? +S(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return N(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone, +b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function $(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],m);else if("string"=== +typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d, +1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(m.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(m.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,m=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!mb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:m;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("",{valign:"top",colSpan:V(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,m,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,m,i]);d=h(a.nTBody);d.children().detach(); +d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&nb(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;P(a);a._drawHold=!1}function ob(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore= +a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,m,l,q,k=0;k")[0];m=f[k+1];if("'"==m||'"'==m){l="";for(q=2;f[k+q]!=m;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(m=l.split("."),i.id=m[0].substr(1,m[0].length-1),i.className=m[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=pb(a);else if("f"==j&& +d.bFilter)g=qb(a);else if("r"==j&&d.bProcessing)g=rb(a);else if("t"==j)g=sb(a);else if("i"==j&&d.bInfo)g=tb(a);else if("p"==j&&d.bPaginate)g=ub(a);else if(0!==n.ext.feature.length){i=n.ext.feature;q=0;for(m=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_", +g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ra(a,h(this).val());P(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a=== +c&&h("select",i).val(d)});return i[0]}function ub(a){var b=a.sPaginationType,c=n.ext.pager[b],d="function"===typeof c,e=function(a){P(a)},b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]} +function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");r(a,null,"processing",[a,b])}function sb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),m=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden", +position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", +{"class":f.sScrollFootInner}).append(m.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:la,sName:"scrolling"});return i[0]}function la(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth, +f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,m=j.children("table"),j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),n=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),Q,L,R,w,Ua=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!== +L&&a.scrollBarVis!==k)a.scrollBarVis=L,$(a);else{a.scrollBarVis=L;p.children("thead, tfoot").remove();u&&(R=u.clone().prependTo(p),Q=u.find("tr"),R=R.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=aa(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},R);f=p.outerWidth();if(""===c){r.width="100%";if(U&&(p.find("tbody").height()>j.offsetHeight|| +"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=v(d),f=p.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Ua.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ua[b]},o);h(L).height(0);u&&(I(C,R),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},R),I(function(a,b){a.style.width=y[b]},Q),h(R).height(0));I(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.childNodes[0].style.height= +"0";a.childNodes[0].style.overflow="hidden";a.style.width=Ua[b]},L);u&&I(function(a,b){a.innerHTML='
    '+A[b]+"
    ";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=y[b]},R);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(Q-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else Q="100%";q.width=v(Q); +g.width=v(Q);u&&(a.nScrollFoot.style.width=v(Q));!e&&U&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();m[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(n[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0, +f=b.length,g,j;e").appendTo(j.find("tbody"));j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");m=ra(a,j.find("thead")[0]);for(n=0;n").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(n=0;n").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a, +b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function X(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var m=[];f=function(a){a.length&& +!h.isArray(a[0])?m.push(a):h.merge(m,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,n=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=X(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=n.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ba(a,b)));for(var f,g=n.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!==k&&h.extend(a.oPreviousSearch,Cb(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Na(a,b){var c=a.renderer,d=n.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"=== +typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Lb.numbers_length,d=Math.floor(c/2);b<=c?c=Y(0,b):a<=d?(c=Y(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=Y(b-(c-2),b):(c=Y(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function Da(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Ya)},"html-num":function(b){return za(b, +a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Ya)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Mb(a){return function(){var b=[ya(this[n.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return n.ext.internal[a].apply(this,b)}}var n=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)}; +this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&la(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a, +b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data(): +c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]}; +this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust(); +(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in n.ext.internal)e&&(this[e]=Mb(e));this.each(function(){var e={},g=1").appendTo(q)); +p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){p.nTFoot=b[0];ea(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Ya=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Ob=function(a){var b=parseInt(a,10);return!isNaN(b)&& +isFinite(a)?b:null},Pb=function(a,b){Za[b]||(Za[b]=RegExp(Qa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Za[b],"."):a},$a=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Pb(a,b));c&&d&&(a=a.replace(Ya,""));return!isNaN(parseFloat(a))&&isFinite(a)},Qb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:$a(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb= +/<.*?>/g,Oa=n.util.throttle,Sb=[],w=Array.prototype,ac=function(a){var b,c,d=n.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof +s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=V(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e); +c._detailsShow&&c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Ub(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Ub(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){db(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Vb=function(a,b, +c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Vb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc): +"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var n=h.map(g,function(a,b){return a.bVisible?b:null});return[n[n.length+b]]}return[aa(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)}, +1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Vb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData}, +1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData, +i,m,l;if(a!==k&&g.bVisible!==a){if(a){var n=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(m=j.length;id;return!0};n.isDataTable= +n.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof n.Api)return!0;h.each(n.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};n.tables=n.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(n.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};n.camelToHungarian=J;o("$()",function(a,b){var c= +this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){oa(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a= +this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT"); +h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable), +(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,n.settings);-1!==c&&n.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=S(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]: +a._);return a.replace("%d",c)});n.version="1.10.19";n.settings=[];n.models={};n.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};n.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};n.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null, +sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};n.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1, +bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+ +a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"}, +oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({}, +n.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Z(n.defaults);n.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null}; +Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[], +aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button", +iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal: +this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{}, +classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); +h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled", +sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"", +sJUIHeader:"",sJUIFooter:""});var Lb=n.ext.pager;h.extend(Lb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e, +f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,n=0,o=function(b,d){var k,s,u,r,v=function(b){Ta(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{m=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":m=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m= +j.sNext;l=r+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":n,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(m).appendTo(b);Wa(u,{action:r},v);n++}}}},s;try{s=h(b).find(H.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+ +s+"]").focus()}}});h.extend(n.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)|| +"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(n.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," ").replace(Aa,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Pb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return M(a)? +"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Da("");h.extend(!0,n.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc: +c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]== +"asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var eb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g,"""):a};n.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return eb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g, +a)+f+(e||"")}}},text:function(){return{display:eb,filter:eb}}};h.extend(n.ext.internal,{_fnExternApiFunc:Mb,_fnBuildAjax:sa,_fnAjaxUpdate:mb,_fnAjaxParameters:vb,_fnAjaxUpdateDraw:wb,_fnAjaxDataSrc:ta,_fnAddColumn:Ea,_fnColumnOptions:ka,_fnAdjustColumnSizing:$,_fnVisibleToColumnIndex:aa,_fnColumnIndexToVisible:ba,_fnVisbleColumns:V,_fnGetColumns:ma,_fnColumnTypes:Ga,_fnApplyColumnDefs:jb,_fnHungarianMap:Z,_fnCamelToHungarian:J,_fnLanguageCompat:Ca,_fnBrowserDetect:hb,_fnAddData:O,_fnAddTr:na,_fnNodeToDataIndex:function(a, +b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:kb,_fnSplitObjNotation:Ja,_fnGetObjectDataFn:S,_fnSetObjectDataFn:N,_fnGetDataMaster:Ka,_fnClearTable:oa,_fnDeleteIndex:pa,_fnInvalidate:da,_fnGetRowElements:Ia,_fnCreateTr:Ha,_fnBuildHead:lb,_fnDrawHead:fa,_fnDraw:P,_fnReDraw:T,_fnAddOptionsHtml:ob,_fnDetectHeader:ea,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:qb,_fnFilterComplete:ga,_fnFilterCustom:zb, +_fnFilterColumn:yb,_fnFilter:xb,_fnFilterCreateSearch:Pa,_fnEscapeRegex:Qa,_fnFilterData:Ab,_fnFeatureHtmlInfo:tb,_fnUpdateInfo:Db,_fnInfoMacros:Eb,_fnInitialise:ha,_fnInitComplete:ua,_fnLengthChange:Ra,_fnFeatureHtmlLength:pb,_fnFeatureHtmlPaginate:ub,_fnPageChange:Ta,_fnFeatureHtmlProcessing:rb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:sb,_fnScrollDraw:la,_fnApplyToChildren:I,_fnCalculateColumnWidths:Fa,_fnThrottle:Oa,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:v, +_fnSortFlatten:X,_fnSort:nb,_fnSortAria:Jb,_fnSortListener:Va,_fnSortAttachListener:Ma,_fnSortingClasses:wa,_fnSortData:Ib,_fnSaveState:xa,_fnLoadState:Kb,_fnSettingsFromNode:ya,_fnLog:K,_fnMap:F,_fnBindAction:Wa,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Sa,_fnRenderer:Na,_fnDataSource:y,_fnRowAttributes:La,_fnExtend:Xa,_fnCalculateEnd:function(){}});h.fn.dataTable=n;n.$=h;h.fn.dataTableSettings=n.settings;h.fn.dataTableExt=n.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()}; +h.each(n,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.min.js b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/default/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + +
    +
    + +
    +
    + + + + + + + + + + $rows +
    BookmarkedSnapshot ($num_links)FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/build/lib/archivebox/themes/legacy/main_index_row.html b/archivebox-0.5.3/build/lib/archivebox/themes/legacy/main_index_row.html new file mode 100644 index 0000000..9112eac --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/themes/legacy/main_index_row.html @@ -0,0 +1,16 @@ + + $bookmarked_date + + + + $title + $tags + + + + 📄 + $num_outputs + + + $url + diff --git a/archivebox-0.5.3/build/lib/archivebox/util.py b/archivebox-0.5.3/build/lib/archivebox/util.py new file mode 100644 index 0000000..5530ab4 --- /dev/null +++ b/archivebox-0.5.3/build/lib/archivebox/util.py @@ -0,0 +1,318 @@ +__package__ = 'archivebox' + +import re +import requests +import json as pyjson + +from typing import List, Optional, Any +from pathlib import Path +from inspect import signature +from functools import wraps +from hashlib import sha256 +from urllib.parse import urlparse, quote, unquote +from html import escape, unescape +from datetime import datetime +from dateparser import parse as dateparser +from requests.exceptions import RequestException, ReadTimeout + +from .vendor.base32_crockford import encode as base32_encode # type: ignore +from w3lib.encoding import html_body_declared_encoding, http_content_type_encoding + +try: + import chardet + detect_encoding = lambda rawdata: chardet.detect(rawdata)["encoding"] +except ImportError: + detect_encoding = lambda rawdata: "utf-8" + +### Parsing Helpers + +# All of these are (str) -> str +# shortcuts to: https://docs.python.org/3/library/urllib.parse.html#url-parsing +scheme = lambda url: urlparse(url).scheme.lower() +without_scheme = lambda url: urlparse(url)._replace(scheme='').geturl().strip('//') +without_query = lambda url: urlparse(url)._replace(query='').geturl().strip('//') +without_fragment = lambda url: urlparse(url)._replace(fragment='').geturl().strip('//') +without_path = lambda url: urlparse(url)._replace(path='', fragment='', query='').geturl().strip('//') +path = lambda url: urlparse(url).path +basename = lambda url: urlparse(url).path.rsplit('/', 1)[-1] +domain = lambda url: urlparse(url).netloc +query = lambda url: urlparse(url).query +fragment = lambda url: urlparse(url).fragment +extension = lambda url: basename(url).rsplit('.', 1)[-1].lower() if '.' in basename(url) else '' +base_url = lambda url: without_scheme(url) # uniq base url used to dedupe links + +without_www = lambda url: url.replace('://www.', '://', 1) +without_trailing_slash = lambda url: url[:-1] if url[-1] == '/' else url.replace('/?', '?') +hashurl = lambda url: base32_encode(int(sha256(base_url(url).encode('utf-8')).hexdigest(), 16))[:20] + +urlencode = lambda s: s and quote(s, encoding='utf-8', errors='replace') +urldecode = lambda s: s and unquote(s) +htmlencode = lambda s: s and escape(s, quote=True) +htmldecode = lambda s: s and unescape(s) + +short_ts = lambda ts: str(parse_date(ts).timestamp()).split('.')[0] +ts_to_date = lambda ts: ts and parse_date(ts).strftime('%Y-%m-%d %H:%M') +ts_to_iso = lambda ts: ts and parse_date(ts).isoformat() + + +URL_REGEX = re.compile( + r'http[s]?://' # start matching from allowed schemes + r'(?:[a-zA-Z]|[0-9]' # followed by allowed alphanum characters + r'|[$-_@.&+]|[!*\(\),]' # or allowed symbols + r'|(?:%[0-9a-fA-F][0-9a-fA-F]))' # or allowed unicode bytes + r'[^\]\[\(\)<>"\'\s]+', # stop parsing at these symbols + re.IGNORECASE, +) + +COLOR_REGEX = re.compile(r'\[(?P\d+)(;(?P\d+)(;(?P\d+))?)?m') + +def is_static_file(url: str): + # TODO: the proper way is with MIME type detection + ext, not only extension + from .config import STATICFILE_EXTENSIONS + return extension(url).lower() in STATICFILE_EXTENSIONS + + +def enforce_types(func): + """ + Enforce function arg and kwarg types at runtime using its python3 type hints + """ + # TODO: check return type as well + + @wraps(func) + def typechecked_function(*args, **kwargs): + sig = signature(func) + + def check_argument_type(arg_key, arg_val): + try: + annotation = sig.parameters[arg_key].annotation + except KeyError: + annotation = None + + if annotation is not None and annotation.__class__ is type: + if not isinstance(arg_val, annotation): + raise TypeError( + '{}(..., {}: {}) got unexpected {} argument {}={}'.format( + func.__name__, + arg_key, + annotation.__name__, + type(arg_val).__name__, + arg_key, + str(arg_val)[:64], + ) + ) + + # check args + for arg_val, arg_key in zip(args, sig.parameters): + check_argument_type(arg_key, arg_val) + + # check kwargs + for arg_key, arg_val in kwargs.items(): + check_argument_type(arg_key, arg_val) + + return func(*args, **kwargs) + + return typechecked_function + + +def docstring(text: Optional[str]): + """attach the given docstring to the decorated function""" + def decorator(func): + if text: + func.__doc__ = text + return func + return decorator + + +@enforce_types +def str_between(string: str, start: str, end: str=None) -> str: + """(12345, , ) -> 12345""" + + content = string.split(start, 1)[-1] + if end is not None: + content = content.rsplit(end, 1)[0] + + return content + + +@enforce_types +def parse_date(date: Any) -> Optional[datetime]: + """Parse unix timestamps, iso format, and human-readable strings""" + + if date is None: + return None + + if isinstance(date, datetime): + return date + + if isinstance(date, (float, int)): + date = str(date) + + if isinstance(date, str): + return dateparser(date) + + raise ValueError('Tried to parse invalid date! {}'.format(date)) + + +@enforce_types +def download_url(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the text""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + ) + + content_type = response.headers.get('Content-Type', '') + encoding = http_content_type_encoding(content_type) or html_body_declared_encoding(response.text) + + if encoding is not None: + response.encoding = encoding + + return response.text + +@enforce_types +def get_headers(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the headers""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + + try: + response = requests.head( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + allow_redirects=True, + ) + if response.status_code >= 400: + raise RequestException + except ReadTimeout: + raise + except RequestException: + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + stream=True + ) + + return pyjson.dumps(dict(response.headers), indent=4) + + +@enforce_types +def chrome_args(**options) -> List[str]: + """helper to build up a chrome shell command with arguments""" + + from .config import CHROME_OPTIONS + + options = {**CHROME_OPTIONS, **options} + + cmd_args = [options['CHROME_BINARY']] + + if options['CHROME_HEADLESS']: + cmd_args += ('--headless',) + + if not options['CHROME_SANDBOX']: + # assume this means we are running inside a docker container + # in docker, GPU support is limited, sandboxing is unecessary, + # and SHM is limited to 64MB by default (which is too low to be usable). + cmd_args += ( + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + ) + + + if not options['CHECK_SSL_VALIDITY']: + cmd_args += ('--disable-web-security', '--ignore-certificate-errors') + + if options['CHROME_USER_AGENT']: + cmd_args += ('--user-agent={}'.format(options['CHROME_USER_AGENT']),) + + if options['RESOLUTION']: + cmd_args += ('--window-size={}'.format(options['RESOLUTION']),) + + if options['TIMEOUT']: + cmd_args += ('--timeout={}'.format((options['TIMEOUT']) * 1000),) + + if options['CHROME_USER_DATA_DIR']: + cmd_args.append('--user-data-dir={}'.format(options['CHROME_USER_DATA_DIR'])) + + return cmd_args + + +def ansi_to_html(text): + """ + Based on: https://stackoverflow.com/questions/19212665/python-converting-ansi-color-codes-to-html + """ + from .config import COLOR_DICT + + TEMPLATE = '
    ' + text = text.replace('[m', '
    ') + + def single_sub(match): + argsdict = match.groupdict() + if argsdict['arg_3'] is None: + if argsdict['arg_2'] is None: + _, color = 0, argsdict['arg_1'] + else: + _, color = argsdict['arg_1'], argsdict['arg_2'] + else: + _, color = argsdict['arg_3'], argsdict['arg_2'] + + return TEMPLATE.format(COLOR_DICT[color][0]) + + return COLOR_REGEX.sub(single_sub, text) + + +class AttributeDict(dict): + """Helper to allow accessing dict values via Example.key or Example['key']""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Recursively convert nested dicts to AttributeDicts (optional): + # for key, val in self.items(): + # if isinstance(val, dict) and type(val) is not AttributeDict: + # self[key] = AttributeDict(val) + + def __getattr__(self, attr: str) -> Any: + return dict.__getitem__(self, attr) + + def __setattr__(self, attr: str, value: Any) -> None: + return dict.__setitem__(self, attr, value) + + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif isinstance(obj, Path): + return str(obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + diff --git a/archivebox-0.5.3/build/lib/archivebox/vendor/__init__.py b/archivebox-0.5.3/build/lib/archivebox/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/.debhelper/generated/archivebox/installed-by-dh_installdocs b/archivebox-0.5.3/debian/.debhelper/generated/archivebox/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/archivebox.debhelper.log b/archivebox-0.5.3/debian/archivebox.debhelper.log new file mode 100644 index 0000000..de33f70 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox.debhelper.log @@ -0,0 +1,19 @@ +dh_update_autotools_config +dh_auto_configure +dh_auto_build +dh_auto_test +dh_prep +dh_auto_install +dh_installdocs +dh_installchangelogs +dh_installinit +dh_perl +dh_link +dh_strip_nondeterminism +dh_compress +dh_fixperms +dh_missing +dh_installdeb +dh_gencontrol +dh_md5sums +dh_builddeb diff --git a/archivebox-0.5.3/debian/archivebox.postinst.debhelper b/archivebox-0.5.3/debian/archivebox.postinst.debhelper new file mode 100644 index 0000000..2c9b172 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3: +if which py3compile >/dev/null 2>&1; then + py3compile -p archivebox +fi +if which pypy3compile >/dev/null 2>&1; then + pypy3compile -p archivebox || true +fi + +# End automatically added section diff --git a/archivebox-0.5.3/debian/archivebox.prerm.debhelper b/archivebox-0.5.3/debian/archivebox.prerm.debhelper new file mode 100644 index 0000000..282f72d --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3: +if which py3clean >/dev/null 2>&1; then + py3clean -p archivebox +else + dpkg -L archivebox | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/archivebox-0.5.3/debian/archivebox.substvars b/archivebox-0.5.3/debian/archivebox.substvars new file mode 100644 index 0000000..914fa79 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-atomicwrites, python3-croniter, python3-crontab, python3-dateparser, python3-django, python3-django-extensions, python3-ipython, python3-mypy-extensions, python3-requests, python3-w3lib, python3:any, youtube-dl +misc:Depends= +misc:Pre-Depends= diff --git a/archivebox-0.5.3/debian/archivebox/DEBIAN/control b/archivebox-0.5.3/debian/archivebox/DEBIAN/control new file mode 100644 index 0000000..a03f45b --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/DEBIAN/control @@ -0,0 +1,30 @@ +Package: archivebox +Version: 0.5.3-1 +Architecture: all +Maintainer: Nick Sweeting +Installed-Size: 938 +Depends: python3-atomicwrites, python3-croniter, python3-crontab, python3-dateparser, python3-django, python3-django-extensions, python3-ipython, python3-mypy-extensions, python3-requests, python3-w3lib, python3:any, youtube-dl, nodejs, chromium-browser, wget, curl, git, ffmpeg, python3-django-jsonfield, ripgrep +Section: python +Priority: optional +Homepage: https://github.com/ArchiveBox/ArchiveBox +Description: The self-hosted internet archive. +
    + +

    ArchiveBox
    The open-source self-hosted web archive.

    + . + ▶️ Quickstart | + Demo | + Github | + Documentation | + Info & Motivation | + Community | + Roadmap + . +
    + "Your own personal internet archive" (网站存档 / 爬虫)
    + 
    + . + + . + + diff --git a/archivebox-0.5.3/debian/archivebox/DEBIAN/md5sums b/archivebox-0.5.3/debian/archivebox/DEBIAN/md5sums new file mode 100644 index 0000000..2f1db73 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/DEBIAN/md5sums @@ -0,0 +1,126 @@ +20e0eff07a6f27bb640067c750a3b328 usr/bin/archivebox +b5ad0d3290ff6a43ce15685c2bdeb420 usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/dependency_links.txt +5b15ee07b1a58d2f77efc56c47c66c01 usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/entry_points.txt +10c6a7ca4e7ce3913366a1d3a5ec7635 usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/requires.txt +e65a9b928b38091ffd6a9142efc76d7f usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/top_level.txt +a2f11497252da68a947de7436621ebb4 usr/lib/python3/dist-packages/archivebox/.flake8 +688591e55f2399c2dccca18dc133adc7 usr/lib/python3/dist-packages/archivebox/LICENSE +ca7d59f8588de5f91eed226dd5269833 usr/lib/python3/dist-packages/archivebox/README.md +05cc08c4f7a16eb7b3034371e81ab34e usr/lib/python3/dist-packages/archivebox/__init__.py +2044702060327d07cf4b394f1ee8f004 usr/lib/python3/dist-packages/archivebox/__main__.py +215ae089a12c854f4c5cf54c3537c0dc usr/lib/python3/dist-packages/archivebox/cli/__init__.py +a0235679ec347b17a4da2b7de46058b5 usr/lib/python3/dist-packages/archivebox/cli/archivebox_add.py +4c7a0dbbe6427e3dfe72c71b1f06e105 usr/lib/python3/dist-packages/archivebox/cli/archivebox_config.py +dea57a0d33a8f589017f384b57caf915 usr/lib/python3/dist-packages/archivebox/cli/archivebox_help.py +542e7fd0b458d2d0ebecb9e0054e2476 usr/lib/python3/dist-packages/archivebox/cli/archivebox_init.py +140bffcb8c334f9926df3700fc3a7b3c usr/lib/python3/dist-packages/archivebox/cli/archivebox_list.py +c0dae677c828d80ef6055df7590eb7b1 usr/lib/python3/dist-packages/archivebox/cli/archivebox_manage.py +a76b9bb7a4db20dd1f06cd713ed19882 usr/lib/python3/dist-packages/archivebox/cli/archivebox_oneshot.py +26b9498e1438087df04ddf8769d3ecdb usr/lib/python3/dist-packages/archivebox/cli/archivebox_remove.py +81b171b3cbc2b4b519854610bd4eda28 usr/lib/python3/dist-packages/archivebox/cli/archivebox_schedule.py +217edd5037476e0e435d586f30cdd87f usr/lib/python3/dist-packages/archivebox/cli/archivebox_server.py +75e28b47f4a314a4ad3b45e3b2d2372f usr/lib/python3/dist-packages/archivebox/cli/archivebox_shell.py +abe650a38c3ca70f361b60cac752100a usr/lib/python3/dist-packages/archivebox/cli/archivebox_status.py +ef3c2139225b3e1721384ba8378711d0 usr/lib/python3/dist-packages/archivebox/cli/archivebox_update.py +70baac1da5d511ad1168d85024460cfd usr/lib/python3/dist-packages/archivebox/cli/archivebox_version.py +60a8430087b9ab81e9c575d79869b5bf usr/lib/python3/dist-packages/archivebox/cli/tests.py +025f92c65e10ba509dc8895ca314f67e usr/lib/python3/dist-packages/archivebox/config.py +c374043c2f120c86275ec8c3374bdd7d usr/lib/python3/dist-packages/archivebox/config_stubs.py +7eda7ac9e0c4c4451faf49cf133fd49d usr/lib/python3/dist-packages/archivebox/core/__init__.py +c3c7c8f948eddd54e529d4f816c51659 usr/lib/python3/dist-packages/archivebox/core/admin.py +3416954d5516f91f182e1b0424f0e42e usr/lib/python3/dist-packages/archivebox/core/apps.py +cf16db9fb86e9cda3fcc837f425df114 usr/lib/python3/dist-packages/archivebox/core/forms.py +06b6ed7e1e2a6534697da15278638bc9 usr/lib/python3/dist-packages/archivebox/core/management/commands/archivebox.py +11863b1f400e6f3355fe6fc510758fb1 usr/lib/python3/dist-packages/archivebox/core/migrations/0001_initial.py +4725a5d030235d794cf6376b745e1b91 usr/lib/python3/dist-packages/archivebox/core/migrations/0002_auto_20200625_1521.py +d1d0c9fb198fa8cb65b9139af1287463 usr/lib/python3/dist-packages/archivebox/core/migrations/0003_auto_20200630_1034.py +9b0a3b9a6d15cdd8570be9d3279b3980 usr/lib/python3/dist-packages/archivebox/core/migrations/0004_auto_20200713_1552.py +0c7dbc9ff489bcb7d30e6e75472db1ff usr/lib/python3/dist-packages/archivebox/core/migrations/0005_auto_20200728_0326.py +5c61293713417e23dced052c7c1179dd usr/lib/python3/dist-packages/archivebox/core/migrations/0006_auto_20201012_1520.py +2515235c69e97ef56a2cf841eda2e4d5 usr/lib/python3/dist-packages/archivebox/core/migrations/0007_archiveresult.py +527ec8a2a550cd46b2665459c5135fd3 usr/lib/python3/dist-packages/archivebox/core/migrations/0008_auto_20210105_1421.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/archivebox/core/migrations/__init__.py +fd0a90ed401f1a88cef4b0a17d2a61da usr/lib/python3/dist-packages/archivebox/core/mixins.py +b5206bcea85425e9061ebb38e2775a99 usr/lib/python3/dist-packages/archivebox/core/models.py +ef513d4b93017ff1e2b8123494596134 usr/lib/python3/dist-packages/archivebox/core/settings.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/archivebox/core/templatetags/__init__.py +96c3038e9d1b691917163c51cb8278b5 usr/lib/python3/dist-packages/archivebox/core/templatetags/core_tags.py +7921097fb98105f70ed684323aedfaa2 usr/lib/python3/dist-packages/archivebox/core/tests.py +09145de4c13f82f60cc1b3d85f3093b0 usr/lib/python3/dist-packages/archivebox/core/urls.py +7fb664948dc7792a44209fe146631b24 usr/lib/python3/dist-packages/archivebox/core/views.py +42c5c88dde758dc2b236480e0ad72538 usr/lib/python3/dist-packages/archivebox/core/welcome_message.py +f0c203511cfb622d117ead1efdfb8453 usr/lib/python3/dist-packages/archivebox/core/wsgi.py +084793aecfc2e145c566dafca9a5caf6 usr/lib/python3/dist-packages/archivebox/extractors/__init__.py +83c409be44edf3266bdb34f31f51177c usr/lib/python3/dist-packages/archivebox/extractors/archive_org.py +7d6f2948c16817f000f232047981b2ba usr/lib/python3/dist-packages/archivebox/extractors/dom.py +b3a7de648a59fe2bf23b99666ab81da3 usr/lib/python3/dist-packages/archivebox/extractors/favicon.py +55bb4fa43d994f9bb5127e532be42b2b usr/lib/python3/dist-packages/archivebox/extractors/git.py +e3938e2aed4a7661b1d91091b019af77 usr/lib/python3/dist-packages/archivebox/extractors/headers.py +70137ea70959bd68278c912618d69cd7 usr/lib/python3/dist-packages/archivebox/extractors/media.py +4901e04ffbb39b93c9a184ef8cbafb69 usr/lib/python3/dist-packages/archivebox/extractors/mercury.py +444e061240cba77435ebadb4b504e576 usr/lib/python3/dist-packages/archivebox/extractors/pdf.py +0761c38461ef6c1a4fe47d5b0b9f1584 usr/lib/python3/dist-packages/archivebox/extractors/readability.py +45cb0ccad39c46fea47a6161dbad349e usr/lib/python3/dist-packages/archivebox/extractors/screenshot.py +88f0dcb2a274affaceacd00f54b2f142 usr/lib/python3/dist-packages/archivebox/extractors/singlefile.py +e5287f7969e4e3e27b661002a14c1be6 usr/lib/python3/dist-packages/archivebox/extractors/title.py +2d333caa3aa0ed9042d74517a5653446 usr/lib/python3/dist-packages/archivebox/extractors/wget.py +36be113ed8d578727cd233bbd7a7cf22 usr/lib/python3/dist-packages/archivebox/index/__init__.py +c0ed6a7ef5e8e047ec9ac1a3f976adac usr/lib/python3/dist-packages/archivebox/index/csv.py +20f44840be4044870b9b636c7a677840 usr/lib/python3/dist-packages/archivebox/index/html.py +100604e5708884f50fdc900fd9052462 usr/lib/python3/dist-packages/archivebox/index/json.py +9cb33b270d2c5eac1426219cad522322 usr/lib/python3/dist-packages/archivebox/index/schema.py +fda86ceb8bbb688f54e4fb7e93542ab5 usr/lib/python3/dist-packages/archivebox/index/sql.py +b4f1a7b7a6432d6b321f6709da1befea usr/lib/python3/dist-packages/archivebox/logging_util.py +23d467a209c2382e9da7fcdba9200add usr/lib/python3/dist-packages/archivebox/main.py +9a36bcd681908fe369426f723caae82a usr/lib/python3/dist-packages/archivebox/manage.py +7f29e21daf2f7670c7c39b328d1b54a3 usr/lib/python3/dist-packages/archivebox/mypy.ini +525ea6ae9b6e2017d6965eb95f096cbf usr/lib/python3/dist-packages/archivebox/package.json +45a1cc9fdc8ab57be7d8f757fa56cf71 usr/lib/python3/dist-packages/archivebox/parsers/__init__.py +29e35b556cec2b5655e4f73acf270559 usr/lib/python3/dist-packages/archivebox/parsers/generic_html.py +de65c73fa7fe6c41ac274beb7698212d usr/lib/python3/dist-packages/archivebox/parsers/generic_json.py +c530a159067c31d915dac2a42d78446a usr/lib/python3/dist-packages/archivebox/parsers/generic_rss.py +f94362cf08c9b74defaf61fb5e5d2ee8 usr/lib/python3/dist-packages/archivebox/parsers/generic_txt.py +adb1220adb7c13da1f35798f01ce228a usr/lib/python3/dist-packages/archivebox/parsers/medium_rss.py +7122155a71ef7ef9bea823093226b278 usr/lib/python3/dist-packages/archivebox/parsers/netscape_html.py +6a6acb970fdec2d7183a531deb89d722 usr/lib/python3/dist-packages/archivebox/parsers/pinboard_rss.py +68df240cd37791a1bd6d667c7cf220df usr/lib/python3/dist-packages/archivebox/parsers/pocket_api.py +5e923efe79b067a923d5b6411818e01f usr/lib/python3/dist-packages/archivebox/parsers/pocket_html.py +477be61f390a0d748550fabcac0cc69f usr/lib/python3/dist-packages/archivebox/parsers/shaarli_rss.py +2f298d7f55581a053a1ad4d8c475af3b usr/lib/python3/dist-packages/archivebox/parsers/wallabag_atom.py +7c9d41d94ce0a9154c05aefbdf9571a5 usr/lib/python3/dist-packages/archivebox/search/__init__.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/archivebox/search/backends/__init__.py +4db03e517d549405522a62eee08b8b4e usr/lib/python3/dist-packages/archivebox/search/backends/ripgrep.py +8c8afad7576dd4b95d5338536035b3e1 usr/lib/python3/dist-packages/archivebox/search/backends/sonic.py +1ea8f355301d159b7ea495c6e9ccc109 usr/lib/python3/dist-packages/archivebox/search/utils.py +2b3355691067f09a330c278ebacc1a1d usr/lib/python3/dist-packages/archivebox/system.py +ded98b773616e1d57d2bb406888e932f usr/lib/python3/dist-packages/archivebox/themes/admin/actions_as_select.html +9eefc8fb5d749b8289fa3d92a41a9a92 usr/lib/python3/dist-packages/archivebox/themes/admin/app_index.html +5b15de2865d2c0fed6324c44926db706 usr/lib/python3/dist-packages/archivebox/themes/admin/base.html +e4cad9c3534c9af3b515e77c939cdd44 usr/lib/python3/dist-packages/archivebox/themes/admin/grid_change_list.html +03db44427a828c825d8f8946d34f0ebb usr/lib/python3/dist-packages/archivebox/themes/admin/login.html +3efdef6e81478d298d2ac6e85eb7924f usr/lib/python3/dist-packages/archivebox/themes/admin/snapshots_grid.html +49792143a5c2a661dca41a59f1cd38f5 usr/lib/python3/dist-packages/archivebox/themes/default/add_links.html +a244220de67e4352540792b13d0c4c8c usr/lib/python3/dist-packages/archivebox/themes/default/base.html +7cc13d785bd8578482eb215ecd93f1eb usr/lib/python3/dist-packages/archivebox/themes/default/core/snapshot_list.html +da7c05931cc57d74f2ef783d59ce8a91 usr/lib/python3/dist-packages/archivebox/themes/default/link_details.html +8dcc7de9cd396dfcdf81f0063d693d1d usr/lib/python3/dist-packages/archivebox/themes/default/main_index.html +4a08c4e5893a6b1f6fd04e361daaca34 usr/lib/python3/dist-packages/archivebox/themes/default/main_index_minimal.html +756501b93ead9d6c8d1d2ba1418dd634 usr/lib/python3/dist-packages/archivebox/themes/default/main_index_row.html +6afe60e785b4031a7b9be5592333a7e0 usr/lib/python3/dist-packages/archivebox/themes/default/static/add.css +d2a4769f3d03c6d08ed6c91e282d4477 usr/lib/python3/dist-packages/archivebox/themes/default/static/admin.css +34ad7059ab0170cb762daba9ac253412 usr/lib/python3/dist-packages/archivebox/themes/default/static/archive.png +7e923ad223e9f33e54d22e50cf2bcce5 usr/lib/python3/dist-packages/archivebox/themes/default/static/bootstrap.min.css +49edc22ce56c60c9655b77b8319a7676 usr/lib/python3/dist-packages/archivebox/themes/default/static/external.png +4150ca19a74adc6d9e1d3a5417f2ad6d usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.css +97fd6a774fc6211e7619aca9a61ca804 usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.js +a09e13ee94d51c524b7e2a728c7d4039 usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.min.js +a8dcf333411ca3c52a00e878cbb42e86 usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_asc.png +9a6486086d09bb38cf66a57cc559ade3 usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_both.png +47cd34ea64b71baf3a3e6dee987fc8a9 usr/lib/python3/dist-packages/archivebox/themes/default/static/sort_desc.png +fd31a4f9bc95400457135161abd1760f usr/lib/python3/dist-packages/archivebox/themes/default/static/spinner.gif +d24ab23be1f33c036cb955ea1485bdd1 usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index.html +05eb8d3747632c1f952113afcd4af0b6 usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index_row.html +8ce539505f0c16b82c1d3f09785af98d usr/lib/python3/dist-packages/archivebox/util.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/archivebox/vendor/__init__.py +157fd3564470d9676e2dce3187864c4b usr/share/doc/archivebox/changelog.Debian.gz diff --git a/archivebox-0.5.3/debian/archivebox/DEBIAN/postinst b/archivebox-0.5.3/debian/archivebox/DEBIAN/postinst new file mode 100755 index 0000000..875e3ad --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3: +if which py3compile >/dev/null 2>&1; then + py3compile -p archivebox +fi +if which pypy3compile >/dev/null 2>&1; then + pypy3compile -p archivebox || true +fi + +# End automatically added section diff --git a/archivebox-0.5.3/debian/archivebox/DEBIAN/prerm b/archivebox-0.5.3/debian/archivebox/DEBIAN/prerm new file mode 100755 index 0000000..7ab7bae --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3: +if which py3clean >/dev/null 2>&1; then + py3clean -p archivebox +else + dpkg -L archivebox | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/archivebox-0.5.3/debian/archivebox/usr/bin/archivebox b/archivebox-0.5.3/debian/archivebox/usr/bin/archivebox new file mode 100755 index 0000000..83c8b0b --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/bin/archivebox @@ -0,0 +1,12 @@ +#! /usr/bin/python3 +# EASY-INSTALL-ENTRY-SCRIPT: 'archivebox==0.5.3','console_scripts','archivebox' +__requires__ = 'archivebox==0.5.3' +import re +import sys +from pkg_resources import load_entry_point + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit( + load_entry_point('archivebox==0.5.3', 'console_scripts', 'archivebox')() + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/PKG-INFO b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/PKG-INFO new file mode 100644 index 0000000..b6534de --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/PKG-INFO @@ -0,0 +1,591 @@ +Metadata-Version: 2.1 +Name: archivebox +Version: 0.5.3 +Summary: The self-hosted internet archive. +Home-page: https://github.com/ArchiveBox/ArchiveBox +Author: Nick Sweeting +Author-email: git@nicksweeting.com +License: MIT +Project-URL: Source, https://github.com/ArchiveBox/ArchiveBox +Project-URL: Documentation, https://github.com/ArchiveBox/ArchiveBox/wiki +Project-URL: Bug Tracker, https://github.com/ArchiveBox/ArchiveBox/issues +Project-URL: Changelog, https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog +Project-URL: Roadmap, https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap +Project-URL: Community, https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community +Project-URL: Donate, https://github.com/ArchiveBox/ArchiveBox/wiki/Donations +Description:
    + +

    ArchiveBox
    The open-source self-hosted web archive.

    + + ▶️ Quickstart | + Demo | + Github | + Documentation | + Info & Motivation | + Community | + Roadmap + +
    +        "Your own personal internet archive" (网站存档 / 爬虫)
    +        
    + + + + + + + + + + +
    +
    + + ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + + Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + + The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + + ### Quickstart + + It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + + ```bash + pip3 install archivebox + archivebox --version + # install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + + mkdir ~/archivebox && cd ~/archivebox # this can be anywhere + archivebox init + + archivebox add 'https://example.com' + archivebox add --depth=1 'https://example.com' + archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all + archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ + archivebox help # to see more options + ``` + + *(click to expand the sections below for full setup instructions)* + +
    + Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + + First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

    + This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + + ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml + docker-compose run archivebox init + docker-compose run archivebox --version + + # start the webserver and open the UI (optional) + docker-compose run archivebox manage createsuperuser + docker-compose up -d + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker-compose run archivebox add 'https://example.com' + docker-compose run archivebox status + docker-compose run archivebox help # to see more options + ``` + +
    + +
    + Get ArchiveBox with docker on any platform + + First make sure you have Docker installed: https://docs.docker.com/get-docker/
    + ```bash + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + docker run -v $PWD:/data -it archivebox/archivebox init + docker run -v $PWD:/data -it archivebox/archivebox --version + + # start the webserver and open the UI (optional) + docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser + docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add links and manage your archive via the CLI: + docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' + docker run -v $PWD:/data -it archivebox/archivebox status + docker run -v $PWD:/data -it archivebox/archivebox help # to see more options + ``` + +
    + +
    + Get ArchiveBox with apt on Ubuntu >=20.04 + + ```bash + sudo add-apt-repository -u ppa:archivebox/archivebox + sudo apt install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + + For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: + ```bash + deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main + ``` + (you may need to install some other dependencies manually however) + +
    + +
    + Get ArchiveBox with brew on macOS >=10.13 + + ```bash + brew install archivebox/archivebox/archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
    + +
    + Get ArchiveBox with pip on any platform + + ```bash + pip3 install archivebox + + # create a new empty directory and initalize your collection (can be anywhere) + mkdir ~/archivebox && cd ~/archivebox + npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' + archivebox init + archivebox --version + # Install any missing extras like wget/git/chrome/etc. manually as needed + + # start the webserver and open the web UI (optional) + archivebox manage createsuperuser + archivebox server 0.0.0.0:8000 + open http://127.0.0.1:8000 + + # you can also add URLs and manage the archive via the CLI and filesystem: + archivebox add 'https://example.com' + archivebox status + archivebox list --html --with-headers > index.html + archivebox list --json --with-headers > index.json + archivebox help # to see more options + ``` + +
    + + --- + +
    + +
    + + DEMO: archivebox.zervice.io/ + For more information, see the full Quickstart guide, Usage, and Configuration docs. +
    + + --- + + + # Overview + + ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + + To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + + The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + + At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
    + CLI Screenshot + Desktop index screenshot + Desktop details page Screenshot + Desktop details page Screenshot
    + Demo | Usage | Screenshots +
    + . . . . . . . . . . . . . . . . . . . . . . . . . . . . +

    + + + ## Key Features + + - [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally + - [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) + - [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** + - Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC + - ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) + - **Doesn't require a constantly-running daemon**, proxy, or native app + - Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) + - Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. + - Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + + ## Input formats + + ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + + ```bash + echo 'http://example.com' | archivebox add + archivebox add 'https://example.com/some/page' + archivebox add < ~/Downloads/firefox_bookmarks_export.html + archivebox add < any_text_with_urls_in_it.txt + archivebox add --depth=1 'https://example.com/some/downloads.html' + archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' + ``` + + - Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) + - RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format + - Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + + See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + + It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + + ## Output formats + + All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + + The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + + ```bash + ls ./archive// + ``` + + - **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details + - **Title:** `title` title of the site + - **Favicon:** `favicon.ico` favicon of the site + - **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file + - **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile + - **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present + - **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving + - **PDF:** `output.pdf` Printed PDF of site using headless chrome + - **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome + - **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome + - **Readability:** `article.html/json` Article text extraction using Readability + - **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org + - **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl + - **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links + - _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + + It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + + ## Dependencies + + You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + + If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + + ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + + ## Caveats + + If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. + ```bash + # don't do this: + archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' + archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + + # without first disabling share the URL with 3rd party APIs: + archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org + archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL + archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google + ``` + + Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. + ```bash + # visiting an archived page with malicious JS: + https://127.0.0.1:8000/archive/1602401954/example.com/index.html + + # example.com/index.js can now make a request to read everything: + https://127.0.0.1:8000/index.html + https://127.0.0.1:8000/archive/* + # then example.com/index.js can send it off to some evil server + ``` + + Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: + ```bash + archivebox add 'https://example.com#2020-10-24' + ... + archivebox add 'https://example.com#2020-10-25' + ``` + + --- + +
    + +
    + + --- + + # Background & Motivation + + Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + + Whether it's to resist censorship by saving articles before they get taken down or edited, or + just to save a collection of early 2010's flash games you love to play, having the tools to + archive internet content enables to you save the stuff you care most about before it disappears. + +
    +
    + Image from WTF is Link Rot?...
    +
    + + The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. + I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + + Because modern websites are complicated and often rely on dynamic content, + ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + + All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + + ## Comparison to Other Projects + + ▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + + comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + + #### User Interface & Intended Purpose + + ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + + #### Private Local Archives vs Centralized Public Archives + + Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + + #### Storage Requirements + + Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + + ## Learn more + + Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + + - [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ + - Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. + - Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + + --- + + # Documentation + + + + We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + + You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + + ## Getting Started + + - [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) + - [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) + - [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + + ## Reference + + - [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) + - [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) + - [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) + - [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) + - [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) + - [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) + - [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) + - [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) + - [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) + - [Python API](https://docs.archivebox.io/en/latest/modules.html) + - REST API (coming soon...) + + ## More Info + + - [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) + - [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) + - [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) + - [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) + - [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) + - [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + + --- + + # ArchiveBox Development + + All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + + ### Setup the dev environment + + First, install the system dependencies from the "Bare Metal" section above. + Then you can clone the ArchiveBox repo and install + ```python3 + git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox + git checkout master # or the branch you want to test + git submodule update --init --recursive + git pull --recurse-submodules + + # Install ArchiveBox + python dependencies + python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] + # or with pipenv: pipenv install --dev && pipenv shell + + # Install node dependencies + npm install + + # Optional: install extractor dependencies manually or with helper script + ./bin/setup.sh + + # Optional: develop via docker by mounting the code dir into the container + # if you edit e.g. ./archivebox/core/models.py on the docker host, runserver + # inside the container will reload and pick up your changes + docker build . -t archivebox + docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload + ``` + + ### Common development tasks + + See the `./bin/` folder and read the source of the bash scripts within. + You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + + #### Run the linters + + ```bash + ./bin/lint.sh + ``` + (uses `flake8` and `mypy`) + + #### Run the integration tests + + ```bash + ./bin/test.sh + ``` + (uses `pytest -s`) + + #### Make migrations or enter a django shell + + ```bash + cd archivebox/ + ./manage.py makemigrations + + cd data/ + archivebox shell + ``` + (uses `pytest -s`) + + #### Build the docs, pip package, and docker image + + ```bash + ./bin/build.sh + + # or individually: + ./bin/build_docs.sh + ./bin/build_pip.sh + ./bin/build_deb.sh + ./bin/build_brew.sh + ./bin/build_docker.sh + ``` + + #### Roll a release + + ```bash + ./bin/release.sh + ``` + (bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + + --- + +
    +

    + +
    + This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

    + +
    + Sponsor us on Github +
    +
    + +
    + + + + +

    + +
    + +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: Topic :: System :: Archiving +Classifier: Topic :: System :: Archiving :: Backup +Classifier: Topic :: System :: Recovery Tools +Classifier: Topic :: Sociology :: History +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: End Users/Desktop +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: Legal Industry +Classifier: Intended Audience :: System Administrators +Classifier: Environment :: Console +Classifier: Environment :: Web Environment +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Framework :: Django +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Provides-Extra: dev diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/dependency_links.txt b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/entry_points.txt b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/entry_points.txt new file mode 100644 index 0000000..14fdb7e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +archivebox = archivebox.cli:main + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/requires.txt b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/requires.txt new file mode 100644 index 0000000..fccac24 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/requires.txt @@ -0,0 +1,15 @@ + +[dev] +bottle +django-stubs +flake8 +ipdb +mypy +pytest +recommonmark +setuptools +sphinx +sphinx-rtd-theme +stdeb +twine +wheel diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/top_level.txt b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/top_level.txt new file mode 100644 index 0000000..74056b6 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox-0.5.3.egg-info/top_level.txt @@ -0,0 +1 @@ +archivebox diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/.flake8 b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/.flake8 new file mode 100644 index 0000000..dd6ba8e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = D100,D101,D102,D103,D104,D105,D202,D203,D205,D400,E131,E241,E252,E266,E272,E701,E731,W293,W503,W291,W391 +select = F,E9,W +max-line-length = 130 +max-complexity = 10 +exclude = migrations,tests,node_modules,vendor,static,venv,.venv,.venv2,.docker-venv diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/LICENSE b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/LICENSE new file mode 100644 index 0000000..ea201f9 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Nick Sweeting + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/README.md b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/README.md new file mode 100644 index 0000000..2e35783 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/README.md @@ -0,0 +1,545 @@ +
    + +

    ArchiveBox
    The open-source self-hosted web archive.

    + +▶️ Quickstart | +Demo | +Github | +Documentation | +Info & Motivation | +Community | +Roadmap + +
    +"Your own personal internet archive" (网站存档 / 爬虫)
    +
    + + + + + + + + + + +
    +
    + +ArchiveBox is a powerful self-hosted internet archiving solution written in Python 3. You feed it URLs of pages you want to archive, and it saves them to disk in a variety of formats depending on the configuration and the content it detects. + +Your archive can be managed through the command line with commands like `archivebox add`, through the built-in Web UI `archivebox server`, or via the Python library API (beta). It can ingest bookmarks from a browser or service like Pocket/Pinboard, your entire browsing history, RSS feeds, or URLs one at a time. You can also schedule regular/realtime imports with `archivebox schedule`. + +The main index is a self-contained `index.sqlite3` file, and each snapshot is stored as a folder `data/archive//`, with an easy-to-read `index.html` and `index.json` within. For each page, ArchiveBox auto-extracts many types of assets/media and saves them in standard formats, with out-of-the-box support for: several types of HTML snapshots (wget, Chrome headless, singlefile), PDF snapshotting, screenshotting, WARC archiving, git repositories, images, audio, video, subtitles, article text, and more. The snapshots are browseable and managable offline through the filesystem, the built-in webserver, or the Python library API. + +### Quickstart + +It works on Linux/BSD (Intel and ARM CPUs with `docker`/`apt`/`pip3`), macOS (with `docker`/`brew`/`pip3`), and Windows (beta with `docker`/`pip3`). + +```bash +pip3 install archivebox +archivebox --version +# install extras as-needed, or use one of full setup methods below to get everything out-of-the-box + +mkdir ~/archivebox && cd ~/archivebox # this can be anywhere +archivebox init + +archivebox add 'https://example.com' +archivebox add --depth=1 'https://example.com' +archivebox schedule --every=day https://getpocket.com/users/USERNAME/feed/all +archivebox oneshot --extract=title,favicon,media https://www.youtube.com/watch?v=dQw4w9WgXcQ +archivebox help # to see more options +``` + +*(click to expand the sections below for full setup instructions)* + +
    +Get ArchiveBox with docker-compose on any platform (recommended, everything included out-of-the-box) + +First make sure you have Docker installed: https://docs.docker.com/get-docker/ +

    +This is the recommended way to run ArchiveBox because it includes *all* the extractors like chrome, wget, youtube-dl, git, etc., as well as full-text search with sonic, and many other great features. + +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +curl -O https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/master/docker-compose.yml +docker-compose run archivebox init +docker-compose run archivebox --version + +# start the webserver and open the UI (optional) +docker-compose run archivebox manage createsuperuser +docker-compose up -d +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker-compose run archivebox add 'https://example.com' +docker-compose run archivebox status +docker-compose run archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with docker on any platform + +First make sure you have Docker installed: https://docs.docker.com/get-docker/
    +```bash +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +docker run -v $PWD:/data -it archivebox/archivebox init +docker run -v $PWD:/data -it archivebox/archivebox --version + +# start the webserver and open the UI (optional) +docker run -v $PWD:/data -it archivebox/archivebox manage createsuperuser +docker run -v $PWD:/data -p 8000:8000 archivebox/archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add links and manage your archive via the CLI: +docker run -v $PWD:/data -it archivebox/archivebox add 'https://example.com' +docker run -v $PWD:/data -it archivebox/archivebox status +docker run -v $PWD:/data -it archivebox/archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with apt on Ubuntu >=20.04 + +```bash +sudo add-apt-repository -u ppa:archivebox/archivebox +sudo apt install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +For other Debian-based systems or older Ubuntu systems you can add these sources to `/etc/apt/sources.list`: +```bash +deb http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +deb-src http://ppa.launchpad.net/archivebox/archivebox/ubuntu focal main +``` +(you may need to install some other dependencies manually however) + +
    + +
    +Get ArchiveBox with brew on macOS >=10.13 + +```bash +brew install archivebox/archivebox/archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
    + +
    +Get ArchiveBox with pip on any platform + +```bash +pip3 install archivebox + +# create a new empty directory and initalize your collection (can be anywhere) +mkdir ~/archivebox && cd ~/archivebox +npm install --prefix . 'git+https://github.com/ArchiveBox/ArchiveBox.git' +archivebox init +archivebox --version +# Install any missing extras like wget/git/chrome/etc. manually as needed + +# start the webserver and open the web UI (optional) +archivebox manage createsuperuser +archivebox server 0.0.0.0:8000 +open http://127.0.0.1:8000 + +# you can also add URLs and manage the archive via the CLI and filesystem: +archivebox add 'https://example.com' +archivebox status +archivebox list --html --with-headers > index.html +archivebox list --json --with-headers > index.json +archivebox help # to see more options +``` + +
    + +--- + +
    + +
    + +DEMO: archivebox.zervice.io/ +For more information, see the full Quickstart guide, Usage, and Configuration docs. +
    + +--- + + +# Overview + +ArchiveBox is a command line tool, self-hostable web-archiving server, and Python library all-in-one. It can be installed on Docker, macOS, and Linux/BSD, and Windows. You can download and install it as a Debian/Ubuntu package, Homebrew package, Python3 package, or a Docker image. No matter which install method you choose, they all provide the same CLI, Web UI, and on-disk data format. + +To use ArchiveBox you start by creating a folder for your data to live in (it can be anywhere on your system), and running `archivebox init` inside of it. That will create a sqlite3 index and an `ArchiveBox.conf` file. After that, you can continue to add/export/manage/etc using the CLI `archivebox help`, or you can run the Web UI (recommended). If you only want to archive a single site, you can run `archivebox oneshot` to avoid having to create a whole collection. + +The CLI is considered "stable", the ArchiveBox Python API and REST APIs are "beta", and the [desktop app](https://github.com/ArchiveBox/desktop) is "alpha". + +At the end of the day, the goal is to sleep soundly knowing that the part of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). You can also self-host your archivebox server on a public domain to provide archive.org-style public access to your site snapshots. + +
    +CLI Screenshot +Desktop index screenshot +Desktop details page Screenshot +Desktop details page Screenshot
    +Demo | Usage | Screenshots +
    +. . . . . . . . . . . . . . . . . . . . . . . . . . . . +

    + + +## Key Features + +- [**Free & open source**](https://github.com/ArchiveBox/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally +- [**Few dependencies**](https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) +- [**Comprehensive documentation**](https://github.com/ArchiveBox/ArchiveBox/wiki), [active development](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) +- Easy to set up **[scheduled importing](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** +- Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC +- ~~**Suitable for paywalled / [authenticated content](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) +- **Doesn't require a constantly-running daemon**, proxy, or native app +- Provides a CLI, Python API, self-hosted web UI, and REST API (WIP) +- Architected to be able to run [**many varieties of scripts during archiving**](https://github.com/ArchiveBox/ArchiveBox/issues/51), e.g. to extract media, summarize articles, [scroll pages](https://github.com/ArchiveBox/ArchiveBox/issues/80), [close modals](https://github.com/ArchiveBox/ArchiveBox/issues/175), expand comment threads, etc. +- Can also [**mirror content to 3rd-party archiving services**](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy + +## Input formats + +ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exports, Browser bookmarks, Browser history, plain text, HTML, markdown, and more! + +```bash +echo 'http://example.com' | archivebox add +archivebox add 'https://example.com/some/page' +archivebox add < ~/Downloads/firefox_bookmarks_export.html +archivebox add < any_text_with_urls_in_it.txt +archivebox add --depth=1 'https://example.com/some/downloads.html' +archivebox add --depth=1 'https://news.ycombinator.com#2020-12-12' +``` + +- Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) +- RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format +- Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + +See the [Usage: CLI](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. + +It also includes a built-in scheduled import feature with `archivebox schedule` and browser bookmarklet, so you can pull in URLs from RSS feeds, websites, or the filesystem regularly/on-demand. + +## Output formats + +All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. + +The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard sqlite3 database (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `archive/` subfolder. Each snapshot subfolder includes a static JSON and HTML index describing its contents, and the snapshot extrator outputs are plain files within the folder (e.g. `media/example.mp4`, `git/somerepo.git`, `static/someimage.png`, etc.) + +```bash + ls ./archive// +``` + +- **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details +- **Title:** `title` title of the site +- **Favicon:** `favicon.ico` favicon of the site +- **Headers:** `headers.json` Any HTTP headers the site returns are saved in a json file +- **SingleFile:** `singlefile.html` HTML snapshot rendered with headless Chrome using SingleFile +- **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present +- **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving +- **PDF:** `output.pdf` Printed PDF of site using headless chrome +- **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome +- **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome +- **Readability:** `article.html/json` Article text extraction using Readability +- **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org +- **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl +- **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links +- _More coming soon! See the [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap)..._ + +It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) via environment variables or config file. + +## Dependencies + +You don't need to install all the dependencies, ArchiveBox will automatically enable the relevant modules based on whatever you have available, but it's recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) with everything preinstalled. + +If you so choose, you can also install ArchiveBox and its dependencies directly on any Linux or macOS systems using the [automated setup script](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) or the [system package manager](https://github.com/ArchiveBox/ArchiveBox/wiki/Install). + +ArchiveBox is written in Python 3 so it requires `python3` and `pip3` available on your system. It also uses a set of optional, but highly recommended external dependencies for archiving sites: `wget` (for plain HTML, static files, and WARC saving), `chromium` (for screenshots, PDFs, JS execution, and more), `youtube-dl` (for audio and video), `git` (for cloning git repos), and `nodejs` (for readability and singlefile), and more. + +## Caveats + +If you're importing URLs containing secret slugs or pages with private content (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of the extractor modules to avoid leaking private URLs to 3rd party APIs during the archiving process. +```bash +# don't do this: +archivebox add 'https://docs.google.com/document/d/12345somelongsecrethere' +archivebox add 'https://example.com/any/url/you/want/to/keep/secret/' + +# without first disabling share the URL with 3rd party APIs: +archivebox config --set SAVE_ARCHIVE_DOT_ORG=False # disable saving all URLs in Archive.org +archivebox config --set SAVE_FAVICON=False # optional: only the domain is leaked, not full URL +archivebox config --set CHROME_BINARY=chromium # optional: switch to chromium to avoid Chrome phoning home to Google +``` + +Be aware that malicious archived JS can also read the contents of other pages in your archive due to snapshot CSRF and XSS protections being imperfect. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. +```bash +# visiting an archived page with malicious JS: +https://127.0.0.1:8000/archive/1602401954/example.com/index.html + +# example.com/index.js can now make a request to read everything: +https://127.0.0.1:8000/index.html +https://127.0.0.1:8000/archive/* +# then example.com/index.js can send it off to some evil server +``` + +Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now ArchiveBox is designed to only archive each URL with each extractor type once. A workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: +```bash +archivebox add 'https://example.com#2020-10-24' +... +archivebox add 'https://example.com#2020-10-25' +``` + +--- + +
    + +
    + +--- + +# Background & Motivation + +Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. + +Whether it's to resist censorship by saving articles before they get taken down or edited, or +just to save a collection of early 2010's flash games you love to play, having the tools to +archive internet content enables to you save the stuff you care most about before it disappears. + +
    +
    + Image from WTF is Link Rot?...
    +
    + +The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. +I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. + +Because modern websites are complicated and often rely on dynamic content, +ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org and Archive.is are capable of saving. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. + +All the archived links are stored by date bookmarked in `./archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. + +## Comparison to Other Projects + +▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + +comparison The aim of ArchiveBox is to go beyond what the Wayback Machine and other public archiving services can do, by adding a headless browser to replay sessions accurately, and by automatically extracting all the content in multiple redundant formats that will survive being passed down to historians and archivists through many generations. + +#### User Interface & Intended Purpose + +ArchiveBox differentiates itself from [similar projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. + +#### Private Local Archives vs Centralized Public Archives + +Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. + +#### Storage Requirements + +Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. + +## Learn more + +Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! + + + +- [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ +- Check out the ArchiveBox [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. +- Or reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + +--- + +# Documentation + + + +We use the [Github wiki system](https://github.com/ArchiveBox/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) (WIP) for documentation. + +You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/ArchiveBox/ArchiveBox/wiki/Home) folder. + +## Getting Started + +- [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) +- [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) +- [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) + +## Reference + +- [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) +- [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) +- [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) +- [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) +- [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) +- [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) +- [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) +- [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) +- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) +- [Python API](https://docs.archivebox.io/en/latest/modules.html) +- REST API (coming soon...) + +## More Info + +- [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) +- [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) +- [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) +- [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) +- [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) + +--- + +# ArchiveBox Development + +All contributions to ArchiveBox are welcomed! Check our [issues](https://github.com/ArchiveBox/ArchiveBox/issues) and [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) for things to work on, and please open an issue to discuss your proposed implementation before working on things! Otherwise we may have to close your PR if it doesn't align with our roadmap. + +### Setup the dev environment + +First, install the system dependencies from the "Bare Metal" section above. +Then you can clone the ArchiveBox repo and install +```python3 +git clone https://github.com/ArchiveBox/ArchiveBox && cd ArchiveBox +git checkout master # or the branch you want to test +git submodule update --init --recursive +git pull --recurse-submodules + +# Install ArchiveBox + python dependencies +python3 -m venv .venv && source .venv/bin/activate && pip install -e .[dev] +# or with pipenv: pipenv install --dev && pipenv shell + +# Install node dependencies +npm install + +# Optional: install extractor dependencies manually or with helper script +./bin/setup.sh + +# Optional: develop via docker by mounting the code dir into the container +# if you edit e.g. ./archivebox/core/models.py on the docker host, runserver +# inside the container will reload and pick up your changes +docker build . -t archivebox +docker run -it -p 8000:8000 \ + -v $PWD/data:/data \ + -v $PWD/archivebox:/app/archivebox \ + archivebox server 0.0.0.0:8000 --debug --reload +``` + +### Common development tasks + +See the `./bin/` folder and read the source of the bash scripts within. +You can also run all these in Docker. For more examples see the Github Actions CI/CD tests that are run: `.github/workflows/*.yaml`. + +#### Run the linters + +```bash +./bin/lint.sh +``` +(uses `flake8` and `mypy`) + +#### Run the integration tests + +```bash +./bin/test.sh +``` +(uses `pytest -s`) + +#### Make migrations or enter a django shell + +```bash +cd archivebox/ +./manage.py makemigrations + +cd data/ +archivebox shell +``` +(uses `pytest -s`) + +#### Build the docs, pip package, and docker image + +```bash +./bin/build.sh + +# or individually: +./bin/build_docs.sh +./bin/build_pip.sh +./bin/build_deb.sh +./bin/build_brew.sh +./bin/build_docker.sh +``` + +#### Roll a release + +```bash +./bin/release.sh +``` +(bumps the version, builds, and pushes a release to PyPI, Docker Hub, and Github Packages) + + +--- + +
    +

    + +
    +This project is maintained mostly in my spare time with the help from generous contributors and Monadical.com. +

    + +
    +Sponsor us on Github +
    +
    + +
    + + + + +

    + +
    diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__init__.py new file mode 100644 index 0000000..b0c00b6 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox' diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__main__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__main__.py new file mode 100644 index 0000000..8afaa27 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/__main__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox' + +import sys + +from .cli import main + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/__init__.py new file mode 100644 index 0000000..f9a55ef --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/__init__.py @@ -0,0 +1,144 @@ +__package__ = 'archivebox.cli' +__command__ = 'archivebox' + +import os +import sys +import argparse + +from typing import Optional, Dict, List, IO, Union +from pathlib import Path + +from ..config import OUTPUT_DIR + +from importlib import import_module + +CLI_DIR = Path(__file__).resolve().parent + +# these common commands will appear sorted before any others for ease-of-use +meta_cmds = ('help', 'version') +main_cmds = ('init', 'info', 'config') +archive_cmds = ('add', 'remove', 'update', 'list', 'status') + +fake_db = ("oneshot",) + +display_first = (*meta_cmds, *main_cmds, *archive_cmds) + +# every imported command module must have these properties in order to be valid +required_attrs = ('__package__', '__command__', 'main') + +# basic checks to make sure imported files are valid subcommands +is_cli_module = lambda fname: fname.startswith('archivebox_') and fname.endswith('.py') +is_valid_cli_module = lambda module, subcommand: ( + all(hasattr(module, attr) for attr in required_attrs) + and module.__command__.split(' ')[-1] == subcommand +) + + +def list_subcommands() -> Dict[str, str]: + """find and import all valid archivebox_.py files in CLI_DIR""" + + COMMANDS = [] + for filename in os.listdir(CLI_DIR): + if is_cli_module(filename): + subcommand = filename.replace('archivebox_', '').replace('.py', '') + module = import_module('.archivebox_{}'.format(subcommand), __package__) + assert is_valid_cli_module(module, subcommand) + COMMANDS.append((subcommand, module.main.__doc__)) + globals()[subcommand] = module.main + + display_order = lambda cmd: ( + display_first.index(cmd[0]) + if cmd[0] in display_first else + 100 + len(cmd[0]) + ) + + return dict(sorted(COMMANDS, key=display_order)) + + +def run_subcommand(subcommand: str, + subcommand_args: List[str]=None, + stdin: Optional[IO]=None, + pwd: Union[Path, str, None]=None) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + + if subcommand not in meta_cmds: + from ..config import setup_django + setup_django(in_memory_db=subcommand in fake_db, check_db=subcommand in archive_cmds) + + module = import_module('.archivebox_{}'.format(subcommand), __package__) + module.main(args=subcommand_args, stdin=stdin, pwd=pwd) # type: ignore + + +SUBCOMMANDS = list_subcommands() + +class NotProvided: + pass + + +def main(args: Optional[List[str]]=NotProvided, stdin: Optional[IO]=NotProvided, pwd: Optional[str]=None) -> None: + args = sys.argv[1:] if args is NotProvided else args + stdin = sys.stdin if stdin is NotProvided else stdin + + subcommands = list_subcommands() + parser = argparse.ArgumentParser( + prog=__command__, + description='ArchiveBox: The self-hosted internet archive', + add_help=False, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--help', '-h', + action='store_true', + help=subcommands['help'], + ) + group.add_argument( + '--version', + action='store_true', + help=subcommands['version'], + ) + group.add_argument( + "subcommand", + type=str, + help= "The name of the subcommand to run", + nargs='?', + choices=subcommands.keys(), + default=None, + ) + parser.add_argument( + "subcommand_args", + help="Arguments for the subcommand", + nargs=argparse.REMAINDER, + ) + command = parser.parse_args(args or ()) + + if command.version: + command.subcommand = 'version' + elif command.help or command.subcommand is None: + command.subcommand = 'help' + + if command.subcommand not in ('help', 'version', 'status'): + from ..logging_util import log_cli_command + + log_cli_command( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR + ) + + run_subcommand( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR, + ) + + +__all__ = ( + 'SUBCOMMANDS', + 'list_subcommands', + 'run_subcommand', + *SUBCOMMANDS.keys(), +) + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_add.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_add.py new file mode 100644 index 0000000..41c7554 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_add.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox add' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import add +from ..util import docstring +from ..config import OUTPUT_DIR, ONLY_NEW +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(add.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=add.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--update-all', #'-n', + action='store_true', + default=not ONLY_NEW, # when ONLY_NEW=True we skip updating old links + help="Also retry previously skipped/failed links when adding new links", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Add the links to the main index without archiving them", + ) + parser.add_argument( + 'urls', + nargs='*', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--depth", + action="store", + default=0, + choices=[0, 1], + type=int, + help="Recursively archive all linked pages up to this many hops away" + ) + parser.add_argument( + "--overwrite", + default=False, + action="store_true", + help="Re-archive URLs from scratch, overwriting any existing files" + ) + parser.add_argument( + "--init", #'-i', + action='store_true', + help="Init/upgrade the curent data directory before adding", + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + urls = command.urls + stdin_urls = accept_stdin(stdin) + if (stdin_urls and urls) or (not stdin and not urls): + stderr( + '[X] You must pass URLs/paths to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + add( + urls=stdin_urls or urls, + depth=command.depth, + update_all=command.update_all, + index_only=command.index_only, + overwrite=command.overwrite, + init=command.init, + extractors=command.extract, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) + + +# TODO: Implement these +# +# parser.add_argument( +# '--mirror', #'-m', +# action='store_true', +# help='Archive an entire site (finding all linked pages below it on the same domain)', +# ) +# parser.add_argument( +# '--crawler', #'-r', +# choices=('depth_first', 'breadth_first'), +# help='Controls which crawler to use in order to find outlinks in a given page', +# default=None, +# ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_config.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_config.py new file mode 100644 index 0000000..f81286c --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_config.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox config' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import config +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(config.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=config.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--get', #'-g', + action='store_true', + help="Get the value for the given config KEYs", + ) + group.add_argument( + '--set', #'-s', + action='store_true', + help="Set the given KEY=VALUE config values", + ) + group.add_argument( + '--reset', #'-s', + action='store_true', + help="Reset the given KEY config values to their defaults", + ) + parser.add_argument( + 'config_options', + nargs='*', + type=str, + help='KEY or KEY=VALUE formatted config values to get or set', + ) + command = parser.parse_args(args or ()) + config_options_str = accept_stdin(stdin) + + config( + config_options_str=config_options_str, + config_options=command.config_options, + get=command.get, + set=command.set, + reset=command.reset, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_help.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_help.py new file mode 100755 index 0000000..46f17cb --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_help.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox help' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import help +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(help.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=help.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + help(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_init.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_init.py new file mode 100755 index 0000000..6255ef2 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_init.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox init' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import init +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(init.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=init.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--force', # '-f', + action='store_true', + help='Ignore unrecognized files in current directory and initialize anyway', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + init( + force=command.force, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_list.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_list.py new file mode 100644 index 0000000..3838cf6 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_list.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox list' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import list_all +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(list_all.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=list_all.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--csv', #'-c', + type=str, + help="Print the output in CSV format with the given columns, e.g.: timestamp,url,extension", + default=None, + ) + group.add_argument( + '--json', #'-j', + action='store_true', + help="Print the output in JSON format with all columns included.", + ) + group.add_argument( + '--html', + action='store_true', + help="Print the output in HTML format" + ) + parser.add_argument( + '--with-headers', + action='store_true', + help='Include the headers in the output document' + ) + parser.add_argument( + '--sort', #'-s', + type=str, + help="List the links sorted using the given key, e.g. timestamp or updated.", + default=None, + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'List only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='List only URLs matching these filter patterns.' + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + if command.with_headers and not (command.json or command.html or command.csv): + stderr( + '[X] --with-headers can only be used with --json, --html or --csv options.\n', + color='red', + ) + raise SystemExit(2) + + matching_folders = list_all( + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + sort=command.sort, + csv=command.csv, + json=command.json, + html=command.html, + with_headers=command.with_headers, + out_dir=pwd or OUTPUT_DIR, + ) + raise SystemExit(not matching_folders) + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_manage.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_manage.py new file mode 100644 index 0000000..f05604e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox manage' + +import sys + +from typing import Optional, List, IO + +from ..main import manage +from ..util import docstring +from ..config import OUTPUT_DIR + + +@docstring(manage.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + manage( + args=args, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_oneshot.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_oneshot.py new file mode 100644 index 0000000..af68bac --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_oneshot.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox oneshot' + +import sys +import argparse + +from pathlib import Path +from typing import List, Optional, IO + +from ..main import oneshot +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(oneshot.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=oneshot.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'url', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + parser.add_argument( + '--out-dir', + type=str, + default=OUTPUT_DIR, + help= "Path to save the single archive folder to, e.g. ./example.com_archive" + ) + command = parser.parse_args(args or ()) + url = command.url + stdin_url = accept_stdin(stdin) + if (stdin_url and url) or (not stdin and not url): + stderr( + '[X] You must pass a URL/path to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + + oneshot( + url=stdin_url or url, + out_dir=Path(command.out_dir).resolve(), + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_remove.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_remove.py new file mode 100644 index 0000000..cb073e9 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_remove.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox remove' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import remove +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(remove.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=remove.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--yes', # '-y', + action='store_true', + help='Remove links instantly without prompting to confirm.', + ) + parser.add_argument( + '--delete', # '-r', + action='store_true', + help=( + "In addition to removing the link from the index, " + "also delete its archived content and metadata folder." + ), + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only URLs bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only URLs bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex','tag'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + help='URLs matching this filter pattern will be removed from the index.' + ) + command = parser.parse_args(args or ()) + filter_str = accept_stdin(stdin) + + remove( + filter_str=filter_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + before=command.before, + after=command.after, + yes=command.yes, + delete=command.delete, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_schedule.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_schedule.py new file mode 100644 index 0000000..ec5e914 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_schedule.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox schedule' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import schedule +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(schedule.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=schedule.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help=("Don't warn about storage space."), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--add', # '-a', + action='store_true', + help='Add a new scheduled ArchiveBox update job to cron', + ) + parser.add_argument( + '--every', # '-e', + type=str, + default=None, + help='Run ArchiveBox once every [timeperiod] (hour/day/month/year or cron format e.g. "0 0 * * *")', + ) + parser.add_argument( + '--depth', # '-d', + type=int, + default=0, + help='Depth to archive to [0] or 1, see "add" command help for more info.', + ) + group.add_argument( + '--clear', # '-c' + action='store_true', + help=("Stop all ArchiveBox scheduled runs (remove cron jobs)"), + ) + group.add_argument( + '--show', # '-s' + action='store_true', + help=("Print a list of currently active ArchiveBox cron jobs"), + ) + group.add_argument( + '--foreground', '-f', + action='store_true', + help=("Launch ArchiveBox scheduler as a long-running foreground task " + "instead of using cron."), + ) + group.add_argument( + '--run-all', # '-a', + action='store_true', + help=("Run all the scheduled jobs once immediately, independent of " + "their configured schedules, can be used together with --foreground"), + ) + parser.add_argument( + 'import_path', + nargs='?', + type=str, + default=None, + help=("Check this path and import any new links on every run " + "(can be either local file or remote URL)"), + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + schedule( + add=command.add, + show=command.show, + clear=command.clear, + foreground=command.foreground, + run_all=command.run_all, + quiet=command.quiet, + every=command.every, + depth=command.depth, + import_path=command.import_path, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_server.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_server.py new file mode 100644 index 0000000..dbacf7e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_server.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox server' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import server +from ..util import docstring +from ..config import OUTPUT_DIR, BIND_ADDR +from ..logging_util import SmartFormatter, reject_stdin + +@docstring(server.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=server.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'runserver_args', + nargs='*', + type=str, + default=[BIND_ADDR], + help='Arguments to pass to Django runserver' + ) + parser.add_argument( + '--reload', + action='store_true', + help='Enable auto-reloading when code or templates change', + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable DEBUG=True mode with more verbose errors', + ) + parser.add_argument( + '--init', + action='store_true', + help='Run archivebox init before starting the server', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + server( + runserver_args=command.runserver_args, + reload=command.reload, + debug=command.debug, + init=command.init, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_shell.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_shell.py new file mode 100644 index 0000000..bcd5fdd --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_shell.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox shell' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import shell +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(shell.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=shell.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + shell( + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_status.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_status.py new file mode 100644 index 0000000..2bef19c --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_status.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox status' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import status +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(status.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=status.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + status(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_update.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_update.py new file mode 100644 index 0000000..6748096 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_update.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox update' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import update +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(update.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=update.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--only-new', #'-n', + action='store_true', + help="Don't attempt to retry previously skipped/failed links when updating", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Update the main index without archiving any content", + ) + parser.add_argument( + '--resume', #'-r', + type=float, + help='Resume the update process from a given timestamp', + default=None, + ) + parser.add_argument( + '--overwrite', #'-x', + action='store_true', + help='Ignore existing archived content and overwrite with new versions (DANGEROUS)', + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="Update only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="Update only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'Update only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex', 'tag', 'search'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='Update only URLs matching these filter patterns.' + ) + parser.add_argument( + "--extract", + type=str, + help="Pass a list of the extractors to be used. If the method name is not correct, it will be ignored. \ + This does not take precedence over the configuration", + default="" + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + update( + resume=command.resume, + only_new=command.only_new, + index_only=command.index_only, + overwrite=command.overwrite, + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + out_dir=pwd or OUTPUT_DIR, + extractors=command.extract, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_version.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_version.py new file mode 100755 index 0000000..e7922f3 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/archivebox_version.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox version' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import version +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(version.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=version.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Only print ArchiveBox version number and nothing else.', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + version( + quiet=command.quiet, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/tests.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/tests.py new file mode 100755 index 0000000..4d7016a --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/cli/tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' + + +import os +import sys +import shutil +import unittest +from pathlib import Path + +from contextlib import contextmanager + +TEST_CONFIG = { + 'USE_COLOR': 'False', + 'SHOW_PROGRESS': 'False', + + 'OUTPUT_DIR': 'data.tests', + + 'SAVE_ARCHIVE_DOT_ORG': 'False', + 'SAVE_TITLE': 'False', + + 'USE_CURL': 'False', + 'USE_WGET': 'False', + 'USE_GIT': 'False', + 'USE_CHROME': 'False', + 'USE_YOUTUBEDL': 'False', +} + +OUTPUT_DIR = 'data.tests' +os.environ.update(TEST_CONFIG) + +from ..main import init +from ..index import load_main_index +from ..config import ( + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, +) + +from . import ( + archivebox_init, + archivebox_add, + archivebox_remove, +) + +HIDE_CLI_OUTPUT = True + +test_urls = ''' +https://example1.com/what/is/happening.html?what=1#how-about-this=1 +https://example2.com/what/is/happening/?what=1#how-about-this=1 +HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f +https://example4.com/what/is/happening.html +https://example5.com/ +https://example6.com + +http://example7.com +[https://example8.com/what/is/this.php?what=1] +[and http://example9.com?what=1&other=3#and-thing=2] +https://example10.com#and-thing=2 " +abcdef +sdflkf[what](https://subb.example12.com/who/what.php?whoami=1#whatami=2)?am=hi +example13.bada +and example14.badb +htt://example15.badc +''' + +stdout = sys.stdout +stderr = sys.stderr + + +@contextmanager +def output_hidden(show_failing=True): + if not HIDE_CLI_OUTPUT: + yield + return + + sys.stdout = open('stdout.txt', 'w+') + sys.stderr = open('stderr.txt', 'w+') + try: + yield + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + except: + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + if show_failing: + with open('stdout.txt', 'r') as f: + print(f.read()) + with open('stderr.txt', 'r') as f: + print(f.read()) + raise + finally: + os.remove('stdout.txt') + os.remove('stderr.txt') + + +class TestInit(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_basic_init(self): + with output_hidden(): + archivebox_init.main([]) + + assert (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + assert len(load_main_index(out_dir=OUTPUT_DIR)) == 0 + + def test_conflicting_init(self): + with open(Path(OUTPUT_DIR) / 'test_conflict.txt', 'w+') as f: + f.write('test') + + try: + with output_hidden(show_failing=False): + archivebox_init.main([]) + assert False, 'Init should have exited with an exception' + except SystemExit: + pass + + assert not (Path(OUTPUT_DIR) / SQL_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / JSON_INDEX_FILENAME).exists() + assert not (Path(OUTPUT_DIR) / HTML_INDEX_FILENAME).exists() + try: + load_main_index(out_dir=OUTPUT_DIR) + assert False, 'load_main_index should raise an exception when no index is present' + except: + pass + + def test_no_dirty_state(self): + with output_hidden(): + init() + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + with output_hidden(): + init() + + +class TestAdd(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_add_arg_url(self): + with output_hidden(): + archivebox_add.main(['https://getpocket.com/users/nikisweeting/feed/all']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 30 + + def test_add_arg_file(self): + test_file = Path(OUTPUT_DIR) / 'test.txt' + with open(test_file, 'w+') as f: + f.write(test_urls) + + with output_hidden(): + archivebox_add.main([test_file]) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + os.remove(test_file) + + def test_add_stdin_url(self): + with output_hidden(): + archivebox_add.main([], stdin=test_urls) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + + +class TestRemove(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + archivebox_add.main([], stdin=test_urls) + + # def tearDown(self): + # shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + + def test_remove_exact(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', 'https://example5.com/']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 11 + + def test_remove_regex(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=regex', r'http(s)?:\/\/(.+\.)?(example\d\.com)']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 4 + + def test_remove_domain(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=domain', 'example5.com', 'example6.com']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 10 + + def test_remove_none(self): + try: + with output_hidden(show_failing=False): + archivebox_remove.main(['--yes', '--delete', 'https://doesntexist.com']) + assert False, 'Should raise if no URLs match' + except: + pass + + +if __name__ == '__main__': + if '--verbose' in sys.argv or '-v' in sys.argv: + HIDE_CLI_OUTPUT = False + + unittest.main() diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config.py new file mode 100644 index 0000000..9a3f9a7 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config.py @@ -0,0 +1,1081 @@ +""" +ArchiveBox config definitons (including defaults and dynamic config options). + +Config Usage Example: + + archivebox config --set MEDIA_TIMEOUT=600 + env MEDIA_TIMEOUT=600 USE_COLOR=False ... archivebox [subcommand] ... + +Config Precedence Order: + + 1. cli args (--update-all / --index-only / etc.) + 2. shell environment vars (env USE_COLOR=False archivebox add '...') + 3. config file (echo "SAVE_FAVICON=False" >> ArchiveBox.conf) + 4. defaults (defined below in Python) + +Documentation: + + https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + +""" + +__package__ = 'archivebox' + +import os +import io +import re +import sys +import json +import getpass +import shutil +import django + +from hashlib import md5 +from pathlib import Path +from typing import Optional, Type, Tuple, Dict, Union, List +from subprocess import run, PIPE, DEVNULL +from configparser import ConfigParser +from collections import defaultdict + +from .config_stubs import ( + SimpleConfigValueDict, + ConfigValue, + ConfigDict, + ConfigDefaultValue, + ConfigDefaultDict, +) + +############################### Config Schema ################################## + +CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { + 'SHELL_CONFIG': { + 'IS_TTY': {'type': bool, 'default': lambda _: sys.stdout.isatty()}, + 'USE_COLOR': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'SHOW_PROGRESS': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'IN_DOCKER': {'type': bool, 'default': False}, + # TODO: 'SHOW_HINTS': {'type: bool, 'default': True}, + }, + + 'GENERAL_CONFIG': { + 'OUTPUT_DIR': {'type': str, 'default': None}, + 'CONFIG_FILE': {'type': str, 'default': None}, + 'ONLY_NEW': {'type': bool, 'default': True}, + 'TIMEOUT': {'type': int, 'default': 60}, + 'MEDIA_TIMEOUT': {'type': int, 'default': 3600}, + 'OUTPUT_PERMISSIONS': {'type': str, 'default': '755'}, + 'RESTRICT_FILE_NAMES': {'type': str, 'default': 'windows'}, + 'URL_BLACKLIST': {'type': str, 'default': r'\.(css|js|otf|ttf|woff|woff2|gstatic\.com|googleapis\.com/css)(\?.*)?$'}, # to avoid downloading code assets as their own pages + }, + + 'SERVER_CONFIG': { + 'SECRET_KEY': {'type': str, 'default': None}, + 'BIND_ADDR': {'type': str, 'default': lambda c: ['127.0.0.1:8000', '0.0.0.0:8000'][c['IN_DOCKER']]}, + 'ALLOWED_HOSTS': {'type': str, 'default': '*'}, + 'DEBUG': {'type': bool, 'default': False}, + 'PUBLIC_INDEX': {'type': bool, 'default': True}, + 'PUBLIC_SNAPSHOTS': {'type': bool, 'default': True}, + 'PUBLIC_ADD_VIEW': {'type': bool, 'default': False}, + 'FOOTER_INFO': {'type': str, 'default': 'Content is hosted for personal archiving purposes only. Contact server owner for any takedown requests.'}, + 'ACTIVE_THEME': {'type': str, 'default': 'default'}, + }, + + 'ARCHIVE_METHOD_TOGGLES': { + 'SAVE_TITLE': {'type': bool, 'default': True, 'aliases': ('FETCH_TITLE',)}, + 'SAVE_FAVICON': {'type': bool, 'default': True, 'aliases': ('FETCH_FAVICON',)}, + 'SAVE_WGET': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET',)}, + 'SAVE_WGET_REQUISITES': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET_REQUISITES',)}, + 'SAVE_SINGLEFILE': {'type': bool, 'default': True, 'aliases': ('FETCH_SINGLEFILE',)}, + 'SAVE_READABILITY': {'type': bool, 'default': True, 'aliases': ('FETCH_READABILITY',)}, + 'SAVE_MERCURY': {'type': bool, 'default': True, 'aliases': ('FETCH_MERCURY',)}, + 'SAVE_PDF': {'type': bool, 'default': True, 'aliases': ('FETCH_PDF',)}, + 'SAVE_SCREENSHOT': {'type': bool, 'default': True, 'aliases': ('FETCH_SCREENSHOT',)}, + 'SAVE_DOM': {'type': bool, 'default': True, 'aliases': ('FETCH_DOM',)}, + 'SAVE_HEADERS': {'type': bool, 'default': True, 'aliases': ('FETCH_HEADERS',)}, + 'SAVE_WARC': {'type': bool, 'default': True, 'aliases': ('FETCH_WARC',)}, + 'SAVE_GIT': {'type': bool, 'default': True, 'aliases': ('FETCH_GIT',)}, + 'SAVE_MEDIA': {'type': bool, 'default': True, 'aliases': ('FETCH_MEDIA',)}, + 'SAVE_ARCHIVE_DOT_ORG': {'type': bool, 'default': True, 'aliases': ('SUBMIT_ARCHIVE_DOT_ORG',)}, + }, + + 'ARCHIVE_METHOD_OPTIONS': { + 'RESOLUTION': {'type': str, 'default': '1440,2000', 'aliases': ('SCREENSHOT_RESOLUTION',)}, + 'GIT_DOMAINS': {'type': str, 'default': 'github.com,bitbucket.org,gitlab.com'}, + 'CHECK_SSL_VALIDITY': {'type': bool, 'default': True}, + + 'CURL_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) curl/{CURL_VERSION}'}, + 'WGET_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/) wget/{WGET_VERSION}'}, + 'CHROME_USER_AGENT': {'type': str, 'default': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}, + + 'COOKIES_FILE': {'type': str, 'default': None}, + 'CHROME_USER_DATA_DIR': {'type': str, 'default': None}, + + 'CHROME_HEADLESS': {'type': bool, 'default': True}, + 'CHROME_SANDBOX': {'type': bool, 'default': lambda c: not c['IN_DOCKER']}, + 'YOUTUBEDL_ARGS': {'type': list, 'default': ['--write-description', + '--write-info-json', + '--write-annotations', + '--write-thumbnail', + '--no-call-home', + '--user-agent', + '--all-subs', + '--extract-audio', + '--keep-video', + '--ignore-errors', + '--geo-bypass', + '--audio-format', 'mp3', + '--audio-quality', '320K', + '--embed-thumbnail', + '--add-metadata']}, + + 'WGET_ARGS': {'type': list, 'default': ['--no-verbose', + '--adjust-extension', + '--convert-links', + '--force-directories', + '--backup-converted', + '--span-hosts', + '--no-parent', + '-e', 'robots=off', + ]}, + 'CURL_ARGS': {'type': list, 'default': ['--silent', + '--location', + '--compressed' + ]}, + 'GIT_ARGS': {'type': list, 'default': ['--recursive']}, + }, + + 'SEARCH_BACKEND_CONFIG' : { + 'USE_INDEXING_BACKEND': {'type': bool, 'default': True}, + 'USE_SEARCHING_BACKEND': {'type': bool, 'default': True}, + 'SEARCH_BACKEND_ENGINE': {'type': str, 'default': 'ripgrep'}, + 'SEARCH_BACKEND_HOST_NAME': {'type': str, 'default': 'localhost'}, + 'SEARCH_BACKEND_PORT': {'type': int, 'default': 1491}, + 'SEARCH_BACKEND_PASSWORD': {'type': str, 'default': 'SecretPassword'}, + # SONIC + 'SONIC_COLLECTION': {'type': str, 'default': 'archivebox'}, + 'SONIC_BUCKET': {'type': str, 'default': 'snapshots'}, + }, + + 'DEPENDENCY_CONFIG': { + 'USE_CURL': {'type': bool, 'default': True}, + 'USE_WGET': {'type': bool, 'default': True}, + 'USE_SINGLEFILE': {'type': bool, 'default': True}, + 'USE_READABILITY': {'type': bool, 'default': True}, + 'USE_MERCURY': {'type': bool, 'default': True}, + 'USE_GIT': {'type': bool, 'default': True}, + 'USE_CHROME': {'type': bool, 'default': True}, + 'USE_NODE': {'type': bool, 'default': True}, + 'USE_YOUTUBEDL': {'type': bool, 'default': True}, + 'USE_RIPGREP': {'type': bool, 'default': True}, + + 'CURL_BINARY': {'type': str, 'default': 'curl'}, + 'GIT_BINARY': {'type': str, 'default': 'git'}, + 'WGET_BINARY': {'type': str, 'default': 'wget'}, + 'SINGLEFILE_BINARY': {'type': str, 'default': 'single-file'}, + 'READABILITY_BINARY': {'type': str, 'default': 'readability-extractor'}, + 'MERCURY_BINARY': {'type': str, 'default': 'mercury-parser'}, + 'YOUTUBEDL_BINARY': {'type': str, 'default': 'youtube-dl'}, + 'NODE_BINARY': {'type': str, 'default': 'node'}, + 'RIPGREP_BINARY': {'type': str, 'default': 'rg'}, + 'CHROME_BINARY': {'type': str, 'default': None}, + + 'POCKET_CONSUMER_KEY': {'type': str, 'default': None}, + 'POCKET_ACCESS_TOKENS': {'type': dict, 'default': {}}, + }, +} + + +########################## Backwards-Compatibility ############################# + + +# for backwards compatibility with old config files, check old/deprecated names for each key +CONFIG_ALIASES = { + alias: key + for section in CONFIG_SCHEMA.values() + for key, default in section.items() + for alias in default.get('aliases', ()) +} +USER_CONFIG = {key for section in CONFIG_SCHEMA.values() for key in section.keys()} + +def get_real_name(key: str) -> str: + """get the current canonical name for a given deprecated config key""" + return CONFIG_ALIASES.get(key.upper().strip(), key.upper().strip()) + + + +################################ Constants ##################################### + +PACKAGE_DIR_NAME = 'archivebox' +TEMPLATES_DIR_NAME = 'themes' + +ARCHIVE_DIR_NAME = 'archive' +SOURCES_DIR_NAME = 'sources' +LOGS_DIR_NAME = 'logs' +STATIC_DIR_NAME = 'static' +SQL_INDEX_FILENAME = 'index.sqlite3' +JSON_INDEX_FILENAME = 'index.json' +HTML_INDEX_FILENAME = 'index.html' +ROBOTS_TXT_FILENAME = 'robots.txt' +FAVICON_FILENAME = 'favicon.ico' +CONFIG_FILENAME = 'ArchiveBox.conf' + +DEFAULT_CLI_COLORS = { + 'reset': '\033[00;00m', + 'lightblue': '\033[01;30m', + 'lightyellow': '\033[01;33m', + 'lightred': '\033[01;35m', + 'red': '\033[01;31m', + 'green': '\033[01;32m', + 'blue': '\033[01;34m', + 'white': '\033[01;37m', + 'black': '\033[01;30m', +} +ANSI = {k: '' for k in DEFAULT_CLI_COLORS.keys()} + +COLOR_DICT = defaultdict(lambda: [(0, 0, 0), (0, 0, 0)], { + '00': [(0, 0, 0), (0, 0, 0)], + '30': [(0, 0, 0), (0, 0, 0)], + '31': [(255, 0, 0), (128, 0, 0)], + '32': [(0, 200, 0), (0, 128, 0)], + '33': [(255, 255, 0), (128, 128, 0)], + '34': [(0, 0, 255), (0, 0, 128)], + '35': [(255, 0, 255), (128, 0, 128)], + '36': [(0, 255, 255), (0, 128, 128)], + '37': [(255, 255, 255), (255, 255, 255)], +}) + +STATICFILE_EXTENSIONS = { + # 99.999% of the time, URLs ending in these extensions are static files + # that can be downloaded as-is, not html pages that need to be rendered + 'gif', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'wbmp', 'ico', 'jng', 'bmp', + 'svg', 'svgz', 'webp', 'ps', 'eps', 'ai', + 'mp3', 'mp4', 'm4a', 'mpeg', 'mpg', 'mkv', 'mov', 'webm', 'm4v', + 'flv', 'wmv', 'avi', 'ogg', 'ts', 'm3u8', + 'pdf', 'txt', 'rtf', 'rtfd', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', + 'atom', 'rss', 'css', 'js', 'json', + 'dmg', 'iso', 'img', + 'rar', 'war', 'hqx', 'zip', 'gz', 'bz2', '7z', + + # Less common extensions to consider adding later + # jar, swf, bin, com, exe, dll, deb + # ear, hqx, eot, wmlc, kml, kmz, cco, jardiff, jnlp, run, msi, msp, msm, + # pl pm, prc pdb, rar, rpm, sea, sit, tcl tk, der, pem, crt, xpi, xspf, + # ra, mng, asx, asf, 3gpp, 3gp, mid, midi, kar, jad, wml, htc, mml + + # These are always treated as pages, not as static files, never add them: + # html, htm, shtml, xhtml, xml, aspx, php, cgi +} + + + +############################## Derived Config ################################## + + +DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { + 'TERM_WIDTH': {'default': lambda c: lambda: shutil.get_terminal_size((100, 10)).columns}, + 'USER': {'default': lambda c: getpass.getuser() or os.getlogin()}, + 'ANSI': {'default': lambda c: DEFAULT_CLI_COLORS if c['USE_COLOR'] else {k: '' for k in DEFAULT_CLI_COLORS.keys()}}, + + 'PACKAGE_DIR': {'default': lambda c: Path(__file__).resolve().parent}, + 'TEMPLATES_DIR': {'default': lambda c: c['PACKAGE_DIR'] / TEMPLATES_DIR_NAME}, + + 'OUTPUT_DIR': {'default': lambda c: Path(c['OUTPUT_DIR']).resolve() if c['OUTPUT_DIR'] else Path(os.curdir).resolve()}, + 'ARCHIVE_DIR': {'default': lambda c: c['OUTPUT_DIR'] / ARCHIVE_DIR_NAME}, + 'SOURCES_DIR': {'default': lambda c: c['OUTPUT_DIR'] / SOURCES_DIR_NAME}, + 'LOGS_DIR': {'default': lambda c: c['OUTPUT_DIR'] / LOGS_DIR_NAME}, + 'CONFIG_FILE': {'default': lambda c: Path(c['CONFIG_FILE']).resolve() if c['CONFIG_FILE'] else c['OUTPUT_DIR'] / CONFIG_FILENAME}, + 'COOKIES_FILE': {'default': lambda c: c['COOKIES_FILE'] and Path(c['COOKIES_FILE']).resolve()}, + 'CHROME_USER_DATA_DIR': {'default': lambda c: find_chrome_data_dir() if c['CHROME_USER_DATA_DIR'] is None else (Path(c['CHROME_USER_DATA_DIR']).resolve() if c['CHROME_USER_DATA_DIR'] else None)}, # None means unset, so we autodetect it with find_chrome_Data_dir(), but emptystring '' means user manually set it to '', and we should store it as None + 'URL_BLACKLIST_PTN': {'default': lambda c: c['URL_BLACKLIST'] and re.compile(c['URL_BLACKLIST'] or '', re.IGNORECASE | re.UNICODE | re.MULTILINE)}, + + 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0]}, + 'VERSION': {'default': lambda c: json.loads((Path(c['PACKAGE_DIR']) / 'package.json').read_text().strip())['version']}, + 'GIT_SHA': {'default': lambda c: c['VERSION'].split('+')[-1] or 'unknown'}, + + 'PYTHON_BINARY': {'default': lambda c: sys.executable}, + 'PYTHON_ENCODING': {'default': lambda c: sys.stdout.encoding.upper()}, + 'PYTHON_VERSION': {'default': lambda c: '{}.{}.{}'.format(*sys.version_info[:3])}, + + 'DJANGO_BINARY': {'default': lambda c: django.__file__.replace('__init__.py', 'bin/django-admin.py')}, + 'DJANGO_VERSION': {'default': lambda c: '{}.{}.{} {} ({})'.format(*django.VERSION)}, + + 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['SAVE_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, + 'CURL_VERSION': {'default': lambda c: bin_version(c['CURL_BINARY']) if c['USE_CURL'] else None}, + 'CURL_USER_AGENT': {'default': lambda c: c['CURL_USER_AGENT'].format(**c)}, + 'CURL_ARGS': {'default': lambda c: c['CURL_ARGS'] or []}, + 'SAVE_FAVICON': {'default': lambda c: c['USE_CURL'] and c['SAVE_FAVICON']}, + 'SAVE_ARCHIVE_DOT_ORG': {'default': lambda c: c['USE_CURL'] and c['SAVE_ARCHIVE_DOT_ORG']}, + + 'USE_WGET': {'default': lambda c: c['USE_WGET'] and (c['SAVE_WGET'] or c['SAVE_WARC'])}, + 'WGET_VERSION': {'default': lambda c: bin_version(c['WGET_BINARY']) if c['USE_WGET'] else None}, + 'WGET_AUTO_COMPRESSION': {'default': lambda c: wget_supports_compression(c) if c['USE_WGET'] else False}, + 'WGET_USER_AGENT': {'default': lambda c: c['WGET_USER_AGENT'].format(**c)}, + 'SAVE_WGET': {'default': lambda c: c['USE_WGET'] and c['SAVE_WGET']}, + 'SAVE_WARC': {'default': lambda c: c['USE_WGET'] and c['SAVE_WARC']}, + 'WGET_ARGS': {'default': lambda c: c['WGET_ARGS'] or []}, + + 'RIPGREP_VERSION': {'default': lambda c: bin_version(c['RIPGREP_BINARY']) if c['USE_RIPGREP'] else None}, + + 'USE_SINGLEFILE': {'default': lambda c: c['USE_SINGLEFILE'] and c['SAVE_SINGLEFILE']}, + 'SINGLEFILE_VERSION': {'default': lambda c: bin_version(c['SINGLEFILE_BINARY']) if c['USE_SINGLEFILE'] else None}, + + 'USE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['SAVE_READABILITY']}, + 'READABILITY_VERSION': {'default': lambda c: bin_version(c['READABILITY_BINARY']) if c['USE_READABILITY'] else None}, + + 'USE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['SAVE_MERCURY']}, + 'MERCURY_VERSION': {'default': lambda c: '1.0.0' if shutil.which(str(bin_path(c['MERCURY_BINARY']))) else None}, # mercury is unversioned + + 'USE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + 'GIT_VERSION': {'default': lambda c: bin_version(c['GIT_BINARY']) if c['USE_GIT'] else None}, + 'SAVE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + + 'USE_YOUTUBEDL': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_VERSION': {'default': lambda c: bin_version(c['YOUTUBEDL_BINARY']) if c['USE_YOUTUBEDL'] else None}, + 'SAVE_MEDIA': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_ARGS': {'default': lambda c: c['YOUTUBEDL_ARGS'] or []}, + + 'USE_CHROME': {'default': lambda c: c['USE_CHROME'] and (c['SAVE_PDF'] or c['SAVE_SCREENSHOT'] or c['SAVE_DOM'] or c['SAVE_SINGLEFILE'])}, + 'CHROME_BINARY': {'default': lambda c: c['CHROME_BINARY'] if c['CHROME_BINARY'] else find_chrome_binary()}, + 'CHROME_VERSION': {'default': lambda c: bin_version(c['CHROME_BINARY']) if c['USE_CHROME'] else None}, + + 'SAVE_PDF': {'default': lambda c: c['USE_CHROME'] and c['SAVE_PDF']}, + 'SAVE_SCREENSHOT': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SCREENSHOT']}, + 'SAVE_DOM': {'default': lambda c: c['USE_CHROME'] and c['SAVE_DOM']}, + 'SAVE_SINGLEFILE': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SINGLEFILE'] and c['USE_NODE']}, + 'SAVE_READABILITY': {'default': lambda c: c['USE_READABILITY'] and c['USE_NODE']}, + 'SAVE_MERCURY': {'default': lambda c: c['USE_MERCURY'] and c['USE_NODE']}, + + 'USE_NODE': {'default': lambda c: c['USE_NODE'] and (c['SAVE_READABILITY'] or c['SAVE_SINGLEFILE'] or c['SAVE_MERCURY'])}, + 'NODE_VERSION': {'default': lambda c: bin_version(c['NODE_BINARY']) if c['USE_NODE'] else None}, + + 'DEPENDENCIES': {'default': lambda c: get_dependency_info(c)}, + 'CODE_LOCATIONS': {'default': lambda c: get_code_locations(c)}, + 'EXTERNAL_LOCATIONS': {'default': lambda c: get_external_locations(c)}, + 'DATA_LOCATIONS': {'default': lambda c: get_data_locations(c)}, + 'CHROME_OPTIONS': {'default': lambda c: get_chrome_info(c)}, +} + + + +################################### Helpers #################################### + + +def load_config_val(key: str, + default: ConfigDefaultValue=None, + type: Optional[Type]=None, + aliases: Optional[Tuple[str, ...]]=None, + config: Optional[ConfigDict]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigValue: + """parse bool, int, and str key=value pairs from env""" + + + config_keys_to_check = (key, *(aliases or ())) + for key in config_keys_to_check: + if env_vars: + val = env_vars.get(key) + if val: + break + if config_file_vars: + val = config_file_vars.get(key) + if val: + break + + if type is None or val is None: + if callable(default): + assert isinstance(config, dict) + return default(config) + + return default + + elif type is bool: + if val.lower() in ('true', 'yes', '1'): + return True + elif val.lower() in ('false', 'no', '0'): + return False + else: + raise ValueError(f'Invalid configuration option {key}={val} (expected a boolean: True/False)') + + elif type is str: + if val.lower() in ('true', 'false', 'yes', 'no', '1', '0'): + raise ValueError(f'Invalid configuration option {key}={val} (expected a string)') + return val.strip() + + elif type is int: + if not val.isdigit(): + raise ValueError(f'Invalid configuration option {key}={val} (expected an integer)') + return int(val) + + elif type is list or type is dict: + return json.loads(val) + + raise Exception('Config values can only be str, bool, int or json') + + +def load_config_file(out_dir: str=None) -> Optional[Dict[str, str]]: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + if config_path.exists(): + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + # flatten into one namespace + config_file_vars = { + key.upper(): val + for section, options in config_file.items() + for key, val in options.items() + } + # print('[i] Loaded config file', os.path.abspath(config_path)) + # print(config_file_vars) + return config_file_vars + return None + + +def write_config_file(config: Dict[str, str], out_dir: str=None) -> ConfigDict: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + from .system import atomic_write + + CONFIG_HEADER = ( + """# This is the config file for your ArchiveBox collection. + # + # You can add options here manually in INI format, or automatically by running: + # archivebox config --set KEY=VALUE + # + # If you modify this file manually, make sure to update your archive after by running: + # archivebox init + # + # A list of all possible config with documentation and examples can be found here: + # https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration + + """) + + out_dir = out_dir or Path(os.getenv('OUTPUT_DIR', '.')).resolve() + config_path = Path(out_dir) / CONFIG_FILENAME + + if not config_path.exists(): + atomic_write(config_path, CONFIG_HEADER) + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + + with open(config_path, 'r') as old: + atomic_write(f'{config_path}.bak', old.read()) + + find_section = lambda key: [name for name, opts in CONFIG_SCHEMA.items() if key in opts][0] + + # Set up sections in empty config file + for key, val in config.items(): + section = find_section(key) + if section in config_file: + existing_config = dict(config_file[section]) + else: + existing_config = {} + config_file[section] = {**existing_config, key: val} + + # always make sure there's a SECRET_KEY defined for Django + existing_secret_key = None + if 'SERVER_CONFIG' in config_file and 'SECRET_KEY' in config_file['SERVER_CONFIG']: + existing_secret_key = config_file['SERVER_CONFIG']['SECRET_KEY'] + + if (not existing_secret_key) or ('not a valid secret' in existing_secret_key): + from django.utils.crypto import get_random_string + chars = 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.' + random_secret_key = get_random_string(50, chars) + if 'SERVER_CONFIG' in config_file: + config_file['SERVER_CONFIG']['SECRET_KEY'] = random_secret_key + else: + config_file['SERVER_CONFIG'] = {'SECRET_KEY': random_secret_key} + + with open(config_path, 'w+') as new: + config_file.write(new) + + try: + # validate the config by attempting to re-parse it + CONFIG = load_all_config() + return { + key.upper(): CONFIG.get(key.upper()) + for key in config.keys() + } + except: + # something went horribly wrong, rever to the previous version + with open(f'{config_path}.bak', 'r') as old: + atomic_write(config_path, old.read()) + + if Path(f'{config_path}.bak').exists(): + os.remove(f'{config_path}.bak') + + return {} + + + +def load_config(defaults: ConfigDefaultDict, + config: Optional[ConfigDict]=None, + out_dir: Optional[str]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigDict: + + env_vars = env_vars or os.environ + config_file_vars = config_file_vars or load_config_file(out_dir=out_dir) + + extended_config: ConfigDict = config.copy() if config else {} + for key, default in defaults.items(): + try: + extended_config[key] = load_config_val( + key, + default=default['default'], + type=default.get('type'), + aliases=default.get('aliases'), + config=extended_config, + env_vars=env_vars, + config_file_vars=config_file_vars, + ) + except KeyboardInterrupt: + raise SystemExit(0) + except Exception as e: + stderr() + stderr(f'[X] Error while loading configuration value: {key}', color='red', config=extended_config) + stderr(' {}: {}'.format(e.__class__.__name__, e)) + stderr() + stderr(' Check your config for mistakes and try again (your archive data is unaffected).') + stderr() + stderr(' For config documentation and examples see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration') + stderr() + raise + raise SystemExit(2) + + return extended_config + +# def write_config(config: ConfigDict): + +# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: + + +# Logging Helpers +def stdout(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stdout.write(prefix + ''.join(strs)) + +def stderr(*args, color: Optional[str]=None, prefix: str='', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stderr.write(prefix + ''.join(strs)) + +def hint(text: Union[Tuple[str, ...], List[str], str], prefix=' ', config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if isinstance(text, str): + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text, **ansi)) + else: + stderr('{}{lightred}Hint:{reset} {}'.format(prefix, text[0], **ansi)) + for line in text[1:]: + stderr('{} {}'.format(prefix, line)) + + +# Dependency Metadata Helpers +def bin_version(binary: Optional[str]) -> Optional[str]: + """check the presence and return valid version line of a specified binary""" + + abspath = bin_path(binary) + if not binary or not abspath: + return None + + try: + version_str = run([abspath, "--version"], stdout=PIPE).stdout.strip().decode() + # take first 3 columns of first line of version info + return ' '.join(version_str.split('\n')[0].strip().split()[:3]) + except OSError: + pass + # stderr(f'[X] Unable to find working version of dependency: {binary}', color='red') + # stderr(' Make sure it\'s installed, then confirm it\'s working by running:') + # stderr(f' {binary} --version') + # stderr() + # stderr(' If you don\'t want to install it, you can disable it via config. See here for more info:') + # stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Install') + return None + +def bin_path(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + + node_modules_bin = Path('.') / 'node_modules' / '.bin' / binary + if node_modules_bin.exists(): + return str(node_modules_bin.resolve()) + + return shutil.which(str(Path(binary).expanduser())) or shutil.which(str(binary)) or binary + +def bin_hash(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + abs_path = bin_path(binary) + if abs_path is None or not Path(abs_path).exists(): + return None + + file_hash = md5() + with io.open(abs_path, mode='rb') as f: + for chunk in iter(lambda: f.read(io.DEFAULT_BUFFER_SIZE), b''): + file_hash.update(chunk) + + return f'md5:{file_hash.hexdigest()}' + +def find_chrome_binary() -> Optional[str]: + """find any installed chrome binaries in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_executable_paths = ( + 'chromium-browser', + 'chromium', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + 'chrome', + 'google-chrome', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'google-chrome-stable', + 'google-chrome-beta', + 'google-chrome-canary', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'google-chrome-unstable', + 'google-chrome-dev', + ) + for name in default_executable_paths: + full_path_exists = shutil.which(name) + if full_path_exists: + return name + + return None + +def find_chrome_data_dir() -> Optional[str]: + """find any installed chrome user data directories in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_profile_paths = ( + '~/.config/chromium', + '~/Library/Application Support/Chromium', + '~/AppData/Local/Chromium/User Data', + '~/.config/chrome', + '~/.config/google-chrome', + '~/Library/Application Support/Google/Chrome', + '~/AppData/Local/Google/Chrome/User Data', + '~/.config/google-chrome-stable', + '~/.config/google-chrome-beta', + '~/Library/Application Support/Google/Chrome Canary', + '~/AppData/Local/Google/Chrome SxS/User Data', + '~/.config/google-chrome-unstable', + '~/.config/google-chrome-dev', + ) + for path in default_profile_paths: + full_path = Path(path).resolve() + if full_path.exists(): + return full_path + return None + +def wget_supports_compression(config): + try: + cmd = [ + config['WGET_BINARY'], + "--compression=auto", + "--help", + ] + return not run(cmd, stdout=DEVNULL, stderr=DEVNULL).returncode + except (FileNotFoundError, OSError): + return False + +def get_code_locations(config: ConfigDict) -> SimpleConfigValueDict: + return { + 'PACKAGE_DIR': { + 'path': (config['PACKAGE_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['PACKAGE_DIR'] / '__main__.py').exists(), + }, + 'TEMPLATES_DIR': { + 'path': (config['TEMPLATES_DIR']).resolve(), + 'enabled': True, + 'is_valid': (config['TEMPLATES_DIR'] / config['ACTIVE_THEME'] / 'static').exists(), + }, + # 'NODE_MODULES_DIR': { + # 'path': , + # 'enabled': , + # 'is_valid': (...).exists(), + # }, + } + +def get_external_locations(config: ConfigDict) -> ConfigValue: + abspath = lambda path: None if path is None else Path(path).resolve() + return { + 'CHROME_USER_DATA_DIR': { + 'path': abspath(config['CHROME_USER_DATA_DIR']), + 'enabled': config['USE_CHROME'] and config['CHROME_USER_DATA_DIR'], + 'is_valid': False if config['CHROME_USER_DATA_DIR'] is None else (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(), + }, + 'COOKIES_FILE': { + 'path': abspath(config['COOKIES_FILE']), + 'enabled': config['USE_WGET'] and config['COOKIES_FILE'], + 'is_valid': False if config['COOKIES_FILE'] is None else Path(config['COOKIES_FILE']).exists(), + }, + } + +def get_data_locations(config: ConfigDict) -> ConfigValue: + return { + 'OUTPUT_DIR': { + 'path': config['OUTPUT_DIR'].resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + 'SOURCES_DIR': { + 'path': config['SOURCES_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['SOURCES_DIR'].exists(), + }, + 'LOGS_DIR': { + 'path': config['LOGS_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['LOGS_DIR'].exists(), + }, + 'ARCHIVE_DIR': { + 'path': config['ARCHIVE_DIR'].resolve(), + 'enabled': True, + 'is_valid': config['ARCHIVE_DIR'].exists(), + }, + 'CONFIG_FILE': { + 'path': config['CONFIG_FILE'].resolve(), + 'enabled': True, + 'is_valid': config['CONFIG_FILE'].exists(), + }, + 'SQL_INDEX': { + 'path': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).resolve(), + 'enabled': True, + 'is_valid': (config['OUTPUT_DIR'] / SQL_INDEX_FILENAME).exists(), + }, + } + +def get_dependency_info(config: ConfigDict) -> ConfigValue: + return { + 'ARCHIVEBOX_BINARY': { + 'path': bin_path(config['ARCHIVEBOX_BINARY']), + 'version': config['VERSION'], + 'hash': bin_hash(config['ARCHIVEBOX_BINARY']), + 'enabled': True, + 'is_valid': True, + }, + 'PYTHON_BINARY': { + 'path': bin_path(config['PYTHON_BINARY']), + 'version': config['PYTHON_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'DJANGO_BINARY': { + 'path': bin_path(config['DJANGO_BINARY']), + 'version': config['DJANGO_VERSION'], + 'hash': bin_hash(config['DJANGO_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'CURL_BINARY': { + 'path': bin_path(config['CURL_BINARY']), + 'version': config['CURL_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': config['USE_CURL'], + 'is_valid': bool(config['CURL_VERSION']), + }, + 'WGET_BINARY': { + 'path': bin_path(config['WGET_BINARY']), + 'version': config['WGET_VERSION'], + 'hash': bin_hash(config['WGET_BINARY']), + 'enabled': config['USE_WGET'], + 'is_valid': bool(config['WGET_VERSION']), + }, + 'NODE_BINARY': { + 'path': bin_path(config['NODE_BINARY']), + 'version': config['NODE_VERSION'], + 'hash': bin_hash(config['NODE_BINARY']), + 'enabled': config['USE_NODE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'SINGLEFILE_BINARY': { + 'path': bin_path(config['SINGLEFILE_BINARY']), + 'version': config['SINGLEFILE_VERSION'], + 'hash': bin_hash(config['SINGLEFILE_BINARY']), + 'enabled': config['USE_SINGLEFILE'], + 'is_valid': bool(config['SINGLEFILE_VERSION']), + }, + 'READABILITY_BINARY': { + 'path': bin_path(config['READABILITY_BINARY']), + 'version': config['READABILITY_VERSION'], + 'hash': bin_hash(config['READABILITY_BINARY']), + 'enabled': config['USE_READABILITY'], + 'is_valid': bool(config['READABILITY_VERSION']), + }, + 'MERCURY_BINARY': { + 'path': bin_path(config['MERCURY_BINARY']), + 'version': config['MERCURY_VERSION'], + 'hash': bin_hash(config['MERCURY_BINARY']), + 'enabled': config['USE_MERCURY'], + 'is_valid': bool(config['MERCURY_VERSION']), + }, + 'GIT_BINARY': { + 'path': bin_path(config['GIT_BINARY']), + 'version': config['GIT_VERSION'], + 'hash': bin_hash(config['GIT_BINARY']), + 'enabled': config['USE_GIT'], + 'is_valid': bool(config['GIT_VERSION']), + }, + 'YOUTUBEDL_BINARY': { + 'path': bin_path(config['YOUTUBEDL_BINARY']), + 'version': config['YOUTUBEDL_VERSION'], + 'hash': bin_hash(config['YOUTUBEDL_BINARY']), + 'enabled': config['USE_YOUTUBEDL'], + 'is_valid': bool(config['YOUTUBEDL_VERSION']), + }, + 'CHROME_BINARY': { + 'path': bin_path(config['CHROME_BINARY']), + 'version': config['CHROME_VERSION'], + 'hash': bin_hash(config['CHROME_BINARY']), + 'enabled': config['USE_CHROME'], + 'is_valid': bool(config['CHROME_VERSION']), + }, + 'RIPGREP_BINARY': { + 'path': bin_path(config['RIPGREP_BINARY']), + 'version': config['RIPGREP_VERSION'], + 'hash': bin_hash(config['RIPGREP_BINARY']), + 'enabled': config['USE_RIPGREP'], + 'is_valid': bool(config['RIPGREP_VERSION']), + }, + # TODO: add an entry for the sonic search backend? + # 'SONIC_BINARY': { + # 'path': bin_path(config['SONIC_BINARY']), + # 'version': config['SONIC_VERSION'], + # 'hash': bin_hash(config['SONIC_BINARY']), + # 'enabled': config['USE_SONIC'], + # 'is_valid': bool(config['SONIC_VERSION']), + # }, + } + +def get_chrome_info(config: ConfigDict) -> ConfigValue: + return { + 'TIMEOUT': config['TIMEOUT'], + 'RESOLUTION': config['RESOLUTION'], + 'CHECK_SSL_VALIDITY': config['CHECK_SSL_VALIDITY'], + 'CHROME_BINARY': config['CHROME_BINARY'], + 'CHROME_HEADLESS': config['CHROME_HEADLESS'], + 'CHROME_SANDBOX': config['CHROME_SANDBOX'], + 'CHROME_USER_AGENT': config['CHROME_USER_AGENT'], + 'CHROME_USER_DATA_DIR': config['CHROME_USER_DATA_DIR'], + } + + +# ****************************************************************************** +# ****************************************************************************** +# ******************************** Load Config ********************************* +# ******* (compile the defaults, configs, and metadata all into CONFIG) ******** +# ****************************************************************************** +# ****************************************************************************** + + +def load_all_config(): + CONFIG: ConfigDict = {} + for section_name, section_config in CONFIG_SCHEMA.items(): + CONFIG = load_config(section_config, CONFIG) + + return load_config(DYNAMIC_CONFIG_SCHEMA, CONFIG) + +# add all final config values in CONFIG to globals in this file +CONFIG = load_all_config() +globals().update(CONFIG) +# this lets us do: from .config import DEBUG, MEDIA_TIMEOUT, ... + + +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** +# ****************************************************************************** + + + +########################### System Environment Setup ########################### + + +# Set timezone to UTC and umask to OUTPUT_PERMISSIONS +os.environ["TZ"] = 'UTC' +os.umask(0o777 - int(OUTPUT_PERMISSIONS, base=8)) # noqa: F821 + +# add ./node_modules/.bin to $PATH so we can use node scripts in extractors +NODE_BIN_PATH = str((Path(CONFIG["OUTPUT_DIR"]).absolute() / 'node_modules' / '.bin')) +sys.path.append(NODE_BIN_PATH) + + + + +########################### Config Validity Checkers ########################### + + +def check_system_config(config: ConfigDict=CONFIG) -> None: + ### Check system environment + if config['USER'] == 'root': + stderr('[!] ArchiveBox should never be run as root!', color='red') + stderr(' For more information, see the security overview documentation:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#do-not-run-as-root') + raise SystemExit(2) + + ### Check Python environment + if sys.version_info[:3] < (3, 6, 0): + stderr(f'[X] Python version is not new enough: {config["PYTHON_VERSION"]} (>3.6 is required)', color='red') + stderr(' See https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') + raise SystemExit(2) + + if config['PYTHON_ENCODING'] not in ('UTF-8', 'UTF8'): + stderr(f'[X] Your system is running python3 scripts with a bad locale setting: {config["PYTHON_ENCODING"]} (it should be UTF-8).', color='red') + stderr(' To fix it, add the line "export PYTHONIOENCODING=UTF-8" to your ~/.bashrc file (without quotes)') + stderr(' Or if you\'re using ubuntu/debian, run "dpkg-reconfigure locales"') + stderr('') + stderr(' Confirm that it\'s fixed by opening a new shell and running:') + stderr(' python3 -c "import sys; print(sys.stdout.encoding)" # should output UTF-8') + raise SystemExit(2) + + # stderr('[i] Using Chrome binary: {}'.format(shutil.which(CHROME_BINARY) or CHROME_BINARY)) + # stderr('[i] Using Chrome data dir: {}'.format(os.path.abspath(CHROME_USER_DATA_DIR))) + if config['CHROME_USER_DATA_DIR'] is not None: + if not (Path(config['CHROME_USER_DATA_DIR']) / 'Default').exists(): + stderr('[X] Could not find profile "Default" in CHROME_USER_DATA_DIR.', color='red') + stderr(f' {config["CHROME_USER_DATA_DIR"]}') + stderr(' Make sure you set it to a Chrome user data directory containing a Default profile folder.') + stderr(' For more info see:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#CHROME_USER_DATA_DIR') + if '/Default' in str(config['CHROME_USER_DATA_DIR']): + stderr() + stderr(' Try removing /Default from the end e.g.:') + stderr(' CHROME_USER_DATA_DIR="{}"'.format(config['CHROME_USER_DATA_DIR'].split('/Default')[0])) + raise SystemExit(2) + + +def check_dependencies(config: ConfigDict=CONFIG, show_help: bool=True) -> None: + invalid_dependencies = [ + (name, info) for name, info in config['DEPENDENCIES'].items() + if info['enabled'] and not info['is_valid'] + ] + if invalid_dependencies and show_help: + stderr(f'[!] Warning: Missing {len(invalid_dependencies)} recommended dependencies', color='lightyellow') + for dependency, info in invalid_dependencies: + stderr( + ' ! {}: {} ({})'.format( + dependency, + info['path'] or 'unable to find binary', + info['version'] or 'unable to detect version', + ) + ) + if dependency in ('SINGLEFILE_BINARY', 'READABILITY_BINARY', 'MERCURY_BINARY'): + hint(('npm install --prefix . "git+https://github.com/ArchiveBox/ArchiveBox.git"', + f'or archivebox config --set SAVE_{dependency.rsplit("_", 1)[0]}=False to silence this warning', + ''), prefix=' ') + stderr('') + + if config['TIMEOUT'] < 5: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' You must allow *at least* 5 seconds for indexing and archive methods to run succesfully.') + stderr(' (Setting it to somewhere between 30 and 3000 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + elif config['USE_CHROME'] and config['TIMEOUT'] < 15: + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' Chrome will fail to archive all sites if set to less than ~15 seconds.') + stderr(' (Setting it to somewhere between 30 and 300 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#archive-method-toggles') + stderr() + + if config['USE_YOUTUBEDL'] and config['MEDIA_TIMEOUT'] < 20: + stderr(f'[!] Warning: MEDIA_TIMEOUT is set too low! (currently set to MEDIA_TIMEOUT={config["MEDIA_TIMEOUT"]} seconds)', color='red') + stderr(' Youtube-dl will fail to archive all media if set to less than ~20 seconds.') + stderr(' (Setting it somewhere over 60 seconds is recommended)') + stderr() + stderr(' If you want to disable media archiving entirely, set SAVE_MEDIA=False instead:') + stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#save_media') + stderr() + +def check_data_folder(out_dir: Union[str, Path, None]=None, config: ConfigDict=CONFIG) -> None: + output_dir = out_dir or config['OUTPUT_DIR'] + assert isinstance(output_dir, (str, Path)) + + sql_index_exists = (Path(output_dir) / SQL_INDEX_FILENAME).exists() + if not sql_index_exists: + stderr('[X] No archivebox index found in the current directory.', color='red') + stderr(f' {output_dir}', color='lightyellow') + stderr() + stderr(' {lightred}Hint{reset}: Are you running archivebox in the right folder?'.format(**config['ANSI'])) + stderr(' cd path/to/your/archive/folder') + stderr(' archivebox [command]') + stderr() + stderr(' {lightred}Hint{reset}: To create a new archive collection or import existing data in this folder, run:'.format(**config['ANSI'])) + stderr(' archivebox init') + raise SystemExit(2) + + from .index.sql import list_migrations + + pending_migrations = [name for status, name in list_migrations() if not status] + + if (not sql_index_exists) or pending_migrations: + if sql_index_exists: + pending_operation = f'apply the {len(pending_migrations)} pending migrations' + else: + pending_operation = 'generate the new SQL main index' + + stderr('[X] This collection was created with an older version of ArchiveBox and must be upgraded first.', color='lightyellow') + stderr(f' {output_dir}') + stderr() + stderr(f' To upgrade it to the latest version and {pending_operation} run:') + stderr(' archivebox init') + raise SystemExit(3) + + sources_dir = Path(output_dir) / SOURCES_DIR_NAME + if not sources_dir.exists(): + sources_dir.mkdir() + + + +def setup_django(out_dir: Path=None, check_db=False, config: ConfigDict=CONFIG, in_memory_db=False) -> None: + check_system_config() + + output_dir = out_dir or Path(config['OUTPUT_DIR']) + + assert isinstance(output_dir, Path) and isinstance(config['PACKAGE_DIR'], Path) + + try: + import django + sys.path.append(str(config['PACKAGE_DIR'])) + os.environ.setdefault('OUTPUT_DIR', str(output_dir)) + assert (config['PACKAGE_DIR'] / 'core' / 'settings.py').exists(), 'settings.py was not found at archivebox/core/settings.py' + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + + if in_memory_db: + # Put the db in memory and run migrations in case any command requires it + from django.core.management import call_command + os.environ.setdefault("ARCHIVEBOX_DATABASE_NAME", ":memory:") + django.setup() + call_command("migrate", interactive=False, verbosity=0) + else: + django.setup() + + if check_db: + sql_index_path = Path(output_dir) / SQL_INDEX_FILENAME + assert sql_index_path.exists(), ( + f'No database file {SQL_INDEX_FILENAME} found in OUTPUT_DIR: {config["OUTPUT_DIR"]}') + except KeyboardInterrupt: + raise SystemExit(2) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config_stubs.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config_stubs.py new file mode 100644 index 0000000..988f58a --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/config_stubs.py @@ -0,0 +1,113 @@ +from pathlib import Path +from typing import Optional, Dict, Union, Tuple, Callable, Pattern, Type, Any, List +from mypy_extensions import TypedDict + + + +SimpleConfigValue = Union[str, bool, int, None, Pattern, Dict[str, Any]] +SimpleConfigValueDict = Dict[str, SimpleConfigValue] +SimpleConfigValueGetter = Callable[[], SimpleConfigValue] +ConfigValue = Union[SimpleConfigValue, SimpleConfigValueDict, SimpleConfigValueGetter] + + +class BaseConfig(TypedDict): + pass + +class ConfigDict(BaseConfig, total=False): + """ + # Regenerate by pasting this quine into `archivebox shell` 🥚 + from archivebox.config import ConfigDict, CONFIG_DEFAULTS + print('class ConfigDict(BaseConfig, total=False):') + print(' ' + '"'*3 + ConfigDict.__doc__ + '"'*3) + for section, configs in CONFIG_DEFAULTS.items(): + for key, attrs in configs.items(): + Type, default = attrs['type'], attrs['default'] + if default is None: + print(f' {key}: Optional[{Type.__name__}]') + else: + print(f' {key}: {Type.__name__}') + print() + """ + IS_TTY: bool + USE_COLOR: bool + SHOW_PROGRESS: bool + IN_DOCKER: bool + + PACKAGE_DIR: Path + OUTPUT_DIR: Path + CONFIG_FILE: Path + ONLY_NEW: bool + TIMEOUT: int + MEDIA_TIMEOUT: int + OUTPUT_PERMISSIONS: str + RESTRICT_FILE_NAMES: str + URL_BLACKLIST: str + + SECRET_KEY: Optional[str] + BIND_ADDR: str + ALLOWED_HOSTS: str + DEBUG: bool + PUBLIC_INDEX: bool + PUBLIC_SNAPSHOTS: bool + FOOTER_INFO: str + ACTIVE_THEME: str + + SAVE_TITLE: bool + SAVE_FAVICON: bool + SAVE_WGET: bool + SAVE_WGET_REQUISITES: bool + SAVE_SINGLEFILE: bool + SAVE_READABILITY: bool + SAVE_MERCURY: bool + SAVE_PDF: bool + SAVE_SCREENSHOT: bool + SAVE_DOM: bool + SAVE_WARC: bool + SAVE_GIT: bool + SAVE_MEDIA: bool + SAVE_ARCHIVE_DOT_ORG: bool + + RESOLUTION: str + GIT_DOMAINS: str + CHECK_SSL_VALIDITY: bool + CURL_USER_AGENT: str + WGET_USER_AGENT: str + CHROME_USER_AGENT: str + COOKIES_FILE: Union[str, Path, None] + CHROME_USER_DATA_DIR: Union[str, Path, None] + CHROME_HEADLESS: bool + CHROME_SANDBOX: bool + + USE_CURL: bool + USE_WGET: bool + USE_SINGLEFILE: bool + USE_READABILITY: bool + USE_MERCURY: bool + USE_GIT: bool + USE_CHROME: bool + USE_YOUTUBEDL: bool + CURL_BINARY: str + GIT_BINARY: str + WGET_BINARY: str + SINGLEFILE_BINARY: str + READABILITY_BINARY: str + MERCURY_BINARY: str + YOUTUBEDL_BINARY: str + CHROME_BINARY: Optional[str] + + YOUTUBEDL_ARGS: List[str] + WGET_ARGS: List[str] + CURL_ARGS: List[str] + GIT_ARGS: List[str] + + +ConfigDefaultValueGetter = Callable[[ConfigDict], ConfigValue] +ConfigDefaultValue = Union[ConfigValue, ConfigDefaultValueGetter] + +ConfigDefault = TypedDict('ConfigDefault', { + 'default': ConfigDefaultValue, + 'type': Optional[Type], + 'aliases': Optional[Tuple[str, ...]], +}, total=False) + +ConfigDefaultDict = Dict[str, ConfigDefault] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/__init__.py new file mode 100644 index 0000000..3e1d607 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox.core' diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/admin.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/admin.py new file mode 100644 index 0000000..832bea3 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/admin.py @@ -0,0 +1,257 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.contrib import admin +from django.urls import path +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.shortcuts import render, redirect +from django.contrib.auth import get_user_model +from django import forms + +from core.models import Snapshot, Tag +from core.forms import AddLinkForm, TagField + +from core.mixins import SearchResultsAdminMixin + +from index.html import snapshot_icons +from util import htmldecode, urldecode, ansi_to_html +from logging_util import printable_filesize +from main import add, remove +from config import OUTPUT_DIR +from extractors import archive_links + +# TODO: https://stackoverflow.com/questions/40760880/add-custom-button-to-django-admin-panel + +def update_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], out_dir=OUTPUT_DIR) +update_snapshots.short_description = "Archive" + +def update_titles(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, methods=('title','favicon'), out_dir=OUTPUT_DIR) +update_titles.short_description = "Pull title" + +def overwrite_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, out_dir=OUTPUT_DIR) +overwrite_snapshots.short_description = "Re-archive (overwrite)" + +def verify_snapshots(modeladmin, request, queryset): + for snapshot in queryset: + print(snapshot.timestamp, snapshot.url, snapshot.is_archived, snapshot.archive_size, len(snapshot.history)) + +verify_snapshots.short_description = "Check" + +def delete_snapshots(modeladmin, request, queryset): + remove(snapshots=queryset, yes=True, delete=True, out_dir=OUTPUT_DIR) + +delete_snapshots.short_description = "Delete" + + +class SnapshotAdminForm(forms.ModelForm): + tags = TagField(required=False) + + class Meta: + model = Snapshot + fields = "__all__" + + def save(self, commit=True): + # Based on: https://stackoverflow.com/a/49933068/3509554 + + # Get the unsave instance + instance = forms.ModelForm.save(self, False) + tags = self.cleaned_data.pop("tags") + + #update save_m2m + def new_save_m2m(): + instance.save_tags(tags) + + # Do we need to save all changes now? + self.save_m2m = new_save_m2m + if commit: + instance.save() + + return instance + + +class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): + list_display = ('added', 'title_str', 'url_str', 'files', 'size') + sort_fields = ('title_str', 'url_str', 'added') + readonly_fields = ('id', 'url', 'timestamp', 'num_outputs', 'is_archived', 'url_hash', 'added', 'updated') + search_fields = ['url', 'timestamp', 'title', 'tags__name'] + fields = (*readonly_fields, 'title', 'tags') + list_filter = ('added', 'updated', 'tags') + ordering = ['-added'] + actions = [delete_snapshots, overwrite_snapshots, update_snapshots, update_titles, verify_snapshots] + actions_template = 'admin/actions_as_select.html' + form = SnapshotAdminForm + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('grid/', self.admin_site.admin_view(self.grid_view),name='grid') + ] + return custom_urls + urls + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return ', '.join(obj.tags.values_list('name', flat=True)) + + def id_str(self, obj): + return format_html( + '{}', + obj.url_hash[:8], + ) + + def title_str(self, obj): + canon = obj.as_link().canonical_outputs() + tags = ''.join( + format_html('{} ', tag.id, tag) + for tag in obj.tags.all() + if str(tag).strip() + ) + return format_html( + '' + '' + '' + '' + '{}' + '', + obj.archive_path, + obj.archive_path, canon['favicon_path'], + obj.archive_path, + 'fetched' if obj.latest_title or obj.title else 'pending', + urldecode(htmldecode(obj.latest_title or obj.title or ''))[:128] or 'Pending...' + ) + mark_safe(f' {tags}') + + def files(self, obj): + return snapshot_icons(obj) + + def size(self, obj): + archive_size = obj.archive_size + if archive_size: + size_txt = printable_filesize(archive_size) + if archive_size > 52428800: + size_txt = mark_safe(f'{size_txt}') + else: + size_txt = mark_safe('...') + return format_html( + '{}', + obj.archive_path, + size_txt, + ) + + def url_str(self, obj): + return format_html( + '{}', + obj.url, + obj.url.split('://www.', 1)[-1].split('://', 1)[-1][:64], + ) + + def grid_view(self, request): + + # cl = self.get_changelist_instance(request) + + # Save before monkey patching to restore for changelist list view + saved_change_list_template = self.change_list_template + saved_list_per_page = self.list_per_page + saved_list_max_show_all = self.list_max_show_all + + # Monkey patch here plus core_tags.py + self.change_list_template = 'admin/grid_change_list.html' + self.list_per_page = 20 + self.list_max_show_all = self.list_per_page + + # Call monkey patched view + rendered_response = self.changelist_view(request) + + # Restore values + self.change_list_template = saved_change_list_template + self.list_per_page = saved_list_per_page + self.list_max_show_all = saved_list_max_show_all + + return rendered_response + + + id_str.short_description = 'ID' + title_str.short_description = 'Title' + url_str.short_description = 'Original URL' + + id_str.admin_order_field = 'id' + title_str.admin_order_field = 'title' + url_str.admin_order_field = 'url' + +class TagAdmin(admin.ModelAdmin): + list_display = ('slug', 'name', 'id') + sort_fields = ('id', 'name', 'slug') + readonly_fields = ('id',) + search_fields = ('id', 'name', 'slug') + fields = (*readonly_fields, 'name', 'slug') + + +class ArchiveBoxAdmin(admin.AdminSite): + site_header = 'ArchiveBox' + index_title = 'Links' + site_title = 'Index' + + def get_urls(self): + return [ + path('core/snapshot/add/', self.add_view, name='Add'), + ] + super().get_urls() + + def add_view(self, request): + if not request.user.is_authenticated: + return redirect(f'/admin/login/?next={request.path}') + + request.current_app = self.name + context = { + **self.each_context(request), + 'title': 'Add URLs', + } + + if request.method == 'GET': + context['form'] = AddLinkForm() + + elif request.method == 'POST': + form = AddLinkForm(request.POST) + if form.is_valid(): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + else: + context["form"] = form + + return render(template_name='add_links.html', request=request, context=context) + +admin.site = ArchiveBoxAdmin() +admin.site.register(get_user_model()) +admin.site.register(Snapshot, SnapshotAdmin) +admin.site.register(Tag, TagAdmin) +admin.site.disable_action('delete_selected') diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/apps.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/forms.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/forms.py new file mode 100644 index 0000000..86b29bb --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/forms.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.core' + +from django import forms + +from ..util import URL_REGEX +from ..vendor.taggit_utils import edit_string_for_tags, parse_tags + +CHOICES = ( + ('0', 'depth = 0 (archive just these URLs)'), + ('1', 'depth = 1 (archive these URLs and all URLs one hop away)'), +) + +from ..extractors import get_default_archive_methods + +ARCHIVE_METHODS = [ + (name, name) + for name, _, _ in get_default_archive_methods() +] + + +class AddLinkForm(forms.Form): + url = forms.RegexField(label="URLs (one per line)", regex=URL_REGEX, min_length='6', strip=True, widget=forms.Textarea, required=True) + depth = forms.ChoiceField(label="Archive depth", choices=CHOICES, widget=forms.RadioSelect, initial='0') + archive_methods = forms.MultipleChoiceField( + required=False, + widget=forms.SelectMultiple, + choices=ARCHIVE_METHODS, + ) +class TagWidgetMixin: + def format_value(self, value): + if value is not None and not isinstance(value, str): + value = edit_string_for_tags(value) + return super().format_value(value) + +class TagWidget(TagWidgetMixin, forms.TextInput): + pass + +class TagField(forms.CharField): + widget = TagWidget + + def clean(self, value): + value = super().clean(value) + try: + return parse_tags(value) + except ValueError: + raise forms.ValidationError( + "Please provide a comma-separated list of tags." + ) + + def has_changed(self, initial_value, data_value): + # Always return False if the field is disabled since self.bound_data + # always uses the initial value in this case. + if self.disabled: + return False + + try: + data_value = self.clean(data_value) + except forms.ValidationError: + pass + + if initial_value is None: + initial_value = [] + + initial_value = [tag.name for tag in initial_value] + initial_value.sort() + + return initial_value != data_value diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/management/commands/archivebox.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/management/commands/archivebox.py new file mode 100644 index 0000000..a68b5d9 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/management/commands/archivebox.py @@ -0,0 +1,18 @@ +__package__ = 'archivebox' + +from django.core.management.base import BaseCommand + + +from .cli import run_subcommand + + +class Command(BaseCommand): + help = 'Run an ArchiveBox CLI subcommand (e.g. add, remove, list, etc)' + + def add_arguments(self, parser): + parser.add_argument('subcommand', type=str, help='The subcommand you want to run') + parser.add_argument('command_args', nargs='*', help='Arguments to pass to the subcommand') + + + def handle(self, *args, **kwargs): + run_subcommand(kwargs['subcommand'], args=kwargs['command_args']) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0001_initial.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0001_initial.py new file mode 100644 index 0000000..73ac78e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2019-05-01 03:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Snapshot', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('url', models.URLField(unique=True)), + ('timestamp', models.CharField(default=None, max_length=32, null=True, unique=True)), + ('title', models.CharField(default=None, max_length=128, null=True)), + ('tags', models.CharField(default=None, max_length=256, null=True)), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(default=None, null=True)), + ], + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0002_auto_20200625_1521.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0002_auto_20200625_1521.py new file mode 100644 index 0000000..4811282 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0002_auto_20200625_1521.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-06-25 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0003_auto_20200630_1034.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0003_auto_20200630_1034.py new file mode 100644 index 0000000..61fd472 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0003_auto_20200630_1034.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.7 on 2020-06-30 10:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20200625_1521'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='added', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(db_index=True, default=None, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(db_index=True, default=None, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(db_index=True, default=None, null=True), + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0004_auto_20200713_1552.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0004_auto_20200713_1552.py new file mode 100644 index 0000000..6983662 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0004_auto_20200713_1552.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-07-13 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_auto_20200630_1034'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, unique=True), + preserve_default=False, + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0005_auto_20200728_0326.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0005_auto_20200728_0326.py new file mode 100644 index 0000000..f367aeb --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0005_auto_20200728_0326.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-07-28 03:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20200713_1552'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(blank=True, db_index=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(blank=True, db_index=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0006_auto_20201012_1520.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0006_auto_20201012_1520.py new file mode 100644 index 0000000..694c990 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0006_auto_20201012_1520.py @@ -0,0 +1,70 @@ +# Generated by Django 3.0.8 on 2020-10-12 15:20 + +from django.db import migrations, models +from django.utils.text import slugify + +def forwards_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags + tag_set = ( + set(tag.strip() for tag in (snapshot.tags_old or '').split(',')) + ) + tag_set.discard("") + + for tag in tag_set: + to_add, _ = TagModel.objects.get_or_create(name=tag, slug=slugify(tag)) + snapshot.tags.add(to_add) + + +def reverse_func(apps, schema_editor): + SnapshotModel = apps.get_model("core", "Snapshot") + TagModel = apps.get_model("core", "Tag") + + db_alias = schema_editor.connection.alias + snapshots = SnapshotModel.objects.all() + for snapshot in snapshots: + tags = snapshot.tags.values_list("name", flat=True) + snapshot.tags_old = ",".join([tag for tag in tags]) + snapshot.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20200728_0326'), + ] + + operations = [ + migrations.RenameField( + model_name='snapshot', + old_name='tags', + new_name='tags_old', + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), + ('slug', models.SlugField(max_length=100, unique=True, verbose_name='slug')), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + }, + ), + migrations.AddField( + model_name='snapshot', + name='tags', + field=models.ManyToManyField(to='core.Tag'), + ), + migrations.RunPython(forwards_func, reverse_func), + migrations.RemoveField( + model_name='snapshot', + name='tags_old', + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0007_archiveresult.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0007_archiveresult.py new file mode 100644 index 0000000..a780376 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0007_archiveresult.py @@ -0,0 +1,97 @@ +# Generated by Django 3.0.8 on 2020-11-04 12:25 + +import json +from pathlib import Path + +from django.db import migrations, models +import django.db.models.deletion + +from config import CONFIG +from index.json import to_json + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +def forwards_func(apps, schema_editor): + from core.models import EXTRACTORS + + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + + snapshots = Snapshot.objects.all() + for snapshot in snapshots: + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + + try: + with open(out_dir / "index.json", "r") as f: + fs_index = json.load(f) + except Exception as e: + continue + + history = fs_index["history"] + + for extractor in history: + for result in history[extractor]: + ArchiveResult.objects.create(extractor=extractor, snapshot=snapshot, cmd=result["cmd"], cmd_version=result["cmd_version"], + start_ts=result["start_ts"], end_ts=result["end_ts"], status=result["status"], pwd=result["pwd"], output=result["output"]) + + +def verify_json_index_integrity(snapshot): + results = snapshot.archiveresult_set.all() + out_dir = Path(CONFIG['ARCHIVE_DIR']) / snapshot.timestamp + with open(out_dir / "index.json", "r") as f: + index = json.load(f) + + history = index["history"] + index_results = [result for extractor in history for result in history[extractor]] + flattened_results = [result["start_ts"] for result in index_results] + + missing_results = [result for result in results if result.start_ts.isoformat() not in flattened_results] + + for missing in missing_results: + index["history"][missing.extractor].append({"cmd": missing.cmd, "cmd_version": missing.cmd_version, "end_ts": missing.end_ts.isoformat(), + "start_ts": missing.start_ts.isoformat(), "pwd": missing.pwd, "output": missing.output, + "schema": "ArchiveResult", "status": missing.status}) + + json_index = to_json(index) + with open(out_dir / "index.json", "w") as f: + f.write(json_index) + + +def reverse_func(apps, schema_editor): + Snapshot = apps.get_model("core", "Snapshot") + ArchiveResult = apps.get_model("core", "ArchiveResult") + for snapshot in Snapshot.objects.all(): + verify_json_index_integrity(snapshot) + + ArchiveResult.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20201012_1520'), + ] + + operations = [ + migrations.CreateModel( + name='ArchiveResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cmd', JSONField()), + ('pwd', models.CharField(max_length=256)), + ('cmd_version', models.CharField(max_length=32)), + ('status', models.CharField(choices=[('succeeded', 'succeeded'), ('failed', 'failed'), ('skipped', 'skipped')], max_length=16)), + ('output', models.CharField(max_length=512)), + ('start_ts', models.DateTimeField()), + ('end_ts', models.DateTimeField()), + ('extractor', models.CharField(choices=[('title', 'title'), ('favicon', 'favicon'), ('wget', 'wget'), ('singlefile', 'singlefile'), ('pdf', 'pdf'), ('screenshot', 'screenshot'), ('dom', 'dom'), ('readability', 'readability'), ('mercury', 'mercury'), ('git', 'git'), ('media', 'media'), ('headers', 'headers'), ('archive_org', 'archive_org')], max_length=32)), + ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Snapshot')), + ], + ), + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0008_auto_20210105_1421.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0008_auto_20210105_1421.py new file mode 100644 index 0000000..e5b3387 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/0008_auto_20210105_1421.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-01-05 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_archiveresult'), + ] + + operations = [ + migrations.AlterField( + model_name='archiveresult', + name='cmd_version', + field=models.CharField(blank=True, default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/mixins.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/mixins.py new file mode 100644 index 0000000..538ca1e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/mixins.py @@ -0,0 +1,23 @@ +from django.contrib import messages + +from archivebox.search import query_search_index + +class SearchResultsAdminMixin(object): + def get_search_results(self, request, queryset, search_term): + ''' Enhances the search queryset with results from the search backend. + ''' + qs, use_distinct = \ + super(SearchResultsAdminMixin, self).get_search_results( + request, queryset, search_term) + + search_term = search_term.strip() + if not search_term: + return qs, use_distinct + try: + qsearch = query_search_index(search_term) + except Exception as err: + messages.add_message(request, messages.WARNING, f'Error from the search backend, only showing results from default admin search fields - Error: {err}') + else: + qs = queryset & qsearch + finally: + return qs, use_distinct diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/models.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/models.py new file mode 100644 index 0000000..13d75b6 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/models.py @@ -0,0 +1,194 @@ +__package__ = 'archivebox.core' + +import uuid + +from django.db import models, transaction +from django.utils.functional import cached_property +from django.utils.text import slugify +from django.db.models import Case, When, Value, IntegerField + +from ..util import parse_date +from ..index.schema import Link +from ..extractors import get_default_archive_methods, ARCHIVE_METHODS_INDEXING_PRECEDENCE + +EXTRACTORS = [(extractor[0], extractor[0]) for extractor in get_default_archive_methods()] +STATUS_CHOICES = [ + ("succeeded", "succeeded"), + ("failed", "failed"), + ("skipped", "skipped") +] + +try: + JSONField = models.JSONField +except AttributeError: + import jsonfield + JSONField = jsonfield.JSONField + + +class Tag(models.Model): + """ + Based on django-taggit model + """ + name = models.CharField(verbose_name="name", unique=True, blank=False, max_length=100) + slug = models.SlugField(verbose_name="slug", unique=True, max_length=100) + + class Meta: + verbose_name = "Tag" + verbose_name_plural = "Tags" + + def __str__(self): + return self.name + + def slugify(self, tag, i=None): + slug = slugify(tag) + if i is not None: + slug += "_%d" % i + return slug + + def save(self, *args, **kwargs): + if self._state.adding and not self.slug: + self.slug = self.slugify(self.name) + + with transaction.atomic(): + slugs = set( + type(self) + ._default_manager.filter(slug__startswith=self.slug) + .values_list("slug", flat=True) + ) + + i = None + while True: + slug = self.slugify(self.name, i) + if slug not in slugs: + self.slug = slug + return super().save(*args, **kwargs) + i = 1 if i is None else i+1 + else: + return super().save(*args, **kwargs) + + +class Snapshot(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + url = models.URLField(unique=True) + timestamp = models.CharField(max_length=32, unique=True, db_index=True) + + title = models.CharField(max_length=128, null=True, blank=True, db_index=True) + + added = models.DateTimeField(auto_now_add=True, db_index=True) + updated = models.DateTimeField(null=True, blank=True, db_index=True) + tags = models.ManyToManyField(Tag) + + keys = ('url', 'timestamp', 'title', 'tags', 'updated') + + def __repr__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + def __str__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + @classmethod + def from_json(cls, info: dict): + info = {k: v for k, v in info.items() if k in cls.keys} + return cls(**info) + + def as_json(self, *args) -> dict: + args = args or self.keys + return { + key: getattr(self, key) + if key != 'tags' else self.tags_str() + for key in args + } + + def as_link(self) -> Link: + return Link.from_json(self.as_json()) + + def as_link_with_details(self) -> Link: + from ..index import load_link_details + return load_link_details(self.as_link()) + + def tags_str(self) -> str: + return ','.join(self.tags.order_by('name').values_list('name', flat=True)) + + @cached_property + def bookmarked(self): + return parse_date(self.timestamp) + + @cached_property + def is_archived(self): + return self.as_link().is_archived + + @cached_property + def num_outputs(self): + return self.archiveresult_set.filter(status='succeeded').count() + + @cached_property + def url_hash(self): + return self.as_link().url_hash + + @cached_property + def base_url(self): + return self.as_link().base_url + + @cached_property + def link_dir(self): + return self.as_link().link_dir + + @cached_property + def archive_path(self): + return self.as_link().archive_path + + @cached_property + def archive_size(self): + return self.as_link().archive_size + + @cached_property + def history(self): + # TODO: use ArchiveResult for this instead of json + return self.as_link_with_details().history + + @cached_property + def latest_title(self): + if ('title' in self.history + and self.history['title'] + and (self.history['title'][-1].status == 'succeeded') + and self.history['title'][-1].output.strip()): + return self.history['title'][-1].output.strip() + return None + + def save_tags(self, tags=()): + tags_id = [] + for tag in tags: + tags_id.append(Tag.objects.get_or_create(name=tag)[0].id) + self.tags.clear() + self.tags.add(*tags_id) + + +class ArchiveResultManager(models.Manager): + def indexable(self, sorted: bool = True): + INDEXABLE_METHODS = [ r[0] for r in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = self.get_queryset().filter(extractor__in=INDEXABLE_METHODS,status='succeeded') + + if sorted: + precedence = [ When(extractor=method, then=Value(precedence)) for method, precedence in ARCHIVE_METHODS_INDEXING_PRECEDENCE ] + qs = qs.annotate(indexing_precedence=Case(*precedence, default=Value(1000),output_field=IntegerField())).order_by('indexing_precedence') + return qs + + +class ArchiveResult(models.Model): + snapshot = models.ForeignKey(Snapshot, on_delete=models.CASCADE) + cmd = JSONField() + pwd = models.CharField(max_length=256) + cmd_version = models.CharField(max_length=32, default=None, null=True, blank=True) + output = models.CharField(max_length=512) + start_ts = models.DateTimeField() + end_ts = models.DateTimeField() + status = models.CharField(max_length=16, choices=STATUS_CHOICES) + extractor = models.CharField(choices=EXTRACTORS, max_length=32) + + objects = ArchiveResultManager() + + def __str__(self): + return self.extractor diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/settings.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/settings.py new file mode 100644 index 0000000..e8ed6b1 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/settings.py @@ -0,0 +1,165 @@ +__package__ = 'archivebox.core' + +import os +import sys + +from pathlib import Path +from django.utils.crypto import get_random_string + +from ..config import ( # noqa: F401 + DEBUG, + SECRET_KEY, + ALLOWED_HOSTS, + PACKAGE_DIR, + ACTIVE_THEME, + TEMPLATES_DIR_NAME, + SQL_INDEX_FILENAME, + OUTPUT_DIR, +) + + +IS_MIGRATING = 'makemigrations' in sys.argv[:3] or 'migrate' in sys.argv[:3] +IS_TESTING = 'test' in sys.argv[:3] or 'PYTEST_CURRENT_TEST' in os.environ +IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3] + +################################################################################ +### Django Core Settings +################################################################################ + +WSGI_APPLICATION = 'core.wsgi.application' +ROOT_URLCONF = 'core.urls' + +LOGIN_URL = '/accounts/login/' +LOGOUT_REDIRECT_URL = '/' +PASSWORD_RESET_URL = '/accounts/password_reset/' +APPEND_SLASH = True + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'core', + + 'django_extensions', +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + + +################################################################################ +### Staticfile and Template Settings +################################################################################ + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME / 'static'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default' / 'static'), +] + +TEMPLATE_DIRS = [ + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / ACTIVE_THEME), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'default'), + str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME), +] + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': TEMPLATE_DIRS, + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + + +################################################################################ +### External Service Settings +################################################################################ + +DATABASE_FILE = Path(OUTPUT_DIR) / SQL_INDEX_FILENAME +DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", DATABASE_FILE) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': DATABASE_NAME, + } +} + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + + +################################################################################ +### Security Settings +################################################################################ + +SECRET_KEY = SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.') + +ALLOWED_HOSTS = ALLOWED_HOSTS.split(',') + +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True + +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = False +SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_AGE = 1209600 # 2 weeks +SESSION_EXPIRE_AT_BROWSER_CLOSE = False +SESSION_SAVE_EVERY_REQUEST = True + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + + +################################################################################ +### Shell Settings +################################################################################ + +SHELL_PLUS = 'ipython' +SHELL_PLUS_PRINT_SQL = False +IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner'] +IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell' +if IS_SHELL: + os.environ['PYTHONSTARTUP'] = str(Path(PACKAGE_DIR) / 'core' / 'welcome_message.py') + + +################################################################################ +### Internationalization & Localization Settings +################################################################################ + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = False +USE_L10N = False +USE_TZ = False + +DATETIME_FORMAT = 'Y-m-d g:iA' +SHORT_DATETIME_FORMAT = 'Y-m-d h:iA' diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/core_tags.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/core_tags.py new file mode 100644 index 0000000..25f0685 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/templatetags/core_tags.py @@ -0,0 +1,47 @@ +from django import template +from django.urls import reverse +from django.contrib.admin.templatetags.base import InclusionAdminNode +from django.templatetags.static import static + + +from typing import Union + +from core.models import ArchiveResult + +register = template.Library() + +@register.simple_tag +def snapshot_image(snapshot): + result = ArchiveResult.objects.filter(snapshot=snapshot, extractor='screenshot', status='succeeded').first() + if result: + return reverse('LinkAssets', args=[f'{str(snapshot.timestamp)}/{result.output}']) + + return static('archive.png') + +@register.filter +def file_size(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + +def result_list(cl): + """ + Monkey patched result + """ + num_sorted_fields = 0 + return { + 'cl': cl, + 'num_sorted_fields': num_sorted_fields, + 'results': cl.result_list, + } + +@register.tag(name='snapshots_grid') +def result_list_tag(parser, token): + return InclusionAdminNode( + parser, token, + func=result_list, + template_name='snapshots_grid.html', + takes_context=False, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/tests.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/tests.py new file mode 100644 index 0000000..4d66077 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/tests.py @@ -0,0 +1,3 @@ +#from django.test import TestCase + +# Create your tests here. diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/urls.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/urls.py new file mode 100644 index 0000000..b8e4baf --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/urls.py @@ -0,0 +1,36 @@ +from django.contrib import admin + +from django.urls import path, include +from django.views import static +from django.conf import settings +from django.views.generic.base import RedirectView + +from core.views import MainIndex, LinkDetails, PublicArchiveView, AddView + + +# print('DEBUG', settings.DEBUG) + +urlpatterns = [ + path('robots.txt', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'robots.txt'}), + path('favicon.ico', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'favicon.ico'}), + + path('docs/', RedirectView.as_view(url='https://github.com/ArchiveBox/ArchiveBox/wiki'), name='Docs'), + + path('archive/', RedirectView.as_view(url='/')), + path('archive/', LinkDetails.as_view(), name='LinkAssets'), + + path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')), + path('add/', AddView.as_view()), + + path('accounts/login/', RedirectView.as_view(url='/admin/login/')), + path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')), + + + path('accounts/', include('django.contrib.auth.urls')), + path('admin/', admin.site.urls), + + path('index.html', RedirectView.as_view(url='/')), + path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}), + path('', MainIndex.as_view(), name='Home'), + path('public/', PublicArchiveView.as_view(), name='public-index'), +] diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/views.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/views.py new file mode 100644 index 0000000..b46e364 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/views.py @@ -0,0 +1,173 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout + +from django.shortcuts import render, redirect + +from django.http import HttpResponse +from django.views import View, static +from django.views.generic.list import ListView +from django.views.generic import FormView +from django.contrib.auth.mixins import UserPassesTestMixin + +from core.models import Snapshot +from core.forms import AddLinkForm + +from ..config import ( + OUTPUT_DIR, + PUBLIC_INDEX, + PUBLIC_SNAPSHOTS, + PUBLIC_ADD_VIEW, + VERSION, + FOOTER_INFO, +) +from main import add +from ..util import base_url, ansi_to_html +from ..index.html import snapshot_icons + + +class MainIndex(View): + template = 'main_index.html' + + def get(self, request): + if request.user.is_authenticated: + return redirect('/admin/core/snapshot/') + + if PUBLIC_INDEX: + return redirect('public-index') + + return redirect(f'/admin/login/?next={request.path}') + + +class LinkDetails(View): + def get(self, request, path): + # missing trailing slash -> redirect to index + if '/' not in path: + return redirect(f'{path}/index.html') + + if not request.user.is_authenticated and not PUBLIC_SNAPSHOTS: + return redirect(f'/admin/login/?next={request.path}') + + try: + slug, archivefile = path.split('/', 1) + except (IndexError, ValueError): + slug, archivefile = path.split('/', 1)[0], 'index.html' + + all_pages = list(Snapshot.objects.all()) + + # slug is a timestamp + by_ts = {page.timestamp: page for page in all_pages} + try: + # print('SERVING STATICFILE', by_ts[slug].link_dir, request.path, path) + response = static.serve(request, archivefile, document_root=by_ts[slug].link_dir, show_indexes=True) + response["Link"] = f'<{by_ts[slug].url}>; rel="canonical"' + return response + except KeyError: + pass + + # slug is a hash + by_hash = {page.url_hash: page for page in all_pages} + try: + timestamp = by_hash[slug].timestamp + return redirect(f'/archive/{timestamp}/{archivefile}') + except KeyError: + pass + + # slug is a URL + by_url = {page.base_url: page for page in all_pages} + try: + # TODO: add multiple snapshot support by showing index of all snapshots + # for given url instead of redirecting to timestamp index + timestamp = by_url[base_url(path)].timestamp + return redirect(f'/archive/{timestamp}/index.html') + except KeyError: + pass + + return HttpResponse( + 'No archived link matches the given timestamp or hash.', + content_type="text/plain", + status=404, + ) + +class PublicArchiveView(ListView): + template = 'snapshot_list.html' + model = Snapshot + paginate_by = 100 + ordering = ['title'] + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + query = self.request.GET.get('q') + if query: + qs = qs.filter(title__icontains=query) + for snapshot in qs: + snapshot.icons = snapshot_icons(snapshot) + return qs + + def get(self, *args, **kwargs): + if PUBLIC_INDEX or self.request.user.is_authenticated: + response = super().get(*args, **kwargs) + return response + else: + return redirect(f'/admin/login/?next={self.request.path}') + + +class AddView(UserPassesTestMixin, FormView): + template_name = "add_links.html" + form_class = AddLinkForm + + def get_initial(self): + """Prefill the AddLinkForm with the 'url' GET parameter""" + if self.request.method == 'GET': + url = self.request.GET.get('url', None) + if url: + return {'url': url} + else: + return super().get_initial() + + def test_func(self): + return PUBLIC_ADD_VIEW or self.request.user.is_authenticated + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(**kwargs), + 'title': "Add URLs", + # We can't just call request.build_absolute_uri in the template, because it would include query parameters + 'absolute_add_path': self.request.build_absolute_uri(self.request.path), + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + def form_valid(self, form): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + extractors = ','.join(form.cleaned_data["archive_methods"]) + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + if extractors: + input_kwargs.update({"extractors": extractors}) + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context = self.get_context_data() + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + return render(template_name=self.template_name, request=self.request, context=context) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/welcome_message.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/welcome_message.py new file mode 100644 index 0000000..ed5d2d7 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/welcome_message.py @@ -0,0 +1,5 @@ +from archivebox.logging_util import log_shell_welcome_msg + + +if __name__ == '__main__': + log_shell_welcome_msg() diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/wsgi.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/wsgi.py new file mode 100644 index 0000000..f933afa --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for archivebox project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.settings') + +application = get_wsgi_application() diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/__init__.py new file mode 100644 index 0000000..a4acef0 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/__init__.py @@ -0,0 +1,182 @@ +__package__ = 'archivebox.extractors' + +import os +from pathlib import Path + +from typing import Optional, List, Iterable, Union +from datetime import datetime +from django.db.models import QuerySet + +from ..index.schema import Link +from ..index.sql import write_link_to_sql_index +from ..index import ( + load_link_details, + write_link_details, +) +from ..util import enforce_types +from ..logging_util import ( + log_archiving_started, + log_archiving_paused, + log_archiving_finished, + log_link_archiving_started, + log_link_archiving_finished, + log_archive_method_started, + log_archive_method_finished, +) +from ..search import write_search_index + +from .title import should_save_title, save_title +from .favicon import should_save_favicon, save_favicon +from .wget import should_save_wget, save_wget +from .singlefile import should_save_singlefile, save_singlefile +from .readability import should_save_readability, save_readability +from .mercury import should_save_mercury, save_mercury +from .pdf import should_save_pdf, save_pdf +from .screenshot import should_save_screenshot, save_screenshot +from .dom import should_save_dom, save_dom +from .git import should_save_git, save_git +from .media import should_save_media, save_media +from .archive_org import should_save_archive_dot_org, save_archive_dot_org +from .headers import should_save_headers, save_headers + + +def get_default_archive_methods(): + return [ + ('title', should_save_title, save_title), + ('favicon', should_save_favicon, save_favicon), + ('wget', should_save_wget, save_wget), + ('singlefile', should_save_singlefile, save_singlefile), + ('pdf', should_save_pdf, save_pdf), + ('screenshot', should_save_screenshot, save_screenshot), + ('dom', should_save_dom, save_dom), + ('readability', should_save_readability, save_readability), #keep readability below wget and singlefile, as it depends on them + ('mercury', should_save_mercury, save_mercury), + ('git', should_save_git, save_git), + ('media', should_save_media, save_media), + ('headers', should_save_headers, save_headers), + ('archive_org', should_save_archive_dot_org, save_archive_dot_org), + ] + +ARCHIVE_METHODS_INDEXING_PRECEDENCE = [('readability', 1), ('singlefile', 2), ('dom', 3), ('wget', 4)] + +@enforce_types +def ignore_methods(to_ignore: List[str]): + ARCHIVE_METHODS = get_default_archive_methods() + methods = filter(lambda x: x[0] not in to_ignore, ARCHIVE_METHODS) + methods = map(lambda x: x[0], methods) + return list(methods) + +@enforce_types +def archive_link(link: Link, overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> Link: + """download the DOM, PDF, and a screenshot into a folder named after the link's timestamp""" + + # TODO: Remove when the input is changed to be a snapshot. Suboptimal approach. + from core.models import Snapshot, ArchiveResult + try: + snapshot = Snapshot.objects.get(url=link.url) # TODO: This will be unnecessary once everything is a snapshot + except Snapshot.DoesNotExist: + snapshot = write_link_to_sql_index(link) + + ARCHIVE_METHODS = get_default_archive_methods() + + if methods: + ARCHIVE_METHODS = [ + method for method in ARCHIVE_METHODS + if method[0] in methods + ] + + out_dir = out_dir or Path(link.link_dir) + try: + is_new = not Path(out_dir).exists() + if is_new: + os.makedirs(out_dir) + + link = load_link_details(link, out_dir=out_dir) + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + log_link_archiving_started(link, out_dir, is_new) + link = link.overwrite(updated=datetime.now()) + stats = {'skipped': 0, 'succeeded': 0, 'failed': 0} + + for method_name, should_run, method_function in ARCHIVE_METHODS: + try: + if method_name not in link.history: + link.history[method_name] = [] + + if should_run(link, out_dir) or overwrite: + log_archive_method_started(method_name) + + result = method_function(link=link, out_dir=out_dir) + + link.history[method_name].append(result) + + stats[result.status] += 1 + log_archive_method_finished(result) + write_search_index(link=link, texts=result.index_texts) + ArchiveResult.objects.create(snapshot=snapshot, extractor=method_name, cmd=result.cmd, cmd_version=result.cmd_version, + output=result.output, pwd=result.pwd, start_ts=result.start_ts, end_ts=result.end_ts, status=result.status) + + else: + # print('{black} X {}{reset}'.format(method_name, **ANSI)) + stats['skipped'] += 1 + except Exception as e: + raise Exception('Exception in archive_methods.save_{}(Link(url={}))'.format( + method_name, + link.url, + )) from e + + # print(' ', stats) + + try: + latest_title = link.history['title'][-1].output.strip() + if latest_title and len(latest_title) >= len(link.title or ''): + link = link.overwrite(title=latest_title) + except Exception: + pass + + write_link_details(link, out_dir=out_dir, skip_sql_index=False) + + log_link_archiving_finished(link, link.link_dir, is_new, stats) + + except KeyboardInterrupt: + try: + write_link_details(link, out_dir=link.link_dir) + except: + pass + raise + + except Exception as err: + print(' ! Failed to archive link: {}: {}'.format(err.__class__.__name__, err)) + raise + + return link + +@enforce_types +def archive_links(all_links: Union[Iterable[Link], QuerySet], overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[Path]=None) -> List[Link]: + + if type(all_links) is QuerySet: + num_links: int = all_links.count() + get_link = lambda x: x.as_link() + all_links = all_links.iterator() + else: + num_links: int = len(all_links) + get_link = lambda x: x + + if num_links == 0: + return [] + + log_archiving_started(num_links) + idx: int = 0 + try: + for link in all_links: + idx += 1 + to_archive = get_link(link) + archive_link(to_archive, overwrite=overwrite, methods=methods, out_dir=Path(link.link_dir)) + except KeyboardInterrupt: + log_archiving_paused(num_links, idx, link.timestamp) + raise SystemExit(0) + except BaseException: + print() + raise + + log_archiving_finished(num_links) + return all_links diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/archive_org.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/archive_org.py new file mode 100644 index 0000000..f5598d6 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/archive_org.py @@ -0,0 +1,112 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional, List, Dict, Tuple +from collections import defaultdict + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + TIMEOUT, + CURL_ARGS, + CHECK_SSL_VALIDITY, + SAVE_ARCHIVE_DOT_ORG, + CURL_BINARY, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_archive_dot_org(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "archive.org.txt").exists(): + # if open(path, 'r').read().strip() != 'None': + return False + + return SAVE_ARCHIVE_DOT_ORG + +@enforce_types +def save_archive_dot_org(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """submit site to archive.org for archiving via their service, save returned archive url""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'archive.org.txt' + archive_org_url = None + submit_url = 'https://web.archive.org/save/{}'.format(link.url) + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + submit_url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + content_location, errors = parse_archive_dot_org_response(result.stdout) + if content_location: + archive_org_url = content_location[0] + elif len(errors) == 1 and 'RobotAccessControlException' in errors[0]: + archive_org_url = None + # raise ArchiveError('Archive.org denied by {}/robots.txt'.format(domain(link.url))) + elif errors: + raise ArchiveError(', '.join(errors)) + else: + raise ArchiveError('Failed to find "content-location" URL header in Archive.org response.') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + if output and not isinstance(output, Exception): + # instead of writing None when archive.org rejects the url write the + # url to resubmit it to archive.org. This is so when the user visits + # the URL in person, it will attempt to re-archive it, and it'll show the + # nicer error message explaining why the url was rejected if it fails. + archive_org_url = archive_org_url or submit_url + with open(str(out_dir / output), 'w', encoding='utf-8') as f: + f.write(archive_org_url) + chmod_file('archive.org.txt', cwd=str(out_dir)) + output = archive_org_url + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) + +@enforce_types +def parse_archive_dot_org_response(response: bytes) -> Tuple[List[str], List[str]]: + # Parse archive.org response headers + headers: Dict[str, List[str]] = defaultdict(list) + + # lowercase all the header names and store in dict + for header in response.splitlines(): + if b':' not in header or not header.strip(): + continue + name, val = header.decode().split(':', 1) + headers[name.lower().strip()].append(val.strip()) + + # Get successful archive url in "content-location" header or any errors + content_location = headers.get('content-location', headers['location']) + errors = headers['x-archive-wayback-runtime-error'] + return content_location, errors + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/dom.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/dom.py new file mode 100644 index 0000000..babbe71 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/dom.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file, atomic_write +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_DOM, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_dom(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / 'output.html').exists(): + return False + + return SAVE_DOM + +@enforce_types +def save_dom(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print HTML of site to file using chrome --dump-html""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.html' + output_path = out_dir / output + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--dump-dom', + link.url + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + atomic_write(output_path, result.stdout) + + if result.returncode: + hints = result.stderr.decode() + raise ArchiveError('Failed to save DOM', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/favicon.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/favicon.py new file mode 100644 index 0000000..5e7c1fb --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/favicon.py @@ -0,0 +1,64 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import chmod_file, run +from ..util import enforce_types, domain +from ..config import ( + TIMEOUT, + SAVE_FAVICON, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CHECK_SSL_VALIDITY, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_favicon(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if (Path(out_dir) / 'favicon.ico').exists(): + return False + + return SAVE_FAVICON + +@enforce_types +def save_favicon(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download site favicon from google's favicon api""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'favicon.ico' + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + '--output', str(output), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + 'https://www.google.com/s2/favicons?domain={}'.format(domain(link.url)), + ] + status = 'pending' + timer = TimedProgress(timeout, prefix=' ') + try: + run(cmd, cwd=str(out_dir), timeout=timeout) + chmod_file(output, cwd=str(out_dir)) + status = 'succeeded' + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/git.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/git.py new file mode 100644 index 0000000..fd20d4b --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/git.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + domain, + extension, + without_query, + without_fragment, +) +from ..config import ( + TIMEOUT, + SAVE_GIT, + GIT_BINARY, + GIT_ARGS, + GIT_VERSION, + GIT_DOMAINS, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_git(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if (out_dir / "git").exists(): + return False + + is_clonable_url = ( + (domain(link.url) in GIT_DOMAINS) + or (extension(link.url) == 'git') + ) + if not is_clonable_url: + return False + + return SAVE_GIT + + +@enforce_types +def save_git(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using git""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'git' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + GIT_BINARY, + 'clone', + *GIT_ARGS, + *([] if CHECK_SSL_VALIDITY else ['-c', 'http.sslVerify=false']), + without_query(without_fragment(link.url)), + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + if result.returncode == 128: + # ignore failed re-download when the folder already exists + pass + elif result.returncode > 0: + hints = 'Got git response code: {}.'.format(result.returncode) + raise ArchiveError('Failed to save git clone', hints) + + chmod_file(output, cwd=str(out_dir)) + + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=GIT_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/headers.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/headers.py new file mode 100644 index 0000000..4e69dec --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/headers.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import atomic_write +from ..util import ( + enforce_types, + get_headers, +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + CURL_ARGS, + CURL_USER_AGENT, + CURL_VERSION, + CHECK_SSL_VALIDITY, + SAVE_HEADERS +) +from ..logging_util import TimedProgress + +@enforce_types +def should_save_headers(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + + output = Path(out_dir or link.link_dir) / 'headers.json' + return not output.exists() and SAVE_HEADERS + + +@enforce_types +def save_headers(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """Download site headers""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() + output: ArchiveOutput = 'headers.json' + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--head', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + try: + json_headers = get_headers(link.url, timeout=timeout) + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "headers.json"), json_headers) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/media.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/media.py new file mode 100644 index 0000000..3792fd2 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/media.py @@ -0,0 +1,81 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + MEDIA_TIMEOUT, + SAVE_MEDIA, + YOUTUBEDL_ARGS, + YOUTUBEDL_BINARY, + YOUTUBEDL_VERSION, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_media(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or link.link_dir + + if is_static_file(link.url): + return False + + if (out_dir / "media").exists(): + return False + + return SAVE_MEDIA + +@enforce_types +def save_media(link: Link, out_dir: Optional[Path]=None, timeout: int=MEDIA_TIMEOUT) -> ArchiveResult: + """Download playlists or individual video, audio, and subtitles using youtube-dl""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'media' + output_path = out_dir / output + output_path.mkdir(exist_ok=True) + cmd = [ + YOUTUBEDL_BINARY, + *YOUTUBEDL_ARGS, + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(output_path), timeout=timeout + 1) + chmod_file(output, cwd=str(out_dir)) + if result.returncode: + if (b'ERROR: Unsupported URL' in result.stderr + or b'HTTP Error 404' in result.stderr + or b'HTTP Error 403' in result.stderr + or b'URL could be a direct video link' in result.stderr + or b'Unable to extract container ID' in result.stderr): + # These happen too frequently on non-media pages to warrant printing to console + pass + else: + hints = ( + 'Got youtube-dl response code: {}.'.format(result.returncode), + *result.stderr.decode().split('\n'), + ) + raise ArchiveError('Failed to save media', hints) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=YOUTUBEDL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/mercury.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/mercury.py new file mode 100644 index 0000000..741c329 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/mercury.py @@ -0,0 +1,104 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from subprocess import CompletedProcess +from typing import Optional, List +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + is_static_file, + +) +from ..config import ( + TIMEOUT, + SAVE_MERCURY, + DEPENDENCIES, + MERCURY_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def ShellError(cmd: List[str], result: CompletedProcess, lines: int=20) -> ArchiveError: + # parse out last line of stderr + return ArchiveError( + f'Got {cmd[0]} response code: {result.returncode}).', + *( + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', lines)[-lines:] + if line.strip() + ), + ) + + +@enforce_types +def should_save_mercury(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'mercury' + return SAVE_MERCURY and MERCURY_VERSION and (not output.exists()) + + +@enforce_types +def save_mercury(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @postlight/mercury-parser""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "mercury" + output = str(output_folder) + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + # Get plain text version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url, + "--format=text" + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_text = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + # Get HTML version of article + cmd = [ + DEPENDENCIES['MERCURY_BINARY']['path'], + link.url + ] + result = run(cmd, cwd=out_dir, timeout=timeout) + try: + article_json = json.loads(result.stdout) + except json.JSONDecodeError: + raise ShellError(cmd, result) + + output_folder.mkdir(exist_ok=True) + atomic_write(str(output_folder / "content.html"), article_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), article_text["content"]) + atomic_write(str(output_folder / "article.json"), article_json) + + # Check for common failure cases + if (result.returncode > 0): + raise ShellError(cmd, result) + except (ArchiveError, Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=MERCURY_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/pdf.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/pdf.py new file mode 100644 index 0000000..1b0201e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/pdf.py @@ -0,0 +1,68 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_PDF, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_pdf(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "output.pdf").exists(): + return False + + return SAVE_PDF + + +@enforce_types +def save_pdf(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print PDF of site to file using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'output.pdf' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--print-to-pdf', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save PDF', hints) + + chmod_file('output.pdf', cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/readability.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/readability.py new file mode 100644 index 0000000..9da620b --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/readability.py @@ -0,0 +1,124 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from tempfile import NamedTemporaryFile + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, atomic_write +from ..util import ( + enforce_types, + download_url, + is_static_file, + +) +from ..config import ( + TIMEOUT, + CURL_BINARY, + SAVE_READABILITY, + DEPENDENCIES, + READABILITY_VERSION, +) +from ..logging_util import TimedProgress + +@enforce_types +def get_html(link: Link, path: Path) -> str: + """ + Try to find wget, singlefile and then dom files. + If none is found, download the url again. + """ + canonical = link.canonical_outputs() + abs_path = path.absolute() + sources = [canonical["singlefile_path"], canonical["wget_path"], canonical["dom_path"]] + document = None + for source in sources: + try: + with open(abs_path / source, "r") as f: + document = f.read() + break + except (FileNotFoundError, TypeError): + continue + if document is None: + return download_url(link.url) + else: + return document + +@enforce_types +def should_save_readability(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + output = Path(out_dir or link.link_dir) / 'readability' + return SAVE_READABILITY and READABILITY_VERSION and (not output.exists()) + + +@enforce_types +def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download reader friendly version using @mozilla/readability""" + + out_dir = Path(out_dir or link.link_dir) + output_folder = out_dir.absolute() / "readability" + output = str(output_folder) + + # Readability Docs: https://github.com/mozilla/readability + + status = 'succeeded' + # fake command to show the user so they have something to try debugging if get_html fails + cmd = [ + CURL_BINARY, + link.url + ] + readability_content = None + timer = TimedProgress(timeout, prefix=' ') + try: + document = get_html(link, out_dir) + temp_doc = NamedTemporaryFile(delete=False) + temp_doc.write(document.encode("utf-8")) + temp_doc.close() + + cmd = [ + DEPENDENCIES['READABILITY_BINARY']['path'], + temp_doc.name + ] + + result = run(cmd, cwd=out_dir, timeout=timeout) + result_json = json.loads(result.stdout) + output_folder.mkdir(exist_ok=True) + readability_content = result_json.pop("textContent") + atomic_write(str(output_folder / "content.html"), result_json.pop("content")) + atomic_write(str(output_folder / "content.txt"), readability_content) + atomic_write(str(output_folder / "article.json"), result_json) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got readability response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('Readability was not able to archive the page', hints) + except (Exception, OSError) as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=READABILITY_VERSION, + output=output, + status=status, + index_texts= [readability_content] if readability_content else [], + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/screenshot.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/screenshot.py new file mode 100644 index 0000000..325584e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/screenshot.py @@ -0,0 +1,67 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SCREENSHOT, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_screenshot(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + if (out_dir / "screenshot.png").exists(): + return False + + return SAVE_SCREENSHOT + +@enforce_types +def save_screenshot(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """take screenshot of site using chrome --headless""" + + out_dir = out_dir or Path(link.link_dir) + output: ArchiveOutput = 'screenshot.png' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--screenshot', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save screenshot', hints) + + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/singlefile.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/singlefile.py new file mode 100644 index 0000000..2e5c389 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/singlefile.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.extractors' + +from pathlib import Path + +from typing import Optional +import json + +from ..index.schema import Link, ArchiveResult, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SINGLEFILE, + DEPENDENCIES, + SINGLEFILE_VERSION, + CHROME_BINARY, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_singlefile(link: Link, out_dir: Optional[Path]=None) -> bool: + out_dir = out_dir or Path(link.link_dir) + if is_static_file(link.url): + return False + + output = out_dir / 'singlefile.html' + return SAVE_SINGLEFILE and SINGLEFILE_VERSION and (not output.exists()) + + +@enforce_types +def save_singlefile(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using single-file""" + + out_dir = out_dir or Path(link.link_dir) + output = str(out_dir.absolute() / "singlefile.html") + + browser_args = chrome_args(TIMEOUT=0) + + # SingleFile CLI Docs: https://github.com/gildas-lormeau/SingleFile/tree/master/cli + browser_args = '--browser-args={}'.format(json.dumps(browser_args[1:])) + cmd = [ + DEPENDENCIES['SINGLEFILE_BINARY']['path'], + '--browser-executable-path={}'.format(CHROME_BINARY), + browser_args, + link.url, + output + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + hints = ( + 'Got single-file response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0): + raise ArchiveError('SingleFile was not able to archive the page', hints) + chmod_file(output) + except (Exception, OSError) as err: + status = 'failed' + # TODO: Make this prettier. This is necessary to run the command (escape JSON internal quotes). + cmd[2] = browser_args.replace('"', "\\\"") + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=SINGLEFILE_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/title.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/title.py new file mode 100644 index 0000000..28cb128 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/title.py @@ -0,0 +1,130 @@ +__package__ = 'archivebox.extractors' + +import re +from html.parser import HTMLParser +from pathlib import Path +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..util import ( + enforce_types, + is_static_file, + download_url, + htmldecode, +) +from ..config import ( + TIMEOUT, + CHECK_SSL_VALIDITY, + SAVE_TITLE, + CURL_BINARY, + CURL_ARGS, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +HTML_TITLE_REGEX = re.compile( + r'' # start matching text after tag + r'(.[^<>]+)', # get everything up to these symbols + re.IGNORECASE | re.MULTILINE | re.DOTALL | re.UNICODE, +) + + +class TitleParser(HTMLParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.title_tag = "" + self.title_og = "" + self.inside_title_tag = False + + @property + def title(self): + return self.title_tag or self.title_og or None + + def handle_starttag(self, tag, attrs): + if tag.lower() == "title" and not self.title_tag: + self.inside_title_tag = True + elif tag.lower() == "meta" and not self.title_og: + attrs = dict(attrs) + if attrs.get("property") == "og:title" and attrs.get("content"): + self.title_og = attrs.get("content") + + def handle_data(self, data): + if self.inside_title_tag and data: + self.title_tag += data.strip() + + def handle_endtag(self, tag): + if tag.lower() == "title": + self.inside_title_tag = False + + +@enforce_types +def should_save_title(link: Link, out_dir: Optional[str]=None) -> bool: + # if link already has valid title, skip it + if link.title and not link.title.lower().startswith('http'): + return False + + if is_static_file(link.url): + return False + + return SAVE_TITLE + +def extract_title_with_regex(html): + match = re.search(HTML_TITLE_REGEX, html) + output = htmldecode(match.group(1).strip()) if match else None + return output + +@enforce_types +def save_title(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """try to guess the page's title from its content""" + + from core.models import Snapshot + + output: ArchiveOutput = None + cmd = [ + CURL_BINARY, + *CURL_ARGS, + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + html = download_url(link.url, timeout=timeout) + try: + # try using relatively strict html parser first + parser = TitleParser() + parser.feed(html) + output = parser.title + if output is None: + raise + except Exception: + # fallback to regex that can handle broken/malformed html + output = extract_title_with_regex(html) + + # if title is better than the one in the db, update db with new title + if isinstance(output, str) and output: + if not link.title or len(output) >= len(link.title): + Snapshot.objects.filter(url=link.url, + timestamp=link.timestamp)\ + .update(title=output) + else: + raise ArchiveError('Unable to detect page title') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/wget.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/wget.py new file mode 100644 index 0000000..331f636 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/extractors/wget.py @@ -0,0 +1,184 @@ +__package__ = 'archivebox.extractors' + +import re +from pathlib import Path + +from typing import Optional +from datetime import datetime + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + without_scheme, + without_fragment, + without_query, + path, + domain, + urldecode, +) +from ..config import ( + WGET_ARGS, + TIMEOUT, + SAVE_WGET, + SAVE_WARC, + WGET_BINARY, + WGET_VERSION, + RESTRICT_FILE_NAMES, + CHECK_SSL_VALIDITY, + SAVE_WGET_REQUISITES, + WGET_AUTO_COMPRESSION, + WGET_USER_AGENT, + COOKIES_FILE, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_wget(link: Link, out_dir: Optional[Path]=None) -> bool: + output_path = wget_output_path(link) + out_dir = out_dir or Path(link.link_dir) + if output_path and (out_dir / output_path).exists(): + return False + + return SAVE_WGET + + +@enforce_types +def save_wget(link: Link, out_dir: Optional[Path]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using wget""" + + out_dir = out_dir or link.link_dir + if SAVE_WARC: + warc_dir = out_dir / "warc" + warc_dir.mkdir(exist_ok=True) + warc_path = warc_dir / str(int(datetime.now().timestamp())) + + # WGET CLI Docs: https://www.gnu.org/software/wget/manual/wget.html + output: ArchiveOutput = None + cmd = [ + WGET_BINARY, + # '--server-response', # print headers for better error parsing + *WGET_ARGS, + '--timeout={}'.format(timeout), + *(['--restrict-file-names={}'.format(RESTRICT_FILE_NAMES)] if RESTRICT_FILE_NAMES else []), + *(['--warc-file={}'.format(str(warc_path))] if SAVE_WARC else []), + *(['--page-requisites'] if SAVE_WGET_REQUISITES else []), + *(['--user-agent={}'.format(WGET_USER_AGENT)] if WGET_USER_AGENT else []), + *(['--load-cookies', COOKIES_FILE] if COOKIES_FILE else []), + *(['--compression=auto'] if WGET_AUTO_COMPRESSION else []), + *([] if SAVE_WARC else ['--timestamping']), + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate', '--no-hsts']), + link.url, + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=str(out_dir), timeout=timeout) + output = wget_output_path(link) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + files_downloaded = ( + int(output_tail[-1].strip().split(' ', 2)[1] or 0) + if 'Downloaded:' in output_tail[-1] + else 0 + ) + hints = ( + 'Got wget response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0 and files_downloaded < 1) or output is None: + if b'403: Forbidden' in result.stderr: + raise ArchiveError('403 Forbidden (try changing WGET_USER_AGENT)', hints) + if b'404: Not Found' in result.stderr: + raise ArchiveError('404 Not Found', hints) + if b'ERROR 500: Internal Server Error' in result.stderr: + raise ArchiveError('500 Internal Server Error', hints) + raise ArchiveError('Wget failed or got an error from the server', hints) + chmod_file(output, cwd=str(out_dir)) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=str(out_dir), + cmd_version=WGET_VERSION, + output=output, + status=status, + **timer.stats, + ) + + +@enforce_types +def wget_output_path(link: Link) -> Optional[str]: + """calculate the path to the wgetted .html file, since wget may + adjust some paths to be different than the base_url path. + + See docs on wget --adjust-extension (-E) + """ + if is_static_file(link.url): + return without_scheme(without_fragment(link.url)) + + # Wget downloads can save in a number of different ways depending on the url: + # https://example.com + # > example.com/index.html + # https://example.com?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + # https://www.example.com/?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc + # > example.com/abc.html + # https://example.com/abc/ + # > example.com/abc/index.html + # https://example.com/abc?v=zzVa_tX1OiI.html + # > example.com/abc?v=zzVa_tX1OiI.html + # https://example.com/abc/?v=zzVa_tX1OiI.html + # > example.com/abc/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc/test.html + # > example.com/abc/test.html + # https://example.com/abc/test?v=zzVa_tX1OiI + # > example.com/abc/test?v=zzVa_tX1OiI.html + # https://example.com/abc/test/?v=zzVa_tX1OiI + # > example.com/abc/test/index.html?v=zzVa_tX1OiI.html + + # There's also lots of complexity around how the urlencoding and renaming + # is done for pages with query and hash fragments or extensions like shtml / htm / php / etc + + # Since the wget algorithm for -E (appending .html) is incredibly complex + # and there's no way to get the computed output path from wget + # in order to avoid having to reverse-engineer how they calculate it, + # we just look in the output folder read the filename wget used from the filesystem + full_path = without_fragment(without_query(path(link.url))).strip('/') + search_dir = Path(link.link_dir) / domain(link.url).replace(":", "+") / urldecode(full_path) + for _ in range(4): + if search_dir.exists(): + if search_dir.is_dir(): + html_files = [ + f for f in search_dir.iterdir() + if re.search(".+\\.[Ss]?[Hh][Tt][Mm][Ll]?$", str(f), re.I | re.M) + ] + if html_files: + return str(html_files[0].relative_to(link.link_dir)) + + # Move up one directory level + search_dir = search_dir.parent + + if str(search_dir) == link.link_dir: + break + + return None diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/__init__.py new file mode 100644 index 0000000..8eab1d3 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/__init__.py @@ -0,0 +1,617 @@ +__package__ = 'archivebox.index' + +import os +import shutil +import json as pyjson +from pathlib import Path + +from itertools import chain +from typing import List, Tuple, Dict, Optional, Iterable +from collections import OrderedDict +from contextlib import contextmanager +from urllib.parse import urlparse +from django.db.models import QuerySet, Q + +from ..util import ( + scheme, + enforce_types, + ExtendedEncoder, +) +from ..config import ( + ARCHIVE_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + OUTPUT_DIR, + TIMEOUT, + URL_BLACKLIST_PTN, + stderr, + OUTPUT_PERMISSIONS +) +from ..logging_util import ( + TimedProgress, + log_indexing_process_started, + log_indexing_process_finished, + log_indexing_started, + log_indexing_finished, + log_parsing_finished, + log_deduping_finished, +) + +from .schema import Link, ArchiveResult +from .html import ( + write_html_link_details, +) +from .json import ( + parse_json_link_details, + write_json_link_details, +) +from .sql import ( + write_sql_main_index, + write_sql_link_details, +) + +from ..search import search_backend_enabled, query_search_index + +### Link filtering and checking + +@enforce_types +def merge_links(a: Link, b: Link) -> Link: + """deterministially merge two links, favoring longer field values over shorter, + and "cleaner" values over worse ones. + """ + assert a.base_url == b.base_url, f'Cannot merge two links with different URLs ({a.base_url} != {b.base_url})' + + # longest url wins (because a fuzzy url will always be shorter) + url = a.url if len(a.url) > len(b.url) else b.url + + # best title based on length and quality + possible_titles = [ + title + for title in (a.title, b.title) + if title and title.strip() and '://' not in title + ] + title = None + if len(possible_titles) == 2: + title = max(possible_titles, key=lambda t: len(t)) + elif len(possible_titles) == 1: + title = possible_titles[0] + + # earliest valid timestamp + timestamp = ( + a.timestamp + if float(a.timestamp or 0) < float(b.timestamp or 0) else + b.timestamp + ) + + # all unique, truthy tags + tags_set = ( + set(tag.strip() for tag in (a.tags or '').split(',')) + | set(tag.strip() for tag in (b.tags or '').split(',')) + ) + tags = ','.join(tags_set) or None + + # all unique source entries + sources = list(set(a.sources + b.sources)) + + # all unique history entries for the combined archive methods + all_methods = set(list(a.history.keys()) + list(a.history.keys())) + history = { + method: (a.history.get(method) or []) + (b.history.get(method) or []) + for method in all_methods + } + for method in all_methods: + deduped_jsons = { + pyjson.dumps(result, sort_keys=True, cls=ExtendedEncoder) + for result in history[method] + } + history[method] = list(reversed(sorted( + (ArchiveResult.from_json(pyjson.loads(result)) for result in deduped_jsons), + key=lambda result: result.start_ts, + ))) + + return Link( + url=url, + timestamp=timestamp, + title=title, + tags=tags, + sources=sources, + history=history, + ) + + +@enforce_types +def validate_links(links: Iterable[Link]) -> List[Link]: + timer = TimedProgress(TIMEOUT * 4) + try: + links = archivable_links(links) # remove chrome://, about:, mailto: etc. + links = sorted_links(links) # deterministically sort the links based on timestamp, url + links = fix_duplicate_links(links) # merge/dedupe duplicate timestamps & urls + finally: + timer.end() + + return list(links) + +@enforce_types +def archivable_links(links: Iterable[Link]) -> Iterable[Link]: + """remove chrome://, about:// or other schemed links that cant be archived""" + for link in links: + try: + urlparse(link.url) + except ValueError: + continue + if scheme(link.url) not in ('http', 'https', 'ftp'): + continue + if URL_BLACKLIST_PTN and URL_BLACKLIST_PTN.search(link.url): + continue + + yield link + + +@enforce_types +def fix_duplicate_links(sorted_links: Iterable[Link]) -> Iterable[Link]: + """ + ensures that all non-duplicate links have monotonically increasing timestamps + """ + # from core.models import Snapshot + + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in sorted_links: + if link.url in unique_urls: + # merge with any other links that share the same url + link = merge_links(unique_urls[link.url], link) + unique_urls[link.url] = link + + return unique_urls.values() + + +@enforce_types +def sorted_links(links: Iterable[Link]) -> Iterable[Link]: + sort_func = lambda link: (link.timestamp.split('.', 1)[0], link.url) + return sorted(links, key=sort_func, reverse=True) + + +@enforce_types +def links_after_timestamp(links: Iterable[Link], resume: Optional[float]=None) -> Iterable[Link]: + if not resume: + yield from links + return + + for link in links: + try: + if float(link.timestamp) <= resume: + yield link + except (ValueError, TypeError): + print('Resume value and all timestamp values must be valid numbers.') + + +@enforce_types +def lowest_uniq_timestamp(used_timestamps: OrderedDict, timestamp: str) -> str: + """resolve duplicate timestamps by appending a decimal 1234, 1234 -> 1234.1, 1234.2""" + + timestamp = timestamp.split('.')[0] + nonce = 0 + + # first try 152323423 before 152323423.0 + if timestamp not in used_timestamps: + return timestamp + + new_timestamp = '{}.{}'.format(timestamp, nonce) + while new_timestamp in used_timestamps: + nonce += 1 + new_timestamp = '{}.{}'.format(timestamp, nonce) + + return new_timestamp + + + +### Main Links Index + +@contextmanager +@enforce_types +def timed_index_update(out_path: Path): + log_indexing_started(out_path) + timer = TimedProgress(TIMEOUT * 2, prefix=' ') + try: + yield + finally: + timer.end() + + assert out_path.exists(), f'Failed to write index file: {out_path}' + log_indexing_finished(out_path) + + +@enforce_types +def write_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + """Writes links to sqlite3 file for a given list of links""" + + log_indexing_process_started(len(links)) + + try: + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + + except (KeyboardInterrupt, SystemExit): + stderr('[!] Warning: Still writing index to disk...', color='lightyellow') + stderr(' Run archivebox init to fix any inconsistencies from an ungraceful exit.') + with timed_index_update(out_dir / SQL_INDEX_FILENAME): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(out_dir / SQL_INDEX_FILENAME, int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + raise SystemExit(0) + + log_indexing_process_finished() + +@enforce_types +def load_main_index(out_dir: Path=OUTPUT_DIR, warn: bool=True) -> List[Link]: + """parse and load existing index with any new links from import_path merged in""" + from core.models import Snapshot + try: + return Snapshot.objects.all() + + except (KeyboardInterrupt, SystemExit): + raise SystemExit(0) + +@enforce_types +def load_main_index_meta(out_dir: Path=OUTPUT_DIR) -> Optional[dict]: + index_path = out_dir / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + meta_dict = pyjson.load(f) + meta_dict.pop('links') + return meta_dict + + return None + + +@enforce_types +def parse_links_from_source(source_path: str, root_url: Optional[str]=None) -> Tuple[List[Link], List[Link]]: + + from ..parsers import parse_links + + new_links: List[Link] = [] + + # parse and validate the import file + raw_links, parser_name = parse_links(source_path, root_url=root_url) + new_links = validate_links(raw_links) + + if parser_name: + num_parsed = len(raw_links) + log_parsing_finished(num_parsed, parser_name) + + return new_links + +@enforce_types +def fix_duplicate_links_in_index(snapshots: QuerySet, links: Iterable[Link]) -> Iterable[Link]: + """ + Given a list of in-memory Links, dedupe and merge them with any conflicting Snapshots in the DB. + """ + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in links: + index_link = snapshots.filter(url=link.url) + if index_link: + link = merge_links(index_link[0].as_link(), link) + + unique_urls[link.url] = link + + return unique_urls.values() + +@enforce_types +def dedupe_links(snapshots: QuerySet, + new_links: List[Link]) -> List[Link]: + """ + The validation of links happened at a different stage. This method will + focus on actual deduplication and timestamp fixing. + """ + + # merge existing links in out_dir and new links + dedup_links = fix_duplicate_links_in_index(snapshots, new_links) + + new_links = [ + link for link in new_links + if not snapshots.filter(url=link.url).exists() + ] + + dedup_links_dict = {link.url: link for link in dedup_links} + + # Replace links in new_links with the dedup version + for i in range(len(new_links)): + if new_links[i].url in dedup_links_dict.keys(): + new_links[i] = dedup_links_dict[new_links[i].url] + log_deduping_finished(len(new_links)) + + return new_links + +### Link Details Index + +@enforce_types +def write_link_details(link: Link, out_dir: Optional[str]=None, skip_sql_index: bool=False) -> None: + out_dir = out_dir or link.link_dir + + write_json_link_details(link, out_dir=out_dir) + write_html_link_details(link, out_dir=out_dir) + if not skip_sql_index: + write_sql_link_details(link) + + +@enforce_types +def load_link_details(link: Link, out_dir: Optional[str]=None) -> Link: + """check for an existing link archive in the given directory, + and load+merge it into the given link dict + """ + out_dir = out_dir or link.link_dir + + existing_link = parse_json_link_details(out_dir) + if existing_link: + return merge_links(existing_link, link) + + return link + + + +LINK_FILTERS = { + 'exact': lambda pattern: Q(url=pattern), + 'substring': lambda pattern: Q(url__icontains=pattern), + 'regex': lambda pattern: Q(url__iregex=pattern), + 'domain': lambda pattern: Q(url__istartswith=f"http://{pattern}") | Q(url__istartswith=f"https://{pattern}") | Q(url__istartswith=f"ftp://{pattern}"), + 'tag': lambda pattern: Q(tags__name=pattern), +} + +@enforce_types +def q_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + q_filter = Q() + for pattern in filter_patterns: + try: + q_filter = q_filter | LINK_FILTERS[filter_type](pattern) + except KeyError: + stderr() + stderr( + f'[X] Got invalid pattern for --filter-type={filter_type}:', + color='red', + ) + stderr(f' {pattern}') + raise SystemExit(2) + return snapshots.filter(q_filter) + +def search_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='search') -> QuerySet: + if not search_backend_enabled(): + stderr() + stderr( + '[X] The search backend is not enabled, set config.USE_SEARCHING_BACKEND = True', + color='red', + ) + raise SystemExit(2) + from core.models import Snapshot + + qsearch = Snapshot.objects.none() + for pattern in filter_patterns: + try: + qsearch |= query_search_index(pattern) + except: + raise SystemExit(2) + + return snapshots & qsearch + +@enforce_types +def snapshot_filter(snapshots: QuerySet, filter_patterns: List[str], filter_type: str='exact') -> QuerySet: + if filter_type != 'search': + return q_filter(snapshots, filter_patterns, filter_type) + else: + return search_filter(snapshots, filter_patterns, filter_type) + + +def get_indexed_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links without checking archive status or data directory validity""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in links + } + +def get_archived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are archived with a valid data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_archived, links) + } + +def get_unarchived_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are unarchived with no data directory or an empty data directory""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_unarchived, links) + } + +def get_present_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that actually exist in the archive/ folder""" + + all_folders = {} + + for entry in (out_dir / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(entry.path) + except Exception: + pass + + all_folders[entry.name] = link + + return all_folders + +def get_valid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs with a valid index matched to the main index and archived content""" + links = [snapshot.as_link_with_details() for snapshot in snapshots.iterator()] + return { + link.link_dir: link + for link in filter(is_valid, links) + } + +def get_invalid_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that are invalid for any reason: corrupted/duplicate/orphaned/unrecognized""" + duplicate = get_duplicate_folders(snapshots, out_dir=OUTPUT_DIR) + orphaned = get_orphaned_folders(snapshots, out_dir=OUTPUT_DIR) + corrupted = get_corrupted_folders(snapshots, out_dir=OUTPUT_DIR) + unrecognized = get_unrecognized_folders(snapshots, out_dir=OUTPUT_DIR) + return {**duplicate, **orphaned, **corrupted, **unrecognized} + + +def get_duplicate_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that conflict with other directories that have the same link URL or timestamp""" + by_url = {} + by_timestamp = {} + duplicate_folders = {} + + data_folders = ( + str(entry) + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir() + if entry.is_dir() and not snapshots.filter(timestamp=entry.name).exists() + ) + + for path in chain(snapshots.iterator(), data_folders): + link = None + if type(path) is not str: + path = path.as_link().link_dir + + try: + link = parse_json_link_details(path) + except Exception: + pass + + if link: + # link folder has same timestamp as different link folder + by_timestamp[link.timestamp] = by_timestamp.get(link.timestamp, 0) + 1 + if by_timestamp[link.timestamp] > 1: + duplicate_folders[path] = link + + # link folder has same url as different link folder + by_url[link.url] = by_url.get(link.url, 0) + 1 + if by_url[link.url] > 1: + duplicate_folders[path] = link + return duplicate_folders + +def get_orphaned_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that contain a valid index but aren't listed in the main index""" + orphaned_folders = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + link = None + try: + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if link and not snapshots.filter(timestamp=entry.name).exists(): + # folder is a valid link data dir with index details, but it's not in the main index + orphaned_folders[str(entry)] = link + + return orphaned_folders + +def get_corrupted_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain a valid index and aren't listed in the main index""" + corrupted = {} + for snapshot in snapshots.iterator(): + link = snapshot.as_link() + if is_corrupt(link): + corrupted[link.link_dir] = link + return corrupted + +def get_unrecognized_folders(snapshots, out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain recognizable archive data and aren't listed in the main index""" + unrecognized_folders: Dict[str, Optional[Link]] = {} + + for entry in (Path(out_dir) / ARCHIVE_DIR_NAME).iterdir(): + if entry.is_dir(): + index_exists = (entry / "index.json").exists() + link = None + try: + link = parse_json_link_details(str(entry)) + except KeyError: + # Try to fix index + if index_exists: + try: + # Last attempt to repair the detail index + link_guessed = parse_json_link_details(str(entry), guess=True) + write_json_link_details(link_guessed, out_dir=str(entry)) + link = parse_json_link_details(str(entry)) + except Exception: + pass + + if index_exists and link is None: + # index exists but it's corrupted or unparseable + unrecognized_folders[str(entry)] = link + + elif not index_exists: + # link details index doesn't exist and the folder isn't in the main index + timestamp = entry.name + if not snapshots.filter(timestamp=timestamp).exists(): + unrecognized_folders[str(entry)] = link + + return unrecognized_folders + + +def is_valid(link: Link) -> bool: + dir_exists = Path(link.link_dir).exists() + index_exists = (Path(link.link_dir) / "index.json").exists() + if not dir_exists: + # unarchived links are not included in the valid list + return False + if dir_exists and not index_exists: + return False + if dir_exists and index_exists: + try: + parsed_link = parse_json_link_details(link.link_dir, guess=True) + return link.url == parsed_link.url + except Exception: + pass + return False + +def is_corrupt(link: Link) -> bool: + if not Path(link.link_dir).exists(): + # unarchived links are not considered corrupt + return False + + if is_valid(link): + return False + + return True + +def is_archived(link: Link) -> bool: + return is_valid(link) and link.is_archived + +def is_unarchived(link: Link) -> bool: + if not Path(link.link_dir).exists(): + return True + return not link.is_archived + + +def fix_invalid_folder_locations(out_dir: Path=OUTPUT_DIR) -> Tuple[List[str], List[str]]: + fixed = [] + cant_fix = [] + for entry in os.scandir(out_dir / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if not link: + continue + + if not entry.path.endswith(f'/{link.timestamp}'): + dest = out_dir / ARCHIVE_DIR_NAME / link.timestamp + if dest.exists(): + cant_fix.append(entry.path) + else: + shutil.move(entry.path, dest) + fixed.append(dest) + timestamp = entry.path.rsplit('/', 1)[-1] + assert link.link_dir == entry.path + assert link.timestamp == timestamp + write_json_link_details(link, out_dir=entry.path) + + return fixed, cant_fix diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/csv.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/csv.py new file mode 100644 index 0000000..804e646 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/csv.py @@ -0,0 +1,37 @@ +__package__ = 'archivebox.index' + +from typing import List, Optional, Any + +from ..util import enforce_types +from .schema import Link + + +@enforce_types +def links_to_csv(links: List[Link], + cols: Optional[List[str]]=None, + header: bool=True, + separator: str=',', + ljust: int=0) -> str: + + cols = cols or ['timestamp', 'is_archived', 'url'] + + header_str = '' + if header: + header_str = separator.join(col.ljust(ljust) for col in cols) + + row_strs = ( + link.to_csv(cols=cols, ljust=ljust, separator=separator) + for link in links + ) + + return '\n'.join((header_str, *row_strs)) + + +@enforce_types +def to_csv(obj: Any, cols: List[str], separator: str=',', ljust: int=0) -> str: + from .json import to_json + + return separator.join( + to_json(getattr(obj, col), indent=None).ljust(ljust) + for col in cols + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/html.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/html.py new file mode 100644 index 0000000..a62e2c7 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/html.py @@ -0,0 +1,164 @@ +__package__ = 'archivebox.index' + +from datetime import datetime +from typing import List, Optional, Iterator, Mapping +from pathlib import Path + +from django.utils.html import format_html +from collections import defaultdict + +from .schema import Link +from ..system import atomic_write +from ..logging_util import printable_filesize +from ..util import ( + enforce_types, + ts_to_date, + urlencode, + htmlencode, + urldecode, +) +from ..config import ( + OUTPUT_DIR, + VERSION, + GIT_SHA, + FOOTER_INFO, + HTML_INDEX_FILENAME, +) + +MAIN_INDEX_TEMPLATE = 'main_index.html' +MINIMAL_INDEX_TEMPLATE = 'main_index_minimal.html' +LINK_DETAILS_TEMPLATE = 'link_details.html' +TITLE_LOADING_MSG = 'Not yet archived...' + + +### Main Links Index + +@enforce_types +def parse_html_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[str]: + """parse an archive index html file and return the list of urls""" + + index_path = Path(out_dir) / HTML_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + for line in f: + if 'class="link-url"' in line: + yield line.split('"')[1] + return () + +@enforce_types +def generate_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = main_index_template(links) + else: + output = main_index_template(links, template=MINIMAL_INDEX_TEMPLATE) + return output + +@enforce_types +def main_index_template(links: List[Link], template: str=MAIN_INDEX_TEMPLATE) -> str: + """render the template for the entire main index""" + + return render_django_template(template, { + 'version': VERSION, + 'git_sha': GIT_SHA, + 'num_links': str(len(links)), + 'date_updated': datetime.now().strftime('%Y-%m-%d'), + 'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'), + 'links': [link._asdict(extended=True) for link in links], + 'FOOTER_INFO': FOOTER_INFO, + }) + + +### Link Details Index + +@enforce_types +def write_html_link_details(link: Link, out_dir: Optional[str]=None) -> None: + out_dir = out_dir or link.link_dir + + rendered_html = link_details_template(link) + atomic_write(str(Path(out_dir) / HTML_INDEX_FILENAME), rendered_html) + + +@enforce_types +def link_details_template(link: Link) -> str: + + from ..extractors.wget import wget_output_path + + link_info = link._asdict(extended=True) + + return render_django_template(LINK_DETAILS_TEMPLATE, { + **link_info, + **link_info['canonical'], + 'title': htmlencode( + link.title + or (link.base_url if link.is_archived else TITLE_LOADING_MSG) + ), + 'url_str': htmlencode(urldecode(link.base_url)), + 'archive_url': urlencode( + wget_output_path(link) + or (link.domain if link.is_archived else '') + ) or 'about:blank', + 'extension': link.extension or 'html', + 'tags': link.tags or 'untagged', + 'size': printable_filesize(link.archive_size) if link.archive_size else 'pending', + 'status': 'archived' if link.is_archived else 'not yet archived', + 'status_color': 'success' if link.is_archived else 'danger', + 'oldest_archive_date': ts_to_date(link.oldest_archive_date), + }) + +@enforce_types +def render_django_template(template: str, context: Mapping[str, str]) -> str: + """render a given html template string with the given template content""" + from django.template.loader import render_to_string + + return render_to_string(template, context) + + +def snapshot_icons(snapshot) -> str: + from core.models import EXTRACTORS + + archive_results = snapshot.archiveresult_set.filter(status="succeeded") + link = snapshot.as_link() + path = link.archive_path + canon = link.canonical_outputs() + output = "" + output_template = '<a href="/{}/{}" class="exists-{}" title="{}">{} </a>' + icons = { + "singlefile": "❶", + "wget": "🆆", + "dom": "🅷", + "pdf": "📄", + "screenshot": "💻", + "media": "📼", + "git": "🅶", + "archive_org": "🏛", + "readability": "🆁", + "mercury": "🅼", + "warc": "📦" + } + exclude = ["favicon", "title", "headers", "archive_org"] + # Missing specific entry for WARC + + extractor_items = defaultdict(lambda: None) + for extractor, _ in EXTRACTORS: + for result in archive_results: + if result.extractor == extractor: + extractor_items[extractor] = result + + for extractor, _ in EXTRACTORS: + if extractor not in exclude: + exists = extractor_items[extractor] is not None + output += output_template.format(path, canon[f"{extractor}_path"], str(exists), + extractor, icons.get(extractor, "?")) + if extractor == "wget": + # warc isn't technically it's own extractor, so we have to add it after wget + exists = list((Path(path) / canon["warc_path"]).glob("*.warc.gz")) + output += output_template.format(exists[0] if exists else '#', canon["warc_path"], str(bool(exists)), "warc", icons.get("warc", "?")) + + if extractor == "archive_org": + # The check for archive_org is different, so it has to be handled separately + target_path = Path(path) / "archive.org.txt" + exists = target_path.exists() + output += '<a href="{}" class="exists-{}" title="{}">{}</a> '.format(canon["archive_org_path"], str(exists), + "archive_org", icons.get("archive_org", "?")) + + return format_html(f'<span class="files-icons" style="font-size: 1.1em; opacity: 0.8">{output}<span>') diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/json.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/json.py new file mode 100644 index 0000000..f24b969 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/json.py @@ -0,0 +1,154 @@ +__package__ = 'archivebox.index' + +import os +import sys +import json as pyjson +from pathlib import Path + +from datetime import datetime +from typing import List, Optional, Iterator, Any, Union + +from .schema import Link +from ..system import atomic_write +from ..util import enforce_types +from ..config import ( + VERSION, + OUTPUT_DIR, + FOOTER_INFO, + GIT_SHA, + DEPENDENCIES, + JSON_INDEX_FILENAME, + ARCHIVE_DIR_NAME, + ANSI +) + + +MAIN_INDEX_HEADER = { + 'info': 'This is an index of site data archived by ArchiveBox: The self-hosted web archive.', + 'schema': 'archivebox.index.json', + 'copyright_info': FOOTER_INFO, + 'meta': { + 'project': 'ArchiveBox', + 'version': VERSION, + 'git_sha': GIT_SHA, + 'website': 'https://ArchiveBox.io', + 'docs': 'https://github.com/ArchiveBox/ArchiveBox/wiki', + 'source': 'https://github.com/ArchiveBox/ArchiveBox', + 'issues': 'https://github.com/ArchiveBox/ArchiveBox/issues', + 'dependencies': DEPENDENCIES, + }, +} + +@enforce_types +def generate_json_index_from_links(links: List[Link], with_headers: bool): + if with_headers: + output = { + **MAIN_INDEX_HEADER, + 'num_links': len(links), + 'updated': datetime.now(), + 'last_run_cmd': sys.argv, + 'links': links, + } + else: + output = links + return to_json(output, indent=4, sort_keys=True) + + +@enforce_types +def parse_json_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + """parse an archive index json file and return the list of links""" + + index_path = Path(out_dir) / JSON_INDEX_FILENAME + if index_path.exists(): + with open(index_path, 'r', encoding='utf-8') as f: + links = pyjson.load(f)['links'] + for link_json in links: + try: + yield Link.from_json(link_json) + except KeyError: + try: + detail_index_path = Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / link_json['timestamp'] + yield parse_json_link_details(str(detail_index_path)) + except KeyError: + # as a last effort, try to guess the missing values out of existing ones + try: + yield Link.from_json(link_json, guess=True) + except KeyError: + print(" {lightyellow}! Failed to load the index.json from {}".format(detail_index_path, **ANSI)) + continue + return () + +### Link Details Index + +@enforce_types +def write_json_link_details(link: Link, out_dir: Optional[str]=None) -> None: + """write a json file with some info about the link""" + + out_dir = out_dir or link.link_dir + path = Path(out_dir) / JSON_INDEX_FILENAME + atomic_write(str(path), link._asdict(extended=True)) + + +@enforce_types +def parse_json_link_details(out_dir: Union[Path, str], guess: Optional[bool]=False) -> Optional[Link]: + """load the json link index from a given directory""" + existing_index = Path(out_dir) / JSON_INDEX_FILENAME + if existing_index.exists(): + with open(existing_index, 'r', encoding='utf-8') as f: + try: + link_json = pyjson.load(f) + return Link.from_json(link_json, guess) + except pyjson.JSONDecodeError: + pass + return None + + +@enforce_types +def parse_json_links_details(out_dir: Union[Path, str]) -> Iterator[Link]: + """read through all the archive data folders and return the parsed links""" + + for entry in os.scandir(Path(out_dir) / ARCHIVE_DIR_NAME): + if entry.is_dir(follow_symlinks=True): + if (Path(entry.path) / 'index.json').exists(): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if link: + yield link + + + +### Helpers + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + + +@enforce_types +def to_json(obj: Any, indent: Optional[int]=4, sort_keys: bool=True, cls=ExtendedEncoder) -> str: + return pyjson.dumps(obj, indent=indent, sort_keys=sort_keys, cls=ExtendedEncoder) + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/schema.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/schema.py new file mode 100644 index 0000000..bc3a25d --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/schema.py @@ -0,0 +1,448 @@ +""" + +WARNING: THIS FILE IS ALL LEGACY CODE TO BE REMOVED. + +DO NOT ADD ANY NEW FEATURES TO THIS FILE, NEW CODE GOES HERE: core/models.py + +""" + +__package__ = 'archivebox.index' + +from pathlib import Path + +from datetime import datetime, timedelta + +from typing import List, Dict, Any, Optional, Union + +from dataclasses import dataclass, asdict, field, fields + + +from ..system import get_dir_size + +from ..config import OUTPUT_DIR, ARCHIVE_DIR_NAME + +class ArchiveError(Exception): + def __init__(self, message, hints=None): + super().__init__(message) + self.hints = hints + +LinkDict = Dict[str, Any] + +ArchiveOutput = Union[str, Exception, None] + +@dataclass(frozen=True) +class ArchiveResult: + cmd: List[str] + pwd: Optional[str] + cmd_version: Optional[str] + output: ArchiveOutput + status: str + start_ts: datetime + end_ts: datetime + index_texts: Union[List[str], None] = None + schema: str = 'ArchiveResult' + + def __post_init__(self): + self.typecheck() + + def _asdict(self): + return asdict(self) + + def typecheck(self) -> None: + assert self.schema == self.__class__.__name__ + assert isinstance(self.status, str) and self.status + assert isinstance(self.start_ts, datetime) + assert isinstance(self.end_ts, datetime) + assert isinstance(self.cmd, list) + assert all(isinstance(arg, str) and arg for arg in self.cmd) + assert self.pwd is None or isinstance(self.pwd, str) and self.pwd + assert self.cmd_version is None or isinstance(self.cmd_version, str) and self.cmd_version + assert self.output is None or isinstance(self.output, (str, Exception)) + if isinstance(self.output, str): + assert self.output + + @classmethod + def guess_ts(_cls, dict_info): + from ..util import parse_date + parsed_timestamp = parse_date(dict_info["timestamp"]) + start_ts = parsed_timestamp + end_ts = parsed_timestamp + timedelta(seconds=int(dict_info["duration"])) + return start_ts, end_ts + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + if guess: + keys = info.keys() + if "start_ts" not in keys: + info["start_ts"], info["end_ts"] = cls.guess_ts(json_info) + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + if "pwd" not in keys: + info["pwd"] = str(Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / json_info["timestamp"]) + if "cmd_version" not in keys: + info["cmd_version"] = "Undefined" + if "cmd" not in keys: + info["cmd"] = [] + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + info['cmd_version'] = info.get('cmd_version') + if type(info["cmd"]) is str: + info["cmd"] = [info["cmd"]] + return cls(**info) + + def to_dict(self, *keys) -> dict: + if keys: + return {k: v for k, v in asdict(self).items() if k in keys} + return asdict(self) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, csv_col=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def duration(self) -> int: + return (self.end_ts - self.start_ts).seconds + +@dataclass(frozen=True) +class Link: + timestamp: str + url: str + title: Optional[str] + tags: Optional[str] + sources: List[str] + history: Dict[str, List[ArchiveResult]] = field(default_factory=lambda: {}) + updated: Optional[datetime] = None + schema: str = 'Link' + + + def __str__(self) -> str: + return f'[{self.timestamp}] {self.url} "{self.title}"' + + def __post_init__(self): + self.typecheck() + + def overwrite(self, **kwargs): + """pure functional version of dict.update that returns a new instance""" + return Link(**{**self._asdict(), **kwargs}) + + def __eq__(self, other): + if not isinstance(other, Link): + return NotImplemented + return self.url == other.url + + def __gt__(self, other): + if not isinstance(other, Link): + return NotImplemented + if not self.timestamp or not other.timestamp: + return + return float(self.timestamp) > float(other.timestamp) + + def typecheck(self) -> None: + from ..config import stderr, ANSI + try: + assert self.schema == self.__class__.__name__ + assert isinstance(self.timestamp, str) and self.timestamp + assert self.timestamp.replace('.', '').isdigit() + assert isinstance(self.url, str) and '://' in self.url + assert self.updated is None or isinstance(self.updated, datetime) + assert self.title is None or (isinstance(self.title, str) and self.title) + assert self.tags is None or isinstance(self.tags, str) + assert isinstance(self.sources, list) + assert all(isinstance(source, str) and source for source in self.sources) + assert isinstance(self.history, dict) + for method, results in self.history.items(): + assert isinstance(method, str) and method + assert isinstance(results, list) + assert all(isinstance(result, ArchiveResult) for result in results) + except Exception: + stderr('{red}[X] Error while loading link! [{}] {} "{}"{reset}'.format(self.timestamp, self.url, self.title, **ANSI)) + raise + + def _asdict(self, extended=False): + info = { + 'schema': 'Link', + 'url': self.url, + 'title': self.title or None, + 'timestamp': self.timestamp, + 'updated': self.updated or None, + 'tags': self.tags or None, + 'sources': self.sources or [], + 'history': self.history or {}, + } + if extended: + info.update({ + 'link_dir': self.link_dir, + 'archive_path': self.archive_path, + + 'hash': self.url_hash, + 'base_url': self.base_url, + 'scheme': self.scheme, + 'domain': self.domain, + 'path': self.path, + 'basename': self.basename, + 'extension': self.extension, + 'is_static': self.is_static, + + 'bookmarked_date': self.bookmarked_date, + 'updated_date': self.updated_date, + 'oldest_archive_date': self.oldest_archive_date, + 'newest_archive_date': self.newest_archive_date, + + 'is_archived': self.is_archived, + 'num_outputs': self.num_outputs, + 'num_failures': self.num_failures, + + 'latest': self.latest_outputs(), + 'canonical': self.canonical_outputs(), + }) + return info + + def as_snapshot(self): + from core.models import Snapshot + return Snapshot.objects.get(url=self.url) + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + info['updated'] = parse_date(info.get('updated')) + info['sources'] = info.get('sources') or [] + + json_history = info.get('history') or {} + cast_history = {} + + for method, method_history in json_history.items(): + cast_history[method] = [] + for json_result in method_history: + assert isinstance(json_result, dict), 'Items in Link["history"][method] must be dicts' + cast_result = ArchiveResult.from_json(json_result, guess) + cast_history[method].append(cast_result) + + info['history'] = cast_history + return cls(**info) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, cols=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def link_dir(self) -> str: + from ..config import CONFIG + return str(Path(CONFIG['ARCHIVE_DIR']) / self.timestamp) + + @property + def archive_path(self) -> str: + from ..config import ARCHIVE_DIR_NAME + return '{}/{}'.format(ARCHIVE_DIR_NAME, self.timestamp) + + @property + def archive_size(self) -> float: + try: + return get_dir_size(self.archive_path)[0] + except Exception: + return 0 + + ### URL Helpers + @property + def url_hash(self): + from ..util import hashurl + + return hashurl(self.url) + + @property + def scheme(self) -> str: + from ..util import scheme + return scheme(self.url) + + @property + def extension(self) -> str: + from ..util import extension + return extension(self.url) + + @property + def domain(self) -> str: + from ..util import domain + return domain(self.url) + + @property + def path(self) -> str: + from ..util import path + return path(self.url) + + @property + def basename(self) -> str: + from ..util import basename + return basename(self.url) + + @property + def base_url(self) -> str: + from ..util import base_url + return base_url(self.url) + + ### Pretty Printing Helpers + @property + def bookmarked_date(self) -> Optional[str]: + from ..util import ts_to_date + + max_ts = (datetime.now() + timedelta(days=30)).timestamp() + + if self.timestamp and self.timestamp.replace('.', '').isdigit(): + if 0 < float(self.timestamp) < max_ts: + return ts_to_date(datetime.fromtimestamp(float(self.timestamp))) + else: + return str(self.timestamp) + return None + + + @property + def updated_date(self) -> Optional[str]: + from ..util import ts_to_date + return ts_to_date(self.updated) if self.updated else None + + @property + def archive_dates(self) -> List[datetime]: + return [ + result.start_ts + for method in self.history.keys() + for result in self.history[method] + ] + + @property + def oldest_archive_date(self) -> Optional[datetime]: + return min(self.archive_dates, default=None) + + @property + def newest_archive_date(self) -> Optional[datetime]: + return max(self.archive_dates, default=None) + + ### Archive Status Helpers + @property + def num_outputs(self) -> int: + return self.as_snapshot().num_outputs + + @property + def num_failures(self) -> int: + return sum(1 + for method in self.history.keys() + for result in self.history[method] + if result.status == 'failed') + + @property + def is_static(self) -> bool: + from ..util import is_static_file + return is_static_file(self.url) + + @property + def is_archived(self) -> bool: + from ..config import ARCHIVE_DIR + from ..util import domain + + output_paths = ( + domain(self.url), + 'output.pdf', + 'screenshot.png', + 'output.html', + 'media', + 'singlefile.html' + ) + + return any( + (Path(ARCHIVE_DIR) / self.timestamp / path).exists() + for path in output_paths + ) + + def latest_outputs(self, status: str=None) -> Dict[str, ArchiveOutput]: + """get the latest output that each archive method produced for link""" + + ARCHIVE_METHODS = ( + 'title', 'favicon', 'wget', 'warc', 'singlefile', 'pdf', + 'screenshot', 'dom', 'git', 'media', 'archive_org', + ) + latest: Dict[str, ArchiveOutput] = {} + for archive_method in ARCHIVE_METHODS: + # get most recent succesful result in history for each archive method + history = self.history.get(archive_method) or [] + history = list(filter(lambda result: result.output, reversed(history))) + if status is not None: + history = list(filter(lambda result: result.status == status, history)) + + history = list(history) + if history: + latest[archive_method] = history[0].output + else: + latest[archive_method] = None + return latest + + + def canonical_outputs(self) -> Dict[str, Optional[str]]: + """predict the expected output paths that should be present after archiving""" + + from ..extractors.wget import wget_output_path + canonical = { + 'index_path': 'index.html', + 'favicon_path': 'favicon.ico', + 'google_favicon_path': 'https://www.google.com/s2/favicons?domain={}'.format(self.domain), + 'wget_path': wget_output_path(self), + 'warc_path': 'warc', + 'singlefile_path': 'singlefile.html', + 'readability_path': 'readability/content.html', + 'mercury_path': 'mercury/content.html', + 'pdf_path': 'output.pdf', + 'screenshot_path': 'screenshot.png', + 'dom_path': 'output.html', + 'archive_org_path': 'https://web.archive.org/web/{}'.format(self.base_url), + 'git_path': 'git', + 'media_path': 'media', + } + if self.is_static: + # static binary files like PDF and images are handled slightly differently. + # they're just downloaded once and aren't archived separately multiple times, + # so the wget, screenshot, & pdf urls should all point to the same file + + static_path = wget_output_path(self) + canonical.update({ + 'title': self.basename, + 'wget_path': static_path, + 'pdf_path': static_path, + 'screenshot_path': static_path, + 'dom_path': static_path, + 'singlefile_path': static_path, + 'readability_path': static_path, + 'mercury_path': static_path, + }) + return canonical + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/sql.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/sql.py new file mode 100644 index 0000000..1e99f67 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/index/sql.py @@ -0,0 +1,106 @@ +__package__ = 'archivebox.index' + +from io import StringIO +from pathlib import Path +from typing import List, Tuple, Iterator +from django.db.models import QuerySet +from django.db import transaction + +from .schema import Link +from ..util import enforce_types +from ..config import OUTPUT_DIR + + +### Main Links Index + +@enforce_types +def parse_sql_main_index(out_dir: Path=OUTPUT_DIR) -> Iterator[Link]: + from core.models import Snapshot + + return ( + Link.from_json(page.as_json(*Snapshot.keys)) + for page in Snapshot.objects.all() + ) + +@enforce_types +def remove_from_sql_main_index(snapshots: QuerySet, out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + snapshots.delete() + +@enforce_types +def write_link_to_sql_index(link: Link): + from core.models import Snapshot + info = {k: v for k, v in link._asdict().items() if k in Snapshot.keys} + tags = info.pop("tags") + if tags is None: + tags = [] + + try: + info["timestamp"] = Snapshot.objects.get(url=link.url).timestamp + except Snapshot.DoesNotExist: + while Snapshot.objects.filter(timestamp=info["timestamp"]).exists(): + info["timestamp"] = str(float(info["timestamp"]) + 1.0) + + snapshot, _ = Snapshot.objects.update_or_create(url=link.url, defaults=info) + snapshot.save_tags(tags) + return snapshot + + +@enforce_types +def write_sql_main_index(links: List[Link], out_dir: Path=OUTPUT_DIR) -> None: + with transaction.atomic(): + for link in links: + write_link_to_sql_index(link) + + +@enforce_types +def write_sql_link_details(link: Link, out_dir: Path=OUTPUT_DIR) -> None: + from core.models import Snapshot + + with transaction.atomic(): + try: + snap = Snapshot.objects.get(url=link.url) + except Snapshot.DoesNotExist: + snap = write_link_to_sql_index(link) + snap.title = link.title + + tag_set = ( + set(tag.strip() for tag in (link.tags or '').split(',')) + ) + tag_list = list(tag_set) or [] + + snap.save() + snap.save_tags(tag_list) + + + +@enforce_types +def list_migrations(out_dir: Path=OUTPUT_DIR) -> List[Tuple[bool, str]]: + from django.core.management import call_command + out = StringIO() + call_command("showmigrations", list=True, stdout=out) + out.seek(0) + migrations = [] + for line in out.readlines(): + if line.strip() and ']' in line: + status_str, name_str = line.strip().split(']', 1) + is_applied = 'X' in status_str + migration_name = name_str.strip() + migrations.append((is_applied, migration_name)) + + return migrations + +@enforce_types +def apply_migrations(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.core.management import call_command + null, out = StringIO(), StringIO() + call_command("makemigrations", interactive=False, stdout=null) + call_command("migrate", interactive=False, stdout=out) + out.seek(0) + + return [line.strip() for line in out.readlines() if line.strip()] + +@enforce_types +def get_admins(out_dir: Path=OUTPUT_DIR) -> List[str]: + from django.contrib.auth.models import User + return User.objects.filter(is_superuser=True) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/logging_util.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/logging_util.py new file mode 100644 index 0000000..f2b8673 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/logging_util.py @@ -0,0 +1,569 @@ +__package__ = 'archivebox' + +import re +import os +import sys +import time +import argparse +from math import log +from multiprocessing import Process +from pathlib import Path + +from datetime import datetime +from dataclasses import dataclass +from typing import Optional, List, Dict, Union, IO, TYPE_CHECKING + +if TYPE_CHECKING: + from .index.schema import Link, ArchiveResult + +from .util import enforce_types +from .config import ( + ConfigDict, + OUTPUT_DIR, + PYTHON_ENCODING, + ANSI, + IS_TTY, + TERM_WIDTH, + SHOW_PROGRESS, + SOURCES_DIR_NAME, + stderr, +) + +@dataclass +class RuntimeStats: + """mutable stats counter for logging archiving timing info to CLI output""" + + skipped: int = 0 + succeeded: int = 0 + failed: int = 0 + + parse_start_ts: Optional[datetime] = None + parse_end_ts: Optional[datetime] = None + + index_start_ts: Optional[datetime] = None + index_end_ts: Optional[datetime] = None + + archiving_start_ts: Optional[datetime] = None + archiving_end_ts: Optional[datetime] = None + +# globals are bad, mmkay +_LAST_RUN_STATS = RuntimeStats() + + + +class SmartFormatter(argparse.HelpFormatter): + """Patched formatter that prints newlines in argparse help strings""" + def _split_lines(self, text, width): + if '\n' in text: + return text.splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) + + +def reject_stdin(caller: str, stdin: Optional[IO]=sys.stdin) -> None: + """Tell the user they passed stdin to a command that doesn't accept it""" + + if stdin and not stdin.isatty(): + stdin_raw_text = stdin.read().strip() + if stdin_raw_text: + stderr(f'[X] The "{caller}" command does not accept stdin.', color='red') + stderr(f' Run archivebox "{caller} --help" to see usage and examples.') + stderr() + raise SystemExit(1) + + +def accept_stdin(stdin: Optional[IO]=sys.stdin) -> Optional[str]: + """accept any standard input and return it as a string or None""" + if not stdin: + return None + elif stdin and not stdin.isatty(): + stdin_str = stdin.read().strip() + return stdin_str or None + return None + + +class TimedProgress: + """Show a progress bar and measure elapsed time until .end() is called""" + + def __init__(self, seconds, prefix=''): + self.SHOW_PROGRESS = SHOW_PROGRESS + if self.SHOW_PROGRESS: + self.p = Process(target=progress_bar, args=(seconds, prefix)) + self.p.start() + + self.stats = {'start_ts': datetime.now(), 'end_ts': None} + + def end(self): + """immediately end progress, clear the progressbar line, and save end_ts""" + + end_ts = datetime.now() + self.stats['end_ts'] = end_ts + + if self.SHOW_PROGRESS: + # terminate if we havent already terminated + try: + # kill the progress bar subprocess + try: + self.p.close() # must be closed *before* its terminnated + except: + pass + self.p.terminate() + self.p.join() + + + # clear whole terminal line + try: + sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + except (IOError, BrokenPipeError): + # ignore when the parent proc has stopped listening to our stdout + pass + except ValueError: + pass + + +@enforce_types +def progress_bar(seconds: int, prefix: str='') -> None: + """show timer in the form of progress bar, with percentage and seconds remaining""" + chunk = '█' if PYTHON_ENCODING == 'UTF-8' else '#' + last_width = TERM_WIDTH() + chunks = last_width - len(prefix) - 20 # number of progress chunks to show (aka max bar width) + try: + for s in range(seconds * chunks): + max_width = TERM_WIDTH() + if max_width < last_width: + # when the terminal size is shrunk, we have to write a newline + # otherwise the progress bar will keep wrapping incorrectly + sys.stdout.write('\r\n') + sys.stdout.flush() + chunks = max_width - len(prefix) - 20 + pct_complete = s / chunks / seconds * 100 + log_pct = (log(pct_complete or 1, 10) / 2) * 100 # everyone likes faster progress bars ;) + bar_width = round(log_pct/(100/chunks)) + last_width = max_width + + # ████████████████████ 0.9% (1/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['green' if pct_complete < 80 else 'lightyellow'], + (chunk * bar_width).ljust(chunks), + ANSI['reset'], + round(pct_complete, 1), + round(s/chunks), + seconds, + )) + sys.stdout.flush() + time.sleep(1 / chunks) + + # ██████████████████████████████████ 100.0% (60/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['red'], + chunk * chunks, + ANSI['reset'], + 100.0, + seconds, + seconds, + )) + sys.stdout.flush() + # uncomment to have it disappear when it hits 100% instead of staying full red: + # time.sleep(0.5) + # sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + # sys.stdout.flush() + except (KeyboardInterrupt, BrokenPipeError): + print() + pass + + +def log_cli_command(subcommand: str, subcommand_args: List[str], stdin: Optional[str], pwd: str): + from .config import VERSION, ANSI + cmd = ' '.join(('archivebox', subcommand, *subcommand_args)) + stderr('{black}[i] [{now}] ArchiveBox v{VERSION}: {cmd}{reset}'.format( + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + VERSION=VERSION, + cmd=cmd, + **ANSI, + )) + stderr('{black} > {pwd}{reset}'.format(pwd=pwd, **ANSI)) + stderr() + +### Parsing Stage + + +def log_importing_started(urls: Union[str, List[str]], depth: int, index_only: bool): + _LAST_RUN_STATS.parse_start_ts = datetime.now() + print('{green}[+] [{}] Adding {} links to index (crawl depth={}){}...{reset}'.format( + _LAST_RUN_STATS.parse_start_ts.strftime('%Y-%m-%d %H:%M:%S'), + len(urls) if isinstance(urls, list) else len(urls.split('\n')), + depth, + ' (index only)' if index_only else '', + **ANSI, + )) + +def log_source_saved(source_file: str): + print(' > Saved verbatim input to {}/{}'.format(SOURCES_DIR_NAME, source_file.rsplit('/', 1)[-1])) + +def log_parsing_finished(num_parsed: int, parser_name: str): + _LAST_RUN_STATS.parse_end_ts = datetime.now() + print(' > Parsed {} URLs from input ({})'.format(num_parsed, parser_name)) + +def log_deduping_finished(num_new_links: int): + print(' > Found {} new URLs not already in index'.format(num_new_links)) + + +def log_crawl_started(new_links): + print() + print('{green}[*] Starting crawl of {} sites 1 hop out from starting point{reset}'.format(len(new_links), **ANSI)) + +### Indexing Stage + +def log_indexing_process_started(num_links: int): + start_ts = datetime.now() + _LAST_RUN_STATS.index_start_ts = start_ts + print() + print('{black}[*] [{}] Writing {} links to main index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + + +def log_indexing_process_finished(): + end_ts = datetime.now() + _LAST_RUN_STATS.index_end_ts = end_ts + + +def log_indexing_started(out_path: str): + if IS_TTY: + sys.stdout.write(f' > {out_path}') + + +def log_indexing_finished(out_path: str): + print(f'\r √ {out_path}') + + +### Archiving Stage + +def log_archiving_started(num_links: int, resume: Optional[float]=None): + start_ts = datetime.now() + _LAST_RUN_STATS.archiving_start_ts = start_ts + print() + if resume: + print('{green}[▶] [{}] Resuming archive updating for {} pages starting from {}...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + resume, + **ANSI, + )) + else: + print('{green}[▶] [{}] Starting archiving of {} snapshots in index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + +def log_archiving_paused(num_links: int, idx: int, timestamp: str): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + print() + print('\n{lightyellow}[X] [{now}] Downloading paused on link {timestamp} ({idx}/{total}){reset}'.format( + **ANSI, + now=end_ts.strftime('%Y-%m-%d %H:%M:%S'), + idx=idx+1, + timestamp=timestamp, + total=num_links, + )) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print(' Continue archiving where you left off by running:') + print(' archivebox update --resume={}'.format(timestamp)) + +def log_archiving_finished(num_links: int): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + assert _LAST_RUN_STATS.archiving_start_ts is not None + seconds = end_ts.timestamp() - _LAST_RUN_STATS.archiving_start_ts.timestamp() + if seconds > 60: + duration = '{0:.2f} min'.format(seconds / 60) + else: + duration = '{0:.2f} sec'.format(seconds) + + print() + print('{}[√] [{}] Update of {} pages complete ({}){}'.format( + ANSI['green'], + end_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + duration, + ANSI['reset'], + )) + print(' - {} links skipped'.format(_LAST_RUN_STATS.skipped)) + print(' - {} links updated'.format(_LAST_RUN_STATS.succeeded + _LAST_RUN_STATS.failed)) + print(' - {} links had errors'.format(_LAST_RUN_STATS.failed)) + print() + print(' {lightred}Hint:{reset} To manage your archive in a Web UI, run:'.format(**ANSI)) + print(' archivebox server 0.0.0.0:8000') + + +def log_link_archiving_started(link: "Link", link_dir: str, is_new: bool): + # [*] [2019-03-22 13:46:45] "Log Structured Merge Trees - ben stopford" + # http://www.benstopford.com/2015/02/14/log-structured-merge-trees/ + # > output/archive/1478739709 + + print('\n[{symbol_color}{symbol}{reset}] [{symbol_color}{now}{reset}] "{title}"'.format( + symbol_color=ANSI['green' if is_new else 'black'], + symbol='+' if is_new else '√', + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + title=link.title or link.base_url, + **ANSI, + )) + print(' {blue}{url}{reset}'.format(url=link.url, **ANSI)) + print(' {} {}'.format( + '>' if is_new else '√', + pretty_path(link_dir), + )) + +def log_link_archiving_finished(link: "Link", link_dir: str, is_new: bool, stats: dict): + total = sum(stats.values()) + + if stats['failed'] > 0 : + _LAST_RUN_STATS.failed += 1 + elif stats['skipped'] == total: + _LAST_RUN_STATS.skipped += 1 + else: + _LAST_RUN_STATS.succeeded += 1 + + +def log_archive_method_started(method: str): + print(' > {}'.format(method)) + + +def log_archive_method_finished(result: "ArchiveResult"): + """quote the argument with whitespace in a command so the user can + copy-paste the outputted string directly to run the cmd + """ + # Prettify CMD string and make it safe to copy-paste by quoting arguments + quoted_cmd = ' '.join( + '"{}"'.format(arg) if ' ' in arg else arg + for arg in result.cmd + ) + + if result.status == 'failed': + if result.output.__class__.__name__ == 'TimeoutExpired': + duration = (result.end_ts - result.start_ts).seconds + hint_header = [ + '{lightyellow}Extractor timed out after {}s.{reset}'.format(duration, **ANSI), + ] + else: + hint_header = [ + '{lightyellow}Extractor failed:{reset}'.format(**ANSI), + ' {reset}{} {red}{}{reset}'.format( + result.output.__class__.__name__.replace('ArchiveError', ''), + result.output, + **ANSI, + ), + ] + + # Prettify error output hints string and limit to five lines + hints = getattr(result.output, 'hints', None) or () + if hints: + hints = hints if isinstance(hints, (list, tuple)) else hints.split('\n') + hints = ( + ' {}{}{}'.format(ANSI['lightyellow'], line.strip(), ANSI['reset']) + for line in hints[:5] if line.strip() + ) + + + # Collect and prefix output lines with indentation + output_lines = [ + *hint_header, + *hints, + '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), + *([' cd {};'.format(result.pwd)] if result.pwd else []), + ' {}'.format(quoted_cmd), + ] + print('\n'.join( + ' {}'.format(line) + for line in output_lines + if line + )) + print() + + +def log_list_started(filter_patterns: Optional[List[str]], filter_type: str): + print('{green}[*] Finding links in the archive index matching these {} patterns:{reset}'.format( + filter_type, + **ANSI, + )) + print(' {}'.format(' '.join(filter_patterns or ()))) + +def log_list_finished(links): + from .index.csv import links_to_csv + print() + print('---------------------------------------------------------------------------------------------------') + print(links_to_csv(links, cols=['timestamp', 'is_archived', 'num_outputs', 'url'], header=True, ljust=16, separator=' | ')) + print('---------------------------------------------------------------------------------------------------') + print() + + +def log_removal_started(links: List["Link"], yes: bool, delete: bool): + print('{lightyellow}[i] Found {} matching URLs to remove.{reset}'.format(len(links), **ANSI)) + if delete: + file_counts = [link.num_outputs for link in links if Path(link.link_dir).exists()] + print( + f' {len(links)} Links will be de-listed from the main index, and their archived content folders will be deleted from disk.\n' + f' ({len(file_counts)} data folders with {sum(file_counts)} archived files will be deleted!)' + ) + else: + print( + ' Matching links will be de-listed from the main index, but their archived content folders will remain in place on disk.\n' + ' (Pass --delete if you also want to permanently delete the data folders)' + ) + + if not yes: + print() + print('{lightyellow}[?] Do you want to proceed with removing these {} links?{reset}'.format(len(links), **ANSI)) + try: + assert input(' y/[n]: ').lower() == 'y' + except (KeyboardInterrupt, EOFError, AssertionError): + raise SystemExit(0) + +def log_removal_finished(all_links: int, to_remove: int): + if all_links == 0: + print() + print('{red}[X] No matching links found.{reset}'.format(**ANSI)) + else: + print() + print('{red}[√] Removed {} out of {} links from the archive index.{reset}'.format( + to_remove, + all_links, + **ANSI, + )) + print(' Index now contains {} links.'.format(all_links - to_remove)) + + +def log_shell_welcome_msg(): + from .cli import list_subcommands + + print('{green}# ArchiveBox Imports{reset}'.format(**ANSI)) + print('{green}from core.models import Snapshot, User{reset}'.format(**ANSI)) + print('{green}from archivebox import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) + print() + print('[i] Welcome to the ArchiveBox Shell!') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Shell-Usage') + print() + print(' {lightred}Hint:{reset} Example use:'.format(**ANSI)) + print(' print(Snapshot.objects.filter(is_archived=True).count())') + print(' Snapshot.objects.get(url="https://example.com").as_json()') + print(' add("https://example.com/some/new/url")') + + + +### Helpers + +@enforce_types +def pretty_path(path: Union[Path, str]) -> str: + """convert paths like .../ArchiveBox/archivebox/../output/abc into output/abc""" + pwd = Path('.').resolve() + # parent = os.path.abspath(os.path.join(pwd, os.path.pardir)) + return str(path).replace(str(pwd) + '/', './') + + +@enforce_types +def printable_filesize(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + + +@enforce_types +def printable_folders(folders: Dict[str, Optional["Link"]], + with_headers: bool=False) -> str: + return '\n'.join( + f'{folder} {link and link.url} "{link and link.title}"' + for folder, link in folders.items() + ) + + + +@enforce_types +def printable_config(config: ConfigDict, prefix: str='') -> str: + return f'\n{prefix}'.join( + f'{key}={val}' + for key, val in config.items() + if not (isinstance(val, dict) or callable(val)) + ) + + +@enforce_types +def printable_folder_status(name: str, folder: Dict) -> str: + if folder['enabled']: + if folder['is_valid']: + color, symbol, note = 'green', '√', 'valid' + else: + color, symbol, note, num_files = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, num_files = 'lightyellow', '-', 'disabled', '-' + + if folder['path']: + if Path(folder['path']).exists(): + num_files = ( + f'{len(os.listdir(folder["path"]))} files' + if Path(folder['path']).is_dir() else + printable_filesize(Path(folder['path']).stat().st_size) + ) + else: + num_files = 'missing' + + path = str(folder['path']).replace(str(OUTPUT_DIR), '.') if folder['path'] else '' + if path and ' ' in path: + path = f'"{path}"' + + # if path is just a plain dot, replace it back with the full path for clarity + if path == '.': + path = str(OUTPUT_DIR) + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + num_files.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) + + +@enforce_types +def printable_dependency_version(name: str, dependency: Dict) -> str: + version = None + if dependency['enabled']: + if dependency['is_valid']: + color, symbol, note, version = 'green', '√', 'valid', '' + + parsed_version_num = re.search(r'[\d\.]+', dependency['version']) + if parsed_version_num: + version = f'v{parsed_version_num[0]}' + + if not version: + color, symbol, note, version = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, version = 'lightyellow', '-', 'disabled', '-' + + path = str(dependency["path"]).replace(str(OUTPUT_DIR), '.') if dependency["path"] else '' + if path and ' ' in path: + path = f'"{path}"' + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(21), + version.ljust(14), + ANSI[color], + note.ljust(8), + ANSI['reset'], + path.ljust(76), + )) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/main.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/main.py new file mode 100644 index 0000000..eb8cd6a --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/main.py @@ -0,0 +1,1131 @@ +__package__ = 'archivebox' + +import os +import sys +import shutil +import platform +from pathlib import Path +from datetime import date + +from typing import Dict, List, Optional, Iterable, IO, Union +from crontab import CronTab, CronSlices +from django.db.models import QuerySet + +from .cli import ( + list_subcommands, + run_subcommand, + display_first, + meta_cmds, + main_cmds, + archive_cmds, +) +from .parsers import ( + save_text_as_source, + save_file_as_source, + parse_links_memory, +) +from .index.schema import Link +from .util import enforce_types # type: ignore +from .system import get_dir_size, dedupe_cron_jobs, CRON_COMMENT +from .index import ( + load_main_index, + parse_links_from_source, + dedupe_links, + write_main_index, + snapshot_filter, + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, + fix_invalid_folder_locations, + write_link_details, +) +from .index.json import ( + parse_json_main_index, + parse_json_links_details, + generate_json_index_from_links, +) +from .index.sql import ( + get_admins, + apply_migrations, + remove_from_sql_main_index, +) +from .index.html import ( + generate_index_from_links, +) +from .index.csv import links_to_csv +from .extractors import archive_links, archive_link, ignore_methods +from .config import ( + stderr, + hint, + ConfigDict, + ANSI, + IS_TTY, + IN_DOCKER, + USER, + ARCHIVEBOX_BINARY, + ONLY_NEW, + OUTPUT_DIR, + SOURCES_DIR, + ARCHIVE_DIR, + LOGS_DIR, + CONFIG_FILE, + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + SQL_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, + check_dependencies, + check_data_folder, + write_config_file, + VERSION, + CODE_LOCATIONS, + EXTERNAL_LOCATIONS, + DATA_LOCATIONS, + DEPENDENCIES, + load_all_config, + CONFIG, + USER_CONFIG, + get_real_name, +) +from .logging_util import ( + TERM_WIDTH, + TimedProgress, + log_importing_started, + log_crawl_started, + log_removal_started, + log_removal_finished, + log_list_started, + log_list_finished, + printable_config, + printable_folders, + printable_filesize, + printable_folder_status, + printable_dependency_version, +) + +from .search import flush_search_index, index_links + +ALLOWED_IN_OUTPUT_DIR = { + 'lost+found', + '.DS_Store', + '.venv', + 'venv', + 'virtualenv', + '.virtualenv', + 'node_modules', + 'package-lock.json', + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, +} + +@enforce_types +def help(out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox help message and usage""" + + all_subcommands = list_subcommands() + COMMANDS_HELP_TEXT = '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in meta_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in main_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in archive_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd not in display_first + ) + + + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('''{green}ArchiveBox v{}: The self-hosted internet archive.{reset} + +{lightred}Active data directory:{reset} + {} + +{lightred}Usage:{reset} + archivebox [command] [--help] [--version] [...args] + +{lightred}Commands:{reset} + {} + +{lightred}Example Use:{reset} + mkdir my-archive; cd my-archive/ + archivebox init + archivebox status + + archivebox add https://example.com/some/page + archivebox add --depth=1 ~/Downloads/bookmarks_export.html + + archivebox list --sort=timestamp --csv=timestamp,url,is_archived + archivebox schedule --every=day https://example.com/some/feed.rss + archivebox update --resume=15109948213.123 + +{lightred}Documentation:{reset} + https://github.com/ArchiveBox/ArchiveBox/wiki +'''.format(VERSION, out_dir, COMMANDS_HELP_TEXT, **ANSI)) + + else: + print('{green}Welcome to ArchiveBox v{}!{reset}'.format(VERSION, **ANSI)) + print() + if IN_DOCKER: + print('When using Docker, you need to mount a volume to use as your data dir:') + print(' docker run -v /some/path:/data archivebox ...') + print() + print('To import an existing archive (from a previous version of ArchiveBox):') + print(' 1. cd into your data dir OUTPUT_DIR (usually ArchiveBox/output) and run:') + print(' 2. archivebox init') + print() + print('To start a new archive:') + print(' 1. Create an empty directory, then cd into it and run:') + print(' 2. archivebox init') + print() + print('For more information, see the documentation here:') + print(' https://github.com/ArchiveBox/ArchiveBox/wiki') + + +@enforce_types +def version(quiet: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Print the ArchiveBox version and dependency information""" + + if quiet: + print(VERSION) + else: + print('ArchiveBox v{}'.format(VERSION)) + p = platform.uname() + print(sys.implementation.name.title(), p.system, platform.platform(), p.machine, '(in Docker)' if IN_DOCKER else '(not in Docker)') + print() + + print('{white}[i] Dependency versions:{reset}'.format(**ANSI)) + for name, dependency in DEPENDENCIES.items(): + print(printable_dependency_version(name, dependency)) + + print() + print('{white}[i] Source-code locations:{reset}'.format(**ANSI)) + for name, folder in CODE_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + print('{white}[i] Secrets locations:{reset}'.format(**ANSI)) + for name, folder in EXTERNAL_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + if DATA_LOCATIONS['OUTPUT_DIR']['is_valid']: + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + for name, folder in DATA_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + else: + print() + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + + print() + check_dependencies() + + +@enforce_types +def run(subcommand: str, + subcommand_args: Optional[List[str]], + stdin: Optional[IO]=None, + out_dir: Path=OUTPUT_DIR) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + run_subcommand( + subcommand=subcommand, + subcommand_args=subcommand_args, + stdin=stdin, + pwd=out_dir, + ) + + +@enforce_types +def init(force: bool=False, out_dir: Path=OUTPUT_DIR) -> None: + """Initialize a new ArchiveBox collection in the current directory""" + from core.models import Snapshot + Path(out_dir).mkdir(exist_ok=True) + is_empty = not len(set(os.listdir(out_dir)) - ALLOWED_IN_OUTPUT_DIR) + + if (Path(out_dir) / JSON_INDEX_FILENAME).exists(): + stderr("[!] This folder contains a JSON index. It is deprecated, and will no longer be kept up to date automatically.", color="lightyellow") + stderr(" You can run `archivebox list --json --with-headers > index.json` to manually generate it.", color="lightyellow") + + existing_index = (Path(out_dir) / SQL_INDEX_FILENAME).exists() + + if is_empty and not existing_index: + print('{green}[+] Initializing a new ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + elif existing_index: + print('{green}[*] Updating existing ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + else: + if force: + stderr('[!] This folder appears to already have files in it, but no index.sqlite3 is present.', color='lightyellow') + stderr(' Because --force was passed, ArchiveBox will initialize anyway (which may overwrite existing files).') + else: + stderr( + ("{red}[X] This folder appears to already have files in it, but no index.sqlite3 present.{reset}\n\n" + " You must run init in a completely empty directory, or an existing data folder.\n\n" + " {lightred}Hint:{reset} To import an existing data folder make sure to cd into the folder first, \n" + " then run and run 'archivebox init' to pick up where you left off.\n\n" + " (Always make sure your data folder is backed up first before updating ArchiveBox)" + ).format(out_dir, **ANSI) + ) + raise SystemExit(2) + + if existing_index: + print('\n{green}[*] Verifying archive folder structure...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building archive folder structure...{reset}'.format(**ANSI)) + + Path(SOURCES_DIR).mkdir(exist_ok=True) + print(f' √ {SOURCES_DIR}') + + Path(ARCHIVE_DIR).mkdir(exist_ok=True) + print(f' √ {ARCHIVE_DIR}') + + Path(LOGS_DIR).mkdir(exist_ok=True) + print(f' √ {LOGS_DIR}') + + write_config_file({}, out_dir=out_dir) + print(f' √ {CONFIG_FILE}') + if (Path(out_dir) / SQL_INDEX_FILENAME).exists(): + print('\n{green}[*] Verifying main SQL index and running migrations...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building main SQL index and running migrations...{reset}'.format(**ANSI)) + + DATABASE_FILE = Path(out_dir) / SQL_INDEX_FILENAME + print(f' √ {DATABASE_FILE}') + print() + for migration_line in apply_migrations(out_dir): + print(f' {migration_line}') + + + assert DATABASE_FILE.exists() + + # from django.contrib.auth.models import User + # if IS_TTY and not User.objects.filter(is_superuser=True).exists(): + # print('{green}[+] Creating admin user account...{reset}'.format(**ANSI)) + # call_command("createsuperuser", interactive=True) + + print() + print('{green}[*] Collecting links from any existing indexes and archive folders...{reset}'.format(**ANSI)) + + all_links = Snapshot.objects.none() + pending_links: Dict[str, Link] = {} + + if existing_index: + all_links = load_main_index(out_dir=out_dir, warn=False) + print(' √ Loaded {} links from existing main index.'.format(all_links.count())) + + # Links in data folders that dont match their timestamp + fixed, cant_fix = fix_invalid_folder_locations(out_dir=out_dir) + if fixed: + print(' {lightyellow}√ Fixed {} data directory locations that didn\'t match their link timestamps.{reset}'.format(len(fixed), **ANSI)) + if cant_fix: + print(' {lightyellow}! Could not fix {} data directory locations due to conflicts with existing folders.{reset}'.format(len(cant_fix), **ANSI)) + + # Links in JSON index but not in main index + orphaned_json_links = { + link.url: link + for link in parse_json_main_index(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_json_links: + pending_links.update(orphaned_json_links) + print(' {lightyellow}√ Added {} orphaned links from existing JSON index...{reset}'.format(len(orphaned_json_links), **ANSI)) + + # Links in data dir indexes but not in main index + orphaned_data_dir_links = { + link.url: link + for link in parse_json_links_details(out_dir) + if not all_links.filter(url=link.url).exists() + } + if orphaned_data_dir_links: + pending_links.update(orphaned_data_dir_links) + print(' {lightyellow}√ Added {} orphaned links from existing archive directories.{reset}'.format(len(orphaned_data_dir_links), **ANSI)) + + # Links in invalid/duplicate data dirs + invalid_folders = { + folder: link + for folder, link in get_invalid_folders(all_links, out_dir=out_dir).items() + } + if invalid_folders: + print(' {lightyellow}! Skipped adding {} invalid link data directories.{reset}'.format(len(invalid_folders), **ANSI)) + print(' X ' + '\n X '.join(f'{folder} {link}' for folder, link in invalid_folders.items())) + print() + print(' {lightred}Hint:{reset} For more information about the link data directories that were skipped, run:'.format(**ANSI)) + print(' archivebox status') + print(' archivebox list --status=invalid') + + + write_main_index(list(pending_links.values()), out_dir=out_dir) + + print('\n{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + if existing_index: + print('{green}[√] Done. Verified and updated the existing ArchiveBox collection.{reset}'.format(**ANSI)) + else: + print('{green}[√] Done. A new ArchiveBox collection was initialized ({} links).{reset}'.format(len(all_links), **ANSI)) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print() + print(' To add new links, you can run:') + print(" archivebox add ~/some/path/or/url/to/list_of_links.txt") + print() + print(' For more usage and examples, run:') + print(' archivebox help') + + json_index = Path(out_dir) / JSON_INDEX_FILENAME + html_index = Path(out_dir) / HTML_INDEX_FILENAME + index_name = f"{date.today()}_index_old" + if json_index.exists(): + json_index.rename(f"{index_name}.json") + if html_index.exists(): + html_index.rename(f"{index_name}.html") + + + +@enforce_types +def status(out_dir: Path=OUTPUT_DIR) -> None: + """Print out some info and statistics about the archive collection""" + + check_data_folder(out_dir=out_dir) + + from core.models import Snapshot + from django.contrib.auth import get_user_model + User = get_user_model() + + print('{green}[*] Scanning archive main index...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {out_dir}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(out_dir, recursive=False, pattern='index.') + size = printable_filesize(num_bytes) + print(f' Index size: {size} across {num_files} files') + print() + + links = load_main_index(out_dir=out_dir) + num_sql_links = links.count() + num_link_details = sum(1 for link in parse_json_links_details(out_dir=out_dir)) + print(f' > SQL Main Index: {num_sql_links} links'.ljust(36), f'(found in {SQL_INDEX_FILENAME})') + print(f' > JSON Link Details: {num_link_details} links'.ljust(36), f'(found in {ARCHIVE_DIR_NAME}/*/index.json)') + print() + print('{green}[*] Scanning archive data directories...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {ARCHIVE_DIR}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(ARCHIVE_DIR) + size = printable_filesize(num_bytes) + print(f' Size: {size} across {num_files} files in {num_dirs} directories') + print(ANSI['black']) + num_indexed = len(get_indexed_folders(links, out_dir=out_dir)) + num_archived = len(get_archived_folders(links, out_dir=out_dir)) + num_unarchived = len(get_unarchived_folders(links, out_dir=out_dir)) + print(f' > indexed: {num_indexed}'.ljust(36), f'({get_indexed_folders.__doc__})') + print(f' > archived: {num_archived}'.ljust(36), f'({get_archived_folders.__doc__})') + print(f' > unarchived: {num_unarchived}'.ljust(36), f'({get_unarchived_folders.__doc__})') + + num_present = len(get_present_folders(links, out_dir=out_dir)) + num_valid = len(get_valid_folders(links, out_dir=out_dir)) + print() + print(f' > present: {num_present}'.ljust(36), f'({get_present_folders.__doc__})') + print(f' > valid: {num_valid}'.ljust(36), f'({get_valid_folders.__doc__})') + + duplicate = get_duplicate_folders(links, out_dir=out_dir) + orphaned = get_orphaned_folders(links, out_dir=out_dir) + corrupted = get_corrupted_folders(links, out_dir=out_dir) + unrecognized = get_unrecognized_folders(links, out_dir=out_dir) + num_invalid = len({**duplicate, **orphaned, **corrupted, **unrecognized}) + print(f' > invalid: {num_invalid}'.ljust(36), f'({get_invalid_folders.__doc__})') + print(f' > duplicate: {len(duplicate)}'.ljust(36), f'({get_duplicate_folders.__doc__})') + print(f' > orphaned: {len(orphaned)}'.ljust(36), f'({get_orphaned_folders.__doc__})') + print(f' > corrupted: {len(corrupted)}'.ljust(36), f'({get_corrupted_folders.__doc__})') + print(f' > unrecognized: {len(unrecognized)}'.ljust(36), f'({get_unrecognized_folders.__doc__})') + + print(ANSI['reset']) + + if num_indexed: + print(' {lightred}Hint:{reset} You can list link data directories by status like so:'.format(**ANSI)) + print(' archivebox list --status=<status> (e.g. indexed, corrupted, archived, etc.)') + + if orphaned: + print(' {lightred}Hint:{reset} To automatically import orphaned data directories into the main index, run:'.format(**ANSI)) + print(' archivebox init') + + if num_invalid: + print(' {lightred}Hint:{reset} You may need to manually remove or fix some invalid data directories, afterwards make sure to run:'.format(**ANSI)) + print(' archivebox init') + + print() + print('{green}[*] Scanning recent archive changes and user logins:{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {LOGS_DIR}/*', ANSI['reset']) + users = get_admins().values_list('username', flat=True) + print(f' UI users {len(users)}: {", ".join(users)}') + last_login = User.objects.order_by('last_login').last() + if last_login: + print(f' Last UI login: {last_login.username} @ {str(last_login.last_login)[:16]}') + last_updated = Snapshot.objects.order_by('updated').last() + if last_updated: + print(f' Last changes: {str(last_updated.updated)[:16]}') + + if not users: + print() + print(' {lightred}Hint:{reset} You can create an admin user by running:'.format(**ANSI)) + print(' archivebox manage createsuperuser') + + print() + for snapshot in links.order_by('-updated')[:10]: + if not snapshot.updated: + continue + print( + ANSI['black'], + ( + f' > {str(snapshot.updated)[:16]} ' + f'[{snapshot.num_outputs} {("X", "√")[snapshot.is_archived]} {printable_filesize(snapshot.archive_size)}] ' + f'"{snapshot.title}": {snapshot.url}' + )[:TERM_WIDTH()], + ANSI['reset'], + ) + print(ANSI['black'], ' ...', ANSI['reset']) + + +@enforce_types +def oneshot(url: str, extractors: str="", out_dir: Path=OUTPUT_DIR): + """ + Create a single URL archive folder with an index.json and index.html, and all the archive method outputs. + You can run this to archive single pages without needing to create a whole collection with archivebox init. + """ + oneshot_link, _ = parse_links_memory([url]) + if len(oneshot_link) > 1: + stderr( + '[X] You should pass a single url to the oneshot command', + color='red' + ) + raise SystemExit(2) + + methods = extractors.split(",") if extractors else ignore_methods(['title']) + archive_link(oneshot_link[0], out_dir=out_dir, methods=methods) + return oneshot_link + +@enforce_types +def add(urls: Union[str, List[str]], + depth: int=0, + update_all: bool=not ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + init: bool=False, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Add a new URL or list of URLs to your archive""" + + assert depth in (0, 1), 'Depth must be 0 or 1 (depth >1 is not supported yet)' + + extractors = extractors.split(",") if extractors else [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # Load list of links from the existing index + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] + all_links = load_main_index(out_dir=out_dir) + + log_importing_started(urls=urls, depth=depth, index_only=index_only) + if isinstance(urls, str): + # save verbatim stdin to sources + write_ahead_log = save_text_as_source(urls, filename='{ts}-import.txt', out_dir=out_dir) + elif isinstance(urls, list): + # save verbatim args to sources + write_ahead_log = save_text_as_source('\n'.join(urls), filename='{ts}-import.txt', out_dir=out_dir) + + new_links += parse_links_from_source(write_ahead_log, root_url=None) + + # If we're going one level deeper, download each link and look for more links + new_links_depth = [] + if new_links and depth == 1: + log_crawl_started(new_links) + for new_link in new_links: + downloaded_file = save_file_as_source(new_link.url, filename=f'{new_link.timestamp}-crawl-{new_link.domain}.txt', out_dir=out_dir) + new_links_depth += parse_links_from_source(downloaded_file, root_url=new_link.url) + + imported_links = list({link.url: link for link in (new_links + new_links_depth)}.values()) + new_links = dedupe_links(all_links, imported_links) + + write_main_index(links=new_links, out_dir=out_dir) + all_links = load_main_index(out_dir=out_dir) + + if index_only: + return all_links + + # Run the archive methods for each link + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + if update_all: + archive_links(all_links, overwrite=overwrite, **archive_kwargs) + elif overwrite: + archive_links(imported_links, overwrite=True, **archive_kwargs) + elif new_links: + archive_links(new_links, overwrite=False, **archive_kwargs) + + return all_links + +@enforce_types +def remove(filter_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + snapshots: Optional[QuerySet]=None, + after: Optional[float]=None, + before: Optional[float]=None, + yes: bool=False, + delete: bool=False, + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Remove the specified URLs from the archive""" + + check_data_folder(out_dir=out_dir) + + if snapshots is None: + if filter_str and filter_patterns: + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif not (filter_str or filter_patterns): + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin.', + color='red', + ) + stderr() + hint(('To remove all urls you can run:', + 'archivebox remove --filter-type=regex ".*"')) + stderr() + raise SystemExit(2) + elif filter_str: + filter_patterns = [ptn.strip() for ptn in filter_str.split('\n')] + + list_kwargs = { + "filter_patterns": filter_patterns, + "filter_type": filter_type, + "after": after, + "before": before, + } + if snapshots: + list_kwargs["snapshots"] = snapshots + + log_list_started(filter_patterns, filter_type) + timer = TimedProgress(360, prefix=' ') + try: + snapshots = list_links(**list_kwargs) + finally: + timer.end() + + + if not snapshots.exists(): + log_removal_finished(0, 0) + raise SystemExit(1) + + + log_links = [link.as_link() for link in snapshots] + log_list_finished(log_links) + log_removal_started(log_links, yes=yes, delete=delete) + + timer = TimedProgress(360, prefix=' ') + try: + for snapshot in snapshots: + if delete: + shutil.rmtree(snapshot.as_link().link_dir, ignore_errors=True) + finally: + timer.end() + + to_remove = snapshots.count() + + flush_search_index(snapshots=snapshots) + remove_from_sql_main_index(snapshots=snapshots, out_dir=out_dir) + all_snapshots = load_main_index(out_dir=out_dir) + log_removal_finished(all_snapshots.count(), to_remove) + + return all_snapshots + +@enforce_types +def update(resume: Optional[float]=None, + only_new: bool=ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: Optional[str]=None, + status: Optional[str]=None, + after: Optional[str]=None, + before: Optional[str]=None, + extractors: str="", + out_dir: Path=OUTPUT_DIR) -> List[Link]: + """Import any new links from subscriptions and retry any previously failed/skipped links""" + + check_data_folder(out_dir=out_dir) + check_dependencies() + new_links: List[Link] = [] # TODO: Remove input argument: only_new + + extractors = extractors.split(",") if extractors else [] + + # Step 1: Filter for selected_links + matching_snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + matching_folders = list_folders( + links=matching_snapshots, + status=status, + out_dir=out_dir, + ) + all_links = [link for link in matching_folders.values() if link] + + if index_only: + for link in all_links: + write_link_details(link, out_dir=out_dir, skip_sql_index=True) + index_links(all_links, out_dir=out_dir) + return all_links + + # Step 2: Run the archive methods for each link + to_archive = new_links if only_new else all_links + if resume: + to_archive = [ + link for link in to_archive + if link.timestamp >= str(resume) + ] + if not to_archive: + stderr('') + stderr(f'[√] Nothing found to resume after {resume}', color='green') + return all_links + + archive_kwargs = { + "out_dir": out_dir, + } + if extractors: + archive_kwargs["methods"] = extractors + + archive_links(to_archive, overwrite=overwrite, **archive_kwargs) + + # Step 4: Re-write links index with updated titles, icons, and resources + all_links = load_main_index(out_dir=out_dir) + return all_links + +@enforce_types +def list_all(filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + status: Optional[str]=None, + after: Optional[float]=None, + before: Optional[float]=None, + sort: Optional[str]=None, + csv: Optional[str]=None, + json: bool=False, + html: bool=False, + with_headers: bool=False, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + """List, filter, and export information about archive entries""" + + check_data_folder(out_dir=out_dir) + + if filter_patterns and filter_patterns_str: + stderr( + '[X] You should either pass filter patterns as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif filter_patterns_str: + filter_patterns = filter_patterns_str.split('\n') + + snapshots = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + if sort: + snapshots = snapshots.order_by(sort) + + folders = list_folders( + links=snapshots, + status=status, + out_dir=out_dir, + ) + + if json: + output = generate_json_index_from_links(folders.values(), with_headers) + elif html: + output = generate_index_from_links(folders.values(), with_headers) + elif csv: + output = links_to_csv(folders.values(), cols=csv.split(','), header=with_headers) + else: + output = printable_folders(folders, with_headers=with_headers) + print(output) + return folders + + +@enforce_types +def list_links(snapshots: Optional[QuerySet]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + after: Optional[float]=None, + before: Optional[float]=None, + out_dir: Path=OUTPUT_DIR) -> Iterable[Link]: + + check_data_folder(out_dir=out_dir) + + if snapshots: + all_snapshots = snapshots + else: + all_snapshots = load_main_index(out_dir=out_dir) + + if after is not None: + all_snapshots = all_snapshots.filter(timestamp__lt=after) + if before is not None: + all_snapshots = all_snapshots.filter(timestamp__gt=before) + if filter_patterns: + all_snapshots = snapshot_filter(all_snapshots, filter_patterns, filter_type) + return all_snapshots + +@enforce_types +def list_folders(links: List[Link], + status: str, + out_dir: Path=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + + check_data_folder(out_dir=out_dir) + + STATUS_FUNCTIONS = { + "indexed": get_indexed_folders, + "archived": get_archived_folders, + "unarchived": get_unarchived_folders, + "present": get_present_folders, + "valid": get_valid_folders, + "invalid": get_invalid_folders, + "duplicate": get_duplicate_folders, + "orphaned": get_orphaned_folders, + "corrupted": get_corrupted_folders, + "unrecognized": get_unrecognized_folders, + } + + try: + return STATUS_FUNCTIONS[status](links, out_dir=out_dir) + except KeyError: + raise ValueError('Status not recognized.') + + +@enforce_types +def config(config_options_str: Optional[str]=None, + config_options: Optional[List[str]]=None, + get: bool=False, + set: bool=False, + reset: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Get and set your ArchiveBox project configuration values""" + + check_data_folder(out_dir=out_dir) + + if config_options and config_options_str: + stderr( + '[X] You should either pass config values as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif config_options_str: + config_options = config_options_str.split('\n') + + config_options = config_options or [] + + no_args = not (get or set or reset or config_options) + + matching_config: ConfigDict = {} + if get or no_args: + if config_options: + config_options = [get_real_name(key) for key in config_options] + matching_config = {key: CONFIG[key] for key in config_options if key in CONFIG} + failed_config = [key for key in config_options if key not in CONFIG] + if failed_config: + stderr() + stderr('[X] These options failed to get', color='red') + stderr(' {}'.format('\n '.join(config_options))) + raise SystemExit(1) + else: + matching_config = CONFIG + + print(printable_config(matching_config)) + raise SystemExit(not matching_config) + elif set: + new_config = {} + failed_options = [] + for line in config_options: + if line.startswith('#') or not line.strip(): + continue + if '=' not in line: + stderr('[X] Config KEY=VALUE must have an = sign in it', color='red') + stderr(f' {line}') + raise SystemExit(2) + + raw_key, val = line.split('=', 1) + raw_key = raw_key.upper().strip() + key = get_real_name(raw_key) + if key != raw_key: + stderr(f'[i] Note: The config option {raw_key} has been renamed to {key}, please use the new name going forwards.', color='lightyellow') + + if key in CONFIG: + new_config[key] = val.strip() + else: + failed_options.append(line) + + if new_config: + before = CONFIG + matching_config = write_config_file(new_config, out_dir=OUTPUT_DIR) + after = load_all_config() + print(printable_config(matching_config)) + + side_effect_changes: ConfigDict = {} + for key, val in after.items(): + if key in USER_CONFIG and (before[key] != after[key]) and (key not in matching_config): + side_effect_changes[key] = after[key] + + if side_effect_changes: + stderr() + stderr('[i] Note: This change also affected these other options that depended on it:', color='lightyellow') + print(' {}'.format(printable_config(side_effect_changes, prefix=' '))) + if failed_options: + stderr() + stderr('[X] These options failed to set (check for typos):', color='red') + stderr(' {}'.format('\n '.join(failed_options))) + raise SystemExit(bool(failed_options)) + elif reset: + stderr('[X] This command is not implemented yet.', color='red') + stderr(' Please manually remove the relevant lines from your config file:') + stderr(f' {CONFIG_FILE}') + raise SystemExit(2) + else: + stderr('[X] You must pass either --get or --set, or no arguments to get the whole config.', color='red') + stderr(' archivebox config') + stderr(' archivebox config --get SOME_KEY') + stderr(' archivebox config --set SOME_KEY=SOME_VALUE') + raise SystemExit(2) + + +@enforce_types +def schedule(add: bool=False, + show: bool=False, + clear: bool=False, + foreground: bool=False, + run_all: bool=False, + quiet: bool=False, + every: Optional[str]=None, + depth: int=0, + import_path: Optional[str]=None, + out_dir: Path=OUTPUT_DIR): + """Set ArchiveBox to regularly import URLs at specific times using cron""" + + check_data_folder(out_dir=out_dir) + + (Path(out_dir) / LOGS_DIR_NAME).mkdir(exist_ok=True) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + + if clear: + print(cron.remove_all(comment=CRON_COMMENT)) + cron.write() + raise SystemExit(0) + + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if every or add: + every = every or 'day' + quoted = lambda s: f'"{s}"' if s and ' ' in str(s) else str(s) + cmd = [ + 'cd', + quoted(out_dir), + '&&', + quoted(ARCHIVEBOX_BINARY), + *(['add', f'--depth={depth}', f'"{import_path}"'] if import_path else ['update']), + '>', + quoted(Path(LOGS_DIR) / 'archivebox.log'), + '2>&1', + + ] + new_job = cron.new(command=' '.join(cmd), comment=CRON_COMMENT) + + if every in ('minute', 'hour', 'day', 'month', 'year'): + set_every = getattr(new_job.every(), every) + set_every() + elif CronSlices.is_valid(every): + new_job.setall(every) + else: + stderr('{red}[X] Got invalid timeperiod for cron task.{reset}'.format(**ANSI)) + stderr(' It must be one of minute/hour/day/month') + stderr(' or a quoted cron-format schedule like:') + stderr(' archivebox init --every=day https://example.com/some/rss/feed.xml') + stderr(' archivebox init --every="0/5 * * * *" https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + cron = dedupe_cron_jobs(cron) + cron.write() + + total_runs = sum(j.frequency_per_year() for j in cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + print() + print('{green}[√] Scheduled new ArchiveBox cron job for user: {} ({} jobs are active).{reset}'.format(USER, len(existing_jobs), **ANSI)) + print('\n'.join(f' > {cmd}' if str(cmd) == str(new_job) else f' {cmd}' for cmd in existing_jobs)) + if total_runs > 60 and not quiet: + stderr() + stderr('{lightyellow}[!] With the current cron config, ArchiveBox is estimated to run >{} times per year.{reset}'.format(total_runs, **ANSI)) + stderr(' Congrats on being an enthusiastic internet archiver! 👌') + stderr() + stderr(' Make sure you have enough storage space available to hold all the data.') + stderr(' Using a compressed/deduped filesystem like ZFS is recommended if you plan on archiving a lot.') + stderr('') + elif show: + if existing_jobs: + print('\n'.join(str(cmd) for cmd in existing_jobs)) + else: + stderr('{red}[X] There are no ArchiveBox cron jobs scheduled for your user ({}).{reset}'.format(USER, **ANSI)) + stderr(' To schedule a new job, run:') + stderr(' archivebox schedule --every=[timeperiod] https://example.com/some/rss/feed.xml') + raise SystemExit(0) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + if foreground or run_all: + if not existing_jobs: + stderr('{red}[X] You must schedule some jobs first before running in foreground mode.{reset}'.format(**ANSI)) + stderr(' archivebox schedule --every=hour https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + print('{green}[*] Running {} ArchiveBox jobs in foreground task scheduler...{reset}'.format(len(existing_jobs), **ANSI)) + if run_all: + try: + for job in existing_jobs: + sys.stdout.write(f' > {job.command.split("/archivebox ")[0].split(" && ")[0]}\n') + sys.stdout.write(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + sys.stdout.flush() + job.run() + sys.stdout.write(f'\r √ {job.command.split("/archivebox ")[-1]}\n') + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + if foreground: + try: + for job in existing_jobs: + print(f' > {job.command.split("/archivebox ")[-1].split(" > ")[0]}') + for result in cron.run_scheduler(): + print(result) + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + +@enforce_types +def server(runserver_args: Optional[List[str]]=None, + reload: bool=False, + debug: bool=False, + init: bool=False, + out_dir: Path=OUTPUT_DIR) -> None: + """Run the ArchiveBox HTTP server""" + + runserver_args = runserver_args or [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # setup config for django runserver + from . import config + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + from django.contrib.auth.models import User + + admin_user = User.objects.filter(is_superuser=True).order_by('date_joined').only('username').last() + + print('{green}[+] Starting ArchiveBox webserver...{reset}'.format(**ANSI)) + if admin_user: + hint('The admin username is{lightblue} {}{reset}\n'.format(admin_user.username, **ANSI)) + else: + print('{lightyellow}[!] No admin users exist yet, you will not be able to edit links in the UI.{reset}'.format(**ANSI)) + print() + print(' To create an admin user, run:') + print(' archivebox manage createsuperuser') + print() + + # fallback to serving staticfiles insecurely with django when DEBUG=False + if not config.DEBUG: + runserver_args.append('--insecure') # TODO: serve statics w/ nginx instead + + # toggle autoreloading when archivebox code changes (it's on by default) + if not reload: + runserver_args.append('--noreload') + + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + + call_command("runserver", *runserver_args) + + +@enforce_types +def manage(args: Optional[List[str]]=None, out_dir: Path=OUTPUT_DIR) -> None: + """Run an ArchiveBox Django management command""" + + check_data_folder(out_dir=out_dir) + from django.core.management import execute_from_command_line + + if (args and "createsuperuser" in args) and (IN_DOCKER and not IS_TTY): + stderr('[!] Warning: you need to pass -it to use interactive commands in docker', color='lightyellow') + stderr(' docker run -it archivebox manage {}'.format(' '.join(args or ['...'])), color='lightyellow') + stderr() + + execute_from_command_line([f'{ARCHIVEBOX_BINARY} manage', *(args or ['help'])]) + + +@enforce_types +def shell(out_dir: Path=OUTPUT_DIR) -> None: + """Enter an interactive ArchiveBox Django shell""" + + check_data_folder(out_dir=out_dir) + + from django.core.management import call_command + call_command("shell_plus") + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/manage.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/manage.py new file mode 100644 index 0000000..1a9b297 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + # if you're a developer working on archivebox, still prefer the archivebox + # versions of ./manage.py commands whenever possible. When that's not possible + # (e.g. makemigrations), you can comment out this check temporarily + + if not ('makemigrations' in sys.argv or 'migrate' in sys.argv): + print("[X] Don't run ./manage.py directly (unless you are a developer running makemigrations):") + print() + print(' Hint: Use these archivebox CLI commands instead of the ./manage.py equivalents:') + print(' archivebox init (migrates the databse to latest version)') + print(' archivebox server (runs the Django web server)') + print(' archivebox shell (opens an iPython Django shell with all models imported)') + print(' archivebox manage [cmd] (any other management commands)') + raise SystemExit(2) + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/mypy.ini b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/mypy.ini new file mode 100644 index 0000000..b1b4489 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +plugins = + mypy_django_plugin.main diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/package.json b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/package.json new file mode 100644 index 0000000..7f8bf66 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/package.json @@ -0,0 +1,21 @@ +{ + "name": "archivebox", + "version": "0.5.3", + "description": "ArchiveBox: The self-hosted internet archive", + "author": "Nick Sweeting <archivebox-npm@sweeting.me>", + "license": "MIT", + "scripts": { + "archivebox": "./bin/archive" + }, + "bin": { + "archivebox-node": "./bin/archive", + "single-file": "./node_modules/.bin/single-file", + "readability-extractor": "./node_modules/.bin/readability-extractor", + "mercury-parser": "./node_modules/.bin/mercury-parser" + }, + "dependencies": { + "@postlight/mercury-parser": "^2.2.0", + "readability-extractor": "git+https://github.com/pirate/readability-extractor.git", + "single-file": "git+https://github.com/gildas-lormeau/SingleFile.git" + } +} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/__init__.py new file mode 100644 index 0000000..441c08a --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/__init__.py @@ -0,0 +1,203 @@ +""" +Everything related to parsing links from input sources. + +For a list of supported services, see the README.md. +For examples of supported import formats see tests/. +""" + +__package__ = 'archivebox.parsers' + +import re +from io import StringIO + +from typing import IO, Tuple, List, Optional +from datetime import datetime +from pathlib import Path + +from ..system import atomic_write +from ..config import ( + ANSI, + OUTPUT_DIR, + SOURCES_DIR_NAME, + TIMEOUT, +) +from ..util import ( + basename, + htmldecode, + download_url, + enforce_types, + URL_REGEX, +) +from ..index.schema import Link +from ..logging_util import TimedProgress, log_source_saved + +from .pocket_html import parse_pocket_html_export +from .pocket_api import parse_pocket_api_export +from .pinboard_rss import parse_pinboard_rss_export +from .wallabag_atom import parse_wallabag_atom_export +from .shaarli_rss import parse_shaarli_rss_export +from .medium_rss import parse_medium_rss_export +from .netscape_html import parse_netscape_html_export +from .generic_rss import parse_generic_rss_export +from .generic_json import parse_generic_json_export +from .generic_html import parse_generic_html_export +from .generic_txt import parse_generic_txt_export + +PARSERS = ( + # Specialized parsers + ('Pocket API', parse_pocket_api_export), + ('Wallabag ATOM', parse_wallabag_atom_export), + ('Pocket HTML', parse_pocket_html_export), + ('Pinboard RSS', parse_pinboard_rss_export), + ('Shaarli RSS', parse_shaarli_rss_export), + ('Medium RSS', parse_medium_rss_export), + + # General parsers + ('Netscape HTML', parse_netscape_html_export), + ('Generic RSS', parse_generic_rss_export), + ('Generic JSON', parse_generic_json_export), + ('Generic HTML', parse_generic_html_export), + + # Fallback parser + ('Plain Text', parse_generic_txt_export), +) + + +@enforce_types +def parse_links_memory(urls: List[str], root_url: Optional[str]=None): + """ + parse a list of URLS without touching the filesystem + """ + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + #urls = list(map(lambda x: x + "\n", urls)) + file = StringIO() + file.writelines(urls) + file.name = "io_string" + links, parser = run_parser_functions(file, timer, root_url=root_url) + timer.end() + + if parser is None: + return [], 'Failed to parse' + return links, parser + + +@enforce_types +def parse_links(source_file: str, root_url: Optional[str]=None) -> Tuple[List[Link], str]: + """parse a list of URLs with their metadata from an + RSS feed, bookmarks export, or text file + """ + + check_url_parsing_invariants() + + timer = TimedProgress(TIMEOUT * 4) + with open(source_file, 'r', encoding='utf-8') as file: + links, parser = run_parser_functions(file, timer, root_url=root_url) + + timer.end() + if parser is None: + return [], 'Failed to parse' + return links, parser + + +def run_parser_functions(to_parse: IO[str], timer, root_url: Optional[str]=None) -> Tuple[List[Link], Optional[str]]: + most_links: List[Link] = [] + best_parser_name = None + + for parser_name, parser_func in PARSERS: + try: + parsed_links = list(parser_func(to_parse, root_url=root_url)) + if not parsed_links: + raise Exception('no links found') + + # print(f'[√] Parser {parser_name} succeeded: {len(parsed_links)} links parsed') + if len(parsed_links) > len(most_links): + most_links = parsed_links + best_parser_name = parser_name + + except Exception as err: # noqa + # Parsers are tried one by one down the list, and the first one + # that succeeds is used. To see why a certain parser was not used + # due to error or format incompatibility, uncomment this line: + + # print('[!] Parser {} failed: {} {}'.format(parser_name, err.__class__.__name__, err)) + # raise + pass + timer.end() + return most_links, best_parser_name + + +@enforce_types +def save_text_as_source(raw_text: str, filename: str='{ts}-stdin.txt', out_dir: Path=OUTPUT_DIR) -> str: + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(out_dir / SOURCES_DIR_NAME / filename.format(ts=ts)) + atomic_write(source_path, raw_text) + log_source_saved(source_file=source_path) + return source_path + + +@enforce_types +def save_file_as_source(path: str, timeout: int=TIMEOUT, filename: str='{ts}-{basename}.txt', out_dir: Path=OUTPUT_DIR) -> str: + """download a given url's content into output/sources/domain-<timestamp>.txt""" + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = str(OUTPUT_DIR / SOURCES_DIR_NAME / filename.format(basename=basename(path), ts=ts)) + + if any(path.startswith(s) for s in ('http://', 'https://', 'ftp://')): + # Source is a URL that needs to be downloaded + print(f' > Downloading {path} contents') + timer = TimedProgress(timeout, prefix=' ') + try: + raw_source_text = download_url(path, timeout=timeout) + raw_source_text = htmldecode(raw_source_text) + timer.end() + except Exception as e: + timer.end() + print('{}[!] Failed to download {}{}\n'.format( + ANSI['red'], + path, + ANSI['reset'], + )) + print(' ', e) + raise SystemExit(1) + + else: + # Source is a path to a local file on the filesystem + with open(path, 'r') as f: + raw_source_text = f.read() + + atomic_write(source_path, raw_source_text) + + log_source_saved(source_file=source_path) + + return source_path + + +def check_url_parsing_invariants() -> None: + """Check that plain text regex URL parsing works as expected""" + + # this is last-line-of-defense to make sure the URL_REGEX isn't + # misbehaving, as the consequences could be disastrous and lead to many + # incorrect/badly parsed links being added to the archive + + test_urls = ''' + https://example1.com/what/is/happening.html?what=1#how-about-this=1 + https://example2.com/what/is/happening/?what=1#how-about-this=1 + HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f + https://example4.com/what/is/happening.html + https://example5.com/ + https://example6.com + + <test>http://example7.com</test> + [https://example8.com/what/is/this.php?what=1] + [and http://example9.com?what=1&other=3#and-thing=2] + <what>https://example10.com#and-thing=2 "</about> + abc<this["https://example11.com/what/is#and-thing=2?whoami=23&where=1"]that>def + sdflkf[what](https://example12.com/who/what.php?whoami=1#whatami=2)?am=hi + example13.bada + and example14.badb + <or>htt://example15.badc</that> + ''' + # print('\n'.join(re.findall(URL_REGEX, test_urls))) + assert len(re.findall(URL_REGEX, test_urls)) == 12 + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_html.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_html.py new file mode 100644 index 0000000..74b3d1f --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_html.py @@ -0,0 +1,53 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + URL_REGEX, +) +from html.parser import HTMLParser +from urllib.parse import urljoin + + +class HrefParser(HTMLParser): + def __init__(self): + super().__init__() + self.urls = [] + + def handle_starttag(self, tag, attrs): + if tag == "a": + for attr, value in attrs: + if attr == "href": + self.urls.append(value) + + +@enforce_types +def parse_generic_html_export(html_file: IO[str], root_url: Optional[str]=None, **_kwargs) -> Iterable[Link]: + """Parse Generic HTML for href tags and use only the url (support for title coming later)""" + + html_file.seek(0) + for line in html_file: + parser = HrefParser() + # example line + # <li><a href="http://example.com/ time_added="1478739709" tags="tag1,tag2">example title</a></li> + parser.feed(line) + for url in parser.urls: + if root_url: + # resolve relative urls /home.html -> https://example.com/home.html + url = urljoin(root_url, url) + + for archivable_url in re.findall(URL_REGEX, url): + yield Link( + url=htmldecode(archivable_url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_json.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_json.py new file mode 100644 index 0000000..e6ed677 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_json.py @@ -0,0 +1,65 @@ +__package__ = 'archivebox.parsers' + +import json + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_generic_json_export(json_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)""" + + json_file.seek(0) + links = json.load(json_file) + json_date = lambda s: datetime.strptime(s, '%Y-%m-%dT%H:%M:%S%z') + + for link in links: + # example line + # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}] + if link: + # Parse URL + url = link.get('href') or link.get('url') or link.get('URL') + if not url: + raise Exception('JSON must contain URL in each entry [{"url": "http://...", ...}, ...]') + + # Parse the timestamp + ts_str = str(datetime.now().timestamp()) + if link.get('timestamp'): + # chrome/ff histories use a very precise timestamp + ts_str = str(link['timestamp'] / 10000000) + elif link.get('time'): + ts_str = str(json_date(link['time'].split(',', 1)[0]).timestamp()) + elif link.get('created_at'): + ts_str = str(json_date(link['created_at']).timestamp()) + elif link.get('created'): + ts_str = str(json_date(link['created']).timestamp()) + elif link.get('date'): + ts_str = str(json_date(link['date']).timestamp()) + elif link.get('bookmarked'): + ts_str = str(json_date(link['bookmarked']).timestamp()) + elif link.get('saved'): + ts_str = str(json_date(link['saved']).timestamp()) + + # Parse the title + title = None + if link.get('title'): + title = link['title'].strip() + elif link.get('description'): + title = link['description'].replace(' — Readability', '').strip() + elif link.get('name'): + title = link['name'].strip() + + yield Link( + url=htmldecode(url), + timestamp=ts_str, + title=htmldecode(title) or None, + tags=htmldecode(link.get('tags')) or '', + sources=[json_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_rss.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_rss.py new file mode 100644 index 0000000..2831844 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/generic_rss.py @@ -0,0 +1,49 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + +@enforce_types +def parse_generic_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse RSS XML-format files into links""" + + rss_file.seek(0) + items = rss_file.read().split('<item>') + items = items[1:] if items else [] + for item in items: + # example item: + # <item> + # <title><![CDATA[How JavaScript works: inside the V8 engine]]> + # Unread + # https://blog.sessionstack.com/how-javascript-works-inside + # https://blog.sessionstack.com/how-javascript-works-inside + # Mon, 21 Aug 2017 14:21:58 -0500 + # + + trailing_removed = item.split('', 1)[0] + leading_removed = trailing_removed.split('', 1)[-1].strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r for r in rows if r.strip().startswith('<{}>'.format(key))][0] + + url = str_between(get_row('link'), '', '') + ts_str = str_between(get_row('pubDate'), '', '') + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z") + title = str_between(get_row('title'), ' Iterable[Link]: + """Parse raw links from each line in a text file""" + + text_file.seek(0) + for line in text_file.readlines(): + if not line.strip(): + continue + + # if the line is a local file path that resolves, then we can archive it + try: + if Path(line).exists(): + yield Link( + url=line, + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + except (OSError, PermissionError): + # nvm, not a valid path... + pass + + # otherwise look for anything that looks like a URL in the line + for url in re.findall(URL_REGEX, line): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + + # look inside the URL for any sub-urls, e.g. for archive.org links + # https://web.archive.org/web/20200531203453/https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + # -> https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + for url in re.findall(URL_REGEX, line[1:]): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/medium_rss.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/medium_rss.py new file mode 100644 index 0000000..8f14f77 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/medium_rss.py @@ -0,0 +1,35 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_medium_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Medium RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.find("channel").findall("item") # type: ignore + for item in items: + url = item.find("link").text # type: ignore + title = item.find("title").text.strip() # type: ignore + ts_str = item.find("pubDate").text # type: ignore + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z") # type: ignore + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/netscape_html.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/netscape_html.py new file mode 100644 index 0000000..a063023 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/netscape_html.py @@ -0,0 +1,39 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_netscape_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse netscape-format bookmarks export files (produced by all browsers)""" + + html_file.seek(0) + pattern = re.compile("]*>(.+)", re.UNICODE | re.IGNORECASE) + for line in html_file: + # example line + #
    example bookmark title + + match = pattern.search(line) + if match: + url = match.group(1) + time = datetime.fromtimestamp(float(match.group(2))) + title = match.group(3).strip() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[html_file.name], + ) + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pinboard_rss.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pinboard_rss.py new file mode 100644 index 0000000..98ff14a --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pinboard_rss.py @@ -0,0 +1,47 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pinboard_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pinboard RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.findall("{http://purl.org/rss/1.0/}item") + for item in items: + find = lambda p: item.find(p).text.strip() if item.find(p) else None # type: ignore + + url = find("{http://purl.org/rss/1.0/}link") + tags = find("{http://purl.org/dc/elements/1.1/}subject") + title = find("{http://purl.org/rss/1.0/}title") + ts_str = find("{http://purl.org/dc/elements/1.1/}date") + + # Pinboard includes a colon in its date stamp timezone offsets, which + # Python can't parse. Remove it: + if ts_str and ts_str[-3:-2] == ":": + ts_str = ts_str[:-3]+ts_str[-2:] + + if ts_str: + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + else: + time = datetime.now() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=htmldecode(tags) or None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_api.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_api.py new file mode 100644 index 0000000..bf3a292 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_api.py @@ -0,0 +1,113 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable, Optional +from configparser import ConfigParser + +from pathlib import Path +from ..vendor.pocket import Pocket + +from ..index.schema import Link +from ..util import enforce_types +from ..system import atomic_write +from ..config import ( + SOURCES_DIR, + POCKET_CONSUMER_KEY, + POCKET_ACCESS_TOKENS, +) + + +COUNT_PER_PAGE = 500 +API_DB_PATH = Path(SOURCES_DIR) / 'pocket_api.db' + +# search for broken protocols that sometimes come from the Pocket API +_BROKEN_PROTOCOL_RE = re.compile('^(http[s]?)(:/(?!/))') + + +def get_pocket_articles(api: Pocket, since=None, page=0): + body, headers = api.get( + state='archive', + sort='oldest', + since=since, + count=COUNT_PER_PAGE, + offset=page * COUNT_PER_PAGE, + ) + + articles = body['list'].values() if isinstance(body['list'], dict) else body['list'] + returned_count = len(articles) + + yield from articles + + if returned_count == COUNT_PER_PAGE: + yield from get_pocket_articles(api, since=since, page=page + 1) + else: + api.last_since = body['since'] + + +def link_from_article(article: dict, sources: list): + url: str = article['resolved_url'] or article['given_url'] + broken_protocol = _BROKEN_PROTOCOL_RE.match(url) + if broken_protocol: + url = url.replace(f'{broken_protocol.group(1)}:/', f'{broken_protocol.group(1)}://') + title = article['resolved_title'] or article['given_title'] or url + + return Link( + url=url, + timestamp=article['time_read'], + title=title, + tags=article.get('tags'), + sources=sources + ) + + +def write_since(username: str, since: str): + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + since_file = ConfigParser() + since_file.optionxform = str + since_file.read(API_DB_PATH) + + since_file[username] = { + 'since': since + } + + with open(API_DB_PATH, 'w+') as new: + since_file.write(new) + + +def read_since(username: str) -> Optional[str]: + if not API_DB_PATH.exists(): + atomic_write(API_DB_PATH, '') + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(API_DB_PATH) + + return config_file.get(username, 'since', fallback=None) + + +@enforce_types +def should_parse_as_pocket_api(text: str) -> bool: + return text.startswith('pocket://') + + +@enforce_types +def parse_pocket_api_export(input_buffer: IO[str], **_kwargs) -> Iterable[Link]: + """Parse bookmarks from the Pocket API""" + + input_buffer.seek(0) + pattern = re.compile(r"^pocket:\/\/(\w+)") + for line in input_buffer: + if should_parse_as_pocket_api(line): + + username = pattern.search(line).group(1) + api = Pocket(POCKET_CONSUMER_KEY, POCKET_ACCESS_TOKENS[username]) + api.last_since = None + + for article in get_pocket_articles(api, since=read_since(username)): + yield link_from_article(article, sources=[line]) + + write_since(username, api.last_since) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_html.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_html.py new file mode 100644 index 0000000..653f21b --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/pocket_html.py @@ -0,0 +1,38 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pocket_html_export(html_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)""" + + html_file.seek(0) + pattern = re.compile("^\\s*
  • (.+)
  • ", re.UNICODE) + for line in html_file: + # example line + #
  • example title
  • + match = pattern.search(line) + if match: + url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url + time = datetime.fromtimestamp(float(match.group(2))) + tags = match.group(3) + title = match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[html_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/shaarli_rss.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/shaarli_rss.py new file mode 100644 index 0000000..4a925f4 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/shaarli_rss.py @@ -0,0 +1,50 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_shaarli_rss_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Shaarli-specific RSS XML-format files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # Aktuelle Trojaner-Welle: Emotet lauert in gefälschten Rechnungsmails | heise online + # + # https://demo.shaarli.org/?cEV4vw + # 2019-01-30T06:06:01+00:00 + # 2019-01-30T06:06:01+00:00 + #

    Permalink

    ]]>
    + #
    + + trailing_removed = entry.split('
    ', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '', '').strip() + url = str_between(get_row('link'), '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/wallabag_atom.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/wallabag_atom.py new file mode 100644 index 0000000..0d77869 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/parsers/wallabag_atom.py @@ -0,0 +1,57 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_wallabag_atom_export(rss_file: IO[str], **_kwargs) -> Iterable[Link]: + """Parse Wallabag Atom files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # <![CDATA[Orient Ray vs Mako: Is There Much Difference? - iknowwatches.com]]> + # + # https://iknowwatches.com/orient-ray-vs-mako/ + # wallabag:wallabag.drycat.fr:milosh:entry:14041 + # 2020-10-18T09:14:02+02:00 + # 2020-10-18T09:13:56+02:00 + # + # + # + + trailing_removed = entry.split('', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '<![CDATA[', ']]>').strip() + url = str_between(get_row('link rel="via"'), '', '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + try: + tags = str_between(get_row('category'), 'label="', '" />') + except: + tags = None + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[rss_file.name], + ) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/__init__.py new file mode 100644 index 0000000..6191ede --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/__init__.py @@ -0,0 +1,108 @@ +from typing import List, Union +from pathlib import Path +from importlib import import_module + +from django.db.models import QuerySet + +from archivebox.index.schema import Link +from archivebox.util import enforce_types +from archivebox.config import stderr, OUTPUT_DIR, USE_INDEXING_BACKEND, USE_SEARCHING_BACKEND, SEARCH_BACKEND_ENGINE + +from .utils import get_indexable_content, log_index_started + +def indexing_enabled(): + return USE_INDEXING_BACKEND + +def search_backend_enabled(): + return USE_SEARCHING_BACKEND + +def get_backend(): + return f'search.backends.{SEARCH_BACKEND_ENGINE}' + +def import_backend(): + backend_string = get_backend() + try: + backend = import_module(backend_string) + except Exception as err: + raise Exception("Could not load '%s' as a backend: %s" % (backend_string, err)) + return backend + +@enforce_types +def write_search_index(link: Link, texts: Union[List[str], None]=None, out_dir: Path=OUTPUT_DIR, skip_text_index: bool=False) -> None: + if not indexing_enabled(): + return + + if not skip_text_index and texts: + from core.models import Snapshot + + snap = Snapshot.objects.filter(url=link.url).first() + backend = import_backend() + if snap: + try: + backend.index(snapshot_id=str(snap.id), texts=texts) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def query_search_index(query: str, out_dir: Path=OUTPUT_DIR) -> QuerySet: + from core.models import Snapshot + + if search_backend_enabled(): + backend = import_backend() + try: + snapshot_ids = backend.search(query) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + raise + else: + # TODO preserve ordering from backend + qsearch = Snapshot.objects.filter(pk__in=snapshot_ids) + return qsearch + + return Snapshot.objects.none() + +@enforce_types +def flush_search_index(snapshots: QuerySet): + if not indexing_enabled() or not snapshots: + return + backend = import_backend() + snapshot_ids=(str(pk) for pk in snapshots.values_list('pk',flat=True)) + try: + backend.flush(snapshot_ids) + except Exception as err: + stderr() + stderr( + f'[X] The search backend threw an exception={err}:', + color='red', + ) + +@enforce_types +def index_links(links: Union[List[Link],None], out_dir: Path=OUTPUT_DIR): + if not links: + return + + from core.models import Snapshot, ArchiveResult + + for link in links: + snap = Snapshot.objects.filter(url=link.url).first() + if snap: + results = ArchiveResult.objects.indexable().filter(snapshot=snap) + log_index_started(link.url) + try: + texts = get_indexable_content(results) + except Exception as err: + stderr() + stderr( + f'[X] An Exception ocurred reading the indexable content={err}:', + color='red', + ) + else: + write_search_index(link, texts, out_dir=out_dir) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/ripgrep.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/ripgrep.py new file mode 100644 index 0000000..840d2d2 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/ripgrep.py @@ -0,0 +1,45 @@ +import re +from subprocess import run, PIPE +from typing import List, Generator + +from archivebox.config import ARCHIVE_DIR, RIPGREP_VERSION +from archivebox.util import enforce_types + +RG_IGNORE_EXTENSIONS = ('css','js','orig','svg') + +RG_ADD_TYPE = '--type-add' +RG_IGNORE_ARGUMENTS = f"ignore:*.{{{','.join(RG_IGNORE_EXTENSIONS)}}}" +RG_DEFAULT_ARGUMENTS = "-ilTignore" # Case insensitive(i), matching files results(l) +RG_REGEX_ARGUMENT = '-e' + +TIMESTAMP_REGEX = r'\/([\d]+\.[\d]+)\/' + +ts_regex = re.compile(TIMESTAMP_REGEX) + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + return + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + return + +@enforce_types +def search(text: str) -> List[str]: + if not RIPGREP_VERSION: + raise Exception("ripgrep binary not found, install ripgrep to use this search backend") + + from core.models import Snapshot + + rg_cmd = ['rg', RG_ADD_TYPE, RG_IGNORE_ARGUMENTS, RG_DEFAULT_ARGUMENTS, RG_REGEX_ARGUMENT, text, str(ARCHIVE_DIR)] + rg = run(rg_cmd, stdout=PIPE, stderr=PIPE, timeout=60) + file_paths = [p.decode() for p in rg.stdout.splitlines()] + timestamps = set() + for path in file_paths: + ts = ts_regex.findall(path) + if ts: + timestamps.add(ts[0]) + + snap_ids = [str(id) for id in Snapshot.objects.filter(timestamp__in=timestamps).values_list('pk', flat=True)] + + return snap_ids diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/sonic.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/sonic.py new file mode 100644 index 0000000..f0beadd --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/backends/sonic.py @@ -0,0 +1,28 @@ +from typing import List, Generator + +from sonic import IngestClient, SearchClient + +from archivebox.util import enforce_types +from archivebox.config import SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD, SONIC_BUCKET, SONIC_COLLECTION + +MAX_SONIC_TEXT_LENGTH = 20000 + +@enforce_types +def index(snapshot_id: str, texts: List[str]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for text in texts: + chunks = [text[i:i+MAX_SONIC_TEXT_LENGTH] for i in range(0, len(text), MAX_SONIC_TEXT_LENGTH)] + for chunk in chunks: + ingestcl.push(SONIC_COLLECTION, SONIC_BUCKET, snapshot_id, str(chunk)) + +@enforce_types +def search(text: str) -> List[str]: + with SearchClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as querycl: + snap_ids = querycl.query(SONIC_COLLECTION, SONIC_BUCKET, text) + return snap_ids + +@enforce_types +def flush(snapshot_ids: Generator[str, None, None]): + with IngestClient(SEARCH_BACKEND_HOST_NAME, SEARCH_BACKEND_PORT, SEARCH_BACKEND_PASSWORD) as ingestcl: + for id in snapshot_ids: + ingestcl.flush_object(SONIC_COLLECTION, SONIC_BUCKET, str(id)) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/utils.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/utils.py new file mode 100644 index 0000000..55c97e7 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/search/utils.py @@ -0,0 +1,44 @@ +from django.db.models import QuerySet + +from archivebox.util import enforce_types +from archivebox.config import ANSI + +def log_index_started(url): + print('{green}[*] Indexing url: {} in the search index {reset}'.format(url, **ANSI)) + print( ) + +def get_file_result_content(res, extra_path, use_pwd=False): + if use_pwd: + fpath = f'{res.pwd}/{res.output}' + else: + fpath = f'{res.output}' + + if extra_path: + fpath = f'{fpath}/{extra_path}' + + with open(fpath, 'r') as file: + data = file.read() + if data: + return [data] + return [] + + +# This should be abstracted by a plugin interface for extractors +@enforce_types +def get_indexable_content(results: QuerySet): + if not results: + return [] + # Only use the first method available + res, method = results.first(), results.first().extractor + if method not in ('readability', 'singlefile', 'dom', 'wget'): + return [] + # This should come from a plugin interface + + if method == 'readability': + return get_file_result_content(res, 'content.txt') + elif method == 'singlefile': + return get_file_result_content(res, '') + elif method == 'dom': + return get_file_result_content(res,'',use_pwd=True) + elif method == 'wget': + return get_file_result_content(res,'',use_pwd=True) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/system.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/system.py new file mode 100644 index 0000000..b27c5e4 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/system.py @@ -0,0 +1,163 @@ +__package__ = 'archivebox' + + +import os +import shutil + +from json import dump +from pathlib import Path +from typing import Optional, Union, Set, Tuple +from subprocess import run as subprocess_run + +from crontab import CronTab +from atomicwrites import atomic_write as lib_atomic_write + +from .util import enforce_types, ExtendedEncoder +from .config import OUTPUT_PERMISSIONS + + + +def run(*args, input=None, capture_output=True, text=False, **kwargs): + """Patched of subprocess.run to fix blocking io making timeout=innefective""" + + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + + if capture_output: + if ('stdout' in kwargs) or ('stderr' in kwargs): + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + + return subprocess_run(*args, input=input, capture_output=capture_output, text=text, **kwargs) + + +@enforce_types +def atomic_write(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True) -> None: + """Safe atomic write to filesystem by writing to temp file + atomic rename""" + + mode = 'wb+' if isinstance(contents, bytes) else 'w' + + # print('\n> Atomic Write:', mode, path, len(contents), f'overwrite={overwrite}') + try: + with lib_atomic_write(path, mode=mode, overwrite=overwrite) as f: + if isinstance(contents, dict): + dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) + elif isinstance(contents, (bytes, str)): + f.write(contents) + except OSError as e: + print(f"[X] OSError: Failed to write {path} with fcntl.F_FULLFSYNC. ({e})") + print(" For data integrity, ArchiveBox requires a filesystem that supports atomic writes.") + print(" Filesystems and network drives that don't implement FSYNC are incompatible and require workarounds.") + raise SystemExit(1) + os.chmod(path, int(OUTPUT_PERMISSIONS, base=8)) + +@enforce_types +def chmod_file(path: str, cwd: str='.', permissions: str=OUTPUT_PERMISSIONS) -> None: + """chmod -R /""" + + root = Path(cwd) / path + if not root.exists(): + raise Exception('Failed to chmod: {} does not exist (did the previous step fail?)'.format(path)) + + if not root.is_dir(): + os.chmod(root, int(OUTPUT_PERMISSIONS, base=8)) + else: + for subpath in Path(path).glob('**/*'): + os.chmod(subpath, int(OUTPUT_PERMISSIONS, base=8)) + + +@enforce_types +def copy_and_overwrite(from_path: Union[str, Path], to_path: Union[str, Path]): + """copy a given file or directory to a given path, overwriting the destination""" + if Path(from_path).is_dir(): + shutil.rmtree(to_path, ignore_errors=True) + shutil.copytree(from_path, to_path) + else: + with open(from_path, 'rb') as src: + contents = src.read() + atomic_write(to_path, contents) + + +@enforce_types +def get_dir_size(path: Union[str, Path], recursive: bool=True, pattern: Optional[str]=None) -> Tuple[int, int, int]: + """get the total disk size of a given directory, optionally summing up + recursively and limiting to a given filter list + """ + num_bytes, num_dirs, num_files = 0, 0, 0 + for entry in os.scandir(path): + if (pattern is not None) and (pattern not in entry.path): + continue + if entry.is_dir(follow_symlinks=False): + if not recursive: + continue + num_dirs += 1 + bytes_inside, dirs_inside, files_inside = get_dir_size(entry.path) + num_bytes += bytes_inside + num_dirs += dirs_inside + num_files += files_inside + else: + num_bytes += entry.stat(follow_symlinks=False).st_size + num_files += 1 + return num_bytes, num_dirs, num_files + + +CRON_COMMENT = 'archivebox_schedule' + + +@enforce_types +def dedupe_cron_jobs(cron: CronTab) -> CronTab: + deduped: Set[Tuple[str, str]] = set() + + for job in list(cron): + unique_tuple = (str(job.slices), job.command) + if unique_tuple not in deduped: + deduped.add(unique_tuple) + cron.remove(job) + + for schedule, command in deduped: + job = cron.new(command=command, comment=CRON_COMMENT) + job.setall(schedule) + job.enable() + + return cron + + +class suppress_output(object): + ''' + A context manager for doing a "deep suppression" of stdout and stderr in + Python, i.e. will suppress all print, even if the print originates in a + compiled C/Fortran sub-function. + This will not suppress raised exceptions, since exceptions are printed + to stderr just before a script exits, and after the context manager has + exited (at least, I think that is why it lets exceptions through). + + with suppress_stdout_stderr(): + rogue_function() + ''' + def __init__(self, stdout=True, stderr=True): + # Open a pair of null files + # Save the actual stdout (1) and stderr (2) file descriptors. + self.stdout, self.stderr = stdout, stderr + if stdout: + self.null_stdout = os.open(os.devnull, os.O_RDWR) + self.real_stdout = os.dup(1) + if stderr: + self.null_stderr = os.open(os.devnull, os.O_RDWR) + self.real_stderr = os.dup(2) + + def __enter__(self): + # Assign the null pointers to stdout and stderr. + if self.stdout: + os.dup2(self.null_stdout, 1) + if self.stderr: + os.dup2(self.null_stderr, 2) + + def __exit__(self, *_): + # Re-assign the real stdout/stderr back to (1) and (2) + if self.stdout: + os.dup2(self.real_stdout, 1) + os.close(self.null_stdout) + if self.stderr: + os.dup2(self.real_stderr, 2) + os.close(self.null_stderr) diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/actions_as_select.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/actions_as_select.html new file mode 100644 index 0000000..86a7719 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/actions_as_select.html @@ -0,0 +1 @@ +actions_as_select diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/app_index.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/app_index.html new file mode 100644 index 0000000..6868b49 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/app_index.html @@ -0,0 +1,18 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/base.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/base.html new file mode 100644 index 0000000..d8ad8d0 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/base.html @@ -0,0 +1,246 @@ +{% load i18n static %} +{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block title %}{% endblock %} | ArchiveBox + +{% block extrastyle %}{% endblock %} +{% if LANGUAGE_BIDI %}{% endif %} +{% block extrahead %}{% endblock %} +{% block responsive %} + + + {% if LANGUAGE_BIDI %}{% endif %} +{% endblock %} +{% block blockbots %}{% endblock %} + + +{% load i18n %} + + + + + + + + + +
    + + {% if not is_popup %} + + + + {% block breadcrumbs %} + + {% endblock %} + {% endif %} + + {% block messages %} + {% if messages %} +
      {% for message in messages %} + {{ message|capfirst }} + {% endfor %}
    + {% endif %} + {% endblock messages %} + + +
    + {% block pretitle %}{% endblock %} + {% block content_title %}{# {% if title %}

    {{ title }}

    {% endif %} #}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
    +
    + + + {% block footer %}{% endblock %} +
    + + + + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/grid_change_list.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/grid_change_list.html new file mode 100644 index 0000000..6894efd --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/grid_change_list.html @@ -0,0 +1,91 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + {{ block.super }} + + {% if cl.formset %} + + {% endif %} + {% if cl.formset or action_form %} + + {% endif %} + {{ media.css }} + {% if not actions_on_top and not actions_on_bottom %} + + {% endif %} +{% endblock %} + +{% block extrahead %} +{{ block.super }} +{{ media.js }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block coltype %}{% endblock %} + +{% block content %} +
    + {% block object-tools %} +
      + {% block object-tools-items %} + {% change_list_object_tools %} + {% endblock %} +
    + {% endblock %} + {% if cl.formset and cl.formset.errors %} +

    + {% if cl.formset.total_error_count == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} +

    + {{ cl.formset.non_form_errors }} + {% endif %} +
    +
    + {% block search %}{% search_form cl %}{% endblock %} + {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %} + +
    {% csrf_token %} + {% if cl.formset %} +
    {{ cl.formset.management_form }}
    + {% endif %} + + {% block result_list %} + {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% comment %} + Table grid + {% result_list cl %} + {% endcomment %} + {% snapshots_grid cl %} + {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% endblock %} + {% block pagination %}{% pagination cl %}{% endblock %} +
    +
    + {% block filters %} + {% if cl.has_filters %} +
    +

    {% translate 'Filter' %}

    + {% if cl.has_active_filters %}

    + ✖ {% translate "Clear all filters" %} +

    {% endif %} + {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} +
    + {% endif %} + {% endblock %} +
    +
    +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/login.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/login.html new file mode 100644 index 0000000..98283f8 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/login.html @@ -0,0 +1,100 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} + +{% block extrastyle %}{{ block.super }} +{{ form.media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} login{% endblock %} + +{% block branding %}

    ArchiveBox Admin

    {% endblock %} + +{% block usertools %} +
    + Back to Main Index +{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block content_title %} +
    + Log in to add, edit, and remove links from your archive. +


    +
    +{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors %} +

    +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

    +{% endif %} + +{% if form.non_field_errors %} +{% for error in form.non_field_errors %} +

    + {{ error }} +

    +{% endfor %} +{% endif %} + +
    + +{% if user.is_authenticated %} +

    +{% blocktrans trimmed %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? +{% endblocktrans %} +

    +{% endif %} + +
    +
    {% csrf_token %} +
    + {{ form.username.errors }} + {{ form.username.label_tag }} {{ form.username }} +
    +
    + {{ form.password.errors }} + {{ form.password.label_tag }} {{ form.password }} + +
    + {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + + {% endif %} +
    + +
    +
    + +
    +

    +
    +
    + If you forgot your password, reset it here or run:
    +
    +archivebox manage changepassword USERNAME
    +
    + +

    +
    +
    + To create a new admin user, run the following: +
    +archivebox manage createsuperuser
    +
    +
    +
    + + (cd into your archive folder before running commands) +
    + + +
    +{% endblock %} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/snapshots_grid.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/snapshots_grid.html new file mode 100644 index 0000000..a7a2d4f --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/admin/snapshots_grid.html @@ -0,0 +1,162 @@ +{% load i18n admin_urls static admin_list %} +{% load core_tags %} + +{% block extrastyle %} + + +{% endblock %} + +{% block content %} +
    + {% for obj in results %} +
    + + + + + +
    + {% if obj.tags_str %} +

    {{obj.tags_str}}

    + {% endif %} + {% if obj.title %} + +

    {{obj.title|truncatechars:55 }}

    +
    + {% endif %} + {% comment %}

    TEXT If needed.

    {% endcomment %} +
    +
    + +
    +
    + {% endfor %} +
    + +{% endblock %} \ No newline at end of file diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/add_links.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/add_links.html new file mode 100644 index 0000000..0b384f5 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/add_links.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block body %} +
    +

    + {% if stdout %} +

    Add new URLs to your archive: results

    +
    +                {{ stdout | safe }}
    +                

    +
    +
    +
    +   Add more URLs ➕ +
    + {% else %} +
    {% csrf_token %} +

    Add new URLs to your archive

    +
    + {{ form.as_p }} +
    + +
    +
    +


    + + {% if absolute_add_path %} +
    +

    Bookmark this link to quickly add to your archive: + Add to ArchiveBox

    +
    + {% endif %} + + {% endif %} +
    +{% endblock %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/base.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/base.html new file mode 100644 index 0000000..a70430e --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/base.html @@ -0,0 +1,284 @@ +{% load static %} + + + + + + Archived Sites + + + + + + {% block extra_head %} + {% endblock %} + + + + + + +
    +
    + +
    +
    + {% block body %} + {% endblock %} +
    + + + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/core/snapshot_list.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/core/snapshot_list.html new file mode 100644 index 0000000..ce2b2fa --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/core/snapshot_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% load static %} + +{% block body %} +
    +
    + + + +
    + + + + + + + + + + + {% for link in object_list %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSnapshot ({{object_list|length}})FilesOriginal URL
    +
    + + {% if page_obj.has_previous %} + « first + previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next + last » + {% endif %} + + + {% if page_obj.has_next %} + next + last » + {% endif %} + +
    +
    +{% endblock %} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/link_details.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/link_details.html new file mode 100644 index 0000000..b1edcfe --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/link_details.html @@ -0,0 +1,488 @@ + + + + {{title}} + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    Added
    + {{bookmarked_date}} +
    +
    +
    First Archived
    + {{oldest_archive_date}} +
    +
    +
    Last Checked
    + {{updated_date}} +
    +
    +
    +
    +
    Type
    +
    {{extension}}
    +
    +
    +
    Tags
    +
    {{tags}}
    +
    +
    +
    Status
    +
    {{status}}
    +
    +
    +
    Saved
    + ✅ {{num_outputs}} +
    +
    +
    Errors
    + ❌ {{num_failures}} +
    +
    +
    Size
    + {{size}} +
    +
    +
    +
    +
    🗃 Files
    + JSON | + WARC | + Media | + Git | + Favicon | + See all... +
    +
    +
    +
    +
    +
    + +
    + + + +

    Wget > WARC

    +

    archive/{{domain}}

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > SingleFile

    +

    archive/singlefile.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Archive.Org

    +

    web.archive.org/web/...

    +
    +
    +
    +
    +
    + +
    + + + +

    Original

    +

    {{domain}}

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > PDF

    +

    archive/output.pdf

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > Screenshot

    +

    archive/screenshot.png

    +
    +
    +
    +
    +
    + +
    + + + +

    Chrome > HTML

    +

    archive/output.html

    +
    +
    +
    +
    +
    + +
    + + + +

    Readability

    +

    archive/readability/...

    +
    +
    +
    +
    +
    +
    + +
    + + + +

    mercury

    +

    archive/mercury/...

    +
    +
    +
    +
    +
    +
    + + + + + + + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index.html new file mode 100644 index 0000000..95af196 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index.html @@ -0,0 +1,255 @@ +{% load static %} + + + + + Archived Sites + + + + + + + + + +
    +
    + +
    +
    + + + + + + + + + + + + {% for link in links %} + {% include 'main_index_row.html' with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_minimal.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_minimal.html new file mode 100644 index 0000000..dcfaa23 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_minimal.html @@ -0,0 +1,24 @@ + + + + Archived Sites + + + + + + + + + + + + + + {% for link in links %} + {% include "main_index_row.html" with link=link %} + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    + + \ No newline at end of file diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_row.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_row.html new file mode 100644 index 0000000..5e21a8c --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/main_index_row.html @@ -0,0 +1,22 @@ +{% load static %} + + + {% if link.bookmarked_date %} {{ link.bookmarked_date }} {% else %} {{ link.added }} {% endif %} + + {% if link.is_archived %} + + {% else %} + + {% endif %} + + {{link.title|default:'Loading...'}} + {% if link.tags_str != None %} {{link.tags_str|default:''}} {% else %} {{ link.tags|default:'' }} {% endif %} + + + + 📄 + {% if link.icons %} {{link.icons}} {% else %} {{ link.num_outputs}} {% endif %} + + + {{link.url}} + \ No newline at end of file diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/add.css b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/add.css new file mode 100644 index 0000000..b128bf4 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/add.css @@ -0,0 +1,62 @@ +.dashboard #content { + width: 100%; + margin-right: 0px; + margin-left: 0px; +} +#submit { + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 10px; + border-radius: 4px; + background-color: #f5dd5d; + color: #333; + font-size: 18px; + font-weight: 800; +} +#add-form button[role="submit"]:hover { + background-color: #e5cd4d; +} +#add-form label { + display: block; + font-size: 16px; +} +#add-form textarea { + width: 100%; + min-height: 300px; +} +#delay-warning div { + border: 1px solid red; + border-radius: 4px; + margin: 10px; + padding: 10px; + font-size: 15px; + background-color: #f5dd5d; +} +#stdout { + background-color: #ded; + padding: 10px 10px; + border-radius: 4px; + white-space: normal; +} +ul#id_depth { + list-style-type: none; + padding: 0; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/admin.css b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/admin.css new file mode 100644 index 0000000..181c06d --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/admin.css @@ -0,0 +1,234 @@ +#logo { + height: 30px; + vertical-align: -6px; + padding-right: 5px; +} +#site-name:hover a { + opacity: 0.9; +} +#site-name .loader { + height: 25px; + width: 25px; + display: inline-block; + border-width: 3px; + vertical-align: -3px; + margin-right: 5px; + margin-top: 2px; +} +#branding h1, #branding h1 a:link, #branding h1 a:visited { + color: mintcream; +} +#header { + background: #aa1e55; + padding: 6px 14px; +} +#content { + padding: 8px 8px; +} +#user-tools { + font-size: 13px; + +} + +div.breadcrumbs { + background: #772948; + color: #f5dd5d; + padding: 6px 15px; +} + +body.model-snapshot.change-list div.breadcrumbs, +body.model-snapshot.change-list #content .object-tools { + display: none; +} + +.module h2, .module caption, .inline-group h2 { + background: #772948; +} + +#content .object-tools { + margin-top: -35px; + margin-right: -10px; + float: right; +} + +#content .object-tools a:link, #content .object-tools a:visited { + border-radius: 0px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; +} + +#content .object-tools a.addlink { + background-blend-mode: difference; +} + +#content #changelist #toolbar { + padding: 0px; + background: none; + margin-bottom: 10px; + border-top: 0px; + border-bottom: 0px; +} + +#content #changelist #toolbar form input[type="submit"] { + border-color: #aa1e55; +} + +#content #changelist-filter li.selected a { + color: #aa1e55; +} + + +/*#content #changelist .actions { + position: fixed; + bottom: 0px; + z-index: 800; +}*/ +#content #changelist .actions { + float: right; + margin-top: -34px; + padding: 0px; + background: none; + margin-right: 0px; + width: auto; +} + +#content #changelist .actions .button { + border-radius: 2px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; + margin-right: 4px; + box-shadow: 4px 4px 4px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.08); +} +#content #changelist .actions .button:hover { + border: 1px solid rgba(0,0,0,0.2); + opacity: 0.9; +} +#content #changelist .actions .button[name=verify_snapshots], #content #changelist .actions .button[name=update_titles] { + background-color: #dedede; + color: #333; +} +#content #changelist .actions .button[name=update_snapshots] { + background-color:lightseagreen; + color: #333; +} +#content #changelist .actions .button[name=overwrite_snapshots] { + background-color: #ffaa31; + color: #333; +} +#content #changelist .actions .button[name=delete_snapshots] { + background-color: #f91f74; + color: rgb(255 248 252 / 64%); +} + + +#content #changelist-filter h2 { + border-radius: 4px 4px 0px 0px; +} + +@media (min-width: 767px) { + #content #changelist-filter { + top: 35px; + width: 110px; + margin-bottom: 35px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered div.xfull { + margin-right: 115px; + } +} + +@media (max-width: 1127px) { + #content #changelist .actions { + position: fixed; + bottom: 6px; + left: 10px; + float: left; + z-index: 1000; + } +} + +#content a img.favicon { + height: 20px; + width: 20px; + vertical-align: -5px; + padding-right: 6px; +} + +#content td, #content th { + vertical-align: middle; + padding: 4px; +} + +#content #changelist table input { + vertical-align: -2px; +} + +#content thead th .text a { + padding: 8px 4px; +} + +#content th.field-added, #content td.field-updated { + word-break: break-word; + min-width: 128px; + white-space: normal; +} + +#content th.field-title_str { + min-width: 300px; +} + +#content td.field-files { + white-space: nowrap; +} +#content td.field-files .exists-True { + opacity: 1; +} +#content td.field-files .exists-False { + opacity: 0.1; + filter: grayscale(100%); +} +#content td.field-size { + white-space: nowrap; +} + +#content td.field-url_str { + word-break: break-all; + min-width: 200px; +} + +#content tr b.status-pending { + font-weight: 200; + opacity: 0.6; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.tags > a > .tag { + float: right; + border-radius: 5px; + background-color: #bfdfff; + padding: 2px 5px; + margin-left: 4px; + margin-top: 1px; +} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/archive.png b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/archive.png new file mode 100644 index 0000000000000000000000000000000000000000..307b45013382851ef1026e111921bd94ba55af1d GIT binary patch literal 17730 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:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/external.png b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/external.png new file mode 100644 index 0000000000000000000000000000000000000000..7e1a5f02aebccd4dcc6b1b0e3040c66ee84270a8 GIT binary patch literal 1647 zcmV-#29WuQP)gwIy-SzeLx3{-TOH0Pa z#^mJWg@uK_zP?jaQ@FUeMn*<ssP45C&jS0Z#=KL|K+q*ZaT6T^=ZwNzx>f$;{MezMmFr z-hyp&pfeg-EEbE!VzF2(7K_DVk;{Eq7SipHWdCbiob5_l5zgwP+;_-S8WPHi#`iyW z(v(2RfaCG2w8fhTLg82%;|yPEGBw{8Lj0lBOs#fPD z0xTl};WJsQGjbahmJlz6v?G;inIw(73980_q z`d%=(mLkC*iCv-ZJo+UDa)Y~&b%mbIJ28$M5`4gFr46&Z?!L`zQe3aQrAs1=ee8qz9E1kOAI z6T=WD0&f#W;B*SpfpMZrVGb}#)F{jY#)t}qxxfffpD-WTPgEzw0DFnrgg9UyQJD}6 z>>=tB;(=kJDq#sQNYo@O1BQr-gr&d$QID`3=qIWXQUJX~EkYWgkGMW<5f)MbJ;Zhb zMpj4%bcyYE@!4@ZcN3DaUi$(S%LCX|c`(_%u&m@q9Sl#B_}VnWH7FfA&d zc6|ygYJl5|&L@^11ICj-aeIMJoyUwA)e2a(@&~sUSUF%F{}r}?rv0YbO`wvrMBv8@ zTeblf0A7%`vLtZP3buL*tWmA!9}!kh!B$FxM@mhR5~qj}_R|F~7iz8-usZQy^q!y) zdD)?kbL8=7d8b(Xl(3!ne8Oho@3!1BvKsE(oa0_nEX~>1xFKsOHsMEEV&W;%DP7je zvrxR`sQZcm%hq`G9CPd~+c9v}{~?kWeqR;)vP15z=)95oXMFLs=CN?6{*oqJiI3_F zyiY!+_{8Af?RYZXah`Kludr9&ro3cf6WH_7$&$Z4EqEh1d}K9.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.js b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.js new file mode 100644 index 0000000..07af1c3 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.dataTables.min.js @@ -0,0 +1,166 @@ +/*! + DataTables 1.10.19 + ©2008-2018 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Z(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Z(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Z(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=n.defaults.oLanguage,c=b.sDecimal;c&&Da(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&(d&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(d&&"Loading..."===b.sLoadingRecords)&&F(a, +a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Da(a)}}function fb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%": +"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1, +overflow:"hidden"}).append(h("
    ").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("
    ").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,n.__browser);a.oScroll.iBarWidth=n.__browser.barWidth} +function ib(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=n.defaults.column,d=a.aoColumns.length,c=h.extend({},n.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},n.models.oSearch,c[d]);ka(a,d,h(b).data())}function ka(a,b,c){var b=a.aoColumns[b], +d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(gb(c),J(n.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=S(g),i=b.mRender? +S(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return N(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone, +b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function $(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],m);else if("string"=== +typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d, +1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(m.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(m.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,m=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!mb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:m;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("",{valign:"top",colSpan:V(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,m,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,m,i]);d=h(a.nTBody);d.children().detach(); +d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&nb(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;P(a);a._drawHold=!1}function ob(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore= +a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,m,l,q,k=0;k")[0];m=f[k+1];if("'"==m||'"'==m){l="";for(q=2;f[k+q]!=m;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(m=l.split("."),i.id=m[0].substr(1,m[0].length-1),i.className=m[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=pb(a);else if("f"==j&& +d.bFilter)g=qb(a);else if("r"==j&&d.bProcessing)g=rb(a);else if("t"==j)g=sb(a);else if("i"==j&&d.bInfo)g=tb(a);else if("p"==j&&d.bPaginate)g=ub(a);else if(0!==n.ext.feature.length){i=n.ext.feature;q=0;for(m=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_", +g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ra(a,h(this).val());P(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a=== +c&&h("select",i).val(d)});return i[0]}function ub(a){var b=a.sPaginationType,c=n.ext.pager[b],d="function"===typeof c,e=function(a){P(a)},b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]} +function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");r(a,null,"processing",[a,b])}function sb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),m=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden", +position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", +{"class":f.sScrollFootInner}).append(m.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:la,sName:"scrolling"});return i[0]}function la(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth, +f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,m=j.children("table"),j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),n=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),Q,L,R,w,Ua=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!== +L&&a.scrollBarVis!==k)a.scrollBarVis=L,$(a);else{a.scrollBarVis=L;p.children("thead, tfoot").remove();u&&(R=u.clone().prependTo(p),Q=u.find("tr"),R=R.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=aa(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},R);f=p.outerWidth();if(""===c){r.width="100%";if(U&&(p.find("tbody").height()>j.offsetHeight|| +"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=v(d),f=p.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Ua.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ua[b]},o);h(L).height(0);u&&(I(C,R),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},R),I(function(a,b){a.style.width=y[b]},Q),h(R).height(0));I(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.childNodes[0].style.height= +"0";a.childNodes[0].style.overflow="hidden";a.style.width=Ua[b]},L);u&&I(function(a,b){a.innerHTML='
    '+A[b]+"
    ";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=y[b]},R);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(Q-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else Q="100%";q.width=v(Q); +g.width=v(Q);u&&(a.nScrollFoot.style.width=v(Q));!e&&U&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();m[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(n[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0, +f=b.length,g,j;e").appendTo(j.find("tbody"));j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");m=ra(a,j.find("thead")[0]);for(n=0;n").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(n=0;n").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a, +b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function X(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var m=[];f=function(a){a.length&& +!h.isArray(a[0])?m.push(a):h.merge(m,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,n=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=X(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=n.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ba(a,b)));for(var f,g=n.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!==k&&h.extend(a.oPreviousSearch,Cb(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Na(a,b){var c=a.renderer,d=n.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"=== +typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Lb.numbers_length,d=Math.floor(c/2);b<=c?c=Y(0,b):a<=d?(c=Y(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=Y(b-(c-2),b):(c=Y(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function Da(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Ya)},"html-num":function(b){return za(b, +a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Ya)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Mb(a){return function(){var b=[ya(this[n.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return n.ext.internal[a].apply(this,b)}}var n=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)}; +this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&la(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a, +b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data(): +c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]}; +this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust(); +(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in n.ext.internal)e&&(this[e]=Mb(e));this.each(function(){var e={},g=1").appendTo(q)); +p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){p.nTFoot=b[0];ea(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Ya=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Ob=function(a){var b=parseInt(a,10);return!isNaN(b)&& +isFinite(a)?b:null},Pb=function(a,b){Za[b]||(Za[b]=RegExp(Qa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Za[b],"."):a},$a=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Pb(a,b));c&&d&&(a=a.replace(Ya,""));return!isNaN(parseFloat(a))&&isFinite(a)},Qb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:$a(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb= +/<.*?>/g,Oa=n.util.throttle,Sb=[],w=Array.prototype,ac=function(a){var b,c,d=n.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof +s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=V(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e); +c._detailsShow&&c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Ub(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Ub(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){db(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Vb=function(a,b, +c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Vb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc): +"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var n=h.map(g,function(a,b){return a.bVisible?b:null});return[n[n.length+b]]}return[aa(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)}, +1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Vb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData}, +1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData, +i,m,l;if(a!==k&&g.bVisible!==a){if(a){var n=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(m=j.length;id;return!0};n.isDataTable= +n.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof n.Api)return!0;h.each(n.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};n.tables=n.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(n.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};n.camelToHungarian=J;o("$()",function(a,b){var c= +this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){oa(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a= +this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT"); +h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable), +(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,n.settings);-1!==c&&n.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=S(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]: +a._);return a.replace("%d",c)});n.version="1.10.19";n.settings=[];n.models={};n.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};n.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};n.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null, +sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};n.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1, +bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+ +a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"}, +oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({}, +n.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Z(n.defaults);n.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null}; +Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[], +aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button", +iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal: +this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{}, +classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); +h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled", +sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"", +sJUIHeader:"",sJUIFooter:""});var Lb=n.ext.pager;h.extend(Lb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e, +f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,n=0,o=function(b,d){var k,s,u,r,v=function(b){Ta(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{m=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":m=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m= +j.sNext;l=r+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":n,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(m).appendTo(b);Wa(u,{action:r},v);n++}}}},s;try{s=h(b).find(H.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+ +s+"]").focus()}}});h.extend(n.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)|| +"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(n.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," ").replace(Aa,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Pb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return M(a)? +"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Da("");h.extend(!0,n.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc: +c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]== +"asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var eb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g,"""):a};n.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return eb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g, +a)+f+(e||"")}}},text:function(){return{display:eb,filter:eb}}};h.extend(n.ext.internal,{_fnExternApiFunc:Mb,_fnBuildAjax:sa,_fnAjaxUpdate:mb,_fnAjaxParameters:vb,_fnAjaxUpdateDraw:wb,_fnAjaxDataSrc:ta,_fnAddColumn:Ea,_fnColumnOptions:ka,_fnAdjustColumnSizing:$,_fnVisibleToColumnIndex:aa,_fnColumnIndexToVisible:ba,_fnVisbleColumns:V,_fnGetColumns:ma,_fnColumnTypes:Ga,_fnApplyColumnDefs:jb,_fnHungarianMap:Z,_fnCamelToHungarian:J,_fnLanguageCompat:Ca,_fnBrowserDetect:hb,_fnAddData:O,_fnAddTr:na,_fnNodeToDataIndex:function(a, +b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:kb,_fnSplitObjNotation:Ja,_fnGetObjectDataFn:S,_fnSetObjectDataFn:N,_fnGetDataMaster:Ka,_fnClearTable:oa,_fnDeleteIndex:pa,_fnInvalidate:da,_fnGetRowElements:Ia,_fnCreateTr:Ha,_fnBuildHead:lb,_fnDrawHead:fa,_fnDraw:P,_fnReDraw:T,_fnAddOptionsHtml:ob,_fnDetectHeader:ea,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:qb,_fnFilterComplete:ga,_fnFilterCustom:zb, +_fnFilterColumn:yb,_fnFilter:xb,_fnFilterCreateSearch:Pa,_fnEscapeRegex:Qa,_fnFilterData:Ab,_fnFeatureHtmlInfo:tb,_fnUpdateInfo:Db,_fnInfoMacros:Eb,_fnInitialise:ha,_fnInitComplete:ua,_fnLengthChange:Ra,_fnFeatureHtmlLength:pb,_fnFeatureHtmlPaginate:ub,_fnPageChange:Ta,_fnFeatureHtmlProcessing:rb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:sb,_fnScrollDraw:la,_fnApplyToChildren:I,_fnCalculateColumnWidths:Fa,_fnThrottle:Oa,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:v, +_fnSortFlatten:X,_fnSort:nb,_fnSortAria:Jb,_fnSortListener:Va,_fnSortAttachListener:Ma,_fnSortingClasses:wa,_fnSortData:Ib,_fnSaveState:xa,_fnLoadState:Kb,_fnSettingsFromNode:ya,_fnLog:K,_fnMap:F,_fnBindAction:Wa,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Sa,_fnRenderer:Na,_fnDataSource:y,_fnRowAttributes:La,_fnExtend:Xa,_fnCalculateEnd:function(){}});h.fn.dataTable=n;n.$=h;h.fn.dataTableSettings=n.settings;h.fn.dataTableExt=n.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()}; +h.each(n,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.min.js b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/default/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + +
    +
    + +
    +
    + + + + + + + + + + $rows +
    BookmarkedSnapshot ($num_links)FilesOriginal URL
    + + + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index_row.html b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index_row.html new file mode 100644 index 0000000..9112eac --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/themes/legacy/main_index_row.html @@ -0,0 +1,16 @@ + + $bookmarked_date + + + + $title + $tags + + + + 📄 + $num_outputs + + + $url + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/util.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/util.py new file mode 100644 index 0000000..5530ab4 --- /dev/null +++ b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/util.py @@ -0,0 +1,318 @@ +__package__ = 'archivebox' + +import re +import requests +import json as pyjson + +from typing import List, Optional, Any +from pathlib import Path +from inspect import signature +from functools import wraps +from hashlib import sha256 +from urllib.parse import urlparse, quote, unquote +from html import escape, unescape +from datetime import datetime +from dateparser import parse as dateparser +from requests.exceptions import RequestException, ReadTimeout + +from .vendor.base32_crockford import encode as base32_encode # type: ignore +from w3lib.encoding import html_body_declared_encoding, http_content_type_encoding + +try: + import chardet + detect_encoding = lambda rawdata: chardet.detect(rawdata)["encoding"] +except ImportError: + detect_encoding = lambda rawdata: "utf-8" + +### Parsing Helpers + +# All of these are (str) -> str +# shortcuts to: https://docs.python.org/3/library/urllib.parse.html#url-parsing +scheme = lambda url: urlparse(url).scheme.lower() +without_scheme = lambda url: urlparse(url)._replace(scheme='').geturl().strip('//') +without_query = lambda url: urlparse(url)._replace(query='').geturl().strip('//') +without_fragment = lambda url: urlparse(url)._replace(fragment='').geturl().strip('//') +without_path = lambda url: urlparse(url)._replace(path='', fragment='', query='').geturl().strip('//') +path = lambda url: urlparse(url).path +basename = lambda url: urlparse(url).path.rsplit('/', 1)[-1] +domain = lambda url: urlparse(url).netloc +query = lambda url: urlparse(url).query +fragment = lambda url: urlparse(url).fragment +extension = lambda url: basename(url).rsplit('.', 1)[-1].lower() if '.' in basename(url) else '' +base_url = lambda url: without_scheme(url) # uniq base url used to dedupe links + +without_www = lambda url: url.replace('://www.', '://', 1) +without_trailing_slash = lambda url: url[:-1] if url[-1] == '/' else url.replace('/?', '?') +hashurl = lambda url: base32_encode(int(sha256(base_url(url).encode('utf-8')).hexdigest(), 16))[:20] + +urlencode = lambda s: s and quote(s, encoding='utf-8', errors='replace') +urldecode = lambda s: s and unquote(s) +htmlencode = lambda s: s and escape(s, quote=True) +htmldecode = lambda s: s and unescape(s) + +short_ts = lambda ts: str(parse_date(ts).timestamp()).split('.')[0] +ts_to_date = lambda ts: ts and parse_date(ts).strftime('%Y-%m-%d %H:%M') +ts_to_iso = lambda ts: ts and parse_date(ts).isoformat() + + +URL_REGEX = re.compile( + r'http[s]?://' # start matching from allowed schemes + r'(?:[a-zA-Z]|[0-9]' # followed by allowed alphanum characters + r'|[$-_@.&+]|[!*\(\),]' # or allowed symbols + r'|(?:%[0-9a-fA-F][0-9a-fA-F]))' # or allowed unicode bytes + r'[^\]\[\(\)<>"\'\s]+', # stop parsing at these symbols + re.IGNORECASE, +) + +COLOR_REGEX = re.compile(r'\[(?P\d+)(;(?P\d+)(;(?P\d+))?)?m') + +def is_static_file(url: str): + # TODO: the proper way is with MIME type detection + ext, not only extension + from .config import STATICFILE_EXTENSIONS + return extension(url).lower() in STATICFILE_EXTENSIONS + + +def enforce_types(func): + """ + Enforce function arg and kwarg types at runtime using its python3 type hints + """ + # TODO: check return type as well + + @wraps(func) + def typechecked_function(*args, **kwargs): + sig = signature(func) + + def check_argument_type(arg_key, arg_val): + try: + annotation = sig.parameters[arg_key].annotation + except KeyError: + annotation = None + + if annotation is not None and annotation.__class__ is type: + if not isinstance(arg_val, annotation): + raise TypeError( + '{}(..., {}: {}) got unexpected {} argument {}={}'.format( + func.__name__, + arg_key, + annotation.__name__, + type(arg_val).__name__, + arg_key, + str(arg_val)[:64], + ) + ) + + # check args + for arg_val, arg_key in zip(args, sig.parameters): + check_argument_type(arg_key, arg_val) + + # check kwargs + for arg_key, arg_val in kwargs.items(): + check_argument_type(arg_key, arg_val) + + return func(*args, **kwargs) + + return typechecked_function + + +def docstring(text: Optional[str]): + """attach the given docstring to the decorated function""" + def decorator(func): + if text: + func.__doc__ = text + return func + return decorator + + +@enforce_types +def str_between(string: str, start: str, end: str=None) -> str: + """(12345, , ) -> 12345""" + + content = string.split(start, 1)[-1] + if end is not None: + content = content.rsplit(end, 1)[0] + + return content + + +@enforce_types +def parse_date(date: Any) -> Optional[datetime]: + """Parse unix timestamps, iso format, and human-readable strings""" + + if date is None: + return None + + if isinstance(date, datetime): + return date + + if isinstance(date, (float, int)): + date = str(date) + + if isinstance(date, str): + return dateparser(date) + + raise ValueError('Tried to parse invalid date! {}'.format(date)) + + +@enforce_types +def download_url(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the text""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + ) + + content_type = response.headers.get('Content-Type', '') + encoding = http_content_type_encoding(content_type) or html_body_declared_encoding(response.text) + + if encoding is not None: + response.encoding = encoding + + return response.text + +@enforce_types +def get_headers(url: str, timeout: int=None) -> str: + """Download the contents of a remote url and return the headers""" + from .config import TIMEOUT, CHECK_SSL_VALIDITY, WGET_USER_AGENT + timeout = timeout or TIMEOUT + + try: + response = requests.head( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + allow_redirects=True, + ) + if response.status_code >= 400: + raise RequestException + except ReadTimeout: + raise + except RequestException: + response = requests.get( + url, + headers={'User-Agent': WGET_USER_AGENT}, + verify=CHECK_SSL_VALIDITY, + timeout=timeout, + stream=True + ) + + return pyjson.dumps(dict(response.headers), indent=4) + + +@enforce_types +def chrome_args(**options) -> List[str]: + """helper to build up a chrome shell command with arguments""" + + from .config import CHROME_OPTIONS + + options = {**CHROME_OPTIONS, **options} + + cmd_args = [options['CHROME_BINARY']] + + if options['CHROME_HEADLESS']: + cmd_args += ('--headless',) + + if not options['CHROME_SANDBOX']: + # assume this means we are running inside a docker container + # in docker, GPU support is limited, sandboxing is unecessary, + # and SHM is limited to 64MB by default (which is too low to be usable). + cmd_args += ( + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + ) + + + if not options['CHECK_SSL_VALIDITY']: + cmd_args += ('--disable-web-security', '--ignore-certificate-errors') + + if options['CHROME_USER_AGENT']: + cmd_args += ('--user-agent={}'.format(options['CHROME_USER_AGENT']),) + + if options['RESOLUTION']: + cmd_args += ('--window-size={}'.format(options['RESOLUTION']),) + + if options['TIMEOUT']: + cmd_args += ('--timeout={}'.format((options['TIMEOUT']) * 1000),) + + if options['CHROME_USER_DATA_DIR']: + cmd_args.append('--user-data-dir={}'.format(options['CHROME_USER_DATA_DIR'])) + + return cmd_args + + +def ansi_to_html(text): + """ + Based on: https://stackoverflow.com/questions/19212665/python-converting-ansi-color-codes-to-html + """ + from .config import COLOR_DICT + + TEMPLATE = '
    ' + text = text.replace('[m', '
    ') + + def single_sub(match): + argsdict = match.groupdict() + if argsdict['arg_3'] is None: + if argsdict['arg_2'] is None: + _, color = 0, argsdict['arg_1'] + else: + _, color = argsdict['arg_1'], argsdict['arg_2'] + else: + _, color = argsdict['arg_3'], argsdict['arg_2'] + + return TEMPLATE.format(COLOR_DICT[color][0]) + + return COLOR_REGEX.sub(single_sub, text) + + +class AttributeDict(dict): + """Helper to allow accessing dict values via Example.key or Example['key']""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Recursively convert nested dicts to AttributeDicts (optional): + # for key, val in self.items(): + # if isinstance(val, dict) and type(val) is not AttributeDict: + # self[key] = AttributeDict(val) + + def __getattr__(self, attr: str) -> Any: + return dict.__getitem__(self, attr) + + def __setattr__(self, attr: str, value: Any) -> None: + return dict.__setitem__(self, attr, value) + + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif isinstance(obj, Path): + return str(obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + diff --git a/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/vendor/__init__.py b/archivebox-0.5.3/debian/archivebox/usr/lib/python3/dist-packages/archivebox/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archivebox-0.5.3/debian/archivebox/usr/share/doc/archivebox/changelog.Debian.gz b/archivebox-0.5.3/debian/archivebox/usr/share/doc/archivebox/changelog.Debian.gz new file mode 100644 index 0000000000000000000000000000000000000000..8f8bf755ed14708d4d57902f8ee57c706b42f3b7 GIT binary patch literal 165 zcmV;W09yYaiwFP!0000211*ca4uUWgMt7d#8`Vg2i(m}?#L2rHH(} z9o$Y%&QU?X?1vlw#ezg7Ho08l>9y+i7=vM_=VRxmECW^;{1`Ojt#qplLygh*D%uox zGjw1T`GID}5Fgxtgd{=f%yZnVZgHQOqjd%cW8>K+HT;dxzCQt%VAr4>uF9dHLME Fri, 08 Jan 2021 08:14:46 -0500 diff --git a/archivebox-0.5.3/debian/compat b/archivebox-0.5.3/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/archivebox-0.5.3/debian/compat @@ -0,0 +1 @@ +9 diff --git a/archivebox-0.5.3/debian/control b/archivebox-0.5.3/debian/control new file mode 100644 index 0000000..5e6bb06 --- /dev/null +++ b/archivebox-0.5.3/debian/control @@ -0,0 +1,34 @@ +Source: archivebox +Maintainer: Nick Sweeting +Section: python +Priority: optional +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9), dh-python, python3-pip, python3-setuptools, python3-wheel, python3-stdeb +Standards-Version: 3.9.1 +Homepage: https://github.com/ArchiveBox/ArchiveBox +X-Python-Version: >= 3.7 + +Package: archivebox +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends}, nodejs, chromium-browser, wget, curl, git, ffmpeg, youtube-dl, python3-atomicwrites, python3-croniter, python3-crontab, python3-dateparser, python3-django, python3-django-extensions, python3-django-jsonfield, python3-mypy-extensions, python3-requests, python3-w3lib, ripgrep +Description: The self-hosted internet archive. +
    + +

    ArchiveBox
    The open-source self-hosted web archive.

    + . + ▶️ Quickstart | + Demo | + Github | + Documentation | + Info & Motivation | + Community | + Roadmap + . +
    + "Your own personal internet archive" (网站存档 / 爬虫)
    + 
    + . + + . + + + diff --git a/archivebox-0.5.3/debian/files b/archivebox-0.5.3/debian/files new file mode 100644 index 0000000..8e2e065 --- /dev/null +++ b/archivebox-0.5.3/debian/files @@ -0,0 +1,2 @@ +archivebox_0.5.3-1_all.deb python optional +archivebox_0.5.3-1_amd64.buildinfo python optional diff --git a/archivebox-0.5.3/debian/rules b/archivebox-0.5.3/debian/rules new file mode 100755 index 0000000..e5863d4 --- /dev/null +++ b/archivebox-0.5.3/debian/rules @@ -0,0 +1,21 @@ +#!/usr/bin/make -f + +# This file was automatically generated by stdeb 0.10.0 at +# Fri, 08 Jan 2021 08:14:46 -0500 + +%: + dh $@ --with python3 --buildsystem=python_distutils + +override_dh_auto_clean: + python3 setup.py clean -a + find . -name \*.pyc -exec rm {} \; + +override_dh_auto_build: + python3 setup.py build --force + +override_dh_auto_install: + python3 setup.py install --force --root=debian/archivebox --no-compile -O0 --install-layout=deb --prefix=/usr + +override_dh_python2: + dh_python2 --no-guessing-versions + diff --git a/archivebox-0.5.3/debian/source/format b/archivebox-0.5.3/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/archivebox-0.5.3/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/archivebox-0.5.3/debian/source/options b/archivebox-0.5.3/debian/source/options new file mode 100644 index 0000000..bcc4bbb --- /dev/null +++ b/archivebox-0.5.3/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore="\.egg-info$" \ No newline at end of file diff --git a/archivebox-0.5.3/setup.cfg b/archivebox-0.5.3/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/archivebox-0.5.3/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/archivebox-0.5.3/setup.py b/archivebox-0.5.3/setup.py new file mode 100755 index 0000000..0754823 --- /dev/null +++ b/archivebox-0.5.3/setup.py @@ -0,0 +1,123 @@ +import json +import setuptools + +from pathlib import Path + + +PKG_NAME = "archivebox" +DESCRIPTION = "The self-hosted internet archive." +LICENSE = "MIT" +AUTHOR = "Nick Sweeting" +AUTHOR_EMAIL="git@nicksweeting.com" +REPO_URL = "https://github.com/ArchiveBox/ArchiveBox" +PROJECT_URLS = { + "Source": f"{REPO_URL}", + "Documentation": f"{REPO_URL}/wiki", + "Bug Tracker": f"{REPO_URL}/issues", + "Changelog": f"{REPO_URL}/wiki/Changelog", + "Roadmap": f"{REPO_URL}/wiki/Roadmap", + "Community": f"{REPO_URL}/wiki/Web-Archiving-Community", + "Donate": f"{REPO_URL}/wiki/Donations", +} + +ROOT_DIR = Path(__file__).parent.resolve() +PACKAGE_DIR = ROOT_DIR / PKG_NAME + +README = (PACKAGE_DIR / "README.md").read_text(encoding='utf-8', errors='ignore') +VERSION = json.loads((PACKAGE_DIR / "package.json").read_text().strip())['version'] + +# To see when setup.py gets called (uncomment for debugging): +# import sys +# print(PACKAGE_DIR, f" (v{VERSION})") +# print('>', sys.executable, *sys.argv) + + +setuptools.setup( + name=PKG_NAME, + version=VERSION, + license=LICENSE, + author=AUTHOR, + author_email=AUTHOR_EMAIL, + description=DESCRIPTION, + long_description=README, + long_description_content_type="text/markdown", + url=REPO_URL, + project_urls=PROJECT_URLS, + python_requires=">=3.7", + setup_requires=[ + "wheel", + ], + install_requires=[ + # only add things here that have corresponding apt python3-packages available + # anything added here also needs to be added to our package dependencies in + # stdeb.cfg (apt), archivebox.rb (brew), Dockerfile, etc. + # if there is no apt python3-package equivalent, then vendor it instead in + # ./archivebox/vendor/ + "requests==2.24.0", + "atomicwrites==1.4.0", + "mypy-extensions==0.4.3", + "django==3.1.3", + "django-extensions==3.0.3", + "dateparser", + "ipython", + "youtube-dl", + "python-crontab==2.5.1", + "croniter==0.3.34", + "w3lib==1.22.0", + ], + extras_require={ + 'dev': [ + "setuptools", + "twine", + "wheel", + "flake8", + "ipdb", + "mypy", + "django-stubs", + "sphinx", + "sphinx-rtd-theme", + "recommonmark", + "pytest", + "bottle", + "stdeb", + ], + }, + packages=[PKG_NAME], + include_package_data=True, # see MANIFEST.in + entry_points={ + "console_scripts": [ + f"{PKG_NAME} = {PKG_NAME}.cli:main", + ], + }, + classifiers=[ + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + + "Topic :: Utilities", + "Topic :: System :: Archiving", + "Topic :: System :: Archiving :: Backup", + "Topic :: System :: Recovery Tools", + "Topic :: Sociology :: History", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Indexing/Search", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Python Modules", + + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: Legal Industry", + "Intended Audience :: System Administrators", + + "Environment :: Console", + "Environment :: Web Environment", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Framework :: Django", + "Typing :: Typed", + ], +) diff --git a/archivebox_0.5.3-1.debian.tar.xz b/archivebox_0.5.3-1.debian.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..bb5e4ca5588a8a09086ae3db9668e0543198c03d GIT binary patch literal 1556 zcmV+v2J89#H+ooF000E$*0e?f03iVu0001VFXf})C;tW0T>vr}NNor1@tIyK_)om_ zN=i<6S-%T;fDxrj3VK@w&_QcnN)n((PWFH~bv^KE7;~VDW?nYx{1FJUkT3I`J=lRy zX}0#fnR(NsQ=#y%t9=zetY{_zLoNe9co9104g?>lpaYn)3`=VEdts?>XR3F9wn&ee zypUIASLUli*(ZqZun`&^zMI=}G5}WA&Ztvt6gQJ=#c6LJKZBIS^RP3jZD{UJ-szq{De{4_l3g4uQ}wi5n^f;Dc-dHv^No@Kze72Dwf2?nezQWnt41 z7~VfJhheCzhzgdWG1?>y<@p!>br`@yse>F;uv1qfAcdRn=5D4EDaVCE* z9_?~5S2X!{)$Fzh7-F7u+)a4CCZ~>s2iTa{nc zB4*h_4zWw50`dBFa&3otTO796b*Iz=t52B#({xl+^S!xlp+(y+YJ%HRJzCHs!@{6J zyk|MJYpZ5o(UI1wjh#Ww3+PV48|y*kDvui%Ifw72Xzx3URR+l=t&w4|Qk=qqy9W80 zP!c@)i7sZx3*GF3w!GoCP3e*}6*$=ora?2WJCzN2 z)n_#;XH%Wj>#gGSgCYQX@k&3i;>f&ZY%ZBb=`X<5wn z1j*V!2ePh`t5q&-oVBWR=(Rk^|NW5^mpX?bwd*FZYAA2ln9;GIo;cbUYsEiiuJP?q>( z+r9o+q=PKvw>GsdXi$}x14yOyfXbTc^0_gov zxN}d1qiiznG&izs)o6fFI68&7NDoUBr)dqpc9SE&y0T|p zs-!YSGAdAx7Ayuf@9-uY=U~@VV%qJ7*D{xz$=h_7r~E5l!?&-1l4G4>1-(8LPTFLZ zf*{P>p9eHJRQ7?zry~GykB<+>tcW{##^F!O!cSLyF#5jEsL%4dVNw~|mvJH9P&cfI-06>z(`9zrbamoCZcGuN_+GNCBl2AMc&q<$O7vFvA;zb;Vn zN4t-&Oqb+&c-mU64(oh1Dh-sEQ4D(&Y?+(y`lsDXV$3VO7h*3PRz*7f<2=$PwcqUl zNsMUSA!hu4{zOfsPhhoIQ5(Bw!uYFv?lbeHS~; literal 0 HcmV?d00001 diff --git a/archivebox_0.5.3-1.dsc b/archivebox_0.5.3-1.dsc new file mode 100644 index 0000000..42dfee6 --- /dev/null +++ b/archivebox_0.5.3-1.dsc @@ -0,0 +1,40 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 3.0 (quilt) +Source: archivebox +Binary: archivebox +Architecture: all +Version: 0.5.3-1 +Maintainer: Nick Sweeting +Homepage: https://github.com/ArchiveBox/ArchiveBox +Standards-Version: 3.9.1 +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9), dh-python, python3-pip, python3-wheel, python3-stdeb +Package-List: + archivebox deb python optional arch=all +Checksums-Sha1: + a350c46e25ac3d3b30fab0efbd0226e5c0fe28a3 258683 archivebox_0.5.3.orig.tar.gz + d1952afed11f3a21e5e3794779edcd3ff6207df5 1556 archivebox_0.5.3-1.debian.tar.xz +Checksums-Sha256: + 50b2c976296734d6e6b54975826c2ada9cd13f2c15dd585a2a94553530cf1541 258683 archivebox_0.5.3.orig.tar.gz + 9bdd60a9601e9dc7768789a4bb865b24b0cb0583887ff4f1f4106fdcae8d8cd7 1556 archivebox_0.5.3-1.debian.tar.xz +Files: + a69bba33bee581722227d9fecdb78f75 258683 archivebox_0.5.3.orig.tar.gz + 77de8c5ee4b0158d7d35b162e1e04417 1556 archivebox_0.5.3-1.debian.tar.xz + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEfVaV07YYhyZHhh1Rw4E3p8FnWYgFAl/4Ws4ACgkQw4E3p8Fn +WYhR1w/+Ib5tG2bcImEI6wQF9+8CC3zYY69XGlfiFp3NOFFZeKBlDugd5h5Wz2TZ +o7GF2N/tbaTWjSpTYTTLzEu54a1aavOX6dgBUBpMkFpCVycUvodQyetegu4ts4NK +CVNB22bUR9lgYIptYKCDza03lMVL7GXCWlafJ96Xs/iDMn8hVquvbEYREzrab2BA +WmQZlmWi9dwU+LJygct5mkZ+CQANiSMaTsD+E2pZL9BEQ5j5hrh2TiVFgL7TqPmV +n4aVV9f2m6uSmpPidajtb857T8O1s7Wt+jn1YYM2Hg1O/A6FaZUX4Osi6fVh5YvY +Qa/eOy3NMv+60UP00ouWcyf1CQY3aYrM4ROA+CfP6EFY85qcGrrGKPzyKR9airfc +lmJ3CEA39Dq+FrxqLTFEIv+DvHpE/LdkVvd5MTj35sIJ1hy06Rwcz3Squhiaslxq +4ZDKQot/Qp1lhD68KBOlrV2POH3QqX6duglbpFP3eGUi56zFB7fosdHQIooiBZp4 +7cqdQq71I0iDLJW/9hRBIAnrHwwJS3s5ZMz3+vjxMojjK0yN9JwZwoYG9xoflYlH +LdM2O1v7uye4ljZhpAcCaM0qdjLy/YrtdfDUS36N0jHV2Rjo7847EC82WLk5MNCx +ivbRUG6qHdkAAQQmA+V0BmNz0EjLAFHyuXr6N8ejwUpNucmms5g= +=5Si8 +-----END PGP SIGNATURE----- diff --git a/archivebox_0.5.3-1_all.deb b/archivebox_0.5.3-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..dce55fb9bd4cd574362d67a20afbfebf140979f1 GIT binary patch literal 194244 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%0DlSqxiEe#2T4j_>F7j* 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

    L_M@paT*`( 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 UOh41lpS92b4<$;@s{n8k05Z!cn*aa+ literal 0 HcmV?d00001

    EbB0k>Dcpc#jKCox}NI*A~Zp0vU;yA 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 fXxNeM00Fzk3xKdY4e|H3vBYQl0ssI200dcD +Changed-By: Nick Sweeting +Description: + archivebox - The self-hosted internet archive. +Changes: + archivebox (0.5.3-1) focal; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + 87f8e8b136545f3ea0e0bd46503a06ddee4295b2 194244 archivebox_0.5.3-1_all.deb + 37ee2c2f698b6ae41cc226f723541391c75e1e64 6265 archivebox_0.5.3-1_amd64.buildinfo +Checksums-Sha256: + 176d539e4139cf2e618b12ce299791cfcdd861a3004a758952dea998dfd09ef3 194244 archivebox_0.5.3-1_all.deb + 7229749267a5d2bbcfe9451320ce8f301aab6747343213d1e80d1c691b5e119f 6265 archivebox_0.5.3-1_amd64.buildinfo +Files: + a33259a861e3555310abd95e0247cd7d 194244 python optional archivebox_0.5.3-1_all.deb + 8ffc941fcedb835850be69ccddf65885 6265 python optional archivebox_0.5.3-1_amd64.buildinfo diff --git a/archivebox_0.5.3-1_source.archivebox-ppa.upload b/archivebox_0.5.3-1_source.archivebox-ppa.upload new file mode 100644 index 0000000..4d55b4b --- /dev/null +++ b/archivebox_0.5.3-1_source.archivebox-ppa.upload @@ -0,0 +1,5 @@ +Successfully uploaded archivebox_0.5.3-1.dsc to ppa.launchpad.net for archivebox-ppa. +Successfully uploaded archivebox_0.5.3.orig.tar.gz to ppa.launchpad.net for archivebox-ppa. +Successfully uploaded archivebox_0.5.3-1.debian.tar.xz to ppa.launchpad.net for archivebox-ppa. +Successfully uploaded archivebox_0.5.3-1_source.buildinfo to ppa.launchpad.net for archivebox-ppa. +Successfully uploaded archivebox_0.5.3-1_source.changes to ppa.launchpad.net for archivebox-ppa. diff --git a/archivebox_0.5.3-1_source.buildinfo b/archivebox_0.5.3-1_source.buildinfo new file mode 100644 index 0000000..658cd4c --- /dev/null +++ b/archivebox_0.5.3-1_source.buildinfo @@ -0,0 +1,220 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 1.0 +Source: archivebox +Binary: archivebox +Architecture: source +Version: 0.5.3-1 +Checksums-Md5: + a8c145339737010beb02a81925cf942a 1822 archivebox_0.5.3-1.dsc +Checksums-Sha1: + ebc9cb5985ca3b9749af86cf3f79a0250888c651 1822 archivebox_0.5.3-1.dsc +Checksums-Sha256: + fd34ba74d913d39b90b9e487a504ed094c7371272c41e902c818091619d58ca0 1822 archivebox_0.5.3-1.dsc +Build-Origin: Ubuntu +Build-Architecture: amd64 +Build-Date: Fri, 08 Jan 2021 08:14:48 -0500 +Build-Tainted-By: + merged-usr-via-symlinks + usr-local-has-configs + usr-local-has-libraries + usr-local-has-programs +Installed-Build-Depends: + autoconf (= 2.69-11.1), + automake (= 1:1.16.1-4ubuntu6), + autopoint (= 0.19.8.1-10build1), + autotools-dev (= 20180224.1), + base-files (= 11ubuntu5.2), + base-passwd (= 3.5.47), + bash (= 5.0-6ubuntu1.1), + binutils (= 2.34-6ubuntu1), + binutils-common (= 2.34-6ubuntu1), + binutils-x86-64-linux-gnu (= 2.34-6ubuntu1), + bsdmainutils (= 11.1.2ubuntu3), + bsdutils (= 1:2.34-0.1ubuntu9.1), + build-essential (= 12.8ubuntu1.1), + bzip2 (= 1.0.8-2), + ca-certificates (= 20201027ubuntu0.20.04.1), + coreutils (= 8.30-3ubuntu2), + cpp (= 4:9.3.0-1ubuntu2), + cpp-9 (= 9.3.0-17ubuntu1~20.04), + dash (= 0.5.10.2-6), + debconf (= 1.5.73), + debhelper (= 12.10ubuntu1), + debianutils (= 4.9.1), + dh-autoreconf (= 19), + dh-python (= 4.20191017ubuntu7), + dh-strip-nondeterminism (= 1.7.0-1), + diffutils (= 1:3.7-3), + dpkg (= 1.19.7ubuntu3), + dpkg-dev (= 1.19.7ubuntu3), + dwz (= 0.13-5), + file (= 1:5.38-4), + findutils (= 4.7.0-1ubuntu1), + g++ (= 4:9.3.0-1ubuntu2), + g++-9 (= 9.3.0-17ubuntu1~20.04), + gcc (= 4:9.3.0-1ubuntu2), + gcc-10-base (= 10.2.0-5ubuntu1~20.04), + gcc-9 (= 9.3.0-17ubuntu1~20.04), + gcc-9-base (= 9.3.0-17ubuntu1~20.04), + gettext (= 0.19.8.1-10build1), + gettext-base (= 0.19.8.1-10build1), + grep (= 3.4-1), + groff-base (= 1.22.4-4build1), + gzip (= 1.10-0ubuntu4), + hostname (= 3.23), + init-system-helpers (= 1.57), + install-info (= 6.7.0.dfsg.2-5), + intltool-debian (= 0.35.0+20060710.5), + libacl1 (= 2.2.53-6), + libarchive-zip-perl (= 1.67-2), + libasan5 (= 9.3.0-17ubuntu1~20.04), + libatomic1 (= 10.2.0-5ubuntu1~20.04), + libattr1 (= 1:2.4.48-5), + libaudit-common (= 1:2.8.5-2ubuntu6), + libaudit1 (= 1:2.8.5-2ubuntu6), + libbinutils (= 2.34-6ubuntu1), + libblkid1 (= 2.34-0.1ubuntu9.1), + libbsd0 (= 0.10.0-1), + libbz2-1.0 (= 1.0.8-2), + libc-bin (= 2.31-0ubuntu9.1), + libc-dev-bin (= 2.31-0ubuntu9.1), + libc6 (= 2.31-0ubuntu9.1), + libc6-dev (= 2.31-0ubuntu9.1), + libcap-ng0 (= 0.7.9-2.1build1), + libcc1-0 (= 10.2.0-5ubuntu1~20.04), + libcroco3 (= 0.6.13-1), + libcrypt-dev (= 1:4.4.10-10ubuntu4), + libcrypt1 (= 1:4.4.10-10ubuntu4), + libctf-nobfd0 (= 2.34-6ubuntu1), + libctf0 (= 2.34-6ubuntu1), + libdb5.3 (= 5.3.28+dfsg1-0.6ubuntu2), + libdebconfclient0 (= 0.251ubuntu1), + libdebhelper-perl (= 12.10ubuntu1), + libdpkg-perl (= 1.19.7ubuntu3), + libelf1 (= 0.176-1.1build1), + libexpat1 (= 2.2.9-1build1), + libffi7 (= 3.3-4), + libfile-stripnondeterminism-perl (= 1.7.0-1), + libgcc-9-dev (= 9.3.0-17ubuntu1~20.04), + libgcc-s1 (= 10.2.0-5ubuntu1~20.04), + libgcrypt20 (= 1.8.5-5ubuntu1), + libgdbm-compat4 (= 1.18.1-5), + libgdbm6 (= 1.18.1-5), + libglib2.0-0 (= 2.64.3-1~ubuntu20.04.1), + libgmp10 (= 2:6.2.0+dfsg-4), + libgomp1 (= 10.2.0-5ubuntu1~20.04), + libgpg-error0 (= 1.37-1), + libicu66 (= 66.1-2ubuntu2), + libisl22 (= 0.22.1-1), + libitm1 (= 10.2.0-5ubuntu1~20.04), + liblsan0 (= 10.2.0-5ubuntu1~20.04), + liblz4-1 (= 1.9.2-2), + liblzma5 (= 5.2.4-1ubuntu1), + libmagic-mgc (= 1:5.38-4), + libmagic1 (= 1:5.38-4), + libmount1 (= 2.34-0.1ubuntu9.1), + libmpc3 (= 1.1.0-1), + libmpdec2 (= 2.4.2-3), + libmpfr6 (= 4.0.2-1), + libncursesw6 (= 6.2-0ubuntu2), + libpam-modules (= 1.3.1-5ubuntu4.1), + libpam-modules-bin (= 1.3.1-5ubuntu4.1), + libpam-runtime (= 1.3.1-5ubuntu4.1), + libpam0g (= 1.3.1-5ubuntu4.1), + libpcre2-8-0 (= 10.34-7), + libpcre3 (= 2:8.39-12build1), + libperl5.30 (= 5.30.0-9ubuntu0.2), + libpipeline1 (= 1.5.2-2build1), + libpython3-stdlib (= 3.8.2-0ubuntu2), + libpython3.8-minimal (= 3.8.5-1~20.04), + libpython3.8-stdlib (= 3.8.5-1~20.04), + libquadmath0 (= 10.2.0-5ubuntu1~20.04), + libreadline8 (= 8.0-4), + libseccomp2 (= 2.4.3-1ubuntu3.20.04.3), + libselinux1 (= 3.0-1build2), + libsigsegv2 (= 2.12-2), + libsmartcols1 (= 2.34-0.1ubuntu9.1), + libsqlite3-0 (= 3.31.1-4ubuntu0.2), + libssl1.1 (= 1.1.1f-1ubuntu2.1), + libstdc++-9-dev (= 9.3.0-17ubuntu1~20.04), + libstdc++6 (= 10.2.0-5ubuntu1~20.04), + libsub-override-perl (= 0.09-2), + libsystemd0 (= 245.4-4ubuntu3.3), + libtinfo6 (= 6.2-0ubuntu2), + libtool (= 2.4.6-14), + libtsan0 (= 10.2.0-5ubuntu1~20.04), + libubsan1 (= 10.2.0-5ubuntu1~20.04), + libuchardet0 (= 0.0.6-3build1), + libudev1 (= 245.4-4ubuntu3.3), + libunistring2 (= 0.9.10-2), + libuuid1 (= 2.34-0.1ubuntu9.1), + libxml2 (= 2.9.10+dfsg-5), + libzstd1 (= 1.4.4+dfsg-3), + linux-libc-dev (= 5.4.0-59.65), + login (= 1:4.8.1-1ubuntu5.20.04), + lsb-base (= 11.1.0ubuntu2), + m4 (= 1.4.18-4), + make (= 4.2.1-1.2), + man-db (= 2.9.1-1), + mawk (= 1.3.4.20200120-2), + mime-support (= 3.64ubuntu1), + ncurses-base (= 6.2-0ubuntu2), + ncurses-bin (= 6.2-0ubuntu2), + openssl (= 1.1.1f-1ubuntu2.1), + patch (= 2.7.6-6), + perl (= 5.30.0-9ubuntu0.2), + perl-base (= 5.30.0-9ubuntu0.2), + perl-modules-5.30 (= 5.30.0-9ubuntu0.2), + po-debconf (= 1.0.21), + python-pip-whl (= 20.0.2-5ubuntu1.1), + python3 (= 3.8.2-0ubuntu2), + python3-all (= 3.8.2-0ubuntu2), + python3-certifi (= 2019.11.28-1), + python3-chardet (= 3.0.4-4build1), + python3-distutils (= 3.8.5-1~20.04.1), + python3-idna (= 2.8-1), + python3-lib2to3 (= 3.8.5-1~20.04.1), + python3-minimal (= 3.8.2-0ubuntu2), + python3-pip (= 20.0.2-5ubuntu1.1), + python3-pkg-resources (= 45.2.0-1), + python3-requests (= 2.22.0-2ubuntu1), + python3-setuptools (= 45.2.0-1), + python3-six (= 1.14.0-2), + python3-stdeb (= 0.8.5-3), + python3-urllib3 (= 1.25.8-2ubuntu0.1), + python3-wheel (= 0.34.2-1), + python3.8 (= 3.8.5-1~20.04), + python3.8-minimal (= 3.8.5-1~20.04), + readline-common (= 8.0-4), + sed (= 4.7-1), + sensible-utils (= 0.0.12+nmu1), + sysvinit-utils (= 2.96-2.1ubuntu1), + tar (= 1.30+dfsg-7), + tzdata (= 2020d-0ubuntu0.20.04), + util-linux (= 2.34-0.1ubuntu9.1), + xz-utils (= 5.2.4-1ubuntu1), + zlib1g (= 1:1.2.11.dfsg-2ubuntu1.2) +Environment: + DEB_BUILD_OPTIONS="parallel=24" + LANG="en_US.UTF-8" + LC_ALL="en_US.UTF-8" + SOURCE_DATE_EPOCH="1610111686" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEfVaV07YYhyZHhh1Rw4E3p8FnWYgFAl/4Wu8ACgkQw4E3p8Fn +WYgIYxAAlaCfOwNwvM1NUT8Xl5qStpSm6CRQ1+zx0KD+vwQ+QfmYCrlRhvVwrpiW +qiAqeyMwROqhHVqSRE55pAmQgOlBKPII3JR4GyL9g5a35IKW6CzTpoq3U5UzxicI +xpMGNkavTvxX7zYWuG4wgYuEMHHvvKR1MyN2ExDlQvej3XSzr8eP0Nqs9hCVS3kP +dlsREb0VXucB2GBvG5kcDPfQ7kZIX2iaLjh0lK9Ij8S3JqQU2GBOH1akz88n2CMM +IHi646ZNhsQ+NGQPskSOqS+8bgiJenXJ024xnsI9JFIaN6BOOLqleBj/EP2oMugo +JFYk9Mxoyo2ja7aRq+XBoQLTbzozusPUoEWaID8f7/E1pp7F42qRYag8qc3E/qTJ +PpVkkTyg8NvwrzI4jHdqieOschQI3NkvghJOL7yoRcmiRJAPdAbpJt58g9l24F/X +A++4BM4HGN8KdWpdk/PUQkk7RWSlEhx9+ITTM46Vt1BqRQr/Yc94ntVpSwrZIU21 +S5DHXckRgslQLpewT5H0756T76P6I3kdU/5f6iMuLU8empBo+MBJXMcHFz+zR5Fu +i60pEos2GUWKA4B16E14PdRmZ85nJVWtLQVO+5x5i5oxOk8RC3nPhKto+wkTBNRd +OGZ610CpkJ0oAKUZguLkR6Oe/DXp62aQptGXyp1QMPHOngn7Q88= +=smXO +-----END PGP SIGNATURE----- diff --git a/archivebox_0.5.3-1_source.changes b/archivebox_0.5.3-1_source.changes new file mode 100644 index 0000000..a98d8b7 --- /dev/null +++ b/archivebox_0.5.3-1_source.changes @@ -0,0 +1,48 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 1.8 +Date: Fri, 08 Jan 2021 08:14:46 -0500 +Source: archivebox +Architecture: source +Version: 0.5.3-1 +Distribution: focal +Urgency: low +Maintainer: Nick Sweeting +Changed-By: Nick Sweeting +Changes: + archivebox (0.5.3-1) focal; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + ebc9cb5985ca3b9749af86cf3f79a0250888c651 1822 archivebox_0.5.3-1.dsc + a350c46e25ac3d3b30fab0efbd0226e5c0fe28a3 258683 archivebox_0.5.3.orig.tar.gz + d1952afed11f3a21e5e3794779edcd3ff6207df5 1556 archivebox_0.5.3-1.debian.tar.xz + 47aea2f95e98f3984a78a743a6b133baef352efc 7133 archivebox_0.5.3-1_source.buildinfo +Checksums-Sha256: + fd34ba74d913d39b90b9e487a504ed094c7371272c41e902c818091619d58ca0 1822 archivebox_0.5.3-1.dsc + 50b2c976296734d6e6b54975826c2ada9cd13f2c15dd585a2a94553530cf1541 258683 archivebox_0.5.3.orig.tar.gz + 9bdd60a9601e9dc7768789a4bb865b24b0cb0583887ff4f1f4106fdcae8d8cd7 1556 archivebox_0.5.3-1.debian.tar.xz + d4b6e02dcc5a15ee1ef94353c243cdb57849e459e47af9e2523d891cb00c43e6 7133 archivebox_0.5.3-1_source.buildinfo +Files: + a8c145339737010beb02a81925cf942a 1822 python optional archivebox_0.5.3-1.dsc + a69bba33bee581722227d9fecdb78f75 258683 python optional archivebox_0.5.3.orig.tar.gz + 77de8c5ee4b0158d7d35b162e1e04417 1556 python optional archivebox_0.5.3-1.debian.tar.xz + 67962d47cc8564035390506015844779 7133 python optional archivebox_0.5.3-1_source.buildinfo + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEfVaV07YYhyZHhh1Rw4E3p8FnWYgFAl/4Wu8ACgkQw4E3p8Fn +WYidQg//ZMUbaHnc3grDV0A+RcK6qqZ3apap6Tp01clcmc3QoMdALtBur2aMW1VV +SJ2XjP+onoTJtzj5JP8UZGmvqzeJTyk/GOollQ7Wunv52E0/7S4UJfckLkm+YtKb +ni1mBnEiNasjTy6OmMhe1mX7ArkxnmJKHb7Taz1KEVZrKjQDrlk9Ct9FaRSO604s +uJJn0oHAfCqzyahWLiqifmvHJ9ECM+b6qMJ/Z8Gs/GQ3Z5SvGIel3T8nu4D+R/1O +yW4osGPdEsEqm9/L8HRsDzVpOpBnn4/7t11TBca6wZMOMxDjI0FKgR86EcrOb/IV +mkKSHN35yTxFoeq3EjlVdwDif0A3K1xExTKluZH2bk/KUXt8bW06h5d2IbByQZes +fvq8hIQ0VRgU3hroZl32d0wk+/tG5YDlfjNTgvwk4zjsUwXisds02ICPqPNd06da +QdwyBIYenNcmCcd4+Gka0phzzOBGxTP4q5UqkY01l16otdW+tQEc8xL4mdQYlOsr +ac7GUkhzhO6Mo1tf3hDxe4bpQ/YRqD4WeZPCIoCKGW1PUFxS2Epx5fjG78fMLxMO +b0SNokBvURle95MaqBAaJBOFp+HTiFcpI+dPvBoLRHaKUrBfwUH/eIl5QaqwxQ7h +sNFLObmoXj2TVo4SAaDvYTCBioDUbbRXzDDlpnRFWU7yXvqnmNI= +=VV8m +-----END PGP SIGNATURE----- diff --git a/archivebox_0.5.3.orig.tar.gz b/archivebox_0.5.3.orig.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..3421ca58084676cb7ec7c1be1ed905f7f865a121 GIT binary patch literal 258683 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⁣dN62*cOU_;WoNf`$Zagu88`X%I>4v!R*iUda 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ΞmoRjw7-|wI9cSH@=&-a~UfA{$C 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