From 6997a738bbca209cc6a8a29cdb71830bfb912f18 Mon Sep 17 00:00:00 2001 From: Dave Reisner Date: Wed, 27 Jul 2011 14:36:27 -0400 Subject: paccache: add new contrib script paccache is a robust and flexible package cache cleaner with a variety of options. Much credit goes to DJ Mills and Pat Brisbin for ideas behind this script. Signed-off-by: Dave Reisner [Dan: add .gitignore entry] Signed-off-by: Dan McGee --- contrib/.gitignore | 1 + contrib/Makefile.am | 4 + contrib/paccache.in | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100755 contrib/paccache.in diff --git a/contrib/.gitignore b/contrib/.gitignore index 020f7db0..1bd145fc 100644 --- a/contrib/.gitignore +++ b/contrib/.gitignore @@ -1,5 +1,6 @@ bacman bash_completion +paccache pacdiff paclist paclog-pkglist diff --git a/contrib/Makefile.am b/contrib/Makefile.am index 69304a44..10b03a2f 100644 --- a/contrib/Makefile.am +++ b/contrib/Makefile.am @@ -1,5 +1,6 @@ OURSCRIPTS = \ bacman \ + paccache \ pacdiff \ paclist \ paclog-pkglist \ @@ -14,6 +15,7 @@ EXTRA_DIST = \ PKGBUILD.vim \ bacman.in \ bash_completion.in \ + paccache.in \ paclog-pkglist.in \ pacdiff.in \ paclist.in \ @@ -29,6 +31,7 @@ MOSTLYCLEANFILES = $(OURSCRIPTS) $(OURFILES) *.tmp edit = sed \ -e 's|@sysconfdir[@]|$(sysconfdir)|g' \ -e 's|@localstatedir[@]|$(localstatedir)|g' \ + -e 's|@SIZECMD[@]|$(SIZECMD)|g' \ -e '1s|!/bin/bash|!$(BASH_SHELL)|g' $(OURSCRIPTS): Makefile @@ -50,6 +53,7 @@ all-am: $(OURSCRIPTS) $(OURFILES) bacman: $(srcdir)/bacman.in bash_completion: $(srcdir)/bash_completion.in +paccache: $(srcdir)/paccache.in pacdiff: $(srcdir)/pacdiff.in paclist: $(srcdir)/paclist.in paclog-pkglist: $(srcdir)/paclog-pkglist.in diff --git a/contrib/paccache.in b/contrib/paccache.in new file mode 100755 index 00000000..0bd0cf79 --- /dev/null +++ b/contrib/paccache.in @@ -0,0 +1,291 @@ +#!/bin/bash +# +# pacache - flexible pacman cache cleaning +# +# Copyright (C) 2011 Dave Reisner +# +# 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, see . + + +shopt -s extglob + +declare -a candidates=() cmdopts=() whitelist=() blacklist=() +declare -i delete=0 dryrun=0 filecount=0 keep=3 move=0 totalsaved=0 +declare cachedir=@localstatedir@/cache/pacman/pkg delim=$'\n' movedir= scanarch= + +msg() { + local mesg=$1; shift + printf "==> $mesg\n" "$@" +} >&2 + +error() { + local mesg=$1; shift + printf "==> ERROR: $mesg\n" "$@" +} >&2 + +die() { + error "$@" + exit 1 +} + +# reads a list of files on stdin and prints out deletion candidates +pkgfilter() { + # there's whitelist and blacklist parameters passed to this + # script after the block of awk. + + awk -v keep="$1" -v scanarch="$2" ' + function parse_filename(filename, parts, count, i, pkgname, arch) { + + count = split(filename, parts, "-") + + i = 1 + pkgname = parts[i++] + while (i <= count - 3) { + pkgname = pkgname "-" parts[i++] + } + + arch = substr(parts[count], 1, index(parts[count], ".") - 1) + + # filter on whitelist or blacklist + if (wlen && !whitelist[pkgname]) return + if (blen && blacklist[pkgname]) return + + if ("" == packages[pkgname,arch]) { + packages[pkgname,arch] = filename + } else { + packages[pkgname,arch] = packages[pkgname,arch] SUBSEP filename + } + } + + BEGIN { + # create whitelist + wlen = ARGV[1]; delete ARGV[1] + for (i = 2; i < 2 + wlen; i++) { + whitelist[ARGV[i]] = 1 + delete ARGV[i] + } + + # create blacklist + blen = ARGV[i]; delete ARGV[i] + while (i++ < ARGC) { + blacklist[ARGV[i]] = 1 + delete ARGV[i] + } + + # read package filenames + while (getline < "/dev/stdin") { + parse_filename($0) + } + } + + END { + for (pkglist in packages) { + # idx[1,2] = idx[pkgname,arch] + split(pkglist, idx, SUBSEP) + + # enforce architecture match if specified + if (!scanarch || scanarch == idx[2]) { + count = split(packages[idx[1], idx[2]], pkgs, SUBSEP) + if (count > keep) { + for(i = 1; i <= count - keep; i++) { + print pkgs[i] + } + } + } + } + }' "${@:3}" +} + +size_to_human() { + awk -v size="$1" ' + BEGIN { + suffix[1] = "KiB" + suffix[2] = "MiB" + suffix[3] = "GiB" + suffix[4] = "TiB" + count = 1 + + while (size > 1024) { + size /= 1024 + count++ + } + + sizestr = sprintf("%.2f", size) + sub(/.?0+$/, "", sizestr) + printf("%s %s", sizestr, suffix[count]) + }' +} + +runcmd() { + if (( needsroot )); then + msg "Privilege escalation required" + if sudo -v &>/dev/null && sudo -l &>/dev/null; then + sudo "$@" + else + printf '%s ' 'root' + su -c "$(printf '%q ' "$@")" + fi + else + "$@" + fi +} + +summarize() { + local -i filecount=$# + local seenarch= seen= arch= name= + local -r pkg_re='(.+)-[^-]+-[0-9]+-([^.]+)\.pkg.*' + + if (( delete )); then + printf -v output 'finished: %d packages removed' "$filecount" + elif (( move )); then + printf -v output "finished: %d packages moved to \`%s'" "$filecount" "$movedir" + elif (( dryrun )); then + if (( verbose )); then + msg "Candidate packages:" + while read -r pkg; do + if (( verbose >= 3 )); then + [[ $pkg =~ $pkg_re ]] && name=${BASH_REMATCH[1]} arch=${BASH_REMATCH[2]} + if [[ -z $seen || $seenarch != $arch || $seen != $name ]]; then + printf '%s (%s):\n' "$name" "$arch" + fi + printf ' %s\n' "$pkg" + elif (( verbose >= 2 )); then + printf "$PWD/%s$delim" "$pkg" + else + printf "%s$delim" "$pkg" + fi + done < <(printf '%s\n' "$@" | sort -V) + printf '\n' >&2 + fi + + printf -v output 'finished dry run: %d candidates' "$filecount" + fi + + msg "$output (diskspace saved: %s)" "$(size_to_human "$totalsaved")" +} + +usage() { + cat < [options] [targets...] + +${0##*/} is a flexible pacman cache cleaning utility, which has numerous +options to help control how much, and what, is deleted from any directory +containing pacman package tarballs. + + Operations: + -d perform a dry run, only finding candidate packages. + -m move candidate packages to 'movedir'. + -r remove candidate packages. + + Options: + -a scan for 'arch' (default: all architectures). + -c scan 'cachedir' for packages (default: @localstatedir@/cache/pacman/pkg). + -f apply force to mv(1) and rm(1) operations. + -h display this help message. + -i ignore 'pkgs', which is a comma separated. Alternatively, + specify '-' to read package names from stdin, newline delimited. + -k keep 'num' of each package in 'cachedir' (default: 3). + -u target uninstalled packages. + -v increase verbosity. specify up to 3 times. + -z use null delimiters for candidate names (only with -v and -vv) + +EOF +} + +if (( ! UID )); then + error "Bad dog, no biscuit. You will be prompted for privilege escalation." + exit 42 +fi + +while getopts ':a:c:dfhi:k:m:rsuvz' opt; do + case $opt in + a) scanarch=$OPTARG ;; + c) cachedir=$OPTARG ;; + d) dryrun=1 ;; + f) cmdopts=(-f) ;; + h) usage + exit 0 ;; + i) if [[ $OPTARG = '-' ]]; then + [[ ! -t 0 ]] && IFS=$'\n' read -r -d '' -a ign + else + IFS=',' read -r -a ign <<< "$OPTARG" + fi + blacklist+=("${ign[@]}") + unset i ign ;; + k) keep=$OPTARG + if [[ $keep != $OPTARG ]] || (( keep < 0 )); then + die 'argument to option -k must be a non-negative integer' + fi ;; + m) move=1 movedir=$OPTARG ;; + r) delete=1 ;; + u) IFS=$'\n' read -r -d '' -a ign < <(pacman -Qq) + blacklist+=("${ign[@]}") + unset ign ;; + v) (( ++verbose )) ;; + z) delim='\0' ;; + :) die "option '--%s' requires an argument" "$OPTARG" ;; + ?) die "invalid option -- '%s'" "$OPTARG" ;; + esac +done +shift $(( OPTIND - 1 )) + +# remaining args are a whitelist +whitelist=("$@") + +# sanity checks +case $(( dryrun+delete+move )) in + 0) die "no operation specified (use -h for help)" ;; + [^1]) die "only one operation may be used at a time" ;; +esac + +[[ -d $cachedir ]] || + die "cachedir \`%s' does not exist or is not a directory" "$cachedir" + +[[ $movedir && ! -d $movedir ]] && + die "move-to directory \`%s' does not exist or is not a directory" "$movedir" + +if (( move || delete )); then + # make it an absolute path since we're about to chdir + [[ ${movedir:0:1} != '/' ]] && movedir=$PWD/$movedir + [[ ! -w $cachedir || ( $movedir && ! -w $movedir ) ]] && needsroot=1 +fi + +# unlikely that this will fail, but better make sure +cd "$cachedir" || die "failed to chdir to \`%s'" "$cachedir" + +# note that these results are returned in an arbitrary order from awk, but +# they'll be resorted (in summarize) iff we have a verbosity level set. +IFS=$'\n' read -r -d '' -a candidates < \ + <(printf '%s\n' *.pkg.tar?(.+([^.])) | sort -V | + pkgfilter "$keep" "$scanarch" \ + "${#whitelist[*]}" "${whitelist[@]}" \ + "${#blacklist[*]}" "${blacklist[@]}") + +if (( ! ${#candidates[*]} )); then + msg 'no candidate packages found for pruning' + exit 1 +fi + +# do this before we destroy anything +totalsaved=$(@SIZECMD@ "${candidates[@]}" | awk '{ sum = $1 } END { print sum }') + +# crush. kill. destroy. +(( verbose )) && cmdopts+=(-v) +if (( delete )); then + runcmd rm "${cmdopts[@]}" "${candidates[@]}" +elif (( move )); then + runcmd mv "${cmdopts[@]}" "${candidates[@]}" "$movedir" +fi + +summarize "${candidates[@]}" -- cgit v1.2.3-24-g4f1b