diff --git a/docs/configuration.rst b/docs/configuration.rst index 346c828e2..f53266481 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -764,3 +764,26 @@ PAPERLESS_OCR_LANGUAGES= PAPERLESS_OCR_LANGUAGE=tur Defaults to none, which does not install any additional languages. + + +.. _configuration-update-checking: + +Update Checking +############### + +PAPERLESS_ENABLE_UPDATE_CHECK= + Enable (or disable) the automatic check for available updates. This feature is disabled + by default but if it is not explicitly set Paperless-ngx will show a message about this. + + If enabled, the feature works by pinging the the Github API for the latest release e.g. + https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest + to determine whether a new version is available. + + Actual updating of the app must still be performed manually. + + Note that for users of thirdy-party containers e.g. linuxserver.io this notification + may be 'ahead' of a new release from the third-party maintainers. + + In either case, no tracking data is collected by the app in any way. + + Defaults to none, which disables the feature. diff --git a/paperless.conf.example b/paperless.conf.example index b1a80de89..de24bde74 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -67,6 +67,7 @@ #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] #PAPERLESS_THUMBNAIL_FONT_NAME= #PAPERLESS_IGNORE_DATES= +#PAPERLESS_ENABLE_UPDATE_CHECK= # Tika settings diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 5889e0e24..d90d3b2d9 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -12,7 +12,7 @@
- + diff --git a/src-ui/src/app/components/app-frame/app-frame.component.scss b/src-ui/src/app/components/app-frame/app-frame.component.scss index 95cb888e7..6fe4c1ed9 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.scss +++ b/src-ui/src/app/components/app-frame/app-frame.component.scss @@ -176,3 +176,22 @@ } } } + +.version-check { + animation: pulse 2s ease-in-out 0s 1; +} + +@keyframes pulse { + 0% { + opacity: 0; + } + 25% { + opacity: 100%; + } + 75% { + opacity: 0; + } + 100% { + opacity: 100%; + } +} diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index a038c9a31..a335aad1d 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -18,6 +18,10 @@ import { DocumentDetailComponent } from '../document-detail/document-detail.comp import { Meta } from '@angular/platform-browser' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' +import { + RemoteVersionService, + AppRemoteVersion, +} from 'src/app/services/rest/remote-version.service' @Component({ selector: 'app-app-frame', @@ -32,10 +36,18 @@ export class AppFrameComponent { private searchService: SearchService, public savedViewService: SavedViewService, private list: DocumentListViewService, - private meta: Meta - ) {} + private meta: Meta, + private remoteVersionService: RemoteVersionService + ) { + this.remoteVersionService + .checkForUpdates() + .subscribe((appRemoteVersion: AppRemoteVersion) => { + this.appRemoteVersion = appRemoteVersion + }) + } versionString = `${environment.appTitle} ${environment.version}` + appRemoteVersion isMenuCollapsed: boolean = true diff --git a/src-ui/src/app/services/rest/remote-version.service.ts b/src-ui/src/app/services/rest/remote-version.service.ts new file mode 100644 index 000000000..ab1b5a66b --- /dev/null +++ b/src-ui/src/app/services/rest/remote-version.service.ts @@ -0,0 +1,23 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { map, Observable } from 'rxjs' +import { environment } from 'src/environments/environment' + +export interface AppRemoteVersion { + version: string + update_available: boolean + feature_is_set: boolean +} + +@Injectable({ + providedIn: 'root', +}) +export class RemoteVersionService { + constructor(private http: HttpClient) {} + + public checkForUpdates(): Observable { + return this.http.get( + `${environment.apiBaseUrl}remote_version/` + ) + } +} diff --git a/src-ui/src/assets/bootstrap-icons.svg b/src-ui/src/assets/bootstrap-icons.svg index f7731a14b..e94852cbd 100644 --- a/src-ui/src/assets/bootstrap-icons.svg +++ b/src-ui/src/assets/bootstrap-icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index 916524e89..cb6ee859a 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -271,6 +271,7 @@ table.table { .popover-body { background-color: var(--ngx-bg-alt); border-color: var(--bs-border-color); + color: var(--bs-body-color); } } diff --git a/src/documents/views.py b/src/documents/views.py index f2e894ee4..831e68b68 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1,6 +1,8 @@ +import json import logging import os import tempfile +import urllib import uuid import zipfile from datetime import datetime @@ -24,6 +26,8 @@ from django.views.decorators.cache import cache_control from django.views.generic import TemplateView from django_filters.rest_framework import DjangoFilterBackend from django_q.tasks import async_task +from packaging import version as packaging_version +from paperless import version from paperless.db import GnuPG from paperless.views import StandardPagination from rest_framework import parsers @@ -666,3 +670,40 @@ class BulkDownloadView(GenericAPIView): ) return response + + +class RemoteVersionView(GenericAPIView): + def get(self, request, format=None): + remote_version = "0.0.0" + is_greater_than_current = False + # TODO: this can likely be removed when frontend settings are saved to DB + feature_is_set = settings.ENABLE_UPDATE_CHECK != "default" + if feature_is_set and settings.ENABLE_UPDATE_CHECK: + try: + with urllib.request.urlopen( + "https://api.github.com/repos/" + + "paperless-ngx/paperless-ngx/releases/latest", + ) as response: + remote = response.read().decode("utf-8") + try: + remote_json = json.loads(remote) + remote_version = remote_json["tag_name"].replace("ngx-", "") + except ValueError: + logger.debug("An error occured parsing remote version json") + except urllib.error.URLError: + logger.debug("An error occured checking for available updates") + + current_version = ".".join([str(_) for _ in version.__version__[:3]]) + is_greater_than_current = packaging_version.parse( + remote_version, + ) > packaging_version.parse( + current_version, + ) + + return Response( + { + "version": remote_version, + "update_available": is_greater_than_current, + "feature_is_set": feature_is_set, + }, + ) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 475315c93..39b850813 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -566,3 +566,7 @@ if os.getenv("PAPERLESS_IGNORE_DATES", ""): d = dateparser.parse(s) if d: IGNORE_DATES.add(d.date()) + +ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default") +if ENABLE_UPDATE_CHECK != "default": + ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK") diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 9dbe39e8a..833788cce 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -14,6 +14,7 @@ from documents.views import DocumentTypeViewSet from documents.views import IndexView from documents.views import LogViewSet from documents.views import PostDocumentView +from documents.views import RemoteVersionView from documents.views import SavedViewViewSet from documents.views import SearchAutoCompleteView from documents.views import SelectionDataView @@ -72,6 +73,11 @@ urlpatterns = [ BulkDownloadView.as_view(), name="bulk_download", ), + re_path( + r"^remote_version/", + RemoteVersionView.as_view(), + name="remoteversion", + ), path("token/", views.obtain_auth_token), ] + api_router.urls,