├── .gitignore ├── .editorconfig ├── .env ├── Dockerfile ├── .dockerignore ├── docker-compose.yml ├── scripts ├── entrypoint.sh ├── download-actual-budget.js ├── backup.sh └── includes.sh ├── docs ├── multiple-sync-ids.md ├── manually-trigger-a-backup.md ├── e2e-encrypted-backups.md ├── multiple-remote-destinations.md └── getting-started.md ├── LICENSE ├── .github └── workflows │ └── docker-image.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # IDE 5 | .vscode 6 | .idea 7 | 8 | # Log 9 | *.log 10 | *.log.* 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.sh] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 1. Please put the value in double quotes to avoid problems. 2 | # 2. To use the file, you need to map the file to `/.env` in the container. 3 | 4 | # RCLONE_REMOTE_NAME="ActualBudgetBackup" 5 | # RCLONE_REMOTE_DIR="/ActualBudgetBackup/" 6 | # RCLONE_GLOBAL_FLAG="" 7 | # CRON="0 0 * * *" 8 | # BACKUP_FILE_SUFFIX="%Y%m%d" 9 | # BACKUP_KEEP_DAYS="0" 10 | # TIMEZONE="UTC" 11 | # ACTUAL_BUDGET_URL= #without quotes and the last / 12 | # ACTUAL_BUDGET_PASSWORD= #without quotes 13 | # ACTUAL_BUDGET_SYNC_ID= #without quotes 14 | # ACTUAL_BUDGET_E2E_PASSWORD=#without quotes -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rclone/rclone:1.66.0 2 | 3 | LABEL "repository"="https://github.com/rodriguestiago0/actual-backup" \ 4 | "homepage"="https://github.com/rodriguestiago0/actual-backup" 5 | 6 | ARG USER_NAME="backuptool" 7 | ARG USER_ID="1100" 8 | 9 | ENV LOCALTIME_FILE="/tmp/localtime" 10 | 11 | COPY scripts/*.js /app/ 12 | COPY scripts/*.sh /app/ 13 | 14 | RUN chmod +x /app/* \ 15 | && apk add --no-cache grep file bash supercronic curl jq zip nodejs npm wget tar xz \ 16 | && ln -sf "${LOCALTIME_FILE}" /etc/localtime \ 17 | && addgroup -g "${USER_ID}" "${USER_NAME}" \ 18 | && adduser -u "${USER_ID}" -Ds /bin/sh -G "${USER_NAME}" "${USER_NAME}" 19 | 20 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose* 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backup: 3 | image: rodriguestiago0/actualbudget-backup:latest 4 | restart: always 5 | environment: 6 | # RCLONE_REMOTE_NAME: 'ActualBudgetBackup' 7 | # RCLONE_REMOTE_DIR: '/ActualBudgetBackup/' 8 | # RCLONE_GLOBAL_FLAG: '' 9 | ACTUAL_BUDGET_URL: 'https://actual.example.com' 10 | ACTUAL_BUDGET_PASSWORD: '' 11 | ACTUAL_BUDGET_SYNC_ID: '' 12 | # ACTUAL_BUDGET_E2E_PASSWORD: '' 13 | # CRON: '0 0 * * *' 14 | # BACKUP_FILE_SUFFIX: '%Y%m%d' 15 | # BACKUP_KEEP_DAYS: 0 16 | # TIMEZONE: 'UTC' 17 | volumes: 18 | - actualbudget-rclone-data:/config/ 19 | # - /path/to/env:/.env 20 | 21 | volumes: 22 | actualbudget-rclone-data: 23 | external: true 24 | name: actualbudget-rclone-data -------------------------------------------------------------------------------- /scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /app/includes.sh 4 | 5 | # rclone command 6 | if [[ "$1" == "rclone" ]]; then 7 | $* 8 | 9 | exit 0 10 | fi 11 | 12 | 13 | function configure_timezone() { 14 | ln -sf "/usr/share/zoneinfo/${TIMEZONE}" "${LOCALTIME_FILE}" 15 | } 16 | 17 | function configure_cron() { 18 | local FIND_CRON_COUNT="$(grep -c 'backup.sh' "${CRON_CONFIG_FILE}" 2> /dev/null)" 19 | if [[ "${FIND_CRON_COUNT}" -eq 0 ]]; then 20 | echo "${CRON} bash /app/backup.sh" >> "${CRON_CONFIG_FILE}" 21 | fi 22 | } 23 | 24 | init_env 25 | check_rclone_connection 26 | configure_timezone 27 | configure_cron 28 | 29 | # backup manually 30 | if [[ "$1" == "backup" ]]; then 31 | color yellow "Manually triggering a backup will only execute the backup script once, and the container will exit upon completion." 32 | 33 | bash "/app/backup.sh" 34 | 35 | exit 0 36 | fi 37 | 38 | # foreground run crond 39 | exec supercronic -passthrough-logs -quiet "${CRON_CONFIG_FILE}" 40 | -------------------------------------------------------------------------------- /docs/multiple-sync-ids.md: -------------------------------------------------------------------------------- 1 | # Multiple sync ids - WIP 2 | 3 | Some users want to backup multiple budget. 4 | 5 | You can achieve this by setting the following environment variables. 6 | 7 |
8 | 9 | ## Usage 10 | 11 | To set additional remote destinations, use the environment variables `ACTUAL_BUDGET_SYNC_ID_N` where: 12 | 13 | - `N` is a serial number, starting from 1 and increasing consecutively for each additional budget. 14 | 15 | Note that if the serial number is not consecutive or the value is empty, the script will break parsing the environment variables for sync ids. 16 | 17 |
18 | 19 | #### Example 20 | 21 | ```yml 22 | ... 23 | environment: 24 | # they have default values 25 | ACTUAL_BUDGET_SYNC_ID: 'random-guid' 26 | ACTUAL_BUDGET_SYNC_ID_1: 'random-guid-1' 27 | ACTUAL_BUDGET_SYNC_ID_2: 'random-guid-2' 28 | ACTUAL_BUDGET_SYNC_ID_4: 'random-guid-4' 29 | ... 30 | ``` 31 | 32 | With the above example, both sync_ids will be backup: `random-guid`, `random-guid-1` and `random-guid-2` but not the `random-guid-4`. 33 | 34 | Note: Sync ID are present in the settings. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ttionya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/manually-trigger-a-backup.md: -------------------------------------------------------------------------------- 1 | # Manually trigger a backup 2 | 3 | Sometimes, it's necessary to manually trigger backup actions. 4 | 5 | This can be useful when other programs are used to consistently schedule tasks or to verify that environment variables are properly configured. 6 | 7 | ## Usage 8 | 9 | Previously, performing an immediate backup required overwriting the entrypoint of the image. However, with the new setup, you can perform a backup directly with a parameterless command. 10 | 11 | If you have already configured your docker compose file correctly, you can trigger this with the following command (You will need to `docker compose down` the running container first if it is running): 12 | 13 | ```shell 14 | docker compose run --rm backup backup 15 | ``` 16 | 17 | If you have not configured Docker compose yet, you can run the image manually, and specify the env variables on the cli as follows 18 | 19 | ```shell 20 | docker run \ 21 | --rm \ 22 | --name actualbudget-backup \ 23 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 24 | -e ... \ 25 | rodriguestiago0/actualbudget-backup:latest backup 26 | ``` 27 | 28 | You also need to mount the rclone config file and set the environment variables. 29 | 30 | The only difference is that the environment variable `CRON` does not work because it does not start the CRON program, but exits the container after the backup is done. 31 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Publish 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ 'v*.*.*' ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | permissions: 11 | packages: write 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Log into registry 29 | uses: docker/login-action@v3 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | 34 | - 35 | name: DockerHub Description 36 | uses: peter-evans/dockerhub-description@v4 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | repository: 'rodriguestiago0/actualbudget-backup' 41 | enable-url-completion: true 42 | 43 | - name: Extract Docker metadata 44 | id: meta 45 | uses: docker/metadata-action@v4 46 | with: 47 | images: rodriguestiago0/actualbudget-backup 48 | 49 | - name: Build and push Docker image 50 | id: build-and-push 51 | uses: docker/build-push-action@v6 52 | with: 53 | context: . 54 | file: ./Dockerfile 55 | platforms: 'linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7' 56 | push: ${{ github.event_name != 'pull_request' }} # Don't push on PR 57 | tags: ${{ steps.meta.outputs.tags }} 58 | labels: ${{ steps.meta.outputs.labels }} 59 | -------------------------------------------------------------------------------- /scripts/download-actual-budget.js: -------------------------------------------------------------------------------- 1 | const api = require('@actual-app/api'); 2 | const path = require('path'); 3 | const { execSync } = require('child_process'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | // Parse arguments using minimist 7 | const dataDir = argv.dataDir || '/tmp/actual-download'; 8 | const destDir = argv.destDir || '/data/backup'; 9 | const serverURL = argv.serverURL || 'http://localhost:5006'; 10 | const password = argv.password || 'password'; 11 | const syncIdList = (argv.syncIds || '').split(','); 12 | const e2ePasswords = (argv.e2ePasswords || '').split(','); 13 | const now = argv.now || 'now'; 14 | 15 | console.log("📥 Starting download from", serverURL); 16 | console.log("🗂 Sync IDs:", syncIdList); 17 | 18 | (async () => { 19 | for (let i = 0; i < syncIdList.length; i++) { 20 | const syncId = syncIdList[i]; 21 | if (!syncId) continue; 22 | 23 | const e2ePassword = e2ePasswords[i] || null; 24 | const zipPath = path.join(destDir, `backup.${syncId}.${now}.zip`); 25 | 26 | console.log(`⬇️ Downloading budget ${syncId} -> ${zipPath}`); 27 | 28 | await api.init({ dataDir, serverURL, password }); 29 | 30 | try { 31 | await api.downloadBudget(syncId, e2ePassword ? { password: e2ePassword } : {}); 32 | console.log(`✅ Budget ${syncId} downloaded successfully.`); 33 | await api.getAccounts(); 34 | await api.shutdown(); 35 | 36 | // Zip the downloaded data 37 | execSync(`cd ${dataDir} && zip -r ${zipPath} .`, { stdio: 'inherit' }); 38 | execSync(`rm -rf ${dataDir}/*`, { stdio: 'inherit' }); 39 | console.log(`📦 Created zip: ${zipPath}`); 40 | } catch (err) { 41 | console.error(`❌ Failed to download ${syncId}:`, err); 42 | } finally { 43 | await api.shutdown(); 44 | } 45 | } 46 | 47 | console.log("🎉 All downloads completed!"); 48 | })(); 49 | -------------------------------------------------------------------------------- /docs/e2e-encrypted-backups.md: -------------------------------------------------------------------------------- 1 | # End-to-end Encrypted Backups 2 | 3 | End-to-end encrypted backups are now supported. If you have specified a key in ActualBackup's End-to-end Encryption settings, they will be backed up successfully. 4 |
5 | 6 | **NOTE:** Similarly to ActualBackup's Export feature, if you back up an end-to-end encrypted file, it is backed up **without encryption**. This is because Actual Backup does not support importing encrypted backups - the database files must be in an unencrypted zip file for successful import. 7 | 8 | If it is necessary that these files remain encrypted, consider a per-file encryption solution, such as an rclone crypt remote. 9 | 10 | After you import your backup, you will have to set a new key to enable end-to-end encryption again. 11 | 12 | 13 |
14 | 15 | ## Usage 16 | If you are backing up a single E2E backup, use the environment variable `ACTUAL_BUDGET_E2E_PASSWORD` to set the same password used in ActualBackup. 17 | 18 | To set additional passwords for different sync targets, use the environment variables `ACTUAL_BUDGET_E2E_PASSWORD_N` where: 19 | 20 | - `N` is a serial number, starting from 1 and increasing consecutively for each additional password. 21 | 22 | Note that if the serial number is not consecutive or the value is empty, the script will break parsing the environment variables for E2E_PASSWORD ids. 23 | 24 | This means that if you are backing up a mixture of budgets where some are encrypted and some are not, you must still specify a `ACTUAL_BUDGET_E2E_PASSWORD_N` value for budgets that do not use an E2E password. You can use any value (such as 'null') but there must be something there or parsing will break. 25 | 26 |
27 | 28 | #### Example 29 | 30 | ```yml 31 | ... 32 | environment: 33 | # they have default values 34 | ACTUAL_BUDGET_SYNC_ID: 'encrypted-random-guid' 35 | ACTUAL_BUDGET_SYNC_ID_1: 'encrypted-random-guid-1' 36 | ACTUAL_BUDGET_SYNC_ID_2: 'random-guid-2' (NOT ENCRYPTED) 37 | ACTUAL_BUDGET_SYNC_ID_3: 'encrypted-random-guid-3' 38 | ACTUAL_BUDGET_E2E_PASSWORD: 'password' 39 | ACTUAL_BUDGET_E2E_PASSWORD_1: 'password-1' 40 | ACTUAL_BUDGET_E2E_PASSWORD_2: 'anything except empty' 41 | ACTUAL_BUDGET_E2E_PASSWORD_3: 'password-3' 42 | 43 | ... 44 | ``` 45 | 46 | With the above example, even though `ACTUAL_BUDGET_SYNC_ID_2` identifies a budget which is not encrypted, `ACTUAL_BUDGET_E2E_PASSWORD_2` must still be a non-empty value. 47 | -------------------------------------------------------------------------------- /docs/multiple-remote-destinations.md: -------------------------------------------------------------------------------- 1 | # Multiple remote destinations 2 | 3 | Some users want to upload their backups to multiple remote destinations. 4 | 5 | You can achieve this by setting the following environment variables. 6 | 7 |
8 | 9 | 10 | 11 | ## Usage 12 | 13 | > **Don't forget to add the new Rclone remote before running with the new environment variables.** 14 | > 15 | > Find more information on how to configure Rclone [here](https://github.com/rodriguestiago0/actualbudget-backup#configure-rclone-%EF%B8%8F-must-read-%EF%B8%8F). 16 | 17 | To set additional remote destinations, use the environment variables `RCLONE_REMOTE_NAME_N` and `RCLONE_REMOTE_DIR_N`, where: 18 | 19 | - `N` is a serial number, starting from 1 and increasing consecutively for each additional destination 20 | - `RCLONE_REMOTE_NAME_N` and `RCLONE_REMOTE_DIR_N` cannot be empty 21 | 22 | Note that if the serial number is not consecutive or the value is empty, the script will break parsing the environment variables for remote destinations. 23 | 24 |
25 | 26 | 27 | 28 | #### Example 29 | 30 | ```yml 31 | ... 32 | environment: 33 | # they have default values 34 | # RCLONE_REMOTE_NAME: ActualBudgetBackup 35 | # RCLONE_REMOTE_DIR: /ActualBudgetBackup/ 36 | RCLONE_REMOTE_NAME_1: extraRemoteName1 37 | RCLONE_REMOTE_DIR_1: extraRemoteDir1 38 | ... 39 | ``` 40 | 41 | With the above example, both remote destinations are available: `ActualBudgetBackup:/ActualBudgetBackup/` and `extraRemoteName1:extraRemoteDir1`. 42 | 43 |
44 | 45 | ```yml 46 | ... 47 | environment: 48 | RCLONE_REMOTE_NAME: remoteName 49 | RCLONE_REMOTE_DIR: remoteDir 50 | RCLONE_REMOTE_NAME_1: extraRemoteName1 51 | RCLONE_REMOTE_DIR_1: extraRemoteDir1 52 | RCLONE_REMOTE_NAME_2: extraRemoteName2 53 | RCLONE_REMOTE_DIR_2: extraRemoteDir2 54 | RCLONE_REMOTE_NAME_3: extraRemoteName3 55 | RCLONE_REMOTE_DIR_3: extraRemoteDir3 56 | RCLONE_REMOTE_NAME_4: extraRemoteName4 57 | RCLONE_REMOTE_DIR_4: extraRemoteDir4 58 | ... 59 | ``` 60 | 61 | With the above example, all 5 remote destinations are available. 62 | 63 |
64 | 65 | ```yml 66 | ... 67 | environment: 68 | RCLONE_REMOTE_NAME: remoteName 69 | RCLONE_REMOTE_DIR: remoteDir 70 | RCLONE_REMOTE_NAME_1: extraRemoteName1 71 | RCLONE_REMOTE_DIR_1: extraRemoteDir1 72 | RCLONE_REMOTE_NAME_2: extraRemoteName2 73 | # RCLONE_REMOTE_DIR_2: extraRemoteDir2 74 | RCLONE_REMOTE_NAME_3: extraRemoteName3 75 | RCLONE_REMOTE_DIR_3: extraRemoteDir3 76 | RCLONE_REMOTE_NAME_4: extraRemoteName4 77 | RCLONE_REMOTE_DIR_4: extraRemoteDir4 78 | ... 79 | ``` 80 | 81 | With the above example, only the remote destinations before `RCLONE_REMOTE_DIR_2` are available: `remoteName:remoteDir` and `extraRemoteName1:extraRemoteDir1`. 82 | 83 |
84 | -------------------------------------------------------------------------------- /scripts/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /app/includes.sh 4 | 5 | function clear_dir() { 6 | rm -rf backup 7 | } 8 | 9 | function backup_file_name () { 10 | # backup zip file 11 | BACKUP_FILE_ZIP="backup/backup.$1.${NOW}.zip" 12 | color blue "file name \"${BACKUP_FILE_ZIP}\"" 13 | } 14 | 15 | function prepare_login_json() { 16 | (printf '%s\0%s\0' "loginMethod" "password" && printf '%s\0%s\0' "password" "${ACTUAL_BUDGET_PASSWORD}") | jq -Rs 'split("\u0000") | . as $a 17 | | reduce range(0; 2) as $i 18 | ({}; . + {($a[2*$i]): ($a[2*$i + 1])})' > /tmp/login.json 19 | } 20 | 21 | # ========================================================== 22 | # 🧩 NEW download_actual_budget() using @actual-app/api 23 | # ========================================================== 24 | function download_actual_budget() { 25 | color blue "Downloading Actual Budget backup using @actual-app/api" 26 | 27 | # Parameters: 28 | API_VERSION=${ACTUAL_API_VERSION:-latest} 29 | API_DOWNLOAD_PATH=${ACTUAL_API_DOWNLOAD_PATH:-/tmp/actual-download} 30 | 31 | # Clean and prepare folders 32 | mkdir -p "${API_DOWNLOAD_PATH}" 33 | mkdir -p "backup" 34 | 35 | color green "Installing @actual-app/api@$API_VERSION (if needed)..." 36 | if ! [ -d "/app/node_modules/@actual-app/api" ]; then 37 | echo "Installing @actual-app/api@$API_VERSION..." 38 | npm install --prefix /app "@actual-app/api@$API_VERSION" --unsafe-perm 39 | else 40 | color green "@actual-app/api@$API_VERSION already installed." 41 | fi 42 | 43 | # Convert arrays to comma-separated strings 44 | SYNC_IDS="$(IFS=, ; echo "${ACTUAL_BUDGET_SYNC_ID_LIST[*]}")" 45 | E2E_PASSWORDS="$(IFS=, ; echo "${ACTUAL_BUDGET_E2E_PASSWORD_LIST[*]}")" 46 | 47 | export NODE_PATH=/app/node_modules 48 | 49 | # Run Node with arguments instead of relying on environment variable export 50 | node "/app/download-actual-budget.js" \ 51 | --syncIds="$SYNC_IDS" \ 52 | --e2ePasswords="$E2E_PASSWORDS" \ 53 | --dataDir="$API_DOWNLOAD_PATH" \ 54 | --destDir="$(pwd)/backup" \ 55 | --serverURL="$ACTUAL_BUDGET_URL" \ 56 | --password="$ACTUAL_BUDGET_PASSWORD" \ 57 | --now="$NOW" 58 | } 59 | 60 | # ========================================================== 61 | 62 | function backup() { 63 | mkdir -p "backup" 64 | 65 | download_actual_budget 66 | 67 | ls -lah "backup" 68 | } 69 | 70 | 71 | function upload() { 72 | for ACTUAL_BUDGET_SYNC_ID_X in "${ACTUAL_BUDGET_SYNC_ID_LIST[@]}" 73 | do 74 | backup_file_name $ACTUAL_BUDGET_SYNC_ID_X 75 | if !(file "${BACKUP_FILE_ZIP}" | grep -q "Zip archive data" ) ; then 76 | color red "File not found \"${BACKUP_FILE_ZIP}\"" 77 | color red "Nothing has been backed up!" 78 | exit 1 79 | fi 80 | done 81 | 82 | # upload 83 | for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" 84 | do 85 | for ACTUAL_BUDGET_SYNC_ID_X in "${ACTUAL_BUDGET_SYNC_ID_LIST[@]}" 86 | do 87 | backup_file_name $ACTUAL_BUDGET_SYNC_ID_X 88 | color blue "upload backup file to storage system $(color yellow "[${BACKUP_FILE_ZIP} -> ${RCLONE_REMOTE_X}]")" 89 | 90 | rclone ${RCLONE_GLOBAL_FLAG} copy "${BACKUP_FILE_ZIP}" "${RCLONE_REMOTE_X}" 91 | if [[ $? != 0 ]]; then 92 | color red "upload failed" 93 | fi 94 | done 95 | done 96 | 97 | } 98 | 99 | function clear_history() { 100 | if [[ "${BACKUP_KEEP_DAYS}" -gt 0 ]]; then 101 | for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" 102 | do 103 | color blue "delete ${BACKUP_KEEP_DAYS} days ago backup files $(color yellow "[${RCLONE_REMOTE_X}]")" 104 | 105 | mapfile -t RCLONE_DELETE_LIST < <(rclone ${RCLONE_GLOBAL_FLAG} lsf "${RCLONE_REMOTE_X}" --min-age "${BACKUP_KEEP_DAYS}d") 106 | 107 | for RCLONE_DELETE_FILE in "${RCLONE_DELETE_LIST[@]}" 108 | do 109 | color yellow "deleting \"${RCLONE_DELETE_FILE}\"" 110 | 111 | rclone ${RCLONE_GLOBAL_FLAG} delete "${RCLONE_REMOTE_X}/${RCLONE_DELETE_FILE}" 112 | if [[ $? != 0 ]]; then 113 | color red "delete \"${RCLONE_DELETE_FILE}\" failed" 114 | fi 115 | done 116 | done 117 | fi 118 | } 119 | 120 | color blue "running the backup program at $(date +"%Y-%m-%d %H:%M:%S %Z")" 121 | 122 | init_env 123 | 124 | NOW="$(date +"${BACKUP_FILE_DATE_FORMAT}")" 125 | 126 | check_rclone_connection 127 | 128 | clear_dir 129 | backup 130 | upload 131 | clear_dir 132 | clear_history 133 | color none "" 134 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This doc should contain the minimal steps required to get a working backup going. It assumes you already have a box with docker running that this will run on, and that that box is able to talk to your Actual server over https. 4 | 5 | ## Setup 6 | 7 | There are two parts to the setup, one to tell the backup system how to connect to your Actual server, and the other to tell the backup system how to connect to the storage system you are going to save the backup to. This guide will cover setting up the storage first, and then the connection to Actual. 8 | 9 | ### Storage 10 | 11 | The backup system uses Rclone to talk to the storage system. At time of writing, Rclone supports 55 different storage systems, so this guide will not tell you exactly how to setup your storage system, for that you should check out [Rclone's excellent documentation](https://rclone.org/docs/). However these are the required steps to get it working in this system. 12 | 13 | 1. Run the following code to start the setup. Note: if you're running this on Windows, replace the `\` at the end of each line with `` ` `` (backticks) 14 | 15 | ```shell 16 | docker run --rm -it \ 17 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 18 | rodriguestiago0/actualbudget-backup:latest \ 19 | rclone config 20 | ``` 21 | 22 | 2. Choose the option to create a new remote, and when prompted for a name call it "ActualBudgetBackup". 23 | 3. From here, follow the instructions from Rclone's Documentation to set up your storage. 24 | 25 | The only thing to note that we're doing differently is that Rclone is running inside Docker. There are a few storage providers that require you to launch a web browser as part of the auth flow (e.g Google Drive and OneDrive). If you are using one of these methods, you will need to answer "no" to Rclone launching the browser for you (as the Docker container is headless), and follow the instructions it will then provide to use Rclone on a different machine to get the auth token. 26 | 27 | ### Connection to Actual 28 | 29 | Next you need to tell the container how it's going to talk to your Actual server. To start with, download the [`docker-compose.yml`](/docker-compose.yml?raw=1) file to your machine. Put it in its own folder somewhere, and then open it for editing. This guide will go over the mandatory and most used fields here. For the the full list, check the [README](/README.md) for what they do. 30 | 31 | #### Mandatory fields 32 | 33 | `ACTUAL_BUDGET_URL` - First, set the url of the Actual Server, including the protocol, (and the port if applicable) (NB: Do NOT add a trailing / to this. e.g. `ACTUAL_BUDGET_URL: 'https://acutal.example.com'` will work, but `ACTUAL_BUDGET_URL: 'https://acutal.example.com/'` will not) 34 | 35 | `ACTUAL_BUDGET_PASSWORD` - Second, you need to put the password for your budget. (NB: If your password contains any single quotes (`'`), you need to escape the by doubling them up e.g. if your password was `123Super'Password` you would need to enter `ACTUAL_BUDGET_PASSWORD: '123Super''Password'`. 36 | 37 | `ACTUAL_BUDGET_SYNC_ID` - Finally, this identifies the budget on the server. To get this ID, open Actual in your web browser, and go to `Settings`. At the bottom, click `Show advanced settings`, and the `Sync ID` should be in the top section there. 38 | 39 | #### Optional fields you might need to change 40 | 41 | `CRON` - This line tells the container what time to perform the backup. By default, it happens at midnight UTC every day. This is fine if your computer is on 24/7, but if the machine you're running this on is only active in the day, you might want to change it to happen when you know it will be on. To do this, enter any valid cron string, but note that the default config only allows one backup per day, so making it occur more frequently will overwrite the first backup. 42 | 43 | `TIMEZONE` - your local timezone. If you're changing the cron time, you will also want to set the timezone, else it will not run at the time you want it to. It's entered in standard TZ data format. e.g. to set the timezone to UK time, you'd set it to `TIMEZONE: 'Europe/London'` 44 | 45 | `BACKUP_KEEP_DAYS` - by default, this tool never deletes old backups. To change this behaviour, set this to the number of days to keep backups for. e.g. for a weeks worth of backups, set `BACKUP_KEEP_DAYS: 7` 46 | 47 | `ACTUAL_BUDGET_SYNC_ID_1` If you have multiple budgets to backup, you can add more sync IDs by using the `ACTUAL_BUDGET_SYNC_ID_1: ''` field to hold the second ID, and you can add as many of those as you want by incrementing the number `ACTUAL_BUDGET_SYNC_ID_2`, `ACTUAL_BUDGET_SYNC_ID_3`… etc. 48 | 49 | ## Testing 50 | 51 | Now all the config is set, you should run a test backup to confirm all the config is correct. To do that, run the following command from the folder where you have the docker compose file: 52 | 53 | ```shell 54 | docker compose run --rm backup backup 55 | ``` 56 | 57 | If everything is ok, this will run for a few seconds, and will finish on a line similar to `upload backup file to storage system [backup/backup..20250208.zip -> ActualBudgetBackup:/ActualBudgetBackup]`. If you check your storage system, you should now have a file called `backup..20250208.zip` stored within there. If anything went wrong, go back and check all your env variables. If you can't work out what's gone wrong, file an issue in this repo. 58 | 59 | ## Starting the automatic backup 60 | 61 | If your test succeeded, you can now start docker running permanently. To do this, issue the following command: 62 | 63 | ```shell 64 | docker compose up -d 65 | ``` 66 | 67 | This will start the container running, and it will automatically start every time docker does. The backup will be performed at whatever time you specified for `CRON`, or at midnight UTC if you didn't specify. 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Actual Budget backup 2 | 3 | Docker containers for [actualbudget](https://actualbudger.org) backup to remote. 4 | 5 | Heavily inspired at [vaultwarden-backup](https://github.com/ttionya/vaultwarden-backup) 6 | 7 | > **Important:** We assume you already read the `actualbudget` [documentation](https://actualbudget.org/docs/), and have an instance up and running. 8 | 9 | ## Getting started guide 10 | 11 | The fastest way to get started is with the [getting started guide](docs/getting-started.md), which contains the required config to get running as quickly as possible. This README contains the full details of all the extras you might need to run as well. 12 | 13 | ## Usage 14 | 15 | ### Configure Rclone (⚠️ MUST READ ⚠️) 16 | 17 | > **For backup, you need to configure Rclone first, otherwise the backup tool will not work.** 18 | 19 | We upload the backup files to the storage system by [Rclone](https://rclone.org/). 20 | 21 | Visit [Rclone's documentation](https://rclone.org/docs/) for more storage system tutorials. Different systems get tokens differently. 22 | 23 | #### Configure and Check 24 | 25 | You can get the token by the following command. 26 | 27 | ```shell 28 | docker run --rm -it \ 29 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 30 | rodriguestiago0/actualbudget-backup:latest \ 31 | rclone config 32 | ``` 33 | 34 | **We recommend setting the remote name to `ActualBudgetBackup`, otherwise you need to specify the environment variable `RCLONE_REMOTE_NAME` as the remote name you set.** 35 | 36 | After setting, check the configuration content by the following command. 37 | 38 | ```shell 39 | docker run --rm -it \ 40 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 41 | rodriguestiago0/actualbudget-backup:latest \ 42 | rclone config show 43 | 44 | # Microsoft Onedrive Example 45 | # [ActualBudgetBackup] 46 | # type = onedrive 47 | # token = {"access_token":"access token","token_type":"token type","refresh_token":"refresh token","expiry":"expiry time"} 48 | # drive_id = driveid 49 | # drive_type = personal 50 | ``` 51 | 52 | ### Backup 53 | 54 | #### Use Docker Compose (Recommend) 55 | 56 | Download `docker-compose.yml` to you machine, edit the [environment variables](#environment-variables) and start it. 57 | 58 | You need to go to the directory where the `docker-compose.yml` file is saved. 59 | 60 | ```shell 61 | # Start 62 | docker-compose up -d 63 | 64 | # Stop 65 | docker-compose stop 66 | 67 | # Restart 68 | docker-compose restart 69 | 70 | # Remove 71 | docker-compose down 72 | ``` 73 | 74 | #### Automatic Backups without docker compose 75 | 76 | Start the backup container with default settings. (automatic backup at 12AM every day) 77 | 78 | ```shell 79 | docker run -d \ 80 | --restart=always \ 81 | --name actualbudget_backup \ 82 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 83 | rodriguestiago0/actualbudget-backup:latest 84 | ``` 85 | 86 | ## Environment Variables 87 | 88 | > **Note:** The container will run with no environment variables specified without error, however if you haven't set at least `ACTUAL_BUDGET_URL`, `ACTUAL_BUDGET_PASSWORD`, and `ACTUAL_BUDGET_SYNC_ID`, no backup will successfully happen. 89 | 90 | ### ACTUAL_BUDGET_URL 91 | 92 | URL for the actual budget server, without a trailing `/` 93 | 94 | ### ACTUAL_BUDGET_PASSWORD 95 | 96 | Password for the actual budget server. If you're setting this through the docker-compose file, Single quotes must be escaped with by doubling them up. e.g. if your password is `SuperGo'oodPassw\ord"1` you would enter `ACTUAL_BUDGET_PASSWORD: 'SuperGo''oodPassw\ord"1'`. If you're using the env file method, you will need to work out your own way to encode your password without breaking the env file. 97 | 98 | ### ACTUAL_BUDGET_SYNC_ID 99 | 100 | Actual Sync ID. You can find this by logging into your Actual server in a web browser, go to `settings > show advanced settings` and the sync ID should be in the top block there. 101 | 102 | ### RCLONE_REMOTE_NAME 103 | 104 | The name of the Rclone remote, which needs to be consistent with the remote name in the rclone config. 105 | 106 | You can view the current remote name with the following command. 107 | 108 | ```shell 109 | docker run --rm -it \ 110 | --mount type=volume,source=actualbudget-rclone-data,target=/config/ \ 111 | rodriguestiago0/actualbudget-backup:latest \ 112 | rclone config show 113 | 114 | # [ActualBudgetBackup] <- this 115 | # ... 116 | ``` 117 | 118 | Default: `ActualBudgetBackup` 119 | 120 | ### RCLONE_REMOTE_DIR 121 | 122 | The folder where backup files are stored in the storage system. 123 | 124 | Default: `/ActualBudgetBackup/` 125 | 126 | ### RCLONE_GLOBAL_FLAG 127 | 128 | Rclone global flags, see [flags](https://rclone.org/flags/). 129 | 130 | **Do not add flags that will change the output, such as `-P`, which will affect the deletion of outdated backup files.** 131 | 132 | Default: `''` 133 | 134 | ### CRON 135 | 136 | Schedule to run the backup script, based on [`supercronic`](https://github.com/aptible/supercronic). You can test the rules [here](https://crontab.guru/#0_0_*_*_*). 137 | 138 | Default: `0 0 * * *` (run the script at 12AM every day) 139 | 140 | ### BACKUP_KEEP_DAYS 141 | 142 | Only keep last a few days backup files in the storage system. Set to `0` to keep all backup files. 143 | 144 | Default: `0` 145 | 146 | ### BACKUP_FILE_SUFFIX 147 | 148 | Each backup file is suffixed by default with `%Y%m%d`. If you back up your budget multiple times a day, that suffix is not unique any more. This environment variable allows you to append a unique suffix to that date to create a unique backup name. 149 | 150 | You can use any character except for `/` since it cannot be used in Linux file names. 151 | 152 | This environment variable combines the functionalities of [`BACKUP_FILE_DATE`](#backup_file_date) and [`BACKUP_FILE_DATE_SUFFIX`](#backup_file_date_suffix), and has a higher priority. You can directly use this environment variable to control the suffix of the backup files. 153 | 154 | Please use the [date man page](https://man7.org/linux/man-pages/man1/date.1.html) for the format notation. 155 | 156 | Default: `%Y%m%d` 157 | 158 | ### TIMEZONE 159 | 160 | Set your timezone name. 161 | 162 | Here is timezone list at [wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). 163 | 164 | Default: `UTC` 165 | 166 |
167 | ※ Other environment variables 168 | 169 | > **You don't need to change these environment variables unless you know what you are doing.** 170 | 171 | ### BACKUP_FILE_DATE 172 | 173 | You should use the [`BACKUP_FILE_SUFFIX`](#backup_file_suffix) environment variable instead. 174 | 175 | Edit this environment variable only if you explicitly want to change the time prefix of the backup file (e.g. 20220101). **Incorrect configuration may result in the backup file being overwritten by mistake.** 176 | 177 | Same rule as [`BACKUP_FILE_DATE_SUFFIX`](#backup_file_date_suffix). 178 | 179 | Default: `%Y%m%d` 180 | 181 | ### BACKUP_FILE_DATE_SUFFIX 182 | 183 | You should use the [`BACKUP_FILE_SUFFIX`](#backup_file_suffix) environment variable instead. 184 | 185 | Each backup file is suffixed by default with `%Y%m%d`. If you back up your budget multiple times a day, that suffix is not unique anymore. 186 | This environment variable allows you to append a unique suffix to that date (`%Y%m%d${BACKUP_FILE_DATE_SUFFIX}`) to create a unique backup name. 187 | 188 | Note that only numbers, upper and lower case letters, `-`, `_`, `%` are supported. 189 | 190 | Please use the [date man page](https://man7.org/linux/man-pages/man1/date.1.html) for the format notation. 191 | 192 | Default: `''` 193 | 194 |
195 | 196 | ## Using `.env` file 197 | 198 | If you prefer using an env file instead of environment variables, you can map the env file containing the environment variables to the `/.env` file in the container. 199 | 200 | ```shell 201 | docker run -d \ 202 | --mount type=bind,source=/path/to/env,target=/.env \ 203 | rodriguestiago0/actualbudget-backup:latest 204 | ``` 205 | 206 | ## Docker Secrets 207 | 208 | As an alternative to passing sensitive information via environment variables, `_FILE` may be appended to the previously listed environment variables. This causes the initialization script to load the values for those variables from files present in the container. In particular, this can be used to load passwords from Docker secrets stored in `/run/secrets/` files. 209 | 210 | ```shell 211 | docker run -d \ 212 | -e ACTUAL_BUDGET_PASSWORD=/run/secrets/actual-budget-password \ 213 | rodriguestiag0/actualbudget-backup:latest 214 | ``` 215 | 216 | ## About Priority 217 | 218 | We will use the environment variables first, followed by the contents of the file ending in `_FILE` as defined by the environment variables. Next, we will use the contents of the file ending in `_FILE` as defined in the `.env` file, and finally the values from the `.env` file itself. 219 | 220 | ## Advance 221 | 222 | - [Multiple remote destinations](docs/multiple-remote-destinations.md) 223 | - [Multiple sync ids](docs/multiple-sync-ids.md) 224 | - [Manually trigger a backup](docs/manually-trigger-a-backup.md) 225 | - [Encrypted files](docs/e2e-encrypted-backups.md) 226 | 227 | ## License 228 | 229 | MIT 230 | -------------------------------------------------------------------------------- /scripts/includes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ENV_FILE="/.env" 4 | CRON_CONFIG_FILE="${HOME}/crontabs" 5 | 6 | #################### Function #################### 7 | ######################################## 8 | # Print colorful message. 9 | # Arguments: 10 | # color 11 | # message 12 | # Outputs: 13 | # colorful message 14 | ######################################## 15 | function color() { 16 | case $1 in 17 | red) echo -e "\033[31m$2\033[0m" ;; 18 | green) echo -e "\033[32m$2\033[0m" ;; 19 | yellow) echo -e "\033[33m$2\033[0m" ;; 20 | blue) echo -e "\033[34m$2\033[0m" ;; 21 | none) echo "$2" ;; 22 | esac 23 | } 24 | 25 | ######################################## 26 | # Check storage system connection success. 27 | # Arguments: 28 | # None 29 | ######################################## 30 | function check_rclone_connection() { 31 | # check configuration exist 32 | rclone ${RCLONE_GLOBAL_FLAG} config show "${RCLONE_REMOTE_NAME}" > /dev/null 2>&1 33 | if [[ $? != 0 ]]; then 34 | color red "rclone configuration information not found" 35 | color blue "Please configure rclone first, check https://github.com/rodriguestiago0/actualbudget-backup#configure-rclone-%EF%B8%8F-must-read-%EF%B8%8F" 36 | exit 1 37 | fi 38 | 39 | # check connection 40 | local HAS_ERROR="FALSE" 41 | 42 | for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" 43 | do 44 | rclone ${RCLONE_GLOBAL_FLAG} mkdir "${RCLONE_REMOTE_X}" 45 | if [[ $? != 0 ]]; then 46 | color red "storage system connection failure $(color yellow "[${RCLONE_REMOTE_X}]")" 47 | 48 | HAS_ERROR="TRUE" 49 | fi 50 | done 51 | 52 | if [[ "${HAS_ERROR}" == "TRUE" ]]; then 53 | exit 1 54 | fi 55 | } 56 | 57 | ######################################## 58 | # Check file is exist. 59 | # Arguments: 60 | # file 61 | ######################################## 62 | function check_file_exist() { 63 | if [[ ! -f "$1" ]]; then 64 | color red "cannot access $1: No such file" 65 | exit 1 66 | fi 67 | } 68 | 69 | ######################################## 70 | # Check directory is exist. 71 | # Arguments: 72 | # directory 73 | ######################################## 74 | function check_dir_exist() { 75 | if [[ ! -d "$1" ]]; then 76 | color red "cannot access $1: No such directory" 77 | exit 1 78 | fi 79 | } 80 | 81 | ######################################## 82 | # Export variables from .env file. 83 | # Arguments: 84 | # None 85 | # Outputs: 86 | # variables with prefix 'DOTENV_' 87 | # Reference: 88 | # https://gist.github.com/judy2k/7656bfe3b322d669ef75364a46327836#gistcomment-3632918 89 | ######################################## 90 | function export_env_file() { 91 | if [[ -f "${ENV_FILE}" ]]; then 92 | color blue "find \"${ENV_FILE}\" file and export variables" 93 | set -a 94 | source <(cat "${ENV_FILE}" | sed -e '/^#/d;/^\s*$/d' -e 's/\(\w*\)[ \t]*=[ \t]*\(.*\)/DOTENV_\1=\2/') 95 | set +a 96 | fi 97 | } 98 | 99 | ######################################## 100 | # Get variables from 101 | # environment variables, 102 | # secret file in environment variables, 103 | # secret file in .env file, 104 | # environment variables in .env file. 105 | # Arguments: 106 | # variable name 107 | # Outputs: 108 | # variable value 109 | ######################################## 110 | function get_env() { 111 | local VAR="$1" 112 | local VAR_FILE="${VAR}_FILE" 113 | local VAR_DOTENV="DOTENV_${VAR}" 114 | local VAR_DOTENV_FILE="DOTENV_${VAR_FILE}" 115 | local VALUE="" 116 | 117 | if [[ -n "${!VAR:-}" ]]; then 118 | VALUE="${!VAR}" 119 | elif [[ -n "${!VAR_FILE:-}" ]]; then 120 | VALUE="$(cat "${!VAR_FILE}")" 121 | elif [[ -n "${!VAR_DOTENV_FILE:-}" ]]; then 122 | VALUE="$(cat "${!VAR_DOTENV_FILE}")" 123 | elif [[ -n "${!VAR_DOTENV:-}" ]]; then 124 | VALUE="${!VAR_DOTENV}" 125 | fi 126 | 127 | export "${VAR}=${VALUE}" 128 | } 129 | 130 | ######################################## 131 | # Get RCLONE_REMOTE_LIST variables. 132 | # Arguments: 133 | # None 134 | # Outputs: 135 | # variable value 136 | ######################################## 137 | function get_rclone_remote_list() { 138 | # RCLONE_REMOTE_LIST 139 | RCLONE_REMOTE_LIST=() 140 | 141 | local i=0 142 | local RCLONE_REMOTE_NAME_X_REFER 143 | local RCLONE_REMOTE_DIR_X_REFER 144 | local RCLONE_REMOTE_X 145 | 146 | # for multiple 147 | while true; do 148 | RCLONE_REMOTE_NAME_X_REFER="RCLONE_REMOTE_NAME_${i}" 149 | RCLONE_REMOTE_DIR_X_REFER="RCLONE_REMOTE_DIR_${i}" 150 | get_env "${RCLONE_REMOTE_NAME_X_REFER}" 151 | get_env "${RCLONE_REMOTE_DIR_X_REFER}" 152 | 153 | if [[ -z "${!RCLONE_REMOTE_NAME_X_REFER}" || -z "${!RCLONE_REMOTE_DIR_X_REFER}" ]]; then 154 | break 155 | fi 156 | 157 | RCLONE_REMOTE_X=$(echo "${!RCLONE_REMOTE_NAME_X_REFER}:${!RCLONE_REMOTE_DIR_X_REFER}" | sed 's@\(/*\)$@@') 158 | RCLONE_REMOTE_LIST=(${RCLONE_REMOTE_LIST[@]} "${RCLONE_REMOTE_X}") 159 | 160 | ((i++)) 161 | done 162 | } 163 | 164 | function init_actual_sync_list() { 165 | ACTUAL_BUDGET_SYNC_ID_LIST=() 166 | 167 | local i=0 168 | local ACTUAL_BUDGET_SYNC_ID_X_REFER 169 | 170 | # for multiple 171 | while true; do 172 | ACTUAL_BUDGET_SYNC_ID_X_REFER="ACTUAL_BUDGET_SYNC_ID_${i}" 173 | get_env "${ACTUAL_BUDGET_SYNC_ID_X_REFER}" 174 | 175 | if [[ -z "${!ACTUAL_BUDGET_SYNC_ID_X_REFER}" ]]; then 176 | break 177 | fi 178 | 179 | ACTUAL_BUDGET_SYNC_ID_LIST=(${ACTUAL_BUDGET_SYNC_ID_LIST[@]} ${!ACTUAL_BUDGET_SYNC_ID_X_REFER}) 180 | 181 | ((i++)) 182 | done 183 | 184 | for ACTUAL_BUDGET_SYNC_ID_X in "${ACTUAL_BUDGET_SYNC_ID_LIST[@]}" 185 | do 186 | color yellow "ACTUAL_BUDGET_SYNC_ID: ${ACTUAL_BUDGET_SYNC_ID_X}" 187 | done 188 | } 189 | 190 | function init_actual_e2e_list(){ 191 | ACTUAL_BUDGET_E2E_PASSWORD_LIST=() 192 | 193 | local i=0 194 | local ACTUAL_BUDGET_E2E_PASSWORD_X_REFER 195 | 196 | # for multiple 197 | while true; do 198 | ACTUAL_BUDGET_E2E_PASSWORD_X_REFER="ACTUAL_BUDGET_E2E_PASSWORD_${i}" 199 | get_env "${ACTUAL_BUDGET_E2E_PASSWORD_X_REFER}" 200 | 201 | if [[ -z "${!ACTUAL_BUDGET_E2E_PASSWORD_X_REFER}" ]]; then 202 | break 203 | fi 204 | 205 | ACTUAL_BUDGET_E2E_PASSWORD_LIST=(${ACTUAL_BUDGET_E2E_PASSWORD_LIST[@]} ${!ACTUAL_BUDGET_E2E_PASSWORD_X_REFER}) 206 | 207 | ((i++)) 208 | done 209 | 210 | for ACTUAL_BUDGET_E2E_PASSWORD_X in "${ACTUAL_BUDGET_E2E_PASSWORD_LIST[@]}" 211 | do 212 | color yellow "ACTUAL_BUDGET_E2E_PASSWORD: *****" 213 | done 214 | } 215 | 216 | function init_actual_env(){ 217 | # ACTUAL BUDGET 218 | get_env ACTUAL_BUDGET_URL 219 | ACTUAL_BUDGET_URL="${ACTUAL_BUDGET_URL:-"https://localhost:5006"}" 220 | color yellow "ACTUAL_BUDGET_URL: ${ACTUAL_BUDGET_URL}" 221 | 222 | get_env ACTUAL_BUDGET_PASSWORD 223 | ACTUAL_BUDGET_PASSWORD="${ACTUAL_BUDGET_PASSWORD:-""}" 224 | color yellow "ACTUAL_BUDGET_PASSWORD: *****" 225 | 226 | get_env ACTUAL_BUDGET_SYNC_ID 227 | 228 | if [[ -z "${ACTUAL_BUDGET_SYNC_ID}" ]]; then 229 | color red "Invalid sync id" 230 | exit 1 231 | fi 232 | 233 | ACTUAL_BUDGET_SYNC_ID_0="${ACTUAL_BUDGET_SYNC_ID}" 234 | 235 | init_actual_sync_list 236 | 237 | 238 | get_env ACTUAL_BUDGET_E2E_PASSWORD 239 | ACTUAL_BUDGET_E2E_PASSWORD_0="${ACTUAL_BUDGET_E2E_PASSWORD}" 240 | 241 | init_actual_e2e_list 242 | } 243 | 244 | ######################################## 245 | # Initialization environment variables. 246 | # Arguments: 247 | # None 248 | # Outputs: 249 | # environment variables 250 | ######################################## 251 | function init_env() { 252 | # export 253 | export_env_file 254 | 255 | # CRON 256 | get_env CRON 257 | CRON="${CRON:-"0 0 * * *"}" 258 | 259 | # RCLONE_REMOTE_NAME 260 | get_env RCLONE_REMOTE_NAME 261 | RCLONE_REMOTE_NAME="${RCLONE_REMOTE_NAME:-"ActualBudgetBackup"}" 262 | RCLONE_REMOTE_NAME_0="${RCLONE_REMOTE_NAME}" 263 | 264 | # RCLONE_REMOTE_DIR 265 | get_env RCLONE_REMOTE_DIR 266 | RCLONE_REMOTE_DIR="${RCLONE_REMOTE_DIR:-"/ActualBudgetBackup/"}" 267 | RCLONE_REMOTE_DIR_0="${RCLONE_REMOTE_DIR}" 268 | 269 | # get RCLONE_REMOTE_LIST 270 | get_rclone_remote_list 271 | 272 | # RCLONE_GLOBAL_FLAG 273 | get_env RCLONE_GLOBAL_FLAG 274 | RCLONE_GLOBAL_FLAG="${RCLONE_GLOBAL_FLAG:-""}" 275 | 276 | # BACKUP_KEEP_DAYS 277 | get_env BACKUP_KEEP_DAYS 278 | BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-"0"}" 279 | 280 | # BACKUP_FILE_DATE_FORMAT 281 | get_env BACKUP_FILE_SUFFIX 282 | get_env BACKUP_FILE_DATE 283 | get_env BACKUP_FILE_DATE_SUFFIX 284 | BACKUP_FILE_DATE="$(echo "${BACKUP_FILE_DATE:-"%Y%m%d"}${BACKUP_FILE_DATE_SUFFIX}" | sed 's/[^0-9a-zA-Z%_-]//g')" 285 | BACKUP_FILE_DATE_FORMAT="$(echo "${BACKUP_FILE_SUFFIX:-"${BACKUP_FILE_DATE}"}" | sed 's/\///g')" 286 | 287 | # TIMEZONE 288 | get_env TIMEZONE 289 | local TIMEZONE_MATCHED_COUNT=$(ls "/usr/share/zoneinfo/${TIMEZONE}" 2> /dev/null | wc -l) 290 | if [[ "${TIMEZONE_MATCHED_COUNT}" -ne 1 ]]; then 291 | TIMEZONE="UTC" 292 | fi 293 | 294 | init_actual_env 295 | 296 | color yellow "========================================" 297 | color yellow "CRON: ${CRON}" 298 | 299 | for RCLONE_REMOTE_X in "${RCLONE_REMOTE_LIST[@]}" 300 | do 301 | color yellow "RCLONE_REMOTE: ${RCLONE_REMOTE_X}" 302 | done 303 | 304 | color yellow "RCLONE_GLOBAL_FLAG: ${RCLONE_GLOBAL_FLAG}" 305 | color yellow "BACKUP_FILE_DATE_FORMAT: ${BACKUP_FILE_DATE_FORMAT} (example \"[filename].$(date +"${BACKUP_FILE_DATE_FORMAT}").zip\")" 306 | color yellow "BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS}" 307 | 308 | color yellow "TIMEZONE: ${TIMEZONE}" 309 | color yellow "========================================" 310 | } 311 | --------------------------------------------------------------------------------