#!/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: