#!/bin/sh # Auto-Record-Cleaner (aurecl) - automatically remove old recorded events # Copyright (C) 2014 LtCmdrLuke # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA # This is a test-script for automatically handling the oldest records in a # specified directory after the directory has reached a certain size. Files # which will be deleted next are first moved to a "last-chance" directory # before they are finally deleted. In each run, only as much files will be # really deleted as needed to limit the maximum allowed size of the directory. # The main intention of this script is its usage in conjunction with the # "channel-crawling"-feature of the Auto-Timer on a neutrino-based set top box. # Basic idea: # Record every event of certain channels and delete always the # oldest recordings in the record directory, but give the user a chance # to see what is about to be deleted next. By running this script regularly # you can keep always the latest x days of all events from a certain channel # available for watching anytime, i.e. fully independent of broadcasting # times. (It basically resembles 'Sky Anytime') # Install & usage: # - Copy the script somewhere on your box and make it executable # - Edit the following variables to correspond to your environment, e.g. # which directory should be watched and how much space you want to give it # - call it regularly, either manually, or in some automated way. When calling # it, provide the command line option "--yes" to confirm, you really want # files to be deleted. Without this command line option, it will perform a # dry run, i.e. just tell you what it would do now. # Related files: # General Configuration: /var/tuxbox/config/auto-record-cleaner.conf # Controlled directories: /var/tuxbox/config/auto-record-cleaner.rules VERSION=0.2 # Changelog: # # 0.2 # - Added new command line options: # -y|--yes twice to deactivate the 5s safety # -q|--quiet deactivate all console output (and also deactivate the 5s safety) # -h|--help shows the command line options # - Logs now to a logfile (besides the console, if not deactivated with -q) # - Now uses own configuration file under /var/tuxbox/config/auto-record-cleaner.conf # * Here the default path for the log-file and the rule-file can be changed # * Provided a template file (auto-record-cleaner.conf.template); default values should be fine. # - Now uses own rules file under /var/tuxbox/config/auto-record-cleaner.rules # * An arbitrary number of different directories can now be cleaned # * Provided a template (auto-record-cleaner.rules.template) with examples # * Basic Syntax is CLEANING_PATH,MAX_SIZE_CP;LAST_CHANCE_PATH,MAX_SIZE_LCP # # 0.1 # - Initial release # - Restrictions: # * only one controlled directory # * Configuration directly in the script # * Shell-only, no information in neutrino # * No log-file, logs to stdout # Beyond this point, no user-configurable settings ############################################################################### NAME="Auto-Record-Cleaner" ME=${0##*/} CONFIG_FILE=/var/tuxbox/config/$ME.conf PID_FILE=/var/run/$ME.pid EXIT_NORMAL=0 EXIT_SIGNAL=1 EXIT_NO_RULE_FILE=2 EXIT_ALREADY_RUNNING=3 EXIT_UNKNOWN_COMMAND_OPTION=4 ####################################################################################### #BEGIN SECTION "Helper functions" signal_handler() { #Handle INT, TERM signals and clean up. log "Caught signal. Cleaning up." cleanup set +f log "done." log "$ME V${VERSION} exiting now." exit $EXIT_SIGNAL } cleanup() { # Remove the pid-file rm -rf $PID_FILE 2>/dev/null } log() { #Log message to log file #$*: Log message if [ "$LOG_FILE" != "" ]; then echo -e $(date +'%F %H:%M:%S') [$$]: "$*" >> $LOG_FILE fi if [ $quiet == 0 ]; then echo -e "$*" fi } begin_ifs_block() { #Backup IFS (input field separator) to restore it after parsing arguments IFS_SAVE=$IFS set -f } end_ifs_block() { #Restore (input field separator) IFS after parsing arguments IFS=$IFS_SAVE set +f } #END SECTION "Helper functions" ####################################################################################### ####################################################################################### #BEGIN SECTION "Initialization" init_config() { #Parse config file (default: /var/tuxbox/config/auto-record-cleaner.conf) if [ -e $CONFIG_FILE ]; then source $CONFIG_FILE 2>/dev/null fi # Initialize the logfile first, so we can write to it... if [ ! -d "${LOG_FILE%/*}" ]; then case $LOG_FILE in [oO][fF][fF]) LOG_FILE='' ;; *) LOG_FILE=/tmp/${ME}_$(date +'%F').log ;; esac fi echo -e "\n\n========================== $NAME started new log at $(date) ======================================" >> $LOG_FILE # Check other settings and reset them to default if unset or invalid if [ ! -e "$RULE_FILE" ]; then RULE_FILE=/var/tuxbox/config/$ME.rules if [ ! -e "$RULE_FILE" ]; then log "ERROR: Rules file '$RULE_FILE' does not exist! Exiting." exit $EXIT_NO_RULE_FILE fi fi } #END SECTION "Initialization" ####################################################################################### ####################################################################################### #BEGIN SECTION "Command line options handling" parse_options() { #Parse auto-record-cleaner command line arguments local option while [ $# -gt 0 ] do option=$1 shift case "$option" in -y|--yes) dry_run=$((dry_run-1)) ;; -q|--quiet) quiet=1 ;; -h|--help) usage exit $EXIT_NORMAL ;; *) echo "Unknown command line option '$option'. What did you want to do? Exiting!" usage exit $EXIT_UNKNOWN_COMMAND_OPTION ;; esac done } usage() { # Print short usage message on console echo -e "Usage: $ME [options]" echo -e "Valid options are:" echo -e "\t-y|--yes\t\tSwitches safety off and really deletes/moves files. Use twice to deactivate 5s safety interval." echo -e "\t-q|--quiet\t\tDeactivates all output on console. Also deactivates 5s safety interval." echo -e "\t-h|--help\t\tPrint this help and exit." } #END SECTION "Command line options handling" ####################################################################################### ####################################################################################### #BEGIN SECTION "Work functions" limit_directory() { # Moves or deletes oldest files in a given directory # Parameters: # $1: The directory from which the files should be moved/deleted # $2: the maximum size of the directory in kB # $3: optional: the directory into which files should be moved. If this # parameter is missing, files will be deleted # Precondition: All provided directories must exist. local dir max_dir_size dir_size dest_dir over_limit ts_files ts dir="$1" max_dir_size="$2" dir_size=$(du -s -k "$dir" | cut -f1) dest_dir="$3" # in case there is a destination folder given, we need to get its size # and subtract this from the current dir size if [ -n "$dest_dir" ]; then dest_dir_size=$(du -s -k "$dest_dir" | cut -f1) dir_size=$((dir_size-dest_dir_size)) log "\t\t\tLimiting '$dir' to max size $((max_dir_size/(1024*1024)))GB by moving the oldest files to '$dest_dir' ... " else log "\t\t\tLimiting '$dir' to max size $((max_dir_size/(1024*1024)))GB by deleting the oldest files ..." fi if [ $dir_size -gt $max_dir_size ]; then over_limit=$((dir_size-max_dir_size)) if [ -z "$dest_dir" ]; then log "\t\t\t\tWe need to delete $((over_limit/(1024*1024)))GB (${over_limit}kB) from '$dir' ..." else log "\t\t\t\tWe need to move $((over_limit/(1024*1024)))GB (${over_limit}kB) from '$dir' to '$dest_dir' ..." fi else log "\t\t\t\tNothing to do in directory '$dir'. Current size has not reached the limit." return 0 fi # we don't want to use the ls command since its output is not POSIX-standard # therefore, we first find all files and then we use the date command to # determine the modification time # first collect all *.ts files ts_files=$(find "$dir" -name "*.ts") # now gather additional information, like modification time and size # (we could have got this info from ls in ONE call..) begin_ifs_block IFS=$'\n' for ts in $ts_files; do ts_date=$(date -r "$ts" +%s) ts_size=$(du -k "$ts" | cut -f1) ts_files_ext="${ts_files_ext}${ts}|$ts_date|$ts_size\n" #echo "$ts|$ts_date|$ts_size" done end_ifs_block # sort the final list with respect to the modification time sorted_ts_files_ext=$(echo -ne "$ts_files_ext" | sort -t '|' -k2) count=0 # now (re)move until limit is reached begin_ifs_block IFS=$'\n' for ts_file in $sorted_ts_files_ext; do IFS='|' set -- $ts_file filename=$1 filetime=$2 filesize=$3 # skip the file, if it is already in dest_dir if [ "${filename%/*}" == "$dest_dir" ]; then #echo -e "\tSkipping $filename .." continue fi xml_file=${filename%".ts"}".xml" if [ $dry_run == 0 ]; then if [ -z "$dest_dir" ]; then log "\t\t\t\tDeleting now '$filename' (${filesize}kB).." rm "$filename" 2>/dev/null if [ -f "$xml_file" ];then log "\t\t\t\tDeleting now also the corresponding '$xml_file' ... " rm "$xml_file" 2>/dev/null fi else log "\t\t\t\tMoving '$filename' (${filesize}kB) to '$dest_dir' ..." mv "$filename" "$dest_dir" 2>/dev/null if [ -f "$xml_file" ];then log "\t\t\t\tMoving now also the corresponding '$xml_file' to '$dest_dir' ... " mv "$xml_file" "$dest_dir" 2>/dev/null fi fi else if [ -z "$dest_dir" ]; then log "\t\t\t\tDRY-RUN: Would remove now '$filename' (${filesize}kB).." if [ -f "$xml_file" ];then log "\t\t\t\tDRY-RUN: Would delete now also the corresponding '$xml_file' ... " fi else log "\t\t\t\tDRY-RUN: Would move now '$filename' (${filesize}kB) to '$dest_dir' ..." if [ -f "$xml_file" ];then log "\t\t\t\tDRY-RUN: Would move now also the corresponding '$xml_file' to '$dest_dir' ... " fi fi fi over_limit=$((over_limit-filesize)) count=$((count+1)) if [ $over_limit -le 0 ]; then removed=$((dir_size-max_dir_size-over_limit)) if [ $dry_run == 0 ]; then if [ -z "$dest_dir" ]; then log "\t\t\t\tDone. Removed $((removed/(1024*1024)))GB (${removed}kB) by deleting $count recorded events." else log "\t\t\t\tDone. Moved $((removed/(1024*1024)))GB (${removed}kB) in $count recorded events to directory '$dest_dir'." fi else if [ -z "$dest_dir" ]; then log "\t\t\t\tDRY_RUN: Done. Would have removed $((removed/(1024*1024)))GB (${removed}kB) by deleting $count recorded events." else log "\t\t\t\tDRY_RUN: Done. Would have moved $((removed/(1024*1024)))GB (${removed}kB) in $count recorded events to directory '$dest_dir'." fi fi # we are done, so break from the loop break fi done end_ifs_block return 0 } #END SECTION "Work functions" ####################################################################################### ############################################################################### #BEGIN Section "Main" # initialize some more variables dry_run=1 quiet=0 # set the signal handler trap signal_handler INT TERM # First initialize the values from the config, otherwise we cannot log anywhere init_config # Now get command line option, as these might override some values from the config or default variables parse_options $@ if [ -e $PID_FILE ]; then log "$ME ist already running. Exiting..." exit $EXIT_ALREADY_RUNNING else echo $$ > $PID_FILE fi # We happily started.. log "" log "$ME V$VERSION initialized and starting main operations." if [ $dry_run == 1 ]; then log "\tThis is a dry run, i.e. no files will be harmed. Use '-y' or '--yes' to deactivate the safety." elif [ $dry_run == 0 ] && [ $quiet == 0 ]; then log "\t!!! WARNING!!! This is now the real thing - files WILL BE DELETED. You can still abort within the next 5 seconds. !!! WARNING !!!" echo "Waiting for 5 more seconds.." sleep 1 echo "Waiting for 4 more seconds.." sleep 1 echo "Waiting for 3 more seconds.." sleep 1 echo "Waiting for 2 more seconds.." sleep 1 echo "Waiting for 1 more seconds.." sleep 1 echo "You have been warned. Proceeding..." else log "\t!!! WARNING!!! $ME is armed and targeting your files. !!! WARNING !!!" fi # now process each directory given in the rule-file rule_line=0 cat $RULE_FILE | while read line; do rule_line=$((${rule_line}+1)) if echo $line | egrep -q '^[[:space:]]*([^#;]+),([0-9]+);?([^;]+)?(,[0-9]+)?$'; then log "" log "\tProcessing rule: '$line'" # split rule line begin_ifs_block IFS=';' set -- $line record_part=$1 last_chance_part=$2 end_ifs_block # split record_part begin_ifs_block IFS=',' set -- $record_part record_dir=$1 record_dir_size=$2 end_ifs_block if [ -n "$last_chance_part" ]; then # split last_chance_part begin_ifs_block IFS=',' set -- $last_chance_part last_chance_dir=$1 last_chance_dir_size=$2 end_ifs_block if [ -z $last_chance_dir_size ]; then last_chance_dir_size=$((record_dir_size / 10)) fi else last_chance_dir="last_chance" last_chance_dir_size=$((record_dir_size / 10)) fi # convert GB into kB record_dir_size_k=$((record_dir_size * 1024 * 1024)) last_chance_dir_size_k=$((last_chance_dir_size * 1024 * 1024)) # print the collected information to the log: log "\t\tCleaning path: '$record_dir', Maximum size: ${record_dir_size}GB (${record_dir_size_k}kB)" log "\t\tLast chance subdirectory: '$last_chance_dir', Maximum size: ${last_chance_dir_size}GB (${last_chance_dir_size_k}kB)" # now check if directories exist # if the cleaning directory does not exist, print an error and continue with next rule if [ ! -d "$record_dir" ]; then log "\t\tThe given directory '$record_dir' does not exist. Create it or correct this rule. Skipping this rule." continue fi #convert the last_chance relative path to an absolut one last_chance_dir="${record_dir%/}/$last_chance_dir" # if the last chance directory does not exist yet, create it. if [ ! -d "$last_chance_dir" ]; then if [ $dry_run == 0 ]; then log "\t\tCreating directory '$last_chance_dir' for last chance files, as it does not exist yet." mkdir "$last_chance_dir" else log "\t\tWould now create directory '$last_chance_dir' for last chance files, as it does not exist yet. (dry-run, i.e. NOT performing any action)" fi fi # get the current size of the directories (in kB)... current_record_dir_usage_k=$(du -s -k "$record_dir" | cut -f1) current_last_chance_dir_size_k=$(du -s -k "$last_chance_dir" | cut -f1) # ... and print them into the log log "\t\tCurrent full size of '$record_dir' (recursively) is $((current_record_dir_usage_k/(1024*1024)))GB (${current_record_dir_usage_k}kB)." log "\t\tCurrent size of '$last_chance_dir' is $((current_last_chance_dir_size_k/(1024*1024)))GB (${current_last_chance_dir_size_k}kB)." # perform some initial checks, if we actually need to do something if [ $((current_record_dir_usage_k-current_last_chance_dir_size_k)) -le $((record_dir_size_k-last_chance_dir_size_k)) ] && [ $current_last_chance_dir_size_k -le $last_chance_dir_size_k ] ;then log "\t\tNothing to do for this rule - disk usage is within the given specification." continue fi over_limit=0 if [ $current_record_dir_usage_k -gt $record_dir_size_k ];then over_limit=$((current_record_dir_usage_k-record_dir_size_k)) log "\t\tWe need to remove $((over_limit/(1024*1024)))GB (${over_limit}kB) from directory '$record_dir'." fi if [ $((current_record_dir_usage_k-current_last_chance_dir_size_k-over_limit)) -gt $((record_dir_size_k-last_chance_dir_size_k)) ];then move_size=$((current_record_dir_usage_k-current_last_chance_dir_size_k-over_limit-(record_dir_size_k-last_chance_dir_size_k))) log "\t\tWe need to move $((move_size/(1024*1024)))GB (${move_size}kB) from directory '$record_dir' to '$last_chance_dir'." fi # first we delete the oldest files from the main directory (including last chance) # if your last_chance is too small, some files will never appear in there limit_directory "$record_dir" "$record_dir_size_k" # now fill the last chance directory limit_directory "$record_dir" "$((record_dir_size_k-last_chance_dir_size_k))" "$last_chance_dir" # final status new_record_dir_usage_k=$(du -s -k "$record_dir" | cut -f1) new_last_chance_dir_size_k=$(du -s -k "$last_chance_dir" | cut -f1) log "\t\tNew full size of '$record_dir' (recursively) is $((new_record_dir_usage_k/(1024*1024)))GB (${new_record_dir_usage_k}kB)." log "\t\tNew size of '$last_chance_dir' is $((new_last_chance_dir_size_k/(1024*1024)))GB (${new_last_chance_dir_size_k}kB)." fi done log "" log "$ME V${VERSION} finished rule processing." cleanup log "========================== $NAME finished at $(date) ======================================" exit $EXIT_NORMAL;