From 4de1d301db640705fa060d4637ddb91e1205a114 Mon Sep 17 00:00:00 2001 From: Andrei Jiroh Halili Date: Tue, 2 Aug 2022 10:01:16 +0000 Subject: [PATCH 1/2] feat(global): add support for built-in rclone-based backups Scripts and some entrypoint script code are copied from https://github.com/ttionya/vaultwarden-backup, although local development testing through Codespaces is needed to ensure stability before merging to the main branch and test in production at vault.recaptime.eu.org. Signed-off-by: GitHub --- Dockerfile | 18 +- scripts/.gitkeep | 0 scripts/backup-chores.sh | 195 +++++++++++++ scripts/backup-integration-stdlib.sh | 403 +++++++++++++++++++++++++++ scripts/restore-chores.sh | 291 +++++++++++++++++++ scripts/vaultwarden-startup | 128 +++++++++ vaultwarden-startup | 78 ------ 7 files changed, 1026 insertions(+), 87 deletions(-) create mode 100644 scripts/.gitkeep create mode 100644 scripts/backup-chores.sh create mode 100644 scripts/backup-integration-stdlib.sh create mode 100644 scripts/restore-chores.sh create mode 100755 scripts/vaultwarden-startup delete mode 100755 vaultwarden-startup diff --git a/Dockerfile b/Dockerfile index 27a67f4..f415b3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ ################## RUNTIME IMAGE ################### # Create using wellbuilt vaultwarden Docker image as the base image -# Modify vaultwarden-startup to suit in your PaaS service you are using to deploy this +# Modify scripts/vaultwarden-startup to suit in your PaaS service you are building +# this image from source. FROM vaultwarden/server:alpine -COPY vaultwarden-startup /usr/bin/vaultwarden-startup +COPY scripts/ /usr/local/bin/ -ENV PORT 3000 +ENV PORT=3000 TZ=UTC EXPOSE 3000 -## because vaultwarden-startup requires bash, so we install that -RUN apk add bash coreutils \ - # Just in case we're still calling the old stuff - && ln -s /usr/bin/vaultwarden-startup /usr/bin/bwrs-startup +RUN apk add --no-cache \ + # Because the entrypoint script and the backup script itself need rclone and bash, + # we'll install them through apk + heirloom-mailx p7zip sqlite supercronic tzdata bash coreutils rclone WORKDIR / -ENTRYPOINT ["usr/bin/dumb-init", "--"] -CMD ["/usr/bin/vaultwarden-startup"] +ENTRYPOINT ["/usr/local/bin/image-entrypoint"] diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/backup-chores.sh b/scripts/backup-chores.sh new file mode 100644 index 0000000..96e0a85 --- /dev/null +++ b/scripts/backup-chores.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +. /app/includes.sh + +function clear_dir() { + rm -rf "${BACKUP_DIR}" +} + +function backup_init() { + NOW="$(date +"${BACKUP_FILE_DATE_FORMAT}")" + # backup vaultwarden database file + BACKUP_FILE_DB="${BACKUP_DIR}/db.${NOW}.sqlite3" + # backup vaultwarden config file + BACKUP_FILE_CONFIG="${BACKUP_DIR}/config.${NOW}.json" + # backup vaultwarden rsakey files + BACKUP_FILE_RSAKEY="${BACKUP_DIR}/rsakey.${NOW}.tar" + # backup vaultwarden attachments directory + BACKUP_FILE_ATTACHMENTS="${BACKUP_DIR}/attachments.${NOW}.tar" + # backup vaultwarden sends directory + BACKUP_FILE_SENDS="${BACKUP_DIR}/sends.${NOW}.tar" + # backup zip file + BACKUP_FILE_ZIP="${BACKUP_DIR}/backup.${NOW}.${ZIP_TYPE}" +} + +function backup_db() { + color blue "backup vaultwarden sqlite database" + + if [[ -f "${DATA_DB}" ]]; then + sqlite3 "${DATA_DB}" ".backup '${BACKUP_FILE_DB}'" + else + color yellow "not found vaultwarden sqlite database, skipping" + fi +} + +function backup_config() { + color blue "backup vaultwarden config" + + if [[ -f "${DATA_CONFIG}" ]]; then + cp -f "${DATA_CONFIG}" "${BACKUP_FILE_CONFIG}" + else + color yellow "not found vaultwarden config, skipping" + fi +} + +function backup_rsakey() { + color blue "backup vaultwarden rsakey" + + local FIND_RSAKEY=$(find "${DATA_RSAKEY_DIRNAME}" -name "${DATA_RSAKEY_BASENAME}*" | xargs -I {} basename {}) + local FIND_RSAKEY_COUNT=$(echo "${FIND_RSAKEY}" | wc -l) + + if [[ "${FIND_RSAKEY_COUNT}" -gt 0 ]]; then + echo "${FIND_RSAKEY}" | tar -c -C "${DATA_RSAKEY_DIRNAME}" -f "${BACKUP_FILE_RSAKEY}" -T - + + color blue "display rsakey tar file list" + + tar -tf "${BACKUP_FILE_RSAKEY}" + else + color yellow "not found vaultwarden rsakey, skipping" + fi +} + +function backup_attachments() { + color blue "backup vaultwarden attachments" + + if [[ -d "${DATA_ATTACHMENTS}" ]]; then + tar -c -C "${DATA_ATTACHMENTS_DIRNAME}" -f "${BACKUP_FILE_ATTACHMENTS}" "${DATA_ATTACHMENTS_BASENAME}" + + color blue "display attachments tar file list" + + tar -tf "${BACKUP_FILE_ATTACHMENTS}" + else + color yellow "not found vaultwarden attachments directory, skipping" + fi +} + +function backup_sends() { + color blue "backup vaultwarden sends" + + if [[ -d "${DATA_SENDS}" ]]; then + tar -c -C "${DATA_SENDS_DIRNAME}" -f "${BACKUP_FILE_SENDS}" "${DATA_SENDS_BASENAME}" + + color blue "display sends tar file list" + + tar -tf "${BACKUP_FILE_SENDS}" + else + color yellow "not found vaultwarden sends directory, skipping" + fi +} + +function backup() { + mkdir -p "${BACKUP_DIR}" + + backup_db + backup_config + backup_rsakey + backup_attachments + backup_sends + + ls -lah "${BACKUP_DIR}" +} + +function backup_package() { + if [[ "${ZIP_ENABLE}" == "TRUE" ]]; then + color blue "package backup file" + + UPLOAD_FILE="${BACKUP_FILE_ZIP}" + + if [[ "${ZIP_TYPE}" == "zip" ]]; then + 7z a -tzip -mx=9 -p"${ZIP_PASSWORD}" "${BACKUP_FILE_ZIP}" "${BACKUP_DIR}"/* + else + 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on -mhe=on -p"${ZIP_PASSWORD}" "${BACKUP_FILE_ZIP}" "${BACKUP_DIR}"/* + fi + + ls -lah "${BACKUP_DIR}" + + color blue "display backup ${ZIP_TYPE} file list" + + 7z l -p"${ZIP_PASSWORD}" "${BACKUP_FILE_ZIP}" + else + color yellow "skip package backup files" + + UPLOAD_FILE="${BACKUP_DIR}" + fi +} + +function upload() { + # upload file not exist + if [[ ! -e "${UPLOAD_FILE}" ]]; then + color red "upload file not found" + + send_mail_content "FALSE" "File upload failed at $(date +"%Y-%m-%d %H:%M:%S %Z"). Reason: Upload file not found." + + exit 1 + fi + + # upload + local HAS_ERROR="FALSE" + + for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" + do + color blue "upload backup file to storage system $(color yellow "[${RCLONE_REMOTE_X}]")" + + rclone ${RCLONE_GLOBAL_FLAG} copy "${UPLOAD_FILE}" "${RCLONE_REMOTE_X}" + if [[ $? != 0 ]]; then + color red "upload failed" + + HAS_ERROR="TRUE" + fi + done + + if [[ "${HAS_ERROR}" == "TRUE" ]]; then + send_mail_content "FALSE" "File upload failed at $(date +"%Y-%m-%d %H:%M:%S %Z")." + + exit 1 + fi +} + +function clear_history() { + if [[ "${BACKUP_KEEP_DAYS}" -gt 0 ]]; then + for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" + do + color blue "delete ${BACKUP_KEEP_DAYS} days ago backup files $(color yellow "[${RCLONE_REMOTE_X}]")" + + mapfile -t RCLONE_DELETE_LIST < <(rclone ${RCLONE_GLOBAL_FLAG} lsf "${RCLONE_REMOTE_X}" --min-age "${BACKUP_KEEP_DAYS}d") + + for RCLONE_DELETE_FILE in "${RCLONE_DELETE_LIST[@]}" + do + color yellow "deleting \"${RCLONE_DELETE_FILE}\"" + + rclone ${RCLONE_GLOBAL_FLAG} delete "${RCLONE_REMOTE_X}/${RCLONE_DELETE_FILE}" + if [[ $? != 0 ]]; then + color red "delete \"${RCLONE_DELETE_FILE}\" failed" + fi + done + done + fi +} + +color blue "running the backup program at $(date +"%Y-%m-%d %H:%M:%S %Z")" + +init_env +check_rclone_connection + +clear_dir +backup_init +backup +backup_package +upload +clear_dir +clear_history + +send_mail_content "TRUE" "The file was successfully uploaded at $(date +"%Y-%m-%d %H:%M:%S %Z")." +send_ping + +color none "" \ No newline at end of file diff --git a/scripts/backup-integration-stdlib.sh b/scripts/backup-integration-stdlib.sh new file mode 100644 index 0000000..08ef064 --- /dev/null +++ b/scripts/backup-integration-stdlib.sh @@ -0,0 +1,403 @@ +#!/bin/bash + +ENV_FILE="/.env" +CRON_CONFIG_FILE="${HOME}/crontabs" +BACKUP_DIR="/bitwarden/backup" +RESTORE_DIR="/bitwarden/restore" +RESTORE_EXTRACT_DIR="/bitwarden/extract" + +#################### Function #################### +######################################## +# Print colorful message. +# Arguments: +# color +# message +# Outputs: +# colorful message +######################################## +function color() { + case $1 in + red) echo -e "\033[31m$2\033[0m" ;; + green) echo -e "\033[32m$2\033[0m" ;; + yellow) echo -e "\033[33m$2\033[0m" ;; + blue) echo -e "\033[34m$2\033[0m" ;; + none) echo "$2" ;; + esac +} + +######################################## +# Check storage system connection success. +# Arguments: +# None +######################################## +function check_rclone_connection() { + # check configuration exist + rclone ${RCLONE_GLOBAL_FLAG} config show "${RCLONE_REMOTE_NAME}" > /dev/null 2>&1 + if [[ $? != 0 ]]; then + color red "rclone configuration information not found" + color blue "Please configure rclone first, check https://github.com/ttionya/vaultwarden-backup/blob/master/README.md#backup" + exit 1 + fi + + # check connection + local HAS_ERROR="FALSE" + + for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" + do + rclone ${RCLONE_GLOBAL_FLAG} mkdir "${RCLONE_REMOTE_X}" + if [[ $? != 0 ]]; then + color red "storage system connection failure $(color yellow "[${RCLONE_REMOTE_X}]")" + + HAS_ERROR="TRUE" + fi + done + + if [[ "${HAS_ERROR}" == "TRUE" ]]; then + exit 1 + fi +} + +######################################## +# Check file is exist. +# Arguments: +# file +######################################## +function check_file_exist() { + if [[ ! -f "$1" ]]; then + color red "cannot access $1: No such file" + exit 1 + fi +} + +######################################## +# Check directory is exist. +# Arguments: +# directory +######################################## +function check_dir_exist() { + if [[ ! -d "$1" ]]; then + color red "cannot access $1: No such directory" + exit 1 + fi +} + +######################################## +# Send mail by mailx. +# Arguments: +# mail subject +# mail content +# Outputs: +# send mail result +######################################## +function send_mail() { + if [[ "${MAIL_DEBUG}" == "TRUE" ]]; then + local MAIL_VERBOSE="-v" + fi + + echo "$2" | mailx ${MAIL_VERBOSE} -s "$1" ${MAIL_SMTP_VARIABLES} "${MAIL_TO}" + if [[ $? != 0 ]]; then + color red "mail sending failed" + else + color blue "mail send was successfully" + fi +} + +######################################## +# Send mail. +# Arguments: +# backup successful +# mail content +######################################## +function send_mail_content() { + if [[ "${MAIL_SMTP_ENABLE}" == "FALSE" ]]; then + return + fi + + # successful + if [[ "$1" == "TRUE" && "${MAIL_WHEN_SUCCESS}" == "TRUE" ]]; then + send_mail "vaultwarden Backup Success" "$2" + fi + + # failed + if [[ "$1" == "FALSE" && "${MAIL_WHEN_FAILURE}" == "TRUE" ]]; then + send_mail "vaultwarden Backup Failed" "$2" + fi +} + +######################################## +# Send health check ping. +# Arguments: +# None +######################################## +function send_ping() { + if [[ -z "${PING_URL}" ]]; then + return + fi + + wget "${PING_URL}" -T 15 -t 10 -O /dev/null -q + if [[ $? != 0 ]]; then + color red "ping sending failed" + else + color blue "ping send was successfully" + fi +} + +######################################## +# Export variables from .env file. +# Arguments: +# None +# Outputs: +# variables with prefix 'DOTENV_' +# Reference: +# https://gist.github.com/judy2k/7656bfe3b322d669ef75364a46327836#gistcomment-3632918 +######################################## +function export_env_file() { + if [[ -f "${ENV_FILE}" ]]; then + color blue "find \"${ENV_FILE}\" file and export variables" + set -a + source <(cat "${ENV_FILE}" | sed -e '/^#/d;/^\s*$/d' -e 's/\(\w*\)[ \t]*=[ \t]*\(.*\)/DOTENV_\1=\2/') + set +a + fi +} + +######################################## +# Get variables from +# environment variables, +# secret file in environment variables, +# secret file in .env file, +# environment variables in .env file. +# Arguments: +# variable name +# Outputs: +# variable value +######################################## +function get_env() { + local VAR="$1" + local VAR_FILE="${VAR}_FILE" + local VAR_DOTENV="DOTENV_${VAR}" + local VAR_DOTENV_FILE="DOTENV_${VAR_FILE}" + local VALUE="" + + if [[ -n "${!VAR:-}" ]]; then + VALUE="${!VAR}" + elif [[ -n "${!VAR_FILE:-}" ]]; then + VALUE="$(cat "${!VAR_FILE}")" + elif [[ -n "${!VAR_DOTENV_FILE:-}" ]]; then + VALUE="$(cat "${!VAR_DOTENV_FILE}")" + elif [[ -n "${!VAR_DOTENV:-}" ]]; then + VALUE="${!VAR_DOTENV}" + fi + + export "${VAR}=${VALUE}" +} + +######################################## +# Get RCLONE_REMOTE_LIST variables. +# Arguments: +# None +# Outputs: +# variable value +######################################## +function get_rclone_remote_list() { + # RCLONE_REMOTE_LIST + RCLONE_REMOTE_LIST=() + + local i=0 + local RCLONE_REMOTE_NAME_X_REFER + local RCLONE_REMOTE_DIR_X_REFER + local RCLONE_REMOTE_X + + # for multiple + while true; do + RCLONE_REMOTE_NAME_X_REFER="RCLONE_REMOTE_NAME_${i}" + RCLONE_REMOTE_DIR_X_REFER="RCLONE_REMOTE_DIR_${i}" + get_env "${RCLONE_REMOTE_NAME_X_REFER}" + get_env "${RCLONE_REMOTE_DIR_X_REFER}" + + if [[ -z "${!RCLONE_REMOTE_NAME_X_REFER}" || -z "${!RCLONE_REMOTE_DIR_X_REFER}" ]]; then + break + fi + + RCLONE_REMOTE_X=$(echo "${!RCLONE_REMOTE_NAME_X_REFER}:${!RCLONE_REMOTE_DIR_X_REFER}" | sed 's@\(/*\)$@@') + RCLONE_REMOTE_LIST=(${RCLONE_REMOTE_LIST[@]} "${RCLONE_REMOTE_X}") + + ((i++)) + done +} + +######################################## +# Initialization environment variables. +# Arguments: +# None +# Outputs: +# environment variables +######################################## +function init_env() { + # export + export_env_file + + init_env_dir + init_env_mail + + # CRON + get_env CRON + CRON="${CRON:-"5 * * * *"}" + + # RCLONE_REMOTE_NAME + get_env RCLONE_REMOTE_NAME + RCLONE_REMOTE_NAME="${RCLONE_REMOTE_NAME:-"BitwardenBackup"}" + RCLONE_REMOTE_NAME_0="${RCLONE_REMOTE_NAME}" + + # RCLONE_REMOTE_DIR + get_env RCLONE_REMOTE_DIR + RCLONE_REMOTE_DIR="${RCLONE_REMOTE_DIR:-"/BitwardenBackup/"}" + RCLONE_REMOTE_DIR_0="${RCLONE_REMOTE_DIR}" + + # get RCLONE_REMOTE_LIST + get_rclone_remote_list + + # RCLONE_GLOBAL_FLAG + get_env RCLONE_GLOBAL_FLAG + RCLONE_GLOBAL_FLAG="${RCLONE_GLOBAL_FLAG:-""}" + + # ZIP_ENABLE + get_env ZIP_ENABLE + ZIP_ENABLE=$(echo "${ZIP_ENABLE}" | tr '[a-z]' '[A-Z]') + if [[ "${ZIP_ENABLE}" == "FALSE" ]]; then + ZIP_ENABLE="FALSE" + else + ZIP_ENABLE="TRUE" + fi + + # ZIP_PASSWORD + get_env ZIP_PASSWORD + ZIP_PASSWORD="${ZIP_PASSWORD:-"WHEREISMYPASSWORD?"}" + + # ZIP_TYPE + get_env ZIP_TYPE + ZIP_TYPE=$(echo "${ZIP_TYPE}" | tr '[A-Z]' '[a-z]') + if [[ "${ZIP_TYPE}" == "7z" ]]; then + ZIP_TYPE="7z" + else + ZIP_TYPE="zip" + fi + + # BACKUP_KEEP_DAYS + get_env BACKUP_KEEP_DAYS + BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-"0"}" + + # BACKUP_FILE_DATE_FORMAT + get_env BACKUP_FILE_DATE_SUFFIX + BACKUP_FILE_DATE_FORMAT=$(echo "%Y%m%d${BACKUP_FILE_DATE_SUFFIX}" | sed 's/[^0-9a-zA-Z%_-]//g') + + # PING_URL + get_env PING_URL + PING_URL="${PING_URL:-""}" + + # TIMEZONE + get_env TIMEZONE + local TIMEZONE_MATCHED_COUNT=$(ls "/usr/share/zoneinfo/${TIMEZONE}" 2> /dev/null | wc -l) + if [[ "${TIMEZONE_MATCHED_COUNT}" -ne 1 ]]; then + TIMEZONE="UTC" + fi + + color yellow "========================================" + color yellow "DATA_DIR: ${DATA_DIR}" + color yellow "DATA_DB: ${DATA_DB}" + color yellow "DATA_CONFIG: ${DATA_CONFIG}" + color yellow "DATA_RSAKEY: ${DATA_RSAKEY}" + color yellow "DATA_ATTACHMENTS: ${DATA_ATTACHMENTS}" + color yellow "DATA_SENDS: ${DATA_SENDS}" + color yellow "========================================" + color yellow "CRON: ${CRON}" + + for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" + do + color yellow "RCLONE_REMOTE: ${RCLONE_REMOTE_X}" + done + + color yellow "RCLONE_GLOBAL_FLAG: ${RCLONE_GLOBAL_FLAG}" + color yellow "ZIP_ENABLE: ${ZIP_ENABLE}" + color yellow "ZIP_PASSWORD: ${#ZIP_PASSWORD} Chars" + color yellow "ZIP_TYPE: ${ZIP_TYPE}" + color yellow "BACKUP_FILE_DATE_FORMAT: ${BACKUP_FILE_DATE_FORMAT}" + color yellow "BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS}" + if [[ -n "${PING_URL}" ]]; then + color yellow "PING_URL: ${PING_URL}" + fi + color yellow "MAIL_SMTP_ENABLE: ${MAIL_SMTP_ENABLE}" + if [[ "${MAIL_SMTP_ENABLE}" == "TRUE" ]]; then + color yellow "MAIL_TO: ${MAIL_TO}" + color yellow "MAIL_WHEN_SUCCESS: ${MAIL_WHEN_SUCCESS}" + color yellow "MAIL_WHEN_FAILURE: ${MAIL_WHEN_FAILURE}" + fi + color yellow "TIMEZONE: ${TIMEZONE}" + color yellow "========================================" +} + +function init_env_dir() { + # DATA_DIR + get_env DATA_DIR + DATA_DIR="${DATA_DIR:-"/bitwarden/data"}" + check_dir_exist "${DATA_DIR}" + + # DATA_DB + get_env DATA_DB + DATA_DB="${DATA_DB:-"${DATA_DIR}/db.sqlite3"}" + + # DATA_CONFIG + DATA_CONFIG="${DATA_DIR}/config.json" + + # DATA_RSAKEY + get_env DATA_RSAKEY + DATA_RSAKEY="${DATA_RSAKEY:-"${DATA_DIR}/rsa_key"}" + DATA_RSAKEY_DIRNAME="$(dirname "${DATA_RSAKEY}")" + DATA_RSAKEY_BASENAME="$(basename "${DATA_RSAKEY}")" + + # DATA_ATTACHMENTS + get_env DATA_ATTACHMENTS + DATA_ATTACHMENTS="$(dirname "${DATA_ATTACHMENTS:-"${DATA_DIR}/attachments"}/useless")" + DATA_ATTACHMENTS_DIRNAME="$(dirname "${DATA_ATTACHMENTS}")" + DATA_ATTACHMENTS_BASENAME="$(basename "${DATA_ATTACHMENTS}")" + + # DATA_SEND + get_env DATA_SENDS + DATA_SENDS="$(dirname "${DATA_SENDS:-"${DATA_DIR}/sends"}/useless")" + DATA_SENDS_DIRNAME="$(dirname "${DATA_SENDS}")" + DATA_SENDS_BASENAME="$(basename "${DATA_SENDS}")" +} + +function init_env_mail() { + # MAIL_SMTP_ENABLE + # MAIL_TO + get_env MAIL_SMTP_ENABLE + get_env MAIL_TO + MAIL_SMTP_ENABLE=$(echo "${MAIL_SMTP_ENABLE}" | tr '[a-z]' '[A-Z]') + if [[ "${MAIL_SMTP_ENABLE}" == "TRUE" && "${MAIL_TO}" ]]; then + MAIL_SMTP_ENABLE="TRUE" + else + MAIL_SMTP_ENABLE="FALSE" + fi + + # MAIL_SMTP_VARIABLES + get_env MAIL_SMTP_VARIABLES + MAIL_SMTP_VARIABLES="${MAIL_SMTP_VARIABLES:-""}" + + # MAIL_WHEN_SUCCESS + get_env MAIL_WHEN_SUCCESS + MAIL_WHEN_SUCCESS=$(echo "${MAIL_WHEN_SUCCESS}" | tr '[a-z]' '[A-Z]') + if [[ "${MAIL_WHEN_SUCCESS}" == "FALSE" ]]; then + MAIL_WHEN_SUCCESS="FALSE" + else + MAIL_WHEN_SUCCESS="TRUE" + fi + + # MAIL_WHEN_FAILURE + get_env MAIL_WHEN_FAILURE + MAIL_WHEN_FAILURE=$(echo "${MAIL_WHEN_FAILURE}" | tr '[a-z]' '[A-Z]') + if [[ "${MAIL_WHEN_FAILURE}" == "FALSE" ]]; then + MAIL_WHEN_FAILURE="FALSE" + else + MAIL_WHEN_FAILURE="TRUE" + fi +} \ No newline at end of file diff --git a/scripts/restore-chores.sh b/scripts/restore-chores.sh new file mode 100644 index 0000000..86c3539 --- /dev/null +++ b/scripts/restore-chores.sh @@ -0,0 +1,291 @@ +#!/bin/bash + +. /app/includes.sh + +RESTORE_FILE_DB="" +RESTORE_FILE_CONFIG="" +RESTORE_FILE_RSAKEY="" +RESTORE_FILE_ATTACHMENTS="" +RESTORE_FILE_SENDS="" +RESTORE_FILE_ZIP="" +RESTORE_FILE_DIR="${RESTORE_DIR}" +ZIP_PASSWORD="" +FORCE_RESTORE="FALSE" + +function clear_extract_dir() { + rm -rf "${RESTORE_EXTRACT_DIR}" +} + +function restore_zip() { + color blue "restore vaultwarden backup zip file" + + local FIND_FILE_DB + local FIND_FILE_CONFIG + local FIND_FILE_RSAKEY + local FIND_FILE_ATTACHMENTS + local FIND_FILE_SENDS + + if [[ -n "${ZIP_PASSWORD}" ]]; then + 7z e -aoa -p"${ZIP_PASSWORD}" -o"${RESTORE_EXTRACT_DIR}" "${RESTORE_FILE_ZIP}" + else + 7z e -aoa -o"${RESTORE_EXTRACT_DIR}" "${RESTORE_FILE_ZIP}" + fi + + if [[ $? == 0 ]]; then + color green "extract vaultwarden backup zip file successful" + else + color red "extract vaultwarden backup zip file failed" + exit 1 + fi + + # get restore db file + FIND_FILE_DB="$( basename "$(ls ${RESTORE_EXTRACT_DIR}/db.*.sqlite3 2>/dev/null)" )" + RESTORE_FILE_DB="${FIND_FILE_DB:-}" + + # get restore config file + FIND_FILE_CONFIG="$( basename "$(ls ${RESTORE_EXTRACT_DIR}/config.*.json 2>/dev/null)" )" + RESTORE_FILE_CONFIG="${FIND_FILE_CONFIG:-}" + + # get restore rsakey file + FIND_FILE_RSAKEY="$( basename "$(ls ${RESTORE_EXTRACT_DIR}/rsakey.*.tar 2>/dev/null)" )" + RESTORE_FILE_RSAKEY="${FIND_FILE_RSAKEY:-}" + + # get restore attachments file + FIND_FILE_ATTACHMENTS="$( basename "$(ls ${RESTORE_EXTRACT_DIR}/attachments.*.tar 2>/dev/null)" )" + RESTORE_FILE_ATTACHMENTS="${FIND_FILE_ATTACHMENTS:-}" + + # get restore sends file + FIND_FILE_SENDS="$( basename "$(ls ${RESTORE_EXTRACT_DIR}/sends.*.tar 2>/dev/null)" )" + RESTORE_FILE_SENDS="${FIND_FILE_SENDS:-}" + + RESTORE_FILE_ZIP="" + RESTORE_FILE_DIR="${RESTORE_EXTRACT_DIR}" + restore_file +} + +function restore_db() { + color blue "restore vaultwarden sqlite database" + + cp -f "${RESTORE_FILE_DB}" "${DATA_DB}" + + if [[ $? == 0 ]]; then + color green "restore vaultwarden sqlite database successful" + else + color red "restore vaultwarden sqlite database failed" + fi +} + +function restore_config() { + color blue "restore vaultwarden config" + + cp -f "${RESTORE_FILE_CONFIG}" "${DATA_CONFIG}" + + if [[ $? == 0 ]]; then + color green "restore vaultwarden config successful" + else + color red "restore vaultwarden config failed" + fi +} + +function restore_rsakey() { + color blue "restore vaultwarden rsakey" + + mkdir -p "${DATA_RSAKEY_DIRNAME}" + tar -x -C "${DATA_RSAKEY_DIRNAME}" -f "${RESTORE_FILE_RSAKEY}" + + if [[ $? == 0 ]]; then + color green "restore vaultwarden rsakey successful" + else + color red "restore vaultwarden rsakey failed" + fi +} + +function restore_attachments() { + color blue "restore vaultwarden attachments" + + # When customizing the attachments folder, the root directory of the tar file + # is the directory name at the time of packing + local RESTORE_FILE_ATTACHMENTS_DIRNAME=$(tar -tf "${RESTORE_FILE_ATTACHMENTS}" | head -n 1 | xargs basename) + local DATA_ATTACHMENTS_EXTRACT="${DATA_ATTACHMENTS}.extract" + + rm -rf "${DATA_ATTACHMENTS}" "${DATA_ATTACHMENTS_EXTRACT}" + mkdir -p "${DATA_ATTACHMENTS_EXTRACT}" + tar -x -C "${DATA_ATTACHMENTS_EXTRACT}" -f "${RESTORE_FILE_ATTACHMENTS}" + mv "${DATA_ATTACHMENTS_EXTRACT}/${RESTORE_FILE_ATTACHMENTS_DIRNAME}" "${DATA_ATTACHMENTS}" + rm -rf "${DATA_ATTACHMENTS_EXTRACT}" + + if [[ $? == 0 ]]; then + color green "restore vaultwarden attachments successful" + else + color red "restore vaultwarden attachments failed" + fi +} + +function restore_sends() { + color blue "restore vaultwarden sends" + + # When customizing the sends folder, the root directory of the tar file + # is the directory name at the time of packing + local RESTORE_FILE_SENDS_DIRNAME=$(tar -tf "${RESTORE_FILE_SENDS}" | head -n 1 | xargs basename) + local DATA_SENDS_EXTRACT="${DATA_SENDS}.extract" + + rm -rf "${DATA_SENDS}" "${DATA_SENDS_EXTRACT}" + mkdir -p "${DATA_SENDS_EXTRACT}" + tar -x -C "${DATA_SENDS_EXTRACT}" -f "${RESTORE_FILE_SENDS}" + mv "${DATA_SENDS_EXTRACT}/${RESTORE_FILE_SENDS_DIRNAME}" "${DATA_SENDS}" + rm -rf "${DATA_SENDS_EXTRACT}" + + if [[ $? == 0 ]]; then + color green "restore vaultwarden sends successful" + else + color red "restore vaultwarden sends failed" + fi +} + +function check_restore_file_exist() { + if [[ ! -f "${RESTORE_FILE_DIR}/$1" ]]; then + color red "$2: cannot access $1: No such file" + exit 1 + fi +} + +function restore_file() { + if [[ -n "${RESTORE_FILE_ZIP}" ]]; then + check_restore_file_exist "${RESTORE_FILE_ZIP}" "--zip-file" + + RESTORE_FILE_ZIP="${RESTORE_FILE_DIR}/${RESTORE_FILE_ZIP}" + + clear_extract_dir + restore_zip + clear_extract_dir + else + if [[ -n "${RESTORE_FILE_DB}" ]]; then + check_restore_file_exist "${RESTORE_FILE_DB}" "--db-file" + + RESTORE_FILE_DB="${RESTORE_FILE_DIR}/${RESTORE_FILE_DB}" + fi + + if [[ -n "${RESTORE_FILE_CONFIG}" ]]; then + check_restore_file_exist "${RESTORE_FILE_CONFIG}" "--config-file" + + RESTORE_FILE_CONFIG="${RESTORE_FILE_DIR}/${RESTORE_FILE_CONFIG}" + fi + + if [[ -n "${RESTORE_FILE_RSAKEY}" ]]; then + check_restore_file_exist "${RESTORE_FILE_RSAKEY}" "--rsakey-file" + + RESTORE_FILE_RSAKEY="${RESTORE_FILE_DIR}/${RESTORE_FILE_RSAKEY}" + fi + + if [[ -n "${RESTORE_FILE_ATTACHMENTS}" ]]; then + check_restore_file_exist "${RESTORE_FILE_ATTACHMENTS}" "--attachments-file" + + RESTORE_FILE_ATTACHMENTS="${RESTORE_FILE_DIR}/${RESTORE_FILE_ATTACHMENTS}" + fi + + if [[ -n "${RESTORE_FILE_SENDS}" ]]; then + check_restore_file_exist "${RESTORE_FILE_SENDS}" "--sends-file" + + RESTORE_FILE_SENDS="${RESTORE_FILE_DIR}/${RESTORE_FILE_SENDS}" + fi + + if [[ -n "${RESTORE_FILE_DB}" ]]; then + restore_db + fi + if [[ -n "${RESTORE_FILE_CONFIG}" ]]; then + restore_config + fi + if [[ -n "${RESTORE_FILE_RSAKEY}" ]]; then + restore_rsakey + fi + if [[ -n "${RESTORE_FILE_ATTACHMENTS}" ]]; then + restore_attachments + fi + if [[ -n "${RESTORE_FILE_SENDS}" ]]; then + restore_sends + fi + fi +} + +function check_empty_input() { + if [[ -z "${RESTORE_FILE_ZIP}${RESTORE_FILE_DB}${RESTORE_FILE_CONFIG}${RESTORE_FILE_RSAKEY}${RESTORE_FILE_ATTACHMENTS}${RESTORE_FILE_SENDS}" ]]; then + color yellow "Empty input" + color none "" + color none "Find out more at https://github.com/ttionya/vaultwarden-backup#restore" + exit 0 + fi +} + +function check_data_dir_exist() { + if [[ ! -d "${DATA_DIR}" ]]; then + color red "vaultwarden data directory not found" + exit 1 + fi +} + +function restore() { + local READ_RESTORE_CONTINUE + + while [[ $# -gt 0 ]]; do + case "$1" in + -p|--password) + shift + ZIP_PASSWORD="$1" + shift + ;; + --zip-file) + shift + RESTORE_FILE_ZIP="$(basename "$1")" + shift + ;; + --db-file) + shift + RESTORE_FILE_DB="$(basename "$1")" + shift + ;; + --config-file) + shift + RESTORE_FILE_CONFIG="$(basename "$1")" + shift + ;; + --rsakey-file) + shift + RESTORE_FILE_RSAKEY="$(basename "$1")" + shift + ;; + --attachments-file) + shift + RESTORE_FILE_ATTACHMENTS="$(basename "$1")" + shift + ;; + --sends-file) + shift + RESTORE_FILE_SENDS="$(basename "$1")" + shift + ;; + -f|--force-restore) + shift + FORCE_RESTORE="TRUE" + shift + ;; + *) + color red "Illegal input" + exit 1 + ;; + esac + done + + init_env_dir + check_empty_input + check_data_dir_exist + + if [[ "${FORCE_RESTORE}" == "TRUE" ]]; then + restore_file + else + color yellow "Restore will overwrite the existing files, continue? (y/N)" + read -p "(Default: n): " READ_RESTORE_CONTINUE + if [[ $(echo "${READ_RESTORE_CONTINUE:-n}" | tr [a-z] [A-Z]) == "Y" ]]; then + restore_file + fi + fi +} \ No newline at end of file diff --git a/scripts/vaultwarden-startup b/scripts/vaultwarden-startup new file mode 100755 index 0000000..873d5e5 --- /dev/null +++ b/scripts/vaultwarden-startup @@ -0,0 +1,128 @@ +#!/usr/bin/dumb-init /bin/bash +#shellcheck shell=bash + +# sources the backup integration's base file where it includes necessary +# shell functions and +source /usr/local/bin/backup-integration-stdlib.sh + +if [[ $TZ != "UTC" ]]; then + TIMEZONE=$TZ +else + TIMEZONE="UTC" +fi + +if [[ "${MAKE_IT_FAIL}" != "" ]]; then + echo "error: MAKE_IT_FAIL variable is defined, aborting startup..." + echo "error: If you ready to check your environment variables for any configiration issues," + echo "error: either set it to 'false' or just remove it entirely on your SaaS provider's" + echo "error: env vars settings page. Exiting..." + exit 2 +elif [[ "${MAKE_IT_FAIL}" == "" ]]; then + echo "==> Configuration seems to be fine, proceeding..." +fi +sleep 2 + +# Let Rocket bind the PORT defined on our Dockerfile +export ROCKET_PORT="${PORT:-"8080"}" + +if [[ ${FF_RCLONE_BACKUP} == "" ]] && [[ ${RCLONE_CONFIG} == "" ]]; then + export ORG_ATTACHMENT_LIMIT=0 USER_ATTACHMENT_LIMIT=0 +elif [[ ${FF_RCLONE_BACKUP} == "true" ]] && [[ ${RCLONE_CONFIG} != "" ]]; then + echo "info: attempting to restore rclone config from base64-encoded form..." + if ! echo "${RECLONE_CONFIG}" | base64 -d > /root/.config/rclone/rclone.config; then + echo "warning: base64 decode failed, disabling feature flag..." + export FF_RCLONE_BACKUP=false + fi + export ORG_ATTACHMENT_LIMIT=${ORG_ATTACHMENT_LIMIT:-"1024000"} USER_ATTACHMENT_LIMIT=${USER_ATTACHMENT_LIMIT:-"1024000"} +fi + +# allow icons to be cached on our container in meanwhile +export DISABLE_ICON_DOWNLOAD=false +# Icon caches management +export ICON_CACHE_TTL=120 +export ICON_CACHE_NEGTTL=0 + +# Checks for different providers to provide customized error messages and some fixes, +# especially for IP addresses. +if [[ -n "$RAILWAY_STATIC_URL" ]]; then + export DEPLOY_METHOD="railway" + export IP_HEADER=X-Forwarded-For +elif [[ -n "$SSO_DSN" ]] || [[ -n "$DOMAIN_ALIASES" ]]; then + export DEPLOY_METHOD="divio" + export IP_HEADER=X-Forwarded-For +fi + +if [[ "${ENABLE_ADMIN}" == "true" ]]; then + echo "==> Enabling admin dashboard..." + if [[ "${ADMIN_TOKEN}" == "" ]]; then + echo "warn: No token found for the admin dashboard, generating one" + export ADMIN_TOKEN=$(openssl rand -hex 24) + echo "warn: Generated token is $ADMIN_TOKEN, this will be reset on restart unless set." + fi +else + echo "==> Admin panel is disabled, attempting to remove ADMIN_TOKEN..." + unset ADMIN_TOKEN +fi +echo +sleep 3 + +echo "==> Checking for RSA_CONTENT..." +sleep 3 +if [ -z "${RSA_CONTENT}" ]; then + echo "warn: RSA_CONTENT is missing, so you'll be logged out after leaving the web vault page." + echo "warn: To remove this warning, please run './tools/generate-rsakey ${DEPLOY_METHOD}' on your machine" +elif [[ ${FF_RCLONE_BACKUP} != "true" ]]; then + echo "==> RSA_CONTENT found, attempting to decode..." + if echo "${RSA_CONTENT}" | base64 -d | tar zxf -; then + export RSA_KEY_FILENAME=${PWD}/rsa_key + else + echo "error: Failed to either decode base64 or untar decoded contents." + echo "error: Run './tools/generate-rsakey ${DEPLOY_METHOD}' on your manchine" + exit 1 + fi +else + echo "info: skipped due to backups enabled" +fi +echo +sleep 3 + +# Do some checks if the database is there +if [[ -z "${DATABASE_URL}" ]] && [[ ${FF_RCLONE_BACKUP} != "true" ]]; then + echo "error: DATABASE_URL is undefined, thus the server will not start. To continue, add an DATABASE_URL secret on your PaaS service." + echo "error: If you're running locally, pease either setup an Postgres DB or presistence through rclone" + exit 1 +fi + +function configure_timezone() { + ln -sf "/usr/share/zoneinfo/${TIMEZONE}" "${LOCALTIME_FILE}" +} + +function configure_cron() { + local FIND_CRON_COUNT="$(grep -c 'backup.sh' "${CRON_CONFIG_FILE}" 2> /dev/null)" + if [[ "${FIND_CRON_COUNT}" -eq 0 ]]; then + echo "${CRON} bash /app/backup.sh" >> "${CRON_CONFIG_FILE}" + fi +} + +function setup_remote_access() { + ssh-keygen -A + /usr/sbin/sshd + if [[ $FF_TAILSCALE == "true" ]]; then + /app/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 & + tailscale up --authkey "${TAILSCALE_AUTHKEY}" --hostname="$TAILSCALE_HOSTNAME" + fi +} + +if [[ "${FF_RCLONE_BACKUP}" == "true" ]]; then + init_env + check_rclone_connection + configure_timezone + configure_cron +fi + +echo "==> Checks done, starting up..." +if [[ $FF_RCLONE_BACKUP == "true" ]]; then + concurrently --prefix "[{name} {pid}]" --kill-others "/start.sh" "supercronic -passthrough-logs -quiet \"${CRON_CONFIG_FILE}\"" +else + bash /start.sh +fi \ No newline at end of file diff --git a/vaultwarden-startup b/vaultwarden-startup deleted file mode 100755 index f7487b9..0000000 --- a/vaultwarden-startup +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -if [[ "${MAKE_IT_FAIL}" == "true" ]]; then - echo "error: MAKE_IT_FAIL is defined, aborting..." - echo "error: If you ready to check your environment variables for any configiration issues," - echo "error: either set it to 'false' or just remove it entirely on your SaaS provider's" - echo "error: env vars settings page. Exiting..." - exit -elif [[ "${MAKE_IT_FAIL}" == "false" ]]; then - echo "==> Looks fine, starting up..." -else - echo "==> MAKE_IT_FAIL var contains unsupported options, starting up..." - echo " Only true or false are supported. Leaving it blank is also fine." -fi -sleep 2 - -# Let Rocket bind the PORT defined on our Dockerfile -export ROCKET_PORT="${PORT:-"8080"}" - -# While its possible to write stuff in the epherimal storage, we prefer to keep these disabled. -# We'll do some if-then statements if you configured these in the future. -export ORG_ATTACHMENT_LIMIT=0 -export USER_ATTACHMENT_LIMIT=0 -# allow icons to be cached on our container in meanwhile -export DISABLE_ICON_DOWNLOAD=false -# Icon caches management -export ICON_CACHE_TTL=120 -export ICON_CACHE_NEGTTL=0 - -# Checks for different providers to provide customized error messages and some fixes. -if [[ -n "$RAILWAY_STATIC_URL" ]]; then - export DEPLOY_METHOD="railway" - export IP_HEADER=X-Forwarded-For -elif [[ -n "$SSO_DSN" ]] || [[ -n "$DOMAIN_ALIASES" ]]; then - export DEPLOY_METHOD="divio" - export IP_HEADER=X-Forwarded-For -fi - -if [[ "${ENABLE_ADMIN}" == "true" ]]; then - echo "==> Enabling admin dashboard..." - if [[ "${ADMIN_TOKEN}" == "" ]]; then - echo " No token found for the admin dashboard, unsetting..." - unset ADMIN_TOKEN - fi -else - echo "==> Admin panel is disabled, removing ADMIN_TOKEN..." - unset ADMIN_TOKEN -fi -echo -sleep 3 - -echo "==> Checking for RSA_CONTENT..." -sleep 3 -if [ -z "${RSA_CONTENT}" ]; then - echo "warn: RSA_CONTENT is missing, so you'll be logged out after leaving the web vault page." - echo "warn: To remove this warning, please run './tools/generate-rsakey ${DEPLOY_METHOD}' on your machine" -else - echo "==> RSA_CONTENT found, attempting to decode..." - if echo "${RSA_CONTENT}" | base64 -d | tar zxf -; then - export RSA_KEY_FILENAME=${PWD}/rsa_key - else - echo "error: Failed to either decode base64 or untar decoded contents." - echo "error: Run './tools/generate-rsakey ${DEPLOY_METHOD}' on your manchine" - exit 1 - fi -fi -echo -sleep 3 - -# Do some checks if the database is there -if [[ -z "${DATABASE_URL}" ]]; then - echo "error: DATABASE_URL is undefined, thus the server will not start. To continue, add an DATABASE_URL secret on your PaaS service." - echo "error: If you're running locally, ease setup an Postgres instance" - exit 1 -fi - -echo "==> Checks done, starting up..." -/bin/sh /start.sh From 347ed49c5f401a571fc64c72437edac24b1373b6 Mon Sep 17 00:00:00 2001 From: Andrei Jiroh Halili Date: Tue, 2 Aug 2022 10:11:33 +0000 Subject: [PATCH 2/2] fix(docker): add npm install part for concurrently We use concurrently if rclone backup feature is enabled to make sure its not we're not firing up command not found when running the container. Signed-off-by: GitHub --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f415b3a..560a9f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,8 @@ EXPOSE 3000 RUN apk add --no-cache \ # Because the entrypoint script and the backup script itself need rclone and bash, # we'll install them through apk - heirloom-mailx p7zip sqlite supercronic tzdata bash coreutils rclone + heirloom-mailx p7zip sqlite supercronic tzdata bash coreutils rclone npm \ + && npm i -g concurrently WORKDIR / ENTRYPOINT ["/usr/local/bin/image-entrypoint"]