Repository: lahwaacz/Scripts Branch: master Commit: afb4233b4c9f Files: 60 Total size: 158.0 KB Directory structure: gitextract_szwjc667/ ├── .gitmodules ├── Colours-EyeCandy/ │ ├── colourbars │ ├── colours │ ├── colourtheme │ ├── hypnotoad.pl │ ├── pacman.sh │ ├── spacey.sh │ └── tanks.sh ├── README.md ├── aur-check ├── aur-release ├── aur-remotebuild ├── backup-system.sh ├── batmanpager ├── bsnap.sh ├── btrfs-diff ├── btrfs-sync ├── btrfs-sync-WIP ├── clean-aur-dir.py ├── convertToUtf8.py ├── fatcp ├── ffparser.py ├── fmount.py ├── forcemp3convert.sh ├── hddtemp.sh ├── imap-notifier.py ├── img2pdf.sh ├── initscreen.sh ├── maildir-strip-attachments.py ├── makeissue.sh ├── mp3convert.py ├── nat-launch-subnet.sh ├── nat-launch.sh ├── notify-brightness.sh ├── notify-volume.sh ├── pacman-disowned.sh ├── pdf-extract.sh ├── perm.sh ├── pythonscripts/ │ ├── __init__.py │ ├── cpu.py │ ├── daemon.py │ ├── ffparser.py │ ├── logger.py │ ├── misc.py │ ├── tempfiles.py │ └── terminal.py ├── qemu-launcher.sh ├── qemu-mac-hasher.py ├── qemu-tap-helper.sh ├── remove-dead-symlinks.sh ├── replaygain.py ├── rexe ├── rmshit.py ├── run-pvserver ├── sway-sensible-terminal ├── teams-attendance-parser.py ├── toggle-touchpad.sh ├── touch-tree.py ├── waybar-khal.py └── x ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitmodules ================================================ [submodule "submodules/cp-p"] path = submodules/cp-p url = ../cp-p.git ================================================ FILE: Colours-EyeCandy/colourbars ================================================ #!/bin/sh # by Him on the Arch boards # ANSI Color -- use these variables to easily have different color # and format output. Make sure to output the reset sequence after # colors (f = foreground, b = background), and use the 'off' # feature for anything you turn on. initializeANSI() { esc="" blackf="${esc}[30m"; redf="${esc}[31m"; greenf="${esc}[32m" yellowf="${esc}[33m" bluef="${esc}[34m"; purplef="${esc}[35m" cyanf="${esc}[36m"; whitef="${esc}[37m" blackb="${esc}[40m"; redb="${esc}[41m"; greenb="${esc}[42m" yellowb="${esc}[43m" blueb="${esc}[44m"; purpleb="${esc}[45m" cyanb="${esc}[46m"; whiteb="${esc}[47m" boldon="${esc}[1m"; boldoff="${esc}[22m" italicson="${esc}[3m"; italicsoff="${esc}[23m" ulon="${esc}[4m"; uloff="${esc}[24m" invon="${esc}[7m"; invoff="${esc}[27m" reset="${esc}[0m" } # note in this first use that switching colors doesn't require a reset # first - the new color overrides the old one. initializeANSI cat << EOF ${redf}▆▆▆▆▆▆▆▆▆▆${reset} ${greenf}▆▆▆▆▆▆▆▆▆▆${reset} ${yellowf}▆▆▆▆▆▆▆▆▆▆${reset} ${bluef}▆▆▆▆▆▆▆▆▆▆${reset} ${purplef}▆▆▆▆▆▆▆▆▆▆${reset} ${cyanf}▆▆▆▆▆▆▆▆▆▆${reset} ${whitef}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${blackf} ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::${reset} ${boldon}${redf}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${greenf}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${yellowf}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${bluef}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${purplef}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${cyanf}▆▆▆▆▆▆▆▆▆▆${reset} ${boldon}${whitef}▆▆▆▆▆▆▆▆▆▆${reset} EOF ================================================ FILE: Colours-EyeCandy/colours ================================================ #!/bin/bash # Original: http://frexx.de/xterm-256-notes/ # http://frexx.de/xterm-256-notes/data/colortable16.sh # Modified by Aaron Griffin # and further by Kazuo Teramoto FGNAMES=(' black ' ' red ' ' green ' ' yellow' ' blue ' 'magenta' ' cyan ' ' white ') BGNAMES=('DFT' 'BLK' 'RED' 'GRN' 'YEL' 'BLU' 'MAG' 'CYN' 'WHT') echo " ┌──────────────────────────────────────────────────────────────────────────┐" for b in $(seq 0 8); do if [ "$b" -gt 0 ]; then bg=$(($b+39)) fi echo -en "\033[0m ${BGNAMES[$b]} │ " for f in $(seq 0 7); do echo -en "\033[${bg}m\033[$(($f+30))m ${FGNAMES[$f]} " done echo -en "\033[0m │" echo -en "\033[0m\n\033[0m │ " for f in $(seq 0 7); do echo -en "\033[${bg}m\033[1;$(($f+30))m ${FGNAMES[$f]} " done echo -en "\033[0m │" echo -e "\033[0m" if [ "$b" -lt 8 ]; then echo " ├──────────────────────────────────────────────────────────────────────────┤" fi done echo " └──────────────────────────────────────────────────────────────────────────┘" ================================================ FILE: Colours-EyeCandy/colourtheme ================================================ #!/bin/bash # # This file echoes a bunch of color codes to the # terminal to demonstrate what's available. Each # line is the color code of one forground color, # out of 17 (default + 16 escapes), followed by a # test use of that color on all nine background # colors (default + 8 escapes). # T='▆ ▆' # The test text echo -e "\n 40m 41m 42m 43m\ 44m 45m 46m 47m"; for FGs in ' m' ' 1m' ' 30m' '1;30m' ' 31m' '1;31m' ' 32m' \ '1;32m' ' 33m' '1;33m' ' 34m' '1;34m' ' 35m' '1;35m' \ ' 36m' '1;36m' ' 37m' '1;37m'; do FG=${FGs// /} echo -en " $FGs \033[$FG $T " for BG in 40m 41m 42m 43m 44m 45m 46m 47m; do echo -en "$EINS \033[$FG\033[$BG $T \033[0m"; done echo; done echo ================================================ FILE: Colours-EyeCandy/hypnotoad.pl ================================================ #!/usr/bin/perl # script by karabaja4 # mail: karabaja4@archlinux.us my $blackFG_yellowBG = "\e[30;43m"; my $blackFG_redBG = "\e[30;41m"; my $blackFG_purpleBG = "\e[30;45m"; my $yellowFG_blackBG = "\e[1;33;40m"; my $yellowFG_redBG = "\e[1;33;41m"; my $redFG_yellowBG = "\e[31;43m"; my $purpleFG_yellowBG = "\e[35;43m"; my $purpleFG_blueBG = "\e[1;35;44m"; my $end = "\e[0m"; system("clear"); print " ${blackFG_yellowBG},'${blackFG_redBG}`${blackFG_yellowBG}`.._${end} ${blackFG_yellowBG},'${blackFG_redBG}`${end}${blackFG_yellowBG}`.${end} ${blackFG_yellowBG}:${blackFG_redBG},${yellowFG_blackBG}--.${end}${blackFG_redBG}_${blackFG_yellowBG}:)\\,:${blackFG_redBG},${yellowFG_blackBG}._,${end}${yellowFG_redBG}.${end}${blackFG_yellowBG}:${end} ${blackFG_yellowBG}:`-${yellowFG_blackBG}-${end}${blackFG_yellowBG},${blackFG_yellowBG}''${end}${redFG_yellowBG}@@\@${end}${blackFG_yellowBG}:`.${yellowFG_redBG}.${end}${blackFG_yellowBG}.';\\${end} All Glory to ${blackFG_yellowBG}`,'${end}${redFG_yellowBG}@@@@@@\@${end}${blackFG_yellowBG}`---'${redFG_yellowBG}@\@${end}${blackFG_yellowBG}`.${end} the HYPNOTOAD! ${blackFG_yellowBG}/${redFG_yellowBG}@@@@@@@@@@@@@@@@\@${end}${blackFG_yellowBG}:${end} ${blackFG_yellowBG}/${redFG_yellowBG}@@@@@@@@@@@@@@@@@@\@${end}${blackFG_yellowBG}\\${end} ${blackFG_yellowBG},'${redFG_yellowBG}@@@@@@@@@@@@@@@@@@@@\@${end}${purpleFG_yellowBG}:\\${end}${blackFG_yellowBG}.___,-.${end} ${blackFG_yellowBG}`...,---'``````-..._${redFG_yellowBG}@@@\@${end}${blackFG_purpleBG}|:${end}${redFG_yellowBG}@@@@@@\@${end}${blackFG_yellowBG}\\${end} ${blackFG_yellowBG}( )${end}${redFG_yellowBG}@@\@${end}${blackFG_purpleBG};:${end}${redFG_yellowBG}@@@\@)@@\@${end}${blackFG_yellowBG}\\${end} ${blackFG_yellowBG}_,-.${end} ${blackFG_yellowBG}`. (${end}${redFG_yellowBG}@@\@${end}${blackFG_purpleBG}//${end}${redFG_yellowBG}@@@@@@@@@\@${end}${blackFG_yellowBG}`'${end}${redFG_yellowBG}@@@\@${end}${blackFG_yellowBG}\\${end} ${blackFG_yellowBG}: `.${end}${blackFG_purpleBG}//${end}${redFG_yellowBG}@@)@@@@@@)@@@@@,\@${end}${blackFG_yellowBG};${end} ${blackFG_purpleBG}|`${purpleFG_yellowBG}.${blackFG_yellowBG} ${end}${purpleFG_yellowBG}_${end}${purpleFG_yellowBG},${blackFG_purpleBG}'/${end}${redFG_yellowBG}@@@@@@@)@@@@)@,'\@${end}${blackFG_yellowBG},'${end} ${blackFG_yellowBG}:${end}${blackFG_purpleBG}`.`${end}${purpleFG_yellowBG}-..____..=${end}${blackFG_purpleBG}:.-${end}${blackFG_yellowBG}':${end}${redFG_yellowBG}@@@@@.@@@@\@_,@@,'${end} ${redFG_yellowBG},'${end}${blackFG_yellowBG}\\ ${end}${blackFG_purpleBG}``--....${end}${purpleFG_blueBG}-)='${end}${blackFG_yellowBG} `.${end}${redFG_yellowBG}_,@\@${end}${blackFG_yellowBG}\\${end} ${redFG_yellowBG})@@\@'``._${end} ${redFG_yellowBG}/\@${end}${redFG_yellowBG}_${end}${redFG_yellowBG}\@${end}${blackFG_yellowBG}`.${end}${blackFG_yellowBG} ${end}${blackFG_redBG}(@)${end}${blackFG_yellowBG} /${end}${redFG_yellowBG}@@@@\@${end}${blackFG_yellowBG})${end} ${redFG_yellowBG}; / \\ \\`-.'${end} ${redFG_yellowBG}(@@\@${end}${redFG_yellowBG}`-:${end}${blackFG_yellowBG}`. ${end}${blackFG_yellowBG}`' ___..'${end}${redFG_yellowBG}@\@${end}${blackFG_yellowBG}_,-'${end} ${redFG_yellowBG}|/${end} ${redFG_yellowBG}`.)${end} ${redFG_yellowBG}`-. `.`.${end}${blackFG_yellowBG}``-----``--${end}${redFG_yellowBG},@\@.'${end} ${redFG_yellowBG}|/`.\\`'${end} ${redFG_yellowBG},',');${end} ${redFG_yellowBG}`${end} ${redFG_yellowBG}(/${end} ${redFG_yellowBG}(/${end} "; ================================================ FILE: Colours-EyeCandy/pacman.sh ================================================ #!/bin/sh # ANSI Color -- use these variables to easily have different color # and format output. Make sure to output the reset sequence after # colors (f = foreground, b = background), and use the 'off' # feature for anything you turn on. initializeANSI() { esc="" blackf="${esc}[30m"; redf="${esc}[31m"; greenf="${esc}[32m" yellowf="${esc}[33m" bluef="${esc}[34m"; purplef="${esc}[35m" cyanf="${esc}[36m"; whitef="${esc}[37m" blackb="${esc}[40m"; redb="${esc}[41m"; greenb="${esc}[42m" yellowb="${esc}[43m" blueb="${esc}[44m"; purpleb="${esc}[45m" cyanb="${esc}[46m"; whiteb="${esc}[47m" boldon="${esc}[1m"; boldoff="${esc}[22m" italicson="${esc}[3m"; italicsoff="${esc}[23m" ulon="${esc}[4m"; uloff="${esc}[24m" invon="${esc}[7m"; invoff="${esc}[27m" reset="${esc}[0m" } # note in this first use that switching colors doesn't require a reset # first - the new color overrides the old one. clear initializeANSI cat << EOF ${yellowf} ▄███████▄${reset} ${redf} ▄██████▄${reset} ${greenf} ▄██████▄${reset} ${bluef} ▄██████▄${reset} ${purplef} ▄██████▄${reset} ${cyanf} ▄██████▄${reset} ${yellowf}▄█████████▀▀${reset} ${redf}▄${whitef}█▀█${redf}██${whitef}█▀█${redf}██▄${reset} ${greenf}▄${whitef}█▀█${greenf}██${whitef}█▀█${greenf}██▄${reset} ${bluef}▄${whitef}█▀█${bluef}██${whitef}█▀█${bluef}██▄${reset} ${purplef}▄${whitef}█▀█${purplef}██${whitef}█▀█${purplef}██▄${reset} ${cyanf}▄${whitef}█▀█${cyanf}██${whitef}█▀█${cyanf}██▄${reset} ${yellowf}███████▀${reset} ${redf}█${whitef}▄▄█${redf}██${whitef}▄▄█${redf}███${reset} ${greenf}█${whitef}▄▄█${greenf}██${whitef}▄▄█${greenf}███${reset} ${bluef}█${whitef}▄▄█${bluef}██${whitef}▄▄█${bluef}███${reset} ${purplef}█${whitef}▄▄█${purplef}██${whitef}▄▄█${purplef}███${reset} ${cyanf}█${whitef}▄▄█${cyanf}██${whitef}▄▄█${cyanf}███${reset} ${yellowf}███████▄${reset} ${redf}████████████${reset} ${greenf}████████████${reset} ${bluef}████████████${reset} ${purplef}████████████${reset} ${cyanf}████████████${reset} ${yellowf}▀█████████▄▄${reset} ${redf}██▀██▀▀██▀██${reset} ${greenf}██▀██▀▀██▀██${reset} ${bluef}██▀██▀▀██▀██${reset} ${purplef}██▀██▀▀██▀██${reset} ${cyanf}██▀██▀▀██▀██${reset} ${yellowf} ▀███████▀${reset} ${redf}▀ ▀ ▀ ▀${reset} ${greenf}▀ ▀ ▀ ▀${reset} ${bluef}▀ ▀ ▀ ▀${reset} ${purplef}▀ ▀ ▀ ▀${reset} ${cyanf}▀ ▀ ▀ ▀${reset} ${boldon}${yellowf} ▄███████▄ ${redf} ▄██████▄ ${greenf} ▄██████▄ ${bluef} ▄██████▄ ${purplef} ▄██████▄ ${cyanf} ▄██████▄${reset} ${boldon}${yellowf}▄█████████▀▀ ${redf}▄${whitef}█▀█${redf}██${whitef}█▀█${redf}██▄ ${greenf}▄${whitef}█▀█${greenf}██${whitef}█▀█${greenf}██▄ ${bluef}▄${whitef}█▀█${bluef}██${whitef}█▀█${bluef}██▄ ${purplef}▄${whitef}█▀█${purplef}██${whitef}█▀█${purplef}██▄ ${cyanf}▄${whitef}█▀█${cyanf}██${whitef}█▀█${cyanf}██▄${reset} ${boldon}${yellowf}███████▀ ${redf}█${whitef}▄▄█${redf}██${whitef}▄▄█${redf}███ ${greenf}█${whitef}▄▄█${greenf}██${whitef}▄▄█${greenf}███ ${bluef}█${whitef}▄▄█${bluef}██${whitef}▄▄█${bluef}███ ${purplef}█${whitef}▄▄█${purplef}██${whitef}▄▄█${purplef}███ ${cyanf}█${whitef}▄▄█${cyanf}██${whitef}▄▄█${cyanf}███${reset} ${boldon}${yellowf}███████▄ ${redf}████████████ ${greenf}████████████ ${bluef}████████████ ${purplef}████████████ ${cyanf}████████████${reset} ${boldon}${yellowf}▀█████████▄▄ ${redf}██▀██▀▀██▀██ ${greenf}██▀██▀▀██▀██ ${bluef}██▀██▀▀██▀██ ${purplef}██▀██▀▀██▀██ ${cyanf}██▀██▀▀██▀██${reset} ${boldon}${yellowf} ▀███████▀ ${redf}▀ ▀ ▀ ▀ ${greenf}▀ ▀ ▀ ▀ ${bluef}▀ ▀ ▀ ▀ ${purplef}▀ ▀ ▀ ▀ ${cyanf}▀ ▀ ▀ ▀${reset} EOF ================================================ FILE: Colours-EyeCandy/spacey.sh ================================================ #!/bin/bash #ANSI color scheme script featuring Space Invaders # # Original: http://crunchbanglinux.org/forums/post/126921/#p126921 # Modified by lolilolicon f=3 b=4 for j in f b; do for i in {0..7}; do eval ${j}${i}=\$\'\\e\[${!j}${i}m\' done done bld=$'\e[1m' rst=$'\e[0m' cat << EOF $f0 ▄██▄ $f1 ▀▄ ▄▀ $f2 ▄▄▄████▄▄▄ $f3 ▄██▄ $f4 ▀▄ ▄▀ $f5 ▄▄▄████▄▄▄ $f6 ▄██▄ $rst $f0▄█▀██▀█▄ $f1 ▄█▀███▀█▄ $f2███▀▀██▀▀███ $f3▄█▀██▀█▄ $f4 ▄█▀███▀█▄ $f5███▀▀██▀▀███ $f6▄█▀██▀█▄ $rst $f0▀▀█▀▀█▀▀ $f1█▀███████▀█ $f2▀▀▀██▀▀██▀▀▀ $f3▀▀█▀▀█▀▀ $f4█▀███████▀█ $f5▀▀▀██▀▀██▀▀▀ $f6▀▀█▀▀█▀▀ $rst $f0▄▀▄▀▀▄▀▄ $f1▀ ▀▄▄ ▄▄▀ ▀ $f2▄▄▀▀ ▀▀ ▀▀▄▄ $f3▄▀▄▀▀▄▀▄ $f4▀ ▀▄▄ ▄▄▀ ▀ $f5▄▄▀▀ ▀▀ ▀▀▄▄ $f6▄▀▄▀▀▄▀▄ $rst $bld $f0 ▄██▄ $f1 ▀▄ ▄▀ $f2 ▄▄▄████▄▄▄ $f3 ▄██▄ $f4 ▀▄ ▄▀ $f5 ▄▄▄████▄▄▄ $f6 ▄██▄ $rst $bld $f0▄█▀██▀█▄ $f1 ▄█▀███▀█▄ $f2███▀▀██▀▀███ $f3▄█▀██▀█▄ $f4 ▄█▀███▀█▄ $f5███▀▀██▀▀███ $f6▄█▀██▀█▄$rst $bld $f0▀▀█▀▀█▀▀ $f1█▀███████▀█ $f2▀▀▀██▀▀██▀▀▀ $f3▀▀█▀▀█▀▀ $f4█▀███████▀█ $f5▀▀▀██▀▀██▀▀▀ $f6▀▀█▀▀█▀▀$rst $bld $f0▄▀▄▀▀▄▀▄ $f1▀ ▀▄▄ ▄▄▀ ▀ $f2▄▄▀▀ ▀▀ ▀▀▄▄ $f3▄▀▄▀▀▄▀▄ $f4▀ ▀▄▄ ▄▄▀ ▀ $f5▄▄▀▀ ▀▀ ▀▀▄▄ $f6▄▀▄▀▀▄▀▄$rst $f7▌$rst $f7▌$rst $f7 ▄█▄ $rst $f7▄█████████▄$rst $f7▀▀▀▀▀▀▀▀▀▀▀$rst EOF ================================================ FILE: Colours-EyeCandy/tanks.sh ================================================ #!/bin/sh # ANSI Color -- use these variables to easily have different color # and format output. Make sure to output the reset sequence after # colors (f = foreground, b = background), and use the 'off' # feature for anything you turn on. initializeANSI() { esc="" blackf="${esc}[30m"; redf="${esc}[31m"; greenf="${esc}[32m" yellowf="${esc}[33m" bluef="${esc}[34m"; purplef="${esc}[35m" cyanf="${esc}[36m"; whitef="${esc}[37m" blackb="${esc}[40m"; redb="${esc}[41m"; greenb="${esc}[42m" yellowb="${esc}[43m" blueb="${esc}[44m"; purpleb="${esc}[45m" cyanb="${esc}[46m"; whiteb="${esc}[47m" boldon="${esc}[1m"; boldoff="${esc}[22m" italicson="${esc}[3m"; italicsoff="${esc}[23m" ulon="${esc}[4m"; uloff="${esc}[24m" invon="${esc}[7m"; invoff="${esc}[27m" reset="${esc}[0m" } # note in this first use that switching colors doesn't require a reset # first - the new color overrides the old one. initializeANSI cat << EOF ${boldon}${redf} █ ${reset} ${boldon}${greenf} █ ${reset} ${boldon}${yellowf} █ ${reset} ${boldon}${bluef} █ ${reset} ${boldon}${purplef} █ ${reset} ${boldon}${cyanf} █ ${reset} ${boldon}${redf}▄▄ █ ▄▄${reset} ${boldon}${greenf}▄▄ █ ▄▄${reset} ${boldon}${yellowf}▄▄ █ ▄▄${reset} ${boldon}${bluef}▄▄ █ ▄▄${reset} ${boldon}${purplef}▄▄ █ ▄▄${reset} ${boldon}${cyanf}▄▄ █ ▄▄${reset} ${boldon}${redf}███▀▀▀███${reset} ${boldon}${greenf}███▀▀▀███${reset} ${boldon}${yellowf}███▀▀▀███${reset} ${boldon}${bluef}███▀▀▀███${reset} ${boldon}${purplef}███▀▀▀███${reset} ${boldon}${cyanf}███▀▀▀███${reset} ${boldon}${redf}███ █ ███${reset} ${boldon}${greenf}███ █ ███${reset} ${boldon}${yellowf}███ █ ███${reset} ${boldon}${bluef}███ █ ███${reset} ${boldon}${purplef}███ █ ███${reset} ${boldon}${cyanf}███ █ ███${reset} ${boldon}${redf}██ ▀▀▀ ██${reset} ${boldon}${greenf}██ ▀▀▀ ██${reset} ${boldon}${yellowf}██ ▀▀▀ ██${reset} ${boldon}${bluef}██ ▀▀▀ ██${reset} ${boldon}${purplef}██ ▀▀▀ ██${reset} ${boldon}${cyanf}██ ▀▀▀ ██${reset} ${redf} █ ${reset} ${greenf} █ ${reset} ${yellowf} █ ${reset} ${bluef} █ ${reset} ${purplef} █ ${reset} ${cyanf} █ ${reset} ${redf}▄▄ █ ▄▄${reset} ${greenf}▄▄ █ ▄▄${reset} ${yellowf}▄▄ █ ▄▄${reset} ${bluef}▄▄ █ ▄▄${reset} ${purplef}▄▄ █ ▄▄${reset} ${cyanf}▄▄ █ ▄▄${reset} ${redf}███▀▀▀███${reset} ${greenf}███▀▀▀███${reset} ${yellowf}███▀▀▀███${reset} ${bluef}███▀▀▀███${reset} ${purplef}███▀▀▀███${reset} ${cyanf}███▀▀▀███${reset} ${redf}███ █ ███${reset} ${greenf}███ █ ███${reset} ${yellowf}███ █ ███${reset} ${bluef}███ █ ███${reset} ${purplef}███ █ ███${reset} ${cyanf}███ █ ███${reset} ${redf}██ ▀▀▀ ██${reset} ${greenf}██ ▀▀▀ ██${reset} ${yellowf}██ ▀▀▀ ██${reset} ${bluef}██ ▀▀▀ ██${reset} ${purplef}██ ▀▀▀ ██${reset} ${cyanf}██ ▀▀▀ ██${reset} EOF ================================================ FILE: README.md ================================================ A bunch of scripts I keep in `~/Scripts`, which is included in `$PATH`. ================================================ FILE: aur-check ================================================ #! /usr/bin/env python3 """ Check the repo for problems and new package versions """ import subprocess from pathlib import Path import tomlkit.toml_file SOURCE_DIRS = [ { "path": Path("~/Arch/packaging/aur/").expanduser(), "nvchecker_source": "aur", }, ] NVCHECKER_CONFIG_FILE = Path("~/Arch/packaging/aur/nvchecker.toml").expanduser() def get_from_SRCINFO(path, key): with open(path, "r") as f: for line in f.readlines(): line = line.strip() if not line or line.startswith("#"): continue k, v = line.split("=", 1) if k.strip() == key: return v.strip() def get_from_PKGBUILD(path, key): with open(path, "r") as f: for line in f.readlines(): if line.startswith(f"{key}="): value = line.split("=", 1)[1].strip() if value.startswith("'") and value.endswith("'"): value = value[1:-1] if value.startswith('"') and value.endswith('"'): value = value[1:-1] return value def nvchecker(): """Updates ``nvchecker`` config file with the sources defined in ``SOURCE_DIRS`` and then runs ``nvchecker``. """ for src in SOURCE_DIRS: root_path = src["path"] # read the config file config_file = tomlkit.toml_file.TOMLFile(NVCHECKER_CONFIG_FILE) config = config_file.read() # iterate over package directories in the source root for pkg in root_path.iterdir(): if not pkg.is_dir(): continue elif not (pkg / "PKGBUILD").is_file(): print(f"WARNING: PKGBUILD not found in {pkg}") continue # extract from .SRCINFO if it exists if (pkg / ".SRCINFO").is_file(): pkgname = get_from_SRCINFO(pkg / ".SRCINFO", "pkgname") # pkgver = get_from_SRCINFO(pkg / ".SRCINFO", "pkgver") else: # extract pkgname and pkgver from PKGBUILD in the most hackish way pkgname = pkg.name # pkgname = get_from_PKGBUILD(pkg / "PKGBUILD", "pkgname") # pkgver = get_from_PKGBUILD(pkg / "PKGBUILD", "pkgver") # ensure that a TOML table for the pkgname exists if pkgname not in config: config.add(pkgname, tomlkit.table()) update_config = True else: update_config = src.get("nvchecker_overwrite", True) # update the config file if update_config: source = src["nvchecker_source"] config[pkgname]["source"] = source if source in {"aur", "archpkg"}: config[pkgname][source] = pkgname elif source == "gitlab": config[pkgname]["host"] = src["nvchecker_host"] config[pkgname]["gitlab"] = src["nvchecker_gitlab_format"].format( remote_pkgname=pkgname ) # write the config file config_file.write(config) # run nvchecker subprocess.run(["nvchecker", "-c", NVCHECKER_CONFIG_FILE], check=True) def check(): nvchecker() # TODO: check if rebuild-detector is installed print("Checking packages that need to be rebuilt...") subprocess.run(["checkrebuild", "-i", "lahwaacz"], check=True) # TODO: list packages that are in the database, but package file is deleted or source is missing if __name__ == "__main__": check() ================================================ FILE: aur-release ================================================ #!/bin/bash # # SPDX-License-Identifier: GPL-3.0-or-later # aur-remotebuild - build packages remotely using aur-chroot # (based on commitpkg from devtools) set -o errexit shopt -s nullglob readonly argv0=release source /usr/share/devtools/lib/common.sh source /usr/share/devtools/lib/util/srcinfo.sh source /usr/share/makepkg/util/parseopts.sh source /usr/share/makepkg/util/util.sh set -eo pipefail if [[ ! -f PKGBUILD ]]; then echo "No PKGBUILD in the current directory!" >&2 exit 1 fi # Check if releasing from a branch if ! branchname=$(git symbolic-ref --short HEAD); then die 'not on any branch' fi if [[ "$branchname" != master ]]; then die 'must be run from the master branch' fi # default arguments server=pkgbuild.com remote_path=public_html/repo rsyncopts=("${RSYNC_OPTS[@]}" --perms --chmod='u=rw,go=r') release_commit=1 release_push=0 release_upload=0 usage() { echo >&2 "Usage: $argv0 [OPTIONS]" echo >&2 "" echo >&2 "Run this script in a PKGBUILD dir to release an already built package." echo >&2 "" echo >&2 "The script comprises the following operations:" echo >&2 "" echo >&2 "- (default) modified version-controlled files are staged for commit" echo >&2 "- (default) all build artifacts are signed with gpg" echo >&2 "- (optional) commits are pushed to the remote git repository" echo >&2 "- (optional) build artifacts are uploaded to the binary repository" echo >&2 "" echo >&2 "OPTIONS" echo >&2 " --no-commit Do not stage version-controlled files for commit" echo >&2 " --push Automatically push commits to the remote git repository" echo >&2 " --upload Automatically upload all build artifacts to the binary" echo >&2 " repository hosted at $server:$remote_path" exit 1 } ## option parsing opt_short='' opt_long=('no-commit' 'push' 'upload') opt_hidden=() if ! parseopts "$opt_short" "${opt_long[@]}" "${opt_hidden[@]}" -- "$@"; then usage fi set -- "${OPTRET[@]}" while true; do case "$1" in --no-commit) release_commit=0 ;; --push) release_push=1 ;; --upload) release_upload=1 ;; --) shift; break ;; esac shift done check_pkgbuild_validity() { # shellcheck source=/usr/share/pacman/PKGBUILD.proto . ./PKGBUILD # skip when there are no sources available if (( ! ${#source[@]} )); then return fi # validate sources hash algo is at least > sha1 local bad_algos=("cksums" "md5sums" "sha1sums") local good_hash_algo=false # from makepkg libmakepkg/util/schema.sh for integ in "${known_hash_algos[@]}"; do local sumname="${integ}sums" if [[ -n ${!sumname} ]] && ! in_array "${sumname}" "${bad_algos[@]}"; then good_hash_algo=true break fi done if ! $good_hash_algo; then die "PKGBUILD lacks a secure cryptographic checksum, insecure algorithms: ${bad_algos[*]}" fi } # Source makepkg.conf; fail if it is not found if [[ -r '/etc/makepkg.conf' ]]; then source '/etc/makepkg.conf' else die '/etc/makepkg.conf not found!' fi # Source user-specific makepkg.conf overrides if [[ -r "${XDG_CONFIG_HOME:-$HOME/.config}/pacman/makepkg.conf" ]]; then # shellcheck source=/dev/null source "${XDG_CONFIG_HOME:-$HOME/.config}/pacman/makepkg.conf" elif [[ -r "$HOME/.makepkg.conf" ]]; then # shellcheck source=/dev/null source "$HOME/.makepkg.conf" fi source=() # shellcheck source=/usr/share/pacman/PKGBUILD.proto . ./PKGBUILD pkgbase=${pkgbase:-$pkgname} if (( ${#validpgpkeys[@]} != 0 )); then if [[ -d keys ]]; then for key in "${validpgpkeys[@]}"; do if [[ ! -f keys/pgp/$key.asc ]]; then export-pkgbuild-keys || die 'Failed to export valid PGP keys for source files' fi done else export-pkgbuild-keys || die 'Failed to export valid PGP keys for source files' fi git add --force -- keys/pgp/* fi # find files which should be under source control needsversioning=(PKGBUILD) for s in "${source[@]}"; do [[ $s != *://* ]] && needsversioning+=("$s") done for i in 'changelog' 'install'; do while read -r file; do # evaluate any bash variables used # shellcheck disable=SC2001 eval "file=\"$(sed "s/^\(['\"]\)\(.*\)\1\$/\2/" <<< "$file")\"" needsversioning+=("$file") done < <(sed -n "s/^[[:space:]]*$i=//p" PKGBUILD) done for key in "${validpgpkeys[@]}"; do needsversioning+=("keys/pgp/$key.asc") done # assert that they really are controlled by git if (( ${#needsversioning[*]} )); then for file in "${needsversioning[@]}"; do # skip none existing files if [[ ! -f "${file}" ]]; then continue fi if ! git ls-files --error-unmatch "$file"; then die "%s is not under version control" "$file" fi done fi # check packages for validity for _arch in "${arch[@]}"; do for _pkgname in "${pkgname[@]}"; do fullver=$(get_full_version "$_pkgname") if pkgfile=$(find_cached_package "$_pkgname" "$fullver" "$_arch"); then check_package_validity "$pkgfile" fi done fullver=$(get_full_version "$pkgbase") if pkgfile=$(find_cached_package "$pkgbase-debug" "$fullver" "$_arch"); then check_package_validity "$pkgfile" fi done # NOTE: not a reality on the AUR... # check for PKGBUILD standards #check_pkgbuild_validity # auto generate .SRCINFO # shellcheck disable=SC2119 write_srcinfo_file if (( release_commit )); then git add --force .SRCINFO if [[ -n $(git status --porcelain --untracked-files=no) ]]; then stat_busy 'Staging files' for f in $(git ls-files --modified); do git add "$f" done for f in $(git ls-files --deleted); do git rm "$f" done stat_done msgtemplate="upgpkg: $(get_full_version)" if [[ -n $1 ]]; then stat_busy 'Committing changes' git commit -q -m "${msgtemplate}: ${1}" || die stat_done else [[ -z ${WORKDIR:-} ]] && setup_workdir msgfile=$(mktemp --tmpdir="${WORKDIR}" commitpkg.XXXXXXXXXX) echo "$msgtemplate" > "$msgfile" if [[ -n $GIT_EDITOR ]]; then $GIT_EDITOR "$msgfile" || die elif giteditor=$(git config --get core.editor); then $giteditor "$msgfile" || die elif [[ -n $VISUAL ]]; then $VISUAL "$msgfile" || die elif [[ -n $EDITOR ]]; then $EDITOR "$msgfile" || die else die "No usable editor found (tried \$GIT_EDITOR, git config [core.editor], \$VISUAL, \$EDITOR)." fi [[ -s $msgfile ]] || die stat_busy 'Committing changes' git commit -v -q -F "$msgfile" || die unlink "$msgfile" stat_done fi fi if (( release_push )); then git_remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}") git_remote=${git_remote_branch%/*} git_remote_url=$(git remote get-url "$git_remote") msg 'Fetching remote changes' git fetch --prune --prune-tags origin || die 'failed to fetch remote changes' # Check if local branch is up to date and contains the latest origin commit if remoteref=$(git rev-parse "$git_remote_branch" 2>/dev/null); then if [[ $(git branch "$branchname" --contains "$remoteref" --format '%(refname:short)') != "$branchname" ]]; then die "local branch is out of date, run 'git pull --rebase'" fi fi msg "Pushing commits to $git_remote_branch where $git_remote is $git_remote_url" git push --tags --set-upstream "$git_remote" "$branchname" || abort else warning "Not pushing commits because --push was not given." fi elif (( release_push )); then warning "Not pushing commits because --no-commit was given." fi declare -a uploads for _arch in "${arch[@]}"; do for _pkgname in "${pkgname[@]}"; do fullver=$(get_full_version "$_pkgname") if ! pkgfile=$(find_cached_package "$_pkgname" "$fullver" "${_arch}"); then warning "Skipping %s: failed to locate package file" "$_pkgname-$fullver-$_arch" continue 2 fi uploads+=("$pkgfile") done fullver=$(get_full_version "$pkgbase") if ! pkgfile=$(find_cached_package "$pkgbase-debug" "$fullver" "$_arch"); then continue fi if ! is_debug_package "$pkgfile"; then continue fi uploads+=("$pkgfile") done for pkgfile in "${uploads[@]}"; do sigfile="${pkgfile}.sig" if [[ ! -f $sigfile ]]; then msg "Signing package %s..." "${pkgfile}" if [[ -n $GPGKEY ]]; then SIGNWITHKEY=(-u "${GPGKEY}") fi gpg --detach-sign --use-agent --no-armor "${SIGNWITHKEY[@]}" "${pkgfile}" || die fi if ! gpg --verify "$sigfile" "$pkgfile" >/dev/null 2>&1; then die "Signature %s is incorrect!" "$sigfile" fi uploads+=("$sigfile") done if (( release_upload )) && [[ ${#uploads[*]} -gt 0 ]]; then new_uploads=() # convert to absolute paths so rsync can work with colons (epoch) while read -r -d '' upload; do new_uploads+=("$upload") done < <(realpath -z "${uploads[@]}") uploads=("${new_uploads[@]}") unset new_uploads msg 'Uploading all package and signature files' rsync "${rsyncopts[@]}" "${uploads[@]}" "$server:$remote_path/" || die # convert to remote paths declare -a remote_pkgfiles for pkgfile in "${uploads[@]}"; do if ! [[ "$pkgfile" = *.sig ]]; then remote_pkgfiles+=("$remote_path"/"$(basename "$pkgfile")") fi done msg 'Updating remote pacman database' ssh -t "${SSH_OPTS[@]}" -- "$server" "./repo add ${remote_pkgfiles[*]@Q} && ./repo update" fi ================================================ FILE: aur-remotebuild ================================================ #!/bin/bash # # SPDX-License-Identifier: GPL-3.0-or-later # aur-remotebuild - build packages remotely using aur-chroot # (partly based on offload-build from devtools) set -o errexit shopt -s nullglob readonly argv0=remotebuild if [[ ! -f PKGBUILD ]]; then echo "No PKGBUILD in the current directory!" >&2 exit 1 fi # default arguments repo_name=lahwaacz chroot_args=( --create --update --build # makechrootpkg options --checkpkg --namcap ) usage() { printf >&2 'Usage: %s HOSTNAME [--repo NAME] [--inspect never|always|failure] [--] \n' "$argv0" exit 1 } server="$1" shift if [[ "$server" == "" ]]; then usage fi source /usr/share/makepkg/util/parseopts.sh ## option parsing opt_short='d:' opt_long=('inspect:') opt_hidden=() if ! parseopts "$opt_short" "${opt_long[@]}" "${opt_hidden[@]}" -- "$@"; then usage fi set -- "${OPTRET[@]}" while true; do case "$1" in --repo) shift; repo_name=$1 ;; --inspect) shift; chroot_args+=(--makechrootpkg-args="-x,$1") ;; --) shift; break ;; esac shift done # pass db name to aur-chroot chroot_args+=( --pacman-conf "/etc/aurutils/pacman-$repo_name.conf" --makepkg-conf "/etc/aurutils/makepkg-$repo_name.conf" ) # pass remaining arguments to aur-chroot if (($#)); then chroot_args+=("$@") fi # aur chroot command to run remotely aur_chroot_cmd=( env # aur-chroot does not preserve SRCDEST and LOGDEST by default https://github.com/aurutils/aurutils/issues/1196 AUR_PACMAN_AUTH="sudo --preserve-env=GNUPGHOME,SSH_AUTH_SOCK,SRCDEST,PKGDEST,LOGDEST" # NOTE: do not clear SRCDEST to use cached directory set in the user's makepkg.conf on the remote host #SRCDEST="" PKGDEST="" LOGDEST="" aur chroot "${chroot_args[@]}" ) ### offload-build-like part source /usr/share/devtools/lib/common.sh source /usr/share/devtools/lib/util/makepkg.sh source /usr/share/devtools/lib/util/srcinfo.sh source /usr/share/makepkg/util/config.sh [[ -z ${WORKDIR:-} ]] && setup_workdir TEMPDIR=$(mktemp --tmpdir="${WORKDIR}" --directory aur-remotebuild.XXXXXXXXXX) export TEMPDIR # Load makepkg.conf variables to be available # shellcheck disable=SC2119 load_makepkg_config # Use a source-only tarball as an intermediate to transfer files. This # guarantees the checksums are okay, and guarantees that all needed files are # transferred, including local sources, install scripts, and changelogs. export SRCPKGDEST="${TEMPDIR}" makepkg_source_package || die "unable to make source package" # Temporary cosmetic workaround makepkg if SRCDEST is set somewhere else # but an empty src dir is created in PWD. Remove once fixed in makepkg. rmdir --ignore-fail-on-non-empty src 2>/dev/null || true # Create a temporary directory on the server remote_temp=$( ssh "${SSH_OPTS[@]}" -- "$server" ' temp="${XDG_CACHE_HOME:-$HOME/.cache}/aur-remotebuild" && mkdir -p "$temp" && mktemp --directory --tmpdir="$temp" ') # Transfer the srcpkg to the server msg "Transferring source package to the server..." _srcpkg=("$SRCPKGDEST"/*"$SRCEXT") srcpkg="${_srcpkg[0]}" rsync "${RSYNC_OPTS[@]}" -- "$srcpkg" "$server":"$remote_temp" || die # Prepare the srcpkg on the server msg "Extracting srcpkg" ssh "${SSH_OPTS[@]}" -- "$server" "cd ${remote_temp@Q} && bsdtar --strip-components 1 -xvf $(basename "$srcpkg")" || die # Run the build command on the server msg "Running aur chroot ${chroot_args[*]}" # shellcheck disable=SC2145 if ssh "${SSH_OPTS[@]}" -t -- "$server" "cd ${remote_temp@Q} && ${aur_chroot_cmd[@]@Q}"; then msg "Build complete" # Get an array of files that should be downloaded from the server mapfile -t files < <( ssh "${SSH_OPTS[@]}" -- "$server" " cd ${remote_temp@Q}"' && while read -r file; do [[ -f "${file}" ]] && printf "%s\n" "${file}" ||: done < <(SRCDEST="" PKGDEST="" LOGDEST="" makepkg --packagelist) && printf "%s\n" '"${remote_temp@Q}/PKGBUILD"' find '"${remote_temp@Q}"' -name "*.log" ') else # Build failed, only the logs should be downloaded from the server mapfile -t files < <( ssh "${SSH_OPTS[@]}" -- "$server" ' find '"${remote_temp@Q}"' -name "*.log" ') fi if (( ${#files[@]} )); then msg 'Downloading files...' rsync "${RSYNC_OPTS[@]}" -- "${files[@]/#/$server:}" "${TEMPDIR}/" || die if is_globfile "${TEMPDIR}"/*.log; then # shellcheck disable=SC2031 mv "${TEMPDIR}"/*.log "${LOGDEST:-${PWD}}/" fi if is_globfile "${TEMPDIR}"/*.pkg.tar*; then # Building a package may change the PKGBUILD during update_pkgver # shellcheck disable=SC2031 mv "${TEMPDIR}/PKGBUILD" "${PWD}/" # shellcheck disable=SC2031 mv "${TEMPDIR}"/*.pkg.tar* "${PKGDEST:-${PWD}}/" else # shellcheck disable=SC2031 error "Build failed, check logs in ${LOGDEST:-${PWD}}" exit 1 fi # auto generate .SRCINFO # shellcheck disable=SC2119 write_srcinfo_file msg "Removing remote temporary directory $remote_temp" ssh "${SSH_OPTS[@]}" -- "$server" "rm -rf -- ${remote_temp@Q}" else exit 1 fi ================================================ FILE: backup-system.sh ================================================ #!/bin/bash # exit on first error set -e backupdir="/media/WD-black/backups" # check if destination dir exists if [[ ! -d "$backupdir" ]]; then echo "Backup directory $backupdir does not exist. Is the drive mounted?" exit 1 fi #homedir="$backupdir/home_rsync_copy" #rootdir="$backupdir/root_rsync_copy" #echo "Syncing / to $rootdir (root permissions required)" #sudo rsync / "$rootdir" -aPhAHX --info=progress2,name0,stats2 --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/home","/swapfile","/.snapshots"} #echo "Syncing ~/ to $homedir" #rsync ~/ $homedir -aPhAHX --one-file-system --info=progress2,name0,stats2 --delete # TODO: # - make snapshot with snapper just before btrfs-sync # - run `sync` before btrfs-sync to make sure that the snapshot is fully written to the disk # - copy the snapper metadata files (info.xml) # - make snapshots of the remaining subvolumes: @postgres @nspawn_containers @var_log echo "Syncing /.snapshots to $backupdir/root (root permissions required)" sudo btrfs-sync --verbose --delete /.snapshots "$backupdir/root" echo "Syncing /home/.snapshots to $backupdir/home (root permissions required)" sudo btrfs-sync --verbose --delete /home/.snapshots "$backupdir/home" ================================================ FILE: batmanpager ================================================ #!/bin/sh # mandoc passes a file name, other tools write to stdout # using `cat "$@"` we take care of both reading from file and stdin # https://github.com/sharkdp/bat/issues/1145#issuecomment-1743518097 exec cat "$@" | col -bx | bat --language man --style plain --pager "$PAGER" ================================================ FILE: bsnap.sh ================================================ #! /usr/bin/bash # exit on first error set -e backupdir="$HOME/_backup_snapshots" usage() { echo $@ >&2 echo "Usage: $0 {snapshot|transfer} ... snapshot Create snapshots for every subvolume configured in '\$backupdir/*'. The subvolume is specified by a symlink '\$backupdir/*/cur' pointing to a Btrfs subvolume. transfer Transfer all snapshots from '\$backupdir/*/' to '/', which should be other Btrfs partition. The tree structure is kept intact. \$backupdir is set to '$backupdir' " >&2 } transfer() { src="$1" # e.g. ~/_backup_snapshots/Bbox/ dst="$2" # e.g. /media/WD1T/backup-lahwaacz/Bbox/ [[ ! -d "$dst" ]] && mkdir "$dst" # get list of snapshots to transfer src_snapshots=($(find "$src" -mindepth 1 -maxdepth 1 -type d | sort)) _len=${#src_snapshots[@]} for ((i=0; i<$_len; i++)); do if [[ -e "$dst/$(basename ${src_snapshots[$i]})" ]]; then # nothing to transfer echo "Snapshot '$dst/$(basename ${src_snapshots[$i]})' already exists" continue fi # There is currently an issue that the snapshots to be used with "btrfs send" # must be physically on the disk, or you may receive a "stale NFS file handle" # error. This is accomplished by "sync" after the snapshot # # ref: http://marc.merlins.org/perso/btrfs/post_2014-03-22_Btrfs-Tips_-Doing-Fast-Incremental-Backups-With-Btrfs-Send-and-Receive.html sync dst_snapshots=($(find "$dst" -mindepth 1 -maxdepth 1 -type d | sort)) if [[ $i -eq 0 ]]; then # no parent, make initial transfer sudo sh -c "btrfs send ${src_snapshots[$i]} | btrfs receive $dst" else sudo sh -c "btrfs send -p ${src_snapshots[(($i-1))]} ${src_snapshots[$i]} | btrfs receive $dst" fi done } case $1 in snapshot) for dir in "$backupdir"/*; do if [[ -L "$dir/cur" ]]; then btrfs subvolume snapshot -r $(realpath "$dir/cur") "$dir/$(date +%F-%T)" else echo "$dir/cur does not exist or is not a symlink" fi done ;; transfer) [ -n "$2" -a -d "$2" ] || usage "Invalid destination path" for dir in "$backupdir"/*; do transfer "$dir" "$2"/$(basename "$dir") done ;; *) usage "Incorrect invocation" esac ================================================ FILE: btrfs-diff ================================================ #!/bin/bash # Author: http://serverfault.com/users/96883/artfulrobot # License: Unknown # # This script will show most files that got modified or added. # Renames and deletions will not be shown. # Read limitations on: # http://serverfault.com/questions/399894/does-btrfs-have-an-efficient-way-to-compare-snapshots # # btrfs send is the best way to do this long term, but as of kernel # 3.14, btrfs send cannot just send a list of changed files without # scanning and sending all the changed data blocks along. usage() { echo $@ >&2; echo "Usage: $0 " >&2; exit 1; } [ $# -eq 2 ] || usage "Incorrect invocation"; SNAPSHOT_OLD=$1; SNAPSHOT_NEW=$2; [ -d $SNAPSHOT_OLD ] || usage "$SNAPSHOT_OLD does not exist"; [ -d $SNAPSHOT_NEW ] || usage "$SNAPSHOT_NEW does not exist"; OLD_TRANSID=`btrfs subvolume find-new "$SNAPSHOT_OLD" 9999999` OLD_TRANSID=${OLD_TRANSID#transid marker was } [ -n "$OLD_TRANSID" -a "$OLD_TRANSID" -gt 0 ] || usage "Failed to find generation for $SNAPSHOT_NEW" btrfs subvolume find-new "$SNAPSHOT_NEW" $OLD_TRANSID | sed '$d' | cut -f17- -d' ' | sort | uniq ================================================ FILE: btrfs-sync ================================================ #!/bin/bash # # Simple script that synchronizes BTRFS snapshots locally. # Features compression, retention policy and automatic incremental sync # set -e set -o pipefail set -o errtrace print_usage() { echo "Usage: $BIN [options] [...] -k|--keep NUM keep only last sync'ed snapshots -d|--delete delete snapshots in that don't exist in -q|--quiet don't display progress -v|--verbose display more information -h|--help show usage can either be a single snapshot, or a folder containing snapshots " } echov() { if [[ "$VERBOSE" == 1 ]]; then echo "$@"; fi } #---------------------------------------------------------------------------------------------------------- # preliminary checks BIN="${0##*/}" [[ $# -lt 2 ]] && { print_usage ; exit 1; } [[ ${EUID} -ne 0 ]] && { echo "Must be run as root. Try 'sudo $BIN'"; exit 1; } # parse arguments KEEP=0 OPTS=$( getopt -o hqzZk:p:dv -l quiet -l help -l keep: -l delete -l verbose -- "$@" 2>/dev/null ) [[ $? -ne 0 ]] && { echo "error parsing arguments"; exit 1; } eval set -- "$OPTS" while true; do case "$1" in -h|--help ) print_usage; exit 0 ;; -q|--quiet ) QUIET=1 ; shift 1 ;; -d|--delete ) DELETE=1 ; shift 1 ;; -k|--keep ) KEEP=$2 ; shift 2 ;; -v|--verbose) VERBOSE=1 ; shift 1 ;; --) shift; break ;; esac done # detect src and dst arguments SRC=( "${@:1:$#-1}" ) DST="${@: -1}" test -x "$SRC" &>/dev/null || { echo "Access error. Do you have adequate permissions for $SRC?" exit 1 } test -x "$DST" &>/dev/null || { echo "Access error. Do you have adequate permissions for $DST?" exit 1 } #---------------------------------------------------------------------------------------------------------- # more checks ## don't overlap if pgrep -F /run/btrfs-sync.pid &>/dev/null; then echo "$BIN is already running" exit 1 fi echo $$ > /run/btrfs-sync.pid ## src checks echov "* Check source" SRCS=() SRCS_BASE=() for s in "${SRC[@]}"; do src="$(realpath "$s")" if ! test -e "$src"; then echo "$s not found" exit 1 fi # check if the src is a read-only subvolume if btrfs subvolume show "$src" &>/dev/null && [[ "$(btrfs property get -ts "$src")" == "ro=true" ]]; then SRCS+=("$src") SRCS_BASE+=("$src") else for dir in $( find "$src" -maxdepth 2 -type d ); do # check if the src is a read-only subvolume if btrfs subvolume show "$dir" &>/dev/null && [[ "$(btrfs property get -ts "$dir")" == "ro=true" ]]; then SRCS+=("$dir") SRCS_BASE+=("$src") fi done fi done if [[ ${#SRCS[@]} -eq 0 ]]; then echo "no BTRFS subvolumes found" exit 1 fi ## use 'pv' command if available PV=( pv -F"time elapsed [%t] | rate %r | total size [%b]" ) if [[ "$QUIET" == "1" ]]; then PV=( cat ) else if ! type pv &>/dev/null; then echo "INFO: install the 'pv' package in order to get a progress indicator" PV=( cat ) fi fi #---------------------------------------------------------------------------------------------------------- # sync snapshots get_dst_snapshots() { # sets DSTS DST_UUIDS local DST="$1" DSTS=() DST_UUIDS=() for dir in $( find "$DST" -maxdepth 2 -type d ); do if btrfs subvolume show "$dir" &>/dev/null; then local UUID=$( btrfs subvolume show "$dir" 2>/dev/null | grep 'Received UUID' | awk '{ print $3 }' ) if [[ "$UUID" != "-" ]] && [[ "$UUID" != "" ]]; then DSTS+=("$dir") DST_UUIDS+=("$UUID") fi fi done } choose_seed() { # sets SEED local SRC="$1" local SRC_BASE="$2" SEED="$SEED_NEXT" if [[ "$SEED" == "" ]]; then # try to get most recent src snapshot that exists in dst to use as a seed local RXID_CALCULATED=0 declare -A PATH_RXID DATE_RXID SHOWP RXIDP DATEP local LIST="$( btrfs subvolume list -su "$SRC" )" local SEED_CANDIDATES=() for id in "${DST_UUIDS[@]}"; do # try to match by UUID local PATH_=$( awk "{ if ( \$14 == \"$id\" ) print \$16 }" <<<"$LIST" ) local DATE=$( awk "{ if ( \$14 == \"$id\" ) print \$11, \$12 }" <<<"$LIST" ) # try to match by received UUID, only if necessary if [[ "$PATH_" == "" ]]; then if [[ "$RXID_CALCULATED" == "0" ]]; then # create table during the first iteration if needed local PATHS=( $( btrfs subvolume list -u "$SRC" | awk '{ print $11 }' ) ) for p in "${PATHS[@]}"; do SHOWP="$( btrfs subvolume show "$( dirname "$SRC" )/$( basename "$p" )" 2>/dev/null )" RXIDP="$( grep 'Received UUID' <<<"$SHOWP" | awk '{ print $3 }' )" DATEP="$( grep 'Creation time' <<<"$SHOWP" | awk '{ print $3, $4 }' )" [[ "$RXIDP" == "" ]] && continue PATH_RXID["$RXIDP"]="$p" DATE_RXID["$RXIDP"]="$DATEP" done RXID_CALCULATED=1 fi PATH_="${PATH_RXID["$id"]}" DATE="${DATE_RXID["$id"]}" fi if [[ "$PATH_" == "" ]] || [[ "$PATH_" == "$( basename "$SRC" )" ]]; then continue fi # if the path does not exist, it is likely relative to the root subvolume # rather than the mounted subvolume if ! test -d "$PATH_" && mountpoint -q "$SRC_BASE"; then local SRC_BASE_SUBVOL=$(findmnt -n -o OPTIONS "$SRC_BASE" | tr "," "\n" | grep "subvol=" | awk -F '=' '{ print $2 }') # drop the leading slash SRC_BASE_SUBVOL="${SRC_BASE_SUBVOL#/}" # replace the prefix in $PATH_ if [[ "$PATH_" =~ "$SRC_BASE_SUBVOL"* ]]; then PATH_="${PATH_#${SRC_BASE_SUBVOL}}" PATH_="$SRC_BASE/$PATH_" fi fi local SECS=$( date -d "$DATE" +"%s" ) SEED_CANDIDATES+=("$SECS|$PATH_") done SEED=$(IFS=$'\n' echo "${SEED_CANDIDATES[@]}" | sort -V | tail -1 | cut -f2 -d'|') fi } exists_at_dst() { local SHOW="$( btrfs subvolume show "$SRC" )" local SRC_UUID="$( grep 'UUID:' <<< "$SHOW" | head -1 | awk '{ print $2 }' )" grep -q "$SRC_UUID" <<<"${DST_UUIDS[@]}" && return 0; local SRC_RXID="$( grep 'Received UUID' <<< "$SHOW" | awk '{ print $3 }' )" grep -q "^-$" <<<"$SRC_RXID" && return 1; grep -q "$SRC_RXID" <<<"${DST_UUIDS[@]}" && return 0; return 1 } ## sync incrementally sync_snapshot() { local SRC="$1" local SRC_BASE="$2" if ! test -d "$SRC" || ! test -d "$SRC_BASE"; then return fi if exists_at_dst "$SRC"; then echov "* Skip existing '$SRC'" return 0 fi choose_seed "$SRC" "$SRC_BASE" # sets SEED echo "SEED=$SEED" # incremental sync argument if [[ "$SEED" != "" ]]; then if test -d "$SEED"; then # Sends the difference between the new snapshot and old snapshot to the # backup location. Using the -c flag instead of -p tells it that there # is an identical subvolume to the old snapshot at the receiving # location where it can get its data. This helps speed up the transfer. local SEED_ARG=( -c "$SEED" ) else echo "INFO: couldn't find $SEED. Non-incremental mode" fi fi # destination path where the subvolume will be sent local DST_SUBVOL="$DST/$( realpath --relative-to "$SRC_BASE" "$SRC" )" if test -d "$DST_SUBVOL"; then echo "ERROR: destination directory $DST_SUBVOL already exists, but was not detected as a Btrfs subvolume." >&2 return 1 fi # create the parent directory at destination mkdir -p "$(dirname "$DST_SUBVOL")" # print info echo -n "* Synchronizing '$SRC' to '$DST_SUBVOL'" if [[ "$SEED" != "" ]]; then echov -n " using seed '$SEED'" fi echo "..." # do it btrfs send -q "${SEED_ARG[@]}" "$SRC" \ | "${PV[@]}" \ | btrfs receive "$(dirname "$DST_SUBVOL")" 2>&1 \ | (grep -v -e'^At subvol ' -e'^At snapshot ' || true) \ || { btrfs subvolume delete "$DST_SUBVOL" 2>/dev/null return 1; } # update DST list DSTS+=("$DST_SUBVOL") DST_UUIDS+=("$SRC_UUID") SEED_NEXT="$SRC" } #---------------------------------------------------------------------------------------------------------- # sync all snapshots found in src echov "* Check destination" get_dst_snapshots "$DST" # sets DSTS DST_UUIDS for (( i=0; i<"${#SRCS[@]}"; i++ )); do src="${SRCS[$i]}" src_base="${SRCS_BASE[$i]}" sync_snapshot "$src" "$src_base" && RET=0 || RET=1 # for i in 1 2; do # [[ "$RET" != "1" ]] && break # echo "* Retrying '$src'..." # sync_snapshot "$src" && RET=0 || RET=1 # done if [[ "$RET" == "1" ]]; then echo "Abort" exit 1 fi done #---------------------------------------------------------------------------------------------------------- # retention policy if [[ "$KEEP" != 0 ]] && [[ ${#DSTS[@]} -gt $KEEP ]]; then echo "* Pruning old snapshots..." for (( i=0; i < $(( ${#DSTS[@]} - KEEP )); i++ )); do PRUNE_LIST+=( "${DSTS[$i]}" ) done btrfs subvolume delete "${PRUNE_LIST[@]}" fi # delete flag if [[ "$DELETE" == 1 ]]; then for dst in "${DSTS[@]}"; do FOUND=0 # for src in "${SRCS[@]}"; do for (( i=0; i<"${#SRCS[@]}"; i++ )); do src="${SRCS[$i]}" echo "checking $src" if [[ "$( basename $src )" == "$( basename $dst )" ]]; then FOUND=1 break fi done if [[ "$FOUND" == 0 ]]; then DEL_LIST+=( "$dst" ) fi done if [[ "$DEL_LIST" != "" ]]; then echo "* Deleting non existent snapshots..." btrfs subvolume delete "${DEL_LIST[@]}" fi fi ================================================ FILE: btrfs-sync-WIP ================================================ #!/bin/bash set -o errtrace version="0.0" name="btrfs-sync" SNAPPER_CONFIG=/etc/conf.d/snapper TMPDIR=$(mktemp -d) PIPE=$TMPDIR/$name.out mkfifo $PIPE systemd-cat -t "$name" < $PIPE & exec 3>$PIPE donotify=0 which notify-send &> /dev/null if [[ $? -ne 0 ]]; then donotify=1 fi error() { printf "==> ERROR: %s\n" "$@" notify_error 'Error' 'Check journal for more information.' } >&2 die() { error "$@" exit 1 } traperror() { printf "Exited due to error on line %s.\n" $1 printf "exit status: %s\n" "$2" printf "command: %s\n" "$3" printf "bash line: %s\n" "$4" printf "function name: %s\n" "$5" exit 1 } trapkill() { die "Exited due to user intervention." } trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR trap trapkill SIGTERM SIGINT usage() { cat < snapper configuration to backup -d, --description snapper description -h, --help print this message -n, --noconfirm do not ask for confirmation -q, --quiet do not send notifications; instead print them. -s, --subvolid subvolume id of the mounted BTRFS subvolume to back up to -u, --UUID UUID of the mounted BTRFS subvolume to back up to See 'man snap-sync' for more details. EOF } while [[ $# -gt 0 ]]; do key="$1" case $key in -d|--description) description="$2" shift 2 ;; -c|--config) selected_configs="$2" shift 2 ;; -u|--UUID) uuid_cmdline="$2" shift 2 ;; -s|--subvolid) subvolid_cmdline="$2" shift 2 ;; -n|--noconfirm) noconfirm="yes" shift ;; -h|--help) usage exit 1 ;; -q|--quiet) donotify=1 shift ;; *) die "Unknown option: '$key'. Run '$name -h' for valid options." ;; esac done notify() { for u in $(users | tr ' ' '\n' | sort -u); do sudo -u $u DISPLAY=:0 \ DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \ notify-send -a $name "$1" "$2" --icon="dialog-$3" done } notify_info() { if [[ $donotify -eq 0 ]]; then notify "$1" "$2" "information" else printf "$1: $2\n" fi } notify_error() { if [[ $donotify -eq 0 ]]; then notify "$1" "$2" "error" else printf "$1: $2\n" fi } [[ $EUID -ne 0 ]] && die "Script must be run as root. See '$name -h' for a description of options" ! [[ -f $SNAPPER_CONFIG ]] && die "$SNAPPER_CONFIG does not exist." description=${description:-"latest incremental backup"} uuid_cmdline=${uuid_cmdline:-"none"} subvolid_cmdline=${subvolid_cmdline:-"5"} noconfirm=${noconfirm:-"no"} if [[ "$uuid_cmdline" != "none" ]]; then notify_info "Backup started" "Starting backups to $uuid_cmdline subvolid=$subvolid_cmdline..." else notify_info "Backup started" "Starting backups. Use command line menu to select disk." fi if [[ "$(findmnt -n -v --target / -o FSTYPE)" == "btrfs" ]]; then EXCLUDE_UUID=$(findmnt -n -v -t btrfs --target / -o UUID) TARGETS=$(findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v $EXCLUDE_UUID | awk '{print $2}') UUIDS=$(findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v $EXCLUDE_UUID | awk '{print $1}') else TARGETS=$(findmnt -n -v -t btrfs -o TARGET --list) UUIDS=$(findmnt -n -v -t btrfs -o UUID --list) fi declare -a TARGETS_ARRAY declare -a UUIDS_ARRAY declare -a SUBVOLIDS_ARRAY i=0 for x in $TARGETS; do SUBVOLIDS_ARRAY[$i]=$(btrfs subvolume show $x | awk '/Subvolume ID:/ { print $3 }') TARGETS_ARRAY[$i]=$x i=$((i+1)) done i=0 disk=-1 disk_count=0 for x in $UUIDS; do UUIDS_ARRAY[$i]=$x if [[ "$x" == "$uuid_cmdline" && ${SUBVOLIDS_ARRAY[$((i))]} == "$subvolid_cmdline" ]]; then disk=$i disk_count=$(($disk_count+1)) fi i=$((i+1)) done if [[ "${#UUIDS_ARRAY[$@]}" -eq 0 ]]; then die "No external btrfs subvolumes found to backup to. Run '$name -h' for more options." fi if [[ "$disk_count" > 1 ]]; then printf "Multiple mount points were found with UUID %s and subvolid %s.\n" "$uuid_cmdline" "$subvolid_cmdline" disk="-1" fi if [[ "$disk" == -1 ]]; then if [[ "$disk_count" == 0 && "$uuid_cmdline" != "none" ]]; then error "A device with UUID $uuid_cmdline and subvolid $subvolid_cmdline was not found to be mounted, or it is not a BTRFS device." fi printf "Select a mounted BTRFS device on your local machine to backup to.\nFor more options, exit and run '$name -h'.\n" while [[ $disk -lt 0 || $disk -gt $i ]]; do for x in "${!TARGETS_ARRAY[@]}"; do printf "%4s) %s (uuid=%s, subvolid=%s)\n" "$((x+1))" "${TARGETS_ARRAY[$x]}" "${UUIDS_ARRAY[$x]}" "${SUBVOLIDS_ARRAY[$x]}" done printf "%4s) Exit\n" "0" read -e -r -p "Enter a number: " disk if ! [[ $disk == ?(-)+([0-9]) ]] || [[ $disk -lt 0 || $disk -gt $i ]]; then printf "\nNo disk selected. Select a disk to continue.\n" disk=-1 fi done if [[ $disk == 0 ]]; then exit 0 fi disk=$(($disk-1)) fi selected_subvolid="${SUBVOLIDS_ARRAY[$((disk))]}" selected_uuid="${UUIDS_ARRAY[$((disk))]}" selected_mnt="${TARGETS_ARRAY[$((disk))]}" printf "\nYou selected the disk with uuid=%s, subvolid=%s.\n" "$selected_uuid" "$selected_subvolid" | tee $PIPE printf "The disk is mounted at '%s'.\n" "$selected_mnt" | tee $PIPE source $SNAPPER_CONFIG if [[ -z $selected_configs ]]; then printf "\nInteractively cycling through all snapper configurations...\n" fi selected_configs=${selected_configs:-$SNAPPER_CONFIGS} declare -a BACKUPDIRS_ARRAY declare -a MYBACKUPDIR_ARRAY declare -a OLD_NUM_ARRAY declare -a OLD_SNAP_ARRAY declare -a NEW_NUM_ARRAY declare -a NEW_SNAP_ARRAY declare -a NEW_INFO_ARRAY declare -a BACKUPLOC_ARRAY declare -a CONT_BACKUP_ARRAY # Initial configuration of where backup directories are i=0 for x in $selected_configs; do if [[ "$(snapper -c $x list -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {cnt++} END {print cnt}')" -gt 1 ]]; then error "More than one snapper entry found with UUID $selected_uuid subvolid $selected_subvolid for configuration $x. Skipping configuration $x." continue fi if [[ "$(snapper -c $x list -t single | awk '/'$name' backup in progress/ {cnt++} END {print cnt}')" -gt 0 ]]; then printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$name" "$x" | tee $PIPE if [[ $noconfirm == "yes" ]]; then printf "'noconfirm' option passed. Failed backups will not be deleted.\n" | tee $PIPE else read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N]? " delete_failed while [[ -n "$delete_failed" && "$delete_failed" != [Yy]"es" && "$delete_failed" != [Yy] && "$delete_failed" != [Nn]"o" && "$delete_failed" != [Nn] ]]; do read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N] " delete_failed if [[ -n "$delete_failed" && "$delete_failed" != [Yy]"es" && "$delete_failed" != [Yy] && "$delete_failed" != [Nn]"o" && "$delete_failed" != [Nn] ]]; then printf "Select 'y' or 'N'.\n" fi done if [[ "$delete_failed" == [Yy]"es" || "$delete_failed" == [Yy] ]]; then snapper -c $x delete $(snapper -c $x list | awk '/'$name' backup in progress/ {print $1}') fi fi fi SNAP_SYNC_EXCLUDE=no if [[ -f "/etc/snapper/configs/$x" ]]; then source /etc/snapper/configs/$x # TODO: snapper -c "$x" --jsonout get-config else die "Selected snapper configuration $x does not exist." fi if [[ $SNAP_SYNC_EXCLUDE == "yes" ]]; then continue fi printf "\n" old_num=$(snapper -c "$x" list -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $1}') old_snap=$SUBVOLUME/.snapshots/$old_num/snapshot OLD_NUM_ARRAY[$i]=$old_num OLD_SNAP_ARRAY[$i]=$old_snap if [[ -z "$old_num" ]]; then printf "No backups have been performed for '%s' on this disk.\n" "$x" read -e -r -p "Enter name of subvolume to store backups, relative to $selected_mnt (to be created if not existing): " mybackupdir printf "This will be the initial backup for snapper configuration '%s' to this disk. This could take awhile.\n" "$x" BACKUPDIR="$selected_mnt/$mybackupdir" test -d "$BACKUPDIR" || btrfs subvolume create "$BACKUPDIR" else mybackupdir=$(snapper -c "$x" list -t single | awk -F"|" '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}') BACKUPDIR="$selected_mnt/$mybackupdir" test -d $BACKUPDIR || die "%s is not a directory on %s.\n" "$BACKUPDIR" "$selected_uuid" fi BACKUPDIRS_ARRAY[$i]="$BACKUPDIR" MYBACKUPDIR_ARRAY[$i]="$mybackupdir" printf "Creating new local snapshot for '%s' configuration...\n" "$x" | tee $PIPE # new_num=$(snapper -c "$x" create --print-number -d "$name backup in progress") new_num=TODO new_snap=$SUBVOLUME/.snapshots/$new_num/snapshot new_info=$SUBVOLUME/.snapshots/$new_num/info.xml sync backup_location=$BACKUPDIR/$x/$new_num/ printf "Will backup %s to %s\n" "$new_snap" "$backup_location/snapshot" | tee $PIPE if (test -d "$backup_location/snapshot") ; then printf "WARNING: Backup directory '%s' already exists. This configuration will be skipped!\n" "$backup_location/snapshot" | tee $PIPE printf "Move or delete destination directory and try backup again.\n" | tee $PIPE fi NEW_NUM_ARRAY[$i]="$new_num" NEW_SNAP_ARRAY[$i]="$new_snap" NEW_INFO_ARRAY[$i]="$new_info" BACKUPLOC_ARRAY[$i]="$backup_location" cont_backup="K" CONT_BACKUP_ARRAY[$i]="yes" if [[ $noconfirm == "yes" ]]; then cont_backup="yes" else while [[ -n "$cont_backup" && "$cont_backup" != [Yy]"es" && "$cont_backup" != [Yy] && "$cont_backup" != [Nn]"o" && "$cont_backup" != [Nn] ]]; do read -e -r -p "Proceed with backup of '$x' configuration [Y/n]? " cont_backup if [[ -n "$cont_backup" && "$cont_backup" != [Yy]"es" && "$cont_backup" != [Yy] && "$cont_backup" != [Nn]"o" && "$cont_backup" != [Nn] ]]; then printf "Select 'Y' or 'n'.\n" fi done fi if [[ "$cont_backup" != [Yy]"es" && "$cont_backup" != [Yy] && -n "$cont_backup" ]]; then CONT_BACKUP_ARRAY[$i]="no" printf "Not backing up '%s' configuration.\n" $x # snapper -c $x delete $new_num fi i=$(($i+1)) done # Actual backing up printf "\nPerforming backups...\n" | tee $PIPE i=-1 for x in $selected_configs; do i=$(($i+1)) SNAP_SYNC_EXCLUDE=no if [[ -f "/etc/snapper/configs/$x" ]]; then source /etc/snapper/configs/$x else die "Selected snapper configuration $x does not exist." fi cont_backup=${CONT_BACKUP_ARRAY[$i]} if [[ $cont_backup == "no" || $SNAP_SYNC_EXCLUDE == "yes" ]]; then notify_info "Backup in progress" "NOTE: Skipping $x configuration." continue fi notify_info "Backup in progress" "Backing up $x configuration." printf "\n" old_num="${OLD_NUM_ARRAY[$i]}" old_snap="${OLD_SNAP_ARRAY[$i]}" BACKUPDIR="${BACKUPDIRS_ARRAY[$i]}" mybackupdir="${MYBACKUPDIR_ARRAY[$i]}" new_num="${NEW_NUM_ARRAY[$i]}" new_snap="${NEW_SNAP_ARRAY[$i]}" new_info="${NEW_INFO_ARRAY[$i]}" backup_location="${BACKUPLOC_ARRAY[$i]}" if (test -d "$backup_location/snapshot") ; then printf "ERROR: Backup directory '%s' already exists. Skipping backup of this configuration!\n" "$backup_location/snapshot" | tee $PIPE continue fi mkdir -p $backup_location if [[ -z "$old_num" ]]; then printf "Sending first snapshot for '%s' configuration...\n" "$x" | tee $PIPE # btrfs send "$new_snap" | btrfs receive "$backup_location" &>/dev/null else printf "Sending incremental snapshot for '%s' configuration...\n" "$x" | tee $PIPE # Sends the difference between the new snapshot and old snapshot to the # backup location. Using the -c flag instead of -p tells it that there # is an identical subvolume to the old snapshot at the receiving # location where it can get its data. This helps speed up the transfer. # btrfs send -c "$old_snap" "$new_snap" | btrfs receive "$backup_location" # printf "Modifying data for old local snapshot for '%s' configuration...\n" "$x" | tee $PIPE # snapper -v -c "$x" modify -d "old snap-sync snapshot (you may remove)" -u "backupdir=,subvolid=,uuid=" -c "number" "$old_num" fi cp "$new_info" "$backup_location" # It's important not to change this userdata in the snapshots, since that's how # we find the previous one. # userdata="backupdir=$mybackupdir, subvolid=$selected_subvolid, uuid=$selected_uuid" # Tag new snapshot as the latest # printf "Tagging local snapshot as latest backup for '%s' configuration...\n" "$x" | tee $PIPE # snapper -v -c "$x" modify -d "$description" -u "$userdata" "$new_num" printf "Backup complete for '%s' configuration.\n" "$x" > $PIPE done printf "\nDone!\n" | tee $PIPE exec 3>&- if [[ "$uuid_cmdline" != "none" ]]; then notify_info "Finished" "Backups to $uuid_cmdline complete!" else notify_info "Finished" "Backups complete!" fi ================================================ FILE: clean-aur-dir.py ================================================ #! /usr/bin/env python import os import sys import re import subprocess pkgname_regex = re.compile("^(?P[a-z0-9@._+-]+)-(?P[a-z0-9._:-]+)-(?Pany|x86_64|i686)\.pkg\.tar(\.xz)?(\.sig)?$", re.IGNORECASE) def usage(): print("Simple utility to clean directories from old Arch's package files, keeping only those currently installed") print("usage: %s PATH" % sys.argv[0]) sys.exit(1) if __name__ == "__main__": if len(sys.argv) != 2: usage() path = sys.argv[1] if not os.path.isdir(path): usage() os.chdir(path) files = {} # remove files that don't match pkgname_reges from further processing!! for f in os.listdir(): if not os.path.isfile(f): continue match = re.match(pkgname_regex, f) if match: # strip extension for future comparison with expac's output files[f] = "{pkgname}-{pkgver}-{arch}".format(**match.groupdict()) # get list of installed packages installed = subprocess.check_output("expac -Qs '%n-%v-%a'", shell=True, universal_newlines=True).splitlines() for f in sorted(files): # compare with the key instead of the whole filename # (drops file extensions like .pkg.tar.{xz,gz}{,.sig} ) ff = files[f] if ff in installed: print("Kept: %s" % f) else: print("Deleted: %s" % f) os.remove(f) ================================================ FILE: convertToUtf8.py ================================================ #! /usr/bin/env python import sys import os import traceback CHARSETS = ("ascii", "cp1250", "cp1252", "iso-8859-9", "iso-8859-15") def is_utf8(filepath): try: file = open(filepath, "rb") file.read().decode('utf-8') file.close() return True except: return False def to_utf8(path): for charset in CHARSETS: try: f = open(path, 'rb') content = f.read().decode(charset) f.close() f = open(path, 'wb') f.write(content.encode('utf-8')) f.close() return "Converting to utf-8: " + os.path.split(path)[1] except: pass return "Unable to open " + os.path.split(path)[1] + " - unknown charset or binary file." def run(): message = "" for filename in sys.argv[1:]: if os.path.isfile(filename): if is_utf8(filename): message += os.path.split(filename)[1] + " is already in utf-8.\n" else: message += to_utf8(filename) + "\n" return message.strip() if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: " + sys.argv[0] + " file1 [file2 ...]") sys.exit(1) try: message = run() except: message = traceback.format_exc() if message != "": print(message) ================================================ FILE: fatcp ================================================ #! /usr/bin/bash # Script for safe copying to FAT32 filesystems. # All bad characters are replaced by '_' (underscore) when copying. # File conflicts (e.g. 'foo?' and 'foo:' are both mapped to 'foo_') are not checked - using 'cp -i' is recommended. # Some resources: # http://askubuntu.com/questions/11634/how-can-i-substitute-colons-when-i-rsync-on-a-usb-key # # Simple (stupid) alternative: # find -type f -name '*.pat' -print0 | tar -c -f - --null --files-from - | tar -C /path/to/dst -v -x -f - --show-transformed --transform 's/?/_/g' # # two arguments are accepted if [[ $# -ne 2 ]]; then echo "Usage: $0 " exit 1 fi base=$(realpath "$1") basedir=$(dirname "$base") dst=$(realpath "$2") # $dst must be existing dir if [[ ! -d "$dst" ]]; then echo "Target directory '$dst' does not exist." exit 1 fi # 'cp' alias CP="cp -i --preserve=all" # characters that will be replaced with '_' BADCHARS='<>|;:!?"*\+' # enhance globbing shopt -s dotglob globstar # function creating target file/dir name mk_target() { local target=${1#"$basedir"} echo "$dst/${target//[$BADCHARS]/_}" } # dirs and files are handled differently if [[ -d "$base" ]]; then target=$(mk_target "$base") mkdir "$target" for src in "$base"/**/*; do target=$(mk_target "$src") if [[ -d "$src" ]]; then mkdir -p -- "$target" elif [[ "$src" != "$target" ]]; then $CP -- "$src" "$target" fi done elif [[ -f "$base" ]]; then target=$(mk_target "$base") if [[ "$src" != "$target" ]]; then $CP -- "$base" "$target" fi fi ================================================ FILE: ffparser.py ================================================ #! /usr/bin/env python import argparse from pythonscripts.ffparser import FFprobeParser if __name__ == "__main__": parser = argparse.ArgumentParser(description="parse ffprobe's json output") option = parser.add_mutually_exclusive_group(required=True) option.add_argument("-a", "--audio", action="store_const", const="audio", dest="option", help="get audio attribute") option.add_argument("-v", "--video", action="store_const", const="video", dest="option", help="get video attribute") option.add_argument("-f", "--format", action="store_const", const="format", dest="option", help="get format attribute") action = parser.add_mutually_exclusive_group(required=True) action.add_argument("-g", "--get", action="store", nargs=1, dest="attribute", help="attribute name to get") action.add_argument("-p", "--print", action="store_true", dest="pprint", help="print all attributes and exit") parser.add_argument("path", action="store", nargs=1, help="path to file to parse") args = parser.parse_args() ffparser = FFprobeParser(args.path[0]) if args.pprint: ffparser.pprint(args.option) else: print(ffparser.get(args.option, args.attribute[0])) ================================================ FILE: fmount.py ================================================ #! /usr/bin/env python3 import argparse import configparser import os import subprocess import sys from pathlib import Path CONFIG = Path.home() / ".config" / "fmount.conf" DEFAULT_MOUNTPATH = Path.home() / "mnt" # we just strip spaces in the mntopts string def reformat_mntopts(mntopts): mntopts = mntopts.split(",") options = [] for opt in mntopts: options.append("=".join(tk.strip() for tk in opt.split("="))) return ",".join(set(options)) def mount_gio(*, scheme: str, host: str, path: str, user: str, port: str, mountpoint: Path): if mountpoint.exists() and not mountpoint.is_symlink(): print(f"Error: path {mountpoint} exists but is not a symlink", file=sys.stderr) return location = f"{scheme}://" if user: location += user + "@" location += host if port: location += ":" + port location += "/" + path # get path to thet gvfs directory XDG_RUNTIME_DIR = os.environ.get("XDG_RUNTIME_DIR") if XDG_RUNTIME_DIR is None: XDG_RUNTIME_DIR = f"/run/user/{os.getuid()}" gvfs = Path(XDG_RUNTIME_DIR) / "gvfs" # save current gvfs mounts if gvfs.is_dir(): mounts_before = set(gvfs.glob(f"{scheme}-share:*")) else: mounts_before = set() print(f"Mounting {location}") cmd = ["gio", "mount", location] subprocess.run(cmd, check=True) if not gvfs.is_dir(): print(f"Error: gvfs directory {gvfs} does not exist", file=sys.stderr) return # detect the new gvfs mount symlink it to mountpoint mounts_after = set(gvfs.glob(f"{scheme}-share:*")) target = list(mounts_after - mounts_before)[0] # hack for inaccessible parents of the path on smb servers if scheme == "smb": _path = Path(path.lstrip("/")) # the first part is the remote share, the rest is the location we want target /= _path.relative_to(_path.parts[0]) # create a symlink from mountpoint to gvfs target mountpoint.symlink_to(target) def mount_sshfs(*, host: str, path: str, user: str, port: str, mountpoint: Path, mntopts: str): uhd = host + ":" + path if user: uhd = user + "@" + uhd cmd = ["sshfs", uhd, str(mountpoint)] if mntopts: cmd += ["-o", mntopts] if port: cmd += ["-p", port] print(f"Mounting at '{mountpoint}'...") # the mountpoint might exist after an error or automatic unmount mountpoint.mkdir(parents=True, exist_ok=True) subprocess.run(cmd, check=True) def mount(name, mountpath: Path, config): mountpoint = mountpath / name scheme = config.get(name, "scheme", fallback="sshfs") host = config.get(name, "host", fallback=name) path = config.get(name, "path", fallback="") user = config.get(name, "user", fallback=None) port = config.get(name, "port", fallback=None) mntopts = config.get(name, "mntopts", fallback="") mntopts = reformat_mntopts(mntopts) if scheme == "sshfs": # sshfs is *much* faster than gvfs return mount_sshfs( host=host, path=path, user=user, port=port, mountpoint=mountpoint, mntopts=mntopts, ) else: return mount_gio( scheme=scheme, host=host, path=path, user=user, port=port, mountpoint=mountpoint, ) def umount(mntpoint: Path): if path.is_mount(): cmd = ["fusermount3", "-u", str(mntpoint)] subprocess.run(cmd, check=True) clean(mntpoint) elif path.is_symlink(): if path.readlink().exists(): cmd = ["gio", "mount", "--unmount", str(mntpoint.resolve())] subprocess.run(cmd, check=True) # do not call clean(path), gio takes a while to remove the target path.unlink() elif path.is_dir(): print(f"Note: directory '{path}' is not a mount point.", file=sys.stderr) return def clean(path: Path): if path.is_symlink() and not path.readlink().exists(): print(f"Removing broken symlink '{path}'...") path.unlink() else: if not path.is_mount() and not any(path.iterdir()): print(f"Removing empty mountpoint '{path}'...") path.rmdir() def cleanAll(mountpath): for file in mountpath.iterdir(): path = mountpath / file if path.is_dir(): clean(path) def writeDefaultConfig(): with open(CONFIG, mode="w", encoding="utf-8") as cfile: print( f"""\ # globals live in the DEFAULT section [DEFAULT] mountpath = {DEFAULT_MOUNTPATH} #mntopts = opt1=val1, opt2=val2, ... # optional #[remote_name] #scheme = ... # optional, either sshfs (default) or anything else supported by gvfs #host = ... # optional, equal to remote_name by default #path = ... # optional, sshfs defaults to remote $HOME #user = ... # optional, .ssh/config is honoured #port = ... # optional, .ssh/config is honoured #mntopts = opt1=val1, opt2=val2, ... # optional """, file=cfile, ) if __name__ == "__main__": config = configparser.ConfigParser() if not CONFIG.exists(): writeDefaultConfig() config.read(CONFIG) parser = argparse.ArgumentParser( description="wrapper for sshfs and gio with a config file" ) parser.add_argument( "--list-available", action="store_true", help="list the hosts defined in the configuration file and exit", ) parser.add_argument( "--list-mounted", action="store_true", help="list the currently mounted hosts and exit", ) parser.add_argument( "-u", "--unmount", action="store_true", help="unmount given host or path" ) parser.add_argument( "host", nargs="*", help="remote name(s) specified in the config file" ) args = parser.parse_args() mountpath = Path( os.path.expanduser( config.get("DEFAULT", "mountpath", fallback=DEFAULT_MOUNTPATH) ) ) if args.list_available: hosts = set(key for key in config.keys() if key != "DEFAULT") for host in sorted(hosts): print(host) elif args.list_mounted: for file in sorted(mountpath.iterdir()): print(file.name) else: if args.host: for host in args.host: if args.unmount: if Path(host).is_dir(): # not a host, but a path path = Path(host) else: path = mountpath / host if not path.exists(): print( f"Note: path '{path}' does not exist.", file=sys.stderr ) umount(path) else: if config.has_section(host): if (mountpath / host).is_mount(): parser.error(f"Host '{host}' is already mounted.") mount(host, mountpath, config) else: parser.error( f"Section '{host}' does not exist in the config file." ) else: parser.error("No hosts were given.") cleanAll(mountpath) ================================================ FILE: forcemp3convert.sh ================================================ #! /bin/bash # forcefully convert any file to mp3 (with fixed bitrate), preserving metadata (if possible) set -e for file in "$@"; do tmpfile="$(mktemp -u)-forcemp3convert.mp3" ffmpeg -i "$file" -acodec libmp3lame -ar 44100 -ab 128k -ac 2 -f mp3 -map_metadata 0 -y "$tmpfile" mv "$tmpfile" "${file%\.*}.mp3" done ================================================ FILE: hddtemp.sh ================================================ #!/bin/bash devices="$@" devices=${devices:-/dev/sda} for device in $devices; do cmd="smartctl -d ata -a $device | grep \"Temperature_Celsius\" | awk '{print \$10}'" if [[ $UID != 0 ]]; then echo "Running \`sudo $cmd\`" temp=$(eval "sudo $cmd") else echo "Running \`$cmd\`" temp=$(eval "$cmd") fi echo "Temperature of $device: $temp°C" done ================================================ FILE: imap-notifier.py ================================================ #!/usr/bin/env python3 import asyncio import email.header import email.parser import imaplib import json import logging import os import subprocess import sys from pathlib import Path import jsonschema import yaml logger = logging.getLogger(__name__) # Define the JSON schema for the configuration file config_schema = { "type": "object", "required": ["accounts"], "properties": { "accounts": { "type": "array", "minItems": 1, "items": { "type": "object", "required": [ "username", "hostname", "password_command", ], "properties": { "username": { "type": "string", "minLength": 1, }, "protocol": { "type": "string", "enum": ["imaps", "imap"], }, "hostname": { "type": "string", "format": "hostname", }, "port": { "type": "integer", "minimum": 1, "maximum": 65535, }, "password_command": { "type": "string", "minLength": 1, }, "include_mailboxes": { "type": "array", "items": {"type": "string"}, "minItems": 1, "uniqueItems": True, }, "exclude_mailboxes": { "type": "array", "items": {"type": "string"}, "minItems": 1, "uniqueItems": True, }, }, "additionalProperties": False, "allOf": [ {"not": {"required": ["include_mailboxes", "exclude_mailboxes"]}} ], }, }, "timeout": { "type": "integer", "minimum": 30, "maximum": 3600, }, }, "additionalProperties": False, } def load_config(config_path: Path): """Load configuration from XDG_CONFIG_HOME/imap-notifier.yaml""" try: with open(config_path, "r") as f: config = yaml.safe_load(f) # Validate the configuration against the schema jsonschema.validate(instance=config, schema=config_schema) return config except FileNotFoundError: logger.error(f"Configuration file not found: {config_path}") return None except yaml.YAMLError as e: logger.error(f"Error parsing configuration file: {e}") return None except jsonschema.ValidationError as e: logger.error(f"Invalid configuration: {e}") return None # helper function to decode MIME-encoded headers # https://docs.python.org/3/library/email.header.html#email.header.decode_header def decode_header(header): if header is None: return None parts = email.header.decode_header(header) decoded = "" for s, charset in parts: if isinstance(s, str): # already str - just append decoded += s else: # byte string - needs to be decoded if charset is None: charset = "ascii" decoded += str(s, encoding=charset) return decoded class IMAPNotifier: xdg_config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_path = Path(xdg_config_home) / "imap-notifier.yaml" xdg_state_home = os.environ.get( "XDG_STATE_HOME", os.path.expanduser("~/.local/state") ) state_file_path = Path(xdg_state_home) / "imap-notifier" / "state.json" def __init__(self): self.config = load_config(self.config_path) self.state = {} self.mail_connections = {} self.shutdown_event = asyncio.Event() def load_state(self): """Load last check times from state file""" try: with open(self.state_file_path, "r") as f: self.state = json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {} def save_state(self): """Save last check times to state file""" try: self.state_file_path.parent.mkdir(parents=True, exist_ok=True) with open(self.state_file_path, "w") as f: json.dump(self.state, f) except Exception as e: logger.error(f"Failed to save state: {e}") def get_account_id(self, account_config): """Generate a unique ID for an account""" username = account_config["username"] protocol = account_config.get("protocol", "imaps") hostname = account_config["hostname"] if protocol == "imaps": port = account_config.get("port", 993) else: port = account_config.get("port", 143) return f"{protocol}://{username}@{hostname}:{port}" def get_password(self, account_config): """Get password using the configured command""" if "password_command" in account_config: try: result = subprocess.run( account_config["password_command"], shell=True, capture_output=True, text=True, check=True, ) return result.stdout.strip() except subprocess.CalledProcessError as e: logger.error( f"Failed to get password for {account_config['username']}: {e}" ) return None else: logger.error( f"No password command configured for {account_config['username']}" ) return None def send_notification(self, message): """Send desktop notification for new email""" try: # Extract sender and subject sender = decode_header(message.get("From")) or "[Unknown Sender]" subject = decode_header(message.get("Subject")) or "[No Subject]" subprocess.run( [ "notify-send", "--app-name=EmailNotification", "--expire-time=3000", # duration in ms "--urgency=normal", # critical would be shown forever "--icon=mail-message-new-symbolic", "--category=email.arrived", "Received new email", f"{sender} — {subject}", ], check=True, ) logger.info(f"Notification sent for email from: {sender}") except subprocess.CalledProcessError as e: logger.error(f"Failed to send notification: {e}") except Exception as e: logger.error(f"Unexpected error sending notification: {e}") def is_connection_alive(self, connection, account_id): """Check if IMAP connection is still alive""" try: # Send a NOOP command to test the connection connection.noop() return True except Exception: logger.warning(f"Connection for account {account_id} is not alive") return False async def connect_to_account(self, account_config): """Establish IMAP connection for an account""" protocol = account_config.get("protocol", "imaps") hostname = account_config["hostname"] try: # Create connection based on whether it's secure (imaps) or not if protocol == "imaps": port = account_config.get("port", 993) client = imaplib.IMAP4_SSL(hostname, port) else: port = account_config.get("port", 143) client = imaplib.IMAP4(hostname, port) # Get password username = account_config["username"] password = self.get_password(account_config) if not password: logger.error(f"No password returned for account {username:!r}") return None client.login(username, password) logger.info( f"Connected to {protocol}://{hostname}:{port} as user {username}" ) return client except Exception as e: logger.error(f"Failed to connect to {protocol}://{hostname}: {e}") return None async def get_new_emails(self, connection, account_id, mailboxes_to_process): """Get new emails since last check""" logger.debug(f"Checking {account_id} for new emails") # Get previous unseen emails from the state account_state = self.state.setdefault(account_id, {}) previous_unseen_message_ids = set(account_state.get("unseen_message_ids", [])) unseen_message_ids = set() new_emails = [] # Process each mailbox for mailbox in mailboxes_to_process: try: # Remove old state data # TODO: remove this after some time if mailbox in account_state: del account_state[mailbox] # Select mailbox connection.select(mailbox) # Search for unseen emails status, messages = connection.search(None, "UNSEEN") if status != "OK": logger.error( f"Failed to search emails in mailbox {mailbox} for account {account_id}" ) continue email_ids = messages[0].split() # Process new emails for email_id in email_ids: try: # Fetch the email headers only status, msg_data = connection.fetch(email_id, "(RFC822.HEADER)") if status == "OK": msg = email.parser.Parser().parsestr( msg_data[0][1].decode("utf-8", errors="ignore") ) # Always get a Message-ID, which uniquely identifies the message. # The `email_id` obtained from IMAP is just numeric identifier in the *mailbox*, # not in the whole account. message_id = msg.get("Message-ID") unseen_message_ids.add(message_id) if message_id not in previous_unseen_message_ids: new_emails.append(msg) except Exception as e: logger.error(f"Failed to fetch email {email_id}: {e}") continue except Exception as e: logger.error( f"Error processing mailbox {mailbox} for account {account_id}: {e}" ) # Update IDs of unseen emails in the state account_state["unseen_message_ids"] = sorted(unseen_message_ids) return new_emails async def process_mailboxes(self, account_config, account_id, connection): """Process mailboxes for an account""" # Determine which mailboxes to process include_mailboxes = account_config.get("include_mailboxes", []) exclude_mailboxes = account_config.get("exclude_mailboxes", []) if include_mailboxes and exclude_mailboxes: logger.error( f"Both include_mailboxes and exclude_mailboxes are defined for account " f"{account_id}. Please specify only one of them." ) return # If no mailboxes specified but exclude_mailboxes is defined, # get all mailboxes from server and filter out excluded ones if not include_mailboxes and exclude_mailboxes: try: # Get all mailboxes from server status, mailbox_list = connection.list() all_mailboxes = [] if status == "OK" and mailbox_list: for item in mailbox_list: # Extract mailbox name from LIST response mailbox_name = item.decode().split(' "/" ')[-1].strip('"') all_mailboxes.append(mailbox_name) # Filter out excluded mailboxes mailboxes_to_process = [ mb for mb in all_mailboxes if mb not in exclude_mailboxes ] except Exception as e: logger.error(f"Error retrieving mailboxes from server: {e}") return elif include_mailboxes: # Use configured mailboxes mailboxes_to_process = include_mailboxes else: # Fallback to INBOX mailboxes_to_process = ["INBOX"] # Get new emails emails = await self.get_new_emails(connection, account_id, mailboxes_to_process) # Send notifications for new emails for message in emails: self.send_notification(message) async def process_account(self, account_config): """Process a single account""" # Generate a unique ID for the account account_id = self.get_account_id(account_config) # Check if there's an existing connection for this account connection = self.mail_connections.get(account_id) try: # If no connection exists or it's closed, create a new one if not connection or not self.is_connection_alive(connection, account_id): connection = await self.connect_to_account(account_config) if not connection: return self.mail_connections[account_id] = connection # Process mailboxes for this account await self.process_mailboxes(account_config, account_id, connection) logger.debug(f"Finished processing account {account_id}") except Exception as e: logger.error(f"Error processing account {account_id}: {e}") # Remove failed connection from cache if account_id in self.mail_connections: del self.mail_connections[account_id] async def run(self): """Run the notifier""" if not self.config: return False timeout = int(self.config.get("timeout", 60)) logger.info(f"Starting mail notifier with timeout {timeout} seconds") while not self.shutdown_event.is_set(): # Process all accounts concurrently async with asyncio.TaskGroup() as tg: for account in self.config["accounts"]: tg.create_task(self.process_account(account)) # Save state after each cycle self.save_state() # Wait before next check await asyncio.sleep(timeout) async def main_async(): """Async main function""" # Create notifier notifier = IMAPNotifier() # Load existing state notifier.load_state() result = True try: # Run the notifier result = await notifier.run() except KeyboardInterrupt: logger.info("Interrupted by user") finally: # Save final state notifier.save_state() logger.info("Notifier stopped") if result is False: sys.exit(1) def main(): """Main function""" # Create event loop and run async main asyncio.run(main_async()) if __name__ == "__main__": # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) main() ================================================ FILE: img2pdf.sh ================================================ #!/bin/bash set -e outfile=$1 ext=tif echo "Converting images to pdf..." declare -a pages # take input pattern "anything_number.ext", sort numerically by "number" for file in $(ls ./*.$ext | sort -t_ -k2,2n); do echo " $file" pdf=$(basename "$file" .$ext).pdf # convert "$file" "$pdf" tiff2pdf -z -F -x 300 -y 300 -o "$pdf" "$file" pages+=("$pdf") done echo "Merging into one pdf..." stapler sel "${pages[@]}" "$outfile" ================================================ FILE: initscreen.sh ================================================ #! /bin/bash # exit on error set -e #hdmi=$(cat /sys/class/drm/card0-HDMI-A-1/status) #vga=$(cat /sys/class/drm/card0-VGA-1/status) if [[ "$WAYLAND_DISPLAY" == "" ]]; then connected=$(xrandr | grep " connected" | sed -e "s/\([A-Z0-9]\+\) connected.*/\1/") else connected=$(swaymsg -pt get_outputs | grep -E "^Output" | awk '{print $2}') fi #echo "initscreen.sh: hdmi $hdmi; vga $vga" if [[ $connected =~ "LVDS-0" ]]; then if [[ $connected =~ "HDMI-0" ]]; then # hdmi only # NOTE: i3 fails if no active output is detected, so we have to first enable second output and then disable the first # xrandr --nograb --output HDMI-0 --auto --primary # xrandr --nograb --output LVDS-0 --off # both # HDMI-0 is primary, LVDS-0 is panned to be vertically aligned to the bottom # xrandr --nograb --output HDMI-0 --auto --primary --output LVDS-0 --auto --left-of HDMI-0 --panning 1366x768+0+312 xrandr --output HDMI-0 --auto --primary --output LVDS-0 --auto --left-of HDMI-0 --panning 1366x768+0+312 # xrandr --output HDMI-0 --auto --primary --output LVDS-0 --auto --right-of HDMI-0 elif [[ $connected =~ "VGA-0" ]]; then # xrandr --nograb --output VGA-0 --auto --output LVDS-0 --mode 1024x768 --primary # TODO: look at --scale argument xrandr --output VGA-0 --auto --primary --output LVDS-0 --auto --below VGA-0 else # xrandr --nograb --output LVDS-0 --auto --primary --output HDMI-0 --off # xrandr --output LVDS-0 --auto --primary --output HDMI-0 --off xrandr --output LVDS-0 --auto --primary --output HDMI-0 --off --output VGA-0 --off fi elif [[ $connected =~ "eDP-1" ]]; then if [[ -f /proc/acpi/button/lid/LID/state ]]; then lid=$(cat /proc/acpi/button/lid/LID/state | awk '{print $2}') else lid="open" fi if [[ "$WAYLAND_DISPLAY" == "" ]]; then if [[ $connected =~ "HDMI-1" ]] && [[ "$lid" == "closed" ]]; then xrandr --output HDMI-1 --auto --primary --output eDP-1 --off echo "Xft.dpi: 96" | xrdb -merge elif [[ $connected =~ "HDMI-1" ]]; then xrandr --output HDMI-1 --auto --primary --output eDP-1 --auto --left-of HDMI-1 else xrandr --output eDP-1 --auto --primary --output HDMI-1 --off echo "Xft.dpi: 168" | xrdb -merge # scale=1.75 fi else if [[ $connected =~ "HDMI-A-1" ]] && [[ "$lid" == "closed" ]]; then swaymsg output HDMI-A-1 enable swaymsg output eDP-1 disable elif [[ $connected =~ "HDMI-A-1" ]]; then swaymsg output HDMI-A-1 enable swaymsg output eDP-1 enable else swaymsg output eDP-1 enable swaymsg output HDMI-A-1 disable fi fi else first=$(echo $connected | cut -f1 -d' ') xrandr --output ${first} --auto --primary fi ================================================ FILE: maildir-strip-attachments.py ================================================ #!/usr/bin/env python3 # Documentation: # - https://docs.python.org/3/library/mailbox.html#mailbox.Maildir # - https://docs.python.org/3/library/mailbox.html#mailbox.MaildirMessage import os import argparse import mailbox DROP_MIN_SIZE = 256 # KiB DROP_CONTENT_TYPES = [ "image/", "video/", "application/pdf", "application/x-extension-pdf", "application/zip", "application/gzip", "application/x-gzip", "application/x-xz", "application/x-7z-compressed", "application/x-zip-compressed", "application/x-rar-compressed", "application/x-msdownload", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.ms-xpsdocument", "application/octet-stream", ] def process_maildir(maildir): dropped_items = 0 dropped_size = 0 mb = mailbox.Maildir(maildir, create=False) for key, message in mb.iteritems(): for part in message.walk(): if part.is_multipart(): continue size = len(part.as_bytes()) / 1024 if size > DROP_MIN_SIZE: print("{}\tsize: {:g} KiB".format(part.get_content_type(), size)) for ct in DROP_CONTENT_TYPES: if part.get_content_type().startswith(ct): part.set_payload("") dropped_items += 1 dropped_size += size # update the message on disk mb.update({key: message}) print("Dropped {} attachements ({:g} MiB).".format(dropped_items, dropped_size / 1024)) def argtype_dir_path(string): if os.path.isdir(string): return string raise NotADirectoryError(string) def argtype_maildir(string): string = argtype_dir_path(string) for sub in ["cur", "new", "tmp"]: subdir = os.path.join(string, sub) if not os.path.isdir(subdir): raise NotADirectoryError(subdir) return string if __name__ == "__main__": ap = argparse.ArgumentParser(description="Strip attachments from messages in a maildir.") ap.add_argument("maildir", metavar="PATH", type=argtype_maildir, help="path to the maildir") args = ap.parse_args() process_maildir(args.maildir) ================================================ FILE: makeissue.sh ================================================ echo -e '\e[H\e[2J' > issue echo -e ' \e[1;30m| \e[34m\\s \\r' >> issue echo -e ' \e[36;1m/\\\\ \e[37m|| \e[36m| | \e[30m|' >> issue echo -e ' \e[36m/ \\\\ \e[37m|| \e[36m| _ \e[30m| \e[32m\\t' >> issue echo -e ' \e[1;36m/ \e[0;36m.. \e[1m\\\\ \e[37m//==\\\\\\\\ ||/= /==\\\\ ||/=\\\\ \e[36m| | |/ \\\\ | | \\\\ / \e[30m| \e[32m\\d' >> issue echo -e ' \e[0;36m/ . . \\\\ \e[37m|| || || | || || \e[36m| | | | | | X \e[1;30m|' >> issue echo -e ' \e[0;36m/ . . \\\\ \e[37m\\\\\\\\==/| || \\\\==/ || || \e[36m| | | | \\\\_/| / \\\\ \e[1;30m| \e[31m\\U' >> issue echo -e ' \e[0;36m/ .. .. \\\\ \e[0;37mA simple, lightweight linux distribution. \e[1;30m|' >> issue echo -e ' \e[0;36m/_\x27 `_\\\\ \e[1;30m| \e[35m\\l \e[0mon \e[1;33m\\n' >> issue echo -e ' \e[0m' >> issue echo -e '' >> issue ================================================ FILE: mp3convert.py ================================================ #! /usr/bin/env python3 import sys import os import argparse import asyncio from concurrent.futures import ThreadPoolExecutor import re import shutil import subprocess import shlex from pythonscripts.cpu import cores_count from pythonscripts.tempfiles import TempFiles from pythonscripts.ffparser import FFprobeParser audio_types = ("mp3", "aac", "ac3", "mp2", "wma", "wav", "mka", "m4a", "ogg", "oga", "flac") audio_file_regex = re.compile("^(?P/(.*/)*)(?P.*(?P\.(" + "|".join(audio_types) + ")))$") ffmpeg_command = "ffmpeg -i {input} -acodec libmp3lame -ar 44100 -ab {bitrate:d}k -ac 2 -f mp3 -map_metadata 0 -y {output}" class GettingBitrateError(Exception): def __init__(self, fname): self.message = "Couldn't get bitrate from file " + fname class ConversionError(Exception): def __init__(self, fname, status, output): self.message = "Error while converting file " + fname + "\nffmpeg exited with status " + str(status) + "\n" + output def get_bitrate(filename): parser = FFprobeParser(filename) bitrate = parser.get("audio", "bit_rate") del parser if bitrate is None: raise GettingBitrateError(filename) else: return bitrate // 1000 def convert(filename, output_extension, bitrate, delete_after=False): tmpfile = tmp.getTempFileName() command = ffmpeg_command.format(input=shlex.quote(filename), bitrate=bitrate, output=shlex.quote(tmpfile)) try: subprocess.run(command, shell=True, check=True, capture_output=True) if delete_after: os.remove(filename) shutil.move(tmpfile, os.path.splitext(filename)[0] + output_extension) tmp.remove(tmpfile) except subprocess.CalledProcessError as e: tmp.remove(tmpfile) raise ConversionError(filename, e.returncode, e.output) class Main(): def __init__(self, args): self.countAudioFiles = 0 self.countHigherBitrate = 0 self.countDifferentFormat = 0 self.countErrors = 0 self.countNonAudioFiles = 0 self.dry_run = args.dry_run self.bitrate = args.bitrate self.verbose = args.verbose self.recursive = args.recursive self.deleteAfter = args.delete_after self.outputExtension = "." + args.output_extension self.paths = args.path def print_stats(self): print() print("-----------collected statistics-----------") print("All audio files (without errors): % 6d" % self.countAudioFiles) print("Converted files: % 6d" % (self.countDifferentFormat + self.countHigherBitrate)) print(" - different format: % 6d" % self.countDifferentFormat) print(" - %3s but higher bitrate: % 6d" % (self.outputExtension[1:], self.countHigherBitrate)) print("Errors: % 6d" % self.countErrors) print("Non-audio files: % 6d" % self.countNonAudioFiles) print("------------------------------------------") def check(self, path): match = re.match(audio_file_regex, path) if not match: self.countNonAudioFiles += 1 return False filename = match.group("filename") ext = match.group("extension") self.countAudioFiles += 1 if ext != self.outputExtension: self.countDifferentFormat += 1 return True bitrate = get_bitrate(path) if self.verbose > 0: sys.stdout.write("% 3s kb/s: %s\n" % (bitrate, filename)) if bitrate > self.bitrate: self.countHigherBitrate += 1 return True return False async def run(self): # We could use the default single-threaded executor with basically the same performance # (because of Python's GIL), but the ThreadPoolExecutor allows to limit the maximum number # of workers and thus the maximum number of concurrent subprocesses. with ThreadPoolExecutor(max_workers=cores_count()) as executor: loop = asyncio.get_event_loop() tasks = [ loop.run_in_executor(executor, self.worker, path) for path in self.queue_generator() ] for result in await asyncio.gather(*tasks): pass self.print_stats() def worker(self, path): path = os.path.abspath(path) try: # check bitrate/filetype etc., skip if conversion not necessary if not self.check(path) or self.dry_run: return print("Converting: {}".format(path)) convert(path, self.outputExtension, self.bitrate, self.deleteAfter) except ConversionError as e: msg = "ERROR: failed to convert file '{}'".format(path) if self.verbose > 0: msg += "\n" + e.message print(msg, file=sys.stderr) self.countErrors += 1 except GettingBitrateError as e: msg = "ERROR: failed to get bitrate from file '{}'".format(path) if self.verbose > 0: msg += "\n" + e.message print(msg, file=sys.stderr) self.countErrors += 1 else: print("Done: {}".format(path)) def queue_generator(self): """ For each directory in self.files returns generator returning full paths to mp3 files in that folder. If self.files contains file paths instead of directory, it's returned as [file]. """ def walk(root): dirs = [] files = [] for entry in os.scandir(root): if entry.is_dir(): dirs.append(entry.name) elif entry.is_file(): files.append(entry.name) # first yield found files, then recurse into subdirs for f in files: yield os.path.join(root, f) if self.recursive: for d in dirs: # recurse into subdir for f in walk(os.path.join(root, d)): yield f for path in self.paths: if os.path.isdir(path): for f in walk(path): yield f else: yield path if __name__ == "__main__": parser = argparse.ArgumentParser(description="convert all audio files in given folder (recursively) to specified bitrate, skip if bitrate is less or equal") parser.add_argument("path", action="store", nargs="+", help="path to file(s) to convert - filename or directory") parser.add_argument("-r", "--recursive", action="store_true", help="browse folders recursively") parser.add_argument("--dry-run", action="store_true", help="don't convert, only print stats") parser.add_argument("-b", "--bitrate", action="store", type=int, metavar="BITRATE", default="128", help="set bitrate - in kb/s, default=128") parser.add_argument("-v", "--verbose", action="count", default=0, help="set verbosity level") parser.add_argument("--delete-after", action="store_true", help="delete old files after conversion") parser.add_argument("--output-extension", choices=audio_types, type=str, default="mp3", help="set output extension") args = parser.parse_args() tmp = TempFiles() main = Main(args) asyncio.run(main.run()) ================================================ FILE: nat-launch-subnet.sh ================================================ #!/bin/bash function print_launch_subnet_usage() { echo "USAGE" echo " $0 " cat <<'CONFIG' REQUIRED VARIABLES # The network interface card (NIC) that is connected to the internet or other # wide area network. wan_nic="wlan0" # The network interface card connected to the subnet. subnet_nic="eth0" # The subnet IP mask. mask=/24 # The subnet IP range. subnet_ip=10.0.0.0$mask # The IP of the subnet NIC on the subnet. server_ip=10.0.0.100$mask # The IP tables binary to use. iptables=/usr/bin/idemptables # The dnsmasq arguments - PID and lease files to use. dnsmasq_pid=/tmp/dhcpd.pid dnsmasq_lease=/tmp/dhcpd.lease # The port of DNS service, see dnsmasq(8) for details. Specify "0" to disable DNS server. dnsmasq_port=53 # The DHCP range, see dnsmasq(8) for details. dnsmasq_dhcp_range="192.168.1.100,192.168.1.200,12h" OPTIONAL VARIABLES # Function or external scripts to run before before and after bringing the # subnet NIC up or down: pre_up, post_up, pre_down, post_down # pre_up as a function: # function pre_up() # { # } # pre_up as a script: # pre_up=/path/to/script # ip_forward=0 # The value of /proc/sys/net/ipv4/ip_forward to restore when shutting down # the subnet. CONFIG } function launch_subnet() { set -e if [[ -z $1 ]] then print_launch_subnet_usage exit 1 else action="$1" fi if [[ -z $wan_nic ]] then echo "wan_nic is undefined" exit 1 fi if [[ -z $subnet_nic ]] then echo "subnet_nic is undefined" exit 1 fi if [[ -z $mask ]] then echo "mask is undefined" exit 1 fi if [[ -z $subnet_ip ]] then echo "subnet_ip is undefined" exit 1 fi if [[ -z $server_ip ]] then echo "server_ip is undefined" exit 1 fi if [[ -z $iptables ]] then echo "iptables is undefined" exit 1 fi if [[ -z $dnsmasq_pid ]] then echo "dnsmasq_pid is undefined" exit 1 fi if [[ -z $dnsmasq_lease ]] then echo "dnsmasq_lease is undefined" exit 1 fi if [[ -z $dnsmasq_port ]] then echo "dnsmasq_port is undefined" exit 1 fi if [[ -z $dnsmasq_dhcp_range ]] then echo "dnsmasq_dhcp_range is undefined" exit 1 fi case "$action" in up) # Enable IP forwarding. echo 1 > /proc/sys/net/ipv4/ip_forward ## iptables rules are changed to fit my firewall config ## see http://xyne.archlinux.ca/notes/network/dhcp_with_dns.html for original rules # Open up DNS (53) and DHCP (67) ports on subnet_nic. "$iptables" -A nat-subnet -i "$subnet_nic" -s "$subnet_ip" -p tcp --dport 53 -j ACCEPT "$iptables" -A nat-subnet -i "$subnet_nic" -s "$subnet_ip" -p udp --dport 53 -j ACCEPT "$iptables" -A nat-subnet -i "$subnet_nic" -p udp --dport 67 -j ACCEPT # Reply to ICMP (ping) packets so clients can check their connections. "$iptables" -A nat-subnet -i "$subnet_nic" -p icmp --icmp-type echo-request -j ACCEPT #"$iptables" -A OUTPUT -i "$subnet_nic" -p icmp --icmp-type echo-reply -j ACCEPT # Allow postrouting to wan_nic (for e.g. internet access on the subnet). "$iptables" -t nat -A POSTROUTING -s "$subnet_ip" -o "$wan_nic" -j MASQUERADE # Enable forwarding from subnet_nic to wan_nic (and back via related and established connections). "$iptables" -A FORWARD -i "$subnet_nic" -s "$subnet_ip" -o "$wan_nic" -j ACCEPT "$iptables" -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # Bring down subnet_nic, configure it and bring it up again. if [[ ! -z $pre_up ]] then ip link set dev "$subnet_nic" down "$pre_up" fi ip link set dev "$subnet_nic" up if [[ ! -z $post_up ]] then "$post_up" fi # Set the static IP for subnet_nic. ip addr add "$server_ip" dev "$subnet_nic" # Ensure the lease file exists. mkdir -p -- "${dnsmasq_lease%/*}" [[ -f $dnsmasq_lease ]] || touch "$dnsmasq_lease" # Launch the DHCP server dnsmasq \ --pid-file="$dnsmasq_pid" \ --dhcp-leasefile="$dnsmasq_lease" \ --port="$dnsmasq_port" \ --interface="$subnet_nic" \ --except-interface=lo \ --bind-interfaces \ --dhcp-range="$dnsmasq_dhcp_range" \ --dhcp-authoritative \ --dhcp-option=6,"${server_ip%/*}" ;; down) # Kill the DHCP server. if [[ -f $dnsmasq_pid ]] then kill $(cat "$dnsmasq_pid") && rm "$dnsmasq_pid" && echo "killed server" fi if [[ ! -z $pre_down ]] then "$pre_down" fi ip addr delete "$server_ip" dev "$subnet_nic" ip link set dev "$subnet_nic" down if [[ ! -z $post_down ]] then "$post_down" fi # Undo all of the changes above in reverse order. "$iptables" -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT "$iptables" -D FORWARD -i "$subnet_nic" -s "$subnet_ip" -o "$wan_nic" -j ACCEPT "$iptables" -t nat -D POSTROUTING -s "$subnet_ip" -o "$wan_nic" -j MASQUERADE #"$iptables" -D OUTPUT -i "$subnet_nic" -p icmp --icmp-type echo-reply -j ACCEPT "$iptables" -D nat-subnet -i "$subnet_nic" -p icmp --icmp-type echo-request -j ACCEPT "$iptables" -D nat-subnet -i "$subnet_nic" -p udp --dport 67 -j ACCEPT "$iptables" -D nat-subnet -i "$subnet_nic" -s "$subnet_ip" -p udp --dport 53 -j ACCEPT "$iptables" -D nat-subnet -i "$subnet_nic" -s "$subnet_ip" -p tcp --dport 53 -j ACCEPT if [[ ! -z $ip_forward ]] then if [[ $ip_forward != $(cat /proc/sys/net/ipv4/ip_forward) ]] then echo $ip_forward > /proc/sys/net/ipv4/ip_forward fi else echo 0 > /proc/sys/net/ipv4/ip_forward fi ;; *) print_launch_subnet_usage exit 1 ;; esac } ================================================ FILE: nat-launch.sh ================================================ #!/bin/bash # Original author: Xyne # http://xyne.archlinux.ca/notes/network/dhcp_with_dns.html function print_usage() { echo "usage: $0 " } if [[ $EUID -ne 0 ]]; then echo "This script must be run as root." >&2 exit 1 fi if [[ -z $3 ]]; then print_usage exit 1 else wan_nic="$1" subnet_nic="$2" action="$3" fi mask=/24 subnet_ip=192.168.1.0$mask server_ip=192.168.1.23$mask iptables=/usr/bin/idemptables dnsmasq_pid=/run/dnsmasq_$subnet_nic.pid dnsmasq_lease=/run/dnsmasq_$subnet_nic.lease dnsmasq_port=0 dnsmasq_dhcp_range="192.168.1.100,192.168.1.150,6h" source nat-launch-subnet.sh launch_subnet "$action" ================================================ FILE: notify-brightness.sh ================================================ #! /bin/bash # LCD brightness notification (level changed by ACPI, no action required) # duration in ms duration=1500 # get brightness level, set title level=$(cat "/sys/class/backlight/intel_backlight/brightness") max=$(cat "/sys/class/backlight/intel_backlight/max_brightness") percent=$(( $level * 100 / $max )) title="LCD brightness" # create fancy bar f=$((percent/10)) e=$((10-f)) fchars='◼◼◼◼◼◼◼◼◼◼' echars='◻◻◻◻◻◻◻◻◻◻' bar="${fchars:0:f}${echars:0:e} $percent%" notify-send --app-name=VolumeNotification --expire-time="$duration" --urgency=low --transient "$title" "$bar" ================================================ FILE: notify-volume.sh ================================================ #!/bin/bash # volume control (up/down/mute/unmute/toggle) + notification # duration in ms duration=1500 notify () { # get volume level percent=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -Po '\d+(?=%)' | head -n 1) # check if muted, set title if [[ $(pactl get-sink-mute @DEFAULT_SINK@) == "Mute: yes" ]]; then title="Volume muted" else title="Volume" fi # create fancy bar f=$((percent/10)) e=$((10-f)) fchars='◼◼◼◼◼◼◼◼◼◼' echars='◻◻◻◻◻◻◻◻◻◻' bar="${fchars:0:f}${echars:0:e} $percent%" notify-send --app-name=VolumeNotification --category=device --expire-time="$duration" --urgency=low --transient "$title" "$bar" } # redirect stdout of this script to /dev/null exec > /dev/null case "$1" in up) pactl set-sink-volume @DEFAULT_SINK@ +5% pactl set-sink-mute @DEFAULT_SINK@ 0 ;; down) pactl set-sink-volume @DEFAULT_SINK@ -5% pactl set-sink-mute @DEFAULT_SINK@ 0 ;; mute) pactl set-sink-mute @DEFAULT_SINK@ 1 ;; unmute) pactl set-sink-mute @DEFAULT_SINK@ 0 ;; toggle) pactl set-sink-mute @DEFAULT_SINK@ toggle ;; esac notify ================================================ FILE: pacman-disowned.sh ================================================ #!/bin/sh tmp=${TMPDIR-/tmp}/pacman-disowned-$UID-$$ db=$tmp/db fs=$tmp/fs mkdir "$tmp" trap 'rm -rf "$tmp"' EXIT pacman -Qlq | sort -u > "$db" find /etc /opt /usr ! -name lost+found \( -type d -printf '%p/\n' -o -print \) | sort > "$fs" comm -23 "$fs" "$db" ================================================ FILE: pdf-extract.sh ================================================ #!/bin/bash # exit on error set -e any2img() { convert -density 150 "$1" -quality 100 "$2" &>/dev/null } pdf2img() { echo "Splitting single pdf file by pages (tiff)" stapler burst "$1" base=${1%.*} for i in "${base}_"*.pdf do out=pg${i#"$base"} # will result in 'pg_123.pdf' out=${out%.*}.tiff # replace extension echo "$out" # any2img "$i" "$out" convert -density 300 "$i" -compress lzw "$out" rm -f "$i" done } djvu2img() { echo "Splitting single djvu file by pages (tiff)" pages=`djvused -e "n" "$1"` for (( i=1; i<=$pages; i++ )) do num=$(printf "%03d" "$i") out="pg_$num.tiff" echo " $out" ddjvu -page=$i -format=tiff "$1" "$out" done } path=$(realpath "$1") filename=$(basename "$path") extension=${filename##*.} basename=${filename%.*} # filename without extension # create directory for extracted images mkdir -p "$basename" cp "$path" "$basename" cd "$basename" if [[ "$extension" == "pdf" ]]; then pdf2img "$filename" rm -f "$filename" elif [[ "$extension" == "djvu" ]]; then djvu2img "$filename" rm -f "$filename" else echo "Supported file types: pdf, djvu" exit 1 fi ================================================ FILE: perm.sh ================================================ #!/bin/bash opt=${1:-'-h'} dir=${2:-'.'} fmode=0644 dmode=0755 case "$1" in -a) # dirs and files find "$2" -type d -exec chmod $dmode "{}" + find "$2" -type f -exec chmod $fmode "{}" + ;; -d) find "$2" -type d -exec chmod $dmode "{}" + ;; -f) find "$2" -type f -exec chmod $fmode "{}" + ;; *) printf "Usage: $(basename $0) option [directory] -a \t set permissions of files and directories to $fmode, resp. $dmode. -d \t set permissions of directories to $dmode. -f \t set permissions of files to $fmode. -h \t print this help. " ;; esac ================================================ FILE: pythonscripts/__init__.py ================================================ #!/usr/bin/env python import os import sys # hack - enable importing from _this_ directory sys.path.append(os.path.dirname(__file__)) from misc import * from tempfiles import * from terminal import * ================================================ FILE: pythonscripts/cpu.py ================================================ #! /usr/bin/env python3 def cores_count(): f = open("/proc/cpuinfo") for line in f.readlines(): if line.startswith("cpu cores"): try: _, n = line.split(":") return int(n.strip()) except ValueError: continue return 1 ================================================ FILE: pythonscripts/daemon.py ================================================ #! /usr/bin/env python import os def spawnDaemon(*args, detach_fds=True): """Spawn a completely detached subprocess (i.e., a daemon). E.g. for mark: spawnDaemon("../bin/producenotify.py", "producenotify.py", "xx") """ if len(args) == 0: raise ValueError("no arguments supplied") # fork the first time (to make a non-session-leader child process) try: pid = os.fork() except OSError as e: raise RuntimeError("1st fork failed: %s [%d]" % (e.strerror, e.errno)) if pid != 0: # parent (calling) process is all done return # detach from controlling terminal (to make child a session-leader) os.setsid() try: pid = os.fork() except OSError as e: raise RuntimeError("2nd fork failed: %s [%d]" % (e.strerror, e.errno)) raise Exception("%s [%d]" % (e.strerror, e.errno)) if pid != 0: # child process is all done os._exit(0) if detach_fds: # grandchild process now non-session-leader, detached from parent # grandchild process must now close all open files try: maxfd = os.sysconf("SC_OPEN_MAX") except (AttributeError, ValueError): maxfd = 1024 for fd in range(maxfd): try: os.close(fd) except OSError: # ERROR, fd wasn't open to begin with (ignored) pass # redirect stdin, stdout and stderr to /dev/null os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) os.dup2(0, 1) os.dup2(0, 2) # and finally let's execute the executable for the daemon! try: os.execvp(args[0], args) except Exception as e: # oops, we're cut off from the world, let's just give up os._exit(255) ================================================ FILE: pythonscripts/ffparser.py ================================================ #!/usr/bin/env python import json import subprocess import shlex from pprint import pprint ffprobe = "ffprobe -v quiet -print_format json -show_format -show_streams " class FFprobeParser: def __init__(self, path): self.data = json.loads(subprocess.check_output(ffprobe + shlex.quote(path), shell=True, universal_newlines=True)) self.format = self.data["format"] self.audio = None self.video = None for stream in self.data["streams"]: if self.audio is None and stream["codec_type"] == "audio": self.audio = stream if self.video is None and stream["codec_type"] == "video": self.video = stream def _get(self, option, attribute): return getattr(self, option)[attribute] def _getBitrate(self, option): if option == "audio": try: return int(self._get("audio", "bit_rate")) except: return int(self._getBitrate("format")) - int(self._getBitrate("video")) elif option == "video": try: return int(self._get("video", "bit_rate")) except: return int(self._getBitrate("format")) - int(self._getBitrate("audio")) elif option == "format": try: return int(self._get("format", "bit_rate")) except: return None def get(self, option, attribute): """ 'option' is one of "audio", "video", "format" 'attribute' is the json attribute to query """ if attribute == "bit_rate": return self._getBitrate(option) else: try: return self._get(option, attribute) except: return None def pprint(self, option): """ 'option' is one of "audio", "video", "format", otherwise 'self.data' is printed """ pprint(getattr(self, option, self.data)) ================================================ FILE: pythonscripts/logger.py ================================================ #! /usr/bin/env python """ Simple logger object. Log level is integer for easy comparison. """ import sys class Logger: def __init__(self, log_level, prog_name): self.log_level = log_level self.prog_name = prog_name self.filename = None def prefix(self, msg): if self.filename is None: return msg return "%s: %s" % (self.filename, msg) def debug(self, msg): if self.log_level >= 4: print(self.prefix(msg)) def info(self, msg): if self.log_level >= 3: print(self.prefix(msg)) def warning(self, msg): if self.log_level >= 2: print(self.prefix("WARNING: %s" % msg)) def error(self, msg): if self.log_level >= 1: sys.stderr.write("%s: %s\n" % (self.prog_name, msg)) def critical(self, msg, retval=1): self.error(msg) sys.exit(retval) ================================================ FILE: pythonscripts/misc.py ================================================ #! /usr/bin/env python """ Human-readable file size. Algorithm does not use a for-loop. It has constant complexity, O(1), and is in theory more efficient than algorithms using a for-loop. Original source code from: http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size """ from math import log unit_list = { "long": list(zip(['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'], [0, 0, 1, 2, 2, 2])), "short": list(zip(['B', 'K', 'M', 'G', 'T', 'P'], [0, 0, 1, 2, 2, 2])), } def format_sizeof(num, unit_format="long"): if num > 1: exponent = min(int(log(num, 1024)), len(unit_list[unit_format]) - 1) quotient = float(num) / 1024**exponent unit, num_decimals = unit_list[unit_format][exponent] format_string = '{:.%sf} {}' % (num_decimals) return format_string.format(quotient, unit) else: return str(int(num)) + " B" """ Nice time format, useful for ETA etc. Output is never longer than 6 characters. """ def format_time(seconds): w, s = divmod(seconds, 3600*24*7) d, s = divmod(s, 3600*24) h, s = divmod(s, 3600) m, s = divmod(s, 60) if w > 0: return "%dw" % w if d > 0: return "%dd%02dh" % (d, h) if h > 0: return "%02dh%02dm" % (h, m) if m > 0: return "%02dm%02ds" % (m, s) return str(s) """ Get content of any readable text file. """ def cat(fname): try: f = open(fname, "r") s = f.read() f.close() return s.strip() except: return None """ Returns a string of at most `max_length` characters, cutting only at word-boundaries. If the string was truncated, `suffix` will be appended. """ import re def smart_truncate(text, max_length=100, suffix='...'): if len(text) > max_length: pattern = r'^(.{0,%d}\S)\s.*' % (max_length-len(suffix)-1) return re.sub(pattern, r'\1' + suffix, text) else: return text """ Recursive directory creation function (like 'mkdir -p' in linux). """ import os def mkdir(path): try: os.makedirs(path) except OSError as e: if e.errno != 17: raise e ================================================ FILE: pythonscripts/tempfiles.py ================================================ #! /usr/bin/env python """ Create temporary file, close file descriptor and return full path of the file. """ import os import tempfile import atexit class TempFiles: def __init__(self): self.tempFiles = [] atexit.register(self.removeAll) def removeAll(self): for file in self.tempFiles[:]: self.remove(file) def remove(self, file): if file in self.tempFiles and os.path.exists(file): os.remove(file) self.tempFiles.remove(file) def getTempFileName(self, prefix="tmp", suffix="", dir=None, text=False): fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix, dir=dir, text=text) os.close(fd) self.tempFiles.append(path) return path ================================================ FILE: pythonscripts/terminal.py ================================================ #! /usr/bin/env python """ Linux terminal colors. """ #import sys COLORS = {"black":30, "red":31, "green":32, "yellow":33, "blue":34, "magenta":35, "cyan":36, "white":37} def colorize(color, text): c = COLORS[color] return "\033[1;%im%s\033[0m" % (c, text) # if sys.stdout.isatty(): # c = COLORS[color] # return "\033[1;%im%s\033[0m" % (c, text) # else: # return text def getColor(status, download_speed=0): if status == "error": return "red" elif status == "active": if download_speed > 0: return "blue" else: return "yellow" elif status == "complete": return "green" elif status == "paused": return "cyan" elif status == "waiting": return "magenta" else: return "" """ Get size of unix terminal as tuple (width, height). When all fails, default value is (80, 25). Original source code from: http://stackoverflow.com/a/566752 """ def getTerminalSize(): import os env = os.environ def ioctl_GWINSZ(fd): try: import fcntl, termios, struct, os cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) except: return None return cr cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except: pass if not cr: try: cr = (env['LINES'], env['COLUMNS']) except: cr = (25, 80) return int(cr[1]), int(cr[0]) ================================================ FILE: qemu-launcher.sh ================================================ #! /usr/bin/bash # Author: Jakub Klinkovský (Lahwaacz) # https://github.com/lahwaacz function print_usage() { echo "usage: $0 " } ## Generate name of TAP interface to create function get_tap_name() { for (( i=0; i<$tap_limit; i++ )); do local name="tap$i" if [[ ! -d "/sys/class/net/$name" ]]; then echo "$name" break fi done } # do not run as root if [[ $EUID -eq 0 ]]; then echo "This script is not supposed to be run as root." >&2 exit 1 fi # parse command line arguments if [[ -z $1 ]]; then print_usage exit 1 else vm_name="$1" fi sudo_args=("-Ap" "Enter your root password (QEMU launcher script)") username=$(whoami) tap_limit=10 # maximum number of TAP interfaces created by this script tap_nic=$(get_tap_name) br_nic="br0-qemu" # bridge interface name (will be created) wan_nic="wlan0" # WAN interface name (for NAT) case "$vm_name" in btrfs) sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" up qemu-system-x86_64 \ -name "$vm_name" \ -monitor stdio \ -enable-kvm -smp 2 -cpu host -m 1024 \ -vga qxl -spice port=5931,disable-ticketing \ -drive file="/home/lahwaacz/virtual_machines/archlinux-btrfs.raw",if=virtio,cache=none -boot once=c \ -net nic,model=virtio,macaddr=$(qemu-mac-hasher.py "$vm_name") -net tap,ifname="$tap_nic",script=no,downscript=no,vhost=on \ -usbdevice tablet sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" down ;; virtarch) sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" up qemu-system-x86_64 \ -name "$vm_name" \ -monitor stdio \ -enable-kvm -smp 2 -cpu host -m 1024 \ -vga qxl -spice port=5931,disable-ticketing \ -drive file="/home/lahwaacz/virtual_machines/archlinux.raw",if=virtio,cache=none -boot once=c \ -net nic,model=virtio,macaddr=$(qemu-mac-hasher.py "$vm_name") -net tap,ifname="$tap_nic",script=no,downscript=no,vhost=on \ -usbdevice tablet sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" down ;; winxp) sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" up qemu-system-i386 \ -name "$vm_name" \ -monitor stdio \ -enable-kvm -smp 2 -cpu host -m 1024 \ -vga qxl -spice port=5930,disable-ticketing \ -drive file="/home/lahwaacz/virtual_machines/winxp.raw",if=virtio,cache=none -boot order=c \ -net nic,model=virtio,macaddr=$(qemu-mac-hasher.py "$vm_name") -net tap,ifname="$tap_nic",script=no,downscript=no,vhost=on \ -usbdevice tablet \ -soundhw ac97 \ -localtime sudo "${sudo_args[@]}" qemu-tap-helper.sh "$username" "$tap_nic" "$br_nic" "$wan_nic" down ;; liveiso) if [[ -z "$2" ]]; then echo "You must specify the ISO file as a second argument." >&2 exit 1 fi qemu-system-x86_64 \ -name "$vm_name" \ -monitor stdio \ -enable-kvm -smp 2 -cpu host -m 1024 \ -vga virtio \ -display gtk,gl=on \ -drive file="$2",if=virtio,media=cdrom -boot once=d \ -net nic -net user \ -usbdevice tablet ;; liveiso-efi) if [[ -z "$2" ]]; then echo "You must specify the ISO file as a second argument." >&2 exit 1 fi if [[ ! -e "/usr/share/ovmf/x64/OVMF_CODE.fd" ]]; then echo "File /usr/share/ovmf/x64/OVMF_CODE.fd does not exist. Is the package ovmf installed?" >&2 exit 1 fi qemu-system-x86_64 \ -bios /usr/share/ovmf/x64/OVMF_CODE.fd \ -name "$vm_name" \ -monitor stdio \ -enable-kvm -smp 2 -cpu host -m 1024 \ -vga virtio \ -display gtk,gl=on \ -drive file="$2",if=virtio,media=cdrom -boot once=d \ -net nic -net user \ -usbdevice tablet ;; *) echo "Unknown VM name specified: $vm_name" >&2 exit 1 ;; esac ### frequently/previously used options: ## user-mode networking # -net nic,model=virtio -net user ## user-mode networking with redirect (localhost:2222 -> 10.0.2.15:22) # -net nic,model=virtio -net user -redir tcp:2222:10.0.2.15:22 ================================================ FILE: qemu-mac-hasher.py ================================================ #!/usr/bin/env python # Author: Jakub Klinkovský (Lahwaacz) # https://github.com/lahwaacz import sys import zlib if len(sys.argv) != 2: print("usage: %s " % sys.argv[0]) sys.exit(1) crc = zlib.crc32(sys.argv[1].encode("utf-8")) & 0xffffffff crc = str(hex(crc))[2:] print("52:54:%s%s:%s%s:%s%s:%s%s" % tuple(crc)) ================================================ FILE: qemu-tap-helper.sh ================================================ #! /usr/bin/bash # Author: Jakub Klinkovský (Lahwaacz) # https://github.com/lahwaacz ########## Functions ########## ## Check if a string represents a network interface # $1: potential interface name function is_interface() { [[ -d "/sys/class/net/$1" ]] } ## Create new TAP interface # $1: name of the interface to create function create_tap() { if ! is_interface "$1"; then echo "Creating TAP interface '$1'" ip tuntap add "$1" mode tap user "$username" ip link set dev "$1" up fi } ## Delete TAP interface # $1: name of the interface to delete function del_tap() { echo "Deleting TAP interface '$1'" ip link set dev "$1" down ip tuntap del "$1" mode tap } ## Check if the bridge has any interface # $1: bridge interface name function bridge_is_empty() { [[ $(ls "/sys/class/net/$1/brif" | wc -w) == "0" ]] } ## Create bridge interface if it does not exist # $1: bridge interface name function create_br() { if is_interface "$1"; then if [[ ! -d "/sys/class/net/$1/brif" ]]; then echo "Interface '$1' already exists and is not a bridge" exit 1 fi else echo "Creating bridge interface '$1'" ip link add name "$1" type bridge ip link set dev "$1" up # Xyne's excellent script to launch NAT echo "Starting NAT" nat-launch.sh "$wan_nic" "$1" up fi } ## Delete bridge interface if it exists and has no interface # $1: bridge interface name function del_br() { if bridge_is_empty "$1"; then # Xyne's excellent script to launch NAT echo "Stopping NAT" nat-launch.sh "$wan_nic" "$1" down echo "Deleting bridge interface '$1'" ip link set dev "$1" down ip link delete "$1" type bridge fi } ## Add interface to the bridge # $1: bridge interface name # $2: name of the interface to add function br_add_iface() { echo "Adding interface '$2' to bridge '$1'" ip link set dev "$2" promisc on up ip addr flush dev "$2" scope host &>/dev/null ip addr flush dev "$2" scope site &>/dev/null ip addr flush dev "$2" scope global &>/dev/null ip link set dev "$2" master "$1" # skip forwarding delay bridge link set dev "$2" state 3 } ## Remove interface from the bridge # $1: bridge interface name # $2: name of the interface to remove function br_rm_iface() { echo "Removing interface '$2' from bridge '$1'" ip link set "$2" promisc off down ip link set dev "$2" nomaster } ########## Main ############### function print_qemu_tap_helper_usage() { echo "usage: $0 " echo " and will be created," echo " NAT from to will be set up" } if [[ $EUID -ne 0 ]]; then echo "This script must be run as root." >&2 exit 1 fi if [[ -z $4 ]]; then print_qemu_tap_helper_usage exit 1 else username="$1" tap_nic="$2" br_nic="$3" wan_nic="$4" action="$5" fi # exit on errors set -e case "$action" in up) create_br "$br_nic" create_tap "$tap_nic" br_add_iface "$br_nic" "$tap_nic" ;; down) br_rm_iface "$br_nic" "$tap_nic" del_tap "$tap_nic" del_br "$br_nic" ;; *) print_qemu_tap_helper_usage exit 1 ;; esac ================================================ FILE: remove-dead-symlinks.sh ================================================ #! /bin/bash # recursively remove dead symlinks shopt -s globstar # non-recursive version: 'for itm in *' for itm in **/* do if [ -h "$itm" ] then target=$(readlink -fn "$itm") if [ ! -e "$target" ] then echo "$itm" rm "$itm" fi fi done ================================================ FILE: replaygain.py ================================================ #! /usr/bin/env python3 import sys import os import argparse import subprocess import asyncio from concurrent.futures import ThreadPoolExecutor import taglib from pythonscripts.cpu import cores_count from pythonscripts.logger import Logger class ReplayGain: """ Will consider all files to belong to one album. """ def __init__(self, logger, options, files): # logger self.log = logger self.log.filename = None # internals self.raw_lines = [] self.data_files = [] self.data_album = {} # options self.force = options.force self.force_album = options.force_album self.force_track = options.force_track self.files = files def run(self): # check if all files have ReplayGain tags; mp3gain runs very long if not (self.force or self.force_album or self.force_track) and self.all_files_have_replaygain_tags(): self.log.error("All files already have ReplayGain tags, no action taken.") return if self.run_mp3gain(): self.update_tags() def all_files_have_replaygain_tags(self): """ Quick analysis to determine if input files contain replaygain_* tags. """ for fname in self.files: # open id3 tag f = taglib.File(fname) tags = set([tag.lower() for tag in f.tags.keys() if tag.lower().startswith("replaygain_")]) return tags == set(["replaygain_track_gain", "replaygain_album_gain", "replaygain_track_peak", "replaygain_album_peak"]) def run_mp3gain(self): """ Compute values for replaygain_* tags. """ self.log.debug("running mp3gain on specified files") cmd = ["mp3gain", "-q", "-o", "-s", "s"] + self.files ret = True try: raw_data = subprocess.check_output(cmd, universal_newlines=True) self.raw_lines = raw_data.splitlines() except subprocess.CalledProcessError as exc: code = exc.returncode msg = "mp3gain returned error status: " + str(code) + "\n" msg += "-----------mp3gain output dump-----------\n" msg += exc.output msg += "\n-----------------------------------------\n" self.log.error(msg) ret = False except Exception as e: print(e) ret = False raise finally: return ret def update_tags(self): """ Add computed replaygain_* tags into all files. """ self.log.debug("parsing mp3gain output") album_parts = self.raw_lines[-1].strip().split("\t") # just in case if album_parts[0] != '"Album"': self.log.error("unable to parse mp3gain output") return a_gain = float(album_parts[2]) # album gain a_peak = float(album_parts[3]) / 32768.0 # album peak del self.raw_lines[0] # header del self.raw_lines[-1] # album summary for line in self.raw_lines: parts = line.strip().split("\t") fname = parts[0] # filename self.log.filename = fname self.log.debug("begin processing file") t_gain = float(parts[2]) # track gain t_peak = float(parts[3]) / 32768.0 # track peak # set t_gain, t_peak, a_gain, a_peak depending on options if self.force_album: t_gain = a_gain t_peak = a_peak elif self.force_track: a_gain = t_gain a_peak = t_peak # open id3 tag f = taglib.File(fname) # update tag f.tags["REPLAYGAIN_TRACK_GAIN"] = "%.2f dB" % t_gain f.tags["REPLAYGAIN_ALBUM_GAIN"] = "%.2f dB" % a_gain f.tags["REPLAYGAIN_TRACK_PEAK"] = "%.6f" % t_peak f.tags["REPLAYGAIN_ALBUM_PEAK"] = "%.6f" % a_peak # save tag self.log.debug("saving modified ID3 tag") f.save() self.log.debug("done processing file") self.log.filename = None class Main: """ Will parse input pattern and create ReplayGain object on every directory found. """ def __init__(self, logger, options): self.logger = logger self.options = options self.recursive = options.recursive self.paths = options.files del options.recursive # don't want to pass it to ReplayGain object del options.files # don't want to pass it to ReplayGain object async def run(self): # We could use the default single-threaded executor with basically the same performance # (because of Python's GIL), but the ThreadPoolExecutor allows to limit the maximum number # of workers and thus the maximum number of concurrent subprocesses. with ThreadPoolExecutor(max_workers=cores_count()) as executor: loop = asyncio.get_event_loop() tasks = [ loop.run_in_executor(executor, self.worker, path) for path in self.queue_generator() ] for result in await asyncio.gather(*tasks): pass def worker(self, paths): paths = sorted(list(paths)) # skip dirs not containing any mp3 file if len(paths) == 0: return # write info print("Procesing:") for path in paths: print(" " + path) try: # create ReplayGain object, pass files and run rg = ReplayGain(self.logger, self.options, paths) rg.run() except Exception as e: print(e, file=sys.stderr) raise def queue_generator(self): """ For each directory in self.files returns generator returning full paths to mp3 files in that folder. If self.files contains file paths instead of directory, it's returned as [file]. """ def walk(root): dirs = [] files = [] for entry in os.scandir(root): if entry.is_dir(): dirs.append(entry.name) elif entry.is_file() and entry.name.endswith(".mp3"): files.append(entry.name) # first yield found files, then recurse into subdirs if files: yield (os.path.join(root, x) for x in files) if self.recursive: for d in dirs: # recurse into subdir for x in walk(os.path.join(root, d)): yield x for path in self.paths: if os.path.isdir(path): for x in walk(path): yield x else: yield [path] def main(prog_name, options): logger = Logger(options.log_level, prog_name) logger.debug("Selected mp3 files:") logger.debug("\n".join(sorted(options.files))) main = Main(logger, options) asyncio.run(main.run()) def argparse_path_handler(path): if not os.path.exists(path): raise argparse.ArgumentTypeError("invalid path: '%s'" % path) if os.path.isfile(path) and not path.endswith(".mp3"): raise argparse.ArgumentTypeError("not a mp3 file: '%s'" % path) return os.path.abspath(path) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Write correct ReplayGain tags into mp3 files; uses mp3gain internally") # log level options log = parser.add_mutually_exclusive_group() log.add_argument("-q", "--quiet", dest="log_level", action="store_const", const=0, default=1, help="do not output error messages") log.add_argument("-v", "--verbose", dest="log_level", action="store_const", const=3, help="output warnings and informational messages") log.add_argument("-d", "--debug", dest="log_level", action="store_const", const=4, help="output debug messages") parser.add_argument("-r", "--recursive", action="store_true", help="when path to directory is specified, browse it recursively (albums still respected)") parser.add_argument("--force", action="store_true", help="force overwriting of existing ID3v2 ReplayGain tags") group = parser.add_mutually_exclusive_group() group.add_argument("--force-album", action="store_true", help="write replaygain_album_{gain,peak} values into replaygain_track_{gain,peak} tags") group.add_argument("--force-track", action="store_true", help="write replaygain_track_{gain,peak} values into replaygain_album_{gain,peak} tags") parser.add_argument("files", nargs="+", metavar="FILE | FOLDER", type=argparse_path_handler, help="path to mp3 file(s) or directory(ies)") args = parser.parse_args() main(sys.argv[0], args) ================================================ FILE: rexe ================================================ #!/bin/bash set -e HOST="" LOCAL_PATH="" REMOTE_PATH="" REXE_DIR="rexe" CMD="" DOWNLOAD="true" EXCLUDE=() EXCLUDE_DOWNLOAD=() function handle_argument() { if [[ "$HOST" == "" ]]; then case "$1" in *:*) HOST="${1%:*}" REMOTE_PATH="${1#*:}" ;; *) HOST="$1" ;; esac return fi if [[ "$CMD" == "" ]]; then CMD="$1" else CMD="$CMD $1" fi } while [ "$#" -gt 0 ]; do if [[ "$CMD" == "" ]]; then case "$1" in --path=*) LOCAL_PATH="${1#*=}"; shift 1;; -p|--path) LOCAL_PATH="$2"; shift 2;; --no-download) DOWNLOAD="false"; shift 1;; --exclude=*) EXCLUDE+=("--exclude" "${1#*=}"); shift 1;; --exclude) EXCLUDE+=("--exclude" "$2"); shift 2;; --exclude-download=*) EXCLUDE_DOWNLOAD+=("--exclude" "${1#*=}"); shift 1;; --exclude-download) EXCLUDE_DOWNLOAD+=("--exclude" "$2"); shift 2;; -*) echo "unknown option: $1" >&2; exit 1;; *) handle_argument "$1"; shift 1;; esac else handle_argument "$1" shift 1 fi done if [[ "$HOST" == "" ]]; then echo "error: remote host was not specified." >&2 exit 1 fi if [[ "$CMD" == "" ]]; then echo "error: remote command was not sepcified." >&2 exit 1 fi # fill in defaults if [[ "$LOCAL_PATH" == "" ]]; then LOCAL_PATH=$(pwd) fi # NOTE: the tmpfs for $XDG_RUNTIME_DIR may be too small (e.g. only 10% of the available RAM) #if [[ "$REMOTE_PATH" == "" ]]; then # REMOTE_PATH=$(ssh "$HOST" echo '$XDG_RUNTIME_DIR') #fi if [[ "$REMOTE_PATH" == "" ]]; then REMOTE_PATH="/tmp" REXE_DIR="rexe_$(ssh "$HOST" whoami)" fi if [[ ! -d "$LOCAL_PATH" ]]; then echo "error: local path '$LOCAL_PATH' is does not exist or is not a directory." >&2 exit 1 fi # create remote main directory for rexe with restricted permissions echo "Creating remote directory '$REMOTE_PATH/$REXE_DIR'..." ssh "$HOST" mkdir -m 0700 -p "$REMOTE_PATH/$REXE_DIR" # change remote path into full path _basename=$(basename "$LOCAL_PATH") REMOTE_PATH="$REMOTE_PATH/$REXE_DIR/$_basename" echo "Uploading local directory '$LOCAL_PATH' to remote directory '$REMOTE_PATH'..." rsync -rlptD "$LOCAL_PATH/" "$HOST:$REMOTE_PATH/" -e ssh -zz --info=progress2 --delete ${EXCLUDE[@]} echo "Executing remote command '$CMD'..." # ignore errors of the ssh command to always run rsync afterwards (even on keyboard interrupt) set +e ssh -t "$HOST" "cd ${REMOTE_PATH@Q}; bash --login -c -- ${CMD@Q}" set -e if [[ "$DOWNLOAD" != "false" ]]; then echo "Synchronizing remote directory '$REMOTE_PATH' into the local directory..." # FIXME: EXCLUDE_DOWNLOAD does not work correctly for wildcards echo rsync -rlptD "$HOST:$REMOTE_PATH/" "$LOCAL_PATH/" -e ssh -zz --info=progress2 --delete ${EXCLUDE[@]} ${EXCLUDE_DOWNLOAD[@]} -v rsync -rlptD "$HOST:$REMOTE_PATH/" "$LOCAL_PATH/" -e ssh -zz --info=progress2 --delete ${EXCLUDE[@]} ${EXCLUDE_DOWNLOAD[@]} -v fi ================================================ FILE: rmshit.py ================================================ #! /usr/bin/env python3 import os import shutil from pathlib import Path import yaml DEFAULT_CONFIG = """ - ~/.adobe # Flash crap - ~/.macromedia # Flash crap - ~/.recently-used - ~/.local/share/recently-used.xbel - ~/.thumbnails - ~/.gconfd - ~/.gconf - ~/.local/share/gegl-0.2 - ~/.FRD/log/app.log # FRD - ~/.FRD/links.txt # FRD - ~/.objectdb # FRD - ~/.gstreamer-0.10 - ~/.pulse - ~/.esd_auth - ~/.config/enchant - ~/.spicec # contains only log file; unconfigurable - ~/.dropbox-dist - ~/.parallel - ~/.dbus - ~/ca2 # WTF? - ~/ca2~ # WTF? - ~/.distlib/ # contains another empty dir, don't know which software creates it - ~/.bazaar/ # bzr insists on creating files holding default values - ~/.bzr.log - ~/.nv/ - ~/.viminfo # configured to be moved to ~/.cache/vim/viminfo, but it is still sometimes created... - ~/.npm/ # npm cache - ~/.java/ - ~/.swt/ - ~/.oracle_jre_usage/ - ~/.openjfx/ - ~/.org.jabref.gui.JabRefMain/ - ~/.org.jabref.gui.MainApplication/ - ~/.jssc/ - ~/.tox/ # cache directory for tox - ~/.pylint.d/ - ~/.qute_test/ - ~/.QtWebEngineProcess/ - ~/.qutebrowser/ # created empty, only with webengine backend - ~/.asy/ - ~/.cmake/ - ~/.gnome/ - ~/unison.log - ~/.texlive/ - ~/.w3m/ - ~/.subversion/ - ~/nvvp_workspace/ # created empty even when the path is set differently in nvvp - ~/.ansible/ - ~/.fltk/ - ~/.vnc/ - ~/.local/share/Trash/ # VSCode puts deleted files here """ def get_size(path): if Path(path).is_dir(): return sum(p.stat().st_size for p in Path(path).rglob("*")) return Path(path).stat().st_size def read_config(): """ Reads the list of shitty files from a YAML config. """ config_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config/")) config_path = Path(config_dir) / "rmshit.yaml" # write default config if it does not exist if not config_path.exists(): with open(config_path, "w") as f: print(DEFAULT_CONFIG.strip(), file=f) with open(config_path, "r") as f: return yaml.safe_load(f) def yesno(question, default="n"): """ Asks the user for YES or NO, always case insensitive. Returns True for YES and False for NO. """ prompt = f"{question} (y/[n]) " ans = input(prompt).strip().lower() if not ans: ans = default if ans == "y": return True return False def format_size(size_in_bytes): """Format file size in bytes to a human-readable string.""" if size_in_bytes <= 0: return "0 bytes" units = ["bytes", "KiB", "MiB", "GiB"] size = float(size_in_bytes) unit_index = min(int((size_in_bytes.bit_length() - 1) // 10), len(units) - 1) size /= 1024**unit_index return f"{size:.4g} {units[unit_index]}" def rmshit(): shittyfiles = read_config() print("Found shittyfiles:") found = [] total_size = 0 for f in shittyfiles: absf = os.path.expanduser(f) if os.path.exists(absf): found.append(absf) size = get_size(absf) total_size += size print(f" {f} ({format_size(size)})") if len(found) == 0: print("No shitty files found :)") return if yesno("Remove all?", default="n"): for f in found: if os.path.isfile(f): os.remove(f) else: shutil.rmtree(f) print(f"All cleaned, {format_size(total_size)} freed.") else: print("No file removed") if __name__ == "__main__": rmshit() ================================================ FILE: run-pvserver ================================================ #!/bin/bash host="$1" if [[ "$host" == "" ]]; then echo "usage: $0 [user@]hostname" exit 1 fi hostname=$(ssh "$host" "uname -n") port=11111 display=42 # NOTE: bash -lc is needed to get full $PATH by sourcing /etc/profile.d/*.sh ssh -C -t -L "localhost:$port:$hostname:$port" "$host" "bash -lc 'xvfb-run --server-num=$display mpirun -np 2 pvserver --displays=:$display --server-port=$port'" ================================================ FILE: sway-sensible-terminal ================================================ #! /usr/bin/env python3 import os import sys import json import subprocess PATH = os.environ.get("PATH", "/usr/bin") TERMINAL = os.environ.get("TERMINAL", "alacritty") ARGS = sys.argv[1:] def get_cwd(tree): for node in tree.get("nodes", []): if node["focused"]: name = node["name"] for part in name.split(":"): if part.startswith("/") and os.path.exists(part): return part cwd = get_cwd(node) if cwd is not None: return cwd if os.path.isfile("/usr/bin/swaymsg"): prog = "swaymsg" elif os.path.isfile("/usr/bin/i3-msg"): prog = "i3-msg" else: raise Exception("Neither swaymsg or i3-msg was found in /usr/bin/") cmd = subprocess.run(f"{prog} -t get_tree", shell=True, check=True, capture_output=True) tree = json.loads(cmd.stdout) cwd = get_cwd(tree) if cwd is not None: if "alacritty" in TERMINAL: ARGS = ["--working-directory", cwd, *ARGS] else: ARGS = ["-d", cwd, *ARGS] for d in PATH.split(":"): path = os.path.join(d, TERMINAL) if not os.path.isfile(path): continue os.execl(path, path, *ARGS) raise Exception(f"Error: terminal '{TERMINAL}' was not found in $PATH ({PATH})") ================================================ FILE: teams-attendance-parser.py ================================================ #! /usr/bin/env python3 """ THE BEER-WARE LICENSE (Revision 42): Jakub Klinkovský wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. """ import argparse import os.path import datetime import sys # maybe depends on the locale in which MS Teams runs... TIMESTAMP_FORMATS = [ "%m/%d/%Y, %I:%M:%S %p", "%d. %m. %Y %H:%M:%S", ] CLASS_LENGTH = datetime.timedelta(minutes=100) def parse_timestamp(timestamp): last_error = None for format in TIMESTAMP_FORMATS: try: return datetime.datetime.strptime(timestamp, format) except ValueError as e: last_error = e continue raise last_error def parse_attendance_list(path): print(f"Parsing file {path}...") data = {} text = open(path, "r", encoding="utf-16").read() for line in text.splitlines(): # parse items on the line name, action, timestamp = line.split("\t") # skip header line if name == "Full Name" or name == "Celé jméno": continue # validate items assert "," in name, name assert action in {"Joined", "Left", "Připojeno", "Odpojil(a) se"}, f"unknown action: {action}" timestamp = parse_timestamp(timestamp) # initialize data user_actions = data.setdefault(name, []) # append action user_actions.append((action, timestamp)) return data def get_attendance(class_start, actions): class_end = class_start + CLASS_LENGTH # make sure actions are sorted by timestamp actions.sort(key=lambda a: a[1]) # calculate attendance = datetime.timedelta() joined = None for i, item in enumerate(actions): action, timestamp = item if action in {"Joined", "Připojeno"}: assert joined is None joined = timestamp elif action in {"Left", "Odpojil(a) se"}: assert joined is not None attendance += timestamp - joined joined = None else: assert False # handle the missing "Left" action if joined is not None: attendance += class_end - joined return attendance def print_attendance(teacher, class_start, data): print(f"Class teacher:\t{teacher}") print(f"Class start:\t{class_start}") print("Attendance:") for name in sorted(data.keys()): attendance = get_attendance(class_start, data[name]) perc = attendance.seconds / CLASS_LENGTH.seconds * 100 print(f" {name:<30}\t{attendance} ({perc:.0f}%)") print() def main(path): data = parse_attendance_list(path) teacher = list(data.keys())[0] class_start = data[teacher][0][1] del data[teacher] print_attendance(teacher, class_start, data) parser = argparse.ArgumentParser(description="parser for MS Teams attendance list files") parser.add_argument("path", nargs="+", help="path to the attendance list file") args = parser.parse_args() for p in args.path: if os.path.isfile(p): main(p) else: print(f"ERROR: {p} is not a file", file=sys.stderr) ================================================ FILE: toggle-touchpad.sh ================================================ #!/bin/sh # Toggle touchpad status # Using libinput and xinput # Use xinput list and do a search for touchpads. Then get the first one and get its name. device="$(xinput list | grep -P '(?<= )[\w\s:]*(?i)(touchpad|synaptics)(?-i).*?(?=\s*id)' -o | head -n1)" # If it was activated disable it and if it wasn't disable it [[ "$(xinput list-props "$device" | grep -P ".*Device Enabled.*\K.(?=$)" -o)" == "1" ]] && xinput disable "$device" || xinput enable "$device" ================================================ FILE: touch-tree.py ================================================ #! /usr/bin/env python # Little script to "touch" directory structure. # Works like 'cp -r', but instead of copying full file, the new file is "touched", # so the tree structure is preserved and only empty files created. import sys import os class Main: def __init__(self, oldRoot, newRoot): self.oldRoot = oldRoot self.newRoot = newRoot def browse(self, path): for file in os.listdir(path): absPath = os.path.join(path, file) relPath = os.path.relpath(absPath, self.oldRoot) if os.path.isdir(absPath): os.mkdir(os.path.join(self.newRoot, relPath)) self.browse(absPath) elif os.path.isfile(absPath): open(os.path.join(self.newRoot, relPath), "w").close() def touchTree(self): os.mkdir(newRoot) self.browse(self.oldRoot) if len(sys.argv) != 3 or not os.path.isdir(sys.argv[1]) or os.path.exists(sys.argv[2]): sys.exit(1) oldRoot = os.path.abspath(sys.argv[1]) newRoot = os.path.abspath(sys.argv[2]) print(oldRoot + " => " + newRoot) main = Main(oldRoot, newRoot) main.touchTree() ================================================ FILE: waybar-khal.py ================================================ #! /usr/bin/env python3 import subprocess import json data = {} cmd = [ "khal", "list", "now", "23:59", "--once", "--format", "{start-time} ({location}) {title}{repeat-symbol}{alarm-symbol}", ] output = subprocess.run(cmd, check=True, text=True, capture_output=True).stdout lines = [line.strip() for line in output.split("\n")] # filter out lines that do not start with a number # (khal list includes headings like "Monday, 2025-03-31" for each day) lines = [line for line in lines if line and line[0].isdigit()] if lines: data["text"] = " " + lines[0] data["tooltip"] = "\n".join(lines) else: data["text"] = "" print(json.dumps(data)) ================================================ FILE: x ================================================ #! /bin/bash # Some references: # https://wiki.archlinux.org/index.php/Bash#Functions # https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/extract/extract.plugin.zsh function extract() { local remove_archive local success local fname local basename local extension success=0 fname=$(realpath "$1") extension=${fname##*.} # remove extension from basename basename=$(basename "${fname%.*}") # hack to recognize .tar.gz etc as extension if [[ "${basename##*.}" == "tar" ]]; then extension="${basename##*.}.$extension" basename=$(basename "${basename%.*}") fi # split \.part[0-9]* from $basename basename="${basename%\.part[0-9]*}" case "$extension" in tar.gz|tgz|tar.bz2|tbz|tbz2|tar.xz|txz|tar.lzma|tlz|tar|tar.zst) mkdir "$basename" tar xvf "$fname" -C "$basename" ;; gz|Z) gzip -dkv "$fname" ;; bz2) bzip2 -dkv "$fname" ;; xz|lzma) xz -dkv "$fname" ;; zst) zstd -dkv "$fname" ;; zip) unzip "$fname" -d "$basename" ;; rar) mkdir "$basename" pushd "$basename" unrar x "$fname" popd ;; 7z) 7za x "$fname" -o"$basename" ;; *) echo "extract: '$fname' cannot be extracted" 1>&2 success=1 ;; esac [[ $success == 0 ]] && success=$? # if destination directory contains only one file/dir, move it to cwd if [[ $success == 0 ]]; then count=$(find "$basename" -maxdepth 1 -mindepth 1 | wc -l) if [[ $count == 1 ]]; then name=$(basename "$(find "$basename" -maxdepth 1 -mindepth 1)") # can't move ./foo/foo into ./foo if [[ "$basename" == "$name" ]]; then tmp="$name.tmp" else tmp="$name" fi mv "$basename/$name" "$tmp" rmdir "$basename" mv "$tmp" "$name" fi fi } if [[ $# == 0 ]]; then echo "Usage: $0 file [file ...]" exit 1 fi while [[ $# > 0 ]]; do if [[ -f "$1" ]]; then extract "$1" else echo "extract: '$1' is not a valid file" fi shift done