shinyscripts/tools/arch-tmpfiles

436 lines
10 KiB
Plaintext
Raw Normal View History

2022-11-13 11:46:30 +01:00
#!/bin/bash
#
# /usr/lib/initscripts/arch-tmpfiles
#
# Control creation, deletion, and cleaning of volatile and temporary files
#
warninvalid() {
local description=$1 file=${2:-${files[$TOTALNUM]}} linenum=${3:-${linenums[$TOTALNUM]}}
printf "%s:line %d: ignoring invalid entry: %s\n" "$file" "$linenum" "$description"
(( ++error ))
} >&2
checkparams() {
shift
local path=$1 mode=$2 uid=$3 gid=$4 age=$5
# mode must be valid octal and 3 or 4 digits
if [[ $mode != '-' ]]; then
if [[ ! $mode =~ ^[0-7]{3,4}$ ]]; then
warninvalid "invalid mode '$mode'"
return 1
fi
fi
# uid must be numeric or a valid user name
# don't try to resolve numeric IDs in case they don't exist
if [[ $uid != '-' ]]; then
if [[ $uid != +([0-9]) ]] && ! getent passwd "$uid" >/dev/null; then
warninvalid "unknown user '$uid'"
return 1
fi
fi
# gid must be numeric or a valid group name
# don't try to resolve numeric IDs in case they don't exist
if [[ $gid != '-' ]]; then
if [[ $gid != +([0-9]) ]] && ! getent group "$gid" >/dev/null; then
warninvalid "unknown group '$gid'"
return 1
fi
fi
# age must be list of numerics separated by the following postfixes:
# s, sec, m, min, h, d, w
# also it can be prefixed by '~'
if [[ $age != '-' ]]; then
if [[ ! $age =~ ^~?([0-9]+(s|sec|m|min|h|d|w)?)+$ ]]; then
warninvalid "invalid age '$age'"
return 1
fi
fi
return 0
}
relabel() {
local -a paths=($1)
local mode=$2 uid=$3 gid=$4
for path in "${paths[@]}"; do
if [[ -e $path ]]; then
[[ $uid != '-' ]] && chown $CHOPTS "$uid" "$path"
[[ $gid != '-' ]] && chgrp $CHOPTS "$gid" "$path"
[[ $mode != '-' ]] && chmod $CHOPTS "$mode" "$path"
fi
done
return 0
}
parse_age() {
local seconds=0
local numbers=(${1//[^0-9]/ })
local units=(${1//[0-9]/ })
for (( i = 0; i < ${#numbers[@]}; i++ )); do
if [ "${units[i]}" == "m" ] || [ "${units[i]}" == "min" ]; then
(( seconds += numbers[i] * 60 ))
elif [ "${units[i]}" == "h" ]; then
(( seconds += numbers[i] * 3600 ))
elif [ "${units[i]}" == "d" ]; then
(( seconds += numbers[i] * 86400 ))
elif [ "${units[i]}" == "w" ]; then
(( seconds += numbers[i] * 604800 ))
else
(( seconds += numbers[i] ))
fi
done
echo $seconds
}
in_list() {
local search=$1
for item in "${EXCLUDE_LIST[@]}"; do
[[ "$search" == $item ]] && return 0
done
return 1
}
cleanup_dir() {
local path=$1 age=$2
local depth=1
# keep first level
if [[ ${age:0:1} == '~' ]]; then
depth=2
age=${age#'~'}
fi
local age=$(parse_age $age)
local current_time=$(date +%s)
while read -d '' file; do
# don't try to remove directories which still contains some files
[[ -d "$file" && $(ls -A "$file") ]] && continue
local mod_time=$(stat -c %Y "$file")
if (( (current_time - mod_time) > age )); then
! in_list "$file" && rm -fd "$file"
fi
done < <(find -P "$path" -mindepth $depth -depth -xdev -print0)
}
_f() {
# Create a file if it doesn't exist yet
local path=$1 mode=$2 uid=$3 gid=$4
if [[ ! -e $path ]]; then
install -m"$mode" -o"$uid" -g"$gid" /dev/null "$path"
fi
}
_F() {
# Create or truncate a file
local path=$1 mode=$2 uid=$3 gid=$4
install -m"$mode" -o"$uid" -g"$gid" /dev/null "$path"
}
_d() {
# Create a directory if it doesn't exist yet
local path=$1 mode=$2 uid=$3 gid=$4 age=$5
if (( CLEAN )); then
if [[ $age != '-' ]] && [[ -d "$path" ]]; then
cleanup_dir "$path" "$age"
fi
fi
if (( CREATE )); then
if [[ ! -d "$path" ]]; then
install -d -m"$mode" -o"$uid" -g"$gid" "$path"
fi
fi
}
_D() {
# Create or empty a directory
local path=$1 mode=$2 uid=$3 gid=$4
if [[ -d $path ]] && (( REMOVE )); then
find "$path" -mindepth 1 -maxdepth 1 -xdev -exec rm -rf {} +
fi
_d "$@"
}
_p() {
# Create a named pipe (FIFO) if it doesn't exist yet
local path=$1 mode=$2 uid=$3 gid=$4
if [[ ! -p "$path" ]]; then
mkfifo -m$mode "$path"
chown "$uid:$gid" "$path"
fi
}
_x() {
# Ignore a path during cleaning. Use this type to exclude paths from clean-up as
# controlled with the Age parameter. Note that lines of this type do not
# influence the effect of r or R lines. Lines of this type accept shell-style
# globs in place of of normal path names.
local path=$1
EXCLUDE_LIST+=("$path*(/*)")
}
_X() {
# Ignore a path during cleanup. Use this type to prevent path removal as controlled
# with the Age parameter. Note that if path is a directory, content of a directory is not
# excluded from clean-up, only directory itself. Lines of this type accept
# shell-style globs in place of normal path names.
local path=$1
EXCLUDE_LIST+=("$path")
}
_r() {
# Remove a file or directory if it exists. This may not be used to remove
# non-empty directories, use R for that. Lines of this type accept shell-style
# globs in place of normal path names.
local path
local -a paths=($1)
for path in "${paths[@]}"; do
if [[ -f $path ]]; then
rm -f "$path"
elif [[ -d $path ]]; then
rmdir "$path"
fi
done
}
_R() {
# Recursively remove a path and all its subdirectories (if it is a directory).
# Lines of this type accept shell-style globs in place of normal path names.
local path
local -a paths=($1)
for path in "${paths[@]}"; do
[[ -d $path ]] && rm -rf --one-file-system "$path"
done
}
_z() {
# Set ownership, access mode and relabel security context of a file or
# directory if it exists. Lines of this type accept shell-style globs in
# place of normal path names.
local -a paths=($1)
local mode=$2 uid=$3 gid=$4
relabel "$@"
}
_Z() {
# Recursively set ownership, access mode and relabel security context of a
# path and all its subdirectories (if it is a directory). Lines of this type
# accept shell-style globs in place of normal path names.
CHOPTS=-R relabel "$@"
}
_m() {
# If the specified file path exists, adjust its access mode, group and user to
# the specified values. If it does not exist, do nothing.
relabel "$@"
}
_w() {
# Write the argument parameter to a file, if the file exists. Lines of this
# type accept shell-style globs in place of normal path names. The argument
# parameter will be written without a trailing newline.
local path
local -a paths=($1)
local argument="$6"
for path in "${paths[@]}"; do
[[ -f $path ]] && echo -n "$argument" > "$path"
done
}
_L() {
# Create a symlink if it does not exist yet.
local path=$1 source=$6
if [[ ! -e "$path" && -e "$source" ]]; then
ln -s "$source" "$path"
fi
}
_L+() {
# Create a symlink if it does not exist yet. If a file already exists where the symlink is to be created, it will be removed and be replaced by the symlink.
local path=$1 source=$6
if [[ -e "$source" ]]; then
ln -sf "$source" "$path"
fi
}
process_lines ()
{
local actions="$1"
TOTALNUM=0
while read -a line; do
(( ++TOTALNUM ))
[[ "${line[0]:0:1}" != $actions ]] && continue
# fill empty parameters
[[ "${line[2]}" ]] || line[2]='-'
[[ "${line[3]}" ]] || line[3]='-'
[[ "${line[4]}" ]] || line[4]='-'
[[ "${line[5]}" ]] || line[5]='-'
# skip invalid entries
if ! checkparams "${line[@]}"; then
continue
fi
# fall back on defaults when parameters are passed as '-'
if [[ ${line[2]} = '-' ]]; then
case ${line[0]} in
p|f|F) line[2]=0644 ;;
d|D) line[2]=0755 ;;
esac
fi
if [[ "${line[0]}" = [pfFdD] ]]; then
[[ ${line[3]} = '-' ]] && line[3]='root'
[[ ${line[4]} = '-' ]] && line[4]='root'
fi
"_${line[@]}"
done < <(printf '%s\n' "${lines[@]}")
}
shopt -s nullglob
shopt -s extglob
declare -i CREATE=0 REMOVE=0 CLEAN=0 ONBOOT=0
declare -i error=0 LINENUM=0 TOTALNUM=0
declare FILE=
declare -A fragments
declare -a tmpfiles_d=(
/usr/lib/tmpfiles.d/*.conf
/etc/tmpfiles.d/*.conf
/run/tmpfiles.d/*.conf
)
declare -a EXCLUDE_LIST lines linenums files
while (( $# )); do
case $1 in
--create) CREATE=1 ;;
--remove) REMOVE=1 ;;
--clean) CLEAN=1 ;;
--boot) ONBOOT=1 ;;
*) break ;;
esac
shift
done
if (( !(CREATE + REMOVE + CLEAN) )); then
printf 'usage: %s [--create] [--remove] [--clean] [--boot] [FILES...]\n' "${0##*/}"
exit 1
fi
# directories declared later in the tmpfiles_d array will override earlier
# directories, on a per file basis.
# Example: `/etc/tmpfiles.d/foo.conf' supersedes `/usr/lib/tmpfiles.d/foo.conf'.
for path in "${tmpfiles_d[@]}"; do
[[ -f $path ]] && fragments[${path##*/}]=${path%/*}
done
# catch errors in functions so we can exit with something meaningful
set -E
trap '(( ++error ))' ERR
# loop through the gathered fragments, sorted globally by filename.
# `/run/tmpfiles/foo.conf' will always be read after `/etc/tmpfiles.d/bar.conf'
while read -d '' fragment; do
LINENUM=0
# make sure that a fragment contains only a base filename
if [[ "$fragment" = /* ]] && [[ -f "$fragment" ]]; then
fragments[${fragment##*/}]=${fragment%/*}
fragment=${fragment##*/}
fi
if [[ -z ${fragments[$fragment]} ]]; then
printf 'warning: %s does not found\n' "$fragment"
continue
fi
printf -v FILE '%s/%s' "${fragments[$fragment]}" "$fragment"
### FILE FORMAT ###
# 0 1 2 3 4 5
# Type Path Mode UID GID Age
# d /run/user 0755 root root 10d
# omit read's -r flag to honor escapes here, so that whitespace can be
# escaped for paths. We will _not_ honor quoted paths. Also make sure that
# last line will be processed even if it does not contain terminating '\n'.
while read -a line || [[ -n "${line[@]}" ]]; do
(( ++LINENUM ))
# skip over comments and empty lines
if (( ! ${#line[*]} )) || [[ ${line[0]:0:1} = '#' ]]; then
continue
fi
# process the lines with unsafe operation marker only if --boot option is
# specified
if [[ "${line[0]}" == *! ]]; then
(( ONBOOT )) || continue
line[0]=${line[0]%!}
fi
# whine about invalid entries
if ! type -t _${line[0]} >/dev/null; then
warninvalid "unknown action '${line[0]}'" "$FILE" "$LINENUM"
continue
fi
# path cannot be empty
if [[ -z "${line[1]}" ]]; then
warninvalid "missed path" "$FILE" "$LINENUM"
continue
fi
(( ++TOTALNUM ))
lines[$TOTALNUM]="${line[@]}"
linenums[$TOTALNUM]=$LINENUM
files[$TOTALNUM]="$FILE"
done <"$FILE"
done < <(printf '%s\0' "${@:-${!fragments[@]}}" | sort -z)
# Fill exclude list first
(( CLEAN )) && process_lines "[xX]"
process_lines "[dD]"
(( CREATE )) && process_lines "[fFpzZmwL]"
(( REMOVE )) && process_lines "[rR]"
exit $error
# vim: set ts=2 sw=2 noet: