├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_form.yaml │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── install-dependencies │ │ └── action.yml │ └── setup │ │ └── action.yml ├── pull_request_template.md ├── scripts │ └── generate-matrix.sh └── workflows │ ├── build-macos.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .tx └── config ├── CONTRIBUTORS.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── codecov.yml ├── noxfile.py ├── package ├── entitlements.plist ├── fix_app_qt_folder_names_for_codesign.py ├── icon-symbolic.svg ├── icon.icns ├── macos-package-app.sh └── vorta.spec ├── pyproject.toml ├── requirements.d ├── Brewfile └── dev.txt ├── setup.cfg ├── setup.py ├── src └── vorta │ ├── __init__.py │ ├── __main__.py │ ├── _version.py │ ├── application.py │ ├── assets │ ├── UI │ │ ├── about_tab.ui │ │ ├── archive_tab.ui │ │ ├── diff_dialog.ui │ │ ├── diff_result.ui │ │ ├── exception_dialog.ui │ │ ├── exclude_dialog.ui │ │ ├── export_window.ui │ │ ├── extract_dialog.ui │ │ ├── import_window.ui │ │ ├── log_page.ui │ │ ├── main_window.ui │ │ ├── misc_tab.ui │ │ ├── networks_page.ui │ │ ├── profile_add.ui │ │ ├── repo_add.ui │ │ ├── repo_tab.ui │ │ ├── schedule_page.ui │ │ ├── schedule_tab.ui │ │ ├── shell_commands_page.ui │ │ ├── source_tab.ui │ │ └── ssh_add.ui │ ├── exclusion_presets │ │ ├── apps.json │ │ ├── browsers.json │ │ ├── dev.json │ │ ├── media.json │ │ └── temp.json │ ├── icons │ │ ├── APACHE.txt │ │ ├── OFL.txt │ │ ├── angle-down-solid.svg │ │ ├── angle-up-solid.svg │ │ ├── broom-solid.svg │ │ ├── check-circle.svg │ │ ├── clock-o.svg │ │ ├── cloud-download.svg │ │ ├── copy.svg │ │ ├── cut.svg │ │ ├── edit.svg │ │ ├── eject.svg │ │ ├── ellipsis-v.svg │ │ ├── exclamation-triangle.svg │ │ ├── eye-slash.svg │ │ ├── eye.svg │ │ ├── file-import-solid.svg │ │ ├── file.svg │ │ ├── folder-on-top.svg │ │ ├── folder-open.svg │ │ ├── folder.svg │ │ ├── globe.svg │ │ ├── gpl_logo.svg │ │ ├── hdd-o-active-mask.svg │ │ ├── hdd-o-active.png │ │ ├── hdd-o-mask.svg │ │ ├── hdd-o.png │ │ ├── help-about.svg │ │ ├── icon.svg │ │ ├── loading.gif │ │ ├── minus.svg │ │ ├── paste.svg │ │ ├── plus.svg │ │ ├── python_logo.svg │ │ ├── refresh.svg │ │ ├── server.svg │ │ ├── settings_wheel.svg │ │ ├── stream-solid.svg │ │ ├── tasks.svg │ │ ├── terminal.svg │ │ ├── trash.svg │ │ ├── unlink.svg │ │ ├── user.svg │ │ ├── view-list-details.svg │ │ ├── view-list-tree.svg │ │ ├── wifi.svg │ │ └── window-restore.svg │ └── metadata │ │ ├── Screenshot-1-Repository.png │ │ ├── Screenshot-2-Sources.png │ │ ├── Screenshot-3-Schedule.png │ │ ├── Screenshot-4-Archives.png │ │ ├── com.borgbase.Vorta.appdata.xml │ │ └── com.borgbase.Vorta.desktop │ ├── autostart.py │ ├── borg │ ├── __init__.py │ ├── _compatibility.py │ ├── borg_job.py │ ├── break_lock.py │ ├── check.py │ ├── compact.py │ ├── create.py │ ├── delete.py │ ├── diff.py │ ├── extract.py │ ├── info_archive.py │ ├── info_repo.py │ ├── init.py │ ├── jobs_manager.py │ ├── list_archive.py │ ├── list_repo.py │ ├── mount.py │ ├── prune.py │ ├── rename.py │ ├── umount.py │ └── version.py │ ├── config.py │ ├── i18n │ ├── __init__.py │ ├── qm │ │ ├── .gitkeep │ │ ├── vorta.ar.qm │ │ ├── vorta.cs.qm │ │ ├── vorta.de.qm │ │ ├── vorta.en.qm │ │ ├── vorta.en_US.qm │ │ ├── vorta.es.qm │ │ ├── vorta.fi.qm │ │ ├── vorta.fr.qm │ │ ├── vorta.gl.qm │ │ ├── vorta.it.qm │ │ ├── vorta.nl.qm │ │ ├── vorta.ru.qm │ │ ├── vorta.sk.qm │ │ └── vorta.sv.qm │ └── ts │ │ ├── .gitkeep │ │ ├── vorta.ar.ts │ │ ├── vorta.cs.ts │ │ ├── vorta.de.ts │ │ ├── vorta.es.ts │ │ ├── vorta.fi.ts │ │ ├── vorta.fr.ts │ │ ├── vorta.gl.ts │ │ ├── vorta.it.ts │ │ ├── vorta.nl.ts │ │ ├── vorta.ru.ts │ │ ├── vorta.sk.ts │ │ └── vorta.sv.ts │ ├── keyring │ ├── __init__.py │ ├── abc.py │ ├── darwin.py │ ├── db.py │ ├── kwallet.py │ └── secretstorage.py │ ├── log.py │ ├── network_status │ ├── __init__.py │ ├── abc.py │ ├── darwin.py │ └── network_manager.py │ ├── notifications.py │ ├── profile_export.py │ ├── qt_single_application.py │ ├── scheduler.py │ ├── store │ ├── __init__.py │ ├── connection.py │ ├── migrations.py │ ├── models.py │ └── settings.py │ ├── tray_menu.py │ ├── updater.py │ ├── utils.py │ └── views │ ├── __init__.py │ ├── about_tab.py │ ├── archive_tab.py │ ├── diff_result.py │ ├── exception_dialog.py │ ├── exclude_dialog.py │ ├── export_window.py │ ├── extract_dialog.py │ ├── import_window.py │ ├── log_page.py │ ├── main_window.py │ ├── misc_tab.py │ ├── networks_page.py │ ├── partials │ ├── __init__.py │ ├── loading_button.py │ ├── password_input.py │ ├── tooltip_button.py │ └── treemodel.py │ ├── profile_add_edit_dialog.py │ ├── repo_add_dialog.py │ ├── repo_tab.py │ ├── schedule_page.py │ ├── schedule_tab.py │ ├── shell_commands_page.py │ ├── source_tab.py │ ├── ssh_dialog.py │ └── utils.py └── tests ├── __init__.py ├── conftest.py ├── integration ├── __init__.py ├── conftest.py ├── test_archives.py ├── test_borg.py ├── test_diff.py ├── test_init.py └── test_repo.py ├── network_manager ├── __init__.py ├── test_darwin.py └── test_network_manager.py └── unit ├── __init__.py ├── borg_json_output ├── check_stderr.json ├── check_stdout.json ├── compact_stderr.json ├── compact_stdout.json ├── create_break_stderr.json ├── create_break_stdout.json ├── create_lock_stderr.json ├── create_lock_stdout.json ├── create_perm_stderr.json ├── create_perm_stdout.json ├── create_stderr.json ├── create_stdout.json ├── delete_stderr.json ├── delete_stdout.json ├── diff_archives_dict_issue_stderr.json ├── diff_archives_dict_issue_stdout.json ├── diff_archives_stderr.json ├── diff_archives_stdout.json ├── info_stderr.json ├── info_stdout.json ├── list_archive_stderr.json ├── list_archive_stdout.json ├── list_stderr.json ├── list_stdout.json ├── prune_stderr.json ├── prune_stdout.json ├── rename_stderr.json └── rename_stdout.json ├── conftest.py ├── profile_exports ├── invalid_no_json.json └── valid.json ├── test_archives.py ├── test_borg.py ├── test_create.py ├── test_diff.py ├── test_excludes.py ├── test_extract.py ├── test_import_export.py ├── test_lock.py ├── test_misc.py ├── test_notifications.py ├── test_password_input.py ├── test_profile.py ├── test_repo.py ├── test_schedule.py ├── test_scheduler.py ├── test_source.py ├── test_treemodel.py └── test_utils.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [**.{yml,yaml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Migrate code style to Black 2 | b6a24debb78b953117a3f637db18942f370a4b85 3 | 4 | # Run pre-commit after adding ruff 5 | 24e1dd5c561bc3da972e41e6fd61961f12a2fc9f 6 | 7 | # Apply ruff sort settings 8 | ba9f1bd3d77dbd0b9efeb1f2f91c743b97ec558e 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py diff=python 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Connect and Contribute 2 | - To discuss everything around using, improving, packaging and translating Vorta, join the [discussion on Github](https://github.com/borgbase/vorta/discussions). 3 | - Report bugs by opening a new [Github issue](https://github.com/borgbase/vorta/issues/new/choose). 4 | - Want to contribute to Vorta? Great! See our [contributor guide](https://vorta.borgbase.com/contributing/) on how to help out with coding, translation and packaging. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_form.yaml: -------------------------------------------------------------------------------- 1 | name: "Bug Report Form" 2 | description: "Report a bug or a similar issue." 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue. Please fill out the below template with as much detail as possible. Incomplete bug reports are likely to be closed without comment. 8 | 9 | If you want to suggest a feature use the `Feature Request` template instead. 10 | If you have any other question, head to 11 | our [Discussions](https://github.com/borgbase/vorta/discussions). 12 | 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Description 17 | description: | 18 | Please describe your issue and its context in a clear and concise way. Please try to reproduce the issue and provide the steps to reproduce it. 19 | Wrap error messages in triple backticks. 20 | placeholder: | 21 | I encountered a bug. 22 | 23 | ```Error message``` 24 | 25 | Steps to reproduce: 26 | 1. 27 | 2. 28 | 3. 29 | 30 | validations: 31 | required: true 32 | 33 | - type: checkboxes 34 | id: reproducible 35 | attributes: 36 | label: Reproduction 37 | description: Please try to reproduce the issue with the steps you provided and capture the logs of this try for the form input below. 38 | options: 39 | - label: I tried to reproduce the issue. 40 | required: true 41 | - label: I was able to reproduce the issue. 42 | 43 | - type: input 44 | id: os 45 | attributes: 46 | label: OS 47 | description: Operating system (and desktop environment) 48 | placeholder: , 49 | validations: 50 | required: true 51 | 52 | - type: input 53 | id: version 54 | attributes: 55 | label: Version of Vorta 56 | description: Vorta and Borg versions can be found in Main Window > Settings/About > About Tab. 57 | validations: 58 | required: true 59 | 60 | - type: dropdown 61 | id: installation 62 | attributes: 63 | label: What did you install Vorta with? 64 | options: 65 | - Homebrew 66 | - Binary 67 | - Distribution package 68 | - Flatpak 69 | - Pip 70 | - Other 71 | validations: 72 | required: true 73 | 74 | - type: input 75 | id: borg 76 | attributes: 77 | label: Version of Borg 78 | description: Vorta and Borg versions can be found in Main Window > Settings/About > About Tab. 79 | 80 | - type: textarea 81 | id: logs 82 | attributes: 83 | label: Logs 84 | description: | 85 | Logs are very important for most issues. Please paste them down below. *(No need for backticks)* 86 | They can be found in Main Window > Settings/About > About Tab. 87 | Logs are more helpful if you include (exactly) the logs that were produced during the steps you described above. 88 | placeholder: paste logs here 89 | render: pytb 90 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug or a similar issue - the classic way 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 17 | 18 | #### Description 19 | 20 | 24 | 25 | I _was_/_wasn't_ able to reproduce the issue. 26 | 27 | 34 | 35 | #### Environment 36 | 37 | - OS: 38 | - Vorta version: 39 | - Installed from: 40 | - Borg version: 41 | 42 | 43 | 44 | #### Logs 45 | 46 | 50 | 51 | ``` 52 | *paste logs here* 53 | ``` 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | 3 | - name: Documentation 4 | url: https://vorta.borgbase.com/ 5 | about: Documentation on installing, using and contributing to Vorta. 6 | 7 | - name: Support 8 | url: https://github.com/borgbase/vorta/discussions/categories/faq 9 | about: Please ask for support in the Discussions FAQ. 10 | 11 | - name: Discussions 12 | url: https://github.com/borgbase/vorta/discussions 13 | about: Discuss everything here. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### The problem 11 | 16 | 17 | 18 | #### Requested Solution 19 | 22 | 23 | #### Alternatives 24 | 27 | 28 | #### Additional context 29 | 32 | -------------------------------------------------------------------------------- /.github/actions/install-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Dependencies 2 | description: Installs system dependencies 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install system dependencies (Linux) 8 | if: runner.os == 'Linux' 9 | shell: bash 10 | run: | 11 | sudo apt update && sudo apt install -y \ 12 | xvfb libssl-dev openssl libacl1-dev libacl1 fuse3 build-essential \ 13 | libxkbcommon-x11-0 dbus-x11 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ 14 | libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libxcb-shape0 \ 15 | libegl1 libxcb-cursor0 libfuse-dev libsqlite3-dev libfuse3-dev pkg-config \ 16 | python3-pkgconfig libxxhash-dev borgbackup appstream 17 | 18 | - name: Install system dependencies (macOS) 19 | if: runner.os == 'macOS' 20 | shell: bash 21 | run: | 22 | brew install openssl readline xz xxhash pkg-config borgbackup 23 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Sets up python and pre-commit 3 | 4 | # note: 5 | # this is a local composite action 6 | # documentation: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action 7 | # code example: https://github.com/GuillaumeFalourd/poc-github-actions/blob/main/.github/actions/local-action/action.yaml 8 | 9 | inputs: 10 | pre-commit: 11 | description: Whether pre-commit shall be setup, too 12 | required: false 13 | default: "" # == false 14 | python-version: 15 | description: The python version to install 16 | required: true 17 | default: "3.10" 18 | install-nox: 19 | description: Whether nox shall be installed 20 | required: false 21 | default: "" # == false 22 | runs: 23 | using: "composite" 24 | steps: 25 | - name: Set up Python ${{ inputs.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ inputs.python-version }} 29 | 30 | - name: Get pip cache dir 31 | shell: bash 32 | id: pip-cache 33 | run: | 34 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 35 | - name: pip cache 36 | uses: actions/cache@v3 37 | with: 38 | path: ${{ steps.pip-cache.outputs.dir }} 39 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg', 'requirements.d/**') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pip- 42 | 43 | - name: Install pre-commit 44 | shell: bash 45 | run: pip install pre-commit 46 | 47 | - name: Install nox 48 | if: ${{ inputs.install-nox }} 49 | shell: bash 50 | run: pip install nox 51 | 52 | - name: Hash python version 53 | if: ${{ inputs.setup-pre-commit }} 54 | shell: bash 55 | run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV 56 | 57 | - name: Caching for Pre-Commit 58 | if: ${{ inputs.setup-pre-commit }} 59 | uses: actions/cache@v3 60 | with: 61 | path: ~/.cache/pre-commit 62 | key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} 63 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | ### Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ### Motivation and Context 13 | 14 | 15 | ### How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ### Screenshots (if appropriate): 21 | 22 | ### Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ### Checklist: 29 | 30 | 31 | - [ ] I have read the [CONTRIBUTING](https://vorta.borgbase.com/contributing/) guide. 32 | - [ ] My code follows the code style of this project. 33 | - [ ] My change requires a change to the documentation. 34 | - [ ] I have updated the documentation accordingly. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] All new and existing tests passed. 37 | 38 | 39 | *I provide my contribution under the terms of the [license](./../LICENSE.txt) of this repository and I affirm the [Developer Certificate of Origin][dco].* 40 | 41 | [dco]: https://developercertificate.org/ 42 | 43 | 46 | -------------------------------------------------------------------------------- /.github/scripts/generate-matrix.sh: -------------------------------------------------------------------------------- 1 | event_name="$1" 2 | branch_name="$2" 3 | 4 | if [[ "$event_name" == "workflow_dispatch" ]] || [[ "$branch_name" == "master" ]]; then 5 | echo '{ 6 | "python-version": ["3.9", "3.10", "3.11", "3.12"], 7 | "os": ["ubuntu-22.04", "macos-14"], 8 | "borg-version": ["1.4.0"] 9 | }' | jq -c . > matrix-unit.json 10 | 11 | echo '{ 12 | "python-version": ["3.11"], 13 | "os": ["ubuntu-22.04"], 14 | "borg-version": ["1.1.18", "1.2.8", "1.4.0"], 15 | "exclude": [{"borg-version": "2.0.0b12", "python-version": "3.8"}] 16 | }' | jq -c . > matrix-integration.json 17 | 18 | elif [[ "$event_name" == "push" ]] || [[ "$event_name" == "pull_request" ]]; then 19 | echo '{ 20 | "python-version": ["3.9", "3.12"], 21 | "os": ["ubuntu-22.04", "macos-14"], 22 | "borg-version": ["1.2.8"] 23 | }' | jq -c . > matrix-unit.json 24 | 25 | echo '{ 26 | "python-version": ["3.11"], 27 | "os": ["ubuntu-22.04"], 28 | "borg-version": ["1.4.0"] 29 | }' | jq -c . > matrix-integration.json 30 | fi 31 | -------------------------------------------------------------------------------- /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | name: Build macOS release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: 'Branch to use for building release' 7 | required: true 8 | default: 'master' 9 | borg_version: 10 | description: 'Borg version to package' 11 | required: true 12 | default: '1.4.0' 13 | macos_version: 14 | description: 'macOS version for building' 15 | required: true 16 | default: 'macos-14' 17 | python_version: 18 | description: 'Python version for building' 19 | required: true 20 | default: '3.12' 21 | 22 | jobs: 23 | build: 24 | runs-on: ${{ github.event.inputs.macos_version }} 25 | 26 | steps: 27 | - name: Check out selected branch 28 | uses: actions/checkout@v3 29 | with: 30 | ref: ${{ github.event.inputs.branch }} 31 | - name: Set up Python ${{ inputs.python_version }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ inputs.python_version }} 35 | - name: Install system dependencies 36 | run: | 37 | brew install openssl readline xz 38 | - name: Install build dependencies 39 | run: | 40 | brew install --cask sparkle 41 | brew install create-dmg 42 | pip3 install --break-system-packages --upgrade pip setuptools wheel 43 | pip3 install --break-system-packages -r dev.txt 44 | working-directory: requirements.d 45 | - name: Install Vorta 46 | run: | 47 | pip3 install --break-system-packages . 48 | - name: Package with PyInstaller 49 | run: | 50 | pyinstaller --clean --noconfirm package/vorta.spec 51 | cp -R $(brew --prefix)/Caskroom/sparkle/*/Sparkle.framework dist/Vorta.app/Contents/Frameworks/ 52 | curl -LJO https://github.com/borgbackup/borg/releases/download/${{ github.event.inputs.borg_version }}/borg-macos1012.tgz 53 | tar xvf borg-macos1012.tgz -C dist/Vorta.app/Contents/Resources/ 54 | cd dist && zip -rq --symlinks Vorta.zip Vorta.app 55 | 56 | - name: Codesign executable 57 | continue-on-error: false 58 | working-directory: dist 59 | env: 60 | MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} 61 | MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} 62 | CERTIFICATE_NAME: ${{ secrets.MACOS_CERTIFICATE_NAME }} 63 | APPLE_ID_USER: ${{ secrets.APPLE_ID_USER }} 64 | APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} 65 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 66 | run: | 67 | echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 68 | security create-keychain -p 123 build.keychain 69 | security default-keychain -s build.keychain 70 | security unlock-keychain -p 123 build.keychain 71 | security import certificate.p12 -k build.keychain -A -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign 72 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 123 build.keychain 73 | python3 ../package/fix_app_qt_folder_names_for_codesign.py Vorta.app 74 | sh ../package/macos-package-app.sh 75 | 76 | # - name: Setup tmate session 77 | # uses: mxschmitt/action-tmate@v3 78 | # if: ${{ failure() }} 79 | # timeout-minutes: 15 80 | 81 | - name: Upload build 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: Vorta.dmg 85 | path: dist/Vorta.dmg 86 | retention-days: 60 87 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues 2 | on: 3 | schedule: 4 | - cron: '50 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v8 14 | with: 15 | days-before-issue-stale: 90 16 | days-before-pr-stale: -1 17 | days-before-issue-close: 7 18 | # days-before-pr-close: 10 19 | 20 | stale-issue-label: "status:stale" 21 | stale-pr-label: "status:stale" 22 | 23 | exempt-issue-labels: > 24 | status:idea, 25 | status:planning, 26 | status:on hold, 27 | status:ready, 28 | type:bug, 29 | type:docs, 30 | type:enhancement, 31 | type:feature, 32 | type:refactor, 33 | type:task, 34 | 35 | stale-issue-message: > 36 | This issue has been automatically marked as stale because it has not had 37 | recent activity. It will be closed if no further activity occurs. Thank you 38 | for your contributions. 39 | close-issue-message: > 40 | This issue was closed because it has been stalled for 7 days with no activity. 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | bin/ 4 | build/ 5 | dist/ 6 | docs/ 7 | *.autosave 8 | __pycache__ 9 | .pytest_cache 10 | .eggs 11 | vorta.egg-info 12 | .coverage 13 | .tox 14 | .python-version 15 | .vagrant 16 | *.log 17 | htmlcov 18 | # virtual python environments 19 | env 20 | venv 21 | .env 22 | .venv 23 | # dirs created by the --development option 24 | .dev_config/ 25 | # Avoid adding translations of source language 26 | # Files are still used by Transifex 27 | src/vorta/i18n/ts/vorta.en.ts 28 | src/vorta/i18n/ts/vorta.en_US.ts 29 | flatpak/app/ 30 | flatpak/.flatpak-builder/ 31 | .vscode 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit is a useful tool which can be setup by contributors 2 | # per-repository in a few simple steps given that the 3 | # repository has a config file like this one. 4 | # 5 | # The configured hooks are run on `git commit`. If one of the hooks makes or 6 | # demands a change of the commits contents the commit process is aborted. 7 | # The hooks can also be run manually through `pre-commit run --all-files`. 8 | 9 | minimum_pre_commit_version: "1.15" 10 | 11 | # The following hooks will be run before a commit is created 12 | repos: 13 | # general stuff 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.4.0 16 | hooks: 17 | # check file system problems 18 | - id: check-case-conflict 19 | - id: check-symlinks 20 | - id: destroyed-symlinks 21 | 22 | # unify whitespace and line ending 23 | - id: trailing-whitespace 24 | args: [--markdown-linebreak-ext=md] 25 | - id: end-of-file-fixer 26 | - id: mixed-line-ending 27 | 28 | # sort requirements.txt files 29 | - id: requirements-txt-fixer 30 | 31 | - repo: https://github.com/charliermarsh/ruff-pre-commit 32 | rev: v0.7.0 33 | hooks: 34 | # Run the linter. 35 | - id: ruff 36 | # Run the formatter. 37 | - id: ruff-format 38 | 39 | # format python files 40 | # - repo: https://github.com/psf/black 41 | # rev: 22.12.0 42 | # hooks: 43 | # - id: black 44 | # files: ^(src/vorta/|tests) 45 | 46 | # # run black on code embedded in docstrings 47 | # - repo: https://github.com/asottile/blacken-docs 48 | # rev: v1.12.1 49 | # hooks: 50 | # - id: blacken-docs 51 | # additional_dependencies: [black] 52 | # args: 53 | # [ 54 | # --line-length, 55 | # "120", 56 | # --skip-string-normalization, 57 | # --target-version, 58 | # py39, 59 | # ] 60 | 61 | # configuration for the pre-commit.ci bot 62 | # only relevant when actually using the bot 63 | ci: 64 | autofix_commit_msg: | 65 | [pre-commit.ci] auto fixes from pre-commit.com hooks 66 | 67 | for more information, see https://pre-commit.ci and 68 | the `.pre-commit-config.yaml` file in this repository. 69 | 70 | autofix_prs: true # default 71 | autoupdate_commit_msg: | 72 | [pre-commit.ci] Autoupdate pre-commit hook versions. 73 | 74 | for more information, see https://pre-commit.ci and 75 | the `.pre-commit-config.yaml` file in this repository. 76 | 77 | submodules: false # default 78 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:borgbase:p:vorta:r:vorta] 5 | file_filter = src/vorta/i18n/ts/vorta..ts 6 | source_file = src/vorta/i18n/ts/vorta.en.ts 7 | source_lang = en 8 | type = QT 9 | minimum_perc = 80 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Vorta Contributors 2 | 3 | ## Programming 4 | The following authors made major code contributions to Vorta source code: 5 | 6 | ### [Manuel Riel](https://github.com/m3nu) 7 | - Original author 8 | 9 | ### [Thomas Waldmann](https://github.com/ThomasWaldmann) 10 | - Major contributions to implement translations 11 | - Clean up style and formatting 12 | 13 | ### [Julian Hofer](https://github.com/Hofer-Julian) 14 | - Flatpak packaging for Linux 15 | - Numerous incremental features 16 | 17 | ### [Bastien](https://github.com/bastiencyr) 18 | - Queuing system for Borg jobs 19 | - Numerous incremental fixes 20 | 21 | ### Others 22 | For other contributors, see [here](https://github.com/borgbase/vorta/graphs/contributors). 23 | 24 | 25 | ## Translations 26 | The following authors contributed translations: 27 | 28 | - German: [Thomas Waldmann](https://github.com/ThomasWaldmann) 29 | - Italian: [Luigi Operoso](https://github.com/brokenpip3) 30 | - French: [David Brassard](https://github.com/dbrassard) 31 | - Czech: [Pavel Borecki](https://www.transifex.com/user/profile/pavelb/) 32 | - Finnish: [Jiri Grönroos](https://en.liberapay.com/artnay/) 33 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/vorta/assets 2 | 3 | # Include all compiled .qm language files, but exclude the source language (en). 4 | recursive-include src/vorta/i18n/qm *.qm 5 | exclude src/vorta/i18n/qm/vorta.en.qm 6 | 7 | recursive-exclude tests * 8 | global-exclude *.DS_Store 9 | global-exclude *.egg-info 10 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | import nox 6 | 7 | borg_version = os.getenv("BORG_VERSION") 8 | 9 | if borg_version: 10 | # Use specified borg version 11 | supported_borgbackup_versions = [borg_version] 12 | else: 13 | # Generate a list of borg versions compatible with system installed python version 14 | system_python_version = tuple(sys.version_info[:3]) 15 | 16 | supported_borgbackup_versions = [ 17 | borgbackup 18 | for borgbackup in ("1.1.18", "1.2.2", "1.2.4", "2.0.0b6") 19 | # Python version requirements for borgbackup versions 20 | if (borgbackup == "1.1.18" and system_python_version >= (3, 5, 0)) 21 | or (borgbackup == "1.2.2" and system_python_version >= (3, 8, 0)) 22 | or (borgbackup == "1.2.4" and system_python_version >= (3, 8, 0)) 23 | or (borgbackup == "2.0.0b6" and system_python_version >= (3, 9, 0)) 24 | ] 25 | 26 | 27 | @nox.session 28 | @nox.parametrize("borgbackup", supported_borgbackup_versions) 29 | def run_tests(session, borgbackup): 30 | # install borgbackup 31 | if sys.platform == 'darwin': 32 | # in macOS there's currently no fuse package which works with borgbackup directly 33 | session.install(f"borgbackup=={borgbackup}") 34 | elif borgbackup == "1.1.18": 35 | # borgbackup 1.1.18 doesn't support pyfuse3 36 | session.install("llfuse") 37 | session.install(f"borgbackup[llfuse]=={borgbackup}") 38 | else: 39 | session.install(f"borgbackup[pyfuse3]=={borgbackup}") 40 | 41 | # install dependencies 42 | session.install("-r", "requirements.d/dev.txt") 43 | session.install("-e", ".") 44 | 45 | # check versions 46 | cli_version = session.run("borg", "--version", silent=True).strip() 47 | cli_version = re.search(r"borg (\S+)", cli_version).group(1) 48 | python_version = session.run("python", "-c", "import borg; print(borg.__version__)", silent=True).strip() 49 | 50 | session.log(f"Borg CLI version: {cli_version}") 51 | session.log(f"Borg Python version: {python_version}") 52 | 53 | assert cli_version == borgbackup 54 | assert python_version == borgbackup 55 | 56 | session.run("pytest", *session.posargs, env={"BORG_VERSION": borgbackup}) 57 | -------------------------------------------------------------------------------- /package/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | com.apple.security.cs.allow-unsigned-executable-memory 9 | 10 | com.apple.security.cs.disable-library-validation 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package/icon-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /package/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/package/icon.icns -------------------------------------------------------------------------------- /package/macos-package-app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Inspired by https://github.com/metabrainz/picard/blob/master/scripts/package/macos-notarize-app.sh 3 | 4 | set -eux 5 | 6 | APP_BUNDLE_ID="com.borgbase.client.macos" 7 | APP_BUNDLE="Vorta" 8 | # CERTIFICATE_NAME="Developer ID Application: Joe Doe (XXXXXX)" 9 | # APPLE_ID_USER="name@example.com" 10 | # APPLE_ID_PASSWORD="CHANGEME" 11 | # APPLE_TEAM_ID="CNMSCAXT48" 12 | 13 | 14 | # Sign app bundle, Sparkle and Borg 15 | codesign --verbose --force --sign "$CERTIFICATE_NAME" --timestamp --deep --options runtime \ 16 | $APP_BUNDLE.app/Contents/Frameworks/Sparkle.framework 17 | 18 | find $APP_BUNDLE.app/Contents/Resources/borg-dir \ 19 | -type f \( -name \*.so -or -name \*.dylib -or -name borg.exe -or -name Python \) \ 20 | -exec codesign --verbose --force --timestamp --deep --sign "${CERTIFICATE_NAME}" \ 21 | --entitlements ../package/entitlements.plist --options runtime {} \; 22 | 23 | codesign --verify --force --verbose --deep \ 24 | --options runtime --timestamp \ 25 | --entitlements ../package/entitlements.plist \ 26 | --sign "$CERTIFICATE_NAME" $APP_BUNDLE.app 27 | 28 | 29 | # Create DMG 30 | rm -rf $APP_BUNDLE.dmg 31 | create-dmg \ 32 | --volname "Vorta Installer" \ 33 | --filesystem APFS \ 34 | --window-size 410 300 \ 35 | --icon-size 100 \ 36 | --icon "Vorta.app" 70 150 \ 37 | --hide-extension "Vorta.app" \ 38 | --app-drop-link 240 150 \ 39 | "Vorta.dmg" \ 40 | "Vorta.app" 41 | 42 | # Notarize DMG 43 | xcrun notarytool submit \ 44 | --output-format plist --wait --timeout 10m \ 45 | --apple-id $APPLE_ID_USER \ 46 | --password $APPLE_ID_PASSWORD \ 47 | --team-id $APPLE_TEAM_ID \ 48 | "$APP_BUNDLE.dmg" 49 | 50 | # Staple the notary ticket 51 | xcrun stapler staple $APP_BUNDLE.dmg 52 | xcrun stapler staple $APP_BUNDLE.app 53 | xcrun stapler validate $APP_BUNDLE.dmg 54 | -------------------------------------------------------------------------------- /package/vorta.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | from vorta.config import ( 8 | APP_NAME, 9 | APP_ID_DARWIN 10 | ) 11 | from vorta._version import __version__ as APP_VERSION 12 | 13 | BLOCK_CIPHER = None 14 | APP_APPCAST_URL = 'https://borgbase.github.io/vorta/appcast.xml' 15 | 16 | 17 | # it is assumed that the cwd is the git repo dir: 18 | SRC_DIR = os.path.join(os.getcwd(), 'src', 'vorta') 19 | 20 | a = Analysis([os.path.join(SRC_DIR, '__main__.py')], 21 | pathex=[SRC_DIR], 22 | binaries=[], 23 | datas=[ 24 | (os.path.join(SRC_DIR, 'assets/UI/*'), 'assets/UI'), 25 | (os.path.join(SRC_DIR, 'assets/icons/*'), 'assets/icons'), 26 | (os.path.join(SRC_DIR, 'assets/exclusion_presets/*'), 'assets/exclusion_presets'), 27 | (os.path.join(SRC_DIR, 'i18n/qm/*'), 'vorta/i18n/qm'), 28 | ], 29 | hiddenimports=[ 30 | 'vorta.keyring.darwin', 31 | 'vorta.keyring.kwallet', 32 | 'vorta.keyring.secretstorage', 33 | ], 34 | hookspath=[], 35 | runtime_hooks=[], 36 | excludes=[], 37 | win_no_prefer_redirects=False, 38 | win_private_assemblies=False, 39 | cipher=BLOCK_CIPHER, 40 | noarchive=False) 41 | 42 | pyz = PYZ(a.pure, a.zipped_data, cipher=BLOCK_CIPHER) 43 | 44 | exe = EXE(pyz, 45 | a.scripts, 46 | exclude_binaries=True, 47 | name=f"vorta-{sys.platform}", 48 | bootloader_ignore_signals=True, 49 | console=False, 50 | debug=False, 51 | strip=False, 52 | upx=True) 53 | 54 | coll = COLLECT(exe, 55 | a.binaries, 56 | a.zipfiles, 57 | a.datas, 58 | debug=False, 59 | strip=False, 60 | upx=False, 61 | name='vorta') 62 | 63 | app = BUNDLE(coll, 64 | name='Vorta.app', 65 | icon='icon.icns', 66 | bundle_identifier=None, 67 | info_plist={ 68 | 'CFBundleName': APP_NAME, 69 | 'CFBundleDisplayName': APP_NAME, 70 | 'CFBundleIdentifier': APP_ID_DARWIN, 71 | 'NSHighResolutionCapable': 'True', 72 | 'LSAppNapIsDisabled': 'True', 73 | 'NSRequiresAquaSystemAppearance': 'False', 74 | 'LSUIElement': '1', 75 | 'LSMinimumSystemVersion': '10.14', 76 | 'CFBundleShortVersionString': APP_VERSION, 77 | 'CFBundleVersion': APP_VERSION, 78 | 'SUFeedURL': APP_APPCAST_URL, 79 | 'LSEnvironment': { 80 | 'LC_CTYPE': 'en_US.UTF-8', 81 | 'PATH': '/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin' 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.ruff] 6 | line-length = 120 7 | # exclude = ["package", "build", "dist", ".git", ".idea", ".cache", ".tox", ".eggs", "./src/vorta/__init__.py", ".direnv", "env"] 8 | include = ["src/**/*.py", "tests/**/*.py"] 9 | 10 | [tool.ruff.lint] 11 | select = [ 12 | "E", # Error 13 | "F", # pyflakes 14 | "I", # isort 15 | "W", # Warning 16 | "YTT", # flake8-2020 17 | ] 18 | ignore = [ 19 | "F401", 20 | ] 21 | 22 | [tool.ruff.format] 23 | quote-style = "preserve" 24 | -------------------------------------------------------------------------------- /requirements.d/Brewfile: -------------------------------------------------------------------------------- 1 | # Install required non-Python dev packages using Homebrew on macOS: 2 | # Run `brew bundle` while in this folder 3 | 4 | brew 'create-dmg' 5 | brew 'qt' 6 | brew 'hub' 7 | brew 'xmlstarlet' 8 | cask 'qt-creator' 9 | cask 'sparkle' 10 | -------------------------------------------------------------------------------- /requirements.d/dev.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | macholib 3 | nox 4 | pkgconfig 5 | pre-commit 6 | pyinstaller 7 | pylint 8 | pytest 9 | pytest-cov 10 | pytest-faulthandler 11 | pytest-mock 12 | pytest-qt 13 | ruff 14 | tox 15 | twine 16 | wheel 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vorta 3 | author = Manuel Riel and Vorta contributors 4 | description = A GUI for Borg Backup 5 | version = attr: vorta._version.__version__ 6 | url = https://github.com/borgbase/vorta 7 | keywords = 8 | backup 9 | borgbackup 10 | # List of classifiers: https://pypi.org/pypi?%3Aaction=list_classifiers 11 | classifiers = 12 | Development Status :: 4 - Beta 13 | Environment :: MacOS X 14 | Environment :: X11 Applications :: Qt 15 | Operating System :: MacOS 16 | Operating System :: POSIX 17 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 18 | Programming Language :: Python :: 3 19 | Topic :: System :: Archiving :: Backup 20 | Topic :: System :: Systems Administration 21 | Topic :: Utilities 22 | long_description = file: README.md 23 | long_description_content_type = text/markdown 24 | license_file = LICENSE.txt 25 | project_urls = 26 | Bug Tracker = https://github.com/borgbase/vorta/issues 27 | Documentation = https://docs.borgbase.com 28 | Source Code = https://github.com/borgbase/vorta 29 | 30 | [options] 31 | packages = find: 32 | package_dir = 33 | =src 34 | include_package_data = true 35 | python_requires = >=3.8 36 | install_requires = 37 | packaging 38 | peewee 39 | platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin' # for others: 2.6+ works consistently. 40 | platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin' # for macOS: breaking changes in 3.0.0, 41 | psutil 42 | pyobjc-core < 10; sys_platform == 'darwin' 43 | pyobjc-framework-Cocoa < 10; sys_platform == 'darwin' 44 | pyobjc-framework-CoreWLAN < 10; sys_platform == 'darwin' 45 | pyobjc-framework-LaunchServices < 10; sys_platform == 'darwin' 46 | pyqt6 47 | secretstorage; sys_platform != 'darwin' 48 | tests_require = 49 | pytest 50 | pytest-qt 51 | pytest-mock 52 | 53 | [options.entry_points] 54 | gui_scripts = 55 | vorta = vorta.__main__:main 56 | 57 | [options.packages.find] 58 | where=src 59 | 60 | [tool:pytest] 61 | addopts = -vs 62 | testpaths = tests 63 | qt_default_raising = true 64 | filterwarnings = 65 | ignore::DeprecationWarning 66 | 67 | [coverage:run] 68 | source = vorta 69 | omit = tests/* 70 | relative_files = true 71 | 72 | [tox:tox] 73 | envlist = py36,py37,py38 74 | skip_missing_interpreters = true 75 | 76 | [testenv] 77 | deps = 78 | pytest 79 | pytest-qt 80 | pytest-mock 81 | commands=pytest 82 | passenv = DISPLAY 83 | 84 | [testenv:ruff] 85 | deps = 86 | ruff 87 | commands=ruff check src tests 88 | 89 | [pycodestyle] 90 | max_line_length = 120 91 | 92 | [pylint.master] 93 | extension-pkg-whitelist=PyQt6 94 | load-plugins= 95 | 96 | [pylint.messages control] 97 | disable= W0511,C0301,R0903,W0212,C0114,C0115,C0116,C0103,E0611,E1120,C0415,R0914,R0912,R0915 98 | 99 | [pylint.format] 100 | max-line-length=120 101 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/vorta/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | -------------------------------------------------------------------------------- /src/vorta/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | 5 | from peewee import SqliteDatabase 6 | 7 | # Need to import config as a whole module instead of individual variables 8 | # because we will be overriding the modules variables 9 | from vorta import config 10 | from vorta._version import __version__ 11 | from vorta.log import init_logger, logger 12 | from vorta.store.connection import init_db 13 | from vorta.updater import get_updater 14 | from vorta.utils import DEFAULT_DIR_FLAG, parse_args 15 | from vorta.views.exception_dialog import ExceptionDialog 16 | 17 | 18 | def main(): 19 | def exception_handler(type, value, tb): 20 | from traceback import format_exception 21 | 22 | logger.critical( 23 | "Uncaught exception, file a report at https://github.com/borgbase/vorta/issues/new/choose", 24 | exc_info=(type, value, tb), 25 | ) 26 | full_exception = ''.join(format_exception(type, value, tb)) 27 | 28 | if app: 29 | exception_dialog = ExceptionDialog(full_exception) 30 | exception_dialog.show() 31 | exception_dialog.raise_() 32 | exception_dialog.activateWindow() 33 | exception_dialog.exec() 34 | else: 35 | # Crashed before app startup, cannot translate 36 | sys.exit(1) 37 | 38 | sys.excepthook = exception_handler 39 | app = None 40 | 41 | args = parse_args() 42 | signal.signal(signal.SIGINT, signal.SIG_DFL) # catch ctrl-c and exit 43 | 44 | want_version = getattr(args, 'version', False) 45 | want_background = getattr(args, 'daemonize', False) 46 | want_development = getattr(args, 'development', False) 47 | 48 | if want_version: 49 | print(f"Vorta {__version__}") # noqa: T201 50 | sys.exit() 51 | 52 | if want_background: 53 | if os.fork(): 54 | sys.exit() 55 | 56 | if want_development: 57 | # if we're using the default dev dir 58 | if want_development is DEFAULT_DIR_FLAG: 59 | config.init_dev_mode(config.default_dev_dir()) 60 | else: 61 | # if we're not using the default dev dir and 62 | # instead we're using whatever dir is passed as an argument 63 | config.init_dev_mode(want_development) 64 | 65 | init_logger(background=want_background) 66 | 67 | # Init database 68 | sqlite_db = SqliteDatabase( 69 | config.SETTINGS_DIR / 'settings.db', 70 | pragmas={ 71 | 'journal_mode': 'wal', 72 | }, 73 | ) 74 | init_db(sqlite_db) 75 | 76 | # Init app after database is available 77 | from vorta.application import VortaApp 78 | 79 | app = VortaApp(sys.argv, single_app=args.profile is None) 80 | app.updater = get_updater() 81 | 82 | sys.exit(app.exec()) 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /src/vorta/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.10.3" 2 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/export_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 555 10 | 78 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 22 | 23 | Include Borg passphrase in export. Use with caution! 24 | 25 | 26 | Include borg passphrase in export 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::Vertical 34 | 35 | 36 | 37 | 20 38 | 40 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | QDialogButtonBox::Cancel|QDialogButtonBox::Save 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/import_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 551 10 | 142 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Borg passphrase: 26 | 27 | 28 | 29 | 30 | 31 | 32 | Enter passphrase 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | QLineEdit::Password 42 | 43 | 44 | 45 | 46 | 47 | false 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Overwrite existing profile 57 | 58 | 59 | 60 | 61 | 62 | 63 | Overwrite existing settings 64 | 65 | 66 | true 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 0 76 | 77 | 78 | 79 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/log_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LogPage 4 | 5 | 6 | 7 | 0 8 | 0 9 | 687 10 | 384 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 11 19 | false 20 | 21 | 22 | 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | false 30 | 31 | 32 | 33 | Time 34 | 35 | 36 | 37 | 38 | Category 39 | 40 | 41 | 42 | 43 | Subcommand 44 | 45 | 46 | 47 | 48 | Repository 49 | 50 | 51 | 52 | 53 | Returncode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | <html><head/><body><p><a href="file:///"><span style=" text-decoration: underline; color:#0984e3;">View the logs</span></a></p></body></html> 62 | 63 | 64 | 0 65 | 66 | 67 | true 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/misc_tab.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 791 10 | 497 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 12 19 | 20 | 21 | 12 22 | 23 | 24 | 25 | 26 | QFrame::NoFrame 27 | 28 | 29 | 0 30 | 31 | 32 | 33 | 34 | 35 | 36 | Qt::Vertical 37 | 38 | 39 | 40 | 20 41 | 40 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/networks_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | NetworksPage 4 | 5 | 6 | 7 | 0 8 | 0 9 | 687 10 | 384 11 | 12 | 13 | 14 | Networks 15 | 16 | 17 | 18 | 12 19 | 20 | 21 | 22 | 23 | 4 24 | 25 | 26 | 27 | 28 | true 29 | 30 | 31 | Allowed Networks: 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 0 40 | 1 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Run backups over metered networks 51 | 52 | 53 | false 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/vorta/assets/UI/schedule_tab.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 687 10 | 544 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | 33 | false 34 | 35 | 36 | 37 | 38 | 39 | 0 40 | 0 41 | 687 42 | 384 43 | 44 | 45 | 46 | Schedule 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 0 55 | 0 56 | 687 57 | 384 58 | 59 | 60 | 61 | Networks 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 0 70 | 0 71 | 687 72 | 384 73 | 74 | 75 | 76 | Log 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 0 85 | 0 86 | 687 87 | 384 88 | 89 | 90 | 91 | Shell Commands 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/vorta/assets/exclusion_presets/apps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Spotify cache and config files", 4 | "slug": "spotify", 5 | "patterns": [ 6 | "fm:*/.cache/spotify", 7 | "fm:*/.config/spotify", 8 | "fm:*/.var/app/com.spotify.Client/cache", 9 | "fm:*/.var/app/com.spotify.Client/config/spotify" 10 | ], 11 | "tags": ["type:media", "media:spotify", "os:linux"], 12 | "author": "SAMAD101, Renner0E" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /src/vorta/assets/exclusion_presets/media.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Media Files", 4 | "slug": "media-files", 5 | "patterns": [ 6 | "fm:*.3g2", 7 | "fm:*.3gp", 8 | "fm:*.aac", 9 | "fm:*.avi", 10 | "fm:*.f4a", 11 | "fm:*.f4b", 12 | "fm:*.f4p", 13 | "fm:*.f4v", 14 | "fm:*.flac", 15 | "fm:*.flv", 16 | "fm:*.m2ts", 17 | "fm:*.m4a", 18 | "fm:*.m4p", 19 | "fm:*.m4v", 20 | "fm:*.mkv", 21 | "fm:*.mov", 22 | "fm:*.movie", 23 | "fm:*.mp2", 24 | "fm:*.mp3", 25 | "fm:*.mp4", 26 | "fm:*.mpeg", 27 | "fm:*.mpg", 28 | "fm:*.mts", 29 | "fm:*.ogg", 30 | "fm:*.opus", 31 | "fm:*.qt", 32 | "fm:*.svi", 33 | "fm:*.vob", 34 | "fm:*.wav", 35 | "fm:*.webm", 36 | "fm:*.wmv", 37 | "fm:*.yuv" 38 | ], 39 | "tags": [ 40 | "type:media", "os:linux", "os:darwin"], 41 | "author": "shivansh02, Renner0E" 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /src/vorta/assets/exclusion_presets/temp.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Temporary Files", 4 | "slug": "temp-files", 5 | "patterns": [ 6 | "fm:*/.tmp", 7 | "fm:*/temp", 8 | "fm:*.swp", 9 | "fm:*.bak", 10 | "fm:*.part" 11 | ], 12 | "tags": [ 13 | "type:system", "os:linux", "os:darwin"], 14 | "author": "shivansh02, Renner0E" 15 | }, 16 | { 17 | "name": "All Cache Files", 18 | "slug": "cache-files", 19 | "patterns": [ 20 | "fm:*/.cache", 21 | "fm:*/Caches" 22 | ], 23 | "tags": [ 24 | "type:system","os:linux", "os:darwin"], 25 | "author": "shivansh02" 26 | }, 27 | { 28 | "name": "Recycle Bin/Trash", 29 | "slug": "recycle-bin-trash", 30 | "patterns": [ 31 | "fm:*/.local/share/Trash/*", 32 | "fm:*/.Trash/*", 33 | "fm:*/.Trash-*/*" 34 | ], 35 | "tags": [ 36 | "type:system", "os:linux", "os:darwin"], 37 | "author": "shivansh02" 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/angle-down-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/angle-up-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/broom-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/clock-o.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/cloud-download.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/cut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/eject.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/ellipsis-v.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/exclamation-triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/eye-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/file-import-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 63 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/folder-on-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/folder-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/hdd-o-active-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/hdd-o-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/icons/hdd-o-active.png -------------------------------------------------------------------------------- /src/vorta/assets/icons/hdd-o-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/hdd-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/icons/hdd-o.png -------------------------------------------------------------------------------- /src/vorta/assets/icons/help-about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/icons/loading.gif -------------------------------------------------------------------------------- /src/vorta/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/paste.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/settings_wheel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/stream-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/tasks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/unlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/view-list-details.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/view-list-tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/wifi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/icons/window-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/vorta/assets/metadata/Screenshot-1-Repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/metadata/Screenshot-1-Repository.png -------------------------------------------------------------------------------- /src/vorta/assets/metadata/Screenshot-2-Sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/metadata/Screenshot-2-Sources.png -------------------------------------------------------------------------------- /src/vorta/assets/metadata/Screenshot-3-Schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/metadata/Screenshot-3-Schedule.png -------------------------------------------------------------------------------- /src/vorta/assets/metadata/Screenshot-4-Archives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/assets/metadata/Screenshot-4-Archives.png -------------------------------------------------------------------------------- /src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.borgbase.Vorta 4 | com.borgbase.Vorta.desktop 5 | Vorta contributors 6 | Vorta 7 | GPL-3.0 8 | CC0-1.0 9 | 10 | mild 11 | 12 | Backup client 13 | 14 |

15 | Vorta is a backup client for macOS and Linux desktops. 16 | It integrates the mighty BorgBackup with your desktop environment 17 | to protect your data from disk failure, ransomware and theft. 18 |

19 |

20 | Why is this great? 21 |

22 |
    23 |
  • Encrypted, deduplicated and compressed backups using Borg as backend.
  • 24 |
  • No vendor lock-in – back up to local drives, your own server or BorgBase, a hosting service for Borg backups.
  • 25 |
  • Open source – free to use, modify, improve and audit.
  • 26 |
  • Flexible profiles to group source folders, backup destinations and schedules.
  • 27 |
  • One place to view all point-in-time archives and restore individual files.
  • 28 |
29 |
30 | 31 | 32 | https://raw.githubusercontent.com/borgbase/vorta/master/src/vorta/assets/metadata/Screenshot-1-Repository.png 33 | 34 | 35 | https://raw.githubusercontent.com/borgbase/vorta/master/src/vorta/assets/metadata/Screenshot-2-Sources.png 36 | 37 | 38 | https://raw.githubusercontent.com/borgbase/vorta/master/src/vorta/assets/metadata/Screenshot-3-Schedule.png 39 | 40 | 41 | https://raw.githubusercontent.com/borgbase/vorta/master/src/vorta/assets/metadata/Screenshot-4-Archives.png 42 | 43 | 44 | 45 | 46 | 47 | See Github for detailed release notes: github.com/borgbase/vorta/releases 48 | 49 | 50 | 51 | https://github.com/borgbase/vorta/issues 52 | https://vorta.borgbase.com/usage/ 53 | https://vorta.borgbase.com/ 54 | https://www.transifex.com/borgbase/vorta/ 55 |
56 | -------------------------------------------------------------------------------- /src/vorta/assets/metadata/com.borgbase.Vorta.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Vorta 3 | GenericName=Backup Software 4 | Exec=vorta 5 | Type=Application 6 | Icon=com.borgbase.Vorta 7 | Categories=Utility;Archiving;Qt; 8 | Keywords=borg; 9 | StartupWMClass=python3 10 | -------------------------------------------------------------------------------- /src/vorta/borg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/borg/__init__.py -------------------------------------------------------------------------------- /src/vorta/borg/_compatibility.py: -------------------------------------------------------------------------------- 1 | from packaging.version import Version 2 | 3 | MIN_BORG_FOR_FEATURE = { 4 | "BLAKE2": Version("1.1.4"), 5 | "ZSTD": Version("1.1.4"), 6 | "JSON_LOG": Version("1.1.0"), 7 | "DIFF_JSON_LINES": Version("1.1.16"), 8 | "COMPACT_SUBCOMMAND": Version("1.2.0a1"), 9 | "V122": Version("1.2.2"), 10 | "V2": Version("2.0.0b10"), 11 | # add new version-checks here. 12 | } 13 | 14 | 15 | class BorgCompatibility: 16 | """ 17 | An internal class that keeps details of the Borg version 18 | in use and allows checking for specific features. Could be used 19 | to customize Borg commands by version in the future. 20 | """ 21 | 22 | version = "1.1.4" 23 | path = "" 24 | 25 | def set_version(self, version, path): 26 | self.version = version 27 | self.path = path 28 | 29 | def check(self, feature_name): 30 | return Version(self.version) >= MIN_BORG_FOR_FEATURE[feature_name] 31 | 32 | def get_version(self): 33 | """Returns the version and path of the Borg binary.""" 34 | return self.version, self.path 35 | -------------------------------------------------------------------------------- /src/vorta/borg/break_lock.py: -------------------------------------------------------------------------------- 1 | from .borg_job import BorgJob 2 | 3 | 4 | class BorgBreakJob(BorgJob): 5 | def started_event(self): 6 | self.app.backup_started_event.emit() 7 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Breaking repository lock…')}") 8 | 9 | def finished_event(self, result): 10 | self.app.backup_finished_event.emit(result) 11 | self.app.backup_progress_event.emit( 12 | f"[{self.params['profile_name']}] {self.tr('Repository lock broken. Please redo your last action.')}" 13 | ) 14 | self.result.emit(result) 15 | 16 | @classmethod 17 | def prepare(cls, profile): 18 | ret = super().prepare(profile) 19 | if not ret['ok']: 20 | return ret 21 | else: 22 | ret['ok'] = False # Set back to false, so we can do our own checks here. 23 | 24 | cmd = ['borg', 'break-lock', '--info', '--log-json'] 25 | cmd.append(f'{profile.repo.url}') 26 | 27 | ret['ok'] = True 28 | ret['cmd'] = cmd 29 | 30 | return ret 31 | -------------------------------------------------------------------------------- /src/vorta/borg/check.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from vorta import config 4 | from vorta.i18n import translate 5 | from vorta.utils import borg_compat 6 | 7 | from .borg_job import BorgJob 8 | 9 | 10 | class BorgCheckJob(BorgJob): 11 | def started_event(self): 12 | self.app.backup_started_event.emit() 13 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Starting consistency check…')}") 14 | 15 | def finished_event(self, result: Dict[str, Any]): 16 | """ 17 | Process that the job terminated with the given results. 18 | 19 | Parameters 20 | ---------- 21 | result : Dict[str, Any] 22 | The (json-like) dictionary containing the job results. 23 | """ 24 | self.app.backup_finished_event.emit(result) 25 | self.result.emit(result) 26 | if result['returncode'] != 0: 27 | self.app.backup_progress_event.emit( 28 | f"[{self.params['profile_name']}] " 29 | + translate('RepoCheckJob', 'Repo check failed. See the logs for details.').format( 30 | config.LOG_DIR.as_uri() 31 | ) 32 | ) 33 | self.app.check_failed_event.emit(result) 34 | else: 35 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Check completed.')}") 36 | 37 | @classmethod 38 | def prepare(cls, profile): 39 | ret = super().prepare(profile) 40 | if not ret['ok']: 41 | return ret 42 | else: 43 | ret['ok'] = False # Set back to false, so we can do our own checks here. 44 | 45 | cmd = ['borg', 'check', '--info', '--log-json', '--progress'] 46 | if borg_compat.check('V2'): 47 | cmd = cmd + ["-r", profile.repo.url] 48 | else: 49 | cmd.append(f'{profile.repo.url}') 50 | 51 | ret['ok'] = True 52 | ret['cmd'] = cmd 53 | 54 | return ret 55 | -------------------------------------------------------------------------------- /src/vorta/borg/compact.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from vorta import config 4 | from vorta.i18n import translate 5 | from vorta.utils import borg_compat 6 | 7 | from .borg_job import BorgJob 8 | 9 | 10 | class BorgCompactJob(BorgJob): 11 | def started_event(self): 12 | self.app.backup_started_event.emit() 13 | self.app.backup_progress_event.emit( 14 | f"[{self.params['profile_name']} {self.tr('Starting repository compaction...')}]" 15 | ) 16 | 17 | def finished_event(self, result: Dict[str, Any]): 18 | """ 19 | Process that the job terminated with the given results. 20 | 21 | Parameters 22 | ---------- 23 | result : Dict[str, Any] 24 | The (json-like) dictionary containing the job results. 25 | """ 26 | self.app.backup_finished_event.emit(result) 27 | self.result.emit(result) 28 | if result['returncode'] != 0: 29 | self.app.backup_progress_event.emit( 30 | f"[{self.params['profile_name']}] " 31 | + translate( 32 | 'BorgCompactJob', 'Errors during compaction. See the logs for details.' 33 | ).format(config.LOG_DIR.as_uri()) 34 | ) 35 | else: 36 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Compaction completed.')}") 37 | 38 | @classmethod 39 | def prepare(cls, profile): 40 | ret = super().prepare(profile) 41 | if not ret['ok']: 42 | return ret 43 | else: 44 | ret['ok'] = False # Set back to false, so we can do our own checks here. 45 | 46 | if not borg_compat.check('COMPACT_SUBCOMMAND'): 47 | raise Exception('The compact action needs Borg >= 1.2.0') 48 | 49 | cmd = ['borg', '--info', '--log-json', 'compact', '--progress'] 50 | if borg_compat.check('V2'): 51 | cmd = cmd + ["-r", profile.repo.url] 52 | else: 53 | cmd.append(f'{profile.repo.url}') 54 | 55 | ret['ok'] = True 56 | ret['cmd'] = cmd 57 | 58 | return ret 59 | -------------------------------------------------------------------------------- /src/vorta/borg/delete.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from vorta.store.models import RepoModel 4 | from vorta.utils import borg_compat 5 | 6 | from .borg_job import BorgJob 7 | 8 | 9 | class BorgDeleteJob(BorgJob): 10 | def started_event(self): 11 | self.app.backup_started_event.emit() 12 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Deleting archive…')}") 13 | 14 | def finished_event(self, result): 15 | # set repo stats to N/A 16 | repo = RepoModel.get(id=result['params']['repo_id']) 17 | repo.total_size = None 18 | repo.unique_csize = None 19 | repo.unique_size = None 20 | repo.total_unique_chunks = None 21 | repo.save() 22 | 23 | self.app.backup_finished_event.emit(result) 24 | self.result.emit(result) 25 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Archive deleted.')}") 26 | 27 | @classmethod 28 | def prepare(cls, profile, archives: List[str]): 29 | ret = super().prepare(profile) 30 | if not ret['ok']: 31 | return ret 32 | else: 33 | ret['ok'] = False # Set back to false, so we can do our own checks here. 34 | 35 | if len(archives) <= 0: 36 | return ret 37 | 38 | cmd = ['borg', 'delete', '--info', '--log-json'] 39 | if borg_compat.check('V2'): 40 | cmd = cmd + ["-r", profile.repo.url, '-a'] 41 | cmd.append(f"re:({'|'.join(archives)})") 42 | else: 43 | cmd.append(f'{profile.repo.url}::{archives[0]}') 44 | cmd.extend(archives[1:]) 45 | 46 | ret['archives'] = archives 47 | ret['cmd'] = cmd 48 | ret['ok'] = True 49 | 50 | return ret 51 | -------------------------------------------------------------------------------- /src/vorta/borg/diff.py: -------------------------------------------------------------------------------- 1 | from vorta.utils import borg_compat 2 | 3 | from .borg_job import BorgJob 4 | 5 | 6 | class BorgDiffJob(BorgJob): 7 | def started_event(self): 8 | self.app.backup_started_event.emit() 9 | self.app.backup_progress_event.emit( 10 | f"[{self.params['profile_name']}] {self.tr('Requesting differences between archives…')}" 11 | ) 12 | 13 | def finished_event(self, result): 14 | self.app.backup_finished_event.emit(result) 15 | self.app.backup_progress_event.emit( 16 | f"[{self.params['profile_name']}] {self.tr('Obtained differences between archives.')}" 17 | ) 18 | self.result.emit(result) 19 | 20 | @classmethod 21 | def prepare(cls, profile, archive_name_1, archive_name_2): 22 | ret = super().prepare(profile) 23 | if not ret['ok']: 24 | return ret 25 | 26 | ret['cmd'] = ['borg', 'diff', '--info', '--log-json'] 27 | ret['json_lines'] = False 28 | if borg_compat.check('DIFF_JSON_LINES'): 29 | ret['cmd'].append('--json-lines') 30 | ret['json_lines'] = True 31 | 32 | if borg_compat.check('V2'): 33 | ret['cmd'].extend(['-r', profile.repo.url, archive_name_1, archive_name_2]) 34 | else: 35 | ret['cmd'].extend([f'{profile.repo.url}::{archive_name_1}', archive_name_2]) 36 | 37 | ret['ok'] = True 38 | ret['archive_name_older'] = archive_name_1 39 | ret['archive_name_newer'] = archive_name_2 40 | 41 | return ret 42 | -------------------------------------------------------------------------------- /src/vorta/borg/extract.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from PyQt6.QtCore import QModelIndex, Qt 4 | 5 | from vorta.utils import borg_compat 6 | from vorta.views.extract_dialog import ExtractTree, FileData 7 | from vorta.views.partials.treemodel import FileSystemItem, path_to_str 8 | 9 | from .borg_job import BorgJob 10 | 11 | 12 | class BorgExtractJob(BorgJob): 13 | def started_event(self): 14 | self.app.backup_started_event.emit() 15 | self.app.backup_progress_event.emit( 16 | f"[{self.params['profile_name']}] {self.tr('Downloading files from archive…')}" 17 | ) 18 | 19 | def finished_event(self, result): 20 | self.app.backup_finished_event.emit(result) 21 | self.result.emit(result) 22 | self.app.backup_progress_event.emit( 23 | f"[{self.params['profile_name']}] {self.tr('Restored files from archive.')}" 24 | ) 25 | 26 | @classmethod 27 | def prepare(cls, profile, archive_name, model: ExtractTree, destination_folder): 28 | ret = super().prepare(profile) 29 | if not ret['ok']: 30 | return ret 31 | else: 32 | ret['ok'] = False # Set back to false, so we can do our own checks here. 33 | 34 | cmd = ['borg', 'extract', '--list', '--info', '--log-json'] 35 | if borg_compat.check('V2'): 36 | cmd += ['-r', profile.repo.url, archive_name] 37 | else: 38 | cmd.append(f'{profile.repo.url}::{archive_name}') 39 | 40 | # process selected items 41 | # all items will be excluded beside the one actively selected in the 42 | # dialog. 43 | # Unselected (and excluded) parent folders will be restored by borg 44 | # but without the metadata stored in the archive. 45 | pattern_file = tempfile.NamedTemporaryFile('w', delete=True) 46 | pattern_file.write("P pf\n") 47 | 48 | indexes = [QModelIndex()] 49 | while indexes: 50 | index = indexes.pop() 51 | 52 | for i in range(model.rowCount(index)): 53 | new_index = model.index(i, 0, index) 54 | indexes.append(new_index) 55 | 56 | item: FileSystemItem[FileData] = new_index.internalPointer() 57 | if item.data.checkstate == Qt.CheckState.Checked: 58 | pattern_file.write("+ " + path_to_str(item.path) + "\n") 59 | 60 | pattern_file.write("- fm:*\n") 61 | pattern_file.flush() 62 | cmd.extend(['--patterns-from', pattern_file.name]) 63 | ret['cleanup_files'].append(pattern_file) 64 | 65 | ret['ok'] = True 66 | ret['cmd'] = cmd 67 | ret['cwd'] = destination_folder 68 | 69 | return ret 70 | 71 | def process_result(self, result: dict): 72 | pass 73 | -------------------------------------------------------------------------------- /src/vorta/borg/info_archive.py: -------------------------------------------------------------------------------- 1 | from vorta.store.models import ArchiveModel, RepoModel 2 | from vorta.utils import borg_compat 3 | 4 | from .borg_job import BorgJob 5 | 6 | 7 | class BorgInfoArchiveJob(BorgJob): 8 | def started_event(self): 9 | self.app.backup_started_event.emit() 10 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Refreshing archive…')}") 11 | 12 | def finished_event(self, result): 13 | self.app.backup_finished_event.emit(result) 14 | self.result.emit(result) 15 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Refreshing archive done.')}") 16 | 17 | @classmethod 18 | def prepare(cls, profile, archive_name): 19 | ret = super().prepare(profile) 20 | if not ret['ok']: 21 | return ret 22 | 23 | ret['ok'] = True 24 | ret['cmd'] = ['borg', 'info', '--log-json', '--json'] 25 | if borg_compat.check('V2'): 26 | ret['cmd'].extend(["-r", profile.repo.url, '-a', archive_name]) 27 | else: 28 | ret['cmd'].append(f'{profile.repo.url}::{archive_name}') 29 | ret['archive_name'] = archive_name 30 | 31 | return ret 32 | 33 | def process_result(self, result): 34 | if result['returncode'] == 0: 35 | remote_archives = result['data'].get('archives', []) 36 | 37 | # get info stored during BorgJob.prepare() 38 | # repo_id = self.params['repo_id'] 39 | repo_id = result['params']['repo_id'] 40 | 41 | # Update remote archives. 42 | for remote_archive in remote_archives: 43 | archive = ArchiveModel.get_or_none(snapshot_id=remote_archive['id'], repo=repo_id) 44 | if archive is None: 45 | # archive id was changed during rename, so we need to find it by name 46 | archive = ArchiveModel.get_or_none(name=remote_archive['name'], repo=repo_id) 47 | archive.snapshot_id = remote_archive['id'] 48 | 49 | archive.name = remote_archive['name'] # incase name changed 50 | # archive.time = parser.parse(remote_archive['time']) 51 | archive.duration = remote_archive['duration'] 52 | archive.size = remote_archive['stats']['deduplicated_size'] 53 | 54 | archive.save() 55 | 56 | if 'cache' in result['data']: 57 | stats = result['data']['cache']['stats'] 58 | repo = RepoModel.get(id=result['params']['repo_id']) 59 | repo.total_size = stats['total_size'] 60 | repo.unique_size = stats['unique_size'] 61 | repo.total_unique_chunks = stats['total_unique_chunks'] 62 | repo.save() 63 | -------------------------------------------------------------------------------- /src/vorta/borg/info_repo.py: -------------------------------------------------------------------------------- 1 | from vorta.i18n import trans_late 2 | from vorta.store.models import RepoModel 3 | from vorta.utils import borg_compat 4 | 5 | from .borg_job import BorgJob, FakeProfile, FakeRepo 6 | 7 | 8 | class BorgInfoRepoJob(BorgJob): 9 | def started_event(self): 10 | self.updated.emit(self.tr('Validating existing repo…')) 11 | 12 | @classmethod 13 | def prepare(cls, params): 14 | """ 15 | Used to validate existing repository when added. 16 | """ 17 | 18 | # Build fake profile because we don't have it in the DB yet. Assume unencrypted. 19 | profile = FakeProfile( 20 | 999, 21 | FakeRepo(params['repo_url'], params['repo_name'], 999, params['extra_borg_arguments'], 'none'), 22 | 'New Repo', 23 | params['ssh_key'], 24 | ) 25 | 26 | ret = super().prepare(profile) 27 | if not ret['ok']: 28 | return ret 29 | else: 30 | ret['ok'] = False # Set back to false, so we can do our own checks here. 31 | 32 | if borg_compat.check('V2'): 33 | cmd = ["borg", "repo-info", "--info", "--json", "--log-json", "-r"] 34 | else: 35 | cmd = ["borg", "info", "--info", "--json", "--log-json"] 36 | cmd.append(profile.repo.url) 37 | 38 | ret['additional_env'] = { 39 | 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': "yes", 40 | 'BORG_RSH': 'ssh -oStrictHostKeyChecking=accept-new', 41 | } 42 | 43 | ret['password'] = params['password'] # Empty password is '', which disables prompt 44 | if params['password'] != '': 45 | # Cannot tell if repo has encryption, assuming based off of password 46 | if not cls.keyring.is_unlocked: 47 | ret['message'] = trans_late('messages', 'Please unlock your password manager.') 48 | return ret 49 | 50 | ret['repo_name'] = params['repo_name'] 51 | ret['ok'] = True 52 | ret['cmd'] = cmd 53 | 54 | return ret 55 | 56 | def process_result(self, result): 57 | if result['returncode'] == 0: 58 | new_repo, _ = RepoModel.get_or_create( 59 | url=result['cmd'][-1], defaults={'name': result['params']['repo_name']} 60 | ) 61 | if 'cache' in result['data']: 62 | stats = result['data']['cache']['stats'] 63 | new_repo.total_size = stats['total_size'] 64 | new_repo.unique_size = stats['unique_size'] 65 | new_repo.total_unique_chunks = stats['total_unique_chunks'] 66 | if 'encryption' in result['data']: 67 | new_repo.encryption = result['data']['encryption']['mode'] 68 | if new_repo.encryption != 'none': 69 | self.keyring.set_password("vorta-repo", new_repo.url, result['params']['password']) 70 | new_repo.extra_borg_arguments = result['params']['extra_borg_arguments'] 71 | 72 | new_repo.save() 73 | -------------------------------------------------------------------------------- /src/vorta/borg/init.py: -------------------------------------------------------------------------------- 1 | from vorta.store.models import RepoModel 2 | from vorta.utils import borg_compat 3 | 4 | from .borg_job import BorgJob, FakeProfile, FakeRepo 5 | 6 | 7 | class BorgInitJob(BorgJob): 8 | def started_event(self): 9 | self.updated.emit(self.tr('Setting up new repo…')) 10 | 11 | @classmethod 12 | def prepare(cls, params): 13 | # Build fake profile because we don't have it in the DB yet. 14 | profile = FakeProfile( 15 | 999, 16 | FakeRepo( 17 | params['repo_url'], 18 | params['repo_name'], 19 | 999, 20 | params['extra_borg_arguments'], 21 | params['encryption'], 22 | ), 23 | 'Init Repo', 24 | params['ssh_key'], 25 | ) 26 | 27 | ret = super().prepare(profile) 28 | if not ret['ok']: 29 | return ret 30 | else: 31 | ret['ok'] = False # Set back to false, so we can do our own checks here. 32 | 33 | if borg_compat.check('V2'): 34 | cmd = [ 35 | "borg", 36 | "repo-create", 37 | "--info", 38 | "--log-json", 39 | f"--encryption={params['encryption']}", 40 | "-r", 41 | params['repo_url'], 42 | ] 43 | else: 44 | cmd = ["borg", "init", "--info", "--log-json"] 45 | cmd.append(f"--encryption={params['encryption']}") 46 | cmd.append(params['repo_url']) 47 | 48 | ret['additional_env'] = {'BORG_RSH': 'ssh -oStrictHostKeyChecking=accept-new'} 49 | 50 | ret['encryption'] = params['encryption'] 51 | ret['password'] = params['password'] 52 | ret['ok'] = True 53 | ret['cmd'] = cmd 54 | 55 | return ret 56 | 57 | def process_result(self, result): 58 | if result['returncode'] == 0: 59 | new_repo, created = RepoModel.get_or_create( 60 | url=result['params']['repo_url'], 61 | defaults={ 62 | 'encryption': result['params']['encryption'], 63 | 'extra_borg_arguments': result['params']['extra_borg_arguments'], 64 | 'name': result['params']['repo_name'], 65 | }, 66 | ) 67 | if new_repo.encryption != 'none': 68 | self.keyring.set_password("vorta-repo", new_repo.url, result['params']['password']) 69 | new_repo.save() 70 | -------------------------------------------------------------------------------- /src/vorta/borg/list_archive.py: -------------------------------------------------------------------------------- 1 | from vorta.utils import borg_compat 2 | 3 | from .borg_job import BorgJob 4 | 5 | 6 | class BorgListArchiveJob(BorgJob): 7 | def started_event(self): 8 | self.app.backup_started_event.emit() 9 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Getting archive content…')}") 10 | 11 | def finished_event(self, result): 12 | self.app.backup_finished_event.emit(result) 13 | self.app.backup_progress_event.emit( 14 | f"[{self.params['profile_name']}] {self.tr('Done getting archive content.')}" 15 | ) 16 | self.result.emit(result) 17 | 18 | @classmethod 19 | def prepare(cls, profile, archive_name): 20 | ret = super().prepare(profile) 21 | if not ret['ok']: 22 | return ret 23 | 24 | ret['archive_name'] = archive_name 25 | ret['cmd'] = [ 26 | 'borg', 27 | 'list', 28 | '--info', 29 | '--log-json', 30 | '--json-lines', 31 | '--format', 32 | # fields to include in json output 33 | "{mode}{user}{group}{size}{" 34 | + ('isomtime' if borg_compat.check('V122') else 'mtime') 35 | + "}{path}{source}{health}{NL}", 36 | ] 37 | 38 | if borg_compat.check('V2'): 39 | ret['cmd'].extend(["-r", profile.repo.url, archive_name]) 40 | else: 41 | ret['cmd'].append(f'{profile.repo.url}::{archive_name}') 42 | 43 | ret['ok'] = True 44 | 45 | return ret 46 | -------------------------------------------------------------------------------- /src/vorta/borg/list_repo.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | 3 | from vorta.store.models import ArchiveModel, RepoModel 4 | from vorta.utils import borg_compat 5 | 6 | from .borg_job import BorgJob 7 | 8 | 9 | class BorgListRepoJob(BorgJob): 10 | def started_event(self): 11 | self.app.backup_started_event.emit() 12 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Refreshing archives…')}") 13 | 14 | def finished_event(self, result): 15 | self.app.backup_finished_event.emit(result) 16 | self.result.emit(result) 17 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Refreshing archives done.')}") 18 | 19 | @classmethod 20 | def prepare(cls, profile): 21 | ret = super().prepare(profile) 22 | if not ret['ok']: 23 | return ret 24 | else: 25 | ret['ok'] = False # Set back to false, so we can do our own checks here. 26 | 27 | if borg_compat.check('V2'): 28 | cmd = ['borg', 'repo-list', '--info', '--log-json', '--json', '-r'] 29 | else: 30 | cmd = ['borg', 'list', '--info', '--log-json', '--json'] 31 | cmd.append(f'{profile.repo.url}') 32 | 33 | ret['ok'] = True 34 | ret['cmd'] = cmd 35 | 36 | return ret 37 | 38 | def process_result(self, result): 39 | if result['returncode'] == 0: 40 | repo, created = RepoModel.get_or_create(url=result['cmd'][-1]) 41 | if not result['data']: 42 | result['data'] = {} # TODO: Workaround for tests. Can't read mock results 2x. 43 | remote_archives = result['data'].get('archives', []) 44 | 45 | # Delete archives that don't exist on the remote side 46 | for archive in ArchiveModel.select().where(ArchiveModel.repo == repo.id): 47 | if not list(filter(lambda s: s['id'] == archive.snapshot_id, remote_archives)): 48 | archive.delete_instance() 49 | 50 | # Add remote archives we don't have locally. 51 | for archive in result['data'].get('archives', []): 52 | new_archive, _ = ArchiveModel.get_or_create( 53 | snapshot_id=archive['id'], 54 | repo=repo.id, 55 | defaults={ 56 | 'name': archive['name'], 57 | 'time': dt.fromisoformat(archive['time']).replace(tzinfo=None), 58 | }, 59 | ) 60 | new_archive.save() 61 | -------------------------------------------------------------------------------- /src/vorta/borg/mount.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from vorta.store.models import SettingsModel 5 | from vorta.utils import SHELL_PATTERN_ELEMENT, borg_compat 6 | 7 | from .borg_job import BorgJob 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class BorgMountJob(BorgJob): 13 | def started_event(self): 14 | self.updated.emit(self.tr('Mounting archive into folder…')) 15 | 16 | @classmethod 17 | def prepare(cls, profile, archive: str = None): 18 | ret = super().prepare(profile) 19 | if not ret['ok']: 20 | return ret 21 | else: 22 | ret['ok'] = False # Set back to false, so we can do our own checks here. 23 | 24 | cmd = ['borg', '--log-json', 'mount'] 25 | 26 | # Try to override existing permissions when mounting an archive. May help to read 27 | # files that come from a different system, like a restrictive NAS. 28 | override_mount_permissions = SettingsModel.get(key='override_mount_permissions').value 29 | if override_mount_permissions: 30 | cmd += ['-o', f"umask=0277,uid={os.getuid()}"] 31 | 32 | if borg_compat.check('V2'): 33 | cmd.extend(["-r", profile.repo.url]) 34 | 35 | if archive: 36 | # in shell patterns ?, * and [...] have a special meaning 37 | pattern = SHELL_PATTERN_ELEMENT.sub(r'\\1', archive) # escape them 38 | cmd.extend(['-a', pattern]) 39 | else: 40 | source = f'{profile.repo.url}' 41 | 42 | if archive: 43 | source += f'::{archive}' 44 | 45 | cmd.append(source) 46 | 47 | if archive: 48 | ret['mounted_archive'] = archive 49 | 50 | ret['ok'] = True 51 | ret['cmd'] = cmd 52 | 53 | return ret 54 | -------------------------------------------------------------------------------- /src/vorta/borg/prune.py: -------------------------------------------------------------------------------- 1 | from vorta.store.models import RepoModel 2 | from vorta.utils import borg_compat, format_archive_name 3 | 4 | from .borg_job import BorgJob 5 | 6 | 7 | class BorgPruneJob(BorgJob): 8 | def started_event(self): 9 | self.app.backup_started_event.emit() 10 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning old archives…')}") 11 | 12 | def finished_event(self, result): 13 | # set repo stats to N/A 14 | repo = RepoModel.get(id=result['params']['repo_id']) 15 | repo.total_size = None 16 | repo.unique_csize = None 17 | repo.unique_size = None 18 | repo.total_unique_chunks = None 19 | repo.save() 20 | 21 | self.app.backup_finished_event.emit(result) 22 | self.result.emit(result) 23 | self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning done.')}") 24 | 25 | @classmethod 26 | def prepare(cls, profile): 27 | ret = super().prepare(profile) 28 | if not ret['ok']: 29 | return ret 30 | else: 31 | ret['ok'] = False # Set back to false, so we can do our own checks here. 32 | 33 | cmd = ['borg', 'prune', '--list', '--info', '--log-json'] 34 | 35 | pruning_opts = [ 36 | '--keep-hourly', 37 | str(profile.prune_hour), 38 | '--keep-daily', 39 | str(profile.prune_day), 40 | '--keep-weekly', 41 | str(profile.prune_week), 42 | '--keep-monthly', 43 | str(profile.prune_month), 44 | '--keep-yearly', 45 | str(profile.prune_year), 46 | ] 47 | 48 | if profile.prune_prefix: 49 | formatted_prune_prefix = format_archive_name(profile, profile.prune_prefix) 50 | 51 | if borg_compat.check('V2'): 52 | pruning_opts += ['-a', f"sh:{formatted_prune_prefix}*"] 53 | elif borg_compat.check('V122'): 54 | pruning_opts += ['-a', f"{formatted_prune_prefix}*"] 55 | else: 56 | pruning_opts += ['--prefix', formatted_prune_prefix] 57 | 58 | if profile.prune_keep_within: 59 | pruning_opts += ['--keep-within', profile.prune_keep_within] 60 | cmd += pruning_opts 61 | if borg_compat.check('V2'): 62 | cmd.extend(["-r", profile.repo.url]) 63 | else: 64 | cmd.append(f'{profile.repo.url}') 65 | 66 | ret['ok'] = True 67 | ret['cmd'] = cmd 68 | 69 | return ret 70 | -------------------------------------------------------------------------------- /src/vorta/borg/rename.py: -------------------------------------------------------------------------------- 1 | from vorta.store.models import ArchiveModel, RepoModel 2 | from vorta.utils import borg_compat 3 | 4 | from .borg_job import BorgJob 5 | 6 | 7 | class BorgRenameJob(BorgJob): 8 | def log_event(self, msg): 9 | self.app.backup_log_event.emit(msg) 10 | 11 | @classmethod 12 | def prepare(cls, profile, old_archive_name, new_archive_name): 13 | ret = super().prepare(profile) 14 | if not ret['ok']: 15 | return ret 16 | else: 17 | ret['ok'] = False # Set back to false, so we can do our own checks here. 18 | 19 | cmd = ['borg', 'rename', '--info', '--log-json'] 20 | if borg_compat.check('V2'): 21 | cmd.extend(["-r", profile.repo.url, old_archive_name, new_archive_name]) 22 | else: 23 | cmd.extend([f'{profile.repo.url}::{old_archive_name}', new_archive_name]) 24 | 25 | ret['old_archive_name'] = old_archive_name 26 | ret['new_archive_name'] = new_archive_name 27 | ret['repo_url'] = profile.repo.url 28 | ret['ok'] = True 29 | ret['cmd'] = cmd 30 | 31 | return ret 32 | 33 | def process_result(self, result): 34 | if result['returncode'] == 0: 35 | repo = RepoModel.get(url=result['params']['repo_url']) 36 | renamed_archive = ArchiveModel.get(name=result['params']['old_archive_name'], repo=repo) 37 | renamed_archive.name = result['params']['new_archive_name'] 38 | renamed_archive.save() 39 | -------------------------------------------------------------------------------- /src/vorta/borg/umount.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import psutil 4 | 5 | from ..i18n import trans_late 6 | from .borg_job import BorgJob 7 | 8 | 9 | class BorgUmountJob(BorgJob): 10 | def started_event(self): 11 | self.updated.emit(self.tr('Unmounting archive…')) 12 | 13 | @classmethod 14 | def prepare(cls, profile, mount_point, archive_name=None): 15 | ret = super().prepare(profile) 16 | if not ret['ok']: 17 | return ret 18 | else: 19 | ret['ok'] = False # Set back to false, so we can do our own checks here. 20 | 21 | archive_mount_points = [] 22 | partitions = psutil.disk_partitions(all=True) 23 | for p in partitions: 24 | if p.device == 'borgfs': 25 | archive_mount_points.append(os.path.normpath(p.mountpoint)) 26 | ret['active_mount_points'] = archive_mount_points 27 | 28 | if len(archive_mount_points) == 0: 29 | ret['message'] = trans_late('messages', 'No active Borg mounts found.') 30 | return ret 31 | if os.path.normpath(mount_point) not in archive_mount_points: 32 | ret['message'] = trans_late('messages', 'Mount point not active.') 33 | return ret 34 | 35 | if archive_name: 36 | ret['current_archive'] = archive_name 37 | ret['mount_point'] = mount_point 38 | 39 | cmd = ['borg', 'umount', '--log-json', mount_point] 40 | 41 | ret['ok'] = True 42 | ret['cmd'] = cmd 43 | 44 | return ret 45 | -------------------------------------------------------------------------------- /src/vorta/borg/version.py: -------------------------------------------------------------------------------- 1 | from vorta.i18n import trans_late 2 | 3 | from .borg_job import BorgJob 4 | 5 | 6 | class BorgVersionJob(BorgJob): 7 | """ 8 | Gets the path of the borg binary to be used and the borg version. 9 | 10 | Used to display under 'Misc' and later for version-specific compatibility. 11 | """ 12 | 13 | def finished_event(self, result): 14 | self.result.emit(result) 15 | 16 | @classmethod 17 | def prepare(cls): 18 | ret = {'ok': False} 19 | 20 | if cls.prepare_bin() is None: 21 | ret['message'] = trans_late('messages', 'Borg binary was not found.') 22 | return ret 23 | 24 | ret['cmd'] = ['borg', '--version'] 25 | ret['ok'] = True 26 | return ret 27 | 28 | def process_result(self, result): 29 | if result['returncode'] == 0: 30 | version = result['data'].strip().split(' ')[1] 31 | path = self.prepare_bin() 32 | result['data'] = {'version': version, 'path': path} 33 | -------------------------------------------------------------------------------- /src/vorta/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import platformdirs 4 | 5 | APP_NAME = 'Vorta' 6 | APP_AUTHOR = 'BorgBase' 7 | APP_ID_DARWIN = 'com.borgbase.client.macos' 8 | SETTINGS_DIR = None 9 | LOG_DIR = None 10 | CACHE_DIR = None 11 | TEMP_DIR = None 12 | PROFILE_BOOTSTRAP_FILE = None 13 | 14 | 15 | def default_dev_dir() -> Path: 16 | """Returns a default dir for config files in the project's main folder""" 17 | return Path(__file__).parent.parent.parent / '.dev_config' 18 | 19 | 20 | def init_from_platformdirs(): 21 | """Initializes config dirs for system-wide use""" 22 | dirs = platformdirs.PlatformDirs(APP_NAME, APP_AUTHOR) 23 | init(dirs.user_data_path, dirs.user_log_path, dirs.user_cache_path, dirs.user_cache_path / 'tmp', Path.home()) 24 | 25 | 26 | def init_dev_mode(dir: Path): 27 | """Initializes config dirs for local use inside provided dir""" 28 | dir_full_path = Path(dir).resolve() 29 | init( 30 | dir_full_path / 'settings', 31 | dir_full_path / 'logs', 32 | dir_full_path / 'cache', 33 | dir_full_path / 'tmp', 34 | dir_full_path, 35 | ) 36 | 37 | 38 | def init(settings: Path, logs: Path, cache: Path, tmp: Path, bootstrap: Path): 39 | """Initializes config directories with provided paths""" 40 | global SETTINGS_DIR 41 | global LOG_DIR 42 | global CACHE_DIR 43 | global TEMP_DIR 44 | global PROFILE_BOOTSTRAP_FILE 45 | SETTINGS_DIR = settings 46 | LOG_DIR = logs 47 | CACHE_DIR = cache 48 | TEMP_DIR = tmp 49 | PROFILE_BOOTSTRAP_FILE = bootstrap / '.vorta-init.json' 50 | ensure_dirs() 51 | 52 | 53 | def ensure_dirs(): 54 | """Creates config dirs and parent dirs if they don't exist""" 55 | # ensure directories exist 56 | for dir in (SETTINGS_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR): 57 | dir.mkdir(parents=True, exist_ok=True) 58 | 59 | 60 | # Make sure that the config values are valid 61 | init_from_platformdirs() 62 | -------------------------------------------------------------------------------- /src/vorta/i18n/qm/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/.gitkeep -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.ar.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.ar.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.cs.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.cs.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.de.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.de.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.en.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.en.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.en_US.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.en_US.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.es.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.es.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.fi.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.fi.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.fr.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.fr.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.gl.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.gl.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.it.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.it.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.nl.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.nl.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.ru.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.ru.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.sk.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.sk.qm -------------------------------------------------------------------------------- /src/vorta/i18n/qm/vorta.sv.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/qm/vorta.sv.qm -------------------------------------------------------------------------------- /src/vorta/i18n/ts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/i18n/ts/.gitkeep -------------------------------------------------------------------------------- /src/vorta/keyring/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/keyring/__init__.py -------------------------------------------------------------------------------- /src/vorta/keyring/abc.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | 4 | from vorta.i18n import trans_late 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class VortaKeyring: 10 | all_keyrings = [ 11 | ('.db', 'VortaDBKeyring'), 12 | ('.darwin', 'VortaDarwinKeyring'), 13 | ('.kwallet', 'VortaKWallet5Keyring'), 14 | ('.secretstorage', 'VortaSecretStorageKeyring'), 15 | ] 16 | 17 | @classmethod 18 | def get_keyring(cls): 19 | """ 20 | Choose available Keyring. First assign a score and then try to initialize it. 21 | """ 22 | available_keyrings = [] 23 | for _module, _class in cls.all_keyrings: 24 | try: 25 | keyring = getattr(importlib.import_module(_module, package='vorta.keyring'), _class) 26 | available_keyrings.append((keyring, keyring.get_priority())) 27 | except Exception as e: 28 | logger.debug(e) 29 | continue 30 | 31 | for keyring, _ in sorted(available_keyrings, key=lambda k: k[1], reverse=True): 32 | try: 33 | instance = keyring() 34 | logger.debug(f"Using {keyring.__name__}") 35 | return instance 36 | except Exception as e: 37 | logger.debug(e) 38 | continue 39 | 40 | def get_backend_warning(self): 41 | if self.is_system: 42 | return trans_late('utils', 'Storing password in your password manager.') 43 | else: 44 | return trans_late('utils', 'Saving password with Vorta settings.') 45 | 46 | def set_password(self, service, repo_url, password): 47 | """ 48 | Writes a password to the underlying store. 49 | """ 50 | raise NotImplementedError 51 | 52 | def get_password(self, service, repo_url): 53 | """ 54 | Retrieve a password from the underlying store. Return None if not found. 55 | """ 56 | raise NotImplementedError 57 | 58 | @property 59 | def is_system(self): 60 | """ 61 | Return True if the current subclass is the system's primary keychain mechanism, 62 | rather than a fallback (like our own VortaDBKeyring). 63 | """ 64 | raise NotImplementedError 65 | 66 | @classmethod 67 | def get_priority(cls): 68 | """ 69 | Return priority of this keyring on current system. Higher is more important. 70 | 71 | Shout-out to https://github.com/jaraco/keyring for this idea. 72 | """ 73 | raise NotImplementedError 74 | 75 | @property 76 | def is_unlocked(self): 77 | """ 78 | Try to unlock the keyring. 79 | Returns True if the keyring is open. Return False if it is closed or locked 80 | """ 81 | raise NotImplementedError 82 | -------------------------------------------------------------------------------- /src/vorta/keyring/db.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import peewee 4 | 5 | from vorta.store.models import SettingsModel 6 | 7 | from .abc import VortaKeyring 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class VortaDBKeyring(VortaKeyring): 13 | """ 14 | Our own fallback keyring service. Uses the main database 15 | to store repo passwords if no other (more secure) backend 16 | is available. 17 | """ 18 | 19 | def set_password(self, service, repo_url, password): 20 | from vorta.store.models import RepoPassword 21 | 22 | keyring_entry, created = RepoPassword.get_or_create(url=repo_url, defaults={'password': password}) 23 | keyring_entry.password = password 24 | keyring_entry.save() 25 | 26 | logger.debug(f"Saved password for repo {repo_url}") 27 | 28 | def get_password(self, service, repo_url): 29 | from vorta.store.models import RepoPassword 30 | 31 | try: 32 | keyring_entry = RepoPassword.get(url=repo_url) 33 | password = keyring_entry.password 34 | logger.debug(f"Retrieved password for repo {repo_url}") 35 | return password 36 | except peewee.DoesNotExist: 37 | return None 38 | 39 | @property 40 | def is_system(self): 41 | return False 42 | 43 | @property 44 | def is_unlocked(self): 45 | return True 46 | 47 | @classmethod 48 | def get_priority(cls): 49 | return 1 if SettingsModel.get(key='use_system_keyring').value else 10 50 | -------------------------------------------------------------------------------- /src/vorta/keyring/kwallet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from PyQt6 import QtDBus 5 | from PyQt6.QtCore import QMetaType, QVariant 6 | 7 | from vorta.keyring.abc import VortaKeyring 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class VortaKWallet5Keyring(VortaKeyring): 13 | """A wrapper for the qtdbus package to support the custom keyring backend""" 14 | 15 | folder_name = 'Vorta' 16 | service_name = "org.kde.kwalletd5" 17 | object_path = "/modules/kwalletd5" 18 | interface_name = 'org.kde.KWallet' 19 | 20 | def __init__(self): 21 | """ 22 | Test whether DBus and KDEWallet are available. 23 | """ 24 | self.iface = QtDBus.QDBusInterface( 25 | self.service_name, 26 | self.object_path, 27 | self.interface_name, 28 | QtDBus.QDBusConnection.sessionBus(), 29 | ) 30 | self.handle = -1 31 | if not (self.iface.isValid() and self.get_result("isEnabled") is True): 32 | raise KWalletNotAvailableException 33 | 34 | def set_password(self, service, repo_url, password): 35 | self.get_result( 36 | "writePassword", 37 | args=[self.handle, self.folder_name, repo_url, password, service], 38 | ) 39 | logger.debug(f"Saved password for repo {repo_url}") 40 | 41 | def get_password(self, service, repo_url): 42 | if not ( 43 | self.is_unlocked and self.get_result("hasEntry", args=[self.handle, self.folder_name, repo_url, service]) 44 | ): 45 | return None 46 | password = self.get_result("readPassword", args=[self.handle, self.folder_name, repo_url, service]) 47 | logger.debug(f"Retrieved password for repo {repo_url}") 48 | return password 49 | 50 | def get_result(self, method, args=[]): 51 | if args: 52 | result = self.iface.callWithArgumentList(QtDBus.QDBus.CallMode.AutoDetect, method, args) 53 | else: 54 | result = self.iface.call(QtDBus.QDBus.CallMode.AutoDetect, method) 55 | return result.arguments()[0] 56 | 57 | @property 58 | def is_unlocked(self): 59 | self.try_unlock() 60 | return self.handle > 0 61 | 62 | def try_unlock(self): 63 | wallet_name = self.get_result("networkWallet") 64 | wId = QVariant(0) 65 | wId.convert(QMetaType(QMetaType.Type.LongLong.value)) 66 | output = self.get_result("open", args=[wallet_name, wId, 'vorta-repo']) 67 | try: 68 | self.handle = int(output) 69 | except ValueError: # For when kwallet is disabled or dbus otherwise broken 70 | self.handle = -2 71 | 72 | @classmethod 73 | def get_priority(cls): 74 | return 6 if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "") else 4 75 | 76 | @property 77 | def is_system(self): 78 | return True 79 | 80 | 81 | class KWalletNotAvailableException(Exception): 82 | pass 83 | -------------------------------------------------------------------------------- /src/vorta/keyring/secretstorage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | import secretstorage 6 | 7 | from vorta.keyring.abc import VortaKeyring 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | LABEL_TEMPLATE = "Vorta Backup Repo {repo_url}" 12 | 13 | 14 | class VortaSecretStorageKeyring(VortaKeyring): 15 | """A wrapper for the secretstorage package to support the custom keyring backend""" 16 | 17 | def __init__(self): 18 | """ 19 | Test whether DBus and a SecretStorage provider are available. 20 | """ 21 | try: 22 | self.connection = secretstorage.dbus_init() 23 | except secretstorage.exceptions.SecretServiceNotAvailableException as e: 24 | logger.debug("SecretStorage provider or DBus daemon is not available.") 25 | raise e 26 | asyncio.set_event_loop(asyncio.new_event_loop()) 27 | self.collection = secretstorage.get_default_collection(self.connection) 28 | 29 | def set_password(self, service, repo_url, password): 30 | """ 31 | Writes a password to the underlying store. 32 | """ 33 | if self.is_unlocked: 34 | asyncio.set_event_loop(asyncio.new_event_loop()) 35 | attributes = { 36 | 'application': 'Vorta', 37 | 'service': service, 38 | 'repo_url': repo_url, 39 | 'xdg:schema': 'org.freedesktop.Secret.Generic', 40 | } 41 | self.collection.create_item( 42 | LABEL_TEMPLATE.format(repo_url=repo_url), 43 | attributes, 44 | password, 45 | replace=True, 46 | ) 47 | logger.debug(f"Saved password for repo {repo_url}") 48 | 49 | def get_password(self, service, repo_url): 50 | """ 51 | Retrieve a password from the underlying store. Return None if not found. 52 | """ 53 | if self.is_unlocked: 54 | asyncio.set_event_loop(asyncio.new_event_loop()) 55 | attributes = { 56 | 'application': 'Vorta', 57 | 'service': service, 58 | 'repo_url': repo_url, 59 | } 60 | items = list(self.collection.search_items(attributes)) 61 | logger.debug('Found %i passwords matching repo URL.', len(items)) 62 | if len(items) > 0: 63 | item = items[0] 64 | if item.is_locked() and item.unlock(): 65 | return None 66 | logger.debug(f"Retrieved password for repo {repo_url}") 67 | return item.get_secret().decode("utf-8") 68 | return None 69 | 70 | @property 71 | def is_unlocked(self): 72 | # unlock() will return True if the unlock prompt is dismissed 73 | return not (self.collection.is_locked() and self.collection.unlock()) 74 | 75 | @classmethod 76 | def get_priority(cls): 77 | return 6 if "GNOME" in os.getenv("XDG_CURRENT_DESKTOP", "") else 5 78 | 79 | @property 80 | def is_system(self): 81 | return True 82 | -------------------------------------------------------------------------------- /src/vorta/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set up logging to user log dir. Uses the platform's default location: 3 | 4 | - linux: $HOME/.cache/Vorta/log 5 | - macOS: $HOME/Library/Logs/Vorta 6 | 7 | """ 8 | 9 | import logging 10 | from logging.handlers import TimedRotatingFileHandler 11 | 12 | from vorta import config 13 | 14 | logger = logging.getLogger() 15 | 16 | 17 | def init_logger(background=False): 18 | logger.setLevel(logging.DEBUG) 19 | logging.getLogger('peewee').setLevel(logging.INFO) 20 | logging.getLogger('PyQt6').setLevel(logging.INFO) 21 | 22 | # create logging format 23 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 24 | 25 | # create handlers 26 | fh = TimedRotatingFileHandler(config.LOG_DIR / 'vorta.log', when='d', interval=1, backupCount=5) 27 | # ensure ".log" suffix 28 | fh.namer = lambda log_name: log_name.replace(".log", "") + ".log" 29 | fh.setLevel(logging.DEBUG) 30 | fh.setFormatter(formatter) 31 | logger.addHandler(fh) 32 | 33 | if background: 34 | pass 35 | else: # log to console, when running in foreground 36 | ch = logging.StreamHandler() 37 | ch.setLevel(logging.DEBUG) 38 | ch.setFormatter(formatter) 39 | logger.addHandler(ch) 40 | -------------------------------------------------------------------------------- /src/vorta/network_status/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/network_status/__init__.py -------------------------------------------------------------------------------- /src/vorta/network_status/abc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime 3 | from typing import List, NamedTuple, Optional 4 | 5 | 6 | class NetworkStatusMonitor: 7 | @classmethod 8 | def get_network_status_monitor(cls) -> 'NetworkStatusMonitor': 9 | if sys.platform == 'darwin': 10 | from .darwin import DarwinNetworkStatus 11 | 12 | return DarwinNetworkStatus() 13 | else: 14 | from .network_manager import ( 15 | DBusException, 16 | NetworkManagerMonitor, 17 | UnsupportedException, 18 | ) 19 | 20 | try: 21 | return NetworkManagerMonitor() 22 | except (UnsupportedException, DBusException): 23 | return NullNetworkStatusMonitor() 24 | 25 | def is_network_status_available(self): 26 | """Is the network status really available, and not just a dummy implementation?""" 27 | return type(self) is not NetworkStatusMonitor 28 | 29 | def is_network_metered(self) -> bool: 30 | """Is the currently connected network a metered connection?""" 31 | raise NotImplementedError() 32 | 33 | def get_current_wifi(self) -> Optional[str]: 34 | """Get current SSID or None if Wifi is off.""" 35 | raise NotImplementedError() 36 | 37 | def get_known_wifis(self) -> List['SystemWifiInfo']: 38 | """Get WiFi networks known to system.""" 39 | raise NotImplementedError() 40 | 41 | 42 | class SystemWifiInfo(NamedTuple): 43 | ssid: str 44 | last_connected: Optional[datetime] 45 | 46 | 47 | class NullNetworkStatusMonitor(NetworkStatusMonitor): 48 | """Dummy implementation, in case we don't have one for current platform.""" 49 | 50 | def is_network_status_available(self): 51 | return False 52 | 53 | def is_network_metered(self) -> bool: 54 | return False 55 | 56 | def get_current_wifi(self) -> Optional[str]: 57 | pass 58 | 59 | def get_known_wifis(self) -> List['SystemWifiInfo']: 60 | return [] 61 | -------------------------------------------------------------------------------- /src/vorta/store/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/store/__init__.py -------------------------------------------------------------------------------- /src/vorta/updater.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from vorta.store.models import SettingsModel 5 | 6 | 7 | def get_updater(): 8 | if sys.platform == 'darwin' and getattr(sys, 'frozen', False): 9 | """ 10 | Use Sparkle framework on macOS. 11 | 12 | Settings: https://sparkle-project.org/documentation/customization/ 13 | Examples: https://programtalk.com/python-examples/objc.loadBundle/ 14 | 15 | To debug: 16 | $ defaults read com.borgbase.client.macos 17 | """ 18 | 19 | import Cocoa 20 | import objc 21 | 22 | bundle_path = os.path.join( 23 | os.path.dirname(sys.executable), 24 | os.pardir, 25 | 'Frameworks', 26 | 'Sparkle.framework', 27 | ) 28 | objc.loadBundle('Sparkle', globals(), bundle_path) 29 | sparkle = SUUpdater.sharedUpdater() # noqa: F821 30 | 31 | # A default Appcast URL is set in vorta.spec, when setting it here it's saved to defaults, 32 | # so we need both cases. 33 | if SettingsModel.get(key='updates_include_beta').value: 34 | appcast_nsurl = Cocoa.NSURL.URLWithString_('https://borgbase.github.io/vorta/appcast-pre.xml') 35 | else: 36 | appcast_nsurl = Cocoa.NSURL.URLWithString_('https://borgbase.github.io/vorta/appcast.xml') 37 | 38 | sparkle.setFeedURL_(appcast_nsurl) 39 | 40 | if SettingsModel.get(key='check_for_updates').value: 41 | sparkle.setAutomaticallyChecksForUpdates_(True) 42 | sparkle.checkForUpdatesInBackground() 43 | 44 | sparkle.setAutomaticallyDownloadsUpdates_(False) 45 | return sparkle 46 | 47 | else: # TODO: implement for Linux 48 | return None 49 | -------------------------------------------------------------------------------- /src/vorta/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/views/__init__.py -------------------------------------------------------------------------------- /src/vorta/views/about_tab.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | from PyQt6 import QtCore, uic 5 | 6 | from vorta import config 7 | from vorta._version import __version__ 8 | from vorta.store.models import BackupProfileMixin 9 | from vorta.utils import get_asset 10 | from vorta.views.utils import get_colored_icon 11 | 12 | uifile = get_asset('UI/about_tab.ui') 13 | AboutTabUI, AboutTabBase = uic.loadUiType(uifile) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class AboutTab(AboutTabBase, AboutTabUI, BackupProfileMixin): 19 | refresh_archive = QtCore.pyqtSignal() 20 | 21 | def __init__(self, parent=None): 22 | """Init.""" 23 | super().__init__(parent) 24 | self.setupUi(parent) 25 | self.versionLabel.setText(__version__) 26 | self.logLink.setText( 27 | f'Click here to view the logs.' 29 | ) 30 | self.gpl_logo.setPixmap(get_colored_icon('gpl_logo', scaled_height=40, return_qpixmap=True)) 31 | self.python_logo.setPixmap(get_colored_icon('python_logo', scaled_height=40, return_qpixmap=True)) 32 | copyright_text = self.copyrightLabel.text() 33 | copyright_text = copyright_text.replace('2020', str(datetime.now().year)) 34 | self.copyrightLabel.setText(copyright_text) 35 | 36 | def set_borg_details(self, version, path): 37 | self.borgVersion.setText(version) 38 | self.borgPath.setText(f"
Path to Borg: {path}
") 39 | -------------------------------------------------------------------------------- /src/vorta/views/exception_dialog.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import platform 3 | 4 | from PyQt6 import uic 5 | from PyQt6.QtWidgets import QApplication 6 | 7 | from vorta._version import __version__ 8 | from vorta.utils import borg_compat 9 | from vorta.views.utils import get_colored_icon 10 | 11 | from ..utils import get_asset 12 | 13 | # Load UI file 14 | uifile = get_asset('UI/exception_dialog.ui') 15 | ExceptionDialogUI, ExceptionDialogBase = uic.loadUiType(uifile) 16 | 17 | 18 | class ExceptionDetails: 19 | @staticmethod 20 | def get_os_details(): 21 | uname_result = platform.uname() 22 | os_details = f"OS: {uname_result.system}\n" 23 | os_details += f"Release: {uname_result.release}\n" 24 | os_details += f"Version: {uname_result.version}" 25 | return os_details 26 | 27 | @staticmethod 28 | def get_exception_details(exception): 29 | details = ExceptionDetails.get_os_details() 30 | details += "\nDate and Time: " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 31 | details += "\nBorg Version: " + borg_compat.version 32 | details += "\nVorta Version: " + __version__ 33 | details += "\n" + exception 34 | return details 35 | 36 | 37 | class ExceptionDialog(ExceptionDialogBase, ExceptionDialogUI): 38 | def __init__(self, exception: str): 39 | super().__init__() 40 | self.setupUi(self) 41 | 42 | self.report_to_github_label.setOpenExternalLinks(True) 43 | self.ignoreButton.clicked.connect(self.close) 44 | self.copyButton.clicked.connect(self.copy_report_to_clipboard) 45 | 46 | self.copyButton.setIcon(get_colored_icon('copy')) 47 | 48 | # Set crash details 49 | details = ExceptionDetails.get_exception_details(exception) 50 | self.crashDetails.setPlainText(details) 51 | 52 | # Set alert image 53 | self.alertImage.setPixmap(get_colored_icon('exclamation-triangle', scaled_height=75, return_qpixmap=True)) 54 | 55 | def copy_report_to_clipboard(self): 56 | cb = QApplication.clipboard() 57 | cb.setText(self.crashDetails.toPlainText(), mode=cb.Mode.Clipboard) 58 | -------------------------------------------------------------------------------- /src/vorta/views/export_window.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from pathlib import Path 4 | 5 | from PyQt6 import uic 6 | from PyQt6.QtCore import Qt 7 | from PyQt6.QtWidgets import QFileDialog, QMessageBox 8 | 9 | from vorta.keyring.abc import VortaKeyring 10 | from vorta.store.models import BackupProfileModel # noqa: F401 11 | from vorta.utils import get_asset 12 | 13 | from ..notifications import VortaNotifications 14 | from ..profile_export import ProfileExport 15 | 16 | uifile_import = get_asset('UI/export_window.ui') 17 | ExportWindowUI, ExportWindowBase = uic.loadUiType(uifile_import) 18 | uifile_export = get_asset('UI/import_window.ui') 19 | ImportWindowUI, ImportWindowBase = uic.loadUiType(uifile_export) 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class ExportWindow(ExportWindowBase, ExportWindowUI): 24 | def __init__(self, profile): 25 | """ 26 | @type profile: BackupProfileModel 27 | """ 28 | super().__init__() 29 | self.profile = profile 30 | self.setupUi(self) 31 | self.setWindowTitle(self.tr("Export Profile")) 32 | self.buttonBox.accepted.connect(self.run) 33 | self.buttonBox.rejected.connect(self.reject) 34 | 35 | self.keyring = VortaKeyring.get_keyring() 36 | profile = self.profile 37 | if profile.repo is None or self.keyring.get_password('vorta-repo', profile.repo.url) is None: 38 | self.storePassword.setCheckState(Qt.CheckState(False)) 39 | self.storePassword.setDisabled(True) 40 | self.storePassword.setToolTip(self.tr('Disclose your borg passphrase (No passphrase set)')) 41 | 42 | def get_file(self): 43 | """Get targeted save file with custom extension""" 44 | default_file = os.path.join(Path.home(), '{}.json'.format(self.profile.name)) 45 | file_name = QFileDialog.getSaveFileName(self, self.tr("Save profile_export"), default_file, "JSON (*.json)")[0] 46 | if file_name: 47 | if not file_name.endswith('.json'): 48 | file_name += '.json' 49 | return file_name 50 | 51 | def on_error(self, error, message): 52 | logger.error(error) 53 | QMessageBox.critical(None, self.tr("Error while exporting"), message) 54 | self.close() 55 | 56 | def run(self): 57 | """Attempt to write profile_export export to file""" 58 | filename = self.get_file() 59 | if not filename: 60 | return False 61 | profile = self.profile 62 | json_string = ProfileExport.from_db(profile, self.storePassword.isChecked()).to_json() 63 | try: 64 | with open(filename, 'w') as file: 65 | file.write(json_string) 66 | except (PermissionError, OSError) as e: 67 | self.on_error( 68 | e, 69 | self.tr('The file {} could not be created. Please choose another location.').format(filename), 70 | ) 71 | return False 72 | else: 73 | notifier = VortaNotifications.pick() 74 | notifier.deliver( 75 | self.tr('Profile export successful!'), 76 | self.tr('Profile export written to {}.').format(filename), 77 | level='info', 78 | ) 79 | self.close() 80 | -------------------------------------------------------------------------------- /src/vorta/views/log_page.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import uic 2 | from PyQt6.QtWidgets import ( 3 | QAbstractItemView, 4 | QApplication, 5 | QHeaderView, 6 | QTableWidgetItem, 7 | ) 8 | 9 | from vorta import config 10 | from vorta.store.models import BackupProfileMixin, EventLogModel 11 | from vorta.utils import get_asset 12 | 13 | uifile = get_asset('UI/log_page.ui') 14 | LogTableUI, LogTableBase = uic.loadUiType(uifile) 15 | 16 | 17 | class LogTableColumn: 18 | Time = 0 19 | Category = 1 20 | Subcommand = 2 21 | Repository = 3 22 | ReturnCode = 4 23 | 24 | 25 | class LogPage(LogTableBase, LogTableUI, BackupProfileMixin): 26 | def __init__(self, parent=None): 27 | super().__init__(parent) 28 | self.setupUi(self) 29 | self.init_ui() 30 | QApplication.instance().backup_finished_event.connect(self.populate_logs) 31 | QApplication.instance().profile_changed_event.connect(self.populate_logs) 32 | 33 | def init_ui(self): 34 | self.logPage.setAlternatingRowColors(True) 35 | header = self.logPage.horizontalHeader() 36 | header.setVisible(True) 37 | [header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) for i in range(5)] 38 | header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) 39 | self.logPage.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 40 | self.logPage.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) 41 | 42 | self.logLink.setText( 43 | f'Click here for complete logs.' 45 | ) 46 | 47 | self.populate_logs() 48 | 49 | def populate_logs(self): 50 | profile = self.profile() 51 | event_logs = [ 52 | s 53 | for s in EventLogModel.select() 54 | .where(EventLogModel.profile == profile.id) 55 | .order_by(EventLogModel.start_time.desc()) 56 | ] 57 | 58 | sorting = self.logPage.isSortingEnabled() 59 | self.logPage.setSortingEnabled(False) 60 | self.logPage.setRowCount(len(event_logs)) 61 | for row, log_line in enumerate(event_logs): 62 | formatted_time = log_line.start_time.strftime('%Y-%m-%d %H:%M') 63 | self.logPage.setItem(row, LogTableColumn.Time, QTableWidgetItem(formatted_time)) 64 | self.logPage.setItem(row, LogTableColumn.Category, QTableWidgetItem(log_line.category)) 65 | self.logPage.setItem(row, LogTableColumn.Subcommand, QTableWidgetItem(log_line.subcommand)) 66 | self.logPage.setItem(row, LogTableColumn.Repository, QTableWidgetItem(log_line.repo_url)) 67 | self.logPage.setItem(row, LogTableColumn.ReturnCode, QTableWidgetItem(str(log_line.returncode))) 68 | self.logPage.setSortingEnabled(sorting) 69 | -------------------------------------------------------------------------------- /src/vorta/views/networks_page.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import uic 2 | from PyQt6.QtCore import Qt 3 | from PyQt6.QtWidgets import QApplication, QCheckBox, QLabel, QListWidget, QListWidgetItem 4 | 5 | from vorta.store.models import BackupProfileMixin, WifiSettingModel 6 | from vorta.utils import get_asset, get_sorted_wifis 7 | 8 | uifile = get_asset('UI/networks_page.ui') 9 | NetworksUI, NetworksBase = uic.loadUiType(uifile) 10 | 11 | 12 | class NetworksPage(NetworksBase, NetworksUI, BackupProfileMixin): 13 | def __init__(self, parent=None): 14 | super().__init__(parent) 15 | self.setupUi(self) 16 | 17 | self.wifiListLabel: QLabel = self.findChild(QLabel, 'wifiListLabel') 18 | self.meteredNetworksCheckBox: QCheckBox = self.findChild(QCheckBox, 'meteredNetworksCheckBox') 19 | self.wifiListWidget: QListWidget = self.findChild(QListWidget, 'wifiListWidget') 20 | 21 | # Connect signals 22 | self.meteredNetworksCheckBox.stateChanged.connect(self.on_metered_networks_state_changed) 23 | self.wifiListWidget.itemChanged.connect(self.save_wifi_item) 24 | QApplication.instance().profile_changed_event.connect(self.populate_wifi) 25 | 26 | self.populate_wifi() 27 | 28 | def on_metered_networks_state_changed(self, state): 29 | profile = self.profile() 30 | attr = 'dont_run_on_metered_networks' 31 | new_value = state != Qt.CheckState.Checked 32 | self.save_profile_attr(attr, new_value) 33 | self.meteredNetworksCheckBox.setChecked(False if profile.dont_run_on_metered_networks else True) 34 | 35 | def populate_wifi(self): 36 | self.wifiListWidget.clear() 37 | profile = self.profile() 38 | if profile: 39 | for wifi in get_sorted_wifis(profile): 40 | item = QListWidgetItem() 41 | item.setText(wifi.ssid) 42 | item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) 43 | if wifi.allowed: 44 | item.setCheckState(Qt.CheckState.Checked) 45 | else: 46 | item.setCheckState(Qt.CheckState.Unchecked) 47 | self.wifiListWidget.addItem(item) 48 | 49 | def save_wifi_item(self, item): 50 | profile = self.profile() 51 | if profile: 52 | db_item = WifiSettingModel.get(ssid=item.text(), profile=profile.id) 53 | db_item.allowed = item.checkState() == Qt.CheckState.Checked 54 | db_item.save() 55 | 56 | def save_profile_attr(self, attr, new_value): 57 | profile = self.profile() 58 | if profile: 59 | setattr(profile, attr, new_value) 60 | profile.save() 61 | -------------------------------------------------------------------------------- /src/vorta/views/partials/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/src/vorta/views/partials/__init__.py -------------------------------------------------------------------------------- /src/vorta/views/partials/loading_button.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adapted from https://stackoverflow.com/questions/53618971/how-to-make-a-qpushbutton-a-loading-button 3 | """ 4 | 5 | from PyQt6 import QtCore, QtGui, QtWidgets 6 | 7 | 8 | class LoadingButton(QtWidgets.QPushButton): 9 | @QtCore.pyqtSlot() 10 | def start(self): 11 | if hasattr(self, "_movie"): 12 | self._movie.start() 13 | 14 | @QtCore.pyqtSlot() 15 | def stop(self): 16 | if hasattr(self, "_movie"): 17 | self._movie.stop() 18 | self.setIcon(QtGui.QIcon()) 19 | 20 | def setGif(self, filename): 21 | if not hasattr(self, "_movie"): 22 | self._movie = QtGui.QMovie(self) 23 | self._movie.setFileName(filename) 24 | self._movie.frameChanged.connect(self.on_frameChanged) 25 | if self._movie.loopCount() != -1: 26 | self._movie.finished.connect(self.start) 27 | self.stop() 28 | 29 | @QtCore.pyqtSlot(int) 30 | def on_frameChanged(self, frameNumber): 31 | self.setIcon(QtGui.QIcon(self._movie.currentPixmap())) 32 | -------------------------------------------------------------------------------- /src/vorta/views/profile_add_edit_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtCore, uic 2 | from PyQt6.QtWidgets import QDialogButtonBox 3 | 4 | from vorta.i18n import trans_late, translate 5 | from vorta.store.models import BackupProfileModel 6 | from vorta.utils import get_asset 7 | 8 | uifile = get_asset('UI/profile_add.ui') 9 | AddProfileUI, AddProfileBase = uic.loadUiType(uifile) 10 | 11 | 12 | class AddProfileWindow(AddProfileBase, AddProfileUI): 13 | profile_changed = QtCore.pyqtSignal(str, int) 14 | 15 | def __init__(self, parent=None): 16 | super().__init__(parent) 17 | self.setupUi(self) 18 | self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) 19 | self.edited_profile = None 20 | 21 | self.buttonBox.rejected.connect(self.close) 22 | self.buttonBox.accepted.connect(self.save) 23 | self.profileNameField.textChanged.connect(self.button_validation) 24 | 25 | self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setText(self.tr("Save")) 26 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(self.tr("Cancel")) 27 | 28 | self.name_blank = trans_late('AddProfileWindow', 'Please enter a profile name.') 29 | self.name_exists = trans_late('AddProfileWindow', 'A profile with this name already exists.') 30 | # Call validate to set initial messages 31 | self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setEnabled(self.validate()) 32 | 33 | def _set_status(self, text): 34 | self.errorText.setText(text) 35 | self.errorText.repaint() 36 | 37 | def save(self): 38 | new_profile = BackupProfileModel(name=self.profileNameField.text()) 39 | new_profile.save() 40 | self.profile_changed.emit(new_profile.name, new_profile.id) 41 | self.accept() 42 | 43 | def button_validation(self): 44 | self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setEnabled(self.validate()) 45 | 46 | def validate(self): 47 | name = self.profileNameField.text() 48 | # A name was entered? 49 | if len(name) == 0: 50 | self._set_status(translate('AddProfileWindow', self.name_blank)) 51 | return False 52 | 53 | # Profile with this name already exists? 54 | exists = BackupProfileModel.select().where(BackupProfileModel.name == name).count() 55 | if exists > 0: 56 | self._set_status(translate('AddProfileWindow', self.name_exists)) 57 | return False 58 | 59 | self._set_status('') 60 | return True 61 | 62 | 63 | class EditProfileWindow(AddProfileWindow): 64 | def __init__(self, parent=None, rename_existing_id=None): 65 | super().__init__(parent) 66 | existing_profile = BackupProfileModel.get(id=rename_existing_id) 67 | self.profileNameField.setText(existing_profile.name) 68 | self.existing_id = rename_existing_id 69 | self.modalTitle.setText(self.tr('Rename Profile')) 70 | 71 | def save(self): 72 | renamed_profile = BackupProfileModel.get(id=self.existing_id) 73 | renamed_profile.name = self.profileNameField.text() 74 | renamed_profile.save() 75 | self.profile_changed.emit(renamed_profile.name, renamed_profile.id) 76 | self.accept() 77 | -------------------------------------------------------------------------------- /src/vorta/views/schedule_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import uic 2 | from PyQt6.QtWidgets import QApplication 3 | 4 | from vorta import application 5 | from vorta.store.models import BackupProfileMixin 6 | from vorta.utils import get_asset 7 | from vorta.views.log_page import LogPage 8 | from vorta.views.networks_page import NetworksPage 9 | from vorta.views.schedule_page import SchedulePage 10 | from vorta.views.shell_commands_page import ShellCommandsPage 11 | from vorta.views.utils import get_colored_icon 12 | 13 | uifile = get_asset('UI/schedule_tab.ui') 14 | ScheduleUI, ScheduleBase = uic.loadUiType(uifile) 15 | 16 | 17 | class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin): 18 | def __init__(self, parent=None): 19 | super().__init__(parent) 20 | self.setupUi(parent) 21 | self.app: application.VortaApp = QApplication.instance() 22 | self.toolBox.setCurrentIndex(0) 23 | self.set_icons() 24 | self.init_log_page() 25 | self.init_shell_commands_page() 26 | self.init_networks_page() 27 | self.init_schedule_page() 28 | self.app.paletteChanged.connect(lambda p: self.set_icons()) 29 | 30 | self.app.backup_finished_event.connect(self.logPage.populate_logs) 31 | 32 | def init_log_page(self): 33 | self.logPage = LogPage(self) 34 | self.logLayout.addWidget(self.logPage) 35 | self.logPage.show() 36 | 37 | def init_shell_commands_page(self): 38 | self.shellCommandsPage = ShellCommandsPage(self) 39 | self.shellCommandsLayout.addWidget(self.shellCommandsPage) 40 | self.shellCommandsPage.show() 41 | 42 | def init_networks_page(self): 43 | self.networksPage = NetworksPage(self) 44 | self.networksLayout.addWidget(self.networksPage) 45 | self.networksPage.show() 46 | 47 | def init_schedule_page(self): 48 | self.schedulePage = SchedulePage(self) 49 | self.scheduleLayout.addWidget(self.schedulePage) 50 | self.schedulePage.show() 51 | 52 | def set_icons(self): 53 | self.toolBox.setItemIcon(0, get_colored_icon('clock-o')) 54 | self.toolBox.setItemIcon(1, get_colored_icon('wifi')) 55 | self.toolBox.setItemIcon(2, get_colored_icon('tasks')) 56 | self.toolBox.setItemIcon(3, get_colored_icon('terminal')) 57 | -------------------------------------------------------------------------------- /src/vorta/views/shell_commands_page.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import uic 2 | from PyQt6.QtWidgets import QApplication, QLineEdit, QWidget 3 | 4 | from vorta.store.models import BackupProfileMixin 5 | from vorta.utils import get_asset 6 | 7 | 8 | class ShellCommandsPage(QWidget, BackupProfileMixin): 9 | def __init__(self, parent=None): 10 | super().__init__(parent) 11 | uifile = get_asset('UI/shell_commands_page.ui') 12 | uic.loadUi(uifile, self) 13 | 14 | self.preBackupCmdLineEdit: QLineEdit = self.findChild(QLineEdit, 'preBackupCmdLineEdit') 15 | self.postBackupCmdLineEdit: QLineEdit = self.findChild(QLineEdit, 'postBackupCmdLineEdit') 16 | self.createCmdLineEdit: QLineEdit = self.findChild(QLineEdit, 'createCmdLineEdit') 17 | self.populate_from_profile() 18 | 19 | self.preBackupCmdLineEdit.textEdited.connect( 20 | lambda new_val, attr='pre_backup_cmd': self.save_profile_attr(attr, new_val) 21 | ) 22 | self.postBackupCmdLineEdit.textEdited.connect( 23 | lambda new_val, attr='post_backup_cmd': self.save_profile_attr(attr, new_val) 24 | ) 25 | self.createCmdLineEdit.textEdited.connect( 26 | lambda new_val, attr='create_backup_cmd': self.save_repo_attr(attr, new_val) 27 | ) 28 | QApplication.instance().profile_changed_event.connect(self.populate_from_profile) 29 | 30 | def populate_from_profile(self): 31 | profile = self.profile() 32 | if profile.repo: 33 | self.createCmdLineEdit.setText(profile.repo.create_backup_cmd) 34 | self.createCmdLineEdit.setEnabled(True) 35 | 36 | self.preBackupCmdLineEdit.setText(profile.pre_backup_cmd) 37 | self.preBackupCmdLineEdit.setEnabled(True) 38 | 39 | self.postBackupCmdLineEdit.setText(profile.post_backup_cmd) 40 | self.postBackupCmdLineEdit.setEnabled(True) 41 | else: 42 | self.createCmdLineEdit.setEnabled(False) 43 | self.preBackupCmdLineEdit.setEnabled(False) 44 | self.postBackupCmdLineEdit.setEnabled(False) 45 | 46 | def save_profile_attr(self, attr, new_value): 47 | profile = self.profile() 48 | setattr(profile, attr, new_value) 49 | profile.save() 50 | 51 | def save_repo_attr(self, attr, new_value): 52 | repo = self.profile().repo 53 | setattr(repo, attr, new_value) 54 | repo.save() 55 | -------------------------------------------------------------------------------- /src/vorta/views/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from PyQt6.QtGui import QIcon, QImage, QPixmap 6 | 7 | from vorta.utils import get_asset, uses_dark_mode 8 | 9 | 10 | def get_colored_icon(icon_name, scaled_height=128, return_qpixmap=False): 11 | """ 12 | Return SVG icon in the correct color. 13 | """ 14 | with open(get_asset(f"icons/{icon_name}.svg"), 'rb') as svg_file: 15 | svg_str = svg_file.read() 16 | if uses_dark_mode(): 17 | svg_str = svg_str.replace(b'#000000', b'#ffffff') 18 | svg_img = QImage.fromData(svg_str).scaledToHeight(scaled_height) 19 | 20 | if return_qpixmap: 21 | return QPixmap(svg_img) 22 | else: 23 | return QIcon(QPixmap(svg_img)) 24 | 25 | 26 | def get_exclusion_presets(): 27 | """ 28 | Loads exclusion presets from JSON files in assets/exclusion_presets. 29 | 30 | Currently the preset name is used as identifier. 31 | """ 32 | allPresets = {} 33 | os_tag = f"os:{sys.platform}" 34 | if getattr(sys, 'frozen', False): 35 | # we are running in a bundle 36 | bundle_dir = os.path.join(sys._MEIPASS, 'assets/exclusion_presets') 37 | else: 38 | # we are running in a normal Python environment 39 | bundle_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../assets/exclusion_presets') 40 | 41 | for preset_file in sorted(os.listdir(bundle_dir)): 42 | with open(os.path.join(bundle_dir, preset_file), 'r') as f: 43 | preset_list = json.load(f) 44 | for preset in preset_list: 45 | if os_tag in preset['tags']: 46 | allPresets[preset['slug']] = { 47 | 'name': preset['name'], 48 | 'patterns': preset['patterns'], 49 | 'tags': preset['tags'], 50 | } 51 | return allPresets 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import vorta._version 5 | 6 | resource_file = os.path.join(os.path.dirname(vorta._version.__file__), 'assets/icons') 7 | sys.path.append(resource_file) 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | from peewee import SqliteDatabase 6 | 7 | import vorta 8 | import vorta.application 9 | import vorta.borg.jobs_manager 10 | 11 | 12 | def pytest_configure(config): 13 | sys._called_from_test = True 14 | pytest._wait_defaults = {'timeout': 20000} 15 | os.environ['LANG'] = 'en' # Ensure we test an English UI 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def qapp(tmpdir_factory): 20 | # DB is required to init QApplication. New DB used for every test. 21 | tmp_db = tmpdir_factory.mktemp('Vorta').join('settings.sqlite') 22 | mock_db = SqliteDatabase(str(tmp_db)) 23 | vorta.store.connection.init_db(mock_db) 24 | 25 | # Needs to be disabled before calling VortaApp() 26 | if sys.platform == 'darwin': 27 | cfg = vorta.store.models.SettingsModel.get(key='check_full_disk_access') 28 | cfg.value = False 29 | cfg.save() 30 | 31 | from vorta.application import VortaApp 32 | 33 | qapp = VortaApp([]) # Only init QApplication once to avoid segfaults while testing. 34 | 35 | yield qapp 36 | mock_db.close() 37 | qapp.quit() 38 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_borg.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains tests that directly call borg commands and verify the exit code. 3 | """ 4 | 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | import vorta.borg 10 | import vorta.store.models 11 | from vorta.borg.info_archive import BorgInfoArchiveJob 12 | from vorta.borg.info_repo import BorgInfoRepoJob 13 | from vorta.borg.prune import BorgPruneJob 14 | 15 | 16 | def test_borg_prune(qapp, qtbot): 17 | """This test runs borg prune on a test repo directly without UI""" 18 | params = BorgPruneJob.prepare(vorta.store.models.BackupProfileModel.select().first()) 19 | thread = BorgPruneJob(params['cmd'], params, qapp) 20 | 21 | with qtbot.waitSignal(thread.result, **pytest._wait_defaults) as blocker: 22 | blocker.connect(thread.updated) 23 | thread.run() 24 | 25 | assert blocker.args[0]['returncode'] == 0 26 | 27 | 28 | def test_borg_repo_info(qapp, qtbot, tmpdir): 29 | """This test runs borg info on a test repo directly without UI""" 30 | repo_info = { 31 | 'repo_url': str(Path(tmpdir).parent / 'repo0'), 32 | 'repo_name': 'repo0', 33 | 'extra_borg_arguments': '', 34 | 'ssh_key': '', 35 | 'password': '', 36 | } 37 | 38 | params = BorgInfoRepoJob.prepare(repo_info) 39 | thread = BorgInfoRepoJob(params['cmd'], params, qapp) 40 | 41 | with qtbot.waitSignal(thread.result, **pytest._wait_defaults) as blocker: 42 | blocker.connect(thread.result) 43 | thread.run() 44 | 45 | assert blocker.args[0]['returncode'] == 0 46 | 47 | 48 | def test_borg_archive_info(qapp, qtbot, archive_env): 49 | """Check that archive info command works""" 50 | params = BorgInfoArchiveJob.prepare(vorta.store.models.BackupProfileModel.select().first(), "test-archive1") 51 | thread = BorgInfoArchiveJob(params['cmd'], params, qapp) 52 | 53 | with qtbot.waitSignal(thread.result, **pytest._wait_defaults) as blocker: 54 | blocker.connect(thread.result) 55 | thread.run() 56 | 57 | assert blocker.args[0]['returncode'] == 0 58 | -------------------------------------------------------------------------------- /tests/integration/test_repo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test backup creation 3 | """ 4 | 5 | import pytest 6 | from PyQt6 import QtCore 7 | 8 | from vorta.store.models import ArchiveModel, EventLogModel 9 | 10 | 11 | def test_create(qapp, qtbot, archive_env): 12 | """Test for manual archive creation""" 13 | main, tab = archive_env 14 | 15 | qtbot.mouseClick(main.createStartBtn, QtCore.Qt.MouseButton.LeftButton) 16 | qtbot.waitUntil(lambda: 'Backup finished.' in main.progressText.text(), **pytest._wait_defaults) 17 | qtbot.waitUntil(lambda: main.createStartBtn.isEnabled(), **pytest._wait_defaults) 18 | 19 | assert EventLogModel.select().count() == 2 20 | assert ArchiveModel.select().count() == 7 21 | assert main.createStartBtn.isEnabled() 22 | assert main.archiveTab.archiveTable.rowCount() == 7 23 | assert main.scheduleTab.logPage.logPage.rowCount() == 2 24 | -------------------------------------------------------------------------------- /tests/network_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/network_manager/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/borg_json_output/check_stderr.json: -------------------------------------------------------------------------------- 1 | {"name": "borg.repository", "message": "Remote: Starting repository check", "type": "log_message", "levelname": "INFO", "time": 1543501427.3991857} 2 | {"name": "borg.repository", "message": "Remote: Starting repository index check", "type": "log_message", "levelname": "INFO", "time": 1543501428.2007568} 3 | {"name": "borg.repository", "message": "Remote: Completed repository check, no problems found.", "type": "log_message", "levelname": "INFO", "time": 1543501428.201012} 4 | {"type": "log_message", "time": 1543501427.141906, "message": "Starting archive consistency check...", "levelname": "INFO", "name": "borg.archive"} 5 | {"type": "log_message", "time": 1543501429.413376, "message": "Analyzing archive nyx2.local-1-2018-11-26T15:33:25 (1/1)", "levelname": "INFO", "name": "borg.archive"} 6 | {"type": "log_message", "time": 1543501430.631774, "message": "Archive consistency check complete, no problems found.", "levelname": "INFO", "name": "borg.archive"} 7 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/check_stdout.json: -------------------------------------------------------------------------------- 1 | {"name": "borg.repository", "message": "Remote: Starting repository check", "type": "log_message", "levelname": "INFO", "time": 1543501427.3991857} 2 | {"name": "borg.repository", "message": "Remote: Starting repository index check", "type": "log_message", "levelname": "INFO", "time": 1543501428.2007568} 3 | {"name": "borg.repository", "message": "Remote: Completed repository check, no problems found.", "type": "log_message", "levelname": "INFO", "time": 1543501428.201012} 4 | {"type": "log_message", "time": 1543501427.141906, "message": "Starting archive consistency check...", "levelname": "INFO", "name": "borg.archive"} 5 | {"type": "log_message", "time": 1543501429.413376, "message": "Analyzing archive nyx2.local-1-2018-11-26T15:33:25 (1/1)", "levelname": "INFO", "name": "borg.archive"} 6 | {"type": "log_message", "time": 1543501430.631774, "message": "Archive consistency check complete, no problems found.", "levelname": "INFO", "name": "borg.archive"} 7 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/compact_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1645335268.9396632, "message": "compaction freed about 56.00 kB repository space.", "levelname": "INFO", "name": "borg.repository"} 2 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/compact_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/compact_stdout.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_break_stderr.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/create_break_stderr.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_break_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/create_break_stdout.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_lock_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1605936838.3639696, "message": "Failed to create/acquire the lock /tmp/another/lock.exclusive (timeout).", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockTimeout"} 2 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_lock_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/create_lock_stdout.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_perm_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1605936460.5992384, "message": "Failed to create/acquire the lock /tmp/another/lock.exclusive ([Errno 13] Permission denied: '/tmp/another/lock.exclusive').", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockFailed"} 2 | {"type": "log_message", "time": 1605936460.5994494, "message": "Traceback (most recent call last):\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4591, in main\n exit_code = archiver.run(args)\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4523, in run\n return set_ec(func(args))\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 161, in wrapper\n with repository:\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 190, in __enter__\n self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 421, in open\n self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 350, in acquire\n self._wait_for_readers_finishing(remove, sleep)\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 363, in _wait_for_readers_finishing\n self._lock.acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 138, in acquire\n raise LockFailed(self.path, str(err)) from None\nborg.locking.LockFailed: Failed to create/acquire the lock /tmp/another/lock.exclusive ([Errno 13] Permission denied: '/tmp/another/lock.exclusive').\n\nPlatform: Linux github 5.8.0-29-generic #31-Ubuntu SMP Fri Nov 6 12:37:59 UTC 2020 x86_64\nLinux: Unknown Linux \nBorg: 1.1.14 Python: CPython 3.8.6 msgpack: 0.5.6\nPID: 64701 CWD: /home/user/Projects/vorta/tests/borg_json_output\nsys.argv: ['/usr/bin/borg', 'create', '--list', '--progress', '--info', '--log-json', '--json', '--filter=AM', '-C', 'lz4', '/tmp/another::github-asdf-2020-11-17T00:05:49', '/home/user/bashrc']\nSSH_ORIGINAL_COMMAND: None\n", "levelname": "ERROR", "name": "borg.archiver"} 3 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_perm_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/create_perm_stdout.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/create_stdout.json: -------------------------------------------------------------------------------- 1 | { 2 | "archive": { 3 | "command_line": [ 4 | "/Users/manu/.pyenv/versions/3.7.1/bin/borg", 5 | "create", 6 | "--list", 7 | "--progress", 8 | "--info", 9 | "--log-json", 10 | "--json", 11 | "w66xh7lj@w66xh7lj.repo.borgbase.com:repo::test-snapadkkfdddasdf", 12 | "/Users/manu/Documents/financial/Allianz" 13 | ], 14 | "duration": 4.454152, 15 | "end": "2018-11-06T14:24:09.000000", 16 | "id": "b7a67208a9329bc48f7e2953b9803ffe0175e776a49d7f1a9c07581e3e7b5a17", 17 | "limits": { 18 | "max_archive_size": 2.851491780813361e-05 19 | }, 20 | "name": "test-snapadkkfdddasdf", 21 | "start": "2018-11-06T14:24:04.000000", 22 | "stats": { 23 | "compressed_size": 2954077, 24 | "deduplicated_size": 2954077, 25 | "nfiles": 10, 26 | "original_size": 3038309 27 | } 28 | }, 29 | "cache": { 30 | "path": "/Users/manu/.cache/borg/daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb", 31 | "stats": { 32 | "total_chunks": 97, 33 | "total_csize": 23892256, 34 | "total_size": 27955635, 35 | "total_unique_chunks": 63, 36 | "unique_csize": 13435127, 37 | "unique_size": 15520474 38 | } 39 | }, 40 | "encryption": { 41 | "mode": "repokey-blake2" 42 | }, 43 | "repository": { 44 | "id": "daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb", 45 | "last_modified": "2018-11-06T14:24:14.000000", 46 | "location": "ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/delete_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1605817314.144782, "message": "Deleting archive: test-archive1 Thu, 2020-11-19 14:20:48 [c361322b52718ac564129d24f34b203bd0e3fd573a0c66469d80e2428dffc9df] (1/1)", "levelname": "INFO", "name": "borg.archiver"} 2 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/delete_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/delete_stdout.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/diff_archives_dict_issue_stderr.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/diff_archives_dict_issue_stderr.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/diff_archives_dict_issue_stdout.json: -------------------------------------------------------------------------------- 1 | added directory Users/manu/Downloads 2 | added 122 B Users/manu/Downloads/.transifexrc 3 | added 9.22 kB Users/manu/Downloads/12042021 ORDER COD NUMBER.doc 4 | removed directory Users/manu/Downloads/test-diff/bar 5 | removed 0 B Users/manu/Downloads/test-diff/bar/2.txt 6 | removed directory Users/manu/Downloads/test-diff/foo 7 | removed 0 B Users/manu/Downloads/test-diff/foo/1.txt 8 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/diff_archives_stderr.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/diff_archives_stderr.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/diff_archives_stdout.json: -------------------------------------------------------------------------------- 1 | +7 B 0 B [-rw-rw-r-- -> -rw-rw-rw-] test/hallo 2 | added 0 B test/tschüss 3 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/info_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1541485706.185808, "message": "using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 2 | {"type": "log_message", "time": 1541485706.329196, "message": "35 self tests completed in 0.14 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 3 | {"type": "log_message", "time": 1541485706.32982, "message": "SSH command line: ['ssh', 'w66xh7lj@w66xh7lj.repo.borgbase.com', 'borg', 'serve', '--umask=077', '--debug']", "levelname": "DEBUG", "name": "borg.remote"} 4 | {"type": "log_message", "time": 1541485712.0352168, "message": "Remote: using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 5 | {"type": "log_message", "time": 1541485712.10305, "message": "Remote: 35 self tests completed in 0.16 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 6 | {"type": "log_message", "time": 1541485711.9786682, "levelname": "DEBUG", "message": "Remote: using builtin fallback logging configuration", "name": "borg.logger"} 7 | {"type": "log_message", "time": 1541485711.9788692, "levelname": "DEBUG", "message": "Remote: Initialized logging system for JSON-based protocol", "name": "borg.remote"} 8 | {"type": "log_message", "time": 1541485712.2395813, "levelname": "DEBUG", "message": "Remote: Resolving repository path b'repo'", "name": "root"} 9 | {"type": "log_message", "time": 1541485712.240386, "levelname": "DEBUG", "message": "Remote: Resolved repository path to '/srv/repos/w66xh7lj/repo'", "name": "root"} 10 | {"type": "log_message", "time": 1541485712.7960937, "levelname": "DEBUG", "message": "Remote: Verified integrity of /srv/repos/w66xh7lj/repo/index.153", "name": "borg.crypto.file_integrity"} 11 | {"type": "log_message", "time": 1541485713.763066, "message": "TAM-verified manifest", "levelname": "DEBUG", "name": "borg.crypto.key"} 12 | {"type": "log_message", "time": 1541485713.779689, "message": "security: read previous location 'ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo'", "levelname": "DEBUG", "name": "borg.cache"} 13 | {"type": "log_message", "time": 1541485713.780284, "message": "security: read manifest timestamp '2018-11-06T06:24:14.199720'", "levelname": "DEBUG", "name": "borg.cache"} 14 | {"type": "log_message", "time": 1541485713.780406, "message": "security: determined newest manifest timestamp as 2018-11-06T06:24:14.199720", "levelname": "DEBUG", "name": "borg.cache"} 15 | {"type": "log_message", "time": 1541485713.7819798, "message": "security: repository checks ok, allowing access", "levelname": "DEBUG", "name": "borg.cache"} 16 | {"type": "log_message", "time": 1541485713.7866921, "message": "Verified integrity of /Users/manu/.cache/borg/daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb/chunks", "levelname": "DEBUG", "name": "borg.crypto.file_integrity"} 17 | {"type": "log_message", "time": 1541485713.7872949, "message": "security: read previous location 'ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo'", "levelname": "DEBUG", "name": "borg.cache"} 18 | {"type": "log_message", "time": 1541485713.787873, "message": "security: read manifest timestamp '2018-11-06T06:24:14.199720'", "levelname": "DEBUG", "name": "borg.cache"} 19 | {"type": "log_message", "time": 1541485713.788007, "message": "security: determined newest manifest timestamp as 2018-11-06T06:24:14.199720", "levelname": "DEBUG", "name": "borg.cache"} 20 | {"type": "log_message", "time": 1541485713.788181, "message": "security: repository checks ok, allowing access", "levelname": "DEBUG", "name": "borg.cache"} 21 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/info_stdout.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "path": "/Users/manu/.cache/borg/daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb", 4 | "stats": { 5 | "total_chunks": 97, 6 | "total_csize": 23892256, 7 | "total_size": 27955635, 8 | "total_unique_chunks": 63, 9 | "unique_csize": 13435127, 10 | "unique_size": 15520474 11 | } 12 | }, 13 | "encryption": { 14 | "mode": "repokey-blake2" 15 | }, 16 | "repository": { 17 | "id": "daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb", 18 | "last_modified": "2018-11-06T14:24:14.000000", 19 | "location": "ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo" 20 | }, 21 | "security_dir": "/Users/manu/.config/borg/security/daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb" 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/list_archive_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1541485706.185808, "message": "using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 2 | {"type": "log_message", "time": 1541485706.329196, "message": "35 self tests completed in 0.14 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 3 | {"type": "log_message", "time": 1541485706.32982, "message": "SSH command line: ['ssh', 'w66xh7lj@w66xh7lj.repo.borgbase.com', 'borg', 'serve', '--umask=077', '--debug']", "levelname": "DEBUG", "name": "borg.remote"} 4 | {"type": "log_message", "time": 1541485712.0352168, "message": "Remote: using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 5 | {"type": "log_message", "time": 1541485712.10305, "message": "Remote: 35 self tests completed in 0.16 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 6 | {"type": "log_message", "time": 1541485711.9786682, "levelname": "DEBUG", "message": "Remote: using builtin fallback logging configuration", "name": "borg.logger"} 7 | {"type": "log_message", "time": 1541485711.9788692, "levelname": "DEBUG", "message": "Remote: Initialized logging system for JSON-based protocol", "name": "borg.remote"} 8 | {"type": "log_message", "time": 1541485712.2395813, "levelname": "DEBUG", "message": "Remote: Resolving repository path b'repo'", "name": "root"} 9 | {"type": "log_message", "time": 1541485712.240386, "levelname": "DEBUG", "message": "Remote: Resolved repository path to '/srv/repos/w66xh7lj/repo'", "name": "root"} 10 | {"type": "log_message", "time": 1541485712.7960937, "levelname": "DEBUG", "message": "Remote: Verified integrity of /srv/repos/w66xh7lj/repo/index.153", "name": "borg.crypto.file_integrity"} 11 | {"type": "log_message", "time": 1541485713.763066, "message": "TAM-verified manifest", "levelname": "DEBUG", "name": "borg.crypto.key"} 12 | {"type": "log_message", "time": 1541485713.779689, "message": "security: read previous location 'ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo'", "levelname": "DEBUG", "name": "borg.cache"} 13 | {"type": "log_message", "time": 1541485713.780284, "message": "security: read manifest timestamp '2018-11-06T06:24:14.199720'", "levelname": "DEBUG", "name": "borg.cache"} 14 | {"type": "log_message", "time": 1541485713.780406, "message": "security: determined newest manifest timestamp as 2018-11-06T06:24:14.199720", "levelname": "DEBUG", "name": "borg.cache"} 15 | {"type": "log_message", "time": 1541485713.7819798, "message": "security: repository checks ok, allowing access", "levelname": "DEBUG", "name": "borg.cache"} 16 | {"type": "log_message", "time": 1541485713.7866921, "message": "Verified integrity of /Users/manu/.cache/borg/daf2e2b94a1b57f0effc96939813ef58d0af04414f92f87c3e092a99adaa90eb/chunks", "levelname": "DEBUG", "name": "borg.crypto.file_integrity"} 17 | {"type": "log_message", "time": 1541485713.7872949, "message": "security: read previous location 'ssh://w66xh7lj@w66xh7lj.repo.borgbase.com/./repo'", "levelname": "DEBUG", "name": "borg.cache"} 18 | {"type": "log_message", "time": 1541485713.787873, "message": "security: read manifest timestamp '2018-11-06T06:24:14.199720'", "levelname": "DEBUG", "name": "borg.cache"} 19 | {"type": "log_message", "time": 1541485713.788007, "message": "security: determined newest manifest timestamp as 2018-11-06T06:24:14.199720", "levelname": "DEBUG", "name": "borg.cache"} 20 | {"type": "log_message", "time": 1541485713.788181, "message": "security: repository checks ok, allowing access", "levelname": "DEBUG", "name": "borg.cache"} 21 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/list_stderr.json: -------------------------------------------------------------------------------- 1 | {"type": "log_message", "time": 1541483306.565752, "message": "using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 2 | {"type": "log_message", "time": 1541483306.700531, "message": "35 self tests completed in 0.13 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 3 | {"type": "log_message", "time": 1541483306.701061, "message": "SSH command line: ['ssh', 'i0fis593@i0fis593.repo.borgbase.com', 'borg', 'serve', '--umask=077', '--debug']", "levelname": "DEBUG", "name": "borg.remote"} 4 | {"type": "log_message", "time": 1541483311.98485, "message": "Remote: using builtin fallback logging configuration", "levelname": "DEBUG", "name": "borg.logger"} 5 | {"type": "log_message", "time": 1541483312.104528, "message": "Remote: 35 self tests completed in 0.30 seconds", "levelname": "DEBUG", "name": "borg.archiver"} 6 | {"type": "log_message", "time": 1541483311.9681718, "name": "borg.logger", "message": "Remote: using builtin fallback logging configuration", "levelname": "DEBUG"} 7 | {"type": "log_message", "time": 1541483311.9683647, "name": "borg.remote", "message": "Remote: Initialized logging system for JSON-based protocol", "levelname": "DEBUG"} 8 | {"type": "log_message", "time": 1541483312.2289863, "name": "root", "message": "Remote: Resolving repository path b'repo'", "levelname": "DEBUG"} 9 | {"type": "log_message", "time": 1541483312.2298186, "name": "root", "message": "Remote: Resolved repository path to '/srv/repos/i0fis593/repo'", "levelname": "DEBUG"} 10 | {"type": "log_message", "time": 1541483312.525757, "name": "borg.crypto.file_integrity", "message": "Remote: Verified integrity of /srv/repos/i0fis593/repo/index.81", "levelname": "DEBUG"} 11 | Enter passphrase for key ssh://i0fis593@i0fis593.repo.borgbase.com/./repo: 12 | {"type": "log_message", "time": 1541483318.230314, "message": "TAM-verified manifest", "levelname": "DEBUG", "name": "borg.crypto.key"} 13 | {"type": "log_message", "time": 1541483318.234584, "message": "security: read previous location 'ssh://i0fis593@i0fis593.repo.borgbase.com/./repo'", "levelname": "DEBUG", "name": "borg.cache"} 14 | {"type": "log_message", "time": 1541483318.2360609, "message": "security: read manifest timestamp '2018-11-06T04:35:11.031517'", "levelname": "DEBUG", "name": "borg.cache"} 15 | {"type": "log_message", "time": 1541483318.236227, "message": "security: determined newest manifest timestamp as 2018-11-06T04:35:11.031517", "levelname": "DEBUG", "name": "borg.cache"} 16 | {"type": "log_message", "time": 1541483318.238312, "message": "security: repository checks ok, allowing access", "levelname": "DEBUG", "name": "borg.cache"} 17 | {"type": "log_message", "time": 1541483318.244082, "message": "RemoteRepository: 213 B bytes sent, 4.19 kB bytes received, 6 messages sent", "levelname": "DEBUG", "name": "borg.remote"} 18 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/list_stdout.json: -------------------------------------------------------------------------------- 1 | { 2 | "archives": [ 3 | { 4 | "archive": "nyx2.local-2018-11-04T23:19:04.864971", 5 | "barchive": "nyx2.local-2018-11-04T23:19:04.864971", 6 | "id": "32c86b44565e0f517abb3f1982f2789794773269ffc233e63c4f9b7e70527147", 7 | "name": "nyx2.local-2018-11-04T23:19:04.864971", 8 | "start": "2018-11-04T23:19:15.000000", 9 | "time": "2018-11-04T23:19:15.000000" 10 | }, 11 | { 12 | "archive": "nyx2.local-2018-11-05T22:12:14.375598", 13 | "barchive": "nyx2.local-2018-11-05T22:12:14.375598", 14 | "id": "6130a386f6a8e44efa35a3ab727a7116402edd9ef426c974b1ef40657313be05", 15 | "name": "nyx2.local-2018-11-05T22:12:14.375598", 16 | "start": "2018-11-05T22:12:45.000000", 17 | "time": "2018-11-05T22:12:45.000000" 18 | }, 19 | { 20 | "archive": "nyx2.local-2018-11-05T22:13:23.166918", 21 | "barchive": "nyx2.local-2018-11-05T22:13:23.166918", 22 | "id": "185d842309bc72e54ff3ca12c8022c473563c33435db413fdd7de1cab38ae9cf", 23 | "name": "nyx2.local-2018-11-05T22:13:23.166918", 24 | "start": "2018-11-05T22:13:32.000000", 25 | "time": "2018-11-05T22:13:32.000000" 26 | }, 27 | { 28 | "archive": "nyx2.local-2018-11-05T23:05:00.117950", 29 | "barchive": "nyx2.local-2018-11-05T23:05:00.117950", 30 | "id": "1bdbfedd59bd7222c3a8cc1a0188966b5db9484a3b0377009499b2c18d4f8ec5", 31 | "name": "nyx2.local-2018-11-05T23:05:00.117950", 32 | "start": "2018-11-05T23:05:09.000000", 33 | "time": "2018-11-05T23:05:09.000000" 34 | }, 35 | { 36 | "archive": "nyx2.local-2018-11-06T09:35:00.569691", 37 | "barchive": "nyx2.local-2018-11-06T09:35:00.569691", 38 | "id": "dd59bb26c1eed3aa21424a252cd73efcff0252156c50d67fb806a3a772c7cb48", 39 | "name": "nyx2.local-2018-11-06T09:35:00.569691", 40 | "start": "2018-11-06T09:35:10.000000", 41 | "time": "2018-11-06T09:35:10.000000" 42 | }, 43 | { 44 | "archive": "nyx2.local-2018-11-06T12:35:00.087922", 45 | "barchive": "nyx2.local-2018-11-06T12:35:00.087922", 46 | "id": "0ad78376f9e8d0f9ad359535c582da910b12a3d8c61ee5cc30bc57b1eb4c1b9a", 47 | "name": "nyx2.local-2018-11-06T12:35:00.087922", 48 | "start": "2018-11-06T12:35:09.000000", 49 | "time": "2018-11-06T12:35:09.000000" 50 | } 51 | ], 52 | "encryption": { 53 | "mode": "repokey-blake2" 54 | }, 55 | "repository": { 56 | "id": "3c1861de5908546a65409ed6b98024c0a4845d5348496010896798368c2424dd", 57 | "last_modified": "2018-11-06T12:35:11.000000", 58 | "location": "ssh://i0fis593@i0fis593.repo.borgbase.com/./repo" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/prune_stderr.json: -------------------------------------------------------------------------------- 1 | {"levelname": "INFO", "message": "Remote: Storage quota: 10.51 MB out of 1.00 GB used.", "time": 1543489279.0468729, "type": "log_message", "name": "borg.repository"} 2 | {"levelname": "INFO", "message": "Remote: Storage quota: 10.49 MB out of 1.00 GB used.", "time": 1543489282.1030364, "type": "log_message", "name": "borg.repository"} 3 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/prune_stdout.json: -------------------------------------------------------------------------------- 1 | { 2 | "archives": [] 3 | } 4 | -------------------------------------------------------------------------------- /tests/unit/borg_json_output/rename_stderr.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/rename_stderr.json -------------------------------------------------------------------------------- /tests/unit/borg_json_output/rename_stdout.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbase/vorta/cd052b615770341359cef2fec5dfafbe0ab2e20b/tests/unit/borg_json_output/rename_stdout.json -------------------------------------------------------------------------------- /tests/unit/profile_exports/invalid_no_json.json: -------------------------------------------------------------------------------- 1 | this is not json 2 | -------------------------------------------------------------------------------- /tests/unit/profile_exports/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Test Profile Restoration", 4 | "added_at": "2020-09-10 01:09:03.402268", 5 | "repo": { 6 | "url": "/tmp/asdf", 7 | "added_at": "2020-09-10 21:48:21.427080", 8 | "encryption": "repokey-blake2", 9 | "unique_size": null, 10 | "unique_csize": null, 11 | "total_size": null, 12 | "total_unique_chunks": null, 13 | "extra_borg_arguments": "" 14 | }, 15 | "ssh_key": null, 16 | "compression": "zstd,8", 17 | "exclude_patterns": null, 18 | "schedule_mode": "off", 19 | "schedule_interval_unit": "hours", 20 | "schedule_interval_count": 2, 21 | "schedule_fixed_hour": 16, 22 | "schedule_fixed_minute": 0, 23 | "validation_on": true, 24 | "validation_weeks": 3, 25 | "prune_on": true, 26 | "prune_hour": 2, 27 | "prune_day": 7, 28 | "prune_week": 4, 29 | "prune_month": 6, 30 | "prune_year": 2, 31 | "prune_keep_within": "10H", 32 | "new_archive_name": "{hostname}-{profile_slug}-{now:%Y-%m-%dT%H:%M:%S}", 33 | "prune_prefix": "{hostname}-{profile_slug}-", 34 | "pre_backup_cmd": "", 35 | "password": "Tr0ub4dor&3", 36 | "post_backup_cmd": "", 37 | "dont_run_on_metered_networks": true, 38 | "SourceFileModel": [ 39 | { 40 | "dir": "/this/is/a/test/file", 41 | "profile": 1, 42 | "added_at": "2020-07-03 03:39:35.226932" 43 | }, 44 | { 45 | "dir": "/this/is/another/test/file", 46 | "profile": 1, 47 | "added_at": "2020-07-03 04:37:02.367233" 48 | }, 49 | { 50 | "dir": "/why/are/you/reading/this", 51 | "profile": 1, 52 | "added_at": "2020-07-03 04:37:05.106150" 53 | } 54 | ], 55 | "WifiSettingModel": [], 56 | "SchemaVersion": { 57 | "id": 1, 58 | "version": 15, 59 | "changed_at": "2020-10-19 19:07:35.305493" 60 | }, 61 | "SettingsModel": [ 62 | { 63 | "id": 1, 64 | "key": "enable_notifications", 65 | "value": true, 66 | "str_value": "", 67 | "label": "Display notifications when background tasks fail", 68 | "type": "checkbox" 69 | }, 70 | { 71 | "id": 2, 72 | "key": "enable_notifications_success", 73 | "value": false, 74 | "str_value": "", 75 | "label": "Also notify about successful background tasks", 76 | "type": "checkbox" 77 | }, 78 | { 79 | "id": 3, 80 | "key": "autostart", 81 | "value": false, 82 | "str_value": "", 83 | "label": "Automatically start Vorta at login", 84 | "type": "checkbox" 85 | }, 86 | { 87 | "id": 4, 88 | "key": "foreground", 89 | "value": true, 90 | "str_value": "", 91 | "label": "Open main window on startup", 92 | "type": "checkbox" 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /tests/unit/test_borg.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import vorta.borg 4 | import vorta.store.models 5 | from vorta.borg.prune import BorgPruneJob 6 | 7 | 8 | def test_borg_prune(qapp, qtbot, mocker, borg_json_output): 9 | stdout, stderr = borg_json_output('prune') 10 | popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) 11 | mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) 12 | 13 | params = BorgPruneJob.prepare(vorta.store.models.BackupProfileModel.select().first()) 14 | thread = BorgPruneJob(params['cmd'], params, qapp) 15 | 16 | with qtbot.waitSignal(thread.result, **pytest._wait_defaults) as blocker: 17 | blocker.connect(thread.updated) 18 | thread.run() 19 | 20 | assert blocker.args[0]['returncode'] == 0 21 | -------------------------------------------------------------------------------- /tests/unit/test_create.py: -------------------------------------------------------------------------------- 1 | from vorta.borg.create import BorgCreateJob 2 | from vorta.store.models import BackupProfileModel, SourceFileModel 3 | 4 | 5 | def test_create_paths_from_command(): 6 | default_profile = BackupProfileModel.get() 7 | default_profile.new_archive_name = 'a1' 8 | 9 | default_profile.repo.create_backup_cmd = '--one-file-system' 10 | result = BorgCreateJob.prepare(default_profile) 11 | 12 | assert 'cmd' in result 13 | assert result['cmd'] == [ 14 | 'borg', 15 | 'create', 16 | '--list', 17 | '--progress', 18 | '--info', 19 | '--log-json', 20 | '--json', 21 | '--filter=AM', 22 | '-C', 23 | 'lz4', 24 | '--one-file-system', 25 | 'i0fi93@i593.repo.borgbase.com:repo::a1', 26 | '/tmp/another', 27 | ] 28 | 29 | default_profile.repo.create_backup_cmd = '--paths-from-command -- echo /tmp/another' 30 | SourceFileModel.delete().execute() 31 | 32 | result = BorgCreateJob.prepare(default_profile) 33 | 34 | assert 'cmd' in result 35 | assert result['cmd'] == [ 36 | 'borg', 37 | 'create', 38 | '--list', 39 | '--progress', 40 | '--info', 41 | '--log-json', 42 | '--json', 43 | '--filter=AM', 44 | '-C', 45 | 'lz4', 46 | '--paths-from-command', 47 | 'i0fi93@i593.repo.borgbase.com:repo::a1', 48 | '--', 49 | 'echo', 50 | '/tmp/another', 51 | ] 52 | -------------------------------------------------------------------------------- /tests/unit/test_excludes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PyQt6 import QtCore 3 | 4 | 5 | @pytest.mark.skip(reason="fails on macos with timeout when checking chromium-cache") 6 | def test_exclusion_preview_populated(qapp, qtbot): 7 | main = qapp.main_window 8 | tab = main.sourceTab 9 | main.tabWidget.setCurrentIndex(1) 10 | 11 | qtbot.mouseClick(tab.bExclude, QtCore.Qt.MouseButton.LeftButton) 12 | qtbot.mouseClick(tab._window.bAddPattern, QtCore.Qt.MouseButton.LeftButton) 13 | 14 | qtbot.keyClicks(tab._window.customExclusionsList.viewport().focusWidget(), "custom pattern") 15 | qtbot.keyClick(tab._window.customExclusionsList.viewport().focusWidget(), QtCore.Qt.Key.Key_Enter) 16 | qtbot.waitUntil(lambda: "custom pattern" in tab._window.exclusionsPreviewText.toPlainText()) 17 | 18 | tab._window.tabWidget.setCurrentIndex(1) 19 | 20 | tab._window.exclusionPresetsModel.itemFromIndex(tab._window.exclusionPresetsModel.index(0, 0)).setCheckState( 21 | QtCore.Qt.CheckState.Checked 22 | ) 23 | 24 | qtbot.waitUntil(lambda: "# chromium-cache" in tab._window.exclusionsPreviewText.toPlainText()) 25 | tab._window.tabWidget.setCurrentIndex(2) 26 | 27 | qtbot.keyClicks(tab._window.rawExclusionsText, "test raw pattern 1") 28 | qtbot.waitUntil(lambda: "test raw pattern 1\n" in tab._window.exclusionsPreviewText.toPlainText()) 29 | 30 | qtbot.mouseClick(tab.bExclude, QtCore.Qt.MouseButton.LeftButton) 31 | qtbot.mouseClick(tab._window.bAddPatternExcludeIfPresent, QtCore.Qt.MouseButton.LeftButton) 32 | 33 | qtbot.keyClicks(tab._window.excludeIfPresentList.viewport().focusWidget(), "exclude_if_present_file") 34 | qtbot.keyClick(tab._window.excludeIfPresentList.viewport().focusWidget(), QtCore.Qt.Key.Key_Enter) 35 | qtbot.waitUntil(lambda: "exclude_if_present_file" in tab._window.exclusionsPreviewText.toPlainText()) 36 | -------------------------------------------------------------------------------- /tests/unit/test_lock.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PyQt6 import QtCore 3 | 4 | import vorta.application 5 | import vorta.borg.borg_job 6 | 7 | 8 | def test_create_perm_error(qapp, borg_json_output, mocker, qtbot): 9 | main = qapp.main_window 10 | mocker.patch.object(vorta.application.QMessageBox, 'show') 11 | 12 | stdout, stderr = borg_json_output('create_perm') 13 | popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) 14 | mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) 15 | 16 | qtbot.mouseClick(main.createStartBtn, QtCore.Qt.MouseButton.LeftButton) 17 | 18 | qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), **pytest._wait_defaults) 19 | assert qapp._msg.text().startswith("You do not have permission") 20 | del qapp._msg 21 | 22 | 23 | def test_create_lock(qapp, borg_json_output, mocker, qtbot): 24 | main = qapp.main_window 25 | mocker.patch.object(vorta.application.QMessageBox, 'show') 26 | 27 | # Trigger locked repo 28 | stdout, stderr = borg_json_output('create_lock') 29 | popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) 30 | mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) 31 | 32 | qtbot.mouseClick(main.createStartBtn, QtCore.Qt.MouseButton.LeftButton) 33 | 34 | qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), **pytest._wait_defaults) 35 | assert "The repository at" in qapp._msg.text() 36 | 37 | # Break locked repo 38 | stdout, stderr = borg_json_output('create_break') 39 | popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) 40 | mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) 41 | 42 | qtbot.waitUntil(lambda: main.createStartBtn.isEnabled(), **pytest._wait_defaults) # Prevent thread collision 43 | qapp._msg.accept() 44 | exp_message_text = 'Repository lock broken. Please redo your last action.' 45 | qtbot.waitUntil(lambda: exp_message_text in main.progressText.text(), **pytest._wait_defaults) 46 | -------------------------------------------------------------------------------- /tests/unit/test_notifications.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from PyQt6 import QtDBus 5 | 6 | import vorta.borg 7 | import vorta.notifications 8 | 9 | 10 | @pytest.mark.skipif(sys.platform != 'linux', reason="DBus notifications only on Linux") 11 | def test_linux_background_notifications(qapp, mocker): 12 | """We can't see notifications, but we watch for exceptions and errors.""" 13 | 14 | notifier = vorta.notifications.VortaNotifications.pick() 15 | assert isinstance(notifier, vorta.notifications.DBusNotifications) 16 | notifier.deliver('Vorta Test', 'test notification', level='error') 17 | 18 | mocker.spy(QtDBus.QDBusInterface, 'call') 19 | notifier.deliver('Vorta Test', 'test notification', level='info') # fails if called. 20 | assert QtDBus.QDBusInterface.call.call_count == 0 21 | -------------------------------------------------------------------------------- /tests/unit/test_schedule.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | from datetime import timedelta 3 | from unittest.mock import MagicMock 4 | 5 | import pytest 6 | from PyQt6 import QtCore 7 | 8 | import vorta.scheduler 9 | from vorta.application import VortaApp 10 | from vorta.store.models import BackupProfileModel, EventLogModel 11 | 12 | PROFILE_NAME = 'Default' 13 | 14 | 15 | @pytest.fixture 16 | def clockmock(monkeypatch): 17 | datetime_mock = MagicMock(wraps=dt) 18 | monkeypatch.setattr(vorta.scheduler, "dt", datetime_mock) 19 | 20 | return datetime_mock 21 | 22 | 23 | def test_schedule_tab(qapp: VortaApp, qtbot, clockmock): 24 | main = qapp.main_window 25 | tab = main.scheduleTab.schedulePage 26 | 27 | # setup 28 | time_now = dt(2020, 5, 6, 4, 30) 29 | clockmock.now.return_value = time_now 30 | 31 | # Work around 32 | # because already 'deleted' scheduletabs are still connected to the signal 33 | qapp.scheduler.schedule_changed.connect(lambda *args: tab.draw_next_scheduled_backup()) 34 | 35 | # Test 36 | qtbot.mouseClick(tab.scheduleOffRadio, QtCore.Qt.MouseButton.LeftButton) 37 | assert tab.nextBackupDateTimeLabel.text() == 'None scheduled' 38 | 39 | tab.scheduleIntervalCount.setValue(5) 40 | qtbot.mouseClick(tab.scheduleIntervalRadio, QtCore.Qt.MouseButton.LeftButton) 41 | assert "None" not in tab.nextBackupDateTimeLabel.text() 42 | 43 | tab.scheduleFixedTime.setTime(QtCore.QTime(23, 59)) 44 | 45 | # Clicking currently broken for this button on github.com only 46 | # qtbot.mouseClick(tab.scheduleFixedRadio, QtCore.Qt.MouseButton.LeftButton) 47 | 48 | # Workaround for github 49 | tab.scheduleFixedRadio.setChecked(True) 50 | tab.scheduleFixedRadio.clicked.emit() 51 | 52 | assert tab.nextBackupDateTimeLabel.text() == 'Run a manual backup first' 53 | 54 | next_backup = time_now.replace(hour=23, minute=59) 55 | last_time = time_now - timedelta(days=2) 56 | 57 | # setup model 58 | profile = BackupProfileModel.get(name=PROFILE_NAME) 59 | profile.schedule_make_up_missed = False 60 | profile.save() 61 | event = EventLogModel( 62 | subcommand='create', 63 | profile=profile.id, 64 | returncode=0, 65 | category='scheduled', 66 | start_time=last_time, 67 | end_time=last_time, 68 | ) 69 | event.save() 70 | 71 | qapp.scheduler.set_timer_for_profile(profile.id) 72 | tab.draw_next_scheduled_backup() 73 | 74 | assert tab.nextBackupDateTimeLabel.text() not in [ 75 | "Run a manual backup first", 76 | "None scheduled", 77 | ] 78 | assert qapp.scheduler.next_job_for_profile(profile.id).time == next_backup 79 | 80 | qapp.scheduler.remove_job(profile.id) 81 | --------------------------------------------------------------------------------