Show banner and hints to upgrade when ArchiveBox is out of date (#1274)
This commit is contained in:
commit
b03ece41e9
4 changed files with 126 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -12,7 +12,26 @@
|
|||
{% endblock %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
|
||||
{% block extrastyle %}{% endblock %}
|
||||
{% block extrastyle %}
|
||||
<style>
|
||||
#upgrade-banner {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
background-color: #f8f8f8;
|
||||
color: #333333;
|
||||
border: 2px solid #772948;
|
||||
padding: 10px 20px;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
}
|
||||
#dismiss-btn {
|
||||
background: #aa1e55;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% if LANGUAGE_BIDI %}
|
||||
<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">
|
||||
|
@ -123,6 +142,41 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
{% if user.is_authenticated and user.is_superuser and CAN_UPGRADE %}
|
||||
if (!localStorage.getItem("bannerDismissed")) {
|
||||
const upgradeVersionTag = "{{VERSIONS_AVAILABLE.recommended_version.tag_name}}"
|
||||
const upgradeVersionURL = "{{VERSIONS_AVAILABLE.recommended_version.html_url}}"
|
||||
const currentVersionTag = "{{VERSION}}"
|
||||
const currentVersionURL = "{{VERSIONS_AVAILABLE.recommended_version.html_url}}"
|
||||
|
||||
createBanner(currentVersionTag, currentVersionURL, upgradeVersionTag, upgradeVersionURL)
|
||||
}
|
||||
|
||||
function createBanner(currentVersionTag, currentVersionURL, upgradeVersionTag, upgradeVersionURL) {
|
||||
const banner = document.createElement('div')
|
||||
banner.setAttribute('id', 'upgrade-banner');
|
||||
banner.innerHTML = `
|
||||
<p>There's a new version of ArchiveBox available!</p>
|
||||
Your version: <a href=${currentVersionURL}>${currentVersionTag}</a> | New version: <a href=${upgradeVersionURL}>${upgradeVersionTag}</a>
|
||||
<p>
|
||||
<a href=https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives>Upgrade Instructions</a> | <a href=https://github.com/ArchiveBox/ArchiveBox/releases>Changelog</a> | <a href=https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap>Roadmap</a>
|
||||
</p>
|
||||
<button id="dismiss-btn">Dismiss</button>
|
||||
`
|
||||
document.body.appendChild(banner);
|
||||
const dismissButton = document.querySelector("#dismiss-btn")
|
||||
if (dismissButton) {
|
||||
dismissButton.addEventListener("click", dismissBanner)
|
||||
}
|
||||
}
|
||||
|
||||
function dismissBanner() {
|
||||
const banner = document.getElementById("upgrade-banner")
|
||||
banner.style.display = "none"
|
||||
localStorage.setItem("bannerDismissed", "true")
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
$ = django.jQuery;
|
||||
$.fn.reverse = [].reverse;
|
||||
|
||||
|
|
Loading…
Reference in a new issue