diff --git a/archivebox/config.py b/archivebox/config.py index 1716036f..9389dbb4 100644 --- a/archivebox/config.py +++ b/archivebox/config.py @@ -30,6 +30,7 @@ import inspect import getpass import platform import shutil +import requests import django from sqlite3 import dbapi2 as sqlite3 @@ -376,7 +377,7 @@ ALLOWED_IN_OUTPUT_DIR = { 'static_index.json', } -def get_version(config): +def get_version(config) -> str: try: return importlib.metadata.version(__package__ or 'archivebox') except importlib.metadata.PackageNotFoundError: @@ -415,6 +416,55 @@ def get_build_time(config) -> str: src_last_modified_unix_timestamp = (config['PACKAGE_DIR'] / 'config.py').stat().st_mtime return datetime.fromtimestamp(src_last_modified_unix_timestamp).strftime('%Y-%m-%d %H:%M:%S %s') +def get_versions_available_on_github(config): + """ + returns a dictionary containing the ArchiveBox GitHub release info for + the recommended upgrade version and the currently installed version + """ + + # we only want to perform the (relatively expensive) check for new versions + # when its most relevant, e.g. when the user runs a long-running command + subcommand_run_by_user = sys.argv[3] + long_running_commands = ('add', 'schedule', 'update', 'status', 'server') + if subcommand_run_by_user not in long_running_commands: + return None + + github_releases_api = "https://api.github.com/repos/ArchiveBox/ArchiveBox/releases" + response = requests.get(github_releases_api) + if response.status_code != 200: + stderr(f'[!] Warning: GitHub API call to check for new ArchiveBox version failed! (status={response.status_code})', color='lightyellow', config=config) + return None + all_releases = response.json() + + installed_version = parse_version_string(config['VERSION']) + + # find current version or nearest older version (to link to) + current_version = None + for idx, release in enumerate(all_releases): + release_version = parse_version_string(release["tag_name"]) + if release_version <= installed_version: + current_version = release + break + + current_version = current_version or releases[-1] + + # recommended version is whatever comes after current_version in the release list + # (perhaps too conservative to only recommend upgrading one version at a time, but it's safest) + try: + recommended_version = all_releases[idx+1] + except IndexError: + recommended_version = None + + return {"recommended_version": recommended_version, "current_version": current_version} + +def can_upgrade(config): + if config['VERSIONS_AVAILABLE'] and config['VERSIONS_AVAILABLE']['recommended_version']: + recommended_version = parse_version_string(config['VERSIONS_AVAILABLE']['recommended_version']['tag_name']) + current_version = parse_version_string(config['VERSIONS_AVAILABLE']['current_version']['tag_name']) + return recommended_version > current_version + return False + + ############################## Derived Config ################################## @@ -441,10 +491,14 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'DIR_OUTPUT_PERMISSIONS': {'default': lambda c: c['OUTPUT_PERMISSIONS'].replace('6', '7').replace('4', '5')}, 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0] or bin_path('archivebox')}, + 'VERSION': {'default': lambda c: get_version(c).split('+', 1)[0]}, 'COMMIT_HASH': {'default': lambda c: get_commit_hash(c)}, 'BUILD_TIME': {'default': lambda c: get_build_time(c)}, + 'VERSIONS_AVAILABLE': {'default': lambda c: get_versions_available_on_github(c)}, + 'CAN_UPGRADE': {'default': lambda c: can_upgrade(c)}, + '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])}, @@ -454,7 +508,7 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'SQLITE_BINARY': {'default': lambda c: inspect.getfile(sqlite3)}, 'SQLITE_VERSION': {'default': lambda c: sqlite3.version}, - #'SQLITE_JOURNAL_MODE': {'default': lambda c: 'wal'}, # set at runtime below, interesting but unused for now + #'SQLITE_JOURNAL_MODE': {'default': lambda c: 'wal'}, # set at runtime below, interesting if changed later but unused for now because its always expected to be wal #'SQLITE_OPTIONS': {'default': lambda c: ['JSON1']}, # set at runtime below 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['SAVE_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, @@ -711,9 +765,11 @@ def load_config(defaults: ConfigDefaultDict, return extended_config -# def write_config(config: ConfigDict): -# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: +def parse_version_string(version: str) -> Tuple[int, int int]: + """parses a version tag string formatted like 'vx.x.x' into (major, minor, patch) ints""" + base = v.split('+')[0].split('v')[-1] # remove 'v' prefix and '+editable' suffix + return tuple(int(part) for part in base.split('.'))[:3] # Logging Helpers diff --git a/archivebox/core/urls.py b/archivebox/core/urls.py index 87261ae2..1f3732d5 100644 --- a/archivebox/core/urls.py +++ b/archivebox/core/urls.py @@ -8,9 +8,12 @@ from django.views.generic.base import RedirectView from core.views import HomepageView, SnapshotView, PublicIndexView, AddView, HealthCheckView +from config import VERSION, VERSIONS_AVAILABLE, CAN_UPGRADE # print('DEBUG', settings.DEBUG) +GLOBAL_CONTEXT = {'VERSION': VERSION, 'VERSIONS_AVAILABLE': VERSIONS_AVAILABLE, 'CAN_UPGRADE': CAN_UPGRADE} + urlpatterns = [ path('public/', PublicIndexView.as_view(), name='public-index'), @@ -30,7 +33,7 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), - path('admin/', admin.site.urls), + path('admin/', admin.site.urls, {'extra_context': GLOBAL_CONTEXT}), path('health/', HealthCheckView.as_view(), name='healthcheck'), path('error/', lambda _: 1/0), diff --git a/archivebox/main.py b/archivebox/main.py index e806ee4d..d80203b2 100755 --- a/archivebox/main.py +++ b/archivebox/main.py @@ -99,6 +99,8 @@ from .config import ( check_data_folder, write_config_file, VERSION, + VERSIONS_AVAILABLE, + CAN_UPGRADE, COMMIT_HASH, BUILD_TIME, CODE_LOCATIONS, @@ -692,6 +694,8 @@ def add(urls: Union[str, List[str]], snapshot.save() # print(f' √ Tagged {len(imported_links)} Snapshots with {len(tags)} tags {tags_str}') + if CAN_UPGRADE: + hint(f"There's a new version of ArchiveBox available! Your current version is {VERSION}. You can upgrade to {VERSIONS_AVAILABLE['recommended_version']['tag_name']} ({VERSIONS_AVAILABLE['recommended_version']['html_url']}). For more on how to upgrade: https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives\n") return all_links @@ -1281,6 +1285,9 @@ def schedule(add: bool=False, print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) raise SystemExit(1) + if CAN_UPGRADE: + hint(f"There's a new version of ArchiveBox available! Your current version is {VERSION}. You can upgrade to {VERSIONS_AVAILABLE['recommended_version']['tag_name']} ({VERSIONS_AVAILABLE['recommended_version']['html_url']}). For more on how to upgrade: https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives\n") + @enforce_types def server(runserver_args: Optional[List[str]]=None, diff --git a/archivebox/templates/admin/base.html b/archivebox/templates/admin/base.html index 0592fa0a..c905884c 100644 --- a/archivebox/templates/admin/base.html +++ b/archivebox/templates/admin/base.html @@ -12,7 +12,26 @@ {% endblock %} - {% block extrastyle %}{% endblock %} + {% block extrastyle %} + + {% endblock %} {% if LANGUAGE_BIDI %} @@ -123,6 +142,41 @@