Skip to content

Instantly share code, notes, and snippets.

@fjf2002
Forked from Changaco/btrfs-undelete
Last active March 29, 2021 22:12
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fjf2002/4580f04b73b11146379cf184cb9fbfb9 to your computer and use it in GitHub Desktop.
Save fjf2002/4580f04b73b11146379cf184cb9fbfb9 to your computer and use it in GitHub Desktop.
btrfs-undelete
#!/bin/bash
# btrfs-undelete
# Copyright (C) 2013 Jörg Walter <info@syntax-k.de>
# Copyright (C) 2018 Franz-Josef Faerber <franzjoseffaerber@gmail.com>
# This program is free software; you can redistribute it and/or modify it under
# the term of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or any later version.
# Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -euo pipefail
# Ensures device isn't mounted any more.
# If variable $auto_unmount is set, tries to unmount.
function ensure_not_mounted {
local device="$1"
local mountpoint="$(cat /proc/mounts | grep -Po '(?<=^'"$device"' )([^ ]+)')"
if [[ -n "$mountpoint" ]]; then
echo "Device $device is still mounted on $mountpoint ."
if [[ -n "$auto_unmount" ]]; then
echo "Unmounting."
if ! umount "$device"; then
echo "Umount failed. The following processes have open files on this mountpoint:"
lsof_for_path "$mountpoint"
echo "Aborting."
exit 2
fi
else
echo "Aborting."
exit 3
fi
fi
}
# List open files for given path prefix
function lsof_for_path {
local path_prefix="$1"
lsof -Fpcfn \
| awk '
BEGIN { print "COMMAND\tPID\tNAME" }
/^p/ { pid = substr($0, 2) }
/^c/ { command = substr($0, 2) }
substr($0,1,5)=="n'$path_prefix'" { print command "\t" pid "\t" substr($0, 2) }'
}
function summary_and_exit {
echo
echo "The following files were restored in directory $out :"
find . -ls
exit 0
}
all_versions_flag=
keep_empty_files_flag=
auto_unmount=
while getopts "aeu" opt; do
case $opt in
a)
all_versions_flag=1
;;
e)
keep_empty_files_flag=1
;;
u)
auto_unmount=1
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift "$((OPTIND-1))"
if [[ $# -lt 3 ]]; then
echo "Usage: $0 [OPTION]... <dev> <file/dir> <dest>" 1>&2
echo
echo "This program tries to recover the given files or directories."
echo "Unless otherwise specified with the options, restoration will"
echo "stop after a generation of matching non-empty files was restored."
echo
echo "<dev> must not be mounted, otherwise this program may appear"
echo "to work but find nothing."
echo
echo "<file/dir> must be specified relative to the filesystem root,"
echo "obviously. It may contain * and ? as wildcards."
echo
echo "<dest> must be a writable directory with enough free space"
echo "to hold the files you're trying to restore."
echo
echo "Options:"
echo " -a Restore all found versions of the matching files"
echo " (instead of only the ones with the highest generation"
echo " id). Files will be suffixed by the generation it first"
echo " occurred in."
echo " -e Restore empty files."
echo " -u Try to unmount automatically."
exit 1
fi
# Canonicalize potentially relative path $1 because we will change dir
dev=$(readlink -f "$1")
file="$2"
file="${file#/}"
file="${file%/}"
regex="${file//\\/\\\\}"
# quote regex special characters
regex="${regex//./\.}"
regex="${regex//+/\+}"
regex="${regex//|/\|}"
regex="${regex//(/\(}"
regex="${regex//)/\)}"
regex="${regex//\[/\[}"
regex="${regex//]/\]}"
regex="${regex//\{/\{}"
regex="${regex//\}/\}}"
# treat shell wildcards specially
regex="${regex//\*/.*}"
regex="${regex//\?/.}"
# extract number of slashes in order to get correct number of closing parens
slashes="${regex//[^\/]/}"
# build final regex
# example: ^/(|home(|/username(|/Desktop(|/.*))))$
# see man btrfs restore, option --path-regex for details
regex="^/(|${regex//\//(|/}(|/.*${slashes//?/)}))\$"
roots="$(mktemp --tmpdir btrfs-undelete.roots.XXXXX)"
out="$(mktemp --tmpdir="$3" -d btrfs-undelete.XXXXX)"
cd "$out"
trap "rm $roots" EXIT
trap "rm -r $out &> /dev/null; exit 1" SIGINT
ensure_not_mounted "$dev"
echo -ne "Searching roots..."
btrfs-find-root "$dev" \
| sed -n -r -e 's/^Well block ([0-9]+)\(gen: ([0-9]+).*/\2\t\1/p' \
| sort -rn \
> $roots || exit 1
echo
i=0
max="$(wc -l <$roots)"
while read gen bytenr; do
((i+=1))
# Add some spaces at the end to erase potential garbage from last echo:
echo -ne "Trying root gen=$gen bytenr=$bytenr... ($i/$max) \r"
gendir="gen_$gen"
genpath="$out/$gendir"
mkdir "$genpath"
# Restore a generation
btrfs restore -t $bytenr --path-regex "$regex" "$dev" "$genpath" &>/dev/null
if [ "$?" = 0 ]; then
found=$(find "$genpath" ! -type d | wc -l)
if [ $found -gt 0 ]; then
echo -e "\nRestored $found file(s) into $genpath"
# Remove already restored files in other generations:
cd "$gendir"
while IFS= read -r -d '' myfile; do
# The asterisk itself must not be quoted
# in order to enable bash filename expansion
find "../"*"/$myfile" \
! -path "../$gendir/*" \
-type f \
-exec cmp --silent "$myfile" {} \; \
-delete
done < <(find "." -type f -print0)
#
if [[ -z "$keep_empty_files_flag" ]]; then
find "." -type f -size 0c -exec echo "Restored {} but it's empty. Deleting it." \; -delete
fi
if [[ -z "$all_versions_flag" ]]; then
echo "Found first match. Exiting."
cd ..
mv "$gendir"/* .
rmdir "$gendir"
summary_and_exit
fi
cd ..
fi
fi
found=$(find "$gendir" ! -type d | wc -l)
if [[ $found -eq 0 ]]; then
# Just empty directories. Remove them.
rm -r "$gendir"
fi
done <$roots
# Add a newline to the last "Trying..." clause
echo
if [[ -z "$all_versions_flag" ]]; then
rm -r $out
echo "Didn't find '$file'"
exit 1
else
# Copy all remaining files together in one directory
gens_mixed="gens_mixed"
mkdir "$gens_mixed"
while read gen bytenr; do
gendir="gen_$gen"
if [[ -d "$gendir" ]]; then
cd "$gendir"
# Suffix files by generation id
find "." ! -type d -exec mv {} {}.$gendir \;
# Poor man's move-and-merge-directories:
cp -al "." "../$gens_mixed"
cd ..
rm -r "$gendir"
fi
done <$roots
mv "$gens_mixed"/* .
rmdir "$gens_mixed"
summary_and_exit
fi
@fjf2002
Copy link
Author

fjf2002 commented Apr 3, 2018

This is an attempt to enhance Changaco/btrfs-undelete .

  • restore multiple versions
  • when restoring multiple versions, only keep files that actually differ in file content
  • fail on errors
  • error message if device is still mounted
  • ability to unmount or list open files if unmount fails
  • print summary of restored files

@endolith
Copy link

Can you write some examples with different <file/dir> formats? wildcards, etc?

@fjf2002
Copy link
Author

fjf2002 commented Jan 19, 2021

Wow I haven't looked at that code for years and can't remember a thing from scratch, sorry. But this fork shouldn't be too far away from Changaco/btrfs-undelete?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment