Merge pull request #1855 from YunoHost/helpers-2.1

Helpers 2.1
This commit is contained in:
Alexandre Aubin 2024-06-20 19:07:21 +02:00 committed by GitHub
commit 2ee8d93f67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 5298 additions and 72 deletions

View file

@ -11,7 +11,7 @@ set +x
YNH_HELPERS_DIR="$SCRIPT_DIR/helpers.v${YNH_HELPERS_VERSION}.d"
case "$YNH_HELPERS_VERSION" in
"1" | "2")
"1" | "2" | "2.1")
readarray -t HELPERS < <(find -L "$YNH_HELPERS_DIR" -mindepth 1 -maxdepth 1 -type f)
for helper in "${HELPERS[@]}"; do
[ -r "$helper" ] && source "$helper"

View file

@ -174,6 +174,19 @@ ynh_mysql_user_exists() {
fi
}
# Check if a mysql database exists
#
# [internal]
#
# usage: ynh_mysql_database_exists database
# | arg: database - the database for which to check existence
# | exit: Return 1 if the database doesn't exist, 0 otherwise
#
ynh_mysql_database_exists() {
local database=$1
mysqlshow | grep -qE "^|\s+$database\s+|"
}
# Drop a user
#
# [internal]
@ -236,7 +249,7 @@ ynh_mysql_remove_db() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
if mysqlshow | grep -q "^| $db_name "; then
if ynh_mysql_database_exists "$db_name"; then
ynh_mysql_drop_db $db_name
else
ynh_print_warn --message="Database $db_name not found"

View file

