Skip to content

Instantly share code, notes, and snippets.

@vindard
Last active February 7, 2023 11:41
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save vindard/e0cd3d41bb403a823f3b5002488e3f90 to your computer and use it in GitHub Desktop.
Save vindard/e0cd3d41bb403a823f3b5002488e3f90 to your computer and use it in GitHub Desktop.
A script built off of @alexbosworth's backup script that monitors lnd's `channel.backup` file for changes and uploads those changes to Dropbox when detected.

Lnd Automated Channel Backup Guide

This script was inspired by @alexbosworth's channel.backup backup script and my own .lnd folder backup script. It monitors lnd's channel.backup file for changes and uploads those changes to Dropbox when detected.

Setup Script

To get started, download the script:

$ cd && wget -qN https://gist.githubusercontent.com/vindard/e0cd3d41bb403a823f3b5002488e3f90/raw/4bcf3c0163f77443a6f7c00caae0750b1fa0d63d/lnd-channel-backup.sh
$ sudo chmod +x lnd-channel-backup.sh

Setup Dropbox API Key

In your web browser, do the following:

  1. Go to https://www.dropbox.com/developers/apps/create and sign in

  2. Choose Dropbox Api

    Dropbox API 1

  3. Choose App Folder

    Dropbox API 2

  4. Name your app and click Create App to proceed

    Dropbox API 3

  5. On the settings page for your new app, scroll down to OAuth 2 and click Generate

    Dropbox API 4

  6. You will now see a string of letters and numbers appear. This is your Api Token. Copy this token and keep it safe for the next steps. This api token will be referenced as <dropbox-api-token> in the next step.

  7. Return to your terminal and run the following to insert the api token into the backup script:

    $ TOKEN=<dropbox-api-token>
    $ cd && sed -i "s/DROPBOX_APITOKEN=\".*\"/DROPBOX_APITOKEN=\"$TOKEN\"/" lnd-channel-backup.sh
    $ unset TOKEN
    

Setup script as systemd service

Create file: sudo nano /etc/systemd/system/backup-channels.service

Note: be sure to change the <user-here> value to your own home folder's name

[Service]
ExecStart=/home/<user-here>/lnd-channel-backup.sh
Restart=always
RestartSec=1
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=backup-channels
User=root
Group=root

[Install]
WantedBy=multi-user.target

Start

sudo systemctl start backup-channels

Monitor

journalctl -fu backup-channels

Run at boot

sudo systemctl enable backup-channels

