├── .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 |
--------------------------------------------------------------------------------