@ -83,7 +83,7 @@ ynh_use_nodejs() {
# ynh_install_nodejs will install the version of node provided as argument by using n.
#
# usage: ynh_install_nodejs --nodejs_version=nodejs_version
# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed.
# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0).
#
# `n` (Node version management) uses the `PATH` variable to store the path of the version of node it is going to use.
# That's how it changes the version
@ -149,9 +149,6 @@ ynh_install_nodejs() {
# Store nodejs_version into the config of this app
ynh_app_setting_set --app=$app --key=nodejs_version --value=$nodejs_version
# Build the update script and set the cronjob
ynh_cron_upgrade_node
ynh_use_nodejs
}
@ -180,62 +177,5 @@ ynh_remove_nodejs() {
ynh_secure_remove --file="$n_install_dir"
ynh_secure_remove --file="/usr/local/n"
sed --in-place "/N_PREFIX/d" /root/.bashrc
rm --force /etc/cron.daily/node_update
fi
}
# Set a cron design to update your node versions
#
# [internal]
#
# This cron will check and update all minor node versions used by your apps.
#
# usage: ynh_cron_upgrade_node
#
# Requires YunoHost version 2.7.12 or higher.
ynh_cron_upgrade_node() {
# Build the update script
cat >"$n_install_dir/node_update.sh" <<EOF
#!/bin/bash
version_path="$node_version_path"
n_install_dir="$n_install_dir"
# Log the date
date
# List all real installed version of node
all_real_version="\$(find \$version_path/* -maxdepth 0 -type d | sed "s@\$version_path/@@g")"
# Keep only the major version number of each line
all_real_version=\$(echo "\$all_real_version" | sed 's/\..*\$//')
# Remove double entries
all_real_version=\$(echo "\$all_real_version" | sort --unique)
# Read each major version
while read version
do
echo "Update of the version \$version"
sudo \$n_install_dir/bin/n \$version
# Find the last "real" version for this major version of node.
real_nodejs_version=\$(find \$version_path/\$version* -maxdepth 0 | sort --version-sort | tail --lines=1)
real_nodejs_version=\$(basename \$real_nodejs_version)
# Update the symbolic link for this version
sudo ln --symbolic --force --no-target-directory \$version_path/\$real_nodejs_version \$version_path/\$version
done <<< "\$(echo "\$all_real_version")"
EOF
chmod +x "$n_install_dir/node_update.sh"
# Build the cronjob
cat >"/etc/cron.daily/node_update" <<EOF
#!/bin/bash
$n_install_dir/node_update.sh >> $n_install_dir/node_update.log
EOF
chmod +x "/etc/cron.daily/node_update"
}

213
helpers/helpers.v2.1.d/apps Normal file
View file

@ -0,0 +1,213 @@
#!/bin/bash
# Install others YunoHost apps
#
# usage: ynh_install_apps --apps="appfoo?domain=domain.foo&path=/foo appbar?domain=domain.bar&path=/bar&admin=USER&language=fr&is_public=1&pass?word=pass&port=666"
# | arg: -a, --apps= - apps to install
#
# Requires YunoHost version *.*.* or higher.
ynh_install_apps() {
# ============ Argument parsing =============
local -A args_array=([a]=apps=)
local apps
ynh_handle_getopts_args "$@"
# ===========================================
# Split the list of apps in an array
local apps_list=($(echo $apps | tr " " "\n"))
local apps_dependencies=""
# For each app
for one_app_and_its_args in "${apps_list[@]}"
do
# Retrieve the name of the app (part before ?)
local one_app=$(cut -d "?" -f1 <<< "$one_app_and_its_args")
[ -z "$one_app" ] && ynh_die "You didn't provided a YunoHost app to install"
yunohost tools update apps
# Installing or upgrading the app depending if it's installed or not
if ! yunohost app list --output-as json --quiet | jq -e --arg id $one_app '.apps[] | select(.id == $id)' >/dev/null
then
# Retrieve the arguments of the app (part after ?)
local one_argument=""
if [[ "$one_app_and_its_args" == *"?"* ]]; then
one_argument=$(cut -d "?" -f2- <<< "$one_app_and_its_args")
one_argument="--args $one_argument"
fi
# Install the app with its arguments
yunohost app install $one_app $one_argument
else
# Upgrade the app
yunohost app upgrade $one_app
fi
if [ ! -z "$apps_dependencies" ]
then
apps_dependencies="$apps_dependencies, $one_app"
else
apps_dependencies="$one_app"
fi
done
ynh_app_setting_set --key=apps_dependencies --value="$apps_dependencies"
}
# Remove other YunoHost apps
#
# Other YunoHost apps will be removed only if no other apps need them.
#
# usage: ynh_remove_apps
#
# Requires YunoHost version *.*.* or higher.
ynh_remove_apps() {
# Retrieve the apps dependencies of the app
local apps_dependencies=$(ynh_app_setting_get --key=apps_dependencies)
ynh_app_setting_delete --key=apps_dependencies
if [ ! -z "$apps_dependencies" ]
then
# Split the list of apps dependencies in an array
local apps_dependencies_list=($(echo $apps_dependencies | tr ", " "\n"))
# For each apps dependencies
for one_app in "${apps_dependencies_list[@]}"
do
# Retrieve the list of installed apps
local installed_apps_list=$(yunohost app list --output-as json --quiet | jq -r .apps[].id)
local required_by=""
local installed_app_required_by=""
# For each other installed app
for one_installed_app in $installed_apps_list
do
# Retrieve the other apps dependencies
one_installed_apps_dependencies=$(ynh_app_setting_get --app=$one_installed_app --key=apps_dependencies)
if [ ! -z "$one_installed_apps_dependencies" ]
then
one_installed_apps_dependencies_list=($(echo $one_installed_apps_dependencies | tr ", " "\n"))
# For each dependency of the other apps
for one_installed_app_dependency in "${one_installed_apps_dependencies_list[@]}"
do
if [[ $one_installed_app_dependency == $one_app ]]; then
required_by="$required_by $one_installed_app"
fi
done
fi
done
# If $one_app is no more required
if [[ -z "$required_by" ]]
then
# Remove $one_app
ynh_print_info "Removing of $one_app"
yunohost app remove $one_app --purge
else
ynh_print_info "$one_app was not removed because it's still required by${required_by}"
fi
done
fi
}
# Spawn a Bash shell with the app environment loaded
#
# usage: ynh_spawn_app_shell --app="app"
# | arg: -a, --app= - the app ID
#
# examples:
# ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"'
# ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash
#
# Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher.
# The spawned shell will have environment variables loaded and environment files sourced
# from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting).
# If the app relies on a specific PHP version, then `php` will be aliased that version. The PHP command will also be appended with the `phpflags` settings.
ynh_spawn_app_shell() {
# ============ Argument parsing =============
local -A args_array=([a]=app=)
local app
ynh_handle_getopts_args "$@"
# ===========================================
# Force Bash to be used to run this helper
if [[ ! $0 =~ \/?bash$ ]]
then
ynh_print_warn "Please use Bash as shell"
exit 1
fi
# Make sure the app is installed
local installed_apps_list=($(yunohost app list --output-as json --quiet | jq -r .apps[].id))
if [[ " ${installed_apps_list[*]} " != *" ${app} "* ]]
then
ynh_print_warn "$app is not in the apps list"
exit 1
fi
# Make sure the app has its own user
if ! id -u "$app" &>/dev/null; then
ynh_print_warn "There is no \"$app\" system user"
exit 1
fi
# Make sure the app has an install_dir setting
local install_dir=$(ynh_app_setting_get --key=install_dir)
if [ -z "$install_dir" ]
then
ynh_print_warn "$app has no install_dir setting (does it use packaging format >=2?)"
exit 1
fi
# Load the app's service name, or default to $app
local service=$(ynh_app_setting_get --key=service)
[ -z "$service" ] && service=$app;
# Export HOME variable
export HOME=$install_dir;
# Load the Environment variables from the app's service
local env_var=$(systemctl show $service.service -p "Environment" --value)
[ -n "$env_var" ] && export $env_var;
# Force `php` to its intended version
# We use `eval`+`export` since `alias` is not propagated to subshells, even with `export`
local php_version=$(ynh_app_setting_get --key=php_version)
local phpflags=$(ynh_app_setting_get --key=phpflags)
if [ -n "$php_version" ]
then
eval "php() { php${php_version} ${phpflags} \"\$@\"; }"
export -f php
fi
# Source the EnvironmentFiles from the app's service
local env_files=($(systemctl show $service.service -p "EnvironmentFiles" --value))
if [ ${#env_files[*]} -gt 0 ]
then
# set -/+a enables and disables new variables being automatically exported. Needed when using `source`.
set -a
for file in ${env_files[*]}
do
[[ $file = /* ]] && source $file
done
set +a
fi
# Activate the Python environment, if it exists
if [ -f $install_dir/venv/bin/activate ]
then
# set -/+a enables and disables new variables being automatically exported. Needed when using `source`.
set -a
source $install_dir/venv/bin/activate
set +a
fi
# cd into the WorkingDirectory set in the service, or default to the install_dir
local env_dir=$(systemctl show $service.service -p "WorkingDirectory" --value)
[ -z $env_dir ] && env_dir=$install_dir;
cd $env_dir
# Spawn the app shell
su -s /bin/bash $app
}

333
helpers/helpers.v2.1.d/apt Normal file
View file

@ -0,0 +1,333 @@
#!/bin/bash
YNH_APT_INSTALL_DEPENDENCIES_REPLACE="true"
# Define and install dependencies with a equivs control file
#
# [packagingv1]
#
# This helper can/should only be called once per app
#
# example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5"
#
# usage: ynh_install_app_dependencies dep [dep [...]]
# | arg: dep - the package name to install in dependence.
# | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc).
#
# Requires YunoHost version 2.6.4 or higher.
ynh_apt_install_dependencies() {
local dependencies=$@
# Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below)
dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')"
local dependencies=${dependencies//|/ | }
local version=$(ynh_read_manifest "version")
local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps
# Handle specific versions
if [[ "$dependencies" =~ [\<=\>] ]]; then
# Replace version specifications by relationships syntax
# https://www.debian.org/doc/debian-policy/ch-relationships.html
# Sed clarification
# [^(\<=\>] ignore if it begins by ( or < = >. To not apply twice.
# [\<=\>] matches < = or >
# \+ matches one or more occurence of the previous characters, for >= or >>.
# [^,]\+ matches all characters except ','
# Ex: 'package>=1.0' will be replaced by 'package (>= 1.0)'
dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')"
fi
# ############################## #
# Specific tweaks related to PHP #
# ############################## #
# Check for specific php dependencies which requires sury
# This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni"
# The (?<=php) syntax corresponds to lookbehind ;)
local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>|)' | sort -u)
if [[ -n "$specific_php_version" ]]
then
# Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing
[[ $(echo $specific_php_version | wc -l) -eq 1 ]] \
|| ynh_die "Inconsistent php versions in dependencies ... found : $specific_php_version"
dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common"
local old_php_version=$(ynh_app_setting_get --key=php_version)
# If the PHP version changed, remove the old fpm conf
if [ -n "$old_php_version" ] && [ "$old_php_version" != "$specific_php_version" ]; then
local old_php_fpm_config_dir=$(ynh_app_setting_get --key=fpm_config_dir)
local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf"
if [[ -f "$old_php_finalphpconf" ]]
then
ynh_backup_if_checksum_is_different "$old_php_finalphpconf"
ynh_remove_fpm_config
fi
fi
# Store php_version into the config of this app
ynh_app_setting_set --key=php_version --value=$specific_php_version
# Set the default php version back as the default version for php-cli.
if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION
then
update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION
fi
elif grep --quiet 'php' <<< "$dependencies"; then
ynh_app_setting_set --key=php_version --value=$YNH_DEFAULT_PHP_VERSION
fi
# Specific tweak related to Postgresql (cf end of the helper)
local psql_installed="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)"
# The first time we run ynh_install_app_dependencies, we will replace the
# entire control file (This is in particular meant to cover the case of
# upgrade script where ynh_install_app_dependencies is called with this
# expected effect) Otherwise, any subsequent call will add dependencies
# to those already present in the equivs control file.
if [[ $YNH_APT_INSTALL_DEPENDENCIES_REPLACE == "true" ]]
then
YNH_APT_INSTALL_DEPENDENCIES_REPLACE="false"
else
local current_dependencies=""
if _ynh_apt_package_is_installed "${app_ynh_deps}"
then
current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) "
current_dependencies=${current_dependencies// | /|}
fi
dependencies="$current_dependencies, $dependencies"
fi
# #############################
# Actual install using equivs #
# #############################
# Prepare the virtual-dependency control file for equivs
local TMPDIR=$(mktemp --directory)
cat >${TMPDIR}/control <<EOF # Make a control file for equivs-build
Section: misc
Priority: optional
Package: ${app_ynh_deps}
Version: ${version}
Depends: ${dependencies}
Architecture: all
Description: Fake package for ${app} (YunoHost app) dependencies
This meta-package is only responsible of installing its dependencies.
EOF
# Make sure to delete equivs' legacy compat file
# It's now handle somewhat magically through the control file
rm -f /usr/share/equivs/template/debian/compat
_ynh_apt update
_ynh_wait_dpkg_free
(
# NB: this is in a subshell (though not sure why exactly not just use pushd/popd...)
cd "$TMPDIR"
# Install the fake package without its dependencies with dpkg --force-depends
LC_ALL=C equivs-build ./control 2>&1
LC_ALL=C dpkg --force-depends --install "./${app_ynh_deps}_${version}_all.deb" 2>&1 | tee ./dpkg_log
)
# Then install the missing dependencies with apt install
_ynh_apt_install --fix-broken \
|| { # If the installation failed
# (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process)
# Parse the list of problematic dependencies from dpkg's log ...
# (relevant lines look like: "foo-ynh-deps depends on bar; however:")
local problematic_dependencies="$(cat $TMPDIR/dpkg_log | grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' | tr '\n' ' ')"
# Fake an install of those dependencies to see the errors
# The sed command here is, Print only from 'Reading state info' to the end.
[[ -n "$problematic_dependencies" ]] && _ynh_apt_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2
ynh_die "Unable to install dependencies"
}
rm --recursive --force "$TMPDIR" # Remove the temp dir.
# check if the package is actually installed
_ynh_apt_package_is_installed "${app_ynh_deps}" || ynh_die "Unable to install dependencies"
# Specific tweak related to Postgresql
# -> trigger postgresql regenconf if we may have just installed postgresql
local psql_installed2="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)"
if [[ "$psql_installed" != "$psql_installed2" ]]
then
yunohost tools regen-conf postgresql
fi
}
# Remove fake package and its dependencies
#
# [packagingv1]
#
# Dependencies will removed only if no other package need them.
#
# usage: ynh_apt_remove_dependencies
#
# Requires YunoHost version 2.6.4 or higher.
ynh_apt_remove_dependencies() {
local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps
local current_dependencies=""
if _ynh_apt_package_is_installed "${app_ynh_deps}"; then
current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) "
current_dependencies=${current_dependencies// | /|}
fi
# Edge case where the app dep may be on hold,
# cf https://forum.yunohost.org/t/migration-error-cause-of-ffsync/20675/4
if apt-mark showhold | grep -q -w ${app_ynh_deps}
then
apt-mark unhold ${app_ynh_deps}
fi
# Remove the fake package and its dependencies if they not still used.
# (except if dpkg doesn't know anything about the package,
# which should be symptomatic of a failed install, and we don't want bash to report an error)
if dpkg-query --show ${app_ynh_deps} &>/dev/null
then
_ynh_apt autoremove --purge ${app_ynh_deps}
fi
}
# Install packages from an extra repository properly.
#
# [packagingv1]
#
# usage: ynh_apt_install_dependencies_from_extra_repository --repo="repo" --package="dep1 dep2" --key=key_url
# | arg: -r, --repo= - Complete url of the extra repository.
# | arg: -p, --package= - The packages to install from this extra repository
# | arg: -k, --key= - url to get the public key.
#
# Requires YunoHost version 3.8.1 or higher.
ynh_apt_install_dependencies_from_extra_repository() {
# ============ Argument parsing =============
local -A args_array=([r]=repo= [p]=package= [k]=key=)
local repo
local package
local key
ynh_handle_getopts_args "$@"
# ===========================================
# Split the repository into uri, suite and components.
repo="${repo#deb }"
local uri="$(echo "$repo" | awk '{ print $1 }')"
local suite="$(echo "$repo" | awk '{ print $2 }')"
local component="${repo##$uri $suite }"
# Add the new repo in sources.list.d
mkdir --parents "/etc/apt/sources.list.d"
echo "deb $uri $suite $component" > "/etc/apt/sources.list.d/$app.list"
# Pin the new repo with the default priority, so it won't be used for upgrades.
# Build $pin from the uri without http and any sub path
local pin="${uri#*://}"
pin="${pin%%/*}"
# Pin repository
mkdir --parents "/etc/apt/preferences.d"
cat << EOF > "/etc/apt/preferences.d/$app"
Package: *
Pin: origin $pin
Pin-Priority: 995
EOF
mkdir --parents "/etc/apt/trusted.gpg.d"
# Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget)
wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor | tee /etc/apt/trusted.gpg.d/$name.gpg >/dev/null
# Update the list of package with the new repo NB: we use -o
# Dir::Etc::sourcelist to only refresh this repo, because
# ynh_apt_install_dependencies will also call an ynh_apt update on its own
# and it's good to limit unecessary requests ... Here we mainly want to
# validate that the url+key is correct before going further
_ynh_apt update -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/$app.list"
# Install requested dependencies from this extra repository.
# NB: because of the mechanism with $ynh_apt_install_DEPENDENCIES_REPLACE,
# this will usually only *append* to the existing list of dependency, not
# replace the existing $app-ynh-deps
ynh_apt_install_dependencies "$package"
# Force to upgrade to the last version...
# Without doing apt install, an already installed dep is not upgraded
local apps_auto_installed="$(apt-mark showauto $package)"
_ynh_apt_install "$package"
[ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed
# Remove this extra repository after packages are installed
ynh_safe_rm "/etc/apt/sources.list.d/$app.list"
ynh_safe_rm "/etc/apt/preferences.d/$app"
ynh_safe_rm "/etc/apt/trusted.gpg.d/$app.gpg"
ynh_apt_update
}
#######################
# Internal misc utils #
#######################
# Check if apt is free to use, or wait, until timeout.
_ynh_wait_dpkg_free() {
local try
set +o xtrace # set +x
# With seq 1 17, timeout will be almost 30 minutes
for try in $(seq 1 17); do
# Check if /var/lib/dpkg/lock is used by another process
if lsof /var/lib/dpkg/lock >/dev/null; then
echo "apt is already in use..."
# Sleep an exponential time at each round
sleep $((try * try))
else
# Check if dpkg hasn't been interrupted and is fully available.
# See this for more information: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174
local dpkg_dir="/var/lib/dpkg/updates/"
# For each file in $dpkg_dir
while read dpkg_file <&9; do
# Check if the name of this file contains only numbers.
if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"; then
# If so, that a remaining of dpkg.
ynh_print_warn "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem."
set -o xtrace # set -x
return 1
fi
done 9<<<"$(ls -1 $dpkg_dir)"
set -o xtrace # set -x
return 0
fi
done
echo "apt still used, but timeout reached !"
set -o xtrace # set -x
}
# Check either a package is installed or not
_ynh_apt_package_is_installed() {
local package=$1
dpkg-query --show --showformat='${db:Status-Status}' "$package" 2>/dev/null \
| grep --quiet "^installed$" &>/dev/null
}
# Return the installed version of an apt package, if installed
_ynh_apt_package_version() {
if _ynh_apt_package_is_installed "$package"; then
dpkg-query --show --showformat='${Version}' "$package" 2>/dev/null
else
echo ''
fi
}
# APT wrapper for non-interactive operation
_ynh_apt() {
_ynh_wait_dpkg_free
LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get --assume-yes --quiet -o=Acquire::Retries=3 -o=Dpkg::Use-Pty=0 $@
}
# Wrapper around "apt install" with the appropriate options
_ynh_apt_install() {
_ynh_apt --no-remove --option Dpkg::Options::=--force-confdef \
--option Dpkg::Options::=--force-confold install $@
}

View file

@ -0,0 +1,275 @@
#!/bin/bash
CAN_BIND=${CAN_BIND:-1}
# Add a file or a directory to the list of paths to backup
#
# usage: ynh_backup /path/to/stuff
#
# NB : note that this helper does *NOT* perform any copy in itself, it only
# declares stuff to be backuped via a CSV which is later picked up by the core
#
# NB 2 : there is a specific behavior for $data_dir (or childs of $data_dir) and
# /var/log/$app which are *NOT* backedup during safety-backup-before-upgrade,
# OR if the setting "do_not_backup_data" is equals 1 for that app
#
# The rationale is that these directories are usually too heavy to be integrated in every backup
# (think for example about Nextcloud with quite a lot of data, or an app with a lot of media files...)
#
# This is coupled to the fact that $data_dir and the log dir won't be (and
# should NOT) be deleted during remove, unless --purge is used. Hence, if the
# upgrade fails and the script is removed prior to restoring the backup, the
# data/logs are not destroyed.
#
ynh_backup() {
local target="$1"
local is_data=false
# If the path starts with /var/log/$app or $data_dir
if ([[ -n "${app:-}" ]] && [[ "$target" == "/var/log/$app*" ]]) || ([[ -n "${data_dir:-}" ]] && [[ "$target" == "$data_dir*" ]])
then
is_data=true
fi
if [[ -n "${app:-}" ]]
then
local do_not_backup_data=$(ynh_app_setting_get --key=do_not_backup_data)
fi
# If backing up core only (used by ynh_backup_before_upgrade),
# don't backup big data items
if [[ "$is_data" == true ]] && ([[ ${do_not_backup_data:-0} -eq 1 ]] || [[ ${BACKUP_CORE_ONLY:-0} -eq 1 ]]); then
if [ $BACKUP_CORE_ONLY -eq 1 ]; then
ynh_print_info "$target will not be saved, because 'BACKUP_CORE_ONLY' is set."
else
ynh_print_info "$target will not be saved, because 'do_not_backup_data' is set."
fi
return 1
fi
# ==============================================================================
# Format correctly source and destination paths
# ==============================================================================
# Be sure the source path is not empty
if [ ! -e "$target" ]; then
ynh_print_warn "File or folder '${target}' to be backed up does not exist"
return 1
fi
# Transform the source path as an absolute path
# If it's a dir remove the ending /
src_path=$(realpath "$target")
# Initialize the dest path with the source path relative to "/".
# eg: src_path=/etc/yunohost -> dest_path=etc/yunohost
dest_path="${src_path#/}"
# Check if dest_path already exists in tmp archive
if [[ -e "${dest_path}" ]]; then
ynh_print_warn "Destination path '${dest_path}' already exist"
return 1
fi
# Add the relative current working directory to the destination path
local rel_dir="${YNH_CWD#$YNH_BACKUP_DIR}"
rel_dir="${rel_dir%/}/"
dest_path="${rel_dir}${dest_path}"
dest_path="${dest_path#/}"
# ==============================================================================
# ==============================================================================
# Write file to backup into backup_list
# ==============================================================================
local src=$(echo "${src_path}" | sed --regexp-extended 's/"/\"\"/g')
local dest=$(echo "${dest_path}" | sed --regexp-extended 's/"/\"\"/g')
echo "\"${src}\",\"${dest}\"" >>"${YNH_BACKUP_CSV}"
# ==============================================================================
# Create the parent dir of the destination path
# It's for retro compatibility, some script consider ynh_backup creates this dir
mkdir --parents $(dirname "$YNH_BACKUP_DIR/${dest_path}")
}
# Return the path in the archive where has been stocked the origin path
#
# [internal]
#
# usage: _get_archive_path ORIGIN_PATH
_get_archive_path() {
# For security reasons we use csv python library to read the CSV
python3 -c "
import sys
import csv
with open(sys.argv[1], 'r') as backup_file:
backup_csv = csv.DictReader(backup_file, fieldnames=['source', 'dest'])
for row in backup_csv:
if row['source']==sys.argv[2].strip('\"'):
print(row['dest'])
sys.exit(0)
raise Exception('Original path for %s not found' % sys.argv[2])
" "${YNH_BACKUP_CSV}" "$1"
return $?
}
# Restore a file or a directory from the backup archive
#
# usage: ynh_restore /path/to/stuff
#
# examples:
# ynh_restore "/etc/nginx/conf.d/$domain.d/$app.conf"
#
# If the file or dir to be restored already exists on the system and is lighter
# than 500 Mo, it is backed up in `/var/cache/yunohost/appconfbackup/`.
# Otherwise, the existing file or dir is removed.
#
# if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into
# `/etc/nginx/conf.d/$domain.d/$app.conf`
# otheriwse, search for a match in the csv (eg: conf/nginx.conf) and restore it into
# `/etc/nginx/conf.d/$domain.d/$app.conf`
#
# Requires YunoHost version 2.6.4 or higher.
ynh_restore() {
target="$1"
local archive_path="$YNH_CWD${target}"
# If the path starts with /var/log/$app or $data_dir
local is_data=false
if ([[ -n "${app:-}" ]] && [[ "$target" == "/var/log/$app*" ]]) || ([[ -n "${data_dir:-}" ]] && [[ "$target" == "$data_dir*" ]])
then
is_data=true
fi
# If archive_path doesn't exist, search for a corresponding path in CSV
if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then
if [[ "$is_data" == true ]]
then
ynh_print_info "Skipping $target which doesn't exists in the archive, probably because restoring from a safety-backup-before-upgrade"
# Assume it's not a big deal, we may be restoring a safety-backup-before-upgrade which doesnt contain those
return 0
else
# (get_archive_path will raise an exception if no match found)
archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$target\")"
fi
fi
# Move the old directory if it already exists
if [[ -e "${target}" ]]; then
# Check if the file/dir size is less than 500 Mo
if [[ $(du --summarize --bytes ${target} | cut --delimiter="/" --fields=1) -le "500000000" ]]; then
local backup_file="/var/cache/yunohost/appconfbackup/${target}.backup.$(date '+%Y%m%d.%H%M%S')"
mkdir --parents "$(dirname "$backup_file")"
mv "${target}" "$backup_file" # Move the current file or directory
else
ynh_safe_rm "${target}"
fi
fi
# Restore target into target
mkdir --parents $(dirname "$target")
# Do a copy if it's just a mounting point
if mountpoint --quiet $YNH_BACKUP_DIR; then
if [[ -d "${archive_path}" ]]; then
archive_path="${archive_path}/."
mkdir --parents "$target"
fi
cp --archive "$archive_path" "${target}"
# Do a move if YNH_BACKUP_DIR is already a copy
else
mv "$archive_path" "${target}"
fi
}
# Restore all files that were previously backuped in an app backup script
#
# usage: ynh_restore_everything
#
# Requires YunoHost version 2.6.4 or higher.
ynh_restore_everything() {
# Deduce the relative path of $YNH_CWD
local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}"
REL_DIR="${REL_DIR%/}/"
# For each destination path begining by $REL_DIR
cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" \
| while read line; do
local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)")
ynh_restore "$ARCHIVE_PATH"
done
}
_ynh_file_checksum_exists() {
local file=$1
local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_'
[[ -n "$(ynh_app_setting_get --key=$checksum_setting_name)" ]]
}
# Calculate and store a file checksum into the app settings
#
# usage: ynh_store_file_checksum /path/to/file
ynh_store_file_checksum() {
local file=$1
local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_'
ynh_app_setting_set --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1)
if ynh_in_ci_tests; then
# Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ...
local file_path_base64=$(echo "$file" | base64 -w0)
mkdir -p /var/cache/yunohost/appconfbackup/
cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64}
fi
# If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup
if [ -n "${backup_file_checksum-}" ]; then
# Print the diff between the previous file and the new one.
# diff return 1 if the files are different, so the || true
diff --report-identical-files --unified --color=always $backup_file_checksum $file >&2 || true
fi
# Unset the variable, so it wouldn't trig a ynh_store_file_checksum without a ynh_backup_if_checksum_is_different before it.
unset backup_file_checksum
}
# Verify the checksum and backup the file if it's different
#
# usage: ynh_backup_if_checksum_is_different /path/to/file
#
# This helper is primarily meant to allow to easily backup personalised/manually
# modified config files.
ynh_backup_if_checksum_is_different() {
local file=$1
local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_'
local checksum_value=$(ynh_app_setting_get --key=$checksum_setting_name)
# backup_file_checksum isn't declare as local, so it can be reuse by ynh_store_file_checksum
backup_file_checksum=""
if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings
if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different
backup_file_checksum="/var/cache/yunohost/appconfbackup/$file.backup.$(date '+%Y%m%d.%H%M%S')"
mkdir --parents "$(dirname "$backup_file_checksum")"
cp --archive "$file" "$backup_file_checksum" # Backup the current file
ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum"
echo "$backup_file_checksum" # Return the name of the backup file
if ynh_in_ci_tests; then
local file_path_base64=$(echo "$file" | base64 -w0)
if test -e /var/cache/yunohost/appconfbackup/original_${file_path_base64}
then
ynh_print_warn "Diff with the original file:"
diff --report-identical-files --unified --color=always /var/cache/yunohost/appconfbackup/original_${file_path_base64} $file >&2 || true
fi
fi
fi
fi
}
# Delete a file checksum from the app settings
#
# usage: ynh_delete_file_checksum /path/to/file
ynh_delete_file_checksum() {
local file=$1
local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_'
ynh_app_setting_delete --key=$checksum_setting_name
}

View file

@ -0,0 +1,363 @@
#!/bin/bash
_ynh_app_config_get_one() {
local short_setting="$1"
local type="$2"
local bind="$3"
local getter="get__${short_setting}"
# Get value from getter if exists
if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; then
old[$short_setting]="$($getter)"
formats[${short_setting}]="yaml"
elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then
old[$short_setting]="$("get__${bind%%(*}" $short_setting $type $bind)"
formats[${short_setting}]="yaml"
elif [[ "$bind" == "null" ]]; then
old[$short_setting]="YNH_NULL"
# Get value from app settings or from another file
elif [[ "$type" == "file" ]]; then
if [[ "$bind" == "settings" ]]; then
ynh_die "File '${short_setting}' can't be stored in settings"
fi
old[$short_setting]="$(ls "$(echo $bind | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)"
file_hash[$short_setting]="true"
# Get multiline text from settings or from a full file
elif [[ "$type" == "text" ]]; then
if [[ "$bind" == "settings" ]]; then
old[$short_setting]="$(ynh_app_setting_get $app $short_setting)"
elif [[ "$bind" == *":"* ]]; then
ynh_die "For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter"
else
old[$short_setting]="$(cat $(echo $bind | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)"
fi
# Get value from a kind of key/value file
else
local bind_after=""
if [[ "$bind" == "settings" ]]; then
bind=":/etc/yunohost/apps/$app/settings.yml"
fi
local bind_key_="$(echo "$bind" | cut -d: -f1)"
bind_key_=${bind_key_:-$short_setting}
if [[ "$bind_key_" == *">"* ]]; then
bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)"
bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)"
fi
local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)"
old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")"
fi
}
_ynh_app_config_apply_one() {
local short_setting="$1"
local setter="set__${short_setting}"
local bind="${binds[$short_setting]}"
local type="${types[$short_setting]}"
if [ "${changed[$short_setting]}" == "true" ]; then
# Apply setter if exists
if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; then
$setter
elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then
"set__${bind%%(*}" $short_setting $type $bind
elif [[ "$bind" == "null" ]]; then
return
# Save in a file
elif [[ "$type" == "file" ]]; then
if [[ "$bind" == "settings" ]]; then
ynh_die "File '${short_setting}' can't be stored in settings"
fi
local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)"
if [[ "${!short_setting}" == "" ]]; then
ynh_backup_if_checksum_is_different "$bind_file"
ynh_safe_rm "$bind_file"
ynh_delete_file_checksum "$bind_file"
ynh_print_info "File '$bind_file' removed"
else
ynh_backup_if_checksum_is_different "$bind_file"
if [[ "${!short_setting}" != "$bind_file" ]]; then
cp "${!short_setting}" "$bind_file"
fi
if _ynh_file_checksum_exists "$bind_file"
then
ynh_store_file_checksum "$bind_file"
fi
ynh_print_info "File '$bind_file' overwritten with ${!short_setting}"
fi
# Save value in app settings
elif [[ "$bind" == "settings" ]]; then
ynh_app_setting_set --key=$short_setting --value="${!short_setting}"
ynh_print_info "Configuration key '$short_setting' edited in app settings"
# Save multiline text in a file
elif [[ "$type" == "text" ]]; then
if [[ "$bind" == *":"* ]]; then
ynh_die "For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter"
fi
local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)"
ynh_backup_if_checksum_is_different "$bind_file"
echo "${!short_setting}" >"$bind_file"
if _ynh_file_checksum_exists "$bind_file"
then
ynh_store_file_checksum "$bind_file"
fi
ynh_print_info "File '$bind_file' overwritten with the content provided in question '${short_setting}'"
# Set value into a kind of key/value file
else
local bind_after=""
local bind_key_="$(echo "$bind" | cut -d: -f1)"
if [[ "$bind_key_" == *">"* ]]; then
bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)"
bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)"
fi
bind_key_=${bind_key_:-$short_setting}
local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)"
ynh_backup_if_checksum_is_different "$bind_file"
ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}"
if _ynh_file_checksum_exists "$bind_file"
then
ynh_store_file_checksum "$bind_file"
fi
# We stored the info in settings in order to be able to upgrade the app
ynh_app_setting_set --key=$short_setting --value="${!short_setting}"
ynh_print_info "Configuration key '$bind_key_' edited into $bind_file"
fi
fi
}
_ynh_app_config_get() {
# From settings
local lines
lines=$(
python3 <<EOL
import toml
from collections import OrderedDict
with open("../config_panel.toml", "r") as f:
file_content = f.read()
loaded_toml = toml.loads(file_content, _dict=OrderedDict)
for panel_name, panel in loaded_toml.items():
if not isinstance(panel, dict): continue
bind_panel = panel.get('bind')
for section_name, section in panel.items():
if not isinstance(section, dict): continue
bind_section = section.get('bind')
if not bind_section:
bind_section = bind_panel
elif bind_section[-1] == ":" and bind_panel and ":" in bind_panel:
regex, bind_panel_file = bind_panel.split(":")
if ">" in bind_section:
bind_section = bind_section + bind_panel_file
else:
bind_section = regex + bind_section + bind_panel_file
for name, param in section.items():
if not isinstance(param, dict):
continue
bind = param.get('bind')
if not bind:
if bind_section:
bind = bind_section
else:
bind = 'settings'
elif bind[-1] == ":" and bind_section and ":" in bind_section:
regex, bind_file = bind_section.split(":")
if ">" in bind:
bind = bind + bind_file
else:
bind = regex + bind + bind_file
if bind == "settings" and param.get('type', 'string') == 'file':
bind = 'null'
print('|'.join([
name,
param.get('type', 'string'),
bind
]))
EOL
)
for line in $lines; do
# Split line into short_setting, type and bind
IFS='|' read short_setting type bind <<<"$line"
binds[${short_setting}]="$bind"
types[${short_setting}]="$type"
file_hash[${short_setting}]=""
formats[${short_setting}]=""
ynh_app_config_get_one $short_setting $type $bind
done
}
_ynh_app_config_apply() {
for short_setting in "${!old[@]}"; do
ynh_app_config_apply_one $short_setting
done
}
_ynh_app_config_show() {
for short_setting in "${!old[@]}"; do
if [[ "${old[$short_setting]}" != YNH_NULL ]]; then
if [[ "${formats[$short_setting]}" == "yaml" ]]; then
ynh_return "${short_setting}:"
ynh_return "$(echo "${old[$short_setting]}" | sed 's/^/ /g')"
else
ynh_return "${short_setting}: '$(echo "${old[$short_setting]}" | sed "s/'/''/g" | sed ':a;N;$!ba;s/\n/\n\n/g')'"
fi
fi
done
}
_ynh_app_config_validate() {
# Change detection
ynh_script_progression "Checking what changed in the new configuration..."
local nothing_changed=true
local changes_validated=true
for short_setting in "${!old[@]}"; do
changed[$short_setting]=false
if [ -z ${!short_setting+x} ]; then
# Assign the var with the old value in order to allows multiple
# args validation
declare -g "$short_setting"="${old[$short_setting]}"
continue
fi
if [ ! -z "${file_hash[${short_setting}]}" ]; then
file_hash[old__$short_setting]=""
file_hash[new__$short_setting]=""
if [ -f "${old[$short_setting]}" ]; then
file_hash[old__$short_setting]=$(sha256sum "${old[$short_setting]}" | cut -d' ' -f1)
if [ -z "${!short_setting}" ]; then
changed[$short_setting]=true
nothing_changed=false
fi
fi
if [ -f "${!short_setting}" ]; then
file_hash[new__$short_setting]=$(sha256sum "${!short_setting}" | cut -d' ' -f1)
if [[ "${file_hash[old__$short_setting]}" != "${file_hash[new__$short_setting]}" ]]; then
changed[$short_setting]=true
nothing_changed=false
fi
fi
else
if [[ "${!short_setting}" != "${old[$short_setting]}" ]]; then
changed[$short_setting]=true
nothing_changed=false
fi
fi
done
if [[ "$nothing_changed" == "true" ]]; then
ynh_print_info "Nothing has changed"
exit 0
fi
# Run validation if something is changed
ynh_script_progression "Validating the new configuration..."
for short_setting in "${!old[@]}"; do
[[ "${changed[$short_setting]}" == "false" ]] && continue
local result=""
if type -t validate__$short_setting | grep -q '^function$' 2>/dev/null; then
result="$(validate__$short_setting)"
elif [[ "$bind" == *"("* ]] && type -t "validate__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then
"validate__${bind%%(*}" $short_setting
fi
if [ -n "$result" ]; then
#
# Return a yaml such as:
#
# validation_errors:
# some_key: "An error message"
# some_other_key: "Another error message"
#
# We use changes_validated to know if this is
# the first validation error
if [[ "$changes_validated" == true ]]; then
ynh_return "validation_errors:"
fi
ynh_return " ${short_setting}: \"$result\""
changes_validated=false
fi
done
# If validation failed, exit the script right now (instead of going into apply)
# Yunohost core will pick up the errors returned via ynh_return previously
if [[ "$changes_validated" == "false" ]]; then
exit 0
fi
}
ynh_app_config_get_one() {
_ynh_app_config_get_one $1 $2 $3
}
ynh_app_config_get() {
_ynh_app_config_get
}
ynh_app_config_show() {
_ynh_app_config_show
}
ynh_app_config_validate() {
_ynh_app_config_validate
}
ynh_app_config_apply_one() {
_ynh_app_config_apply_one $1
}
ynh_app_config_apply() {
_ynh_app_config_apply
}
ynh_app_action_run() {
local runner="run__$1"
# Get value from getter if exists
if type -t "$runner" 2>/dev/null | grep -q '^function$' 2>/dev/null; then
$runner
#ynh_return "result:"
#ynh_return "$(echo "${result}" | sed 's/^/ /g')"
else
ynh_die "No handler defined in app's script for action $1. If you are the maintainer of this app, you should define '$runner'"
fi
}
ynh_app_config_run() {
declare -Ag old=()
declare -Ag changed=()
declare -Ag file_hash=()
declare -Ag binds=()
declare -Ag types=()
declare -Ag formats=()
case $1 in
show)
ynh_app_config_get
ynh_app_config_show
;;
apply)
max_progression=4
ynh_script_progression "Reading config panel description and current configuration..."
ynh_app_config_get
ynh_app_config_validate
ynh_script_progression "Applying the new configuration..."
ynh_app_config_apply
ynh_script_progression "Configuration of $app completed"
;;
*)
ynh_app_action_run $1
esac
}

View file

@ -0,0 +1,126 @@
#!/bin/bash
# Create a dedicated fail2ban config (jail and filter conf files)
#
# usage 1: ynh_config_add_fail2ban --logpath=log_file --failregex=filter
# | arg: -l, --logpath= - Log file to be checked by fail2ban
# | arg: -r, --failregex= - Failregex to be looked for by fail2ban
#
# usage 2: ynh_config_add_fail2ban
# | arg: -t, --use_template - Use this helper in template mode
#
# This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf`
# See the documentation of `ynh_config_add` for a description of the template
# format and how placeholders are replaced with actual variables.
#
# Generally your template will look like that by example (for synapse):
# ```
# f2b_jail.conf:
# [__APP__]
# enabled = true
# port = http,https
# filter = __APP__
# logpath = /var/log/__APP__/logfile.log
# maxretry = 5
# ```
# ```
# f2b_filter.conf:
# [INCLUDES]
# before = common.conf
# [Definition]
#
# # Part of regex definition (just used to make more easy to make the global regex)
# __synapse_start_line = .? \- synapse\..+ \-
#
# # Regex definition.
# failregex = ^%(__synapse_start_line)s INFO \- POST\-(\d+)\- <HOST> \- \d+ \- Received request\: POST /_matrix/client/r0/login\??<SKIPLINES>%(__synapse_start_line)s INFO \- POST\-\1\- Got login request with identifier: \{u'type': u'm.id.user', u'user'\: u'(.+?)'\}, medium\: None, address: None, user\: u'\5'<SKIPLINES>%(__synapse_start_line)s WARNING \- \- (Attempted to login as @\5\:.+ but they do not exist|Failed password login for user @\5\:.+)$
#
# ignoreregex =
# ```
#
# -----------------------------------------------------------------------------
#
# Note about the "failregex" option:
#
# regex to match the password failure messages in the logfile. The host must be
# matched by a group named "`host`". The tag "`<HOST>`" can be used for standard
# IP/hostname matching and is only an alias for `(?:::f{4,6}:)?(?P<host>[\w\-.^_]+)`
#
# You can find some more explainations about how to make a regex here :
# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters
#
# To validate your regex you can test with this command:
# ```
# fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf
# ```
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add_fail2ban() {
# ============ Argument parsing =============
local -A args_array=([l]=logpath= [r]=failregex=)
local logpath
local failregex
ynh_handle_getopts_args "$@"
# ===========================================
# If failregex is provided, Build a config file on-the-fly using $logpath and $failregex
if [[ -n "${failregex:-}" ]]; then
test -n "$logpath" || ynh_die "ynh_config_add_fail2ban expects a logfile path as first argument and received nothing."
echo "
[__APP__]
enabled = true
port = http,https
filter = __APP__
logpath = __LOGPATH__
maxretry = 5
" >"$YNH_APP_BASEDIR/conf/f2b_jail.conf"
echo "
[INCLUDES]
before = common.conf
[Definition]
failregex = __FAILREGEX__
ignoreregex =
" >"$YNH_APP_BASEDIR/conf/f2b_filter.conf"
fi
ynh_config_add --template="f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf"
ynh_config_add --template="f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf"
# if "$logpath" doesn't exist (as if using --use_template argument), assign
# "$logpath" using the one in the previously generated fail2ban conf file
if [ -z "${logpath:-}" ]; then
# the first sed deletes possibles spaces and the second one extract the path
logpath=$(grep "^logpath" "/etc/fail2ban/jail.d/$app.conf" | sed "s/ //g" | sed "s/logpath=//g")
fi
# Create the folder and logfile if they doesn't exist,
# as fail2ban require an existing logfile before configuration
mkdir -p "/var/log/$app"
if [ ! -f "$logpath" ]; then
touch "$logpath"
fi
# Make sure log folder's permissions are correct
chown -R "$app:$app" "/var/log/$app"
chmod -R u=rwX,g=rX,o= "/var/log/$app"
ynh_systemctl --service=fail2ban --action=reload --wait_until="(Started|Reloaded) Fail2Ban Service" --log_path=systemd
local fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")"
if [[ -n "$fail2ban_error" ]]; then
ynh_print_warn "Fail2ban failed to load the jail for $app"
ynh_print_warn "${fail2ban_error#*WARNING}"
fi
}
# Remove the dedicated fail2ban config (jail and filter conf files)
#
# usage: ynh_config_remove_fail2ban
#
# Requires YunoHost version 3.5.0 or higher.
ynh_config_remove_fail2ban() {
ynh_safe_rm "/etc/fail2ban/jail.d/$app.conf"
ynh_safe_rm "/etc/fail2ban/filter.d/$app.conf"
ynh_systemctl --service=fail2ban --action=reload
}

View file

@ -0,0 +1,185 @@
#!/bin/bash
# Internal helper design to allow helpers to use getopts to manage their arguments
#
# [internal]
#
# example: function my_helper()
# {
# local -A args_array=( [a]=arg1= [b]=arg2= [c]=arg3 )
# local arg1
# local arg2
# local arg3
# ynh_handle_getopts_args "$@"
#
# [...]
# }
# my_helper --arg1 "val1" -b val2 -c
#
# usage: ynh_handle_getopts_args "$@"
# | arg: $@ - Simply "$@" to tranfert all the positionnal arguments to the function
#
# This helper need an array, named "args_array" with all the arguments used by the helper
# that want to use ynh_handle_getopts_args
# Be carreful, this array has to be an associative array, as the following example:
# local -A args_array=( [a]=arg1 [b]=arg2= [c]=arg3 )
# Let's explain this array:
# a, b and c are short options, -a, -b and -c
# arg1, arg2 and arg3 are the long options associated to the previous short ones. --arg1, --arg2 and --arg3
# For each option, a short and long version has to be defined.
# Let's see something more significant
# local -A args_array=( [u]=user [f]=finalpath= [d]=database )
#
# NB: Because we're using 'declare' without -g, the array will be declared as a local variable.
#
# Please keep in mind that the long option will be used as a variable to store the values for this option.
# For the previous example, that means that $finalpath will be fill with the value given as argument for this option.
#
# Also, in the previous example, finalpath has a '=' at the end. That means this option need a value.
# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path
# If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path
# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1.
#
# To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments.
# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array.
# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2.
#
# Requires YunoHost version 3.2.2 or higher.
ynh_handle_getopts_args() {
# Manage arguments only if there's some provided
set +o xtrace # set +x
if [ $# -eq 0 ]; then
set -o xtrace # set -x
return
fi
# Store arguments in an array to keep each argument separated
local arguments=("$@")
# For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u)
# And built parameters string for getopts
# ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value)
local getopts_parameters=""
local option_flag=""
for option_flag in "${!args_array[@]}"; do
# Concatenate each option_flags of the array to build the string of arguments for getopts
# Will looks like 'abcd' for -a -b -c -d
# If the value of an option_flag finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob)
# Check the last character of the value associate to the option_flag
if [ "${args_array[$option_flag]: -1}" = "=" ]; then
# For an option with additionnal values, add a ':' after the letter for getopts.
getopts_parameters="${getopts_parameters}${option_flag}:"
else
getopts_parameters="${getopts_parameters}${option_flag}"
fi
# Check each argument given to the function
local arg=""
# ${#arguments[@]} is the size of the array
for arg in $(seq 0 $((${#arguments[@]} - 1))); do
# Escape options' values starting with -. Otherwise the - will be considered as another option.
arguments[arg]="${arguments[arg]//--${args_array[$option_flag]}-/--${args_array[$option_flag]}\\TOBEREMOVED\\-}"
# And replace long option (value of the option_flag) by the short option, the option_flag itself
# (e.g. for [u]=user, --user will be -u)
# Replace long option with = (match the beginning of the argument)
arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")"
# And long option without = (match the whole line)
arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")"
done
done
# Read and parse all the arguments
# Use a function here, to use standart arguments $@ and be able to use shift.
parse_arg() {
# Read all arguments, until no arguments are left
while [ $# -ne 0 ]; do
# Initialize the index of getopts
OPTIND=1
# Parse with getopts only if the argument begin by -, that means the argument is an option
# getopts will fill $parameter with the letter of the option it has read.
local parameter=""
getopts ":$getopts_parameters" parameter || true
if [ "$parameter" = "?" ]; then
ynh_die "Invalid argument: -${OPTARG:-}"
elif [ "$parameter" = ":" ]; then
ynh_die "-$OPTARG parameter requires an argument."
else
local shift_value=1
# Use the long option, corresponding to the short option read by getopts, as a variable
# (e.g. for [u]=user, 'user' will be used as a variable)
# Also, remove '=' at the end of the long option
# The variable name will be stored in 'option_var'
local option_var="${args_array[$parameter]%=}"
# If this option doesn't take values
# if there's a '=' at the end of the long option name, this option takes values
if [ "${args_array[$parameter]: -1}" != "=" ]; then
# 'eval ${option_var}' will use the content of 'option_var'
eval ${option_var}=1
else
# Read all other arguments to find multiple value for this option.
# Load args in a array
local all_args=("$@")
# If the first argument is longer than 2 characters,
# There's a value attached to the option, in the same array cell
if [ ${#all_args[0]} -gt 2 ]; then
# Remove the option and the space, so keep only the value itself.
all_args[0]="${all_args[0]#-${parameter} }"
# At this point, if all_args[0] start with "-", then the argument is not well formed
if [ "${all_args[0]:0:1}" == "-" ]; then
ynh_die "Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?"
fi
# Reduce the value of shift, because the option has been removed manually
shift_value=$((shift_value - 1))
fi
# Declare the content of option_var as a variable.
eval ${option_var}=""
# Then read the array value per value
local i
for i in $(seq 0 $((${#all_args[@]} - 1))); do
# If this argument is an option, end here.
if [ "${all_args[$i]:0:1}" == "-" ]; then
# Ignore the first value of the array, which is the option itself
if [ "$i" -ne 0 ]; then
break
fi
else
# Ignore empty parameters
if [ -n "${all_args[$i]}" ]; then
# Else, add this value to this option
# Each value will be separated by ';'
if [ -n "${!option_var}" ]; then
# If there's already another value for this option, add a ; before adding the new value
eval ${option_var}+="\;"
fi
# Remove the \ that escape - at beginning of values.
all_args[i]="${all_args[i]//\\TOBEREMOVED\\/}"
# For the record.
# We're using eval here to get the content of the variable stored itself as simple text in $option_var...
# Other ways to get that content would be to use either ${!option_var} or declare -g ${option_var}
# But... ${!option_var} can't be used as left part of an assignation.
# declare -g ${option_var} will create a local variable (despite -g !) and will not be available for the helper itself.
# So... Stop fucking arguing each time that eval is evil... Go find an other working solution if you can find one!
eval ${option_var}+='"${all_args[$i]}"'
fi
shift_value=$((shift_value + 1))
fi
done
fi
fi
# Shift the parameter and its argument(s)
shift $shift_value
done
}
# Call parse_arg and pass the modified list of args as an array of arguments.
parse_arg "${arguments[@]}"
set -o xtrace # set -x
}

197
helpers/helpers.v2.1.d/go Normal file
View file

@ -0,0 +1,197 @@
#!/bin/bash
ynh_go_try_bash_extension() {
if [ -x src/configure ]; then
src/configure && make -C src || {
ynh_print_info "Optional bash extension failed to build, but things will still work normally."
}
fi
}
goenv_install_dir="/opt/goenv"
go_version_path="$goenv_install_dir/versions"
# goenv_ROOT is the directory of goenv, it needs to be loaded as a environment variable.
export GOENV_ROOT="$goenv_install_dir"
_ynh_load_go_in_path_and_other_tweaks() {
# Get the absolute path of this version of go
local go_path="$go_version_path/$app/bin"
# Load the path of this version of go in $PATH
if [[ :$PATH: != *":$go_path"* ]]; then
PATH="$go_path:$PATH"
fi
# Export PATH such that it's available through sudo -E / ynh_exec_as $app
export PATH
# This is in full lowercase such that it gets replaced in templates
path_with_go="$PATH"
PATH_with_go="$PATH"
# Sets the local application-specific go version
pushd ${install_dir}
$goenv_install_dir/bin/goenv local $go_version
popd
}
# Install a specific version of Go using goenv
#
# The installed version is defined by $nodejs_version which should be defined as global prior to calling this helper
#
# This helper creates a /etc/profile.d/goenv.sh that configures PATH environment for goenv
# for every LOGIN user, hence your user must have a defined shell (as opposed to /usr/sbin/nologin)
#
# Don't forget to execute go-dependent command in a login environment
# (e.g. sudo --login option)
# When not possible (e.g. in systemd service definition), please use direct path
# to goenv shims (e.g. $goenv_ROOT/shims/bundle)
#
# usage: ynh_go_install
#
# Requires YunoHost version 3.2.2 or higher.
ynh_go_install () {
[[ -n "${go_version:-}" ]] || ynh_die "\$go_version should be defined prior to calling ynh_go_install"
# Load goenv path in PATH
local CLEAR_PATH="$goenv_install_dir/bin:$PATH"
# Remove /usr/local/bin in PATH in case of Go prior installation
PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
# Move an existing Go binary, to avoid to block goenv
test -x /usr/bin/go && mv /usr/bin/go /usr/bin/go_goenv
# Install or update goenv
mkdir -p $goenv_install_dir
pushd "$goenv_install_dir"
if ! [ -x "$goenv_install_dir/bin/goenv" ]; then
ynh_print_info "Downloading goenv..."
git init -q
git remote add origin https://github.com/syndbg/goenv.git
else
ynh_print_info "Updating goenv..."
fi
git fetch -q --tags --prune origin
local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
git checkout -q "$git_latest_tag"
ynh_go_try_bash_extension
goenv=$goenv_install_dir/bin/goenv
popd
# Install or update xxenv-latest
mkdir -p "$goenv_install_dir/plugins/xxenv-latest"
pushd "$goenv_install_dir/plugins/xxenv-latest"
if ! [ -x "$goenv_install_dir/plugins/xxenv-latest/bin/goenv-latest" ]; then
ynh_print_info "Downloading xxenv-latest..."
git init -q
git remote add origin https://github.com/momo-lab/xxenv-latest.git
else
ynh_print_info "Updating xxenv-latest..."
fi
git fetch -q --tags --prune origin
local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
git checkout -q "$git_latest_tag"
popd
# Enable caching
mkdir -p "${goenv_install_dir}/cache"
# Create shims directory if needed
mkdir -p "${goenv_install_dir}/shims"
# Restore /usr/local/bin in PATH
PATH=$CLEAR_PATH
# And replace the old Go binary
test -x /usr/bin/go_goenv && mv /usr/bin/go_goenv /usr/bin/go
# Install the requested version of Go
local final_go_version=$(goenv latest --print "$go_version")
ynh_print_info "Installation of Go-$final_go_version"
goenv install --quiet --skip-existing "$final_go_version" 2>&1
# Store go_version into the config of this app
ynh_app_setting_set --app="$app" --key="go_version" --value="$final_go_version"
go_version=$final_go_version
# Cleanup Go versions
_ynh_go_cleanup
# Set environment for Go users
echo "#goenv
export GOENV_ROOT=$goenv_install_dir
export PATH=\"$goenv_install_dir/bin:$PATH\"
eval \"\$(goenv init -)\"
#goenv" > /etc/profile.d/goenv.sh
# Load the environment
eval "$(goenv init -)"
_ynh_load_go_in_path_and_other_tweaks
}
# Remove the version of Go used by the app.
#
# This helper will also cleanup Go versions
#
# usage: ynh_go_remove
ynh_go_remove () {
local go_version=$(ynh_app_setting_get --key="go_version")
# Load goenv path in PATH
local CLEAR_PATH="$goenv_install_dir/bin:$PATH"
# Remove /usr/local/bin in PATH in case of Go prior installation
PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
# Remove the line for this app
ynh_app_setting_delete --key="go_version"
# Cleanup Go versions
_ynh_go_cleanup
}
# Remove no more needed versions of Go used by the app.
#
# This helper will check what Go version are no more required,
# and uninstall them
# If no app uses Go, goenv will be also removed.
#
# usage: _ynh_go_cleanup
_ynh_go_cleanup () {
# List required Go versions
local installed_apps=$(yunohost app list --output-as json --quiet | jq -r .apps[].id)
local required_go_versions=""
for installed_app in $installed_apps
do
local installed_app_go_version=$(ynh_app_setting_get --app=$installed_app --key="go_version")
if [[ $installed_app_go_version ]]
then
required_go_versions="${installed_app_go_version}\n${required_go_versions}"
fi
done
# Remove no more needed Go versions
local installed_go_versions=$(goenv versions --bare --skip-aliases | grep -Ev '/')
for installed_go_version in $installed_go_versions
do
if ! `echo ${required_go_versions} | grep "${installed_go_version}" 1>/dev/null 2>&1`
then
ynh_print_info "Removing of Go-$installed_go_version"
$goenv_install_dir/bin/goenv uninstall --force "$installed_go_version"
fi
done
# If none Go version is required
if [[ ! $required_go_versions ]]
then
# Remove goenv environment configuration
ynh_print_info "Removing of goenv"
ynh_safe_rm "$goenv_install_dir"
ynh_safe_rm "/etc/profile.d/goenv.sh"
fi
}

View file

@ -0,0 +1,118 @@
#!/bin/bash
# Print a message to stderr and terminate the current script
#
# usage: ynh_die "Some message"
ynh_die() {
echo "$1" 1>&2
exit 1
}
# Print an "INFO" message
#
# usage: ynh_print_info "Some message"
ynh_print_info() {
echo "$1" >&$YNH_STDINFO
}
# Print a warning on stderr
#
# usage: ynh_print_warn "Some message"
ynh_print_warn() {
echo "$1" >&2
}
# Execute a command and redirect stderr to stdout
#
# usage: ynh_hide_warnings your command and args
# | arg: command - command to execute
#
ynh_hide_warnings() {
# Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077
"$@" 2>&1
}
# Execute a command and redirect stderr in /dev/null. Print stderr on error.
#
# usage: ynh_exec_and_print_stderr_only_if_error your command and args
# | arg: command - command to execute
#
# Note that you should NOT quote the command but only prefix it with ynh_exec_and_print_stderr_only_if_error
#
# Requires YunoHost version 11.2 or higher.
ynh_exec_and_print_stderr_only_if_error() {
logfile="$(mktemp)"
rc=0
# Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077
"$@" 2> "$logfile" || rc="$?"
if (( rc != 0 )); then
cat "$logfile" >&2
ynh_safe_rm "$logfile"
return "$rc"
fi
}
# Return data to the YunoHost core for later processing
# (to be used by special hooks like app config panel and core diagnosis)
#
# usage: ynh_return somedata
#
# Requires YunoHost version 3.6.0 or higher.
ynh_return() {
echo "$1" >>"$YNH_STDRETURN"
}
# Initial definitions for ynh_script_progression
increment_progression=0
previous_weight=0
max_progression=-1
# Set the scale of the progression bar
# progress_string(0,1,2) should have the size of the scale.
progress_scale=20
progress_string2="####################"
progress_string1="++++++++++++++++++++"
progress_string0="...................."
# Print a progress bar showing the progression of an app script
#
# usage: ynh_script_progression "Some message"
#
# Requires YunoHost version 3.5.0 or higher.
ynh_script_progression() {
set +o xtrace # set +x
# Compute $max_progression (if we didn't already)
if [ "$max_progression" = -1 ]; then
# Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented.
local helper_calls=
max_progression="$(grep --count "^[^#]*ynh_script_progression" $0)"
fi
# Increment each execution of ynh_script_progression in this script by the weight of the previous call.
increment_progression=$(($increment_progression + $previous_weight))
# Store the weight of the current call in $previous_weight for next call
previous_weight=1
# Reduce $increment_progression to the size of the scale
local effective_progression=$(($increment_progression * $progress_scale / $max_progression))
# If last is specified, fill immediately the progression_bar
# Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task
# expected_progression is the progression expected after the current task
local expected_progression="$((($increment_progression + 1) * $progress_scale / $max_progression - $effective_progression))"
# Hack for the "--last" message
if grep -qw 'completed' <<< "$1";
then
effective_progression=$progress_scale
expected_progression=0
fi
# left_progression is the progression not yet done
local left_progression="$(($progress_scale - $effective_progression - $expected_progression))"
# Build the progression bar with $effective_progression, work done, $expected_progression, current work and $left_progression, work to be done.
local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}"
echo "[$progression_bar] > ${1}" >&$YNH_STDINFO
set -o xtrace # set -x
}

View file

@ -0,0 +1,77 @@
#!/bin/bash
FIRST_CALL_TO_LOGROTATE="true"
# Add a logrotate configuration to manage log files / log directory
#
# usage: ynh_config_add_logrotate [/path/to/log/file/or/folder]
#
# If not argument is provided, `/var/log/$app/*.log` is used as default.
#
# The configuration is autogenerated by YunoHost
# (ie it doesnt come from a specific app template like nginx or systemd conf)
#
# Requires YunoHost version 2.6.4 or higher.
ynh_config_add_logrotate() {
logfile="$1"
set -o noglob
if [[ -z "$logfile" ]]; then
logfile="/var/log/${app}/*.log"
elif [[ "${logfile##*.}" != "log" ]] && [[ "${logfile##*.}" != "txt" ]]; then
logfile="$logfile/*.log"
fi
set +o noglob
for stuff in $logfile
do
mkdir --parents $(dirname "$stuff")
# Make sure the permissions of the parent dir are correct (otherwise the config file could be ignored and the corresponding logs never rotated)
chmod 750 $(dirname "$stuff")
done
local tempconf="$(mktemp)"
cat << EOF >$tempconf
$logfile {
# Rotate if the logfile exceeds 100Mo
size 100M
# Keep 12 old log maximum
rotate 12
# Compress the logs with gzip
compress
# Compress the log at the next cycle. So keep always 2 non compressed logs
delaycompress
# Copy and truncate the log to allow to continue write on it. Instead of moving the log.
copytruncate
# Do not trigger an error if the log is missing
missingok
# Do not rotate if the log is empty
notifempty
# Keep old logs in the same dir
noolddir
}
EOF
if [[ "$FIRST_CALL_TO_LOGROTATE" == "true" ]]
then
cat $tempconf > /etc/logrotate.d/$app
else
cat $tempconf >> /etc/logrotate.d/$app
fi
FIRST_CALL_TO_LOGROTATE="false"
chmod 644 "/etc/logrotate.d/$app"
}
# Remove the app's logrotate config.
#
# usage: ynh_remove_logrotate
#
# Requires YunoHost version 2.6.4 or higher.
ynh_config_remove_logrotate() {
if [ -e "/etc/logrotate.d/$app" ]; then
rm "/etc/logrotate.d/$app"
fi
}

View file

@ -0,0 +1,271 @@
#!/bin/bash
# Execute a mongo command
#
# example: ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("wekan")'
# example: ynh_mongo_exec --command="db.getMongo().getDBNames().indexOf(\"wekan\")"
#
# usage: ynh_mongo_exec [--database=database] --command="command"
# | arg: -d, --database= - The database to connect to
# | arg: -c, --command= - The command to evaluate
#
#
ynh_mongo_exec() {
# ============ Argument parsing =============
local -A args_array=( [d]=database= [c]=command= )
local database
local command
ynh_handle_getopts_args "$@"
database="${database:-}"
# ===========================================
if [ -n "$database" ]
then
mongosh --quiet <<EOF
use $database
${command}
quit()
EOF
else
mongosh --quiet --eval="$command"
fi
}
# Drop a database
#
# [internal]
#
# If you intend to drop the database *and* the associated user,
# consider using ynh_mongo_remove_db instead.
#
# usage: ynh_mongo_drop_db --database=database
# | arg: -d, --database= - The database name to drop
#
#
ynh_mongo_drop_db() {
# ============ Argument parsing =============
local -A args_array=( [d]=database= )
local database
ynh_handle_getopts_args "$@"
# ===========================================
ynh_mongo_exec --database="$database" --command='db.runCommand({dropDatabase: 1})'
}
# Dump a database
#
# example: ynh_mongo_dump_db --database=wekan > ./dump.bson
#
# usage: ynh_mongo_dump_db --database=database
# | arg: -d, --database= - The database name to dump
# | ret: the mongodump output
#
#
ynh_mongo_dump_db() {
# ============ Argument parsing =============
local -A args_array=( [d]=database= )
local database
ynh_handle_getopts_args "$@"
# ===========================================
mongodump --quiet --db="$database" --archive
}
# Create a user
#
# [internal]
#
# usage: ynh_mongo_create_user --db_user=user --db_pwd=pwd --db_name=name
# | arg: -u, --db_user= - The user name to create
# | arg: -p, --db_pwd= - The password to identify user by
# | arg: -n, --db_name= - Name of the database to grant privilegies
#
#
ynh_mongo_create_user() {
# ============ Argument parsing =============
local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= )
local db_user
local db_name
local db_pwd
ynh_handle_getopts_args "$@"
# ===========================================
# Create the user and set the user as admin of the db
ynh_mongo_exec --database="$db_name" --command='db.createUser( { user: "'${db_user}'", pwd: "'${db_pwd}'", roles: [ { role: "readWrite", db: "'${db_name}'" } ] } );'
# Add clustermonitoring rights
ynh_mongo_exec --database="$db_name" --command='db.grantRolesToUser("'${db_user}'",[{ role: "clusterMonitor", db: "admin" }]);'
}
# Check if a mongo database exists
#
# usage: ynh_mongo_database_exists --database=database
# | arg: -d, --database= - The database for which to check existence
# | exit: Return 1 if the database doesn't exist, 0 otherwise
#
#
ynh_mongo_database_exists() {
# ============ Argument parsing =============
local -A args_array=([d]=database=)
local database
ynh_handle_getopts_args "$@"
# ===========================================
if [ $(ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("'${database}'")') -lt 0 ]
then
return 1
else
return 0
fi
}
# Restore a database
#
# example: ynh_mongo_restore_db --database=wekan < ./dump.bson
#
# usage: ynh_mongo_restore_db --database=database
# | arg: -d, --database= - The database name to restore
#
#
ynh_mongo_restore_db() {
# ============ Argument parsing =============
local -A args_array=( [d]=database= )
local database
ynh_handle_getopts_args "$@"
# ===========================================
mongorestore --quiet --db="$database" --archive
}
# Drop a user
#
# [internal]
#
# usage: ynh_mongo_drop_user --db_user=user --db_name=name
# | arg: -u, --db_user= - The user to drop
# | arg: -n, --db_name= - Name of the database
#
#
ynh_mongo_drop_user() {
# ============ Argument parsing =============
local -A args_array=( [u]=db_user= [n]=db_name= )
local db_user
local db_name
ynh_handle_getopts_args "$@"
# ===========================================
ynh_mongo_exec --database="$db_name" --command='db.dropUser("'$db_user'", {w: "majority", wtimeout: 5000})'
}
# Create a database, an user and its password. Then store the password in the app's config
#
# usage: ynh_mongo_setup_db --db_user=user --db_name=name [--db_pwd=pwd]
# | arg: -u, --db_user= - Owner of the database
# | arg: -n, --db_name= - Name of the database
# | arg: -p, --db_pwd= - Password of the database. If not provided, a password will be generated
#
# After executing this helper, the password of the created database will be available in $db_pwd
# It will also be stored as "mongopwd" into the app settings.
#
#
ynh_mongo_setup_db() {
# ============ Argument parsing =============
local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= )
local db_user
local db_name
db_pwd=""
ynh_handle_getopts_args "$@"
# ===========================================
local new_db_pwd=$(ynh_string_random) # Generate a random password
# If $db_pwd is not provided, use new_db_pwd instead for db_pwd
db_pwd="${db_pwd:-$new_db_pwd}"
# Create the user and grant access to the database
ynh_mongo_create_user --db_user="$db_user" --db_pwd="$db_pwd" --db_name="$db_name"
# Store the password in the app's config
ynh_app_setting_set --key=db_pwd --value=$db_pwd
}
# Remove a database if it exists, and the associated user
#
# usage: ynh_mongo_remove_db --db_user=user --db_name=name
# | arg: -u, --db_user= - Owner of the database
# | arg: -n, --db_name= - Name of the database
#
#
ynh_mongo_remove_db() {
# ============ Argument parsing =============
local -A args_array=( [u]=db_user= [n]=db_name= )
local db_user
local db_name
ynh_handle_getopts_args "$@"
# ===========================================
if ynh_mongo_database_exists --database=$db_name; then # Check if the database exists
ynh_mongo_drop_db --database=$db_name # Remove the database
else
ynh_print_warn "Database $db_name not found"
fi
# Remove mongo user if it exists
ynh_mongo_drop_user --db_user=$db_user --db_name=$db_name
}
# Install MongoDB and integrate MongoDB service in YunoHost
#
# The installed version is defined by $mongo_version which should be defined as global prior to calling this helper
#
# usage: ynh_install_mongo
#
ynh_install_mongo() {
[[ -n "${mongo_version:-}" ]] || ynh_die "\$mongo_version should be defined prior to calling ynh_install_mongo"
ynh_print_info "Installing MongoDB Community Edition ..."
local mongo_debian_release=$YNH_DEBIAN_VERSION
if [[ $(cat /proc/cpuinfo) != *"avx"* && "$mongo_version" != "4.4" ]]; then
ynh_print_warn "Installing Mongo 4.4 as $mongo_version is not compatible with your cpu (see https://docs.mongodb.com/manual/administration/production-notes/#x86_64)."
mongo_version="4.4"
fi
if [[ "$mongo_version" == "4.4" ]]; then
ynh_print_warn "Switched to buster install as Mongo 4.4 is not compatible with $mongo_debian_release."
mongo_debian_release=buster
fi
ynh_install_extra_app_dependencies --repo="deb http://repo.mongodb.org/apt/debian $mongo_debian_release/mongodb-org/$mongo_version main" --package="mongodb-org mongodb-org-server mongodb-org-tools mongodb-mongosh" --key="https://www.mongodb.org/static/pgp/server-$mongo_version.asc"
mongodb_servicename=mongod
# Make sure MongoDB is started and enabled
systemctl enable $mongodb_servicename --quiet
systemctl daemon-reload --quiet
ynh_systemctl --service=$mongodb_servicename --action=restart --wait_until="aiting for connections" --log_path="/var/log/mongodb/$mongodb_servicename.log"
# Integrate MongoDB service in YunoHost
yunohost service add $mongodb_servicename --description="MongoDB daemon" --log="/var/log/mongodb/$mongodb_servicename.log"
# Store mongo_version into the config of this app
ynh_app_setting_set --key=mongo_version --value=$mongo_version
}
# Remove MongoDB
# Only remove the MongoDB service integration in YunoHost for now
# if MongoDB package as been removed
#
# usage: ynh_remove_mongo
#
#
ynh_remove_mongo() {
# Only remove the mongodb service if it is not installed.
if ! _ynh_apt_package_is_installed "mongodb*"
then
ynh_print_info "Removing MongoDB service..."
mongodb_servicename=mongod
# Remove the mongodb service
yunohost service remove $mongodb_servicename
ynh_safe_rm "/var/lib/mongodb"
ynh_safe_rm "/var/log/mongodb"
fi
}

View file

@ -0,0 +1,102 @@
#!/bin/bash
readonly MEDIA_GROUP=multimedia
readonly MEDIA_DIRECTORY=/home/yunohost.multimedia
# Initialize the multimedia directory system
#
# usage: ynh_multimedia_build_main_dir
#
# Requires YunoHost version 4.2 or higher.
ynh_multimedia_build_main_dir() {
## Création du groupe multimedia
groupadd -f $MEDIA_GROUP
## Création des dossiers génériques
mkdir -p "$MEDIA_DIRECTORY"
mkdir -p "$MEDIA_DIRECTORY/share"
mkdir -p "$MEDIA_DIRECTORY/share/Music"
mkdir -p "$MEDIA_DIRECTORY/share/Picture"
mkdir -p "$MEDIA_DIRECTORY/share/Video"
mkdir -p "$MEDIA_DIRECTORY/share/eBook"
## Création des dossiers utilisateurs
for user in $(yunohost user list --output-as json | jq -r '.users | keys[]'); do
mkdir -p "$MEDIA_DIRECTORY/$user"
mkdir -p "$MEDIA_DIRECTORY/$user/Music"
mkdir -p "$MEDIA_DIRECTORY/$user/Picture"
mkdir -p "$MEDIA_DIRECTORY/$user/Video"
mkdir -p "$MEDIA_DIRECTORY/$user/eBook"
ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share"
# Création du lien symbolique dans le home de l'utilisateur.
#link will only be created if the home directory of the user exists and if it's located in '/home' folder
local user_home="$(getent passwd $user | cut -d: -f6 | grep '^/home/')"
if [[ -d "$user_home" ]]; then
ln -sfn "$MEDIA_DIRECTORY/$user" "$user_home/Multimedia"
fi
# Propriétaires des dossiers utilisateurs.
chown -R $user "$MEDIA_DIRECTORY/$user"
done
# Default yunohost hooks for post_user_create,delete will take care
# of creating/deleting corresponding multimedia folders when users
# are created/deleted in the future...
## Application des droits étendus sur le dossier multimedia.
# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other:
setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" || true
# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers.
setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" || true
# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl.
setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" || true
}
# Add a directory in yunohost.multimedia
#
# usage: ynh_multimedia_addfolder --source_dir="source_dir" --dest_dir="dest_dir"
#
# | arg: -s, --source_dir= - Source directory - The real directory which contains your medias.
# | arg: -d, --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia"
#
# This "directory" will be a symbolic link to a existing directory.
#
# Requires YunoHost version 4.2 or higher.
ynh_multimedia_addfolder() {
# ============ Argument parsing =============
local -A args_array=([s]=source_dir= [d]=dest_dir=)
local source_dir
local dest_dir
ynh_handle_getopts_args "$@"
# ===========================================
# Ajout d'un lien symbolique vers le dossier à partager
ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir"
## Application des droits étendus sur le dossier ajouté
# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other:
setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir"
# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers.
setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir"
# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl.
setfacl -RL -m m::rwx "$source_dir"
}
# Allow an user to have an write authorisation in multimedia directories
#
# usage: ynh_multimedia_addaccess user_name
#
# | arg: -u, --user_name= - The name of the user which gain this access.
#
# Requires YunoHost version 4.2 or higher.
ynh_multimedia_addaccess() {
# ============ Argument parsing =============
local -A args_array=([u]=user_name=)
local user_name
ynh_handle_getopts_args "$@"
# ===========================================
groupadd -f multimedia
usermod -a -G multimedia $user_name
}

View file

@ -0,0 +1,114 @@
#!/bin/bash
# Run SQL instructions in a database ($db_name by default)
#
# usage: ynh_mysql_db_shell [database] <<< "instructions"
# | arg: database= - the database to connect to (by default, $db_name)
#
# examples:
# ynh_mysql_db_shell $db_name <<< "UPDATE ...;"
# ynh_mysql_db_shell < /path/to/file.sql
#
ynh_mysql_db_shell() {
local database=${1:-$db_name}
mysql -B $database
}
# Create a database and grant optionnaly privilegies to a user
#
# [internal]
#
# usage: ynh_mysql_create_db db [user [pwd]]
# | arg: db - the database name to create
# | arg: user - the user to grant privilegies
# | arg: pwd - the password to identify user by
#
ynh_mysql_create_db() {
local db=$1
local sql="CREATE DATABASE ${db};"
# grant all privilegies to user
if [[ $# -gt 1 ]]; then
sql+=" GRANT ALL PRIVILEGES ON ${db}.* TO '${2}'@'localhost'"
if [[ -n ${3:-} ]]; then
sql+=" IDENTIFIED BY '${3}'"
fi
sql+=" WITH GRANT OPTION;"
fi
mysql -B <<< "$sql"
}
# Drop a database
#
# [internal]
#
# If you intend to drop the database *and* the associated user,
# consider using ynh_mysql_remove_db instead.
#
# usage: ynh_mysql_drop_db db
# | arg: db - the database name to drop
#
ynh_mysql_drop_db() {
mysql -B <<< "DROP DATABASE ${1};"
}
# Dump a database
#
# usage: ynh_mysql_dump_db database
# | arg: -d, --database= - the database name to dump (by default, $db_name)
# | ret: The mysqldump output
#
# example: ynh_mysql_dump_db "roundcube" > ./dump.sql
#
ynh_mysql_dump_db() {
local database=${1:-$db_name}
mysqldump --single-transaction --skip-dump-date --routines "$database"
}
# Create a user
#
# [internal]
#
# usage: ynh_mysql_create_user user pwd [host]
# | arg: user - the user name to create
# | arg: pwd - the password to identify user by
#
ynh_mysql_create_user() {
mysql -B <<< "CREATE USER '${1}'@'localhost' IDENTIFIED BY '${2}';"
}
# Check if a mysql user exists
#
# [internal]
#
# usage: ynh_mysql_user_exists user
# | arg: user= - the user for which to check existence
# | ret: 0 if the user exists, 1 otherwise.
ynh_mysql_user_exists() {
local user=$1
[[ -n "$(mysql -B <<< "SELECT User from mysql.user WHERE User = '$user';")" ]]
}
# Check if a mysql database exists
#
# usage: ynh_mysql_database_exists database
# | arg: database - the database for which to check existence
# | exit: Return 1 if the database doesn't exist, 0 otherwise
#
ynh_mysql_database_exists() {
local database=$1
mysqlshow | grep -q "^| $database "
}
# Drop a user
#
# [internal]
#
# usage: ynh_mysql_drop_user user
# | arg: user - the user name to drop
#
ynh_mysql_drop_user() {
mysql -B <<< "DROP USER '${1}'@'localhost';"
}

View file

@ -0,0 +1,65 @@
#!/bin/bash
# Create a dedicated nginx config
#
# usage: ynh_config_add_nginx
#
# This will use a template in `../conf/nginx.conf`
# See the documentation of `ynh_config_add` for a description of the template
# format and how placeholders are replaced with actual variables.
#
# Additionally, ynh_config_add_nginx will replace:
# - `#sub_path_only` by empty string if `path` is not `'/'`
# - `#root_path_only` by empty string if `path` *is* `'/'`
#
# This allows to enable/disable specific behaviors dependenging on the install
# location
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add_nginx() {
local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf"
ynh_config_add --template="nginx.conf" --destination="$finalnginxconf"
if [ "${path:-}" != "/" ]; then
ynh_replace --match="^#sub_path_only" --replace="" --file="$finalnginxconf"
else
ynh_replace --match="^#root_path_only" --replace="" --file="$finalnginxconf"
fi
ynh_store_file_checksum "$finalnginxconf"
ynh_systemctl --service=nginx --action=reload
}
# Remove the dedicated nginx config
#
# usage: ynh_config_remove_nginx
#
# Requires YunoHost version 2.7.2 or higher.
ynh_config_remove_nginx() {
ynh_safe_rm "/etc/nginx/conf.d/$domain.d/$app.conf"
ynh_systemctl --service=nginx --action=reload
}
# Regen the nginx config in a change url context
#
# usage: ynh_config_change_url_nginx
#
# Requires YunoHost version 11.1.9 or higher.
ynh_config_change_url_nginx() {
# Make a backup of the original NGINX config file if manually modified
# (nb: this is possibly different from the same instruction called by
# ynh_config_add inside ynh_config_add_nginx because the path may have
# changed if we're changing the domain too...)
local old_nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf
ynh_backup_if_checksum_is_different "$old_nginx_conf_path"
ynh_delete_file_checksum "$old_nginx_conf_path"
ynh_safe_rm "$old_nginx_conf_path"
# Regen the nginx conf
ynh_config_add_nginx
}

View file

@ -0,0 +1,127 @@
#!/bin/bash
n_install_dir="/opt/node_n"
node_version_path="$n_install_dir/n/versions/node"
# N_PREFIX is the directory of n, it needs to be loaded as a environment variable.
export N_PREFIX="$n_install_dir"
_ynh_load_nodejs_in_path_and_other_tweaks() {
# Get the absolute path of this version of node
local nodejs_path="$node_version_path/$nodejs_version/bin"
# Load the path of this version of node in $PATH
if [[ :$PATH: != *":$nodejs_path"* ]]; then
PATH="$nodejs_path:$PATH"
fi
# Export PATH such that it's available through sudo -E / ynh_exec_as $app
export PATH
# This is in full lowercase such that it gets replaced in templates
path_with_nodejs="$PATH"
PATH_with_nodejs="$PATH"
# Prevent yet another Node and Corepack madness, with Corepack wanting the user to confirm download of Yarn
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
}
# Install a specific version of nodejs, using 'n'
#
# The installed version is defined by $nodejs_version which should be defined as global prior to calling this helper
#
# usage: ynh_nodejs_install
#
# `n` (Node version management) uses the `PATH` variable to store the path of the version of node it is going to use.
# That's how it changes the version
#
# Adds the appropriate, specific version of nodejs to the PATH variable (which
# is also exported, to ease the use of ynh_exec_as_app). Also define variable
# PATH_with_nodejs to be used in the systemd config
# (Environment="PATH=__PATH_WITH_NODEJS__")
#
# Requires YunoHost version 2.7.12 or higher.
ynh_nodejs_install() {
# Use n, https://github.com/tj/n to manage the nodejs versions
[[ -n "${nodejs_version:-}" ]] || ynh_die "\$nodejs_version should be defined prior to calling ynh_nodejs_install"
# Create $n_install_dir
mkdir --parents "$n_install_dir"
# Load n path in PATH
CLEAR_PATH="$n_install_dir/bin:$PATH"
# Remove /usr/local/bin in PATH in case of node prior installation
PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
# Move an existing node binary, to avoid to block n.
test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n
test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n
# Install (or update if YunoHost vendor/ folder updated since last install) n
mkdir -p $n_install_dir/bin/
cp "$YNH_HELPERS_DIR/vendor/n/n" $n_install_dir/bin/n
# Tweak for n to understand it's installed in $N_PREFIX
ynh_replace --match="^N_PREFIX=\${N_PREFIX-.*}$" --replace="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --file="$n_install_dir/bin/n"
# Restore /usr/local/bin in PATH
PATH=$CLEAR_PATH
# And replace the old node binary.
test -x /usr/bin/node_n && mv /usr/bin/node_n /usr/bin/node
test -x /usr/bin/npm_n && mv /usr/bin/npm_n /usr/bin/npm
# Install the requested version of nodejs
uname=$(uname --machine)
if [[ $uname =~ aarch64 || $uname =~ arm64 ]]; then
n $nodejs_version --arch=arm64
else
n $nodejs_version
fi
# Find the last "real" version for this major version of node.
real_nodejs_version=$(find $node_version_path/$nodejs_version* -maxdepth 0 | sort --version-sort | tail --lines=1)
real_nodejs_version=$(basename $real_nodejs_version)
# Create a symbolic link for this major version if the file doesn't already exist
if [ ! -e "$node_version_path/$nodejs_version" ]; then
ln --symbolic --force --no-target-directory $node_version_path/$real_nodejs_version $node_version_path/$nodejs_version
fi
# Store the ID of this app and the version of node requested for it
echo "$YNH_APP_INSTANCE_NAME:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version"
# Store nodejs_version into the config of this app
ynh_app_setting_set --key=nodejs_version --value=$nodejs_version
_ynh_load_nodejs_in_path_and_other_tweaks
}
# Remove the version of node used by the app.
#
# usage: ynh_nodejs_remove
#
# This helper will check if another app uses the same version of node.
# - If not, this version of node will be removed.
# - If no other app uses node, n will be also removed.
#
# Requires YunoHost version 2.7.12 or higher.
ynh_nodejs_remove() {
[[ -n "${nodejs_version:-}" ]] || ynh_die "\$nodejs_version should be defined prior to calling ynh_nodejs_remove"
# Remove the line for this app
sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$n_install_dir/ynh_app_version"
# If no other app uses this version of nodejs, remove it.
if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version"; then
$n_install_dir/bin/n rm $nodejs_version
fi
# If no other app uses n, remove n
if [ ! -s "$n_install_dir/ynh_app_version" ]; then
ynh_safe_rm "$n_install_dir"
ynh_safe_rm "/usr/local/n"
sed --in-place "/N_PREFIX/d" /root/.bashrc
fi
}

View file

@ -0,0 +1,331 @@
#!/bin/bash
# Create a new permission for the app
#
# Example 1: `ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \
# --label="My app admin" --show_tile=true`
#
# This example will create a new permission permission with this following effect:
# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'.
# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin
#
#
# Example 2:
#
# ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \
# --label="MyApp API" --protected=true
#
# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission.
# In case of an API with need to be always public it avoid that the admin break anything.
# With this permission all client will be allowed to access to the url 'domain.tld/api'.
# Note that in this case no tile will be show on the SSO.
# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application.
# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case.
# So in this case it's better to disable this option for all API.
#
#
# usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false]
# [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false]
# [--protected=true|false]
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if 'show_tile' is enabled, this URL will be the URL of the tile.
# | arg: -A, --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden
# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true
# | arg: -a, --allowed= - (optional) A list of group/user to allow for the permission
# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "APP_LABEL (permission name)".
# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. Defaults to false for the permission different than 'main'.
# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'.
#
# [packagingv1]
#
# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they
# start with '/'. For example:
# / -> domain.tld/app
# /admin -> domain.tld/app/admin
# domain.tld/app/api -> domain.tld/app/api
#
# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:".
# For example:
# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
#
# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is:
# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls'
# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter)
#
#
# About the authentication header (auth_header parameter).
# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application:
# - "Auth-User": username
# - "Remote-User": username
# - "Email": user email
#
# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly.
# See https://github.com/YunoHost/issues/issues/1420 for more informations
#
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_create() {
# ============ Argument parsing =============
local -A args_array=([p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected=)
local permission
local url
local additional_urls
local auth_header
local allowed
local label
local show_tile
local protected
ynh_handle_getopts_args "$@"
url=${url:-}
additional_urls=${additional_urls:-}
auth_header=${auth_header:-}
allowed=${allowed:-}
label=${label:-}
show_tile=${show_tile:-}
protected=${protected:-}
# ===========================================
if [[ -n $url ]]; then
url=",url='$url'"
fi
if [[ -n $additional_urls ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --additional_urls /urlA /urlB
# will be:
# additional_urls=['/urlA', '/urlB']
additional_urls=",additional_urls=['${additional_urls//;/\',\'}']"
fi
if [[ -n $auth_header ]]; then
if [ $auth_header == "true" ]; then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $allowed ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --allowed alice bob
# will be:
# allowed=['alice', 'bob']
allowed=",allowed=['${allowed//;/\',\'}']"
fi
if [[ -n ${label:-} ]]; then
label=",label='$label'"
else
label=",label='$permission'"
fi
if [[ -n ${show_tile:-} ]]; then
if [ $show_tile == "true" ]; then
show_tile=",show_tile=True"
else
show_tile=",show_tile=False"
fi
fi
if [[ -n ${protected:-} ]]; then
if [ $protected == "true" ]; then
protected=",protected=True"
else
protected=",protected=False"
fi
fi
yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)"
}
# Remove a permission for the app (note that when the app is removed all permission is automatically removed)
#
# [packagingv1]
#
# example: ynh_permission_delete --permission=editors
#
# usage: ynh_permission_delete --permission="permission"
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_delete() {
# ============ Argument parsing =============
local -A args_array=([p]=permission=)
local permission
ynh_handle_getopts_args "$@"
# ===========================================
yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')"
}
# Check if a permission exists
#
# [packagingv1]
#
# usage: ynh_permission_exists --permission=permission
# | arg: -p, --permission= - the permission to check
# | exit: Return 1 if the permission doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_exists() {
# ============ Argument parsing =============
local -A args_array=([p]=permission=)
local permission
ynh_handle_getopts_args "$@"
# ===========================================
yunohost user permission list "$app" --output-as json --quiet \
| jq -e --arg perm "$app.$permission" '.permissions[$perm]' >/dev/null
}
# Redefine the url associated to a permission
#
# [packagingv1]
#
# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]]
# [--auth_header=true|false] [--clear_urls]
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments ("").
# | arg: -a, --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden.
# | arg: -r, --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden
# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application
# | arg: -c, --clear_urls - (optional) Clean all urls (url and additional_urls)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_url() {
# ============ Argument parsing =============
local -A args_array=([p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls)
local permission
local url
local add_url
local remove_url
local auth_header
local clear_urls
ynh_handle_getopts_args "$@"
url=${url:-}
add_url=${add_url:-}
remove_url=${remove_url:-}
auth_header=${auth_header:-}
clear_urls=${clear_urls:-}
# ===========================================
if [[ -n $url ]]; then
url=",url='$url'"
fi
if [[ -n $add_url ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add_url /urlA /urlB
# will be:
# add_url=['/urlA', '/urlB']
add_url=",add_url=['${add_url//;/\',\'}']"
fi
if [[ -n $remove_url ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove_url /urlA /urlB
# will be:
# remove_url=['/urlA', '/urlB']
remove_url=",remove_url=['${remove_url//;/\',\'}']"
fi
if [[ -n $auth_header ]]; then
if [ $auth_header == "true" ]; then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ]; then
clear_urls=",clear_urls=True"
fi
yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)"
}
# Update a permission for the app
#
# [packagingv1]
#
# usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]]
#
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -a, --add= - the list of group or users to enable add to the permission
# | arg: -r, --remove= - the list of group or users to remove from the permission
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_update() {
# ============ Argument parsing =============
local -A args_array=([p]=permission= [a]=add= [r]=remove=)
local permission
local add
local remove
ynh_handle_getopts_args "$@"
add=${add:-}
remove=${remove:-}
# ===========================================
if [[ -n $add ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add alice bob
# will be:
# add=['alice', 'bob']
add=",add=['${add//';'/"','"}']"
fi
if [[ -n $remove ]]; then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove alice bob
# will be:
# remove=['alice', 'bob']
remove=",remove=['${remove//';'/"','"}']"
fi
yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove , force=True)"
}
# Check if a permission has an user
#
# example: ynh_permission_has_user --permission=main --user=visitors
#
# usage: ynh_permission_has_user --permission=permission --user=user
# | arg: -p, --permission= - the permission to check
# | arg: -u, --user= - the user seek in the permission
# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.1 or higher.
ynh_permission_has_user() {
# ============ Argument parsing =============
local -A args_array=([p]=permission= [u]=user=)
local permission
local user
ynh_handle_getopts_args "$@"
# ===========================================
if ! ynh_permission_exists --permission=$permission; then
return 1
fi
# Check both allowed and corresponding_users sections in the json
for section in "allowed" "corresponding_users"; do
if yunohost user permission info "$app.$permission" --output-as json --quiet \
| jq -e --arg user $user --arg section $section '.[$section] | index($user)' >/dev/null; then
return 0
fi
done
return 1
}

317
helpers/helpers.v2.1.d/php Normal file
View file

@ -0,0 +1,317 @@
#!/bin/bash
# (this is used in the apt helpers, big meh ...)
readonly YNH_DEFAULT_PHP_VERSION=7.4
# Legacy: auto-convert phpversion to php_version (for consistency with nodejs_version, ruby_version, ...)
if [[ -n "${app:-}" ]] && [[ -n "${phpversion:-}" ]]
then
if [[ -z "${php_version:-}" ]]
then
php_version=$phpversion
ynh_app_setting_set --key=php_version --value=$php_version
fi
ynh_app_setting_delete --key=phpversion
unset phpversion
fi
# Create a dedicated PHP-FPM config
#
# usage: ynh_config_add_phpfpm
#
# This helper assumes the app has an conf/extra_php-fpm.conf snippet
#
# The actual PHP configuration will be automatically generated,
# and your extra_php-fpm.conf will be appended (typically contains PHP upload limits)
#
# The resulting configuration will be deployed to the appropriate place, /etc/php/$php_version/fpm/pool.d/$app.conf
#
# Performance-related options in the PHP conf, such as :
# pm.max_children, pm.start_servers, pm.min_spare_servers pm.max_spare_servers
# are computed from two parameters called "usage" and "footprint" which can be set to low/medium/high. (cf details below)
#
# If you wish to tweak those, please initialize the settings `fpm_usage` and `fpm_footprint`
# *prior* to calling this helper. Otherwise, "low" will be used as a default for both values.
#
# Otherwise, if you want the user to have control over these, we encourage to create a config panel
# (which should ultimately be standardized by the core ...)
#
# The footprint of the service will be used to defined the maximum footprint we can allow, which is half the maximum RAM.
# So it will be used to defined 'pm.max_children'
# A lower value for the footprint will allow more children for 'pm.max_children'. And so for
# 'pm.start_servers', 'pm.min_spare_servers' and 'pm.max_spare_servers' which are defined from the
# value of 'pm.max_children'
# NOTE: 'pm.max_children' can't exceed 4 times the number of processor's cores.
#
# The usage value will defined the way php will handle the children for the pool.
# A value set as 'low' will set the process manager to 'ondemand'. Children will start only if the
# service is used, otherwise no child will stay alive. This config gives the lower footprint when the
# service is idle. But will use more proc since it has to start a child as soon it's used.
# Set as 'medium', the process manager will be at dynamic. If the service is idle, a number of children
# equal to pm.min_spare_servers will stay alive. So the service can be quick to answer to any request.
# The number of children can grow if needed. The footprint can stay low if the service is idle, but
# not null. The impact on the proc is a little bit less than 'ondemand' as there's always a few
# children already available.
# Set as 'high', the process manager will be set at 'static'. There will be always as many children as
# 'pm.max_children', the footprint is important (but will be set as maximum a quarter of the maximum
# RAM) but the impact on the proc is lower. The service will be quick to answer as there's always many
# children ready to answer.
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add_phpfpm() {
# ============ Argument parsing =============
local -A args_array=([g]=group=)
local group
ynh_handle_getopts_args "$@"
group=${group:-}
# ===========================================
# If the PHP version changed, remove the old fpm conf
# (NB: This stuff is also handled by the apt helper, which is usually triggered before this helper)
# FIXME: so is this still needed @_@
local old_php_version=$(ynh_app_setting_get --key=php_version)
if [ -n "$old_php_version" ] && [ "$old_php_version" != "$php_version" ]; then
local old_php_fpm_config_dir=$(ynh_app_setting_get --key=fpm_config_dir)
local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf"
if [[ -f "$old_php_finalphpconf" ]]
then
ynh_backup_if_checksum_is_different "$old_php_finalphpconf"
ynh_remove_fpm_config
fi
fi
local fpm_service="php${php_version}-fpm"
local fpm_config_dir="/etc/php/$php_version/fpm"
# Create the directory for FPM pools
mkdir --parents "$fpm_config_dir/pool.d"
# FIXME: zzzz do we really need those ...
ynh_app_setting_set --key=fpm_config_dir --value="$fpm_config_dir"
ynh_app_setting_set --key=fpm_service --value="$fpm_service"
ynh_app_setting_set --key=php_version --value=$php_version
# Define the values to use for the configuration of PHP.
_ynh_get_scalable_phpfpm
local phpfpm_group=$([[ -n "$group" ]] && echo "$group" || echo "$app")
local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf"
echo "
[__APP__]
user = __APP__
group = __PHPFPM_GROUP__
chdir = __INSTALL_DIR__
listen = /var/run/php/php__PHP_VERSION__-fpm-__APP__.sock
listen.owner = www-data
listen.group = www-data
pm = __PHP_PM__
pm.max_children = __PHP_MAX_CHILDREN__
pm.max_requests = 500
request_terminate_timeout = 1d
" >"$phpfpm_path"
if [ "$php_pm" = "dynamic" ]; then
echo "
pm.start_servers = __PHP_START_SERVERS__
pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__
pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__
" >>"$phpfpm_path"
elif [ "$php_pm" = "ondemand" ]; then
echo "
pm.process_idle_timeout = 10s
" >>"$phpfpm_path"
fi
# Concatene the extra config.
if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then
cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >>"$phpfpm_path"
fi
ynh_config_add --template="$phpfpm_path" --destination="$fpm_config_dir/pool.d/$app.conf"
# Validate that the new php conf doesn't break php-fpm entirely
if ! php-fpm${php_version} --test 2>/dev/null; then
php-fpm${php_version} --test || true
ynh_safe_rm "$fpm_config_dir/pool.d/$app.conf"
ynh_die "The new configuration broke php-fpm?"
fi
ynh_systemctl --service=$fpm_service --action=reload
}
# Remove the dedicated PHP-FPM config
#
# usage: ynh_config_remove_phpfpm
#
# Requires YunoHost version 2.7.2 or higher.
ynh_config_remove_phpfpm() {
local fpm_config_dir=$(ynh_app_setting_get --key=fpm_config_dir)
ynh_safe_rm "$fpm_config_dir/pool.d/$app.conf"
ynh_systemctl --service="php${php_version}-fpm" --action=reload
}
# Define the values to configure PHP-FPM
#
# [internal]
#
# usage: _ynh_get_scalable_phpfpm
# Footprint can be defined via the "fpm_footprint", to be set prior to calling this helper
# low - Less than 20 MB of RAM by pool.
# medium - Between 20 MB and 40 MB of RAM by pool.
# high - More than 40 MB of RAM by pool.
# Or specify exactly the footprint, the load of the service as MB by pool instead of having a standard value.
# To have this value, use the following command and stress the service.
# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP
#
# Usage can be defined via the "fpm_usage", to be set prior to calling this helper
# low - Personal usage, behind the SSO.
# medium - Low usage, few people or/and publicly accessible.
# high - High usage, frequently visited website.
#
_ynh_get_scalable_phpfpm() {
# If no usage provided, default to the value existing in setting ... or to low
local fpm_usage_in_setting=$(ynh_app_setting_get --key=fpm_usage)
local usage=${fpm_usage_in_setting:-low}
ynh_app_setting_set --key=fpm_usage --value=$usage
# If no footprint provided, default to the value existing in setting ... or to low
local fpm_footprint_in_setting=$(ynh_app_setting_get --key=fpm_footprint)
local footprint=${fpm_footprint_in_setting:-low}
ynh_app_setting_set --key=fpm_footprint --value=$footprint
# Set all characters as lowercase
if [ "$footprint" = "low" ]; then
footprint=20
elif [ "$footprint" = "medium" ]; then
footprint=35
elif [ "$footprint" = "high" ]; then
footprint=50
fi
# Define the factor to determine min_spare_servers
# to avoid having too few children ready to start for heavy apps
if [ $footprint -le 20 ]; then
min_spare_servers_factor=8
elif [ $footprint -le 35 ]; then
min_spare_servers_factor=5
else
min_spare_servers_factor=3
fi
# Define the way the process manager handle child processes.
if [ "$usage" = "low" ]; then
php_pm=ondemand
elif [ "$usage" = "medium" ]; then
php_pm=dynamic
elif [ "$usage" = "high" ]; then
php_pm=static
else
ynh_die "Does not recognize '$usage' as an usage value."
fi
# Get the total of RAM available, except swap.
local max_ram=$(ynh_get_ram --total)
at_least_one() {
# Do not allow value below 1
if [ $1 -le 0 ]; then
echo 1
else
echo $1
fi
}
# Define pm.max_children
# The value of pm.max_children is the total amount of ram divide by 2 and divide again by the footprint of a pool for this app.
# So if PHP-FPM start the maximum of children, it won't exceed half of the ram.
php_max_children=$(($max_ram / 2 / $footprint))
# If process manager is set as static, use half less children.
# Used as static, there's always as many children as the value of pm.max_children
if [ "$php_pm" = "static" ]; then
php_max_children=$(($php_max_children / 2))
fi
php_max_children=$(at_least_one $php_max_children)
# To not overload the proc, limit the number of children to 4 times the number of cores.
local core_number=$(nproc)
local max_proc=$(($core_number * 4))
if [ $php_max_children -gt $max_proc ]; then
php_max_children=$max_proc
fi
# Get a potential forced value for php_max_children
local php_forced_max_children=$(ynh_app_setting_get --key=php_forced_max_children)
if [ -n "$php_forced_max_children" ]; then
php_max_children=$php_forced_max_children
fi
if [ "$php_pm" = "dynamic" ]; then
# Define pm.start_servers, pm.min_spare_servers and pm.max_spare_servers for a dynamic process manager
php_min_spare_servers=$(($php_max_children / $min_spare_servers_factor))
php_min_spare_servers=$(at_least_one $php_min_spare_servers)
php_max_spare_servers=$(($php_max_children / 2))
php_max_spare_servers=$(at_least_one $php_max_spare_servers)
php_start_servers=$(($php_min_spare_servers + ($php_max_spare_servers - $php_min_spare_servers) / 2))
php_start_servers=$(at_least_one $php_start_servers)
else
php_min_spare_servers=0
php_max_spare_servers=0
php_start_servers=0
fi
}
# Execute a command with Composer
#
# Will use $install_dir as workdir unless $composer_workdir exists (but that shouldnt be necessary)
#
# You may also define composer_user=root prior to call this helper if you absolutely need composer to run as root, but this is discouraged...
#
# usage: ynh_composer_exec commands
#
# Requires YunoHost version 4.2 or higher.
ynh_composer_exec() {
local workdir="${composer_workdir:-$install_dir}"
COMPOSER_HOME="$workdir/.composer" \
COMPOSER_MEMORY_LIMIT=-1 \
sudo -E -u "${composer_user:-$app}" \
php${php_version} "$workdir/composer.phar" $@ \
-d "$workdir" --no-interaction --no-ansi 2>&1
}
# Install and initialize Composer in the given directory
#
# The installed version is defined by $composer_version which should be defined
# as global prior to calling this helper.
#
# Will use $install_dir as workdir unless $composer_workdir exists (but that shouldnt be necessary)
#
# usage: ynh_composer_install
#
# Requires YunoHost version 4.2 or higher.
ynh_composer_install() {
local workdir="${composer_workdir:-$install_dir}"
[[ -n "${composer_version}" ]] || ynh_die "\$composer_version should be defined before calling ynh_composer_install. (In the past, this was called \$YNH_COMPOSER_VERSION)"
[[ ! -e "$workdir/composer.phar" ]] || ynh_safe_rm $workdir/composer.phar
local composer_url="https://getcomposer.org/download/$composer_version/composer.phar"
# NB. we have to declare the var as local first,
# otherwise 'local foo=$(false) || echo 'pwet'" does'nt work
# because local always return 0 ...
local out
# Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget)
out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$workdir/composer.phar $composer_url 2>&1) \
|| ynh_die "$out"
}

View file

@ -0,0 +1,120 @@
#!/bin/bash
PSQL_ROOT_PWD_FILE=/etc/yunohost/psql
PSQL_VERSION=13
# Run SQL instructions in a database ($db_name by default)
#
# usage: ynh_psql_db_shell database <<< "instructions"
# | arg: database - the database to connect to (by default, $db_name)
#
# examples:
# ynh_psql_db_shell $db_name <<< "UPDATE ...;"
# ynh_psql_db_shell < /path/to/file.sql
#
ynh_psql_db_shell() {
local database="${1:-$db_name}"
sudo --login --user=postgres psql "$database"
}
# Create a database and grant optionnaly privilegies to a user
#
# [internal]
#
# usage: ynh_psql_create_db db [user]
# | arg: db - the database name to create
# | arg: user - the user to grant privilegies
#
ynh_psql_create_db() {
local db=$1
local user=${2:-}
local sql="CREATE DATABASE ${db};"
# grant all privilegies to user
if [ -n "$user" ]; then
sql+="ALTER DATABASE ${db} OWNER TO ${user};"
sql+="GRANT ALL PRIVILEGES ON DATABASE ${db} TO ${user} WITH GRANT OPTION;"
fi
sudo --login --user=postgres psql <<< "$sql"
}
# Drop a database
#
# [internal]
#
# If you intend to drop the database *and* the associated user,
# consider using ynh_psql_remove_db instead.
#
# usage: ynh_psql_drop_db db
# | arg: db - the database name to drop
#
ynh_psql_drop_db() {
local db=$1
# First, force disconnection of all clients connected to the database
# https://stackoverflow.com/questions/17449420/postgresql-unable-to-drop-database-because-of-some-auto-connections-to-db
sudo --login --user=postgres psql $db <<< "REVOKE CONNECT ON DATABASE $db FROM public;"
sudo --login --user=postgres psql $db <<< "SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$db' AND pid <> pg_backend_pid();"
sudo --login --user=postgres dropdb $db
}
# Dump a database
#
# usage: ynh_psql_dump_db database
# | arg: database - the database name to dump (by default, $db_name)
# | ret: the psqldump output
#
# example: ynh_psql_dump_db 'roundcube' > ./dump.sql
#
ynh_psql_dump_db() {
local database="${1:-$db_name}"
sudo --login --user=postgres pg_dump "$database"
}
# Create a user
#
# [internal]
#
# usage: ynh_psql_create_user user pwd
# | arg: user - the user name to create
# | arg: pwd - the password to identify user by
#
ynh_psql_create_user() {
sudo --login --user=postgres psql <<< "CREATE USER $user WITH ENCRYPTED PASSWORD '$pwd'"
}
# Check if a psql user exists
#
# [packagingv1]
#
# usage: ynh_psql_user_exists user
# | arg: user= - the user for which to check existence
# | exit: Return 1 if the user doesn't exist, 0 otherwise
#
ynh_psql_user_exists() {
local user=$1
sudo --login --user=postgres psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user"
}
# Check if a psql database exists
#
# usage: ynh_psql_database_exists database
# | arg: database - the database for which to check existence
# | exit: Return 1 if the database doesn't exist, 0 otherwise
#
ynh_psql_database_exists() {
local database=$1
sudo --login --user=postgres psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"
}
# Drop a user
#
# [internal]
#
# usage: ynh_psql_drop_user user
# | arg: user - the user name to drop
#
ynh_psql_drop_user() {
sudo --login --user=postgres psql <<< "DROP USER ${1};"
}

View file

@ -0,0 +1,39 @@
#!/bin/bash
# get the first available redis database
#
# usage: ynh_redis_get_free_db
# | returns: the database number to use
ynh_redis_get_free_db() {
local result max db
result=$(redis-cli INFO keyspace)
# get the num
max=$(cat /etc/redis/redis.conf | grep ^databases | grep -Eow "[0-9]+")
db=0
# default Debian setting is 15 databases
for i in $(seq 0 "$max")
do
if ! echo "$result" | grep -q "db$i"
then
db=$i
break 1
fi
db=-1
done
test "$db" -eq -1 && ynh_die "No available Redis databases..."
echo "$db"
}
# Create a master password and set up global settings
# Please always call this script in install and restore scripts
#
# usage: ynh_redis_remove_db database
# | arg: database - the database to erase
ynh_redis_remove_db() {
local db=$1
redis-cli -n "$db" flushdb
}

258
helpers/helpers.v2.1.d/ruby Normal file
View file

@ -0,0 +1,258 @@
#!/bin/bash
rbenv_install_dir="/opt/rbenv"
ruby_version_path="$rbenv_install_dir/versions"
# RBENV_ROOT is the directory of rbenv, it needs to be loaded as a environment variable.
export RBENV_ROOT="$rbenv_install_dir"
export rbenv_root="$rbenv_install_dir"
_ynh_load_ruby_in_path_and_other_tweaks() {
# Get the absolute path of this version of Ruby
local ruby_path="$ruby_version_path/$app/bin"
# Load the path of this version of ruby in $PATH
if [[ :$PATH: != *":$ruby_path"* ]]; then
PATH="$ruby_path:$PATH"
fi
# Export PATH such that it's available through sudo -E / ynh_exec_as $app
export PATH
# This is in full lowercase such that it gets replaced in templates
path_with_ruby="$PATH"
PATH_with_ruby="$PATH"
# Sets the local application-specific Ruby version
pushd ${install_dir}
$rbenv_install_dir/bin/rbenv local $ruby_version
popd
}
# Install a specific version of Ruby using rbenv
#
# The installed version is defined by $ruby_version which should be defined as global prior to calling this helper
#
# This helper creates a /etc/profile.d/rbenv.sh that configures PATH environment for rbenv
# for every LOGIN user, hence your user must have a defined shell (as opposed to /usr/sbin/nologin)
#
# Don't forget to execute ruby-dependent command in a login environment
# (e.g. sudo --login option)
# When not possible (e.g. in systemd service definition), please use direct path
# to rbenv shims (e.g. $RBENV_ROOT/shims/bundle)
#
# usage: ynh_ruby_install
#
# Adds the appropriate, specific version of ruby to the PATH variable (which
# is also exported, to ease the use of ynh_exec_as_app). Also define variable
# PATH_with_ruby to be used in the systemd config
# (Environment="PATH=__PATH_WITH_RUBY__")
#
# Requires YunoHost version 3.2.2 or higher.
ynh_ruby_install () {
[[ -n "${ruby_version:-}" ]] || ynh_die "\$ruby_version should be defined prior to calling ynh_ruby_install"
# Load rbenv path in PATH
local CLEAR_PATH="$rbenv_install_dir/bin:$PATH"
# Remove /usr/local/bin in PATH in case of Ruby prior installation
PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
# Move an existing Ruby binary, to avoid to block rbenv
test -x /usr/bin/ruby && mv /usr/bin/ruby /usr/bin/ruby_rbenv
# Install or update rbenv
mkdir -p $rbenv_install_dir
rbenv="$(command -v rbenv $rbenv_install_dir/bin/rbenv | grep "$rbenv_install_dir/bin/rbenv" | head -1)"
if [ -n "$rbenv" ]; then
pushd "${rbenv%/*/*}"
if git remote -v 2>/dev/null | grep "https://github.com/rbenv/rbenv.git"; then
echo "Updating rbenv..."
git pull -q --tags origin master
ynh_ruby_try_bash_extension
else
echo "Reinstalling rbenv..."
cd ..
ynh_safe_rm $rbenv_install_dir
mkdir -p $rbenv_install_dir
cd $rbenv_install_dir
git init -q
git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1
git checkout -q -b master origin/master
ynh_ruby_try_bash_extension
rbenv=$rbenv_install_dir/bin/rbenv
fi
popd
else
echo "Installing rbenv..."
pushd $rbenv_install_dir
git init -q
git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1
git checkout -q -b master origin/master
ynh_ruby_try_bash_extension
rbenv=$rbenv_install_dir/bin/rbenv
popd
fi
mkdir -p "${rbenv_install_dir}/plugins"
ruby_build="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-install rbenv-install | head -1)"
if [ -n "$ruby_build" ]; then
pushd "${ruby_build%/*/*}"
if git remote -v 2>/dev/null | grep "https://github.com/rbenv/ruby-build.git"; then
echo "Updating ruby-build..."
git pull -q origin master
fi
popd
else
echo "Installing ruby-build..."
git clone -q https://github.com/rbenv/ruby-build.git "${rbenv_install_dir}/plugins/ruby-build"
fi
rbenv_alias="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-alias rbenv-alias | head -1)"
if [ -n "$rbenv_alias" ]; then
pushd "${rbenv_alias%/*/*}"
if git remote -v 2>/dev/null | grep "https://github.com/tpope/rbenv-aliases.git"; then
echo "Updating rbenv-aliases..."
git pull -q origin master
fi
popd
else
echo "Installing rbenv-aliases..."
git clone -q https://github.com/tpope/rbenv-aliases.git "${rbenv_install_dir}/plugins/rbenv-aliase"
fi
rbenv_latest="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-latest rbenv-latest | head -1)"
if [ -n "$rbenv_latest" ]; then
pushd "${rbenv_latest%/*/*}"
if git remote -v 2>/dev/null | grep "https://github.com/momo-lab/xxenv-latest.git"; then
echo "Updating xxenv-latest..."
git pull -q origin master
fi
popd
else
echo "Installing xxenv-latest..."
git clone -q https://github.com/momo-lab/xxenv-latest.git "${rbenv_install_dir}/plugins/xxenv-latest"
fi
# Enable caching
mkdir -p "${rbenv_install_dir}/cache"
# Create shims directory if needed
mkdir -p "${rbenv_install_dir}/shims"
# Restore /usr/local/bin in PATH
PATH=$CLEAR_PATH
# And replace the old Ruby binary
test -x /usr/bin/ruby_rbenv && mv /usr/bin/ruby_rbenv /usr/bin/ruby
# Install the requested version of Ruby
local final_ruby_version=$(rbenv latest --print $ruby_version)
if ! [ -n "$final_ruby_version" ]; then
final_ruby_version=$ruby_version
fi
echo "Installing Ruby $final_ruby_version"
RUBY_CONFIGURE_OPTS="--disable-install-doc --with-jemalloc" MAKE_OPTS="-j2" rbenv install --skip-existing $final_ruby_version > /dev/null 2>&1
# Store ruby_version into the config of this app
ynh_app_setting_set --key=ruby_version --value=$final_ruby_version
ruby_version=$final_ruby_version
# Remove app virtualenv
if rbenv alias --list | grep --quiet "$app "
then
rbenv alias $app --remove
fi
# Create app virtualenv
rbenv alias $app $final_ruby_version
# Cleanup Ruby versions
_ynh_ruby_cleanup
# Set environment for Ruby users
echo "#rbenv
export RBENV_ROOT=$rbenv_install_dir
export PATH=\"$rbenv_install_dir/bin:$PATH\"
eval \"\$(rbenv init -)\"
#rbenv" > /etc/profile.d/rbenv.sh
# Load the environment
eval "$(rbenv init -)"
_ynh_load_ruby_in_path_and_other_tweaks
}
# Remove the version of Ruby used by the app.
#
# This helper will also cleanup Ruby versions
#
# usage: ynh_ruby_remove
ynh_ruby_remove () {
local ruby_version=$(ynh_app_setting_get --key=ruby_version)
# Load rbenv path in PATH
local CLEAR_PATH="$rbenv_install_dir/bin:$PATH"
# Remove /usr/local/bin in PATH in case of Ruby prior installation
PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
rbenv alias $app --remove
# Remove the line for this app
ynh_app_setting_delete --key=ruby_version
# Cleanup Ruby versions
_ynh_ruby_cleanup
}
# Remove no more needed versions of Ruby used by the app.
#
# This helper will check what Ruby version are no more required,
# and uninstall them
# If no app uses Ruby, rbenv will be also removed.
_ynh_ruby_cleanup () {
# List required Ruby versions
local installed_apps=$(yunohost app list | grep -oP 'id: \K.*$')
local required_ruby_versions=""
for installed_app in $installed_apps
do
local installed_app_ruby_version=$(ynh_app_setting_get --app=$installed_app --key="ruby_version")
if [[ -n "$installed_app_ruby_version" ]]
then
required_ruby_versions="${installed_app_ruby_version}\n${required_ruby_versions}"
fi
done
# Remove no more needed Ruby versions
local installed_ruby_versions=$(rbenv versions --bare --skip-aliases | grep -Ev '/')
for installed_ruby_version in $installed_ruby_versions
do
if ! echo ${required_ruby_versions} | grep -q "${installed_ruby_version}"
then
echo "Removing Ruby-$installed_ruby_version"
$rbenv_install_dir/bin/rbenv uninstall --force $installed_ruby_version
fi
done
# If none Ruby version is required
if [[ -z "$required_ruby_versions" ]]
then
# Remove rbenv environment configuration
echo "Removing rbenv"
ynh_safe_rm "$rbenv_install_dir"
ynh_safe_rm "/etc/profile.d/rbenv.sh"
fi
}
ynh_ruby_try_bash_extension() {
if [ -x src/configure ]; then
src/configure && make -C src 2>&1 || {
ynh_print_info "Optional bash extension failed to build, but things will still work normally."
}
fi
}

View file

@ -0,0 +1,95 @@
#!/bin/bash
# Get an application setting
#
# usage: ynh_app_setting_get --app=app --key=key
# | arg: -a, --app= - the application id
# | arg: -k, --key= - the setting to get
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_get() {
# ============ Argument parsing =============
local _globalapp=${app-:}
local -A args_array=([a]=app= [k]=key=)
local app
local key
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
# ===========================================
ynh_app_setting "get" "$app" "$key"
}
# Set an application setting
#
# usage: ynh_app_setting_set --app=app --key=key --value=value
# | arg: -a, --app= - the application id
# | arg: -k, --key= - the setting name to set
# | arg: -v, --value= - the setting value to set
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_set() {
# ============ Argument parsing =============
local _globalapp=${app-:}
local -A args_array=([a]=app= [k]=key= [v]=value=)
local app
local key
local value
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
# ===========================================
ynh_app_setting "set" "$app" "$key" "$value"
}
# Delete an application setting
#
# usage: ynh_app_setting_delete --app=app --key=key
# | arg: -a, --app= - the application id
# | arg: -k, --key= - the setting to delete
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_delete() {
# ============ Argument parsing =============
local _globalapp=${app-:}
local -A args_array=([a]=app= [k]=key=)
local app
local key
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
# ===========================================
ynh_app_setting "delete" "$app" "$key"
}
# Small "hard-coded" interface to avoid calling "yunohost app" directly each
# time dealing with a setting is needed (which may be so slow on ARM boards)
#
# [internal]
#
ynh_app_setting() {
set +o xtrace # set +x
ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python3 - <<EOF
import os, yaml, sys
app, action = os.environ['APP'], os.environ['ACTION'].lower()
key, value = os.environ['KEY'], os.environ.get('VALUE', None)
setting_file = "/etc/yunohost/apps/%s/settings.yml" % app
assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file
with open(setting_file) as f:
settings = yaml.safe_load(f)
if action == "get":
if key in settings:
print(settings[key])
else:
if action == "delete":
if key in settings:
del settings[key]
elif action == "set":
settings[key] = value
else:
raise ValueError("action should either be get, set or delete")
with open(setting_file, "w") as f:
yaml.safe_dump(settings, f, default_flow_style=False)
EOF
set -o xtrace # set -x
}

View file

@ -0,0 +1,139 @@
#!/bin/bash
# Generate a random string
#
# usage: ynh_string_random [--length=string_length]
# | arg: -l, --length= - the string length to generate (default: 24)
# | arg: -f, --filter= - the kind of characters accepted in the output (default: 'A-Za-z0-9')
# | ret: the generated string
#
# example: pwd=$(ynh_string_random --length=8)
#
# Requires YunoHost version 2.2.4 or higher.
ynh_string_random() {
# ============ Argument parsing =============
local -A args_array=([l]=length= [f]=filter=)
local length
local filter
ynh_handle_getopts_args "$@"
length=${length:-24}
filter=${filter:-'A-Za-z0-9'}
# ===========================================
dd if=/dev/urandom bs=1 count=1000 2>/dev/null \
| tr --complement --delete "$filter" \
| sed --quiet 's/\(.\{'"$length"'\}\).*/\1/p'
}
# Substitute/replace a string (or expression) by another in a file
#
# usage: ynh_replace --match=match --replace=replace --file=file
# | arg: -m, --match= - String to be searched and replaced in the file
# | arg: -r, --replace= - String that will replace matches
# | arg: -f, --file= - File in which the string will be replaced.
#
# As this helper is based on sed command, regular expressions and references to
# sub-expressions can be used (see sed manual page for more information)
#
# Requires YunoHost version 2.6.4 or higher.
ynh_replace() {
# ============ Argument parsing =============
local -A args_array=([m]=match= [r]=replace= [f]=file=)
local match
local replace
local file
ynh_handle_getopts_args "$@"
# ===========================================
set +o xtrace # set +x
local delimit=$'\001'
# Escape the delimiter if it's in the string.
match=${match//${delimit}/"\\${delimit}"}
replace=${replace//${delimit}/"\\${delimit}"}
set -o xtrace # set -x
sed --in-place "s${delimit}${match}${delimit}${replace}${delimit}g" "$file"
}
# Substitute/replace a special string by another in a file
#
# usage: ynh_replace_special_string --match=match --replace=replace --file=file
# | arg: -m, --match= - String to be searched and replaced in the file
# | arg: -r, --replace= - String that will replace matches
# | arg: -f, --file= - File in which the string will be replaced.
#
# This helper will use ynh_replace, but as you can use special
# characters, you can't use some regular expressions and sub-expressions.
#
# Requires YunoHost version 2.7.7 or higher.
ynh_replace_special_string() {
# ============ Argument parsing =============
local -A args_array=([m]=match= [r]=replace= [f]=file=)
local match
local replace
local file
ynh_handle_getopts_args "$@"
# ===========================================
# Escape any backslash to preserve them as simple backslash.
match=${match//\\/"\\\\"}
replace=${replace//\\/"\\\\"}
# Escape the & character, who has a special function in sed.
match=${match//&/"\&"}
replace=${replace//&/"\&"}
ynh_replace --match="$match" --replace="$replace" --file="$file"
}
# Sanitize a string intended to be the name of a database
#
# [packagingv1]
#
# usage: ynh_sanitize_dbid --db_name=name
# | arg: -n, --db_name= - name to correct/sanitize
# | ret: the corrected name
#
# example: dbname=$(ynh_sanitize_dbid $app)
#
# Underscorify the string (replace - and . by _)
#
# Requires YunoHost version 2.2.4 or higher.
ynh_sanitize_dbid() {
# ============ Argument parsing =============
local -A args_array=([n]=db_name=)
local db_name
ynh_handle_getopts_args "$@"
# ===========================================
# We should avoid having - and . in the name of databases. They are replaced by _
echo ${db_name//[-.]/_}
}
# Normalize the url path syntax
#
# Handle the slash at the beginning of path and its absence at ending
# Return a normalized url path
#
# examples:
# url_path=$(ynh_normalize_url_path $url_path)
# ynh_normalize_url_path example # -> /example
# ynh_normalize_url_path /example # -> /example
# ynh_normalize_url_path /example/ # -> /example
# ynh_normalize_url_path / # -> /
#
# usage: ynh_normalize_url_path path_to_normalize
#
# Requires YunoHost version 2.6.4 or higher.
ynh_normalize_url_path() {
local path_url=$1
test -n "$path_url" || ynh_die "ynh_normalize_url_path expect a URL path as first argument and received nothing."
if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a /
path_url="/$path_url" # Add / at begin of path variable
fi
if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character.
path_url="${path_url:0:${#path_url}-1}" # Delete the last character
fi
echo $path_url
}

View file

@ -0,0 +1,172 @@
#!/bin/bash
# Create a dedicated systemd config
#
# usage: ynh_config_add_systemd [--service=service] [--template=template]
# | arg: -s, --service= - Service name (optionnal, `$app` by default)
# | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning `../conf/systemd.service` will be used as template)
#
# This will use the template `../conf/<templatename>.service`.
#
# See the documentation of `ynh_config_add` for a description of the template
# format and how placeholders are replaced with actual variables.
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add_systemd() {
# ============ Argument parsing =============
local -A args_array=([s]=service= [t]=template=)
local service
local template
ynh_handle_getopts_args "$@"
service="${service:-$app}"
template="${template:-systemd.service}"
# ===========================================
ynh_config_add --template="$template" --destination="/etc/systemd/system/$service.service"
systemctl enable $service --quiet
systemctl daemon-reload
}
# Remove the dedicated systemd config
#
# usage: ynh_config_remove_systemd service
# | arg: service - Service name (optionnal, $app by default)
ynh_config_remove_systemd() {
local service="${1:-$app}"
if [ -e "/etc/systemd/system/$service.service" ]; then
ynh_systemctl --service=$service --action=stop
systemctl disable $service --quiet
ynh_safe_rm "/etc/systemd/system/$service.service"
systemctl daemon-reload
fi
}
# Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started
#
# usage: ynh_systemctl [--service=service] [--action=action] [ [--wait_until="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ]
# | arg: -n, --service= - Name of the service to start. Default : `$app`
# | arg: -a, --action= - Action to perform with systemctl. Default: start
# | arg: -w, --wait_until= - The pattern to find in the log to attest the service is effectively fully started.
# | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log`
# | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds.
# | arg: -e, --length= - Length of the error log displayed for debugging : Default : 20
#
# Requires YunoHost version 3.5.0 or higher.
ynh_systemctl() {
# ============ Argument parsing =============
local -A args_array=([n]=service= [a]=action= [w]=wait_until= [p]=log_path= [t]=timeout= [e]=length=)
local service
local action
local wait_until
local length
local log_path
local timeout
ynh_handle_getopts_args "$@"
service="${service:-$app}"
action=${action:-start}
wait_until=${wait_until:-}
length=${length:-20}
log_path="${log_path:-/var/log/$service/$service.log}"
timeout=${timeout:-300}
# ===========================================
# Manage case of service already stopped
if [ "$action" == "stop" ] && ! systemctl is-active --quiet $service; then
return 0
fi
# Start to read the log
if [[ -n "$wait_until" ]]; then
local templog="$(mktemp)"
# Following the starting of the app in its log
if [ "$log_path" == "systemd" ]; then
# Read the systemd journal
journalctl --unit=$service --follow --since=-0 --quiet >"$templog" &
# Get the PID of the journalctl command
local pid_tail=$!
else
# Read the specified log file
tail --follow=name --retry --lines=0 "$log_path" >"$templog" 2>&1 &
# Get the PID of the tail command
local pid_tail=$!
fi
fi
# Use reload-or-restart instead of reload. So it wouldn't fail if the service isn't running.
if [ "$action" == "reload" ]; then
action="reload-or-restart"
fi
local time_start="$(date --utc --rfc-3339=seconds | cut -d+ -f1) UTC"
# If the service fails to perform the action
if ! systemctl $action $service; then
# Show syslog for this service
journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service >&2
# If a log is specified for this service, show also the content of this log
if [ -e "$log_path" ]; then
tail --lines=$length "$log_path" >&2
fi
_ynh_clean_check_starting
return 1
fi
# Start the timeout and try to find wait_until
if [[ -n "${wait_until:-}" ]]; then
set +x
local i=0
local starttime=$(date +%s)
for i in $(seq 1 $timeout); do
# Read the log until the sentence is found, that means the app finished to start. Or run until the timeout
if [ "$log_path" == "systemd" ]; then
# For systemd services, we in fact dont rely on the templog, which for some reason is not reliable, but instead re-read journalctl every iteration, starting at the timestamp where we triggered the action
if journalctl --unit=$service --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$wait_until"; then
ynh_print_info "The service $service has correctly executed the action ${action}."
break
fi
else
if grep --extended-regexp --quiet "$wait_until" "$templog"; then
ynh_print_info "The service $service has correctly executed the action ${action}."
break
fi
fi
if [ $i -eq 30 ]; then
echo "(this may take some time)" >&2
fi
# Also check the timeout using actual timestamp, because sometimes for some reason,
# journalctl may take a huge time to run, and we end up waiting literally an entire hour
# instead of 5 min ...
if [[ "$(( $(date +%s) - $starttime))" -gt "$timeout" ]]
then
i=$timeout
break
fi
sleep 1
done
set -x
if [ $i -ge 3 ]; then
echo "" >&2
fi
if [ $i -eq $timeout ]; then
ynh_print_warn "The service $service didn't fully executed the action ${action} before the timeout."
ynh_print_warn "Please find here an extract of the end of the log of the service $service:"
journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service >&2
if [ -e "$log_path" ]; then
ynh_print_warn "==="
tail --lines=$length "$log_path" >&2
fi
fi
_ynh_clean_check_starting
fi
}
_ynh_clean_check_starting() {
if [ -n "${pid_tail:-}" ]; then
# Stop the execution of tail.
kill -SIGTERM $pid_tail 2>&1
fi
if [ -n "${templog:-}" ]; then
ynh_safe_rm "$templog" 2>&1
fi
}

View file

@ -0,0 +1,388 @@
#!/bin/bash
# Create a dedicated config file from a template
#
# usage: ynh_config_add --template="template" --destination="destination"
# | arg: -t, --template= - Template config file to use
# | arg: -d, --destination= - Destination of the config file
# | arg: -j, --jinja - Use jinja template instead of legacy __MY_VAR__
#
# examples:
# ynh_config_add --template=".env" --destination="$install_dir/.env" use the template file "../conf/.env"
# ynh_config_add --jinja --template="config.j2" --destination="$install_dir/config" use the template file "../conf/config.j2"
# ynh_config_add --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf"
#
##
## How it works in "legacy" mode
##
# The template can be by default the name of a file in the conf directory
# of a YunoHost Package, a relative path or an absolute path.
#
# The helper will use the template `template` to generate a config file
# `destination` by replacing the following keywords with global variables
# that should be defined before calling this helper :
# ```
# __USER__ by $app
# __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH
# ```
# And any dynamic variables that should be defined before calling this helper like:
# ```
# __DOMAIN__ by $domain
# __PATH__ by $path
# __APP__ by $app
# __VAR_1__ by $var_1
# __VAR_2__ by $var_2
# ```
#
##
## When --jinja is enabled
##
# For a full documentation of the template you can refer to: https://jinja.palletsprojects.com/en/3.1.x/templates/
# In Yunohost context there are no really some specificity except that all variable passed are of type string.
# So here are some example of recommended usage:
#
# If you need a conditional block
#
# {% if should_my_block_be_shown == 'true' %}
# ...
# {% endif %}
#
# or
#
# {% if should_my_block_be_shown == '1' %}
# ...
# {% endif %}
#
# If you need to iterate with loop:
#
# {% for yolo in var_with_multiline_value.splitlines() %}
# ...
# {% endfor %}
#
# or
#
# {% for jail in my_var_with_coma.split(',') %}
# ...
# {% endfor %}
#
# The helper will verify the checksum and backup the destination file
# if it's different before applying the new template.
#
# And it will calculate and store the destination file checksum
# into the app settings when configuration is done.
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add() {
# ============ Argument parsing =============
local -A args_array=([t]=template= [d]=destination= [j]=jinja)
local template
local destination
local jinja
ynh_handle_getopts_args "$@"
jinja="${jinja:-0}"
# ===========================================
local template_path
if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then
template_path="$YNH_APP_BASEDIR/conf/$template"
elif [ -f "$template" ]; then
template_path=$template
else
ynh_die "The provided template $template doesn't exist"
fi
ynh_backup_if_checksum_is_different "$destination"
# Make sure to set the permissions before we copy the file
# This is to cover a case where an attacker could have
# created a file beforehand to have control over it
# (cp won't overwrite ownership / modes by default...)
touch $destination
chmod 640 $destination
_ynh_apply_default_permissions $destination
if [[ "$jinja" == 1 ]]
then
# This is ran in a subshell such that the "export" does not "contaminate" the main process
(
export $(compgen -v)
j2 "$template_path" -f env -o $destination
)
else
cp -f "$template_path" "$destination"
ynh_replace_vars --file="$destination"
fi
ynh_store_file_checksum "$destination"
}
# Replace variables in a file
#
# [internal]
#
# usage: ynh_replace_vars --file="file"
# | arg: -f, --file= - File where to replace variables
#
# The helper will replace the following keywords with global variables
# that should be defined before calling this helper :
# __PATH__ by $path
# __PATH__/ by $path/ if $path != /, or just / otherwise (instead of //)
# __USER__ by $app
# __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH
#
# And any dynamic variables that should be defined before calling this helper like:
# __DOMAIN__ by $domain
# __APP__ by $app
# __VAR_1__ by $var_1
# __VAR_2__ by $var_2
#
# Requires YunoHost version 4.1.0 or higher.
ynh_replace_vars() {
# ============ Argument parsing =============
local -A args_array=([f]=file=)
local file
ynh_handle_getopts_args "$@"
# ===========================================
# Replace specific YunoHost variables
if test -n "${path:-}"; then
# path_slash_less is path, or a blank value if path is only '/'
local path_slash_less=${path%/}
ynh_replace --match="__PATH__/" --replace="$path_slash_less/" --file="$file"
ynh_replace --match="__PATH__" --replace="$path" --file="$file"
fi
if test -n "${app:-}"; then
ynh_replace --match="__USER__" --replace="$app" --file="$file"
fi
if test -n "${ynh_node_load_PATH:-}"; then
ynh_replace --match="__YNH_NODE_LOAD_PATH__" --replace="$ynh_node_load_PATH" --file="$file"
fi
# Replace others variables
# List other unique (__ __) variables in $file
local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g"))
set +o xtrace # set +x
# Do the replacement
local delimit=@
for one_var in "${uniques_vars[@]}"; do
# Validate that one_var is indeed defined
# -v checks if the variable is defined, for example:
# -v FOO tests if $FOO is defined
# -v $FOO tests if ${!FOO} is defined
# More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964
[[ -v "${one_var:-}" ]] || ynh_die "Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file"
# Escape delimiter in match/replace string
match_string="__${one_var^^}__"
match_string=${match_string//${delimit}/"\\${delimit}"}
replace_string="${!one_var}"
replace_string=${replace_string//\\/\\\\}
replace_string=${replace_string//${delimit}/"\\${delimit}"}
# Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs)
sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file"
done
set -o xtrace # set -x
}
# Get a value from heterogeneous file (yaml, json, php, python...)
#
# usage: ynh_read_var_in_file --file=PATH --key=KEY
# | arg: -f, --file= - the path to the file
# | arg: -k, --key= - the key to get
# | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file)
#
# This helpers match several var affectation use case in several languages
# We don't use jq or equivalent to keep comments and blank space in files
# This helpers work line by line, it is not able to work correctly
# if you have several identical keys in your files
#
# Example of line this helpers can managed correctly
# .yml
# title: YunoHost documentation
# email: 'yunohost@yunohost.org'
# .json
# "theme": "colib'ris",
# "port": 8102
# "some_boolean": false,
# "user": null
# .ini
# some_boolean = On
# action = "Clear"
# port = 20
# .php
# $user=
# user => 20
# .py
# USER = 8102
# user = 'https://donate.local'
# CUSTOM['user'] = 'YunoHost'
#
# Requires YunoHost version 4.3 or higher.
ynh_read_var_in_file() {
# ============ Argument parsing =============
local -A args_array=([f]=file= [k]=key= [a]=after=)
local file
local key
local after
ynh_handle_getopts_args "$@"
after="${after:-}"
# ===========================================
[[ -f $file ]] || ynh_die "File $file does not exists"
set +o xtrace # set +x
# Get the line number after which we search for the variable
local line_number=1
if [[ -n "$after" ]]; then
line_number=$(grep -m1 -n $after $file | cut -d: -f1)
if [[ -z "$line_number" ]]; then
set -o xtrace # set -x
return 1
fi
fi
local filename="$(basename -- "$file")"
local ext="${filename##*.}"
local endline=',;'
local assign="=>|:|="
local comments="#"
local string="\"'"
if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then
endline='#'
fi
if [[ "$ext" =~ ^ini|env$ ]]; then
comments="[;#]"
fi
if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then
comments="//"
fi
local list='\[\s*['$string']?\w+['$string']?\]'
local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*'
var_part+="[$string]?${key}[$string]?"
var_part+='\s*\]?\s*'
var_part+="($assign)"
var_part+='\s*'
# Extract the part after assignation sign
local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
set -o xtrace # set -x
echo YNH_NULL
return 0
fi
# Remove comments if needed
local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")"
local first_char="${expression:0:1}"
if [[ "$first_char" == '"' ]]; then
echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g'
elif [[ "$first_char" == "'" ]]; then
echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g"
else
echo "$expression"
fi
set -o xtrace # set -x
}
# Set a value into heterogeneous file (yaml, json, php, python...)
#
# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE
# | arg: -f, --file= - the path to the file
# | arg: -k, --key= - the key to set
# | arg: -v, --value= - the value to set
# | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file)
#
# Requires YunoHost version 4.3 or higher.
ynh_write_var_in_file() {
# ============ Argument parsing =============
local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=)
local file
local key
local value
local after
ynh_handle_getopts_args "$@"
after="${after:-}"
# ===========================================
[[ -f $file ]] || ynh_die "File $file does not exists"
set +o xtrace # set +x
# Get the line number after which we search for the variable
local after_line_number=1
if [[ -n "$after" ]]; then
after_line_number=$(grep -m1 -n $after $file | cut -d: -f1)
if [[ -z "$after_line_number" ]]; then
set -o xtrace # set -x
return 1
fi
fi
local filename="$(basename -- "$file")"
local ext="${filename##*.}"
local endline=',;'
local assign="=>|:|="
local comments="#"
local string="\"'"
if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then
endline='#'
fi
if [[ "$ext" =~ ^ini|env$ ]]; then
comments="[;#]"
fi
if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then
comments="//"
fi
local list='\[\s*['$string']?\w+['$string']?\]'
local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*'
var_part+="[$string]?${key}[$string]?"
var_part+='\s*\]?\s*'
var_part+="($assign)"
var_part+='\s*'
# Extract the part after assignation sign
local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
set -o xtrace # set -x
return 1
fi
local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)"
value_line_number=$((after_line_number + value_line_number))
local range="${after_line_number},${value_line_number} "
# Remove comments if needed
local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")"
endline=${expression_with_comment#"$expression"}
endline="$(echo "$endline" | sed 's/\\/\\\\/g')"
value="$(echo "$value" | sed 's/\\/\\\\/g')"
value=${value//&/"\&"}
local first_char="${expression:0:1}"
delimiter=$'\001'
if [[ "$first_char" == '"' ]]; then
# \ and sed is quite complex you need 2 \\ to get one in a sed
# So we need \\\\ to go through 2 sed
value="$(echo "$value" | sed 's/"/\\\\"/g')"
sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file}
elif [[ "$first_char" == "'" ]]; then
# \ and sed is quite complex you need 2 \\ to get one in a sed
# However double quotes implies to double \\ to
# So we need \\\\\\\\ to go through 2 sed and 1 double quotes str
value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")"
sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file}
else
if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then
value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"'
fi
if [[ "$ext" =~ ^yaml|yml$ ]]; then
value=" $value"
fi
sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file}
fi
set -o xtrace # set -x
}

182
helpers/helpers.v2.1.d/user Normal file
View file

@ -0,0 +1,182 @@
#!/bin/bash
# Check if a YunoHost user exists
#
# usage: ynh_user_exists --username=username
# | arg: -u, --username= - the username to check
# | ret: 0 if the user exists, 1 otherwise.
#
# example: ynh_user_exists 'toto' || echo "User does not exist"
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_exists() {
# ============ Argument parsing =============
local -A args_array=([u]=username=)
local username
ynh_handle_getopts_args "$@"
# ===========================================
yunohost user list --output-as json --quiet | jq -e ".users.\"${username}\"" >/dev/null
}
# Retrieve a YunoHost user information
#
# usage: ynh_user_get_info --username=username --key=key
# | arg: -u, --username= - the username to retrieve info from
# | arg: -k, --key= - the key to retrieve
# | ret: the value associate to that key
#
# example: mail=$(ynh_user_get_info --username="toto" --key=mail)
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_get_info() {
# ============ Argument parsing =============
local -A args_array=([u]=username= [k]=key=)
local username
local key
ynh_handle_getopts_args "$@"
# ===========================================
yunohost user info "$username" --output-as json --quiet | jq -r ".$key"
}
# Get the list of YunoHost users
#
# usage: ynh_user_list
# | ret: one username per line as strings
#
# example: for u in $(ynh_user_list); do ... ; done
#
# Requires YunoHost version 2.4.0 or higher.
ynh_user_list() {
yunohost user list --output-as json --quiet | jq -r ".users | keys[]"
}
# Check if a user exists on the system
#
# [packagingv1]
#
# usage: ynh_system_user_exists --username=username
# | arg: -u, --username= - the username to check
# | ret: 0 if the user exists, 1 otherwise.
#
# Requires YunoHost version 2.2.4 or higher.
ynh_system_user_exists() {
# ============ Argument parsing =============
local -A args_array=([u]=username=)
local username
ynh_handle_getopts_args "$@"
# ===========================================
getent passwd "$username" &>/dev/null
}
# Check if a group exists on the system
#
# [packagingv1]
#
# usage: ynh_system_group_exists --group=group
# | arg: -g, --group= - the group to check
# | ret: 0 if the group exists, 1 otherwise.
#
# Requires YunoHost version 3.5.0.2 or higher.
ynh_system_group_exists() {
# ============ Argument parsing =============
local -A args_array=([g]=group=)
local group
ynh_handle_getopts_args "$@"
# ===========================================
getent group "$group" &>/dev/null
}
# Create a system user
#
# [packagingv1]
#
# usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell] [--groups="group1 group2"]
# | arg: -u, --username= - Name of the system user that will be create
# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home
# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
# | arg: -g, --groups - Add the user to system groups. Typically meant to add the user to the ssh.app / sftp.app group (e.g. for borgserver, my_webapp)
#
# Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) :
# ```
# ynh_system_user_create --username=nextcloud
# ```
# Create a discourse user using /var/www/discourse as home directory and the default login shell :
# ```
# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell
# ```
#
# Requires YunoHost version 2.6.4 or higher.
ynh_system_user_create() {
# ============ Argument parsing =============
local -A args_array=([u]=username= [h]=home_dir= [s]=use_shell [g]=groups=)
local username
local home_dir
local use_shell
local groups
ynh_handle_getopts_args "$@"
use_shell="${use_shell:-0}"
home_dir="${home_dir:-}"
groups="${groups:-}"
# ===========================================
if ! ynh_system_user_exists --username="$username"; then # Check if the user exists on the system
# If the user doesn't exist
if [ -n "$home_dir" ]; then # If a home dir is mentioned
local user_home_dir="--home-dir $home_dir"
else
local user_home_dir="--no-create-home"
fi
if [ $use_shell -eq 1 ]; then # If we want a shell for the user
local shell="" # Use default shell
else
local shell="--shell /usr/sbin/nologin"
fi
useradd $user_home_dir --system --user-group $username $shell || ynh_die "Unable to create $username system account"
fi
local group
for group in $groups; do
usermod -a -G "$group" "$username"
done
}
# Delete a system user
#
# [packagingv1]
#
# usage: ynh_system_user_delete --username=user_name
# | arg: -u, --username= - Name of the system user that will be create
#
# Requires YunoHost version 2.6.4 or higher.
ynh_system_user_delete() {
# ============ Argument parsing =============
local -A args_array=([u]=username=)
local username
ynh_handle_getopts_args "$@"
# ===========================================
# Check if the user exists on the system
if ynh_system_user_exists --username="$username"; then
deluser $username
else
ynh_print_warn "The user $username was not found"
fi
# Check if the group exists on the system
if ynh_system_group_exists --group="$username"; then
delgroup $username
fi
}
# Execute a command after sudoing as $app
#
# Note that exported bash env variables are kept (using -E option of sudo)
#
# usage: ynh_exec_as_app COMMAND [ARG ...]
ynh_exec_as_app() {
sudo -E -u"$app" "$@"
}

View file

@ -0,0 +1,629 @@
#!/bin/bash
YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)}
# Handle script crashes / failures
#
# [internal]
#
# usage:
# ynh_exit_properly is used only by the helper ynh_abort_if_errors.
# You should not use it directly.
# Instead, add to your script:
# ynh_clean_setup () {
# instructions...
# }
#
# This function provide a way to clean some residual of installation that not managed by remove script.
#
# It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script
#
# Requires YunoHost version 2.6.4 or higher.
ynh_exit_properly() {
local exit_code=$?
if [[ "${YNH_APP_ACTION:-}" =~ ^install$|^upgrade$|^restore$ ]]
then
rm -rf "/var/cache/yunohost/download/"
fi
if [ "$exit_code" -eq 0 ]; then
exit 0 # Exit without error if the script ended correctly
fi
trap '' EXIT # Ignore new exit signals
# Do not exit anymore if a command fail or if a variable is empty
set +o errexit # set +e
set +o nounset # set +u
# Small tempo to avoid the next message being mixed up with other DEBUG messages
sleep 0.5
if type -t ynh_clean_setup >/dev/null; then # Check if the function exist in the app script.
ynh_clean_setup # Call the function to do specific cleaning for the app.
fi
# Exit with error status
# We don't call ynh_die basically to avoid unecessary 10-ish
# debug lines about parsing args and stuff just to exit 1..
exit 1
}
# Exits if an error occurs during the execution of the script.
#
# [packagingv1]
#
# usage: ynh_abort_if_errors
#
# This configure the rest of the script execution such that, if an error occurs
# or if an empty variable is used, the execution of the script stops immediately
# and a call to `ynh_clean_setup` is triggered if it has been defined by your script.
#
# Requires YunoHost version 2.6.4 or higher.
ynh_abort_if_errors() {
set -o errexit # set -e; Exit if a command fail
set -o nounset # set -u; And if a variable is used unset
trap ynh_exit_properly EXIT # Capturing exit signals on shell script
}
# When running an app script, auto-enable ynh_abort_if_errors except for remove script
if [[ "${YNH_CONTEXT:-}" != "regenconf" ]] && [[ "${YNH_APP_ACTION}" != "remove" ]]
then
ynh_abort_if_errors
fi
# Download, check integrity, uncompress and patch upstream sources
#
# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] [--full_replace]
# | arg: -d, --dest_dir= - Directory where to setup sources
# | arg: -s, --source_id= - Name of the source, defaults to `main` (when the sources resource exists in manifest.toml) or (legacy) `app` otherwise
# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs' (no trailing `/` for folders)
# | arg: -r, --full_replace= - Remove previous sources before installing new sources (can be 1 or 0, default to 0)
#
# #### New 'sources' resources
#
# (See also the resources documentation which may be more complete?)
#
# This helper will read infos from the 'sources' resources in the manifest.toml of the app
# and expect a structure like:
#
# ```toml
# [resources.sources]
# [resources.sources.main]
# url = "https://some.address.to/download/the/app/archive"
# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL
# ```
#
# ##### Optional flags
#
# ```text
# format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract
# "zip" # automatically guessed from the extension of the URL, but can be set explicitly. Will use `unzip` to extract
# "docker" # useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` to extract
# "whatever" # an arbitrary value, not really meaningful except to imply that the file won't be extracted
#
# in_subdir = true # default, there's an intermediate subdir in the archive before accessing the actual files
# false # sources are directly in the archive root
# n # (special cases) an integer representing a number of subdirs levels to get rid of
#
# extract = true # default if file is indeed an archive such as .zip, .tar.gz, .tar.bz2, ...
# = false # default if file 'format' is not set and the file is not to be extracted because it is not an archive but a script or binary or whatever asset.
# # in which case the file will only be `mv`ed to the location possibly renamed using the `rename` value
#
# rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical
# platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for
# ```
#
# You may also define assets url and checksum per-architectures such as:
# ```toml
# [resources.sources]
# [resources.sources.main]
# amd64.url = "https://some.address.to/download/the/app/archive/when/amd64"
# amd64.sha256 = "0123456789abcdef"
# armhf.url = "https://some.address.to/download/the/app/archive/when/armhf"
# armhf.sha256 = "fedcba9876543210"
# ```
#
# In which case ynh_setup_source --dest_dir="$install_dir" will automatically pick the appropriate source depending on the arch
#
# The helper will:
# - Download the specific URL if there is no local archive
# - Check the integrity with the specific sha256 sum
# - Uncompress the archive to `$dest_dir`.
# - If `in_subdir` is true, the first level directory of the archive will be removed.
# - If `in_subdir` is a numeric value, the N first level directories will be removed.
# - Patches named `patches/${src_id}-*.patch` will be applied to `$dest_dir`
#
# Requires YunoHost version 2.6.4 or higher.
ynh_setup_source() {
# ============ Argument parsing =============
local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep= [r]=full_replace)
local dest_dir
local source_id
local keep
local full_replace
ynh_handle_getopts_args "$@"
keep="${keep:-}"
full_replace="${full_replace:-0}"
source_id="${source_id:-main}"
# ===========================================
local sources_json=$(ynh_read_manifest "resources.sources[\"$source_id\"]")
if jq -re ".url" <<< "$sources_json"
then
local arch_prefix=""
else
local arch_prefix=".$YNH_ARCH"
fi
local src_url="$(jq -r "$arch_prefix.url" <<< "$sources_json" | sed 's/^null$//')"
local src_sum="$(jq -r "$arch_prefix.sha256" <<< "$sources_json" | sed 's/^null$//')"
local src_sumprg="sha256sum"
local src_format="$(jq -r ".format" <<< "$sources_json" | sed 's/^null$//')"
local src_in_subdir="$(jq -r ".in_subdir" <<< "$sources_json" | sed 's/^null$//')"
src_in_subdir=${src_in_subdir:-true}
local src_extract="$(jq -r ".extract" <<< "$sources_json" | sed 's/^null$//')"
local src_platform="$(jq -r ".platform" <<< "$sources_json" | sed 's/^null$//')"
local src_rename="$(jq -r ".rename" <<< "$sources_json" | sed 's/^null$//')"
[[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id$arch_prefix ?"
[[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id$arch_prefix ?"
if [[ -z "$src_format" ]]
then
if [[ "$src_url" =~ ^.*\.zip$ ]] || [[ "$src_url" =~ ^.*/zipball/.*$ ]]
then
src_format="zip"
elif [[ "$src_url" =~ ^.*\.tar\.gz$ ]] || [[ "$src_url" =~ ^.*\.tgz$ ]] || [[ "$src_url" =~ ^.*/tar\.gz/.*$ ]] || [[ "$src_url" =~ ^.*/tarball/.*$ ]]
then
src_format="tar.gz"
elif [[ "$src_url" =~ ^.*\.tar\.xz$ ]]
then
src_format="tar.xz"
elif [[ "$src_url" =~ ^.*\.tar\.bz2$ ]]
then
src_format="tar.bz2"
elif [[ -z "$src_extract" ]]
then
src_extract="false"
fi
fi
src_format=${src_format:-tar.gz}
src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]')
src_extract=${src_extract:-true}
if [[ "$src_extract" != "true" ]] && [[ "$src_extract" != "false" ]]
then
ynh_die "For source $source_id, expected either 'true' or 'false' for the extract parameter"
fi
# (Unused?) mecanism where one can have the file in a special local cache to not have to download it...
local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${source_id}"
# Gotta use this trick with 'dirname' because source_id may contain slashes x_x
mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id})
src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}"
if [ "$src_format" = "docker" ]; then
src_platform="${src_platform:-"linux/$YNH_ARCH"}"
else
if test -e "$local_src"; then
cp $local_src $src_filename
fi
[ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?"
# If the file was prefetched but somehow doesn't match the sum, rm and redownload it
if [ -e "$src_filename" ] && ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status
then
rm -f "$src_filename"
fi
# Only redownload the file if it wasnt prefetched
if [ ! -e "$src_filename" ]
then
# NB. we have to declare the var as local first,
# otherwise 'local foo=$(false) || echo 'pwet'" does'nt work
# because local always return 0 ...
local out
# Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget)
out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \
|| ynh_die "$out"
fi
# Check the control sum
if ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status
then
local actual_sum="$(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1)"
local actual_size="$(du -hs ${src_filename} | cut --fields=1)"
rm -f ${src_filename}
ynh_die "Corrupt source for ${src_url}: Expected sha256sum to be ${src_sum} but got ${actual_sum} (size: ${actual_size})."
fi
fi
# Keep files to be backup/restored at the end of the helper
# Assuming $dest_dir already exists
rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/
if [ -n "$keep" ] && [ -e "$dest_dir" ]; then
local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID}
mkdir -p $keep_dir
local stuff_to_keep
for stuff_to_keep in $keep; do
if [ -e "$dest_dir/$stuff_to_keep" ]; then
mkdir --parents "$(dirname "$keep_dir/$stuff_to_keep")"
cp --archive "$dest_dir/$stuff_to_keep" "$keep_dir/$stuff_to_keep"
fi
done
fi
if [ "$full_replace" -eq 1 ]; then
ynh_safe_rm "$dest_dir"
fi
# Extract source into the app dir
mkdir --parents "$dest_dir"
if [ -n "${install_dir:-}" ] && [ "$dest_dir" == "$install_dir" ]; then
_ynh_apply_default_permissions $dest_dir
fi
if [[ "$src_extract" == "false" ]]; then
if [[ -z "$src_rename" ]]
then
mv $src_filename $dest_dir
else
mv $src_filename $dest_dir/$src_rename
fi
elif [[ "$src_format" == "docker" ]]; then
"$YNH_HELPERS_DIR/vendor/docker-image-extract/docker-image-extract" -p $src_platform -o $dest_dir $src_url 2>&1
elif [[ "$src_format" == "zip" ]]; then
# Zip format
# Using of a temp directory, because unzip doesn't manage --strip-components
if $src_in_subdir; then
local tmp_dir=$(mktemp --directory)
unzip -quo $src_filename -d "$tmp_dir"
cp --archive $tmp_dir/*/. "$dest_dir"
ynh_safe_rm "$tmp_dir"
else
unzip -quo $src_filename -d "$dest_dir"
fi
ynh_safe_rm "$src_filename"
else
local strip=""
if [ "$src_in_subdir" != "false" ]; then
if [ "$src_in_subdir" == "true" ]; then
local sub_dirs=1
else
local sub_dirs="$src_in_subdir"
fi
strip="--strip-components $sub_dirs"
fi
if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]]; then
tar --extract --file=$src_filename --directory="$dest_dir" $strip
else
ynh_die "Archive format unrecognized."
fi
ynh_safe_rm "$src_filename"
fi
# Apply patches
if [ -d "$YNH_APP_BASEDIR/patches/" ]; then
local patches_folder=$(realpath $YNH_APP_BASEDIR/patches/)
# Check if any file matching the pattern exists, cf https://stackoverflow.com/a/34195247
if compgen -G "$patches_folder/${source_id}-*.patch" >/dev/null; then
pushd "$dest_dir"
for p in $patches_folder/${source_id}-*.patch; do
echo $p
patch --strip=1 <$p || ynh_print_warn "Packagers /!\\ patch $p failed to apply"
done
popd
fi
fi
# Keep files to be backup/restored at the end of the helper
# Assuming $dest_dir already exists
if [ -n "$keep" ]; then
local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID}
local stuff_to_keep
for stuff_to_keep in $keep; do
if [ -e "$keep_dir/$stuff_to_keep" ]; then
mkdir --parents "$(dirname "$dest_dir/$stuff_to_keep")"
# We add "--no-target-directory" (short option is -T) to handle the special case
# when we "keep" a folder, but then the new setup already contains the same dir (but possibly empty)
# in which case a regular "cp" will create a copy of the directory inside the directory ...
# resulting in something like /var/www/$app/data/data instead of /var/www/$app/data
# cf https://unix.stackexchange.com/q/94831 for a more elaborate explanation on the option
cp --archive --no-target-directory "$keep_dir/$stuff_to_keep" "$dest_dir/$stuff_to_keep"
fi
done
fi
rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/
}
# Curl abstraction to help with POST requests to local pages (such as installation forms)
#
# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ...
# | arg: page_uri - Path (relative to `$path`) of the page where POST data will be sent
# | arg: key1=value1 - (Optionnal) POST key and corresponding value
# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value
# | arg: ... - (Optionnal) More POST keys and values
#
# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2"
#
# For multiple calls, cookies are persisted between each call for the same app
#
# `$domain` and `$path` should be defined externally (and correspond to the domain.tld and the /path (of the app?))
#
# Requires YunoHost version 2.6.4 or higher.
ynh_local_curl() {
# Define url of page to curl
local local_page=$(ynh_normalize_url_path $1)
local full_path=$path$local_page
if [ "${path}" == "/" ]; then
full_path=$local_page
fi
local full_page_url=https://localhost$full_path
# Concatenate all other arguments with '&' to prepare POST data
local POST_data=""
local arg=""
for arg in "${@:2}"; do
POST_data="${POST_data}${arg}&"
done
if [ -n "$POST_data" ]; then
# Add --data arg and remove the last character, which is an unecessary '&'
POST_data="--data ${POST_data::-1}"
fi
# Wait untils nginx has fully reloaded (avoid curl fail with http2)
sleep 2
local cookiefile=/tmp/ynh-$app-cookie.txt
touch $cookiefile
chown root $cookiefile
chmod 700 $cookiefile
# Temporarily enable visitors if needed...
local visitors_enabled=$(ynh_permission_has_user --permission="main" --user="visitors" && echo yes || echo no)
if [[ $visitors_enabled == "no" ]]; then
ynh_permission_update --permission="main" --add="visitors"
fi
# Curl the URL
curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile
if [[ $visitors_enabled == "no" ]]; then
ynh_permission_update --permission="main" --remove="visitors"
fi
}
_acceptable_path_to_delete() {
local file=$1
local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*} /etc/yunohost/{apps,domains,hooks.d} /opt/yunohost 2> /dev/null)
# Legacy : A couple apps still have data in /home/$app ...
if [[ -n "${app:-}" ]]
then
forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app")
fi
# Use realpath to normalize the path ..
# i.e convert ///foo//bar//..///baz//// to /foo/baz
file=$(realpath --no-symlinks "$file")
if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then
return 1
else
return 0
fi
}
# Remove a file or a directory, checking beforehand that it's not a disastrous location to rm such as entire /var or /home
#
# usage: ynh_safe_rm path_to_remove
#
# Requires YunoHost version 2.6.4 or higher.
ynh_safe_rm() {
local target="$1"
set +o xtrace # set +x
if [ $# -ge 2 ]; then
ynh_print_warn "/!\ Packager ! You provided more than one argument to ynh_safe_rm but it will be ignored... Use this helper with one argument at time."
fi
if [[ -z "$target" ]]; then
ynh_print_warn "ynh_safe_rm called with empty argument, ignoring."
elif [[ ! -e $target ]]; then
ynh_print_info "'$target' wasn't deleted because it doesn't exist."
elif ! _acceptable_path_to_delete "$target"; then
ynh_print_warn "Not deleting '$target' because it is not an acceptable path to delete."
else
rm --recursive "$target"
fi
set -o xtrace # set -x
}
# Read the value of a key in the app's manifest
#
# usage: ynh_read_manifest "key"
# | arg: key - Name of the key to find
# | ret: the value associate to that key
#
# Requires YunoHost version 3.5.0 or higher.
ynh_read_manifest() {
cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq ".$1" --raw-output
}
# Return the app upstream version, deduced from `$YNH_APP_MANIFEST_VERSION` and strippig the `~ynhX` part
#
# usage: ynh_app_upstream_version
# | ret: the version number of the upstream app
#
# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2`
#
# Requires YunoHost version 3.5.0 or higher.
ynh_app_upstream_version() {
echo "${$YNH_APP_MANIFEST_VERSION/~ynh*/}"
}
# Return 0 if the "upstream" part of the version changed, or 1 otherwise (ie only the ~ynh suffix changed)
#
# usage: if ynh_app_upstream_version_changed; then ...
ynh_app_upstream_version_changed() {
# "UPGRADE_PACKAGE" means only the ~ynh prefix changed
[[ "$YNH_APP_UPGRADE_TYPE" == "UPGRADE_PACKAGE" ]] && return 1 || return 0
}
# Compare the current package version is strictly lower than another version given as an argument
#
# example: if ynh_app_upgrading_from_version_before 2.3.2~ynh1; then ...
#
# Requires YunoHost version 11.2 or higher.
ynh_app_upgrading_from_version_before() {
local version=$1
[[ $version =~ '~ynh' ]] || ynh_die "Invalid argument for version, should include the ~ynhX prefix"
dpkg --compare-versions $YNH_APP_CURRENT_VERSION lt $version
}
# Compare the current package version is lower or equal to another version given as an argument
#
# example: if ynh_app_upgrading_from_version_before_or_equal_to 2.3.2~ynh1; then ...
#
# Requires YunoHost version 11.2 or higher.
ynh_app_upgrading_from_version_before_or_equal_to() {
local version=$1
[[ $version =~ '~ynh' ]] || ynh_die "Invalid argument for version, should include the ~ynhX prefix"
dpkg --compare-versions $YNH_APP_CURRENT_VERSION le $version
}
# Check if we should enforce sane default permissions (= disable rwx for 'others')
# on file/folders handled with ynh_setup_source and ynh_config_add
#
# [internal]
#
# Having a file others-readable or a folder others-executable(=enterable)
# is a security risk comparable to "chmod 777"
#
# Configuration files may contain secrets. Or even just being able to enter a
# folder may allow an attacker to do nasty stuff (maybe a file or subfolder has
# some write permission enabled for 'other' and the attacker may edit the
# content or create files as leverage for priviledge escalation ...)
#
# The sane default should be to set ownership to $app:$app.
# In specific case, you may want to set the ownership to $app:www-data
# for example if nginx needs access to static files.
#
_ynh_apply_default_permissions() {
local target=$1
chmod o-rwx $target
chmod g-w $target
chown -R root:root $target
if ynh_system_user_exists --username=$app; then
chown $app:$app $target
fi
# Crons should be owned by root
# Also we don't want systemd conf, nginx conf or others stuff to be owned by the app,
# otherwise they could self-edit their own systemd conf and escalate privilege
if echo "$target" | grep -q '^/etc/cron\|/etc/php\|/etc/nginx/conf.d\|/etc/fail2ban\|/etc/systemd/system'
then
chmod 400 $target
chown root:root $target
fi
}
int_to_bool() {
sed -e 's/^1$/True/g' -e 's/^0$/False/g' -e 's/^true$/True/g' -e 's/^false$/False/g'
}
toml_to_json() {
python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))'
}
# Validate an IP address
#
# usage: ynh_validate_ip --family=family --ip_address=ip_address
# | ret: 0 for valid ip addresses, 1 otherwise
#
# example: ynh_validate_ip 4 111.222.333.444
#
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip() {
# ============ Argument parsing =============
local -A args_array=([f]=family= [i]=ip_address=)
local family
local ip_address
ynh_handle_getopts_args "$@"
# ===========================================
[ "$family" == "4" ] || [ "$family" == "6" ] || return 1
# http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298
python3 /dev/stdin <<EOF
import socket
import sys
family = { "4" : socket.AF_INET, "6" : socket.AF_INET6 }
try:
socket.inet_pton(family["$family"], "$ip_address")
except socket.error:
sys.exit(1)
sys.exit(0)
EOF
}
# Get the total or free amount of RAM+swap on the system
#
# [packagingv1]
#
# usage: ynh_get_ram [--free|--total]
# | arg: -f, --free - Count free RAM+swap
# | arg: -t, --total - Count total RAM+swap
# | ret: the amount of free ram, in MB (MegaBytes)
#
# Requires YunoHost version 3.8.1 or higher.
ynh_get_ram() {
# ============ Argument parsing =============
local -A args_array=([f]=free [t]=total)
local free
local total
ynh_handle_getopts_args "$@"
free=${free:-0}
total=${total:-0}
# ===========================================
if [ $free -eq $total ]; then
ynh_print_warn "You have to choose --free or --total when using ynh_get_ram"
ram=0
elif [ $free -eq 1 ]; then
local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}')
local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}')
local free_ram_swap=$((free_ram + free_swap))
local ram=$free_ram_swap
elif [ $total -eq 1 ]; then
local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}')
local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}')
local total_ram_swap=$((total_ram + total_swap))
local ram=$total_ram_swap
fi
echo $ram
}
# Check if the scripts are being run by the package_check in CI
#
# usage: ynh_in_ci_tests
#
# Return 0 if in CI, 1 otherwise
#
# Requires YunoHost version 11.3 or higher.
ynh_in_ci_tests() {
[ "${PACKAGE_CHECK_EXEC:-0}" -eq 1 ]
}

View file

@ -0,0 +1 @@
../vendor

View file

@ -162,6 +162,8 @@ EOF
mkdir -p ${pending_dir}/etc/dpkg/origins/
cp dpkg-origins ${pending_dir}/etc/dpkg/origins/yunohost
# Remove legacy hackish/clumsy nodejs autoupdate which ends up filling up space with ambiguous upgrades >_>
touch "/etc/cron.daily/node_update"
}
do_post_regen() {

View file

@ -1486,6 +1486,9 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None):
for permission_name in user_permission_list(apps=[app])["permissions"].keys():
permission_delete(permission_name, force=True, sync_perm=False)
if purge and os.path.exists(f"/var/log/{app}"):
shutil.rmtree(f"/var/log/{app}")
if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path)

