-
-
Save fjf2002/4580f04b73b11146379cf184cb9fbfb9 to your computer and use it in GitHub Desktop.
btrfs-undelete
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Can you write some examples with different <file/dir>
formats? wildcards, etc?
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
This is an attempt to enhance Changaco/btrfs-undelete .