#!/bin/bash
# SET GPG KEY FOR ENCRYPTING WITH (COMPRESSES AS WELL)
GPG=""
# SET DROPBOX API KEY FOR UPLOADS
DROPBOX_APITOKEN=""
# OPTIONAL SET A DEVICE NAME TO BE USED FOR BACKUPS (DEFAULTS TO /etc/hostname)
DEVICE=""
# INOTIFY CHECK
# --------------
install_inotify () {
sudo apt update
sudo apt install -y inotify-tools
}
inotifycheck () {
dpkg -s "inotify-tools" &> /dev/null
if [ ! $? -eq 0 ]; then
install_inotify
fi
}
# SETUP
# --------------
setup_files_and_folders () {
# Setup remote folder name
if [ -z "$DEVICE" ] ; then
DEVICE=$(echo $(cat /etc/hostname))
fi
DEVICE=$(echo $DEVICE | awk '{print tolower($0)}' | sed -e 's/ /-/g')
REMOTE_BACKUP_FOLDER=.clnbackup-$DEVICE
# Setup folders and filenames to upload from
BACKUPS_DIR=/mnt/ext/apps-data/backups
LIGHTNING_BACKUPS_DIR=$BACKUPS_DIR/lightning
SOURCEFILE=$LIGHTNING_BACKUPS_DIR/lightningd.sqlite3.backup
BACKUP_MD5SUM=$LIGHTNING_BACKUPS_DIR/backup.md5
TEMP_MD5="$BACKUP_MD5SUM.temp"
ORIGINALFILE=/home/bitcoin/.lightning/bitcoin/lightningd.sqlite3
if [[ ! -e ${SOURCEFILE} ]]; then
echo "Core-lightning backups not setup yet, exiting..."
return 1
fi
}
# CHECKS
# --------------
online_check () {
wget -q --tries=10 --timeout=20 --spider http://google.com
if [[ $? -eq 0 ]]; then
ONLINE=true
else
ONLINE=false
fi
#echo "Online: "$ONLINE
}
dropbox_api_check () {
VALID_DROPBOX_APITOKEN=false
curl -s -X POST https://api.dropboxapi.com/2/users/get_current_account \
--header "Authorization: Bearer "$DROPBOX_APITOKEN | grep rror
if [[ ! $? -eq 0 ]] ; then
VALID_DROPBOX_APITOKEN=true
else
echo "Invalid Dropbox API Token!"
fi
}
dropbox_upload_check () {
UPLOAD_TO_DROPBOX=false
if [ ! -z $DROPBOX_APITOKEN ] ; then
online_check
if [ $ONLINE = true ] ; then
dropbox_api_check
else
echo "Please check that the internet is connected and try again."
fi
if [ $VALID_DROPBOX_APITOKEN = true ] ; then
UPLOAD_TO_DROPBOX=true
fi
else
echo "Missing value for 'DROPBOX_APITOKEN'"
fi
}
# PREPARE
# --------------
check_md5sum () {
MD5SUM_FILENAME=$1
HASHFILE=$2
if [[ ! -e $MD5SUM_FILENAME ]]; then
echo "File does not exist: $MD5SUM_FILENAME"
return 1
fi
if [[ ! -e "$HASHFILE" ]]; then
return 1
fi
if [[ ! $(md5sum "$MD5SUM_FILENAME" | awk '{print $1}') = $(cat "$HASHFILE") ]]; then
return 1
fi
}
write_md5sum () {
md5sum $1 | awk '{print $1}' > $2
}
get_sizes () {
DB_SIZE=$(du -m "$ORIGINALFILE" | awk '{print $1}')
BACKUP_SIZE=$(du -m "$SOURCEFILE" | awk '{print $1}')
if [ -z $DB_SIZE ]; then
echo "# No $ORIGINALFILE is present"
echo "# Make sure core-ln is setup first"
return 1
fi
}
encrypt_backup () {
FILE_TO_ENCRYPT=$1
ENCRYPTED_FILENAME=$2
if [[ -z $FILE_TO_ENCRYPT ]]; then echo "No file passed to encrypt"; return 1; fi
rm $FILE_TO_ENCRYPT.gpg 2> /dev/null
GPGNOTFOUND=$(gpg -k $GPG 2>&1 >/dev/null | grep -c error)
if [ $GPGNOTFOUND -gt 0 ]; then
gpg --recv-keys $GPG
fi
gpg --trust-model always -r $GPG -e $FILE_TO_ENCRYPT
mv $FILE_TO_ENCRYPT.gpg $ENCRYPTED_FILENAME
}
prepare_file () {
FILE_TO_PREPARE=$1
GPGFILE=$FILE_TO_PREPARE.gpg
TEMPFILE=$2
BACKUP_TAR_FILE=$3
# Copy to get around the issue of incomplete file if
# original file gets edited.
echo "> Copying file to upload..."
cp $FILE_TO_PREPARE $TEMPFILE
write_md5sum "$TEMPFILE" "$BACKUP_MD5SUM.temp"
echo "> Done copying." && echo
UPLOAD_MD5=$(dirname $GPGFILE)/md5-full.txt
echo "> Encrypting '$TEMPFILE'..."
md5sum $TEMPFILE > $UPLOAD_MD5
encrypt_backup \
$TEMPFILE \
$GPGFILE
rm $TEMPFILE
echo "> Done encrypting." && echo
echo "> Packing files into tar archive: $BACKUP_TAR_FILE ..."
pushd $(dirname $GPGFILE) > /dev/null
tar -czf $BACKUP_TAR_FILE \
$(basename $GPGFILE) \
$(basename $UPLOAD_MD5)
rm $GPGFILE $UPLOAD_MD5
popd > /dev/null
echo "> Done packing." && echo
}
run_compaction () {
get_sizes || return 1
# https://github.com/lightningd/plugins/tree/master/backup#performing-backup-compaction
echo "$DB_SIZE MB $ORIGINALFILE"
echo "$BACKUP_SIZE MB $SOURCEFILE"
if [ "$BACKUP_SIZE" -gt $((DB_SIZE+200)) ] ; then
echo "# The backup is 200MB+ larger than the db, running 'lightning-cli backup-compact' ..."
su - bitcoin -c 'lightning-cli backup-compact'
else
echo "The backup is not significantly larger than the db, there is no need to compact."
fi
}
# UPLOAD
# --------------
upload_to_dropbox_single () {
echo "> Upload via single api ($BACKUP_SIZE MB packed in $BACKUP_TAR_FILE_SIZE MB archive)"
FILENAME=$(basename $1)
FINISH=$(curl -s -X POST https://content.dropboxapi.com/2/files/upload \
--header "Authorization: Bearer "${DROPBOX_APITOKEN}"" \
--header "Dropbox-API-Arg: {\"path\": \"/"$REMOTE_BACKUP_FOLDER"/"$FILENAME"\",\"mode\": \"overwrite\",\"autorename\": true,\"mute\": false,\"strict_conflict\": false}" \
--header "Content-Type: application/octet-stream" \
--data-binary @$1)
# echo $FINISH | jq .
UPLOADTIME=$(echo $FINISH | jq -r '.server_modified // empty')
if [ ! -z $UPLOADTIME ] ; then
echo "Successfully uploaded!"
mv $TEMP_MD5 $BACKUP_MD5SUM
else
echo "Unknown error when uploading..."
echo $FINISH | jq \
|| $FINISH
rm $TEMP_MD5
return 1
fi
}
upload_to_dropbox_chunks () {
echo "> Upload via chunks api ($BACKUP_SIZE MB packed in $BACKUP_TAR_FILE_SIZE MB archive)"
FILENAME=$(basename $1)
echo "Starting upload..."
/usr/local/bin/dropbox_uploader.sh \
-f "/home/bitcoin/.dropbox_uploader" \
upload "$1" "$REMOTE_BACKUP_FOLDER/$FILENAME"
# -p \
write_md5sum $TEMPFILE $BACKUP_MD5SUM
}
upload_to_dropbox () {
FILENAME=$(basename $1)
TEMPFILE=$1-temp
BACKUP_TAR_FILE=$1.tar.gz
echo "Starting file encrypt & archive: $1"
prepare_file \
$1 \
$TEMPFILE \
$BACKUP_TAR_FILE
if [[ ! "$?" -eq 0 ]]; then echo "Failed to prepare file"; return 1; fi
if [[ ! -e $BACKUP_TAR_FILE ]]; then echo "Failed to prepare file" && return 1; fi
BACKUP_TAR_FILE_SIZE=$(du -m "$BACKUP_TAR_FILE" | awk '{print $1}')
if [ "$BACKUP_TAR_FILE_SIZE" -lt 290 ] ; then
upload_to_dropbox_single $BACKUP_TAR_FILE || upload_to_dropbox_chunks $BACKUP_TAR_FILE
else
upload_to_dropbox_chunks $BACKUP_TAR_FILE
fi
UPLOAD_STATUS=$?
rm $BACKUP_TAR_FILE
return $UPLOAD_STATUS
}
# RUN CHECKS AND IF PASS, EXECUTE BACKUP TO DROPBOX
run_dropbox_backup () {
dropbox_upload_check
if [ $UPLOAD_TO_DROPBOX = true ] ; then
upload_to_dropbox $1 || return 1
fi
}
run_backup_on_change () {
echo
echo "Checking for compaction..."
run_compaction || return 1
echo
echo "Starting backup file upload: '"${SOURCEFILE}"'"
run_dropbox_backup $SOURCEFILE || return 1
echo "---"
}
##############
# RUN SCRIPT
##############
run () {
inotifycheck
setup_files_and_folders || return 1
while true; do
while ! check_md5sum $SOURCEFILE $BACKUP_MD5SUM; do
echo "File does not match last uploaded md5sum, running upload..."
run_backup_on_change
done
inotifywait $SOURCEFILE
run_backup_on_change
echo
done
}
run
#!/bin/bash
# SET DROPBOX API KEY FOR UPLOADS
DROPBOX_APITOKEN=""
# OPTIONAL SET A DEVICE NAME TO BE USED FOR BACKUPS (DEFAULTS TO /etc/hostname)
DEVICE=""
# INOTIFY CHECK
# --------------
install_inotify () {
sudo apt update
sudo apt install -y inotify-tools
}
inotifycheck () {
dpkg -s "inotify-tools" &> /dev/null
if [ ! $? -eq 0 ]; then
install_inotify
fi
}
# SETUP
# --------------
setup_files_and_folders () {
# Fetches the user whose home folder the directories will be stored under
ADMINUSER=( $(ls /home | grep -v bitcoin) )
if [ -z "$DEVICE" ] ; then
DEVICE=$(echo $(cat /etc/hostname))
fi
DEVICE=$(echo $DEVICE | awk '{print tolower($0)}' | sed -e 's/ /-/g')
# Setup folders and filenames
DATADIR=/home/bitcoin/.lnd
WORKINGDIR=/home/$ADMINUSER/lnd-data-backups
BACKUPFOLDER=.lndbackup-$DEVICE
# channel.backup file details
CHANFILEDIR=data/chain/bitcoin/mainnet
BACKUPFILE=channel.backup
SOURCEFILE=$DATADIR/$CHANFILEDIR/$BACKUPFILE
# Make sure necessary folders exist
if [[ ! -e ${WORKINGDIR} ]]; then
mkdir -p ${WORKINGDIR}
fi
cd ${WORKINGDIR}
if [[ ! -e ${BACKUPFOLDER} ]]; then
mkdir -p ${BACKUPFOLDER}
fi
cd ${BACKUPFOLDER}
}
# CHECKS
# --------------
online_check () {
wget -q --tries=10 --timeout=20 --spider http://google.com
if [[ $? -eq 0 ]]; then
ONLINE=true
else
ONLINE=false
fi
#echo "Online: "$ONLINE
}
dropbox_api_check () {
VALID_DROPBOX_APITOKEN=false
curl -s -X POST https://api.dropboxapi.com/2/users/get_current_account \
--header "Authorization: Bearer "$DROPBOX_APITOKEN | grep rror
if [[ ! $? -eq 0 ]] ; then
VALID_DROPBOX_APITOKEN=true
else
echo "Invalid Dropbox API Token!"
fi
}
dropbox_upload_check () {
UPLOAD_TO_DROPBOX=false
if [ ! -z $DROPBOX_APITOKEN ] ; then
online_check
if [ $ONLINE = true ] ; then
dropbox_api_check
else
echo "Please check that the internet is connected and try again."
fi
if [ $VALID_DROPBOX_APITOKEN = true ] ; then
UPLOAD_TO_DROPBOX=true
fi
fi
}
# UPLOAD
# --------------
upload_to_dropbox () {
FINISH=$(curl -s -X POST https://content.dropboxapi.com/2/files/upload \
--header "Authorization: Bearer "${DROPBOX_APITOKEN}"" \
--header "Dropbox-API-Arg: {\"path\": \"/"$BACKUPFOLDER"/"$1"\",\"mode\": \"overwrite\",\"autorename\": true,\"mute\": false,\"strict_conflict\": false}" \
--header "Content-Type: application/octet-stream" \
--data-binary @$1)
#echo $FINISH | jq .
UPLOADTIME=$(echo $FINISH | jq -r .server_modified)
if [ ! -z $UPLOADTIME ] ; then
echo "Successfully uploaded!"
else
echo "Unknown error when uploading..."
fi
}
# RUN CHECKS AND IF PASS, EXECUTE BACKUP TO DROPBOX
run_dropbox_backup () {
dropbox_upload_check
if [ $UPLOAD_TO_DROPBOX = true ] ; then
upload_to_dropbox $1
fi
}
##############
# RUN SCRIPT
##############
run_backup_on_change () {
echo "Copying backup file..."
cp $SOURCEFILE $BACKUPFILE
md5sum $SOURCEFILE > $BACKUPFILE.md5
sed -i 's/\/.*\///g' $BACKUPFILE.md5
echo
echo "Uploading backup file: '"${BACKUPFILE}"'..."
run_dropbox_backup $BACKUPFILE
echo "---"
echo "Uploading signature: '"${BACKUPFILE}.md5"'..."
run_dropbox_backup $BACKUPFILE.md5
}
run () {
inotifycheck
setup_files_and_folders
while true; do
inotifywait $SOURCEFILE
run_backup_on_change
echo
done
}
run
@giulnz
Copy link

giulnz commented Apr 18, 2019

Hi thanks usuful and clean work. Can i ask you what's md5 purpose ?
Is it for integrity check in case of restore ?
Also i don't understand what sed -i 's/\/.*\///g' do.

@vindard
Copy link
Author

vindard commented Jun 4, 2019

Hey @giulnz, really sorry for the late reply! Yes the md5 is to attach a signature of the original file for any verification purposes. Can be checked when the file is moved off the device, or later on when the file is being restored.

That sed command I believe removes the filename from the string that's returned when md5sum $SOURCEFILE is run. If I remember correctly, it's to save just the md5 hash produced to the .md5 file and to remove any extra data that might have been added along.

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