View file

@ -155,6 +155,19 @@ class AppResource:
elif manager and manager.current and "version" in manager.current:
app_upstream_version = manager.current["version"].split("~")[0]
# FIXME : should use packaging.version to properly parse / compare versions >_>
self.helpers_version = None
if manager and manager.wanted and manager.wanted.get("integration", {}).get("helpers_version"):
self.helpers_version = manager.wanted.get("integration", {}).get("helpers_version")
elif manager and manager.current and manager.current.get("integration", {}).get("helpers_version"):
self.helpers_version = manager.current.get("integration", {}).get("helpers_version")
elif manager and manager.wanted and manager.wanted.get("packaging_format"):
self.helpers_version = str(manager.wanted.get("packaging_format"))
elif manager and manager.current and manager.current.get("packaging_format"):
self.helpers_version = str(manager.current.get("packaging_format"))
if not self.helpers_version:
self.helpers_version = "1"
replacements: dict[str, str] = {
"__APP__": self.app,
"__YNH_ARCH__": system_arch(),
@ -1182,11 +1195,19 @@ class AptDependenciesAppResource(AppResource):
}
def provision_or_update(self, context: Dict = {}):
script = " ".join(["ynh_install_app_dependencies", *self.packages])
if float(self.helpers_version) >= 2.1:
ynh_apt_install_dependencies = "ynh_apt_install_dependencies"
ynh_apt_install_dependencies_from_extra_repository = "ynh_apt_install_dependencies_from_extra_repository"
else:
ynh_apt_install_dependencies = "ynh_install_app_dependencies"
ynh_apt_install_dependencies_from_extra_repository = "ynh_install_extra_app_dependencies"
script = " ".join([ynh_apt_install_dependencies, *self.packages])
for repo, values in self.extras.items():
script += "\n" + " ".join(
[
"ynh_install_extra_app_dependencies",
ynh_apt_install_dependencies_from_extra_repository,
f"--repo='{values['repo']}'",
f"--key='{values['key']}'",
f"--package='{' '.join(values['packages'])}'",
@ -1197,7 +1218,12 @@ class AptDependenciesAppResource(AppResource):
self._run_script("provision_or_update", script)
def deprovision(self, context: Dict = {}):
self._run_script("deprovision", "ynh_remove_app_dependencies")
if float(self.helpers_version) >= 2.1:
ynh_apt_remove_dependencies = "ynh_apt_remove_dependencies"
else:
ynh_apt_remove_dependencies = "ynh_remove_app_dependencies"
self._run_script("deprovision", ynh_apt_remove_dependencies)
class PortsResource(AppResource):
@ -1455,12 +1481,17 @@ class DatabaseAppResource(AppResource):
db_name = self.get_setting("db_name") or db_user
if self.dbtype == "mysql":
self._run_script(
"deprovision", f"ynh_mysql_remove_db '{db_name}' '{db_user}'"
)
db_helper_name = "mysql"
elif self.dbtype == "postgresql":
db_helper_name = "psql"
else:
raise RuntimeError(f"Invalid dbtype {self.dbtype}")
self._run_script(
"deprovision", f"ynh_psql_remove_db '{db_name}' '{db_user}'"
"deprovision", f"""
ynh_{db_helper_name}_database_exists "{db_name}" && ynh_{db_helper_name}_drop_db "{db_name}" || true
ynh_{db_helper_name}_user_exists "{db_user}" && ynh_{db_helper_name}_drop_user "{db_user}" || true
"""
)
self.delete_setting("db_name")