diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index d61538c5c..1124e3938 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -660,6 +660,10 @@ app: -f: full: --file help: Folder or tarball for upgrade + -F: + full: --force + help: Force the update, even though the app is up to date + action: store_true ### app_change_url() change-url: diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 8647002e0..95f8ddc52 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -436,9 +436,8 @@ ynh_app_upstream_version () { local manifest # Manage arguments with getopts ynh_handle_getopts_args "$@" - manifest="${manifest:-../manifest.json}" - version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") + version_key=$YNH_APP_MANIFEST_VERSION echo "${version_key/~ynh*/}" } @@ -461,9 +460,8 @@ ynh_app_package_version () { local manifest # Manage arguments with getopts ynh_handle_getopts_args "$@" - manifest="${manifest:-../manifest.json}" - version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") + version_key=$YNH_APP_MANIFEST_VERSION echo "${version_key/*~ynh/}" } @@ -516,3 +514,49 @@ ynh_check_app_version_changed () { fi echo $return_value } + +# Compare the current package version against another version given as an argument. +# This is really useful when we need to do some actions only for some old package versions. +# +# example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 +# This example will check if the installed version is lower than (lt) the version 2.3.2~ynh1 +# +# Generally you might probably use it as follow in the upgrade script +# +# if ynh_compare_current_package_version --comparaison lt --version 2.3.2~ynh1 +# then +# # Do something that is needed for the package version older than 2.3.2~ynh1 +# fi +# +# usage: ynh_compare_current_package_version --comparison lt|le|eq|ne|ge|gt +# | arg: --comparison - Comparison type. Could be : lt (lower than), le (lower or equal), +# | eq (equal), ne (not equal), ge (greater or equal), gt (greater than) +# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like 2.3.1~ynh4) +# +# Return 0 if the evaluation is true. 1 if false. +# +# Requires YunoHost version 3.8.0 or higher. +ynh_compare_current_package_version() { + local legacy_args=cv + declare -Ar args_array=( [c]=comparison= [v]=version= ) + local version + local comparison + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local current_version=$YNH_APP_CURRENT_VERSION + + # Check the syntax of the versions + if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]] + then + ynh_die "Invalid argument for version." + fi + + # Check validity of the comparator + if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then + ynh_die "Invialid comparator must be : lt, le, eq, ne, ge, gt" + fi + + # Return the return value of dpkg --compare-versions + dpkg --compare-versions $current_version $comparison $version +} diff --git a/src/yunohost/app.py b/src/yunohost/app.py index b7ae4f6ba..f9f31c64a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -184,11 +184,21 @@ def app_info(app, full=False): def _app_upgradable(app_infos): + from packaging import version # Determine upgradability # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos.get("from_catalog", None): + # Firstly use the version to know if an upgrade is available + app_is_in_catalog = bool(app_infos.get("from_catalog")) + installed_version = version.parse(app_infos.get("version", "0~ynh0")) + version_in_catalog = version.parse(app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0")) + + if app_is_in_catalog and '~ynh' in str(installed_version) and '~ynh' in str(version_in_catalog): + if installed_version < version_in_catalog: + return "yes" + + if not app_is_in_catalog: return "url_required" if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"): return "url_required" @@ -378,6 +388,7 @@ def app_change_url(operation_logger, app, domain, path): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_PATH"] = old_path @@ -441,7 +452,7 @@ def app_change_url(operation_logger, app, domain, path): hook_callback('post_app_change_url', args=args_list, env=env_dict) -def app_upgrade(app=[], url=None, file=None): +def app_upgrade(app=[], url=None, file=None, force=False): """ Upgrade app @@ -451,6 +462,7 @@ def app_upgrade(app=[], url=None, file=None): url -- Git url to fetch for upgrade """ + from packaging import version from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.permission import permission_sync_to_user from yunohost.regenconf import manually_modified_files @@ -491,12 +503,41 @@ def app_upgrade(app=[], url=None, file=None): elif app_dict["upgradable"] == "url_required": logger.warning(m18n.n('custom_app_url_required', app=app_instance_name)) continue - elif app_dict["upgradable"] == "yes": + elif app_dict["upgradable"] == "yes" or force: manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) else: logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) continue + # Manage upgrade type and avoid any upgrade if there is nothing to do + upgrade_type = "UNKNOWN" + # Get current_version and new version + app_new_version = version.parse(manifest.get("version", "?")) + app_current_version = version.parse(app_dict.get("version", "?")) + if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): + if app_current_version >= app_new_version and not force: + # In case of upgrade from file or custom repository + # No new version available + logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) + # Save update time + now = int(time.time()) + app_setting(app_instance_name, 'update_time', now) + app_setting(app_instance_name, 'current_revision', manifest.get('remote', {}).get('revision', "?")) + continue + elif app_current_version > app_new_version: + upgrade_type = "DOWNGRADE_FORCED" + elif app_current_version == app_new_version: + upgrade_type = "UPGRADE_FORCED" + else: + app_current_version_upstream, app_current_version_pkg = str(app_current_version).split("~ynh") + app_new_version_upstream, app_new_version_pkg = str(app_new_version).split("~ynh") + if app_current_version_upstream == app_new_version_upstream: + upgrade_type = "UPGRADE_PACKAGE" + elif app_current_version_pkg == app_new_version_pkg: + upgrade_type = "UPGRADE_APP" + else: + upgrade_type = "UPGRADE_FULL" + # Check requirements _check_manifest_requirements(manifest, app_instance_name=app_instance_name) _assert_system_is_sane_for_app(manifest, "pre") @@ -515,6 +556,9 @@ def app_upgrade(app=[], url=None, file=None): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type + env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) + env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() @@ -745,6 +789,9 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") + + # Start register change on system operation_logger.extra.update({'env': env_dict}) # We'll check that the app didn't brutally edit some system configuration @@ -854,6 +901,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") # Execute remove script operation_logger_remove = OperationLogger('remove_on_failed_install', @@ -1050,6 +1098,7 @@ def app_remove(operation_logger, app): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") operation_logger.extra.update({'env': env_dict}) operation_logger.flush()