Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save matthiasseghers/7003aab2d272d773bab00a62f90ac3b3 to your computer and use it in GitHub Desktop.

Select an option

Save matthiasseghers/7003aab2d272d773bab00a62f90ac3b3 to your computer and use it in GitHub Desktop.
Script to backup Firefly III database, uploads and config files installed with docker-compose
#!/bin/bash
files_to_backup=(.*.env .env docker-compose.yml )
info() { echo -e "\\033[1;36m[INFO]\\033[0m \\033[36m$*\\033[0m" >&2; }
warn() { echo -e "\\033[1;33m[WARNING]\\033[0m \\033[33m$*\\033[0m" >&2; }
fatal() { echo -e "\\033[1;31m[FATAL]\\033[0m \\033[31m$*\\033[0m" >&2; exit 1; }
intro () {
echo " ====================================================="
echo " Backup & Restore docker based FireFly III v1.6 "
echo " ====================================================="
echo " It automatically detects db & upload volumes based on the name matching the following regex: firefly[_-](iii|)[_-]?"
echo " Requirements:"
echo " - Place the script in the same directory where your docker-compose.yml and .env files are saved"
echo " Warning: The destination directory is created if it does not exist"
}
usage () {
echo "Usage: $0 backup|restore /tmp/backup/destination/dir [no_files]"
echo "- backup|restore - Action you want to execute"
echo "- destination path of your backup file including file name"
echo "- optionally backup or restore volumns only when no_files parameter is passed"
echo "Example backup: $0 backup /home/backup/firefly-2022-01-01.tar.gz"
echo "Example restore: $0 restore /home/backup/firefly-2022-01-01.tar.gz"
echo "To backup once per day you can add something like this to your cron:"
echo "1 01 * * * bash /home/myname/backuper.sh backup /home/backup/\$(date '+%F').tar.gz"
echo "To restore a database of a specific Firefly version follow the steps below"
echo "- Look for the Firefly version from your backup. It's in <backup>.tar.gz/version.txt"
echo "- Use the Firefly docker tag (https://hub.docker.com/r/fireflyiii/core/tags) corresponding to your version"
echo "- Change the firefly image to a tag from your version. Example:"
echo " image: fireflyiii/core:version-6.1.10"
echo "- Run docker compose up"
echo "- Run this script to restore the backup"
}
backup () {
script_path="$1"
# Ensure destination directory exists
dest_dir="$(dirname "$2")"
if [ ! -d "$dest_dir" ]; then
info "Creating destination directory: $dest_dir"
mkdir -p "$dest_dir"
fi
# Set full_path directly, do not use realpath (file may not exist yet)
full_path="$2"
dest_path="$(dirname "$full_path")"
dest_file="$(basename "$full_path")"
upload_volume="$3"
no_files=$4
to_backup=()
if [ -f "$full_path" ]; then
warn "Provided file path already exists: $full_path. Overwriting"
fi
# Create temporary directory
tmp_dir="$dest_path/tmp"
if [ ! -d "$tmp_dir" ]; then
mkdir "$tmp_dir"
fi
# Files backup
if [ "$no_files" = "false" ]; then
not_found=()
for pattern in "${files_to_backup[@]}"; do
for file in ${script_path}/${pattern}; do
if [[ ! -f $file ]]; then
not_found+=("$file")
else
cp "$file" "$tmp_dir/"
to_backup+=($(basename "$file"))
fi
done
done
if ((${#not_found[@]})); then
warn "The following files were not found in $script_path: ${not_found[@]}. Skipping."
fi
if ((${#to_backup[@]})); then
info "Backing up the following files in $script_path: ${to_backup[@]}"
fi
fi
# Version
app_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_core)
app_version=$(docker exec -it $app_container grep -F "'version'" /var/www/html/config/firefly.php | tr -s ' ' | cut -d "'" -f 4)
db_version=$(docker exec -it $app_container grep -F "'db_version'" /var/www/html/config/firefly.php | tr -s ' ' | tr -d ',' | cut -d " " -f 4)
info 'Backing up App & database version numbers.'
echo -e "Application: $app_version\nDatabase: $db_version" > "$tmp_dir/version.txt"
to_backup+=(version.txt)
# DB container
db_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_db)
if [ -z "$db_container" ]; then
warn "db container is not running. Not backing up."
else
info 'Backing up database'
docker exec $db_container bash -c '/usr/bin/mariadb-dump -u $MYSQL_USER --password="$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "$tmp_dir/firefly_db.sql"
to_backup+=("firefly_db.sql")
fi
# Upload Volume
if [ -z "$upload_volume" ]; then
warn "upload volume does NOT exist. Not backing up."
else
info 'Backing up upload volume'
# Correct tar command: archive contents of /tmp inside the container, not root
docker run --rm -v "$upload_volume:/tmp_upload" -v "$tmp_dir:/backup" alpine tar -czf "/backup/firefly_upload.tar.gz" -C /tmp_upload .
to_backup+=("firefly_upload.tar.gz")
fi
# Compress
tar -C "$tmp_dir" -czf "$dest_path/$dest_file" --files-from <(printf "%s\n" "${to_backup[@]}")
# Clean up
for file in "${to_backup[@]}"; do
rm -f "$tmp_dir/$file"
done
rmdir "$tmp_dir"
}
restore () {
script_path="$1"
full_path=$(realpath $2)
src_path="$(dirname $full_path)"
backup_file="$(basename $full_path)"
upload_volume="$3"
no_files=$4
if [ ! -f "$src_path/$backup_file" ]; then
fatal "Provided backup file does not exist: $path"
fi
# Create temporary directory
tmp_dir="$src_path/tmp"
if [ ! -d "$tmp_dir" ]; then
mkdir "$tmp_dir"
fi
# Files restore
if [ "$no_files" = "false" ]; then
tar -C "$tmp_dir" -xf "$src_path/$backup_file"
# Remove readarray, not needed
not_found=()
restored=()
for f in "${files_to_backup[@]}"; do
if [ ! -f "$tmp_dir/$f" ]; then
not_found+=("$f")
else
cp "$tmp_dir/$f" "$script_path/"
restored+=("$f")
fi
done
if ((${#not_found[@]})); then
warn "The following files were not found in backup: ${not_found[@]}. Skipping."
fi
if ((${#restored[@]})); then
info "Restoring the following files: ${restored[@]}"
fi
else
tar -C "$tmp_dir" -xf "$src_path/$backup_file" firefly_db.sql firefly_upload.tar.gz
restored=(firefly_db.sql firefly_upload.tar.gz)
fi
if [ ! -z "$upload_volume" ]; then
warn "The upload volume exists. Overwriting."
fi
docker run --rm -v "$upload_volume:/recover" -v "$tmp_dir:/backup" alpine tar -xf /backup/firefly_upload.tar.gz -C /recover --strip 1
restored+=(firefly_upload.tar.gz)
# Improved db_container detection
docker_ps_names=$(docker ps --format '{{.Names}}')
if [ -n "$docker_ps_names" ]; then
db_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_db)
else
db_container=""
fi
if [ -z "$db_container" ]; then
warn "The db container is not running. Not restoring."
else
info 'Restoring database'
cat "$tmp_dir/firefly_db.sql" | docker exec -i "$db_container" bash -c '/usr/bin/mariadb -u $MYSQL_USER --password="$MYSQL_PASSWORD" "$MYSQL_DATABASE"'
restored+=(firefly_db.sql)
fi
restored+=(version.txt)
# Clean up
rm -rf "$tmp_dir"/* "$tmp_dir"/.[!.]* "$tmp_dir"/..?*
rmdir "$tmp_dir"
}
main () {
intro
if [ $# -lt 2 ]; then
fatal "Not enough parameters.\n$(usage)"
fi
backuper_dir="$(dirname $0)"
action=$1
path="$2"
if [ -z "$3" ]; then
no_files=false
else
no_files=true
fi
if [ -d "$path" ]; then
fatal "Path is an existing directory. It has to be a file path"
fi
upload_volume="$(docker volume ls | grep -F "firefly_iii_upload" | tr -s ' ' | cut -d ' ' -f 2)"
if [ "$action" == 'backup' ]; then
backup "$backuper_dir" "$path" "$upload_volume" $no_files
elif [ "$action" == 'restore' ]; then
restore "$backuper_dir" "$path" "$upload_volume" $no_files
else
fatal "Unrecognized action $action\n$(usage)"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment