[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://editorconfig.org/\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\nindent_style = space\nindent_size = 4\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# Migrate code style to Black\n7957af562d5ce8266b177039783be4dc8bdd7898\n"
  },
  {
    "path": ".gitattributes",
    "content": "borg/_version.py export-subst\n\n*.py diff=python\ndocs/usage/*.rst.inc merge=ours\ndocs/man/* merge=ours\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: borgbackup\nliberapay: borgbackup\nopen_collective: borgbackup\ncustom: ['https://www.borgbackup.org/support/fund.html']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nThank you for reporting an issue.\n\n*IMPORTANT* – Before creating a new issue, please look around:\n - BorgBackup documentation: https://borgbackup.readthedocs.io/en/stable/index.html\n - FAQ: https://borgbackup.readthedocs.io/en/stable/faq.html\n - Open issues in the GitHub tracker: https://github.com/borgbackup/borg/issues\n\nIf you cannot find a similar problem, then create a new issue.\n\nPlease fill in as much of the template as possible.\n-->\n\n## Have you checked the BorgBackup docs, FAQ, and open GitHub issues?\n\nNo\n\n## Is this a bug/issue report or a question?\n\nBug/Issue/Question\n\n## System information. For client/server mode, post info for both machines.\n\n#### Your Borg version (borg -V).\n\n#### Operating system (distribution) and version.\n\n#### Hardware/network configuration and filesystems used.\n\n#### How much data is handled by Borg?\n\n#### Full Borg command line that led to the problem (leave out excludes and passwords).\n\n\n## Describe the problem you're observing.\n\n#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.\n\n#### Include any warnings/errors/backtraces from the system logs\n\n<!--\n\nIf this complaint relates to Borg performance, please include CRUD benchmark\nresults and any steps you took to troubleshoot.\nHow to run the benchmark: https://borgbackup.readthedocs.io/en/stable/usage/benchmark.html\n\n*IMPORTANT* – Please mark logs and terminal command output, otherwise GitHub will not display them correctly.\nAn example is provided below.\n\nExample:\n```\nthis is an example of how log text should be marked (wrap it with ```)\n```\n-->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nThank you for contributing to BorgBackup!\n\nPlease make sure your PR complies with our contribution guidelines:\nhttps://borgbackup.readthedocs.io/en/latest/development.html#contributions\n-->\n\n## Description\n\n<!-- What does this PR do? Reference any related issues with \"fixes #XXXX\". -->\n\n\n## Checklist\n\n- [ ] PR is against `master` (or maintenance branch if only applicable there)\n- [ ] New code has tests and docs where appropriate\n- [ ] Tests pass (run `tox` or the relevant test subset)\n- [ ] Commit messages are clean and reference related issues\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n  - package-ecosystem: \"pip\"\n    directory: \"/requirements.d\"\n    ignore:\n    - dependency-name: \"black\"\n    schedule:\n      interval: \"weekly\"\n    cooldown:\n      semver-major-days: 90\n      semver-minor-days: 30\n    groups:\n      pip-dependencies:\n        patterns:\n          - \"*\"\n\n"
  },
  {
    "path": ".github/workflows/backport.yml",
    "content": "name: Backport pull request\n\non:\n  pull_request_target:\n    types: [closed]\n  issue_comment:\n    types: [created]\n\npermissions:\n  contents: write # so it can comment\n  pull-requests: write # so it can create pull requests\n\njobs:\n  backport:\n    name: Backport pull request\n    runs-on: ubuntu-24.04\n    timeout-minutes: 5\n\n    # Only run when pull request is merged\n    # or when a comment starting with `/backport` is created by someone other than the\n    # https://github.com/backport-action bot user (user id: 97796249). Note that if you use your\n    # own PAT as `github_token`, that you should replace this id with yours.\n    if: >\n        (\n          github.event_name == 'pull_request_target' &&\n          github.event.pull_request.merged\n        ) || (\n          github.event_name == 'issue_comment' &&\n          github.event.issue.pull_request &&\n          github.event.comment.user.id != 97796249 &&\n          startsWith(github.event.comment.body, '/backport')\n        )\n    steps:\n        - uses: actions/checkout@v6\n        - name: Create backport pull requests\n          uses: korthout/backport-action@v4\n          with:\n            label_pattern: '^port/(.+)$'\n"
  },
  {
    "path": ".github/workflows/black.yaml",
    "content": "# https://black.readthedocs.io/en/stable/integrations/github_actions.html#usage\n# See also what we use locally in requirements.d/codestyle.txt — this should be the same version here.\n\nname: Lint\n\non:\n  push:\n    paths:\n      - '**.py'\n      - 'pyproject.toml'\n      - '.github/workflows/black.yaml'\n  pull_request:\n    paths:\n      - '**.py'\n      - 'pyproject.toml'\n      - '.github/workflows/black.yaml'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\njobs:\n  lint:\n    runs-on: ubuntu-22.04\n    timeout-minutes: 5\n    steps:\n      - uses: actions/checkout@v6\n      - uses: psf/black@c6755bb741b6481d6b3d3bb563c83fa060db96c9  # 26.3.1\n        with:\n            version: \"~= 24.0\"\n"
  },
  {
    "path": ".github/workflows/canary.yml",
    "content": "name: Canary (Unlocked Requirements)\n\non:\n  schedule:\n    - cron: '0 7 * * *'  # Run at 07:00 UTC\n  workflow_dispatch:      # Allow manual trigger\n\npermissions:\n  contents: read\n\njobs:\n  canary_tests:\n    name: Canary (${{ matrix.os }}, ${{ matrix.python-version }}, ${{ matrix.toxenv }})\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 360\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          # A representative subset of environments\n          - os: ubuntu-22.04\n            python-version: '3.10'\n            toxenv: py310-llfuse\n          - os: ubuntu-24.04\n            python-version: '3.12'\n            toxenv: py312-pyfuse3\n          - os: ubuntu-24.04\n            python-version: '3.14'\n            toxenv: py314-mfusepy\n          - os: macos-15\n            python-version: '3.14'\n            toxenv: py314-none\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n        fetch-tags: true\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install Linux packages\n      if: ${{ runner.os == 'Linux' }}\n      shell: bash\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y pkg-config build-essential\n        sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev\n        if [[ \"${{ matrix.toxenv }}\" == *\"llfuse\"* ]]; then\n          sudo apt-get install -y libfuse-dev fuse\n        elif [[ \"${{ matrix.toxenv }}\" == *\"pyfuse3\"* || \"${{ matrix.toxenv }}\" == *\"mfusepy\"* ]]; then\n          sudo apt-get install -y libfuse3-dev fuse3\n        fi\n\n    - name: Install macOS packages\n      if: ${{ runner.os == 'macOS' }}\n      shell: bash\n      run: |\n        brew bundle install || true\n\n    - name: Install Python requirements (UNLOCKED)\n      shell: bash\n      run: |\n        python -m pip install --upgrade pip setuptools wheel\n        # Use UNLOCKED requirements to catch upstream breakages\n        pip install -r requirements.d/development.txt\n\n    - name: Install borgbackup\n      shell: bash\n      run: |\n        if [[ \"${{ matrix.toxenv }}\" == *\"llfuse\"* ]]; then\n          pip install -e \".[llfuse,cockpit]\"\n        elif [[ \"${{ matrix.toxenv }}\" == *\"pyfuse3\"* ]]; then\n          pip install -e \".[pyfuse3,cockpit]\"\n        elif [[ \"${{ matrix.toxenv }}\" == *\"mfusepy\"* ]]; then\n          pip install -e \".[mfusepy,cockpit]\"\n        else\n          pip install -e \".[cockpit]\"\n        fi\n\n    - name: Run tests (Canary)\n      shell: bash\n      run: |\n        if [[ \"${{ matrix.toxenv }}\" == *\"-windows\" ]]; then\n          python -m pytest -n4 --benchmark-skip -vv -rs -k \"not remote\" --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml\n        else\n          # Force tox to use the unlocked requirements in its environment creation\n          # by overriding the deps if possible, or just trusting it uses development.txt\n          # which we already installed in the root. Actually tox creates its own venv.\n          # We need to tell tox to use the unlocked file.\n          tox -e ${{ matrix.toxenv }} --override \"env_run_base.deps=[-rrequirements.d/development.txt]\"\n        fi\n\n  windows_canary:\n\n    if: true  # can be used to temporarily disable the build\n    name: Canary (Windows)\n    runs-on: windows-latest\n    timeout-minutes: 180\n\n    env:\n      PY_COLORS: 1\n\n    defaults:\n      run:\n        shell: msys2 {0}\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: msys2/setup-msys2@v2\n        with:\n          msystem: UCRT64\n          update: true\n\n      - name: Install system packages\n        run: ./scripts/msys2-install-deps development\n\n      - name: Build python venv\n        run: |\n          # building cffi / argon2-cffi in the venv fails, so we try to use the system packages\n          python -m venv --system-site-packages env\n          . env/bin/activate\n          # python -m pip install --upgrade pip\n          # pip install --upgrade setuptools build wheel\n          pip install -r requirements.d/pyinstaller.txt\n\n      - name: Build\n        run: |\n          # build borg.exe\n          . env/bin/activate\n          pip install -e \".[cockpit,s3,sftp,rest,rclone]\"\n          mkdir -p dist/binary\n          pyinstaller -y --clean --distpath=dist/binary scripts/borg.exe.spec\n          # build sdist and wheel in dist/...\n          python -m build\n\n      - name: Run tests\n        run: |\n          # Ensure locally built binary in ./dist/binary/borg-dir is found during tests\n          export PATH=\"$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH\"\n          borg.exe -V\n          . env/bin/activate\n          python -m pytest -n4 --benchmark-skip -vv -rs -k \"not remote\" --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "# badge: https://github.com/borgbackup/borg/workflows/CI/badge.svg?branch=master\n\nname: CI\n\non:\n  push:\n    branches: [ master ]\n    tags:\n    - '2.*'\n  pull_request:\n    branches: [ master ]\n    paths:\n    - '**.py'\n    - '**.pyx'\n    - '**.c'\n    - '**.h'\n    - '**.yml'\n    - '**.toml'\n    - '**.cfg'\n    - '**.ini'\n    - 'requirements.d/*'\n    - '!docs/**'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\npermissions:\n  contents: read\n\njobs:\n  lint:\n\n    runs-on: ubuntu-22.04\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v6\n    - uses: astral-sh/ruff-action@v3\n\n  security:\n\n    runs-on: ubuntu-24.04\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.10'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install bandit[toml]\n    - name: Run Bandit\n      run: |\n        bandit -r src/borg -c pyproject.toml\n\n  asan_ubsan:\n\n    runs-on: ubuntu-24.04\n    timeout-minutes: 25\n    needs: [lint]\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        # Just fetching one commit is not enough for setuptools-scm, so we fetch all.\n        fetch-depth: 0\n        fetch-tags: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.12'\n\n    - name: Install system packages\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y pkg-config build-essential\n        sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install -r requirements.d/development.lock.txt\n\n    - name: Build Borg with ASan/UBSan\n      # Build the C/Cython extensions with AddressSanitizer and UndefinedBehaviorSanitizer enabled.\n      # How this works:\n      # - The -fsanitize=address,undefined flags inject runtime checks into our native code. If a bug is hit\n      #   (e.g., buffer overflow, use-after-free, out-of-bounds, or undefined behavior), the sanitizer prints\n      #   a detailed error report to stderr, including a stack trace, and forces the process to exit with\n      #   non-zero status. In CI, this will fail the step/job so you will notice.\n      # - ASAN_OPTIONS/UBSAN_OPTIONS configure the sanitizers' runtime behavior (see below for meanings).\n      env:\n        CFLAGS: \"-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined\"\n        CXXFLAGS: \"-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined\"\n        LDFLAGS: \"-fsanitize=address,undefined\"\n        # ASAN_OPTIONS controls AddressSanitizer runtime tweaks:\n        # - detect_leaks=0: Disable LeakSanitizer to avoid false positives with CPython/pymalloc in short-lived tests.\n        # - strict_string_checks=1: Make invalid string operations (e.g., over-reads) more likely to be detected.\n        # - check_initialization_order=1: Catch uses that depend on static initialization order (C++).\n        # - detect_stack_use_after_return=1: Detect stack-use-after-return via stack poisoning (may increase overhead).\n        ASAN_OPTIONS: \"detect_leaks=0:strict_string_checks=1:check_initialization_order=1:detect_stack_use_after_return=1\"\n        # UBSAN_OPTIONS controls UndefinedBehaviorSanitizer runtime:\n        # - print_stacktrace=1: Include a stack trace for UB reports to ease debugging.\n        #   Note: UBSan is recoverable by default (process may continue after reporting). If you want CI to\n        #   abort immediately and fail on the first UB, add `halt_on_error=1` (e.g., UBSAN_OPTIONS=\"print_stacktrace=1:halt_on_error=1\").\n        UBSAN_OPTIONS: \"print_stacktrace=1\"\n        # PYTHONDEVMODE enables additional Python runtime checks and warnings.\n        PYTHONDEVMODE: \"1\"\n      run: pip install -e .\n\n    - name: Run tests under sanitizers\n      env:\n        ASAN_OPTIONS: \"detect_leaks=0:strict_string_checks=1:check_initialization_order=1:detect_stack_use_after_return=1\"\n        UBSAN_OPTIONS: \"print_stacktrace=1\"\n        PYTHONDEVMODE: \"1\"\n      # Ensure the ASan runtime is loaded first to avoid \"ASan runtime does not come first\" warnings.\n      # We discover libasan/libubsan paths via gcc and preload them for the Python test process.\n      # the remote tests are slow and likely won't find anything useful\n      run: |\n        set -euo pipefail\n        export LD_PRELOAD=\"$(gcc -print-file-name=libasan.so):$(gcc -print-file-name=libubsan.so)\"\n        echo \"Using LD_PRELOAD=$LD_PRELOAD\"\n        pytest -v --benchmark-skip -k \"not remote\"\n\n  native_tests:\n\n    needs: [lint]\n    permissions:\n      contents: read\n      id-token: write\n      attestations: write\n    strategy:\n      fail-fast: true\n      # noinspection YAMLSchemaValidation\n      matrix: >-\n        ${{ fromJSON(\n          github.event_name == 'pull_request' && '{\n            \"include\": [\n              {\"os\": \"ubuntu-22.04\", \"python-version\": \"3.10\", \"toxenv\": \"mypy\"},\n              {\"os\": \"ubuntu-22.04\", \"python-version\": \"3.11\", \"toxenv\": \"docs\"},\n              {\"os\": \"ubuntu-22.04\", \"python-version\": \"3.10\", \"toxenv\": \"py310-llfuse\"},\n              {\"os\": \"ubuntu-24.04\", \"python-version\": \"3.12\", \"toxenv\": \"py312-pyfuse3\"},\n              {\"os\": \"ubuntu-24.04\", \"python-version\": \"3.14\", \"toxenv\": \"py314-mfusepy\"}\n            ]\n          }' || '{\n            \"include\": [\n              {\"os\": \"ubuntu-22.04\", \"python-version\": \"3.11\", \"toxenv\": \"py311-pyfuse3\", \"binary\": \"borg-linux-glibc235-x86_64-gh\"},\n              {\"os\": \"ubuntu-22.04-arm\", \"python-version\": \"3.11\", \"toxenv\": \"py311-pyfuse3\", \"binary\": \"borg-linux-glibc235-arm64-gh\"},\n              {\"os\": \"ubuntu-24.04\", \"python-version\": \"3.12\", \"toxenv\": \"py312-llfuse\"},\n              {\"os\": \"ubuntu-24.04\", \"python-version\": \"3.13\", \"toxenv\": \"py313-pyfuse3\"},\n              {\"os\": \"ubuntu-24.04\", \"python-version\": \"3.14\", \"toxenv\": \"py314-mfusepy\"},\n              {\"os\": \"macos-15\", \"python-version\": \"3.11\", \"toxenv\": \"py311-none\", \"binary\": \"borg-macos-15-arm64-gh\"},\n              {\"os\": \"macos-15-intel\", \"python-version\": \"3.11\", \"toxenv\": \"py311-none\", \"binary\": \"borg-macos-15-x86_64-gh\"}\n            ]\n          }'\n        ) }}\n    env:\n      TOXENV: ${{ matrix.toxenv }}\n\n    runs-on: ${{ matrix.os }}\n    # macOS machines can be slow, if overloaded.\n    timeout-minutes: 360\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        # Just fetching one commit is not enough for setuptools-scm, so we fetch all.\n        fetch-depth: 0\n        fetch-tags: true\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Cache pip\n      uses: actions/cache@v5\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-${{ runner.arch }}-pip-${{ hashFiles('requirements.d/development.lock.txt') }}\n        restore-keys: |\n            ${{ runner.os }}-${{ runner.arch }}-pip-\n            ${{ runner.os }}-${{ runner.arch }}-\n\n    - name: Cache tox environments\n      uses: actions/cache@v5\n      with:\n        path: .tox\n        key: ${{ runner.os }}-${{ runner.arch }}-tox-${{ matrix.toxenv }}-${{ hashFiles('requirements.d/development.lock.txt', 'pyproject.toml') }}\n        restore-keys: |\n          ${{ runner.os }}-${{ runner.arch }}-tox-${{ matrix.toxenv }}-\n          ${{ runner.os }}-${{ runner.arch }}-tox-\n\n    - name: Install Linux packages\n      if: ${{ runner.os == 'Linux' }}\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y pkg-config build-essential\n        sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev\n        sudo apt-get install -y bash zsh fish  # for shell completion tests\n        sudo apt-get install -y rclone openssh-server curl\n        if [[ \"$TOXENV\" == *\"llfuse\"* ]]; then\n          sudo apt-get install -y libfuse-dev fuse  # Required for Python llfuse module\n        elif [[ \"$TOXENV\" == *\"pyfuse3\"* || \"$TOXENV\" == *\"mfusepy\"* ]]; then\n          sudo apt-get install -y libfuse3-dev fuse3  # Required for Python pyfuse3 module\n        fi\n\n    - name: Install macOS packages\n      if: ${{ runner.os == 'macOS' }}\n      run: |\n        brew unlink pkg-config@0.29.2 || true\n        brew bundle install\n\n    - name: Configure OpenSSH SFTP server (test only)\n      if: ${{ runner.os == 'Linux' && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}\n      run: |\n        sudo mkdir -p /run/sshd\n        sudo useradd -m -s /bin/bash sftpuser || true\n        # Create SSH key for the CI user and authorize it for sftpuser\n        mkdir -p ~/.ssh\n        chmod 700 ~/.ssh\n        test -f ~/.ssh/id_ed25519 || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519\n        sudo mkdir -p /home/sftpuser/.ssh\n        sudo chmod 700 /home/sftpuser/.ssh\n        sudo cp ~/.ssh/id_ed25519.pub /home/sftpuser/.ssh/authorized_keys\n        sudo chown -R sftpuser:sftpuser /home/sftpuser/.ssh\n        sudo chmod 600 /home/sftpuser/.ssh/authorized_keys\n        # Allow publickey auth and enable Subsystem sftp\n        sudo sed -i 's/^#\\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config\n        sudo sed -i 's/^#\\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config\n        if ! grep -q '^Subsystem sftp' /etc/ssh/sshd_config; then echo 'Subsystem sftp /usr/lib/openssh/sftp-server' | sudo tee -a /etc/ssh/sshd_config; fi\n        # Ensure host keys exist to avoid slow generation on first sshd start\n        sudo ssh-keygen -A\n        # Start sshd (listen on default 22 inside runner)\n        sudo /usr/sbin/sshd -D &\n        # Add host key to known_hosts so paramiko trusts it\n        ssh-keyscan -H localhost 127.0.0.1 | tee -a ~/.ssh/known_hosts\n        # Start ssh-agent and add our key so paramiko can use the agent\n        eval \"$(ssh-agent -s)\"\n        ssh-add ~/.ssh/id_ed25519\n        # Export SFTP test URL for tox via GITHUB_ENV\n        echo \"BORG_TEST_SFTP_REPO=sftp://sftpuser@localhost:22/borg/sftp-repo\" >> $GITHUB_ENV\n\n    - name: Install and configure MinIO S3 server (test only)\n      if: ${{ runner.os == 'Linux' && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}\n      run: |\n        set -e\n        arch=$(uname -m)\n        case \"$arch\" in\n          x86_64|amd64) srv_url=https://dl.min.io/server/minio/release/linux-amd64/minio; cli_url=https://dl.min.io/client/mc/release/linux-amd64/mc ;;\n          aarch64|arm64) srv_url=https://dl.min.io/server/minio/release/linux-arm64/minio; cli_url=https://dl.min.io/client/mc/release/linux-arm64/mc ;;\n          *) echo \"Unsupported arch: $arch\"; exit 1 ;;\n        esac\n        curl -fsSL -o /usr/local/bin/minio \"$srv_url\"\n        curl -fsSL -o /usr/local/bin/mc \"$cli_url\"\n        sudo chmod +x /usr/local/bin/minio /usr/local/bin/mc\n        export PATH=/usr/local/bin:$PATH\n        # Start MinIO on :9000 with default credentials (minioadmin/minioadmin)\n        MINIO_DIR=\"$GITHUB_WORKSPACE/.minio-data\"\n        MINIO_LOG=\"$GITHUB_WORKSPACE/.minio.log\"\n        mkdir -p \"$MINIO_DIR\"\n        nohup minio server \"$MINIO_DIR\" --address \":9000\" >\"$MINIO_LOG\" 2>&1 &\n        # Wait for MinIO port to be ready\n        for i in $(seq 1 60); do (echo > /dev/tcp/127.0.0.1/9000) >/dev/null 2>&1 && break; sleep 1; done\n        # Configure client and create bucket\n        mc alias set local http://127.0.0.1:9000 minioadmin minioadmin\n        mc mb --ignore-existing local/borg\n        # Export S3 test URL for tox via GITHUB_ENV\n        echo \"BORG_TEST_S3_REPO=s3:minioadmin:minioadmin@http://127.0.0.1:9000/borg/s3-repo\" >> $GITHUB_ENV\n\n    - name: Install Python requirements\n      run: |\n        python -m pip install --upgrade pip setuptools wheel\n        pip install -r requirements.d/development.lock.txt\n\n    - name: Install borgbackup\n      run: |\n        if [[ \"$TOXENV\" == *\"llfuse\"* ]]; then\n          pip install -ve \".[llfuse,cockpit,s3,sftp,rest,rclone]\"\n        elif [[ \"$TOXENV\" == *\"pyfuse3\"* ]]; then\n          pip install -ve \".[pyfuse3,cockpit,s3,sftp,rest,rclone]\"\n        elif [[ \"$TOXENV\" == *\"mfusepy\"* ]]; then\n          pip install -ve \".[mfusepy,cockpit,s3,sftp,rest,rclone]\"\n        else\n          pip install -ve \".[cockpit,s3,sftp,rest,rclone]\"\n        fi\n\n    - name: Build Borg fat binaries (${{ matrix.binary }})\n      if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}\n      run: |\n        pip install -r requirements.d/pyinstaller.txt\n        mkdir -p dist/binary\n        pyinstaller --clean --distpath=dist/binary scripts/borg.exe.spec\n\n    - name: Smoke-test the built binary (${{ matrix.binary }})\n      if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}\n      run: |\n        pushd dist/binary\n        echo \"single-file binary\"\n        chmod +x borg.exe\n        ./borg.exe -V\n        echo \"single-directory binary\"\n        chmod +x borg-dir/borg.exe\n        ./borg-dir/borg.exe -V\n        tar czf borg.tgz borg-dir\n        popd\n        # Ensure locally built binary in ./dist/binary/borg-dir is found during tests\n        export PATH=\"$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH\"\n        echo \"borg.exe binary in PATH\"\n        borg.exe -V\n\n    - name: Prepare binaries (${{ matrix.binary }})\n      if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}\n      run: |\n        mkdir -p artifacts\n        if [ -f dist/binary/borg.exe ]; then\n          cp dist/binary/borg.exe artifacts/${{ matrix.binary }}\n        fi\n        if [ -f dist/binary/borg.tgz ]; then\n          cp dist/binary/borg.tgz artifacts/${{ matrix.binary }}.tgz\n        fi\n        echo \"binary files\"\n        ls -l artifacts/\n\n    - name: Attest binaries provenance (${{ matrix.binary }})\n      if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}\n      uses: actions/attest-build-provenance@v4\n      with:\n        subject-path: 'artifacts/*'\n\n    - name: Upload binaries (${{ matrix.binary }})\n      if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}\n      uses: actions/upload-artifact@v7\n      with:\n        name: ${{ matrix.binary }}\n        path: artifacts/*\n        if-no-files-found: error\n\n    - name: run tox env\n      run: |\n        # do not use fakeroot, but run as root. avoids the dreaded EISDIR sporadic failures. see #2482.\n        #sudo -E bash -c \"tox -e py\"\n        # Ensure locally built binary in ./dist/binary/borg-dir is found during tests\n        export PATH=\"$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH\"\n        tox --skip-missing-interpreters\n\n    - name: Upload test results to Codecov\n      if: ${{ !cancelled() && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}\n      uses: codecov/codecov-action@v5\n      env:\n        OS: ${{ runner.os }}\n        python: ${{ matrix.python-version }}\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n        report_type: test_results\n        env_vars: OS,python\n        files: test-results.xml\n\n    - name: Upload coverage to Codecov\n      if: ${{ !cancelled() && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}\n      uses: codecov/codecov-action@v5\n      env:\n        OS: ${{ runner.os }}\n        python: ${{ matrix.python-version }}\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n        report_type: coverage\n        env_vars: OS,python\n        files: coverage.xml\n\n  vm_tests:\n    permissions:\n      contents: read\n      id-token: write\n      attestations: write\n    runs-on: ubuntu-24.04\n    timeout-minutes: 180\n    needs: [lint]\n    continue-on-error: true\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: freebsd\n            version: '14.3'\n            display_name: FreeBSD\n            # Controls binary build and provenance attestation on tags\n            do_binaries: true\n            artifact_prefix: borg-freebsd-14-x86_64-gh\n\n          - os: netbsd\n            version: '10.1'\n            display_name: NetBSD\n            do_binaries: false\n\n          - os: openbsd\n            version: '7.8'\n            display_name: OpenBSD\n            do_binaries: false\n\n          - os: omnios\n            version: 'r151056'\n            display_name: OmniOS\n            do_binaries: false\n\n          - os: haiku\n            version: 'r1beta5'\n            display_name: Haiku\n            do_binaries: false\n\n    steps:\n      - name: Check out repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          fetch-tags: true\n\n      - name: Test on ${{ matrix.display_name }}\n        id: cross_os\n        uses: cross-platform-actions/action@v0.32.0\n        env:\n          DO_BINARIES: ${{ matrix.do_binaries }}\n        with:\n          operating_system: ${{ matrix.os }}\n          version: ${{ matrix.version }}\n          shell: bash\n          run: |\n            set -euxo pipefail\n\n            case \"${{ matrix.os }}\" in\n              freebsd)\n                export IGNORE_OSVERSION=yes\n                sudo -E pkg update -f\n                sudo -E pkg install -y liblz4 pkgconf\n                sudo -E pkg install -y fusefs-libs\n                sudo -E kldload fusefs\n                sudo -E sysctl vfs.usermount=1\n                sudo -E chmod 666 /dev/fuse\n                sudo -E pkg install -y rust\n                sudo -E pkg install -y gmake\n                sudo -E pkg install -y git\n                sudo -E pkg install -y python310 py310-sqlite3\n                sudo -E pkg install -y python311 py311-sqlite3 py311-pip py311-virtualenv\n                sudo ln -sf /usr/local/bin/python3.11 /usr/local/bin/python3\n                sudo ln -sf /usr/local/bin/python3.11 /usr/local/bin/python\n                sudo ln -sf /usr/local/bin/pip3.11 /usr/local/bin/pip3\n                sudo ln -sf /usr/local/bin/pip3.11 /usr/local/bin/pip\n\n                # required for libsodium/pynacl build\n                export MAKE=gmake\n\n                python -m venv .venv\n                . .venv/bin/activate\n                python -V\n                pip -V\n                python -m pip install --upgrade pip wheel\n                pip install -r requirements.d/development.lock.txt\n                pip install -e \".[mfusepy,cockpit,s3,sftp,rest,rclone]\"\n                tox -e py311-mfusepy\n\n                if [[ \"${{ matrix.do_binaries }}\" == \"true\" && \"${{ startsWith(github.ref, 'refs/tags/') }}\" == \"true\" ]]; then\n                  python -m pip install -r requirements.d/pyinstaller.txt\n                  mkdir -p dist/binary\n                  pyinstaller --clean --distpath=dist/binary scripts/borg.exe.spec\n                  pushd dist/binary\n                  echo \"single-file binary\"\n                  chmod +x borg.exe\n                  ./borg.exe -V\n                  echo \"single-directory binary\"\n                  chmod +x borg-dir/borg.exe\n                  ./borg-dir/borg.exe -V\n                  tar czf borg.tgz borg-dir\n                  popd\n                  mkdir -p artifacts\n                  if [ -f dist/binary/borg.exe ]; then\n                    cp -v dist/binary/borg.exe artifacts/${{ matrix.artifact_prefix }}\n                  fi\n                  if [ -f dist/binary/borg.tgz ]; then\n                    cp -v dist/binary/borg.tgz artifacts/${{ matrix.artifact_prefix }}.tgz\n                  fi\n                fi\n                ;;\n\n              netbsd)\n                arch=\"$(uname -m)\"\n                sudo -E mkdir -p /usr/pkg/etc/pkgin\n                echo \"https://ftp.NetBSD.org/pub/pkgsrc/packages/NetBSD/${arch}/10.1/All\" | sudo tee /usr/pkg/etc/pkgin/repositories.conf > /dev/null\n                sudo -E pkgin update\n                sudo -E pkgin -y upgrade\n                sudo -E pkgin -y install lz4 git\n                sudo -E pkgin -y install rust\n                sudo -E pkgin -y install pkg-config\n                sudo -E pkgin -y install py311-pip py311-virtualenv py311-tox\n                sudo -E ln -sf /usr/pkg/bin/python3.11 /usr/pkg/bin/python3\n                sudo -E ln -sf /usr/pkg/bin/pip3.11 /usr/pkg/bin/pip3\n                sudo -E ln -sf /usr/pkg/bin/virtualenv-3.11 /usr/pkg/bin/virtualenv3\n                sudo -E ln -sf /usr/pkg/bin/tox-3.11 /usr/pkg/bin/tox3\n\n                # Ensure base system admin tools are on PATH for the non-root shell\n                export PATH=\"/sbin:/usr/sbin:$PATH\"\n\n                echo \"--- Preparing an extattr-enabled filesystem ---\"\n                # On many NetBSD setups /tmp is tmpfs without extended attributes.\n                # Create a FFS image with extended attributes enabled and use it for TMPDIR.\n                VNDDEV=\"vnd0\"\n                IMGFILE=\"/tmp/fs.img\"\n                sudo -E dd if=/dev/zero of=${IMGFILE} bs=1m count=1024\n                sudo -E vndconfig -c \"${VNDDEV}\" \"${IMGFILE}\"\n                sudo -E newfs -O 2ea /dev/r${VNDDEV}a\n                MNT=\"/mnt/eafs\"\n                sudo -E mkdir -p ${MNT}\n                sudo -E mount -t ffs -o extattr /dev/${VNDDEV}a $MNT\n                export TMPDIR=\"${MNT}/tmp\"\n                sudo -E mkdir -p ${TMPDIR}\n                sudo -E chmod 1777 ${TMPDIR}\n\n                touch ${TMPDIR}/testfile\n                lsextattr user ${TMPDIR}/testfile && echo \"[xattr] *** xattrs SUPPORTED on ${TMPDIR}! ***\"\n\n                tox3 -e py311-none\n                ;;\n\n              openbsd)\n                sudo -E pkg_add lz4 git\n                sudo -E pkg_add rust\n                sudo -E pkg_add openssl%3.5\n                sudo -E pkg_add py3-pip py3-virtualenv py3-tox\n\n                export BORG_OPENSSL_NAME=eopenssl35\n                tox -e py312-none\n                ;;\n\n              omnios)\n                sudo pkg install gcc14 git pkg-config python-313 gnu-make gnu-coreutils\n                sudo ln -sf /usr/bin/python3.13 /usr/bin/python3\n                sudo ln -sf /usr/bin/python3.13-config /usr/bin/python3-config\n                sudo python3 -m ensurepip\n                sudo python3 -m pip install virtualenv\n\n                python3 -m venv .venv\n                . .venv/bin/activate\n                python -V\n                pip -V\n                python -m pip install --upgrade pip wheel\n                pip install -r requirements.d/development.lock.txt\n                # no fuse support on omnios in our tests usually\n                pip install -e .\n\n                tox -e py313-none\n                ;;\n\n              haiku)\n                pkgman refresh\n                pkgman install -y git pkgconfig lz4\n                pkgman install -y openssl3\n                pkgman install -y rust_bin\n                pkgman install -y python3.10\n                pkgman install -y cffi\n                pkgman install -y lz4_devel openssl3_devel libffi_devel\n\n                # there is no pkgman package for tox, so we install it into a venv\n                python3 -m ensurepip --upgrade\n                python3 -m pip install --upgrade pip wheel\n                python3 -m venv .venv\n                . .venv/bin/activate\n\n                export PKG_CONFIG_PATH=\"/system/develop/lib/pkgconfig:/system/lib/pkgconfig:${PKG_CONFIG_PATH:-}\"\n                export BORG_LIBLZ4_PREFIX=/system/develop\n                export BORG_OPENSSL_PREFIX=/system/develop\n                pip install -r requirements.d/development.lock.txt\n                pip install -e .\n\n                # troubles with either tox or pytest xdist, so we run pytest manually:\n                pytest -v -n auto -rs --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml --benchmark-skip -k \"not remote and not socket\"\n                ;;\n            esac\n\n      - name: Upload artifacts\n        if: startsWith(github.ref, 'refs/tags/') && matrix.do_binaries\n        uses: actions/upload-artifact@v7\n        with:\n          name: ${{ matrix.artifact_prefix }}\n          path: artifacts/*\n          if-no-files-found: ignore\n\n      - name: Attest provenance\n        if: startsWith(github.ref, 'refs/tags/') && matrix.do_binaries\n        uses: actions/attest-build-provenance@v4\n        with:\n          subject-path: 'artifacts/*'\n\n      - name: Upload test results to Codecov\n        if: ${{ !cancelled() }}\n        uses: codecov/codecov-action@v5\n        env:\n          OS: ${{ matrix.os }}\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          report_type: test_results\n          env_vars: OS\n          files: test-results.xml\n\n      - name: Upload coverage to Codecov\n        if: ${{ !cancelled() }}\n        uses: codecov/codecov-action@v5\n        env:\n          OS: ${{ matrix.os }}\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          report_type: coverage\n          env_vars: OS\n          files: coverage.xml\n\n  windows_tests:\n\n    if: true  # can be used to temporarily disable the build\n    runs-on: windows-latest\n    timeout-minutes: 90\n    needs: [lint]\n\n    env:\n      PY_COLORS: 1\n      MSYS2_ARG_CONV_EXCL: \"*\"\n      MSYS2_ENV_CONV_EXCL: \"*\"\n\n    defaults:\n      run:\n        shell: msys2 {0}\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: msys2/setup-msys2@v2\n        with:\n          msystem: UCRT64\n          update: true\n\n      - name: Install system packages\n        run: ./scripts/msys2-install-deps development\n\n      - name: Build python venv\n        run: |\n          # building cffi / argon2-cffi in the venv fails, so we try to use the system packages\n          python -m venv --system-site-packages env\n          . env/bin/activate\n          # python -m pip install --upgrade pip\n          # pip install --upgrade setuptools build wheel\n          pip install -r requirements.d/pyinstaller.txt\n\n      - name: Build\n        run: |\n          # build borg.exe\n          . env/bin/activate\n          pip install -e \".[cockpit,s3,sftp,rest,rclone]\"\n          mkdir -p dist/binary\n          pyinstaller -y --clean --distpath=dist/binary scripts/borg.exe.spec\n          # build sdist and wheel in dist/...\n          python -m build\n\n      - uses: actions/upload-artifact@v7\n        with:\n          name: borg-windows\n          path: dist/binary/borg.exe\n\n      - name: Run tests\n        run: |\n          # Ensure locally built binary in ./dist/binary/borg-dir is found during tests\n          export PATH=\"$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH\"\n          borg.exe -V\n          . env/bin/activate\n          python -m pytest -n4 --benchmark-skip -vv -rs -k \"not remote\" --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml\n\n      - name: Upload test results to Codecov\n        if: ${{ !cancelled() }}\n        uses: codecov/codecov-action@v5\n        env:\n          OS: ${{ runner.os }}\n          python: '3.11'\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          report_type: test_results\n          env_vars: OS,python\n          files: test-results.xml\n\n      - name: Upload coverage to Codecov\n        if: ${{ !cancelled() }}\n        uses: codecov/codecov-action@v5\n        env:\n          OS: ${{ runner.os }}\n          python: '3.11'\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          report_type: coverage\n          env_vars: OS,python\n          files: coverage.xml\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# CodeQL semantic code analysis engine\n\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - '**.py'\n      - '**.pyx'\n      - '**.c'\n      - '**.h'\n      - '.github/workflows/codeql-analysis.yml'\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ master ]\n    paths:\n      - '**.py'\n      - '**.pyx'\n      - '**.c'\n      - '**.h'\n      - '.github/workflows/codeql-analysis.yml'\n  schedule:\n    - cron: '39 2 * * 5'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-24.04\n    timeout-minutes: 20\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'cpp', 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n      with:\n        # Just fetching one commit is not enough for setuptools-scm, so we fetch all.\n        fetch-depth: 0\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: 3.11\n    - name: Cache pip\n      uses: actions/cache@v5\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-pip-${{ hashFiles('requirements.d/development.txt') }}\n        restore-keys: |\n            ${{ runner.os }}-pip-\n            ${{ runner.os }}-\n    - name: Install requirements\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y pkg-config build-essential\n        sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n    - name: Build and install Borg\n      run: |\n        python3 -m venv ../borg-env\n        source ../borg-env/bin/activate\n        pip3 install -r requirements.d/development.txt\n        pip3 install -ve .\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "MANIFEST\ndocs/_build\nbuild\ndist\n.tox\nsrc/borg/compress.c\nsrc/borg/hashindex.c\nsrc/borg/crypto/low_level.c\nsrc/borg/item.c\nsrc/borg/chunkers/buzhash.c\nsrc/borg/chunkers/buzhash64.c\nsrc/borg/chunkers/reader.c\nsrc/borg/checksums.c\nsrc/borg/platform/darwin.c\nsrc/borg/platform/freebsd.c\nsrc/borg/platform/netbsd.c\nsrc/borg/platform/linux.c\nsrc/borg/platform/syncfilerange.c\nsrc/borg/platform/posix.c\nsrc/borg/platform/windows.c\nsrc/borg/_version.py\n*.egg-info\n*.pyc\n*.pyd\n*.so\n.idea/\n.junie/\n.vscode/\nborg.exe\n.coverage\n.coverage.*\ncoverage.xml\ntest-results.xml\n.vagrant\n.DS_Store\n"
  },
  {
    "path": ".mailmap",
    "content": "Abdel-Rahman <abodyxplay1@gmail.com>\nBrian Johnson <brian@sherbang.com>\nCarlo Teubner <carlo.teubner@gmail.com>\nMark Edgington <edgimar@gmail.com>\nLeo Famulari <leo@famulari.name>\nMarian Beermann <public@enkore.de>\nThomas Waldmann <tw@waldmann-edv.de>\nDan Christensen <jdc@uwo.ca> <jdc+github@uwo.ca>\nAntoine Beaupré <anarcat@koumbit.org> <anarcat@debian.org> <anarcat@users.noreply.github.com>\nHartmut Goebel <h.goebel@crazy-compilers.com> <htgoebel@users.noreply.github.com>\nMichael Gajda <michaelg@speciesm.net> <michael.gajda@tu-dortmund.de>\nMilkey Mouse <milkeymouse@meme.institute> <milkey-mouse@users.noreply.github.com>\nRonny Pfannschmidt <opensource@ronnypfannschmidt.de> <ronny.pfannschmidt@redhat.com>\nStefan Tatschner <rumpelsepp@sevenbyte.org> <stefan@sevenbyte.org>\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n-   repo: https://github.com/psf/black\n    rev: 24.8.0\n    hooks:\n    -   id: black\n- repo: https://github.com/astral-sh/ruff-pre-commit\n  rev: v0.15.0\n  hooks:\n    - id: ruff\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# .readthedocs.yaml - Read the Docs configuration file.\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details.\n\nversion: 2\n\nbuild:\n    os: ubuntu-22.04\n    tools:\n        python: \"3.11\"\n    jobs:\n        post_checkout:\n            - git fetch --unshallow\n    apt_packages:\n        - build-essential\n        - pkg-config\n        - libacl1-dev\n        - libssl-dev\n        - liblz4-dev\n\npython:\n    install:\n        - requirements: requirements.d/development.lock.txt\n        - requirements: requirements.d/docs.txt\n        - method: pip\n          path: .\n\nsphinx:\n    configuration: docs/conf.py\n\nformats:\n    - htmlzip\n"
  },
  {
    "path": "AUTHORS",
    "content": "Email addresses listed here are not intended for support.\nPlease see the `support section`_ instead.\n\n.. _support section: https://borgbackup.readthedocs.io/en/stable/support.html\n\nBorg authors (\"The Borg Collective\")\n------------------------------------\n\n- Thomas Waldmann <tw@waldmann-edv.de>\n- Radek Podgorny <radek@podgorny.cz>\n- Yuri D'Elia\n- Michael Hanselmann <public@hansmi.ch>\n- Teemu Toivanen <public@profnetti.fi>\n- Marian Beermann <public@enkore.de>\n- Martin Hostettler <textshell@uchuujin.de>\n- Daniel Reichelt <hacking@nachtgeist.net>\n- Lauri Niskanen <ape@ape3000.com>\n- Abdel-Rahman A. (Abogical)\n- Gu1nness <guinness@crans.org>\n- Andrey Andreyevich Bienkowski <hexagon-recursion@posteo.net>\n\nRetired\n```````\n\n- Antoine Beaupré <anarcat@debian.org>\n\nBorg is a fork of Attic.\n\nAttic authors\n-------------\n\nAttic is written and maintained by Jonas Borgström and various contributors:\n\nAttic Development Lead\n``````````````````````\n- Jonas Borgström <jonas@borgstrom.se>\n\nAttic Patches and Suggestions\n`````````````````````````````\n- Brian Johnson\n- Cyril Roussillon\n- Dan Christensen\n- Jeremy Maitin-Shepard\n- Johann Klähn\n- Petros Moisiadis\n- Thomas Waldmann\n"
  },
  {
    "path": "Brewfile",
    "content": "brew 'pkgconf'\nbrew 'lz4'\nbrew 'openssl@3'\n\n# osxfuse (aka macFUSE) is only required for \"borg mount\",\n# but won't work on GitHub Actions' workers.\n# It requires installing a kernel extension, so some users\n# may want it and some won't.\n\n#cask 'osxfuse'\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2015-2025 The Borg Collective (see AUTHORS file)\nCopyright (C) 2010-2014 Jonas Borgström <jonas@borgstrom.se>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n 1. Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n 2. Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n 3. The name of the author may not be used to endorse or promote\n    products derived from this software without specific prior\n    written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "# The files we need to include in the sdist are handled automatically by\n# setuptools_scm - it includes all git-committed files.\n# But we want to exclude some committed files/directories not needed in the sdist:\nexclude .editorconfig .gitattributes .gitignore .mailmap Vagrantfile\nprune .github\ninclude src/borg/platform/darwin.c src/borg/platform/freebsd.c src/borg/platform/linux.c src/borg/platform/posix.c\ninclude src/borg/platform/syncfilerange.c\ninclude src/borg/platform/windows.c\n"
  },
  {
    "path": "README.rst",
    "content": "This is borg2!\n--------------\n\nPlease note that this is the README for borg2 / master branch.\n\nFor the stable version's docs, please see here:\n\nhttps://borgbackup.readthedocs.io/en/stable/\n\nBorg2 is currently in beta testing and might get major and/or\nbreaking changes between beta releases (and there is no beta to\nnext-beta upgrade code, so you will have to delete and re-create repos).\n\nThus, **DO NOT USE BORG2 FOR YOUR PRODUCTION BACKUPS!** Please help with\ntesting it, but set it up *additionally* to your production backups.\n\nTODO: the screencasts need a remake using borg2, see here:\n\nhttps://github.com/borgbackup/borg/issues/6303\n\n\nWhat is BorgBackup?\n-------------------\n\nBorgBackup (short: Borg) is a deduplicating backup program.\nOptionally, it supports compression and authenticated encryption.\n\nThe main goal of Borg is to provide an efficient and secure way to back up data.\nThe data deduplication technique used makes Borg suitable for daily backups\nsince only changes are stored.\nThe authenticated encryption technique makes it suitable for backups to targets not\nfully trusted.\n\nSee the `installation manual`_ or, if you have already\ndownloaded Borg, ``docs/installation.rst`` to get started with Borg.\nThere is also an `offline documentation`_ available, in multiple formats.\n\n.. _installation manual: https://borgbackup.readthedocs.io/en/master/installation.html\n.. _offline documentation: https://readthedocs.org/projects/borgbackup/downloads\n\nMain features\n~~~~~~~~~~~~~\n\n**Space efficient storage**\n  Deduplication based on content-defined chunking is used to reduce the number\n  of bytes stored: each file is split into a number of variable length chunks\n  and only chunks that have never been seen before are added to the repository.\n\n  A chunk is considered duplicate if its id_hash value is identical.\n  A cryptographically strong hash or MAC function is used as id_hash, e.g.\n  (hmac-)sha256.\n\n  To deduplicate, all the chunks in the same repository are considered, no\n  matter whether they come from different machines, from previous backups,\n  from the same backup or even from the same single file.\n\n  Compared to other deduplication approaches, this method does NOT depend on:\n\n  * file/directory names staying the same: So you can move your stuff around\n    without killing the deduplication, even between machines sharing a repo.\n\n  * complete files or time stamps staying the same: If a big file changes a\n    little, only a few new chunks need to be stored - this is great for VMs or\n    raw disks.\n\n  * The absolute position of a data chunk inside a file: Stuff may get shifted\n    and will still be found by the deduplication algorithm.\n\n**Speed**\n  * performance-critical code (chunking, compression, encryption) is\n    implemented in C/Cython\n  * local caching\n  * quick detection of unmodified files\n\n**Data encryption**\n    All data can be protected client-side using 256-bit authenticated encryption\n    (AES-OCB or chacha20-poly1305), ensuring data confidentiality, integrity and\n    authenticity.\n\n**Obfuscation**\n    Optionally, Borg can actively obfuscate, e.g., the size of files/chunks to\n    make fingerprinting attacks more difficult.\n\n**Compression**\n    All data can be optionally compressed:\n\n    * lz4 (super fast, low compression)\n    * zstd (wide range from high speed and low compression to high compression\n      and lower speed)\n    * zlib (medium speed and compression)\n    * lzma (low speed, high compression)\n\n**Off-site backups**\n    Borg can store data on any remote host accessible over SSH. If Borg is\n    installed on the remote host, significant performance gains can be achieved\n    compared to using a network file system (sshfs, NFS, ...).\n\n**Backups mountable as file systems**\n    Backup archives are mountable as user-space file systems for easy interactive\n    backup examination and restores (e.g., by using a regular file manager).\n\n**Easy installation on multiple platforms**\n    We offer single-file binaries that do not require installing anything -\n    you can just run them on these platforms:\n\n    * Linux\n    * macOS\n    * FreeBSD\n    * OpenBSD and NetBSD (no xattrs/ACLs support or binaries yet)\n    * Cygwin (experimental, no binaries yet)\n    * Windows Subsystem for Linux (WSL) on Windows 10/11 (experimental)\n\n**Free and Open Source Software**\n  * security and functionality can be audited independently\n  * licensed under the BSD (3-clause) license, see `License`_ for the\n    complete license\n\nEasy to use\n~~~~~~~~~~~\n\nFor ease of use, set the BORG_REPO environment variable::\n\n    $ export BORG_REPO=/path/to/repo\n\nCreate a new backup repository (see ``borg repo-create --help`` for encryption options)::\n\n    $ borg repo-create -e repokey-aes-ocb\n\nCreate a new backup archive::\n\n    $ borg create Monday1 ~/Documents\n\nNow do another backup, just to show off the great deduplication::\n\n    $ borg create -v --stats Monday2 ~/Documents\n    Repository: /path/to/repo\n    Archive name: Monday2\n    Archive fingerprint: 7714aef97c1a24539cc3dc73f79b060f14af04e2541da33d54c7ee8e81a00089\n    Time (start): Mon, 2022-10-03 19:57:35 +0200\n    Time (end):   Mon, 2022-10-03 19:57:35 +0200\n    Duration: 0.01 seconds\n    Number of files: 24\n    Original size: 29.73 MB\n    Deduplicated size: 520 B\n\n\nHelping, donations and bounties, becoming a Patron\n--------------------------------------------------\n\nYour help is always welcome!\n\nSpread the word, give feedback, help with documentation, testing or development.\n\nYou can also give monetary support to the project, see here for details:\n\nhttps://www.borgbackup.org/support/fund.html\n\nLinks\n-----\n\n* `Main website <https://borgbackup.readthedocs.io/>`_\n* `Releases <https://github.com/borgbackup/borg/releases>`_,\n  `PyPI packages <https://pypi.org/project/borgbackup/>`_ and\n  `Changelog <https://github.com/borgbackup/borg/blob/master/docs/changes.rst>`_\n* `Offline documentation <https://readthedocs.org/projects/borgbackup/downloads>`_\n* `GitHub <https://github.com/borgbackup/borg>`_ and\n  `Issue tracker <https://github.com/borgbackup/borg/issues>`_.\n* `Web chat (IRC) <https://web.libera.chat/#borgbackup>`_ and\n  `Mailing list <https://mail.python.org/mailman/listinfo/borgbackup>`_\n* `License <https://borgbackup.readthedocs.io/en/master/authors.html#license>`_\n* `Security contact <https://borgbackup.readthedocs.io/en/master/support.html#security-contact>`_\n\nCompatibility notes\n-------------------\n\nEXPECT THAT WE WILL BREAK COMPATIBILITY REPEATEDLY WHEN MAJOR RELEASE NUMBER\nCHANGES (like when going from 0.x.y to 1.0.0 or from 1.x.y to 2.0.0).\n\nNOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES.\n\nTHIS IS SOFTWARE IN DEVELOPMENT, DECIDE FOR YOURSELF WHETHER IT FITS YOUR NEEDS.\n\nSecurity issues should be reported to the `Security contact`_ (or\nsee ``docs/support.rst`` in the source distribution).\n\n.. start-badges\n\n|doc| |build| |coverage| |bestpractices|\n\n.. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=master\n        :alt: Documentation\n        :target: https://borgbackup.readthedocs.io/en/master/\n\n.. |build| image:: https://github.com/borgbackup/borg/workflows/CI/badge.svg?branch=master\n        :alt: Build Status (master)\n        :target: https://github.com/borgbackup/borg/actions\n\n.. |coverage| image:: https://codecov.io/github/borgbackup/borg/coverage.svg?branch=master\n        :alt: Test Coverage\n        :target: https://codecov.io/github/borgbackup/borg?branch=master\n\n.. |screencast_basic| image:: https://asciinema.org/a/133292.png\n        :alt: BorgBackup Basic Usage\n        :target: https://asciinema.org/a/133292?autoplay=1&speed=1\n        :width: 100%\n\n.. _installation: https://asciinema.org/a/133291?autoplay=1&speed=1\n\n.. _advanced usage: https://asciinema.org/a/133293?autoplay=1&speed=1\n\n.. |bestpractices| image:: https://bestpractices.coreinfrastructure.org/projects/271/badge\n        :alt: Best Practices Score\n        :target: https://bestpractices.coreinfrastructure.org/projects/271\n\n.. end-badges\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nThese Borg releases are currently supported with security updates.\n\n| Version | Supported          |\n|---------|--------------------|\n| 2.0.x   | :x: (beta)         |\n| 1.4.x   | :white_check_mark: |\n| 1.2.x   | :x: (no new releases, critical fixes may still be backported) |\n| 1.1.x   | :x:                |\n| < 1.1   | :x:                |\n\n## Reporting a Vulnerability\n\nSee here:\n\nhttps://borgbackup.readthedocs.io/en/latest/support.html#security-contact\n"
  },
  {
    "path": "Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n# Automated creation of testing environments/binaries on miscellaneous platforms\n\n$cpus = Integer(ENV.fetch('VMCPUS', '8'))  # create VMs with that many cpus\n$xdistn = Integer(ENV.fetch('XDISTN', '8'))  # dispatch tests to that many pytest workers\n$wmem = $xdistn * 256  # give the VM additional memory for workers [MB]\n\ndef packages_debianoid(user)\n  return <<-EOF\n    export DEBIAN_FRONTEND=noninteractive\n    # this is to avoid grub asking about which device it should install to:\n    echo \"set grub-pc/install_devices /dev/sda\" | debconf-communicate\n    apt-get -y -qq update\n    apt-get -y -qq dist-upgrade\n    # for building borgbackup and dependencies:\n    apt install -y pkg-config\n    apt install -y libssl-dev libacl1-dev liblz4-dev || true\n    apt install -y libfuse-dev fuse || true\n    apt install -y libfuse3-dev fuse3 || true\n    apt install -y locales || true\n    sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen\n    usermod -a -G fuse #{user}\n    chgrp fuse /dev/fuse\n    chmod 666 /dev/fuse\n    apt install -y fakeroot build-essential git curl\n    apt install -y python3-dev python3-setuptools virtualenv\n    # for building python:\n    apt install -y zlib1g-dev libbz2-dev libncurses5-dev libreadline-dev liblzma-dev libsqlite3-dev libffi-dev\n  EOF\nend\n\ndef packages_freebsd\n  return <<-EOF\n    # in case the VM has no hostname set\n    hostname freebsd\n    # install all the (security and other) updates, base system\n    freebsd-update --not-running-from-cron fetch install\n    # for building borgbackup and dependencies:\n    pkg install -y liblz4 pkgconf\n    pkg install -y fusefs-libs || true\n    pkg install -y fusefs-libs3 || true\n    pkg install -y rust\n    pkg install -y git bash  # fakeroot causes lots of troubles on freebsd\n    pkg install -y python310 py310-sqlite3\n    pkg install -y python311 py311-sqlite3 py311-pip py311-virtualenv\n    # make sure there is a python3/pip3/virtualenv command\n    ln -sf /usr/local/bin/python3.11 /usr/local/bin/python3\n    ln -sf /usr/local/bin/pip-3.11 /usr/local/bin/pip3\n    ln -sf /usr/local/bin/virtualenv-3.11 /usr/local/bin/virtualenv\n    # make bash default / work:\n    chsh -s bash vagrant\n    mount -t fdescfs fdesc /dev/fd\n    echo 'fdesc        /dev/fd         fdescfs         rw      0       0' >> /etc/fstab\n    # make FUSE work\n    echo 'fuse_load=\"YES\"' >> /boot/loader.conf\n    echo 'vfs.usermount=1' >> /etc/sysctl.conf\n    kldload fusefs\n    sysctl vfs.usermount=1\n    pw groupmod operator -M vagrant\n    # /dev/fuse has group operator\n    chmod 666 /dev/fuse\n    # install all the (security and other) updates, packages\n    pkg update\n    yes | pkg upgrade\n    echo 'export BORG_OPENSSL_PREFIX=/usr' >> ~vagrant/.bash_profile\n    # (re)mount / with acls\n    mount -o acls /\n  EOF\nend\n\ndef packages_openbsd\n  return <<-EOF\n    hostname \"openbsd77.localdomain\"\n    echo \"$(hostname)\" > /etc/myname\n    echo \"127.0.0.1\tlocalhost\" > /etc/hosts\n    echo \"::1 localhost\" >> /etc/hosts\n    echo \"127.0.0.1\t$(hostname) $(hostname -s)\" >> /etc/hosts\n    echo \"https://ftp.eu.openbsd.org/pub/OpenBSD\" > /etc/installurl\n    ftp https://cdn.openbsd.org/pub/OpenBSD/$(uname -r)/$(uname -m)/comp$(uname -r | tr -d .).tgz\n    tar -C / -xzphf comp$(uname -r | tr -d .).tgz\n    rm comp$(uname -r | tr -d .).tgz\n    pkg_add bash\n    chsh -s bash vagrant\n    pkg_add lz4\n    pkg_add git  # no fakeroot\n    pkg_add rust\n    pkg_add openssl%3.4\n    pkg_add py3-pip\n    pkg_add py3-virtualenv\n    echo 'export BORG_OPENSSL_NAME=eopenssl30' >> ~vagrant/.bash_profile\n  EOF\nend\n\ndef packages_netbsd\n  return <<-EOF\n    echo 'https://ftp.NetBSD.org/pub/pkgsrc/packages/NetBSD/$arch/9.3/All' > /usr/pkg/etc/pkgin/repositories.conf\n    pkgin update\n    pkgin -y upgrade\n    pkg_add lz4 git\n    pkg_add rust\n    pkg_add bash\n    chsh -s bash vagrant\n    echo \"export PROMPT_COMMAND=\" >> ~vagrant/.bash_profile  # bug in netbsd 9.3, .bash_profile broken for screen\n    echo \"export PROMPT_COMMAND=\" >> ~root/.bash_profile  # bug in netbsd 9.3, .bash_profile broken for screen\n    pkg_add pkg-config\n    # pkg_add fuse  # llfuse supports netbsd, but is still buggy.\n    # https://bitbucket.org/nikratio/python-llfuse/issues/70/perfuse_open-setsockopt-no-buffer-space\n    pkg_add py311-sqlite3 py311-pip py311-virtualenv py311-expat\n    ln -s /usr/pkg/bin/python3.11 /usr/pkg/bin/python\n    ln -s /usr/pkg/bin/python3.11 /usr/pkg/bin/python3\n    ln -s /usr/pkg/bin/pip3.11 /usr/pkg/bin/pip\n    ln -s /usr/pkg/bin/pip3.11 /usr/pkg/bin/pip3\n    ln -s /usr/pkg/bin/virtualenv-3.11 /usr/pkg/bin/virtualenv\n    ln -s /usr/pkg/bin/virtualenv-3.11 /usr/pkg/bin/virtualenv3\n  EOF\nend\n\ndef package_update_openindiana\n  return <<-EOF\n    echo \"nameserver 1.1.1.1\" > /etc/resolv.conf\n    # needs separate provisioning step + reboot to become effective:\n    pkg update\n  EOF\nend\n\ndef packages_openindiana\n  return <<-EOF\n    pkg install gcc-13 git\n    pkg install pkg-config\n    pkg install python-313\n    ln -sf /usr/bin/python3.13 /usr/bin/python3\n    ln -sf /usr/bin/python3.13-config /usr/bin/python3-config\n    python3 -m ensurepip\n    ln -sf /usr/bin/pip3.13 /usr/bin/pip3\n    pip3 install virtualenv\n    # let borg's pkg-config find openssl:\n    pfexec pkg set-mediator -V 3 openssl\n  EOF\nend\n\ndef install_pyenv(boxname)\n  return <<-EOF\n    echo 'export PYTHON_CONFIGURE_OPTS=\"${PYTHON_CONFIGURE_OPTS} --enable-shared\"' >> ~/.bash_profile\n    echo 'export PYENV_ROOT=\"$HOME/.pyenv\"' >> ~/.bash_profile\n    echo 'export PATH=\"$PYENV_ROOT/bin:$PATH\"' >> ~/.bash_profile\n    . ~/.bash_profile\n    curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash\n    echo 'eval \"$(pyenv init --path)\"' >> ~/.bash_profile\n    echo 'export PYENV_ROOT=\"$HOME/.pyenv\"' >> ~/.bashrc\n    echo 'export PATH=\"$PYENV_ROOT/bin:$PATH\"' >> ~/.bashrc\n    echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc\n    echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc\n  EOF\nend\n\ndef install_pythons(boxname)\n  return <<-EOF\n    . ~/.bash_profile\n    echo \"PYTHON_CONFIGURE_OPTS: ${PYTHON_CONFIGURE_OPTS}\"\n    pyenv install 3.13.8\n    pyenv rehash\n  EOF\nend\n\ndef build_sys_venv(boxname)\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg\n    virtualenv --python=python3 borg-env\n  EOF\nend\n\ndef build_pyenv_venv(boxname)\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg\n    # use the latest 3.13 release\n    pyenv global 3.13.8\n    pyenv virtualenv 3.13.8 borg-env\n    ln -s ~/.pyenv/versions/borg-env .\n  EOF\nend\n\ndef install_borg(fuse)\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg\n    . borg-env/bin/activate\n    pip install -U wheel  # upgrade wheel, might be too old\n    cd borg\n    pip install -r requirements.d/development.lock.txt\n    python3 scripts/make.py clean\n    # install borgstore WITH all options, so it pulls in the needed\n    # requirements, so they will also get into the binaries built. #8574\n    pip install borgstore[sftp,s3]\n    pip install -e .[#{fuse}]\n  EOF\nend\n\ndef install_pyinstaller()\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg\n    . borg-env/bin/activate\n    pip install -r requirements.d/pyinstaller.txt\n  EOF\nend\n\ndef build_binary_with_pyinstaller(boxname)\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg\n    . borg-env/bin/activate\n    cd borg\n    pyinstaller --clean --distpath=/vagrant/borg scripts/borg.exe.spec\n    echo 'export PATH=\"/vagrant/borg:$PATH\"' >> ~/.bash_profile\n    cd .. && tar -czvf borg.tgz borg-dir\n  EOF\nend\n\ndef run_tests(boxname, skip_env)\n  return <<-EOF\n    . ~/.bash_profile\n    cd /vagrant/borg/borg\n    . ../borg-env/bin/activate\n    if which pyenv 2> /dev/null; then\n      # for testing, use the earliest point releases of the supported python versions:\n      pyenv global 3.13.8\n      pyenv local 3.13.8\n    fi\n    # otherwise: just use the system python\n    # some OSes can only run specific test envs, e.g. because they miss FUSE support:\n    export TOX_SKIP_ENV='#{skip_env}'\n    if which fakeroot 2> /dev/null; then\n      echo \"Running tox WITH fakeroot -u\"\n      fakeroot -u tox --skip-missing-interpreters\n    else\n      echo \"Running tox WITHOUT fakeroot -u\"\n      tox --skip-missing-interpreters\n    fi\n  EOF\nend\n\ndef fs_init(user)\n  return <<-EOF\n    # clean up (wrong/outdated) stuff we likely got via rsync:\n    rm -rf /vagrant/borg/borg/.tox 2> /dev/null\n    rm -rf /vagrant/borg/borg/borgbackup.egg-info 2> /dev/null\n    rm -rf /vagrant/borg/borg/__pycache__ 2> /dev/null\n    find /vagrant/borg/borg/src -name '__pycache__' -exec rm -rf {} \\\\; 2> /dev/null\n    chown -R #{user} /vagrant/borg\n    touch ~#{user}/.bash_profile ; chown #{user} ~#{user}/.bash_profile\n    echo 'export LANG=en_US.UTF-8' >> ~#{user}/.bash_profile\n    echo 'export LC_CTYPE=en_US.UTF-8' >> ~#{user}/.bash_profile\n    echo 'export XDISTN=#{$xdistn}' >> ~#{user}/.bash_profile\n  EOF\nend\n\nVagrant.configure(2) do |config|\n  # use rsync to copy content to the folder\n  config.vm.synced_folder \".\", \"/vagrant/borg/borg\", :type => \"rsync\", :rsync__args => [\"--verbose\", \"--archive\", \"--delete\", \"--exclude\", \".python-version\"], :rsync__chown => false\n  # do not let the VM access . on the host machine via the default shared folder!\n  config.vm.synced_folder \".\", \"/vagrant\", disabled: true\n\n  config.vm.provider :virtualbox do |v|\n    #v.gui = true\n    v.cpus = $cpus\n  end\n\n  config.vm.define \"noble\" do |b|\n    b.vm.box = \"bento/ubuntu-24.04\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_sys_venv(\"noble\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"noble\", \".*none.*\")\n  end\n\n  config.vm.define \"jammy\" do |b|\n    b.vm.box = \"ubuntu/jammy64\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_sys_venv(\"jammy\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"jammy\", \".*none.*\")\n  end\n\n  config.vm.define \"trixie\" do |b|\n    b.vm.box = \"debian/testing64\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"trixie\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"trixie\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"trixie\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"trixie\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"trixie\", \".*none.*\")\n  end\n\n  config.vm.define \"bookworm32\" do |b|\n    b.vm.box = \"generic-x32/debian12\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"bookworm32\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"bookworm32\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"bookworm32\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"bookworm32\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"bookworm32\", \".*none.*\")\n  end\n\n  config.vm.define \"bookworm\" do |b|\n    b.vm.box = \"debian/bookworm64\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"bookworm\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"bookworm\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"bookworm\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"bookworm\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"bookworm\", \".*none.*\")\n  end\n\n  config.vm.define \"bullseye\" do |b|\n    b.vm.box = \"debian/bullseye64\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages debianoid\", :type => :shell, :inline => packages_debianoid(\"vagrant\")\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"bullseye\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"bullseye\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"bullseye\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"bullseye\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"bullseye\", \".*none.*\")\n  end\n\n  config.vm.define \"freebsd13\" do |b|\n    b.vm.box = \"generic/freebsd13\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.ssh.shell = \"sh\"\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages freebsd\", :type => :shell, :inline => packages_freebsd\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"freebsd13\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"freebsd13\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"freebsd13\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"freebsd13\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"freebsd13\", \".*(pyfuse3|none).*\")\n  end\n\n  config.vm.define \"freebsd14\" do |b|\n    b.vm.box = \"generic/freebsd14\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.ssh.shell = \"sh\"\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages freebsd\", :type => :shell, :inline => packages_freebsd\n    b.vm.provision \"install pyenv\", :type => :shell, :privileged => false, :inline => install_pyenv(\"freebsd14\")\n    b.vm.provision \"install pythons\", :type => :shell, :privileged => false, :inline => install_pythons(\"freebsd14\")\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_pyenv_venv(\"freebsd14\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"llfuse\")\n    b.vm.provision \"install pyinstaller\", :type => :shell, :privileged => false, :inline => install_pyinstaller()\n    b.vm.provision \"build binary with pyinstaller\", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller(\"freebsd14\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"freebsd14\", \".*(pyfuse3|none).*\")\n  end\n\n  config.vm.define \"openbsd7\" do |b|\n    b.vm.box = \"l3system/openbsd77-amd64\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 1024 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages openbsd\", :type => :shell, :inline => packages_openbsd\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_sys_venv(\"openbsd7\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"nofuse\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"openbsd7\", \".*fuse.*\")\n  end\n\n  config.vm.define \"netbsd9\" do |b|\n    b.vm.box = \"generic/netbsd9\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 4096 + $wmem  # need big /tmp tmpfs in RAM!\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"packages netbsd\", :type => :shell, :inline => packages_netbsd\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_sys_venv(\"netbsd9\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"nofuse\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"netbsd9\", \".*fuse.*\")\n  end\n\n  # rsync on openindiana has troubles, does not set correct owner for /vagrant/borg and thus gives lots of\n  # permission errors. can be manually fixed in the VM by: sudo chown -R vagrant /vagrant/borg ; then rsync again.\n  config.vm.define \"openindiana\" do |b|\n    b.vm.box = \"openindiana/hipster\"\n    b.vm.provider :virtualbox do |v|\n      v.memory = 2048 + $wmem\n    end\n    b.vm.provision \"fs init\", :type => :shell, :inline => fs_init(\"vagrant\")\n    b.vm.provision \"package update openindiana\", :type => :shell, :inline => package_update_openindiana, :reboot => true\n    b.vm.provision \"packages openindiana\", :type => :shell, :inline => packages_openindiana\n    b.vm.provision \"build env\", :type => :shell, :privileged => false, :inline => build_sys_venv(\"openindiana\")\n    b.vm.provision \"install borg\", :type => :shell, :privileged => false, :inline => install_borg(\"nofuse\")\n    b.vm.provision \"run tests\", :type => :shell, :privileged => false, :inline => run_tests(\"openindiana\", \".*fuse.*\")\n  end\nend\n"
  },
  {
    "path": "docs/3rd_party/README",
    "content": "Here we store third-party documentation, licenses, etc.\n\nPlease note that all files inside the \"borg\" package directory (except those\nexcluded in setup.py) will be installed, so do not keep docs or licenses\nthere.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and an HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\t-rm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/borg.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/borg.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/borg\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/borg\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\tmake -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n"
  },
  {
    "path": "docs/_static/Makefile",
    "content": "\nall: logo.pdf logo.png\n\nlogo.pdf: logo.svg\n\tinkscape logo.svg --export-pdf=logo.pdf\n\nlogo.png: logo.svg\n\tinkscape logo.svg --export-png=logo.png --export-dpi=72,72\n\nclean:\n\trm -f logo.pdf logo.png\n"
  },
  {
    "path": "docs/_static/logo_font.txt",
    "content": "Black Ops One\nJames Grieshaber\nSIL Open Font License, 1.1\n\nhttps://www.google.com/fonts/specimen/Black+Ops+One\n"
  },
  {
    "path": "docs/_templates/globaltoc.html",
    "content": "<div class=\"sidebar-block\">\n  <div class=\"sidebar-toc\">\n    {# Restrict the sidebar ToC depth to two levels while generating command usage pages.\n       This avoids superfluous entries for each \"Description\" and \"Examples\" heading. #}\n    {% if pagename.startswith(\"usage/\") and pagename not in (\n      \"usage/general\", \"usage/help\", \"usage/debug\", \"usage/notes\",\n    ) %}\n      {% set maxdepth = 2 %}\n    {% else %}\n      {% set maxdepth = 3 %}\n    {% endif %}\n\n    {% set toctree = toctree(maxdepth=maxdepth, collapse=True) %}\n    {% if toctree %}\n      {{ toctree }}\n    {% else %}\n      {{ toc }}\n    {% endif %}\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_templates/layout.html",
    "content": "{%- extends \"basic/layout.html\" %}\n\n{# Do this so that Bootstrap is included before the main CSS file. #}\n{%- block htmltitle %}\n  {% set script_files = script_files + [\"_static/myscript.js\"] %}\n  <!-- Licensed under the Apache 2.0 License -->\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"{{ pathto('_static/fonts/open-sans/stylesheet.css', 1) }}\" />\n  <!-- Licensed under the SIL Open Font License -->\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"{{ pathto('_static/fonts/source-serif-pro/source-serif-pro.css', 1) }}\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"{{ pathto('_static/css/bootstrap.min.css', 1) }}\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"{{ pathto('_static/css/bootstrap-theme.min.css', 1) }}\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  {{ super() }}\n{%- endblock %}\n\n{%- block extrahead %}\n  {% if theme_touch_icon %}\n  <link rel=\"apple-touch-icon\" href=\"{{ pathto('_static/' ~ theme_touch_icon, 1) }}\" />\n  {% endif %}\n  <meta name=\"readthedocs-addons-api-version\" content=\"1\" />\n  {{ super() }}\n{% endblock %}\n\n{# Displays the URL for the homepage if it's set, or the master_doc if it is not. #}\n{% macro homepage() -%}\n  {%- if theme_homepage %}\n    {%- if hasdoc(theme_homepage) %}\n      {{ pathto(theme_homepage) }}\n    {%- else %}\n      {{ theme_homepage }}\n    {%- endif %}\n  {%- else %}\n    {{ pathto(master_doc) }}\n  {%- endif %}\n{%- endmacro %}\n\n{# Displays the URL for the tospage if it's set, or falls back to the homepage macro. #}\n{% macro tospage() -%}\n  {%- if theme_tospage %}\n    {%- if hasdoc(theme_tospage) %}\n      {{ pathto(theme_tospage) }}\n    {%- else %}\n      {{ theme_tospage }}\n    {%- endif %}\n  {%- else %}\n    {{ homepage() }}\n  {%- endif %}\n{%- endmacro %}\n\n{# Displays the URL for the projectpage if it's set, or falls back to the homepage macro. #}\n{% macro projectlink() -%}\n  {%- if theme_projectlink %}\n    {%- if hasdoc(theme_projectlink) %}\n      {{ pathto(theme_projectlink) }}\n    {%- else %}\n      {{ theme_projectlink }}\n    {%- endif %}\n  {%- else %}\n    {{ homepage() }}\n  {%- endif %}\n{%- endmacro %}\n\n{# Displays the next and previous links both before and after the content. #}\n{% macro render_relations() -%}\n  {% if prev or next %}\n  <div class=\"footer-relations\">\n    {% if prev %}\n      <div class=\"pull-left\">\n        <a class=\"btn btn-default\" href=\"{{ prev.link|e }}\" title=\"{{ _('previous chapter')}} (use the left arrow)\">{{ prev.title }}</a>\n      </div>\n    {% endif %}\n    {%- if next and next.title != '&lt;no title&gt;' %}\n      <div class=\"pull-right\">\n        <a class=\"btn btn-default\" href=\"{{ next.link|e }}\" title=\"{{ _('next chapter')}} (use the right arrow)\">{{ next.title }}</a>\n      </div>\n    {%- endif %}\n    </div>\n    <div class=\"clearer\"></div>\n  {% endif %}\n{%- endmacro %}\n\n{%- macro guzzle_sidebar() %}\n  <div id=\"left-column\">\n    <div class=\"sphinxsidebar\">\n      {%- if sidebars != None %}\n        {#- New-style sidebar: explicitly include/exclude templates. #}\n        {%- for sidebartemplate in sidebars %}\n        {%- include sidebartemplate %}\n        {%- endfor %}\n      {% else %}\n        {% include \"logo-text.html\" %}\n        {% include \"globaltoc.html\" %}\n        {% include \"searchbox.html\" %}\n      {%- endif %}\n    </div>\n  </div>\n{%- endmacro %}\n\n{%- block content %}\n\n  {%- if pagename == 'index' and theme_index_template %}\n    {% include theme_index_template %}\n  {%- else %}\n    <div class=\"container-wrapper\">\n\n      <div id=\"mobile-toggle\">\n        <a href=\"#\"><span class=\"glyphicon glyphicon-align-justify\" aria-hidden=\"true\"></span></a>\n      </div>\n\n      {%- block sidebar1 %}{{ guzzle_sidebar() }}{% endblock %}\n\n      {%- block document_wrapper %}\n        {%- block document %}\n        <div id=\"right-column\">\n          {% block breadcrumbs %}\n          <div role=\"navigation\" aria-label=\"breadcrumbs navigation\">\n            <ol class=\"breadcrumb\">\n              <li><a href=\"{{ pathto(master_doc) }}\">Docs</a></li>\n              {% for doc in parents %}\n                <li><a href=\"{{ doc.link|e }}\">{{ doc.title }}</a></li>\n              {% endfor %}\n              <li>{{ title }}</li>\n            </ol>\n          </div>\n          {% endblock %}\n          <div class=\"document clearer body\" role=\"main\">\n            {% block body %} {% endblock %}\n          </div>\n          {%- block bottom_rel_links %}\n            {{ render_relations() }}\n          {%- endblock %}\n        </div>\n        <div class=\"clearfix\"></div>\n        {%- endblock %}\n      {%- endblock %}\n\n      {%- block comments -%}\n        {% if theme_disqus_comments_shortname %}\n        <div class=\"container comment-container\">\n          {% include \"comments.html\" %}\n        </div>\n        {% endif %}\n      {%- endblock %}\n    </div>\n  {%- endif %}\n  {%- endblock %}\n\n{%- block footer %}\n<script type=\"text/javascript\">\n  $(\"#mobile-toggle a\").click(function () {\n    $(\"#left-column\").toggle();\n  });\n</script>\n<script type=\"text/javascript\" src=\"{{ pathto('_static/js/bootstrap.js', 1)}}\"></script>\n{%- block footer_wrapper %}\n  <div class=\"footer\">\n    &copy; Copyright {{ copyright }}. Created using <a href=\"http://sphinx.pocoo.org/\">Sphinx</a>.\n  </div>\n{%- endblock %}\n{%- block ga %}\n  {%- if theme_google_analytics_account %}\n    <script type=\"text/javascript\">\n      var _gaq = _gaq || [];\n      _gaq.push(['_setAccount', '{{ theme_google_analytics_account }}']);\n      _gaq.push(['_trackPageview']);\n      (function() {\n        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\n        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\n        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\n      })();\n    </script>\n  {%- endif %}\n{%- endblock %}\n{%- endblock %}\n"
  },
  {
    "path": "docs/_templates/logo-text.html",
    "content": "<a href=\"{{ homepage() }}\" class=\"text-logo\">\n  <img src='{{ pathto('_static/logo.svg', 1) }}' width='100%'>\n\n  {{ theme_project_nav_name or shorttitle }}\n</a>\n"
  },
  {
    "path": "docs/_templates/versionselector.html",
    "content": "<div class=\"version-selector\" id=\"borg-version-selector\" style=\"display:none;\">\n  <label for=\"version-select\">Select your Borg version:</label>\n  <select id=\"version-select\"></select>\n</div>\n<script type=\"text/javascript\">\n  // Populate the version selector using ReadTheDocs data if available.\n  function borgInitVersionSelector(data) {\n    var versions = data && data.versions && data.versions.active;\n    if (!versions || !versions.length) return;\n    var current = data.versions && data.versions.current && data.versions.current.slug;\n    var select = document.getElementById(\"version-select\");\n    if (!select) return;\n    versions.forEach(function(v) {\n      var opt = document.createElement(\"option\");\n      opt.value = v.urls.documentation;\n      opt.textContent = v.slug;\n      if (v.slug === current) opt.selected = true;\n      select.appendChild(opt);\n    });\n    select.addEventListener(\"change\", function() {\n      window.location.href = this.value;\n    });\n    document.getElementById(\"borg-version-selector\").style.display = \"\";\n  }\n\n  document.addEventListener(\"readthedocs-addons-data-ready\", function(event) {\n    borgInitVersionSelector(event.detail.data());\n  });\n</script>\n"
  },
  {
    "path": "docs/authors.rst",
    "content": ".. include:: global.rst.inc\n\nAuthors\n=======\n\n.. include:: ../AUTHORS\n\nLicense\n=======\n\n.. _license:\n\n.. include:: ../LICENSE\n   :literal:\n"
  },
  {
    "path": "docs/binaries/00_README.txt",
    "content": "Binary BorgBackup builds\n========================\n\nGeneral notes\n-------------\n\nThe binaries are supposed to work on the specified platform without installing anything else.\n\nThere are some limitations, though:\n- for Linux, your system must have the same or newer glibc version as the one used for building\n- for macOS, you need to have the same or newer macOS version as the one used for building\n- for other OSes, there are likely similar limitations\n\nIf you don't find something working on your system, check the older borg releases.\n\n*.asc are GnuPG signatures - only provided for locally built binaries.\n*.exe (or no extension) is the single-file fat binary.\n*.tgz is the single-directory fat binary (extract it once with tar -xzf).\n\nUsing the single-directory build is faster and does not require as much space\nin the temporary directory as the self-extracting single-file build.\n\nmacOS: to avoid issues, download the file via the command line OR remove the\n       \"quarantine\" attribute after downloading:\n       $ xattr -dr com.apple.quarantine borg-macos1012.tgz\n\n\nDownload the correct files\n--------------------------\n\nBinaries built on GitHub servers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nborg-linux-glibc235-x86_64-gh Linux AMD/Intel (built on Ubuntu 22.04 LTS with glibc 2.35)\nborg-linux-glibc235-arm64-gh  Linux ARM (built on Ubuntu 22.04 LTS with glibc 2.35)\n\nborg-macos-15-arm64-gh        macOS Apple Silicon (built on macOS 15 w/o FUSE support)\nborg-macos-15-x86_64-gh       macOS Intel (built on macOS 15 w/o FUSE support)\n\nborg-freebsd-14-x86_64-gh     FreeBSD AMD/Intel (built on FreeBSD 14)\n\nBinaries built locally\n~~~~~~~~~~~~~~~~~~~~~~\n\nborg-linux-glibc231-x86_64 Linux (built on Debian 11 \"Bullseye\" with glibc 2.31)\n\nNote: if you don't find a specific binary here, check release 1.4.1 or 1.2.9.\n\nVerifying your download\n-----------------------\n\nI provide GPG signatures for files which I have built locally on my machines.\n\nTo check the GPG signature, download both the file and the corresponding\nsignature (*.asc file) and then (on the shell) type, for example:\n\n    gpg --recv-keys 9F88FB52FAF7B393\n    gpg --verify borgbackup.tar.gz.asc borgbackup.tar.gz\n\nThe files are signed by:\n\nThomas Waldmann <tw@waldmann-edv.de>\nGPG key fingerprint: 6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393\n\nMy fingerprint is also in the footer of all my BorgBackup mailing list posts.\n\n\nProvenance attestations for GitHub-built binaries\n-------------------------------------------------\n\nFor binaries built on GitHub (files with a \"-gh\" suffix in the name), we publish\nan artifact provenance attestation that proves the binary was built by our\nGitHub Actions workflow from a specific commit or tag. You can verify this using\nthe GitHub CLI (gh). Install it from https://cli.github.com/ and make sure you\nuse a recent version that supports \"gh attestation\".\n\nPractical example (Linux, 2.0.0b20 tag):\n\n    curl -LO https://github.com/borgbackup/borg/releases/download/2.0.0b20/borg-linux-glibc235-x86_64-gh\n    gh attestation verify --repo borgbackup/borg --source-ref refs/tags/2.0.0b20 borg-linux-glibc235-x86_64-gh\n\nIf verification succeeds, gh prints a summary stating the subject (your file),\nthat it was attested by GitHub Actions, and the job/workflow reference.\n\n\nInstalling\n----------\n\nIt is suggested that you rename or symlink the binary to just \"borg\".\nIf you need \"borgfs\", just also symlink it to the same binary; it will\ndetect internally under which name it was invoked.\n\nOn UNIX-like platforms, /usr/local/bin/ or ~/bin/ is a nice place for it,\nbut you can invoke it from anywhere by providing the full path to it.\n\nMake sure the file is readable and executable (chmod +rx borg on UNIX-like\nplatforms).\n\n\nReporting issues\n----------------\n\nPlease first check the FAQ and whether a GitHub issue already exists.\n\nIf you find a NEW issue, please open a ticket on our issue tracker:\n\nhttps://github.com/borgbackup/borg/issues/\n\nThere, please give:\n- the version number (it is displayed if you invoke borg -V)\n- the sha256sum of the binary\n- a good description of what the issue is\n- a good description of how to reproduce your issue\n- a traceback with system info (if you have one)\n- your precise platform (CPU, 32/64-bit?), OS, distribution, release\n- your Python and (g)libc versions\n\n"
  },
  {
    "path": "docs/book.rst",
    "content": ":orphan:\n\n.. include:: global.rst.inc\n\nBorg documentation\n==================\n\n.. When you add an element here, do not forget to add it to index.rst.\n.. Note: Some things are in appendices (see latex_appendices in conf.py).\n\n.. toctree::\n    :maxdepth: 2\n\n    introduction\n    installation\n    quickstart\n    usage\n    deployment\n    faq\n    support\n    changes\n    internals\n    development\n    authors\n"
  },
  {
    "path": "docs/borg_theme/css/borg.css",
    "content": "@import url(\"theme.css\");\n\ndt code {\n    font-weight: normal;\n}\n\n#internals .toctree-wrapper > ul {\n    column-count: 3;\n    -webkit-column-count: 3;\n}\n\n#internals .toctree-wrapper > ul > li {\n    display: inline-block;\n    font-weight: bold;\n}\n\n#internals .toctree-wrapper > ul > li > ul {\n    font-weight: normal;\n}\n\n/* bootstrap has a .container class which clashes with docutils' container class. */\n.docutils.container {\n    width: auto;\n    margin: 0;\n    padding: 0;\n}\n\n/* the default (38px) produces a jumpy baseline in Firefox on Linux. */\nh1 {\n    font-size: 36px;\n}\n\n.text-logo {\n    background-color: #000200;\n    color: #00dd00;\n}\n\n.text-logo:hover,\n.text-logo:active,\n.text-logo:focus {\n    color: #5afe57;\n}\n\n/* by default the top and bottom margins are unequal which looks a bit unbalanced. */\n.sidebar-block {\n    padding: 0;\n    margin: 14px 0 24px 0;\n}\n\n#borg-documentation h1 + p .external img {\n    width: 100%;\n}\n\n.container.experimental,\n#debugging-facilities {\n    /* don't change text dimensions */\n    margin: 0 -30px; /* padding below + border width */\n    padding: 0 10px; /* 10 px visual margin between edge of text and the border */\n    /* fallback for browsers that don't have repeating-linear-gradient: thick, red lines */\n    border-left: 20px solid red;\n    border-right: 20px solid red;\n    /* fancy red stripes */\n    border-image: repeating-linear-gradient(\n            -45deg,rgba(255,0,0,0.1) 0,rgba(255,0,0,0.75) 10px,rgba(0,0,0,0) 10px,rgba(0,0,0,0) 20px,rgba(255,0,0,0.75) 20px) 0 20 repeat;\n}\n\n.topic {\n    margin: 0 1em;\n    padding: 0 1em;\n    /* #4e4a4a = background of the ToC sidebar */\n    border-left: 2px solid #4e4a4a;;\n    border-right: 2px solid #4e4a4a;;\n}\n\ntable.docutils:not(.footnote) td,\ntable.docutils:not(.footnote) th {\n    padding: .2em;\n}\n\ntable.docutils:not(.footnote) {\n    border-collapse: collapse;\n    border: none;\n}\n\ntable.docutils:not(.footnote) td,\ntable.docutils:not(.footnote) th {\n    border: 1px solid #ddd;\n}\n\ntable.docutils:not(.footnote) tr:first-child th,\ntable.docutils:not(.footnote) tr:first-child td {\n    border-top: 0;\n}\n\ntable.docutils:not(.footnote) tr:last-child td {\n    border-bottom: 0;\n}\n\ntable.docutils:not(.footnote) tr td:first-child,\ntable.docutils:not(.footnote) tr th:first-child {\n    border-left: 0;\n}\n\ntable.docutils:not(.footnote) tr td:last-child,\ntable.docutils:not(.footnote) tr th:last-child,\ntable.docutils.borg-options-table tr td {\n    border-right: 0;\n}\n\ntable.docutils.option-list tr td,\ntable.docutils.borg-options-table tr td {\n    border-left: 0;\n    border-right: 0;\n}\n\ntable.docutils.borg-options-table tr td:first-child:not([colspan=\"3\"]) {\n    border-top: 0;\n    border-bottom: 0;\n}\n\n.borg-options-table td[colspan=\"3\"] p {\n    margin: 0;\n}\n\n.borg-options-table {\n    width: 100%;\n}\n\nkbd, /* used in usage pages for options */\ncode,\n.rst-content tt.literal,\n.rst-content tt.literal,\n.rst-content code.literal,\n.rst-content tt,\n.rst-content code,\np .literal,\np .literal span {\n    border: none;\n    padding: 0;\n    color: black; /* slight contrast with #404040 of regular text */\n    background: none;\n}\n\nkbd {\n    box-shadow: none;\n    line-height: 23px;\n    word-wrap: normal;\n    font-size: 15px;\n    font-family: Consolas, monospace;\n}\n\n.borg-options-table tr td:nth-child(2) .pre {\n    white-space: nowrap;\n}\n\n.borg-options-table tr td:first-child {\n    width: 2em;\n}\n\ncite {\n    white-space: nowrap;\n    color: black; /* slight contrast with #404040 of regular text */\n    font-family: Consolas, \"Andale Mono WT\", \"Andale Mono\", \"Lucida Console\", \"Lucida Sans Typewriter\",\n    \"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\", \"Liberation Mono\", \"Nimbus Mono L\", Monaco, \"Courier New\", Courier, monospace;\n    font-style: normal;\n    text-decoration: underline;\n}\n\n.borg-common-opt-ref {\n    font-weight: bold;\n}\n\n.sidebar-toc ul li.toctree-l2 a,\n.sidebar-toc ul li.toctree-l3 a {\n    padding-right: 25px;\n}\n\n#common-options .option {\n    white-space: nowrap;\n}\n/* Remove the right-column max-width cap so content fills the full available width. */\n#right-column {\n    max-width: none;\n}\n/* Hide the default RTD flyout since we show the version selector in the sidebar. */\nreadthedocs-flyout {\n    display: none !important;\n}\n/* Version selector in the sidebar. */\n.version-selector {\n    padding: 0 22px;\n    margin: 7px 0 7px 0;\n    font-size: 14px;\n}\n.version-selector label {\n    display: block;\n    margin-bottom: 4px;\n    color: #000;\n}\n.version-selector select {\n    width: 100%;\n    padding: 4px;\n    background-color: #fafafa;\n    color: #000;\n    border: 1px solid #ccc;\n    border-radius: 3px;\n}\n.version-selector::after {\n    content: '';\n    display: block;\n    border-top: 1px solid #ccc;\n    margin: 7px 0 0 0;\n}\n/* Reduce top and bottom margin of searchbox block to 7px to match separator spacing. */\n.sidebar-block:has(#main-search) {\n    margin-top: 7px;\n    margin-bottom: 7px;\n}\n/* Reduce the separator margin below the search block to 7px. */\n.sphinxsidebar > .sidebar-block:has(#main-search):after {\n    margin: 7px 22px 0 22px;\n}\n"
  },
  {
    "path": "docs/changes.rst",
    "content": ".. _important_notes:\n\nImportant notes 2.x\n===================\n\nThis section provides information about security and corruption issues.\n\n(nothing to see here yet)\n\n.. _upgradenotes2:\n\nUpgrade Notes\n=============\n\nborg 1.2.x/1.4.x to borg 2.0\n----------------------------\n\nCompatibility notes:\n\n- This is a major \"breaking\" release that is not compatible with existing repositories.\n\n  We tried to put all the necessary \"breaking\" changes into this release, so we\n  hopefully do not need another breaking release in the near future. The changes\n  were necessary for improved security, improved speed and parallelism,\n  unblocking future improvements, getting rid of legacy crap and design\n  limitations, having less and simpler code to maintain.\n\n  You can use \"borg transfer\" to transfer archives from borg 1.2/1.4 repos to\n  a new borg 2.0 repo, but it will need some time and space.\n\n  Before using \"borg transfer\", you must have upgraded to borg >= 1.2.6 (or\n  another borg version that was patched to fix CVE-2023-36811) and\n  you must have followed the upgrade instructions at top of the change log\n  relating to manifest and archive TAMs (borg2 just requires these TAMs now).\n\n- Command-line syntax was changed; scripts and wrappers will need changes:\n\n  - You will usually either export BORG_REPO=<MYREPO> into your environment or\n    call borg like: \"borg -r <MYREPO> <COMMAND>\".\n    In the docs, we usually omit \"-r ...\" for brevity.\n  - The scp-style REPO syntax was removed; please use ssh://..., #6697\n  - ssh:// URLs: Removed support for /~otheruser/, /~/ and /./, #6855.\n    New format:\n\n    - ssh://user@host:port/relative/path\n    - ssh://user@host:port//absolute/path\n  - -P / --prefix option was removed; please use the similar -a / --match-archives.\n  - Archive names don't need to be unique anymore. To the contrary:\n    It is now strongly recommended to use the identical name for borg create\n    within the same series of archives to make borg work more efficiently.\n    The name now identifies a series of archives; to identify a single archive,\n    please use aid:<archive-hash-prefix>, e.g., borg delete aid:d34db33f\n  - In case you do NOT want to adopt the \"series name\" way of naming archives\n    (like \"myarchive\") as we recommend, but keep using always-changing names\n    (like \"myserver-myarchive-20241231\"), you can do that, but then you must\n    make use of BORG_FILES_CACHE_SUFFIX and either set it to a constant suffix\n    (like \"all\") or to a unique suffix per archive series (like\n    \"myserver-myarchive\") so that borg can find the correct files cache.\n    For the \"all\" variant, you must also set BORG_FILES_CACHE_TTL to a value\n    greater than the count of different archives series you write to that repo.\n    Usually borg uses a different files cache suffix per archive (series) name\n    and defaults to BORG_FILES_CACHE_TTL=2 because that is sufficient for that.\n  - The archive ID is always given separately from the repository.\n    Unlike in borg 1.x, you must not give repo::archive.\n  - The series name or archive ID is either given as a positional parameter,\n    like:\n\n    - borg create documents ~/Documents\n    - borg diff aid:deadbeef aid:d34db33f\n  - or, if the command makes sense for an arbitrary amount of archives, archives\n    can be selected using a glob pattern, like:\n\n    - borg delete -a 'sh:myarchive-2024-??-??'\n    - borg recreate -a 'sh:myarchive-2024-??-??'\n  - some borg 1.x commands that supported working on a repo AND on an archive\n    were split into 2 commands, some others were renamed:\n\n    - borg 2 repo commands:\n\n      - borg repo-create  # was: borg init\n      - borg repo-list\n      - borg repo-info\n      - borg repo-delete\n      - borg repo-compress\n      - borg repo-space\n    - borg 2 archive commands:\n\n      - borg create NAME ...\n      - borg list ID\n      - borg extract ID ...\n      - borg diff ID1 ID2\n      - borg rename ID NEWNAME\n      - borg info ID\n      - borg delete ID\n      - borg recreate ID ...\n      - borg mount -a ID mountpoint ...\n\n    For more details, please consult the docs or --help option output.\n  - create/recreate/import-tar --timestamp: defaults to local timezone\n    now (was: UTC)\n- some deprecated options were removed:\n\n  - removed --remote-ratelimit (use --upload-ratelimit)\n  - removed --numeric-owner (use --numeric-ids)\n  - removed --nobsdflags (use --noflags)\n  - removed --noatime (default now, see also --atime)\n  - removed --save-space option (does not change behaviour)\n- removed --bypass-lock option\n- removed borg config command (only worked locally anyway)\n- compact command now requires access to the borg key if the repo is encrypted\n  or authenticated\n- using --list together with --progress is now disallowed (except with --log-json), #7219\n- the --glob-archives option was renamed to --match-archives (the short option\n  name -a is unchanged) and extended to support different pattern styles:\n\n  - id: for identical string match (this is the new default!)\n  - sh: for shell pattern / globbing match (this was used by --glob-archives)\n  - re: for regular expression match\n\n  So you might need to edit your scripts like e.g.::\n\n      borg 1.x: --glob-archives 'myserver-2024-*'\n      borg 2.0: --match-archives 'sh:myserver-2024-*'\n\n- use platformdirs 3.x.x instead of home-grown code. Due to that:\n\n  - XDG_*_HOME is not honoured on macOS and on Windows.\n  - BORG_BASE_DIR can still be used to enforce some base dir + .config/ or .cache/.\n  - on macOS, the default directories move to native locations:\n    config/data: ``~/Library/Application Support/borg/``,\n    cache: ``~/Library/Caches/borg/``,\n    runtime: ``~/Library/Caches/TemporaryItems/borg/``.\n  - on Windows, the default directories are:\n    config: ``C:\\Users\\<user>\\AppData\\Roaming\\borg``,\n    cache: ``C:\\Users\\<user>\\AppData\\Local\\borg\\Cache``,\n    data: ``C:\\Users\\<user>\\AppData\\Local\\borg``.\n  - **keyfile users on macOS (and Windows)**: borg 2 will look for key files in the\n    new platform-specific config directory instead of ``~/.config/borg/keys/`` where\n    borg 1.x stored them. You can set ``BORG_KEYS_DIR`` to point to the old location,\n    or copy the key file to the new location. See :ref:`env_vars` and the ``borg transfer``\n    documentation for details.\n- create: different included/excluded status chars, #7321\n\n  - dry-run: now uses \"+\" (was: \"-\") and \"-\" (was: \"x\") for included/excluded status\n  - non-dry-run: now uses \"-\" (was: \"x\") for excluded files\n\n  Option --filter=... might need an update, if you filter for the status chars\n  that were changed.\n- borg is now more strict and disallows giving some options multiple times -\n  if that makes no sense. Highlander options, see #6269. That might make scripts\n  fail now that somehow \"worked\" before (but maybe didn't work as intended due to\n  the contradicting options).\n\n.. _changelog:\n\nChange Log 2.x\n==============\n\nVersion 2.0.0b21 (2026-03-16)\n-----------------------------\n\nPlease note:\n\nBeta releases are only for testing on NEW repos - do not use for production.\n\nFor upgrade and compatibility hints, please also read the section \"Upgrade Notes\"\nabove.\n\nNew features:\n\n- support https/http (REST) repositories via borgstore, #9480\n- use jsonargparse as CLI argument/option parser, also supporting YAML configs\n  for defaults and auto-generated environment variables to override defaults, #6551\n- create --paths-from-shell-command, #5968\n- create: add --tags/--hostname/--username, #9401, #9402\n- create: implement \"file changed while backup\" detection on Windows, #9382\n- prune -v: now displays archive counts (total, kept, pruned), #9262\n- list --format: add fingerprint placeholder (fast!)\n- archive: use 3 timestamps (cleanly separate nominal archive timestamp from\n  borg operation start/end info), #9400\n- benchmark crud: add --json-lines output option, #9165\n\nFixes:\n\n- prune: fix Archive.DoesNotExist when using --list, #9416\n- remove_dotdot_prefixes: remove bad assert, #9406\n- create --compress: expose Padmé size obfuscation (250) via CLI, #9286\n- remote: fix StoreObjectNotFound exception lost over RPC, #9380\n- passphrase: fail if multiple passphrase environment variables are set, #8834\n- cockpit: fix subprocess invocation in frozen binaries\n- cockpit: start the Borg runner after all widgets are mounted\n- debug format-obj: support all repository object types, #9391\n- benchmark crud: suppress compact warnings during benchmark runs, #9365\n- fix file: URL parsing for Windows\n\n  - Linux: /abs/path -> file:///abs/path\n  - Windows: c:/abs/path -> file:///c:/abs/path\n\nOther changes:\n\n- y2038: SUPPORT_32BIT_PLATFORMS = False, #9429.\n  Not as bad as it sounds: 32bit platforms with 64bit time_t will still work.\n  As of 2026, this is pretty much every platform that can run Borg reasonably well.\n- remove handwritten bash and zsh shell completions, #9178.\n  these are now auto-generated via ``borg completion bash/zsh`` (using shtab).\n  fish completions are kept until shtab gains fish support.\n- mount: warn about symlinks pointing outside of the mountpoint, #9254\n- use FILE_FLAG_WRITE_THROUGH on Windows for SyncFile data durability, #9388\n- extract: do not delete existing directory if possible, #4233\n- extract --continue: optimize processing of already existing dirs\n- mount: FUSE FS performance fix\n- prune: print hint to run compact to free space\n- prune: use same method to delete archives as delete subcommand, #9424\n- use xxhash from PyPI, #6535\n- use zstd from python lib or backports.zstd (python<'3.14'), #9261\n- swidth: use cross-platform implementation, #7493\n- platform: use F_FULLSYNC on macOS for SyncFile data durability, #9383\n- cache: add seek()/tell() to SyncFile, use SaveFile in _write_files_cache, #9390\n- cache: remove try_upgrade_to_b14() legacy migration, #9371\n- remove unnecessary checks: API_VERSION, check_python\n- time calculations: avoid floating point\n- Version: do not access private attributes, #9263\n- Windows platform (win32):\n\n  - normalize drive letters, #9279\n  - Path separator: internally always use \"/\", accept also \"\\\" for CLI arguments\n  - map_chars: deal with invalid chars in paths on Windows\n- binary build:\n\n  - use pyinstaller 6.18.0 for Python 3.14 compatibility\n  - do not exclude ssl, needed for pyfuse3/trio, #9196\n  - build with cockpit,s3,sftp extras installed, #9241\n  - build Linux binaries with pyfuse3, #9196\n- Documentation:\n\n  - jsonargparse: update docs about configs, auto-generated environment variables, precedence\n  - fix S3 URL description, #9249\n  - add a note that you need to install boto3 if you want to use S3/B2 URLs\n  - rename BORG_RLIST_FORMAT to BORG_REPO_LIST_FORMAT, #9411\n  - document platformdirs change and platform-specific directory paths, #7332\n  - borgbackup.org: move RTD version selector to sidebar top-left, #8204\n  - update SECURITY.md version table, #9346\n  - fuse: add thread/async safety warning\n  - upgrade http:// URLs to https:// and remove dead librelist.com link, #9342, #9302\n  - man pages: fix broken :ref: references (e.g. borg_patterns), #7239\n  - update deprecated pypi.python.org URLs to pypi.org, #9337\n  - consolidate key backup info in borg key export, #6204\n  - fix typos found by codespell, #9295\n  - GitHub: enhance pull request template, #9334\n  - archive specification, FAQ, #9248, #9053\n- testing / CI:\n\n  - scripts/linux-run: run commands (e.g. tox) in a podman linux container,\n    very useful when developing on macOS to test under Linux.\n  - CI: add testing on omniOS (\"OpenSolaris\")\n  - CI: use OpenBSD 7.8\n  - CI: fix and re-enable Windows testing\n  - CI: faster with borg-dir/borg.exe, #9236\n  - add dependabot, #9308, #9349\n  - add top-level permissions for least-privilege security, #9344\n  - completion: focused tests for auto-generated shell completions\n    (syntax validation, size sanity, borg-specific preamble behavior)\n  - Speed up benchmark CPU tests with _BORG_BENCHMARK_CPU_TEST env var, #9414\n  - fix mismatch in xattr test, #9238\n  - xattr: document fakeroot xattr as Linux-only, add missing fakeroot skipping on FreeBSD, #9394\n  - testsuite: remove deprecated manual cleanup in create_cmd_test\n  - add borg.exe to PATH\n  - fix tmpdir check on netbsd\n  - enable Codecov Test Analytics, upgrade to codecov-action@v5\n  - codecov: nothing to do for mypy and docs envs\n  - add missing timeout-minutes to codeql, backport, and lint workflows, #9298\n  - add path filters to lint and codeql workflows, #9328\n  - cache tox environments\n  - remove redundant tox runs, parallelize better, avoid unnecessary steps\n  - add concurrency groups to cancel stale workflow runs, #9310\n  - improve collecting coverage information, improve coverage, #9448\n  - use locked requirements, add canary job, #9361\n  - fix spurious sparse test fail on win32, #7616\n  - fix race condition in test_with_lock, #8810\n  - speed up prune/list/repo-list tests, #9324\n  - add test for cockpit feature\n  - add a borg create/extract timestamp test for y2261.\n\n\nVersion 2.0.0b20 (2025-12-24)\n-----------------------------\n\nNew features:\n\n- fat binary builds on GitHub (see assets on the GitHub releases page):\n\n  - for Linux with glibc 2.35+ (Intel/AMD and ARM64)\n  - for macOS 15+ (Apple Silicon/ARM64 and Intel)\n  - using GitHub artifact attestations for release binaries, #9134\n- borg --cockpit: show status display based on Textual\n- Linux ACLs: use acl_to_any_text to avoid libacl name lookups, #8753.\n- export-tar/import-tar: support for POSIX ACLs (PAX format)\n- NetBSD: xattr support, #1332\n- mount: alternatively, work with high-level fuse library \"mfusepy\", which\n  supports fuse 2 and 3, #9194. Try it with: pip install borg[mfusepy]\n- diff: --sort-by=field[,field,...], #8998\n- list --format: add \"inode\" placeholder\n- info: show cwd at the time of backup creation, #6191\n- improved tty-less progress reporting (--progress), #9055\n- BORG_MSGPACK_VERSION_CHECK=no to optionally disable the msgpack version\n  check; default is \"yes\", use at your own risk, #9109.\n- completion: generate completion scripts for supported shells, #9172,\n  uses shtab, supports bash and zsh.\n\nFixes:\n\n- extract: fs flags: use get/set to influence only specific flags, #9039, Linux, FreeBSD, macOS\n- transfer: fix borg transfer corrupting the source repo index, #9022\n- transfer: create a chunks list entry for missing chunks, see #9208\n- transfer: fix AttributeError with --dry-run, see #9199\n- old archives might not have a comment in metadata, see #9208\n- HardLinkManager: allow NoneType for contentless hardlinks, see #9208\n- legacyrepository: remove auto_recover, #9022\n- legacyremote: accept raise_missing in get/get_many to avoid TypeError\n  with callers that pass it; no behavior change on legacy protocol, #9199\n- fix reading borg 1.x repo index, #9022\n- enable S3/B2 support of borgstore\n- mount --show-rc: display main process return code (rc), #8308\n- create: add exception handler for NODUMP-excluded directories, #9032\n- json: include archive keys in JSON lines when requested via --format, #9095\n- ensure valid file URLs are created from Windows paths\n- Windows: add missing guards around `preexec_fn=ignore_sigint`\n- preprocess_args: fix option name matching\n\nOther changes:\n\n- support Python 3.14, msgpack 1.1.2, use Cython 3.2.3\n- require setuptools>=78.1.1, #9042\n- \"bsdflags\" set_flags: remove compression flag support (did not work anyway)\n- Brewfile: use openssl@3\n- buzhash/buzhash64: initialise all-zero memory more efficiently\n- tests:\n\n  - add fuzzing tests for chunkers\n  - add tests for diff output of archives with hard links\n  - read_only CM: skip test if cmd_immutable is unsuccessful, fixes #9021\n  - save space in test_create_* tests\n  - CI/tests: add SFTP/rclone/S3 repo testing\n  - CI: add local servers for S3 and SFTP testing\n  - CI: add misc. BSDs and Haiku OS (on GitHub Actions)\n  - CI: do dynamic code analysis, #6819\n  - transfer: add test for unexpected src repo index change, #9022\n  - pyproject.toml: correctly define test environments for FUSE testing\n  - add granularity_sleep, #9150\n  - use context manager when opening files in patterns_test\n  - FUSE related fixes/improvements, #9182\n  - fix pynacl/libsodium build on freebsd, #9214\n  - filter_xattrs now also filters some macOS xattrs\n  - transfer: add --dry-run test\n  - refactor id <-> name lookup for monkeypatching\n  - CI: netbsd: enable xattrs on TMPDIR\n  - improve fs cleanup directly after tests\n- Vagrant:\n\n  - add Debian testing/Trixie box\n  - add an OpenBSD 7.7 box\n  - fix OpenIndiana box\n  - drop macOS 10.12 box (binaries are built on GitHub now)\n  - use Python 3.13.8 for binary building and tests\n  - use PyInstaller 6.14.2 for binary building\n- docs:\n\n  - update README for binaries\n  - improve borg help patterns, #7144\n  - patterns: clarify scope of default pattern style, #9004\n  - extract: document how to use wildcards in PATHs, #8589\n  - how to debug borg mount, #5461\n  - document what happens when a new keyfile repo is created at the same path, #6230\n  - update install docs to include `SETUPTOOLS_SCM_PRETEND_VERSION`\n  - highlight archive series naming for fast incrementals, #8955\n  - add Arch Linux to the 'Installing from source' docs\n  - add systemd-inhibit and examples, #8989\n  - code/docs: fix typos and grammar\n  - some fixes/updates to the FAQ\n\n\nVersion 2.0.0b19 (2025-07-02)\n-----------------------------\n\nFixes:\n\n- reader: fix corruption issue \"forgetting\" all-zero bytestrings, #8963\n- import-tar: normalize the tarinfo name/linkname when used as hlm key.\n  also: when printing the path, use the already normalized item.path.\n- import-tar: fix the dotslash issue, add test\n\nNew features:\n\n- create --files-changed=MODE option, #8958.\n  control how borg detects whether a file has changed while it was backed up,\n  valid modes are ctime (default), mtime (2nd best) or disabled (not recommended).\n\nOther changes:\n\n- to_key_filename: raise length limit to 120, #8966.\n  This works around a test failure on systems with deep build directories.\n\n\nVersion 2.0.0b18 (2025-06-19)\n-----------------------------\n\nNew features:\n\n- experimental new \"buzhash64\" chunker (later, after testing, this shall become\n  the default chunker in borg2):\n\n  - add own cryptographically secure pseudo-random number generator (CSPRNG)\n    based on AES256-CTR to create deterministic random, based on a 256bit seed.\n  - use that to deterministically create a perfectly balanced buzhash64 table.\n  - \"buzhash64\" chunker computes 64bit hash values for the chunking decision.\n  - performance is similar to \"buzhash\" (measured on Apple M3P cpu).\n\n  That should also resolve these points of criticism about the old \"buzhash\"\n  32bit code:\n\n  - table_base: that the bits are not randomly distributed enough\n  - that an XORed seed cancels out for specific window sizes\n  - that XORing the table with a seed is equivalent to XORing the computed hash\n    value with another constant\n\n  Please test the chunkers extensively (e.g. with borg create, borg transfer),\n  we can hardly change them \"in production\", because chunking differently also\n  means not deduplicating with old chunks. So, in case there are changes\n  needed, we need to find and fix them now while borg is in beta.\n\n  See also some other chunker changes listed below \"Other changes\".\n- serve: add --permissions option as an alternative to BORG_REPO_PERMISSIONS env var\n- create: auto-exclude items based on xattrs or NODUMP, see #4972\n\n  no options yet, just hardcoded macOS and Linux xattrs.\n  removed the --exclude-nodump option, it is also done automagically now.\n\n  also: create: read stat attrs, xattrs, ACLs early, before file contents.\n\nFixes:\n\n- compact: fix cleaning archives directory (catch correct exception, use\n  logger.warning, improve error msg)\n\nOther changes:\n\n- support Python 3.14\n- msgpack: allow 1.1.1, version check: ignore \"rc\" or other version elements\n- add derive_key to derive new keys from existing key material\n- refactor the chunkers, #8882 #8883:\n\n  - transform buzhash chunker C code to Cython\n  - split concerns into FileFMAPReader, FileReader, Chunker*:\n\n    - FileFMAPReader reads blocks from the input file, supporting sparse\n      files and fmaps.\n    - FileReader uses FileFMAPReader to fill its buffer and offers clients a\n      `.read(size)` method so they can read pieces of the data.\n    - all chunkers now use the FileReader/FileFMAPReader code\n  - split code and test module into packages\n- \"fixed\" chunker: add fixed chunker tests to selftest\n- \"fixed\" chunker: do not assert on short header read\n- \"buzhash*\" chunker: use safe_fadvise\n- \"buzhash\" chunker: reject even window size, #8868\n- fish: fix archive name completion\n- refactor: modularize tests\n- refactor: use pathlib.Path\n- tests / CI:\n\n  - CI: add bandit, a security-oriented static analysis tool\n  - CI: disable windows as the file:// repo URLs are still broken on windows.\n  - tests: tox: use native pyproject.toml configuration\n  - more chunker-related tests\n- docs:\n\n  - add docs for serve --permissions / BORG_REPO_PERMISSIONS\n  - borg-serve: simplify example of env in authorized_keys, #8318\n  - fix mistyped CVE number\n\n\nVersion 2.0.0b17 (2025-05-23)\n-----------------------------\n\nNew features:\n\n- transfer: implement --chunker-params to re-chunk while transferring, #8706\n- list --depth=N: list files up to N depth in path hierarchy, #8268\n- compact: also clean up files cache, #8852\n- `BORG_REPO_PERMISSIONS=all|no-delete|write-only|read-only`, #8823\n\n  The posixfs borgstore backend implements permissions to make\n  testing with differently permissive stores easier.\n\n  The env var selects from pre-defined permission configurations\n  within borg and gives the chosen permissions config to borgstore.\n  borg uses borgstore's posixfs backend only for file: and ssh: repos.\n\nFixes:\n\n- correct the signature of __set_name__ as cython 3.1 added support,\n  fixing build on Cython 3.1, #6858\n- compact/check: fix bug not writing the complete index, #8813\n- compact: add --iec option, #8831\n- check/compact/analyze: show archive timestamp in local tz, #8814\n- repo-space: enable ssh: repo testing, fix AttributeError, #8815\n- repo-info: fix output formatting\n\nOther changes:\n\n- require borgstore 0.3.x\n- some updates and fixes for shell completions, needs more work\n- dir_is_tagged/_is_cachedir: add fd-based operations\n- cython: suppress compiler warning about CYTHON_FALLTHROUGH in unreachable code\n- source code: `pyupgrade --py310-plus ./**/*.py`\n- tests:\n\n  - add/improve tests for repo-compress --stats, transfer, repo-space\n  - split helpers tests from a single module into borg.testsuite.helpers package\n  - save temp space (good for ramdisk users)\n  - fix diff cmd test on macOS HFS+, #8860\n  - test validity of shell completion files\n  - CI: fix and enable windows CI, #8728\n  - CI: upload coverage for windows tests\n  - CI: install zsh and fish so we can test shell completions\n- docs:\n\n  - must have the release tags in the local repo, #8582\n  - remove outdated docs/man files about borg change-passphrase\n  - add S3/B2 urls to documentation for repository urls, #8833\n\n\nVersion 2.0.0b16 (2025-05-06)\n-----------------------------\n\nFixes:\n\n- chunks cache: invalidate old chunk index cache, #8795\n- compact: always write updated chunkindex to repo, #8791\n- ChunksMixin: don't use self._chunks until it is demand-built, #8785\n- AdhocWithFilesCache: fix call to _maybe_write_chunks_cache\n- format_time: output date/time in local tz, #8802\n- check: ask for key passphrase early, #1931\n- only obfuscate the size of file content chunks, #7559\n- better support other repo by misc. passphrase env vars, #8457\n\n  - passphrases now come from `BORG_[OTHER_]PASSPHRASE`, `BORG_[OTHER_]PASSCOMMAND`\n    or `BORG_[OTHER_]PASSPHRASE_FD`.\n  - `borg repo-create --repo B --other-repo A` does not silently copy the\n    passphrase of key A to key B anymore, but either asks for the passphrase\n    or reads it from env vars.\n\nOther changes:\n\n- remove support for / testing on Python 3.9\n- docs: borg serve --repo is not supported, #8591\n- remove remainders of append-only and quota support\n- remove cygwin < 2.8.0 bug workaround\n- fix remote api versioning\n\n\nVersion 2.0.0b15 (2025-04-22)\n-----------------------------\n\nNew features:\n\n- compact: without --stats, it will be faster by using the cached chunks index.\n  with --stats it will be as slow as before, listing all repo objs.\n- compact: support --dry-run (do nothing), #8300\n- extract: --dry-run now displays +/- status flags (included/excluded), #8564\n- allow timespan to be specified with common time units, #8624\n- enhance passphrase handling, #8496.\n\n  Setting `BORG_DEBUG_PASSPHRASE=YES` enables passphrase debug logging to\n  stderr, showing passphrase, hex utf-8 byte sequence and related env vars if\n  a wrong passphrase was encountered.\n\n  Setting `BORG_DISPLAY_PASSPHRASE=YES` now always shows passphrase and its hex\n  utf-8 byte sequence.\n- add {unixtime} placeholder, #8522\n- implement padme chunk size obfuscation (SPEC 250), #8705\n- macOS: retrieve birthtime in nanosecond precision via system call, #8724\n\nBug fixes:\n\n- borg exits when assertions are disabled with Python optimizations, #8649\n- yes(): deal with UnicodeDecodeError in input(), #6984\n- fix remote repository exception handling / modern exit codes, #8631\n- freebsd: fix nfs4 acl processing, #8756.\n  This issue only affected borg extract --numeric-ids when processing NFS4\n  ACLs, it didn't affect POSIX ACL processing.\n\nOther changes:\n\n- adapt to and require borghash 0.1.0\n- adapt to and require borgstore 0.2.0 (new s3/b2 backend, fixes/improvements)\n- create: remove --make-parent-dirs option (borgstore now does this automatically), #8619\n- iter_items: decouple item iteration and content data chunks preloading\n- remote: simplify code, add debug logging\n- pyproject.toml: SPDX expression for license, add license-files, #8771\n- Item: remove .chunks_healthy, #8559\n- OpenBSD fixes:\n\n  - support other OpenSSL versions on OpenBSD, #8553\n  - vagrant: fix OpenBSD box, #8506\n  - Filter test output with LibreSSL related warnings on OpenBSD\n- macOS: fix brew's broken pkg-config -> pkgconf transition\n- tests: ignore 'com.apple.provenance' xattr (macOS specific)\n- vagrant updates:\n\n  - use pyinstaller 6.11.1 (also use this in msys2 build scripts)\n  - use python 3.12.10\n  - build binaries with borgstore[sftp], #8574\n- docs:\n\n  - automated backup: append to SYSTEMD_WANTS rather than overwrite, #8641\n  - fix udev rule priority in automated-local.rst, #8639\n  - FAQ: Why backups are slow on a Linux server that is a member of a windows domain? #8636\n  - within a shell, cli options with special characters may require quoting, #8578\n  - update prune documentation for new --keep-within intervals, #8630\n  - borg serve: recommend using a simple shell, #3818\n  - update install docs (requirements, pkgconfig, fuse), #8342\n  - libffi-dev is required for argon2-cffi-bindings\n  - add undelete command to index\n  - borg commands updated with --repo option, #8550\n  - FAQ: add entry about pure-python msgpack warning, #8323\n  - readthedocs theme fixes\n\n    - bring back highlighted content preview in search results.\n    - fix erroneous warning about missing javascript support.\n\n\nVersion 2.0.0b14 (2024-11-17)\n-----------------------------\n\nNew features:\n\n- delete: now only soft-deletes archives (same for prune)\n- repo-list: --deleted lists deleted archives\n- undelete: undelete soft-deleted archives, #8500\n\nFixes:\n\n- chunks index cache:\n\n  - enable partial/incremental updates (F_NEW flag).\n  - write chunks index every 10mins, #8503.\n    this makes sure progress is not totally lost when a backup is interrupted.\n  - write to repo/cache/chunks.<HASH> to enable parallel updates.\n- mount: fix check_pending_archive to give correct root dir, #8528\n\nOther changes:\n\n- repo-compress: reduce memory consumption (F_COMPRESS flag)\n- files cache: reduce memory consumption, #5756\n- check: rename --undelete-archives to --find-lost-archives\n- check: rebuild_archives_directory: accelerate by only reading metadata\n- shell completions: adapt zsh for borg 2.0.0b13 - needs more work!\n- chunk index: rename .refcount to .flags, use it for user and system flags.\n- vagrant:\n\n  - add bookworm32 box for 32bit platform testing\n  - fix pythons on freebsd14\n  - simplify openindiana box setup\n- docs:\n\n  - remove --bypass-lock, small changes regarding compression\n  - FAQ: clean up entries regarding SSH settings\n\n\nVersion 2.0.0b13 (2024-10-31)\n-----------------------------\n\nNew features:\n\n- implement special tags, @PROT tag for protecting archives, #953.\n\n  borg won't delete/prune/recreate protected archives.\n- prune: add quarterly pruning strategy, #8337.\n- import-tar/export-tar: add xattr support for PAX format, #2521.\n\nFixes:\n\n- simple error msgs for existing / non-existing repo, no tracebacks, #8475.\n- mount: create unique directory names, #8461.\n- diff: suppress modified changes for files which weren't actually modified.\n- diff: do not test for ctime difference on windows.\n- prune: fix exception when NAME is given, #8486\n- repo-create: build and cache an empty ChunkIndex.\n- work around missing size/nfiles archive metadata, #8491\n- lock after checking repo exists, #8485\n\nOther changes:\n\n- new file:, rclone:, ssh:, sftp: URLs, #8372, #8446.\n\n  new way to deal with absolute vs. relative paths.\n- require borgstore ~= 0.1.0, require borghash ~= 0.0.1.\n- new hashtable code based on borghash project:\n\n  - borghash replaces old / hard to maintain _hashindex.c code.\n  - implement ChunkIndex, NSIndex1, FuseVersionsIndex using borghash.HashTableNT.\n  - rewrite NSIndex1 (borg 1.x) on-disk format read/write methods in Cython.\n  - remove NSIndex (early borg2) data structure / serialization code for repo index.\n  - change xxh64 seed for ChunkIndex to invalidate old cache contents.\n  - chunks index: show hashtable stats at debug log level, #506.\n- check (repository part): build and cache a ChunkIndex.\n\n  check (archives part): use cached ChunkIndex from check (repository part).\n- export-tar: switch default to PAX format.\n- docs:\n\n  - update URL docs\n  - mount: document on-demand loading, perf tips, #7173.\n  - borg/borgfs detects internally under which name it was invoked, #8207.\n  - better link modern return codes, #8370.\n  - binary: using the directory build is faster, #8008.\n  - update \"Running the tests (using the pypi package)\", #6386.\n- github CI:\n\n  - temporarily disabled windows CI, #8474.\n  - msys2: use pyinstaller 6.10.0.\n  - msys2: install rclone.\n- tests:\n\n  - rename test files so that pytest default discovery finds them.\n  - call register_assert_rewrite before importing borg.testsuite.\n  - move conftest.py one directory level higher.\n  - remove hashindex tests from selftests (borghash project has own tests).\n\n\nVersion 2.0.0b12 (2024-10-03)\n-----------------------------\n\nNew features:\n\n- tag: new command to set, add, remove tags.\n- repo-list: add tags/hostname/username/comment to default format, reorder, adjust.\n\n  Idea: not putting these into the archive name, but keeping them separate.\n- repo-list --short: only print archive IDs (unique IDs, used for scripting).\n- implement --match-archives user:USERNAME host:HOSTNAME tags:TAG1,TAG2,...\n- allow -a / --match-archives multiple times (logical AND).\n\n  E.g.: borg delete -a home -a user:kenny -a host:kenny-pc\n- analyze: list changed chunks' sizes per directory.\n\nFixes:\n\n- locking: also refresh the lock in other repo methods. avoid repo lock\n  getting stale when processing lots of unchanged files, #8442.\n- make sure the store gets closed in case of exceptions, #8413.\n- msgpack: increase max_buffer_size to ~4GiB, #8440.\n- Location.canonical_path: fix protocol and host display, #8446.\n\nOther changes:\n\n- give borgstore.Store a complete levels configuration, #8432.\n- add BORG_STORE_DATA_LEVELS=2 env var.\n- check: also display archive timestamp.\n- vagrant:\n\n  - use python 3.12.6 for binary builds.\n  - new testing box based on bento/ubuntu-24.04.\n  - install Rust on BSD.\n\n\nVersion 2.0.0b11 (2024-09-26)\n-----------------------------\n\nNew features:\n\n- Support rclone:// URLs for borg repositories.\n\n  This enables 70+ cloud storage products, including Amazon S3, Backblaze B2,\n  Ceph, Dropbox, ftp(s), Google Cloud Storage, Google Drive, Microsoft Azure,\n  Microsoft OneDrive, OpenStack Swift, pCloud, Seafile, sftp, SMB / CIFS and\n  WebDAV!\n\n  See https://rclone.org/ for more details.\n- Parallel operations in same repo from same client (same user/machine).\n- Archive series feature, #7930.\n\n  TL;DR: a NAME now identifies a series of identically named archives,\n  to identify a specific single archive, use aid:<archive hash>.\n\n  in borg 1.x, we used to put a timestamp into the archive name, because borg1\n  required unique archive names.\n\n  borg2 does not require unique archive names, but it encourages you to even\n  use a identical archive names within the same SERIES of archives, e.g. you\n  could backup user files to archives named \"user-files\" and system files to\n  archives named \"system-files\".\n  that makes matching (e.g. for prune, for the files cache, ...) much simpler\n  and borg now KNOWS which archives belong to the same series (because they all\n  have the same name).\n- info/delete/prune: allow positional NAME argument, e.g.:\n\n  - borg prune --keep-daily 30 <seriesname>\n  - borg delete aid:<archive hash>\n- create: also archive inode number, #8362\n\n  Borg can use this when using archive series to rebuild the local files cache\n  from the previous archive (of the same series) in the repository.\n\nFixes:\n\n- Remove superfluous repository.list() call. for high latency repos\n  (like sftp, cloud), this improves performance of borg check and compact.\n- repository.list: refresh lock more frequently\n- misc. commands fixed for non-unique archive names\n- remote: allow get_manifest method\n- files cache: fix rare race condition with data loss potential, #3536\n- storelocking: misc. fixes / cleanups\n\nOther changes:\n\n- Cache the chunks index in the repository, #8397.\n  Improves high latency repo performance for most commands compared to b10.\n- repo-compress: faster by using chunks index rather than repository.list().\n- Files cache entries now have both ctime AND mtime.\n- Borg updates the ctime and mtime of known and \"unchanged\" files, #4915.\n- Rebuild files cache from previous archive in same series, #8385.\n- Reduce RAM usage by splitting the files cache by archive series, #5658.\n- Remove AdHocCache, remove BORG_CACHE_IMPL (we only have one implementation).\n- Docs: user@ and :port are optional in sftp and ssh URLs.\n- CI: re-enable windows build after fixing it.\n- Upgrade pyinstaller to 6.10.0.\n- Increase IDS_PER_CHUNK, #6945.\n\n\nVersion 2.0.0b10 (2024-09-09)\n-----------------------------\n\nNew features:\n\n- borgstore based repository, file:, ssh: and sftp: for now, more possible.\n- repository stores objects separately now, not using segment files.\n  this has more fs overhead, but needs much less I/O because no segment\n  files compaction is required anymore. also, no repository index is\n  needed anymore because we can directly find the objects by their ID.\n- locking: new borgstore based repository locking with automatic stale\n  lock removal (if lock does not get refreshed, if lock owner process is dead).\n- simultaneous repository access for many borg commands except check/compact.\n  the cache lock for adhocwithfiles is still exclusive though, so use\n  BORG_CACHE_IMPL=adhoc if you want to try that out using only 1 machine\n  and 1 user (that implementation doesn't use a cache lock). When using\n  multiple client machines or users, it also works with the default cache.\n- delete/prune: much quicker now and can be undone.\n- check --repair --undelete-archives: bring archives back from the dead.\n- repo-space: manage reserved space in repository (avoid dead-end situation if\n  repository filesystem runs full).\n\nBugs/issues fixed:\n\n- a lot! all linked from PR #8332.\n\nOther changes:\n\n- repository: remove transactions, solved differently and much simpler now\n  (convergence and write order primarily).\n- repository: replaced precise reference counting with \"object exists in repo?\"\n  and \"garbage collection of unused objects\".\n- cache: remove transactions, remove chunks cache.\n  removed LocalCache, BORG_CACHE_IMPL=local, solving all related issues.\n  as in beta 9, adhowwithfiles is the default implementation.\n- compact: needs the borg key now (run it clientside), -v gives nice stats.\n- transfer: archive transfers from borg 1.x need the --from-borg1 option\n- check: reimplemented / bigger changes.\n- code: got rid of a metric ton of not needed complexity.\n  when borg does not need to read borg 1.x repos/archives anymore, after\n  users have transferred their archives, even much more can be removed.\n- docs: updated / removed outdated stuff\n- renamed r* commands to repo-*\n\n\nVersion 2.0.0b9 (2024-07-20)\n----------------------------\n\nNew features:\n\n- add BORG_CACHE_IMPL, default is \"adhocwithfiles\" to test the new cache\n  implementation, featuring an adhoc non-persistent chunks cache and a\n  persistent files cache. See the docs for other values.\n\n  Requires to run \"borg check --repair --archives-only\" to delete orphaned\n  chunks before running \"borg compact\" to free space! These orphans are\n  expected due to the simplified refcounting with the AdHocFilesCache.\n- make BORG_EXIT_CODES=\"modern\" the default, #8110\n- add BORG_USE_CHUNKS_ARCHIVE env var, #8280\n- automatically rebuild cache on exception, #5213\n\nBug fixes:\n\n- fix Ctrl-C / SIGINT behaviour for pyinstaller-made binaries, #8155\n- delete: fix error handling with Ctrl-C\n- rcompress: fix error handling with Ctrl-C\n- delete: fix error handling when no archive is specified, #8256\n- setup.py: fix import error reporting for cythonize import, see #8208\n- create: deal with EBUSY, #8123\n- benchmark: inherit options --rsh --remote-path, #8099\n- benchmark: fix return value, #8113\n- key export: fix crash when no path is given, fix exception handling\n\nOther changes:\n\n- setup.py: detect noexec build fs issue, see #8208\n- improve acl_get / acl_set error handling (forward port from 1.4-maint)\n- allow msgpack 1.1.0\n- vagrant: use pyinstaller 6.7.0\n- use Python 3.11.9 for binary builds\n- require Cython 3.0.3 at least, #8133\n- docs: add non-root deployment strategy\n\n\nVersion 2.0.0b8 (2024-02-20)\n----------------------------\n\nNew features:\n\n- create: add the slashdot hack, update docs, #4685\n- BORG_EXIT_CODES=modern: optional more specific return codes (for errors and warnings).\n\n  The default value of this new environment variable is \"legacy\", which should result in\n  a behaviour similar to borg 1.2 and older (only using rc 0, 1 and 2).\n  \"modern\" exit codes are much more specific (see the internals/frontends docs).\n- implement \"borg version\" (shows client and server version), #7829\n\nFixes:\n\n- docs: CVE-2023-36811 upgrade steps: consider checkpoint archives, #7802\n- check/compact: fix spurious reappearance of orphan chunks since borg 1.2, #6687 -\n  this consists of 2 fixes:\n\n  - for existing chunks: check --repair: recreate shadow index, #7897 #6687\n  - for newly created chunks: update shadow index when doing a double-put, #7896 #5661\n\n  If you have experienced issue #6687, you may want to run borg check --repair\n  after upgrading to borg 1.2.7 to recreate the shadow index and get rid of the\n  issue for existing chunks.\n- check: fix return code for index entry value discrepancies\n- LockRoster.modify: no KeyError if element was already gone, #7937\n- create --X-from-command: run subcommands with a clean environment, #7916\n- list --sort-by: support \"archive\" as alias of \"name\", #7873\n- fix rc and msg if arg parsing throws an exception, #7885\n- PATH: do not accept empty strings, #4221\n- fix invalid pattern argument error msg\n- zlib legacy decompress fixes, #7883\n\nOther changes:\n\n- replace archive/manifest TAMs by typed repo objects (ro_type), docs, #7670\n- crypto: use a one-step kdf for session keys, #7953\n- remove recreate --recompress option, use the more efficient repo-wide \"rcompress\".\n- include unistd.h in _chunker.c (fix for Python 3.13)\n- allow msgpack 1.0.7\n- allow platformdirs 4, #7950\n- use and require cython3\n- move conftest.py to src/borg/testsuite, #6386\n- use less setup.py, use pip and build\n- linux: use pkgconfig to find libacl\n- borg.logger: use same method params as python logging\n- create and use Brewfile, document \"brew bundle\" install (macOS)\n- blacken master branch\n- prevent CLI argument issues in scripts/glibc_check.py\n- pyproject.toml: exclude source files which have been compiled, #7828\n- sdist: dynamically compute readme (long_description)\n- init: better borg key export instructions\n- scripts/make.py: move clean, build_man, build_usage to there,\n  so we do not need to invoke setup.py directly, update docs\n- vagrant:\n\n  - use openssl 3.0 on macOS\n  - add script for fetching borg binaries from VMs, #7989\n  - use generic/openbsd7 box\n  - netbsd: test on py311 only\n  - remove debian 9 \"stretch\" box\n  - use freebsd 14, #6871\n  - use python 3.9.4 for tests, latest python 3.11.7 for binary builds\n  - use pyinstaller 6.3.0\n- docs:\n\n  - add typical PR workflow to development docs, #7495\n  - improve docs for borg with-lock, add example #8024\n  - create disk/partition sector backup by disk serial number\n  - Add \"check.rebuild_refcounts\" message\n  - not only attack/unsafe, can also be a fs issue, #7853\n  - use virtualenv on Cygwin\n  - readthedocs: also build offline docs, #7835\n  - do not refer to setup.py installation method\n  - how to run the testsuite using the dist package\n  - requirements are defined in pyproject.toml\n\n\nVersion 2.0.0b7 (2023-09-14)\n----------------------------\n\nNew features:\n\n- BORG_WORKAROUNDS=authenticated_no_key to extract from authenticated repos\n  without having the borg key, #7700\n\nFixes:\n\n- archive tam verify security fix, fixes CVE-2023-36811\n- remote logging/progress: use callback to send queued records, #7662\n- make_path_safe: remove test for backslashes, #7651\n- benchmark cpu: use sanitized path, #7654\n- create: do not try to read parent dir of recursion root, #7746\n\nOther changes:\n\n- always implicitly require archive TAMs (all archives have TAMs since borg 1.2.6)\n- always implicitly require manifest TAMs (manifests have TAMs since borg 1.0.9)\n- rlist: remove support for {tam} placeholder, archives are now always TAM-authenticated.\n- support / test on Python 3.12\n- allow msgpack 1.0.6 (which has py312 wheels), #7810\n- manifest: move item_keys into config dict (manifest.version == 2 now), #7710\n- replace \"datetime.utcfromtimestamp\" to avoid deprecation warnings with Python 3.12\n- properly normalise paths on Windows (forward slashes, integrate drive letter into path)\n- Docs:\n\n  - move upgrade / compat. notes to own section, see #7546\n  - fix borg delete examples, #7759\n  - improve rcreate / related repos docs\n  - automated-local.rst: use UUID for consistent udev rule\n  - rewrite `borg check` docs, #7578\n  - misc. other docs updates\n- Tests / CI / Vagrant:\n\n  - major testsuite refactoring: a lot more tests now use pytest, #7626\n  - freebsd: add some ACL tests, #7745\n  - fix test_disk_full, #7617\n  - fix failing test_get_runtime_dir test on OpenBSD, #7719\n  - CI: run on ubuntu 22.04\n  - CI: test building the docs\n  - simplify flake8 config, fix some complaints\n  - use pyinstaller 5.13.1 to build the borg binaries\n\n\nVersion 2.0.0b6 (2023-06-11)\n----------------------------\n\nNew features:\n\n- diff: include changes in ctime and mtime, #7248\n- diff: sort JSON output alphabetically\n- diff --content-only: option added to ignore metadata changes\n- diff: add --format option, #4634\n- import-tar --ignore-zeros: new option to support importing concatenated tars, #7432\n- debug id-hash / parse-obj / format-obj: new debug commands, #7406\n- transfer --compression=C --recompress=M: recompress while transferring, #7529\n- extract --continue: continue a previously interrupted extraction, #1356\n- prune --list-kept/--list-pruned: only list the kept (or pruned) archives, #7511\n- prune --short/--format: enable users to format the list output, #3238\n- implement BORG_<CMD>_FORMAT env vars for prune, list, rlist, #5166\n- rlist: size and nfiles format keys\n- implement unix domain (ipc) socket support, #6183::\n\n      borg serve --socket  # server side (not started automatically!)\n      borg -r socket:///path/to/repo ...  # client side\n\n- add get_runtime_dir / BORG_RUNTIME_DIR (contains e.g. .sock and .pid file)\n- support shell-style alternatives, like: sh:image.{png,jpg}, #7602\n\nFixes:\n\n- do not retry on permission errors (pointless)\n- transfer: verify chunks we get using assert_id, #7383\n- fix config/cache dir compatibility issues, #7445\n- xattrs: fix namespace processing on FreeBSD, #6997\n- ProgressIndicatorPercent: fix space computation for wide chars, #3027\n- delete: remove --cache-only option, #7440.\n  for deleting the cache only, use: borg rdelete --cache-only\n- borg debug get-obj/put-obj: fixed chunk id\n- create: ignore empty paths, print warning, #5637\n- extract: support extraction of atime/mtime on win32\n- benchmark crud: use TemporaryDirectory below given path, #4706\n- Ensure that cli options specified with action=Highlander can only be set once, even\n  if the set value is a default value. Add tests for action=Highlander, #7500, #6269.\n- Fix argparse error messages from misc. validators (being more specific).\n- put security infos into data dir, add BORG_DATA_DIR env var, #5760\n- setup.cfg: remove setup_requires (we have a pyproject.toml for that), #7574\n- do not crash for empty archives list in borg rlist date based matching, #7522\n- sanitize paths during archive creation and extraction, #7108 #7099\n- make sure we do not get backslashes into item paths\n\nOther changes:\n\n- allow msgpack 1.0.5 also\n- development.lock.txt: upgrade cython to 0.29.35, misc. other upgrades\n- clarify platformdirs requirements, #7393.\n  3.0.0 is only required for macOS due to breaking changes.\n  2.6.0 was the last breaking change for Linux/UNIX.\n- mount: improve mountpoint error msgs, see #7496\n- more Highlander options, #6269\n- Windows: simplify building (just use pip)\n- refactor toplevel exception handling, #6018\n- remove nonce management, related repo methods (not needed for borg2)\n- borg.remote: remove support for borg < 1.1.0\n  ($LOG, logging setup, exceptions, rpc tuple data format, version)\n- new remote and progress logging, #7604\n- borg.logger: add logging debugging functionality\n- add function to clear empty directories at end of compact process\n- unify scanning and listing of segment dirs / segment files, #7597\n- replace `LRUCache` internals with `OrderedDict`\n- docs:\n\n  - add installation instructions for Windows\n  - improve --one-file-system help and docs (macOS APFS), #5618 #4876\n  - BORG_KEY_FILE: clarify docs, #7444\n  - installation: add link to OS dependencies, #7356\n  - update FAQ about locale/unicode issues, #6999\n  - improve mount options rendering, #7359\n  - make timestamps in manual pages reproducible.\n  - describe performing pull-backups via ssh remote forwarding\n  - suggest to use forced command when using remote-forwarding via ssh\n  - fix some -a / --match-archives docs issues\n  - incl./excl. options header, clarify --path-from-stdin exclusive control\n  - add note about MAX_DATA_SIZE\n  - update security support docs\n  - improve patterns help\n\n- CI / tests / vagrant:\n\n  - added pre-commit for linting purposes, #7476\n  - resolved mode bug and added sleep clause for darwin systems, #7470\n  - \"auto\" compressor tests: do not assume zlib is better than lz4, #7363\n  - add stretch64 VM with deps built from source\n  - misc. other CI / test fixes and updates\n  - vagrant: add lunar64 VM, fix packages_netbsd\n  - avoid long ids in pytest output\n  - tox: package = editable-legacy, #7580\n  - tox under fakeroot: fix finding setup_docs, #7391\n  - check buzhash chunksize distribution, #7586\n  - use debian/bookworm64 box\n\n\nVersion 2.0.0b5 (2023-02-27)\n----------------------------\n\nNew features:\n\n- create: implement retries for individual fs files\n  (e.g. if a file changed while we read it, if a file had an OSError)\n- info: add used storage quota, #7121\n- transfer: support --progress\n- create/recreate/import-tar: add --checkpoint-volume option\n- support date-based matching for archive selection,\n  add --newer/--older/--newest/--oldest options, #7062 #7296\n\nFixes:\n\n- disallow --list with --progress, #7219\n- create: fix --list --dry-run output for directories, #7209\n- do no assume hardlink_master=True if not present, #7175\n- fix item_ptrs orphaned chunks of checkpoint archives\n- avoid orphan content chunks on BackupOSError, #6709\n- transfer: fix bug in obfuscated data upgrade code\n- fs.py: fix bug in f-string (thanks mypy!)\n- recreate: when --target is given, do not detect \"nothing to do\", #7254\n- locking (win32): deal with os.rmdir/listdir PermissionErrors\n- locking: thread id must be parsed as hex from lock file name\n- extract: fix mtime when ResourceFork xattr is set (macOS specific), #7234\n- recreate: without --chunker-params borg shall not rechunk, #7336\n- allow mixing --progress and --list in log-json mode\n- add \"files changed while reading\" to Statistics class, #7354\n- fixed keys determination in Statistics.__add__(), #7355\n\nOther changes:\n\n- use local time / local timezone to output timestamps, #7283\n- update development.lock.txt, including a setuptools security fix, #7227\n- remove --save-space option (does not change behaviour)\n- remove part files from final archive\n- remove --consider-part-files, related stats code, update docs\n- transfer: drop part files\n- check: show id of orphaned chunks\n- ArchiveItem.cmdline list-of-str -> .command_line str, #7246\n- Item: symlinks: rename .source to .target, #7245\n- Item: make user/group/uid/gid optional\n- create: do not store user/group for stdin data by default, #7249\n- extract: chown only if we have u/g info in archived item, #7249\n- export-tar: for items w/o uid/gid, default to 0/0, #7249\n- fix some uid/gid lookup code / tests for win32\n- cache.py: be less verbose during cache sync\n- update bash completion script commands and options, #7273\n- require and use platformdirs 3.x.x package, tests\n- better included/excluded status chars, docs, #7321\n- undef NDEBUG for chunker and hashindex (make assert() work)\n- assert_id: better be paranoid (add back same crypto code as in old borg), #7362\n- check --verify_data: always decompress and call assert_id(), #7362\n- make hashindex_compact simpler and probably faster, minor fixes, cleanups, more tests\n- hashindex minor fixes, refactor, tweaks, tests\n- pyinstaller: remove icon\n- validation / placeholders / JSON:\n\n  - implement (text|binary)_to_json: key (text), key_b64 (base64(binary))\n  - remove bpath, barchive, bcomment placeholders / JSON keys\n  - archive metadata: make sure hostname and username have no surrogate escapes\n  - text attributes (like archive name, comment): validate more strictly, #2290\n  - transfer: validate archive names and comment before transfer\n  - json output: use text_to_json (path, target), #6151\n- docs:\n\n  - docs and comments consistency, readability and spelling fixes\n  - fix --progress display description, #7180\n  - document how borg deals with non-unicode bytes in JSON output\n  - document another way to get UTF-8 encoding on stdin/stdout/stderr, #2273\n  - pruning interprets timestamps in the local timezone where borg prune runs\n  - shellpattern: add license, use copyright/license markup\n  - key change-passphrase: fix --encryption value in examples\n  - remove BORG_LIBB2_PREFIX (not used any more)\n  - Installation: Update Fedora in distribution list, #7357\n  - add .readthedocs.yaml (use py311, use non-shallow clone)\n- tests:\n\n  - fix archiver tests on Windows, add running the tests to Windows CI\n  - fix tox4 passenv issue, #7199\n  - github actions updates (fix deprecation warnings)\n  - add tests for borg transfer/upgrade\n  - fix test hanging reading FIFO when `borg create` failed\n  - mypy inspired fixes / updates\n  - fix prune tests, prune in localtime\n  - do not look up uid 0 / gid 0, but current process uid/gid\n  - safe_unlink tests: use os.link to support win32 also\n  - fix test_size_on_disk_accurate for large st_blksize, #7250\n  - relaxed timestamp comparisons, use same_ts_ns\n  - add test for extracted directory mtime\n  - use \"fail\" chunker to test erroneous input file skipping\n\n\nVersion 2.0.0b4 (2022-11-27)\n----------------------------\n\nFixes:\n\n- transfer/upgrade: fix borg < 1.2 chunker_params, #7079\n- transfer/upgrade: do not access Item._dict, #7077\n- transfer/upgrade: fix crash in borg transfer, #7156\n- archive.save(): always use metadata from stats, #7072\n- benchmark: fixed TypeError in compression benchmarks, #7075\n- fix repository.scan api minimum requirement\n- fix args.paths related argparsing, #6994\n\nOther changes:\n\n- tar_filter: recognize .tar.zst as zstd, #7093\n- adding performance statistics to borg create, #6991\n- docs: add rcompress to usage index\n- tests:\n\n  - use github and MSYS2 for Windows CI, #7097\n  - win32 and cygwin: test fixes / skip hanging test\n  - vagrant / github CI: use python 3.11.0 / 3.10.8\n- vagrant:\n\n  - upgrade pyinstaller to 5.6.2 (supports python 3.11)\n  - use python 3.11 to build the borg binary\n\nVersion 2.0.0b3 (2022-10-02)\n----------------------------\n\nFixes:\n\n- transfer: fix user/group == None crash with borg1 archives\n- compressors: avoid memoryview related TypeError\n- check: fix uninitialised variable if repo is completely empty, #7034\n- do not use version_tuple placeholder in setuptools_scm template, #7024\n- get_chunker: fix missing sparse=False argument, #7056\n\nNew features:\n\n- rcompress: do a repo-wide (re)compression, #7037\n- implement pattern support for --match-archives, #6504\n- BORG_LOCK_WAIT=n env var to set default for --lock-wait option, #5279\n\nOther:\n\n- repository.scan: misc. fixes / improvements\n- metadata: differentiate between empty/zero and unknown, #6908\n- CI: test pyfuse3 with python 3.11\n- use more relative imports\n- make borg.testsuite.archiver a package, split archiver tests into many modules\n- support reading new, improved hashindex header format, #6960.\n  added version number and num_empty to the HashHeader, fixed alignment.\n- vagrant: upgrade pyinstaller 4.10 -> 5.4.1, use python 3.9.14 for binary build\n- item.pyx: use more Cython (faster, uses less memory), #5763\n\n\nVersion 2.0.0b2 (2022-09-10)\n----------------------------\n\nBug fixes:\n\n- xattrs / extended stat: improve exception handling, #6988\n- fix and refactor replace_placeholders, #6966\n\nNew features:\n\n- support archive timestamps with utc offsets, adapt them when using\n  borg transfer to transfer from borg 1.x repos (append +00:00 for UTC).\n- create/recreate/import-tar --timestamp: accept giving timezone via\n  its utc offset. defaults to local timezone, if no utc offset is given.\n\nOther changes:\n\n- chunks: have separate encrypted metadata (ctype, clevel, csize, size)\n\n  chunk = enc_meta_len16 + encrypted(msgpacked(meta)) + encrypted(compressed(data)).\n\n  this breaks repo format compatibility, you need to create fresh repos!\n- repository api: flags support, #6982\n- OpenBSD only - statically link OpenSSL, #6474.\n  Avoid conflicting with shared libcrypto from the base OS pulled in via dependencies.\n- restructured source code\n- update diagrams to odg format, #6928\n\nVersion 2.0.0b1 (2022-08-08)\n----------------------------\n\nNew features:\n\n- massively increase archive metadata stream size limit, #1473.\n  currently rather testing the code, scalability will improve later, see #6945.\n- rcreate --copy-crypt-key: copy crypt_key from key of other repo, #6710.\n  default: create new, random authenticated encryption key.\n- prune/delete --checkpoint-interval=1800 and ctrl-c/SIGINT support, #6284\n\nFixes:\n\n- ctrl-c must not kill important subprocesses, #6912\n- transfer: check whether ID hash method and chunker secret are same.\n  add PlaintextKey and AuthenticatedKey support to uses_same_id_hash function.\n- check: try harder to create the key, #5719\n- SaveFile: use a custom mkstemp with mode support, #6933, #6400\n- make setuptools happy, #6874\n- fix misc. compiler warnings\n- list: fix {flags:<WIDTH>} formatting, #6081\n\nOther changes:\n\n- new crypto does not need to call ._assert_id(), update code and docs.\n  https://github.com/borgbackup/borg/pull/6463#discussion_r925436156\n- check: --verify-data does not need to decompress with new crypto modes\n- Key: crypt_key instead of enc_key + enc_hmac_key, #6611\n- misc. docs updates and improvements\n- CI: test on macOS 12 without fuse / fuse tests\n- repository: add debug logging for issue #6687\n- _version.py: remove trailing blank, add LF at EOF (make pep8 checker happy)\n\n\nVersion 2.0.0a4 (2022-07-17)\n----------------------------\n\nNew features:\n\n- recreate: consider level for recompression, #6698, #3622\n\nOther changes:\n\n- stop using libdeflate\n- CI: add mypy (if we add type hints, it can do type checking)\n- big changes to the source code:\n\n  - split up archiver module, transform it into a package\n  - use Black for automated code formatting\n  - remove some legacy code\n  - adapt/fix code for mypy\n- use language_level = 3str for cython (this will be the default in cython 3)\n- docs: document HardLinkManager and hlid, #2388\n\n\nVersion 2.0.0a3 (2022-07-04)\n----------------------------\n\nFixes:\n\n- check repo version, accept old repos only for --other-repo (e.g. rcreate/transfer).\n  v2 is the default repo version for borg 2.0. v1 repos must only be used in a\n  read-only way, e.g. for --other-repo=V1_REPO with borg init and borg transfer!\n\nNew features:\n\n- transfer: --upgrader=NoOp is the default.\n  This is to support general-purpose transfer of archives between related borg2\n  repos.\n- transfer: --upgrader=From12To20 must be used to transfer (and convert) archives\n  from borg 1.2 repos to borg 2.0 repos.\n\nOther changes:\n\n- removed some deprecated options\n- removed -P (aka --prefix) option, #6806. The option -a (aka --glob-archives)\n  can be used for same purpose and is more powerful, e.g.: -a 'PREFIX*'\n- rcreate: always use argon2 kdf for new repos, #6820\n- rcreate: remove legacy encryption modes for new repos, #6490\n\n\nVersion 2.0.0a2 (2022-06-26)\n----------------------------\n\nChanges:\n\n- split repo and archive name into separate args, #948\n\n  - use -r or --repo or BORG_REPO env var to give the repository\n  - use --other-repo or BORG_OTHER_REPO to give another repo (e.g. borg transfer)\n  - use positional argument for archive name or `-a ARCH_GLOB`\n- remove support for scp-style repo specification, use ssh://...\n- simplify stats output: repo ops -> repo stats, archive ops -> archive stats\n- repository index: add payload size (==csize) and flags to NSIndex entries\n- repository index: set/query flags, iteration over flagged items (NSIndex)\n- repository: sync write file in get_fd\n- stats: deduplicated size now, was deduplicated compressed size in borg 1.x\n- remove csize support at most places in the code (chunks index, stats, get_size,\n  Item.chunks)\n- replace problematic/ugly hardlink_master approach of borg 1.x by:\n\n  - symmetric hlid (all hardlinks pointing to same inode have same hlid)\n  - all archived hardlinked regular files have a chunks list\n- borg rcreate --other-repo=OTHER_REPO: reuse key material from OTHER_REPO, #6554.\n  This is useful if you want to use borg transfer to transfer archives from an\n  existing borg 1.1/1.2 repo. If the chunker secret and the id key and algorithm\n  stay the same, the deduplication will also work between past and future backups.\n- borg transfer:\n\n  - efficiently copy archives from a borg 1.1/1.2 repo to a new repo.\n    uses deduplication and does not decompress/recompress file content data.\n  - does some cleanups / fixes / conversions:\n\n    - disallow None value for .user/group/chunks/chunks_healthy\n    - cleanup msgpack related str/bytes mess, use new msgpack spec, #968\n    - obfuscation: fix byte order for size, #6701\n    - compression: use the 2 bytes for type and level, #6698\n    - use version 2 for new archives\n    - convert timestamps int/bigint -> msgpack.Timestamp, see #2323\n    - all hardlinks have chunks, maybe chunks_healthy, hlid\n    - remove the zlib type bytes hack\n    - make sure items with chunks have precomputed size\n    - removes the csize element from the tuples in the Item.chunks list\n    - clean item of attic 0.13 'acl' bug remnants\n- crypto: see 1.3.0a1 log entry\n- removed \"borg upgrade\" command (not needed any more)\n- compact: removed --cleanup-commits option\n- docs: fixed quickstart and usage docs with new cli command syntax\n- docs: removed the parts talking about potential AES-CTR mode issues\n  (we will not use that any more).\n\n\nVersion 1.3.0a1 (2022-04-15)\n----------------------------\n\nAlthough this was released as 1.3.0a1, it can be also seen as 2.0.0a1 as it was\nlater decided to do breaking changes and thus the major release number had to\nbe increased (thus, there will not be a 1.3.0 release, but 2.0.0).\n\nNew features:\n\n- init: new --encryption=(repokey|keyfile)-[blake2-](aes-ocb|chacha20-poly1305)\n\n  - New, better, faster crypto (see encryption-aead diagram in the docs), #6463.\n  - New AEAD cipher suites: AES-OCB and CHACHA20-POLY1305.\n  - Session keys are derived via HKDF from random session id and master key.\n  - Nonces/MessageIVs are counters starting from 0 for each session.\n  - AAD: chunk id, key type, messageIV, sessionID are now authenticated also.\n  - Solves the potential AES-CTR mode counter management issues of the legacy crypto.\n- init: --key-algorithm=argon2 (new default KDF, older pbkdf2 also still available)\n\n  borg key change-passphrase / change-location keeps the key algorithm unchanged.\n- key change-algorithm: to upgrade existing keys to argon2 or downgrade to pbkdf2.\n\n  We recommend you to upgrade unless you have to keep the key compatible with older versions of borg.\n- key change-location: usable for repokey <-> keyfile location change\n- benchmark cpu: display benchmarks of cpu bound stuff\n- export-tar: new --tar-format=PAX (default: GNU)\n- import-tar/export-tar: can use PAX format for ctime and atime support\n- import-tar/export-tar: --tar-format=BORG: roundtrip ALL item metadata, #5830\n- repository: create and use version 2 repos only for now\n- repository: implement PUT2: header crc32, overall xxh64, #1704\n\nOther changes:\n\n- require python >= 3.9, #6315\n- simplify libs setup, #6482\n- unbundle most bundled 3rd party code, use libs, #6316\n- use libdeflate.crc32 (Linux and all others) or zlib.crc32 (macOS)\n- repository: code cleanups / simplifications\n- internal crypto api: speedups / cleanups / refactorings / modernisation\n- remove \"borg upgrade\" support for \"attic backup\" repos\n- remove PassphraseKey code and borg key migrate-to-repokey command\n- OpenBSD: build borg with OpenSSL (not: LibreSSL), #6474\n- remove support for LibreSSL, #6474\n- remove support for OpenSSL < 1.1.1\n"
  },
  {
    "path": "docs/changes_0.x.rst",
    "content": ".. _changelog_0x:\n\nChange Log 0.x\n==============\n\nVersion 0.30.0 (2016-01-23)\n---------------------------\n\nCompatibility notes:\n\n- The new default logging level is WARNING. Previously, it was INFO, which was\n  more verbose. Use -v (or --info) to show once again log level INFO messages.\n  See the \"general\" section in the usage docs.\n- For borg create, you need --list (in addition to -v) to see the long file\n  list (was needed so you can have e.g. --stats alone without the long list)\n- See below about BORG_DELETE_I_KNOW_WHAT_I_AM_DOING (was:\n  BORG_CHECK_I_KNOW_WHAT_I_AM_DOING)\n\nBug fixes:\n\n- fix crash when using borg create --dry-run --keep-tag-files, #570\n- make sure teardown with cleanup happens for Cache and RepositoryCache,\n  avoiding leftover locks and TEMP dir contents, #285 (partially), #548\n- fix locking KeyError, partial fix for #502\n- log stats consistently, #526\n- add abbreviated weekday to timestamp format, fixes #496\n- strip whitespace when loading exclusions from file\n- unset LD_LIBRARY_PATH before invoking ssh, fixes strange OpenSSL library\n  version warning when using the borg binary, #514\n- add some error handling/fallback for C library loading, #494\n- added BORG_DELETE_I_KNOW_WHAT_I_AM_DOING for check in \"borg delete\", #503\n- remove unused \"repair\" rpc method name\n\nNew features:\n\n- borg create: implement exclusions using regular expression patterns.\n- borg create: implement inclusions using patterns.\n- borg extract: support patterns, #361\n- support different styles for patterns:\n\n  - fnmatch (`fm:` prefix, default when omitted), like borg <= 0.29.\n  - shell (`sh:` prefix) with `*` not matching directory separators and\n    `**/` matching 0..n directories\n  - path prefix (`pp:` prefix, for unifying borg create pp1 pp2 into the\n    patterns system), semantics like in borg <= 0.29\n  - regular expression (`re:`), new!\n- --progress option for borg upgrade (#291) and borg delete <archive>\n- update progress indication more often (e.g. for borg create within big\n  files or for borg check repo), #500\n- finer chunker granularity for items metadata stream, #547, #487\n- borg create --list is now used (in addition to -v) to enable the verbose\n  file list output\n- display borg version below tracebacks, #532\n\nOther changes:\n\n- hashtable size (and thus: RAM and disk consumption) follows a growth policy:\n  grows fast while small, grows slower when getting bigger, #527\n- Vagrantfile: use pyinstaller 3.1 to build binaries, freebsd sqlite3 fix,\n  fixes #569\n- no separate binaries for centos6 any more because the generic linux binaries\n  also work on centos6 (or in general: on systems with a slightly older glibc\n  than debian7\n- dev environment: require virtualenv<14.0 so we get a py32 compatible pip\n- docs:\n\n  - add space-saving chunks.archive.d trick to FAQ\n  - important: clarify -v and log levels in usage -> general, please read!\n  - sphinx configuration: create a simple man page from usage docs\n  - add a repo server setup example\n  - disable unneeded SSH features in authorized_keys examples for security.\n  - borg prune only knows \"--keep-within\" and not \"--within\"\n  - add gource video to resources docs, #507\n  - add netbsd install instructions\n  - authors: make it more clear what refers to borg and what to attic\n  - document standalone binary requirements, #499\n  - rephrase the mailing list section\n  - development docs: run build_api and build_usage before tagging release\n  - internals docs: hash table max. load factor is 0.75 now\n  - markup, typo, grammar, phrasing, clarifications and other fixes.\n  - add gcc gcc-c++ to redhat/fedora/corora install docs, fixes #583\n\n\nVersion 0.29.0 (2015-12-13)\n---------------------------\n\nCompatibility notes:\n\n- When upgrading to 0.29.0, you need to upgrade client as well as server\n  installations due to the locking and command-line interface changes; otherwise\n  you'll get an error message about an RPC protocol mismatch or a wrong command-line\n  option.\n  If you run a server that needs to support both old and new clients, it is\n  suggested that you have a \"borg-0.28.2\" and a \"borg-0.29.0\" command.\n  clients then can choose via e.g. \"borg --remote-path=borg-0.29.0 ...\".\n- The default waiting time for a lock changed from infinity to 1 second for a\n  better interactive user experience. If the repo you want to access is\n  currently locked, borg will now terminate after 1s with an error message.\n  If you have scripts that should wait for the lock for a longer time, use\n  --lock-wait N (with N being the maximum wait time in seconds).\n\nBug fixes:\n\n- hash table tuning (better chosen hashtable load factor 0.75 and prime initial\n  size of 1031 gave ~1000x speedup in some scenarios)\n- avoid creation of an orphan lock for one case, #285\n- --keep-tag-files: fix file mode and multiple tag files in one directory, #432\n- fixes for \"borg upgrade\" (attic repo converter), #466\n- remove --progress isatty magic (and also --no-progress option) again, #476\n- borg init: display proper repo URL\n- fix format of umask in help pages, #463\n\nNew features:\n\n- implement --lock-wait, support timeout for UpgradableLock, #210\n- implement borg break-lock command, #157\n- include system info below traceback, #324\n- sane remote logging, remote stderr, #461:\n\n  - remote log output: intercept it and log it via local logging system,\n    with \"Remote: \" prefixed to message. log remote tracebacks.\n  - remote stderr: output it to local stderr with \"Remote: \" prefixed.\n- add --debug and --info (same as --verbose) to set the log level of the\n  builtin logging configuration (which otherwise defaults to warning), #426\n  note: there are few messages emitted at DEBUG level currently.\n- optionally configure logging via env var BORG_LOGGING_CONF\n- add --filter option for status characters: e.g. to show only the added\n  or modified files (and also errors), use \"borg create -v --filter=AME ...\".\n- more progress indicators, #394\n- use ISO-8601 date and time format, #375\n- \"borg check --prefix\" to restrict archive checking to that name prefix, #206\n\nOther changes:\n\n- hashindex_add C implementation (speed up cache re-sync for new archives)\n- increase FUSE read_size to 1024 (speed up metadata operations)\n- check/delete/prune --save-space: free unused segments quickly, #239\n- increase rpc protocol version to 2 (see also Compatibility notes), #458\n- silence borg by default (via default log level WARNING)\n- get rid of C compiler warnings, #391\n- upgrade OS X FUSE to 3.0.9 on the OS X binary build system\n- use python 3.5.1 to build binaries\n- docs:\n\n  - new mailing list borgbackup@python.org, #468\n  - readthedocs: color and logo improvements\n  - load coverage icons over SSL (avoids mixed content)\n  - more precise binary installation steps\n  - update release procedure docs about OS X FUSE\n  - FAQ entry about unexpected 'A' status for unchanged file(s), #403\n  - add docs about 'E' file status\n  - add \"borg upgrade\" docs, #464\n  - add developer docs about output and logging\n  - clarify encryption, add note about client-side encryption\n  - add resources section, with videos, talks, presentations, #149\n  - Borg moved to Arch Linux [community]\n  - fix wrong installation instructions for archlinux\n\n\nVersion 0.28.2 (2015-11-15)\n---------------------------\n\nNew features:\n\n- borg create --exclude-if-present TAGFILE - exclude directories that have the\n  given file from the backup. You can additionally give --keep-tag-files to\n  preserve just the directory roots and the tag-files (but not back up other\n  directory contents), #395, attic #128, attic #142\n\nOther changes:\n\n- do not create docs sources at build time (just have them in the repo),\n  completely remove have_cython() hack, do not use the \"mock\" library at build\n  time, #384\n- avoid hidden import, make it easier for PyInstaller, easier fix for #218\n- docs:\n\n  - add description of item flags / status output, fixes #402\n  - explain how to regenerate usage and API files (build_api or\n    build_usage) and when to commit usage files directly into git, #384\n  - minor install docs improvements\n\n\nVersion 0.28.1 (2015-11-08)\n---------------------------\n\nBug fixes:\n\n- do not try to build api / usage docs for production install,\n  fixes unexpected \"mock\" build dependency, #384\n\nOther changes:\n\n- avoid using msgpack.packb at import time\n- fix formatting issue in changes.rst\n- fix build on readthedocs\n\n\nVersion 0.28.0 (2015-11-08)\n---------------------------\n\nCompatibility notes:\n\n- changed return codes (exit codes), see docs. in short:\n  old: 0 = ok, 1 = error. now: 0 = ok, 1 = warning, 2 = error\n\nNew features:\n\n- refactor return codes (exit codes), fixes #61\n- add --show-rc option enable \"terminating with X status, rc N\" output, fixes 58, #351\n- borg create backups atime and ctime additionally to mtime, fixes #317\n  - extract: support atime additionally to mtime\n  - FUSE: support ctime and atime additionally to mtime\n- support borg --version\n- emit a warning if we have a slow msgpack installed\n- borg list --prefix=thishostname- REPO, fixes #205\n- Debug commands (do not use except if you know what you do: debug-get-obj,\n  debug-put-obj, debug-delete-obj, debug-dump-archive-items.\n\nBug fixes:\n\n- setup.py: fix bug related to BORG_LZ4_PREFIX processing\n- fix \"check\" for repos that have incomplete chunks, fixes #364\n- borg mount: fix unlocking of repository at umount time, fixes #331\n- fix reading files without touching their atime, #334\n- non-ascii ACL fixes for Linux, FreeBSD and OS X, #277\n- fix acl_use_local_uid_gid() and add a test for it, attic #359\n- borg upgrade: do not upgrade repositories in place by default, #299\n- fix cascading failure with the index conversion code, #269\n- borg check: implement 'cmdline' archive metadata value decoding, #311\n- fix RobustUnpacker, it missed some metadata keys (new atime and ctime keys\n  were missing, but also bsdflags). add check for unknown metadata keys.\n- create from stdin: also save atime, ctime (cosmetic)\n- use default_notty=False for confirmations, fixes #345\n- vagrant: fix msgpack installation on centos, fixes #342\n- deal with unicode errors for symlinks in same way as for regular files and\n  have a helpful warning message about how to fix wrong locale setup, fixes #382\n- add ACL keys the RobustUnpacker must know about\n\nOther changes:\n\n- improve file size displays, more flexible size formatters\n- explicitly commit to the units standard, #289\n- archiver: add E status (means that an error occurred when processing this\n  (single) item\n- do binary releases via \"github releases\", closes #214\n- create: use -x and --one-file-system (was: --do-not-cross-mountpoints), #296\n- a lot of changes related to using \"logging\" module and screen output, #233\n- show progress display if on a tty, output more progress information, #303\n- factor out status output so it is consistent, fix surrogates removal,\n  maybe fixes #309\n- move away from RawConfigParser to ConfigParser\n- archive checker: better error logging, give chunk_id and sequence numbers\n  (can be used together with borg debug-dump-archive-items).\n- do not mention the deprecated passphrase mode\n- emit a deprecation warning for --compression N (giving a just a number)\n- misc .coverragerc fixes (and coverage measurement improvements), fixes #319\n- refactor confirmation code, reduce code duplication, add tests\n- prettier error messages, fixes #307, #57\n- tests:\n\n  - add a test to find disk-full issues, #327\n  - travis: also run tests on Python 3.5\n  - travis: use tox -r so it rebuilds the tox environments\n  - test the generated pyinstaller-based binary by archiver unit tests, #215\n  - vagrant: tests: announce whether fakeroot is used or not\n  - vagrant: add vagrant user to fuse group for debianoid systems also\n  - vagrant: llfuse install on darwin needs pkgconfig installed\n  - vagrant: use pyinstaller from develop branch, fixes #336\n  - benchmarks: test create, extract, list, delete, info, check, help, fixes #146\n  - benchmarks: test with both the binary and the python code\n  - archiver tests: test with both the binary and the python code, fixes #215\n  - make basic test more robust\n- docs:\n\n  - moved docs to borgbackup.readthedocs.org, #155\n  - a lot of fixes and improvements, use mobile-friendly RTD standard theme\n  - use zlib,6 compression in some examples, fixes #275\n  - add missing rename usage to docs, closes #279\n  - include the help offered by borg help <topic> in the usage docs, fixes #293\n  - include a list of major changes compared to attic into README, fixes #224\n  - add OS X install instructions, #197\n  - more details about the release process, #260\n  - fix linux glibc requirement (binaries built on debian7 now)\n  - build: move usage and API generation to setup.py\n  - update docs about return codes, #61\n  - remove api docs (too much breakage on rtd)\n  - borgbackup install + basics presentation (asciinema)\n  - describe the current style guide in documentation\n  - add section about debug commands\n  - warn about not running out of space\n  - add example for rename\n  - improve chunker params docs, fixes #362\n  - minor development docs update\n\n\nVersion 0.27.0 (2015-10-07)\n---------------------------\n\nNew features:\n\n- \"borg upgrade\" command - attic -> borg one time converter / migration, #21\n- temporary hack to avoid using lots of disk space for chunks.archive.d, #235:\n  To use it: rm -rf chunks.archive.d ; touch chunks.archive.d\n- respect XDG_CACHE_HOME, attic #181\n- add support for arbitrary SSH commands, attic #99\n- borg delete --cache-only REPO (only delete cache, not REPO), attic #123\n\n\nBug fixes:\n\n- use Debian 7 (wheezy) to build pyinstaller borgbackup binaries, fixes slow\n  down observed when running the Centos6-built binary on Ubuntu, #222\n- do not crash on empty lock.roster, fixes #232\n- fix multiple issues with the cache config version check, #234\n- fix segment entry header size check, attic #352\n  plus other error handling improvements / code deduplication there.\n- always give segment and offset in repo IntegrityErrors\n\n\nOther changes:\n\n- stop producing binary wheels, remove docs about it, #147\n- docs:\n  - add warning about prune\n  - generate usage include files only as needed\n  - development docs: add Vagrant section\n  - update / improve / reformat FAQ\n  - hint to single-file pyinstaller binaries from README\n\n\nVersion 0.26.1 (2015-09-28)\n---------------------------\n\nThis is a minor update, just docs and new pyinstaller binaries.\n\n- docs update about python and binary requirements\n- better docs for --read-special, fix #220\n- re-built the binaries, fix #218 and #213 (glibc version issue)\n- update web site about single-file pyinstaller binaries\n\nNote: if you did a python-based installation, there is no need to upgrade.\n\n\nVersion 0.26.0 (2015-09-19)\n---------------------------\n\nNew features:\n\n- Faster cache sync (do all in one pass, remove tar/compression stuff), #163\n- BORG_REPO env var to specify the default repo, #168\n- read special files as if they were regular files, #79\n- implement borg create --dry-run, attic issue #267\n- Normalize paths before pattern matching on OS X, #143\n- support OpenBSD and NetBSD (except xattrs/ACLs)\n- support / run tests on Python 3.5\n\nBug fixes:\n\n- borg mount repo: use absolute path, attic #200, attic #137\n- chunker: use off_t to get 64bit on 32bit platform, #178\n- initialize chunker fd to -1, so it's not equal to STDIN_FILENO (0)\n- fix reaction to \"no\" answer at delete repo prompt, #182\n- setup.py: detect lz4.h header file location\n- to support python < 3.2.4, add less buggy argparse lib from 3.2.6 (#194)\n- fix for obtaining ``char *`` from temporary Python value (old code causes\n  a compile error on Mint 17.2)\n- llfuse 0.41 install troubles on some platforms, require < 0.41\n  (UnicodeDecodeError exception due to non-ascii llfuse setup.py)\n- cython code: add some int types to get rid of unspecific python add /\n  subtract operations (avoid ``undefined symbol FPE_``... error on some platforms)\n- fix verbose mode display of stdin backup\n- extract: warn if a include pattern never matched, fixes #209,\n  implement counters for Include/ExcludePatterns\n- archive names with slashes are invalid, attic issue #180\n- chunker: add a check whether the POSIX_FADV_DONTNEED constant is defined -\n  fixes building on OpenBSD.\n\nOther changes:\n\n- detect inconsistency / corruption / hash collision, #170\n- replace versioneer with setuptools_scm, #106\n- docs:\n\n  - pkg-config is needed for llfuse installation\n  - be more clear about pruning, attic issue #132\n- unit tests:\n\n  - xattr: ignore security.selinux attribute showing up\n  - ext3 seems to need a bit more space for a sparse file\n  - do not test lzma level 9 compression (avoid MemoryError)\n  - work around strange mtime granularity issue on netbsd, fixes #204\n  - ignore st_rdev if file is not a block/char device, fixes #203\n  - stay away from the setgid and sticky mode bits\n- use Vagrant to do easy cross-platform testing (#196), currently:\n\n  - Debian 7 \"wheezy\" 32bit, Debian 8 \"jessie\" 64bit\n  - Ubuntu 12.04 32bit, Ubuntu 14.04 64bit\n  - Centos 7 64bit\n  - FreeBSD 10.2 64bit\n  - OpenBSD 5.7 64bit\n  - NetBSD 6.1.5 64bit\n  - Darwin (OS X Yosemite)\n\n\nVersion 0.25.0 (2015-08-29)\n---------------------------\n\nCompatibility notes:\n\n- lz4 compression library (liblz4) is a new requirement (#156)\n- the new compression code is very compatible: as long as you stay with zlib\n  compression, older borg releases will still be able to read data from a\n  repo/archive made with the new code (note: this is not the case for the\n  default \"none\" compression, use \"zlib,0\" if you want a \"no compression\" mode\n  that can be read by older borg). Also the new code is able to read repos and\n  archives made with older borg versions (for all zlib levels  0..9).\n\nDeprecations:\n\n- --compression N (with N being a number, as in 0.24) is deprecated.\n  We keep the --compression 0..9 for now not to break scripts, but it is\n  deprecated and will be removed later, so better fix your scripts now:\n  --compression 0 (as in 0.24) is the same as --compression zlib,0 (now).\n  BUT: if you do not want compression, use --compression none\n  (which is the default).\n  --compression 1 (in 0.24) is the same as --compression zlib,1 (now)\n  --compression 9 (in 0.24) is the same as --compression zlib,9 (now)\n\nNew features:\n\n- create --compression none (default, means: do not compress, just pass through\n  data \"as is\". this is more efficient than zlib level 0 as used in borg 0.24)\n- create --compression lz4 (super-fast, but not very high compression)\n- create --compression zlib,N (slower, higher compression, default for N is 6)\n- create --compression lzma,N (slowest, highest compression, default N is 6)\n- honor the nodump flag (UF_NODUMP) and do not back up such items\n- list --short just outputs a simple list of the files/directories in an archive\n\nBug fixes:\n\n- fixed --chunker-params parameter order confusion / malfunction, fixes #154\n- close fds of segments we delete (during compaction)\n- close files which fell out the lrucache\n- fadvise DONTNEED now is only called for the byte range actually read, not for\n  the whole file, fixes #158.\n- fix issue with negative \"all archives\" size, fixes #165\n- restore_xattrs: ignore if setxattr fails with EACCES, fixes #162\n\nOther changes:\n\n- remove fakeroot requirement for tests, tests run faster without fakeroot\n  (test setup does not fail any more without fakeroot, so you can run with or\n  without fakeroot), fixes #151 and #91.\n- more tests for archiver\n- recover_segment(): don't assume we have an fd for segment\n- lrucache refactoring / cleanup, add dispose function, py.test tests\n- generalize hashindex code for any key length (less hardcoding)\n- lock roster: catch file not found in remove() method and ignore it\n- travis CI: use requirements file\n- improved docs:\n\n  - replace hack for llfuse with proper solution (install libfuse-dev)\n  - update docs about compression\n  - update development docs about fakeroot\n  - internals: add some words about lock files / locking system\n  - support: mention BountySource and for what it can be used\n  - theme: use a lighter green\n  - add pypi, wheel, dist package based install docs\n  - split install docs into system-specific preparations and generic instructions\n\n\nVersion 0.24.0 (2015-08-09)\n---------------------------\n\nIncompatible changes (compared to 0.23):\n\n- borg now always issues --umask NNN option when invoking another borg via ssh\n  on the repository server. By that, it's making sure it uses the same umask\n  for remote repos as for local ones. Because of this, you must upgrade both\n  server and client(s) to 0.24.\n- the default umask is 077 now (if you do not specify via --umask) which might\n  be a different one as you used previously. The default umask avoids that\n  you accidentally give access permissions for group and/or others to files\n  created by borg (e.g. the repository).\n\nDeprecations:\n\n- \"--encryption passphrase\" mode is deprecated, see #85 and #97.\n  See the new \"--encryption repokey\" mode for a replacement.\n\nNew features:\n\n- borg create --chunker-params ... to configure the chunker, fixes #16\n  (attic #302, attic #300, and somehow also #41).\n  This can be used to reduce memory usage caused by chunk management overhead,\n  so borg does not create a huge chunks index/repo index and eats all your RAM\n  if you back up lots of data in huge files (like VM disk images).\n  See docs/misc/create_chunker-params.txt for more information.\n- borg info now reports chunk counts in the chunk index.\n- borg create --compression 0..9 to select zlib compression level, fixes #66\n  (attic #295).\n- borg init --encryption repokey (to store the encryption key into the repo),\n  fixes #85\n- improve at-end error logging, always log exceptions and set exit_code=1\n- LoggedIO: better error checks / exceptions / exception handling\n- implement --remote-path to allow non-default-path borg locations, #125\n- implement --umask M and use 077 as default umask for better security, #117\n- borg check: give a named single archive to it, fixes #139\n- cache sync: show progress indication\n- cache sync: reimplement the chunk index merging in C\n\nBug fixes:\n\n- fix segfault that happened for unreadable files (chunker: n needs to be a\n  signed size_t), #116\n- fix the repair mode, #144\n- repo delete: add destroy to allowed rpc methods, fixes issue #114\n- more compatible repository locking code (based on mkdir), maybe fixes #92\n  (attic #317, attic #201).\n- better Exception msg if no Borg is installed on the remote repo server, #56\n- create a RepositoryCache implementation that can cope with >2GiB,\n  fixes attic #326.\n- fix Traceback when running check --repair, attic #232\n- clarify help text, fixes #73.\n- add help string for --no-files-cache, fixes #140\n\nOther changes:\n\n- improved docs:\n\n  - added docs/misc directory for misc. writeups that won't be included\n    \"as is\" into the html docs.\n  - document environment variables and return codes (attic #324, attic #52)\n  - web site: add related projects, fix web site url, IRC #borgbackup\n  - Fedora/Fedora-based install instructions added to docs\n  - Cygwin-based install instructions added to docs\n  - updated AUTHORS\n  - add FAQ entries about redundancy / integrity\n  - clarify that borg extract uses the cwd as extraction target\n  - update internals doc about chunker params, memory usage and compression\n  - added docs about development\n  - add some words about resource usage in general\n  - document how to back up a raw disk\n  - add note about how to run borg from virtual env\n  - add solutions for (ll)fuse installation problems\n  - document what borg check does, fixes #138\n  - reorganize borgbackup.github.io sidebar, prev/next at top\n  - deduplicate and refactor the docs / README.rst\n\n- use borg-tmp as prefix for temporary files / directories\n- short prune options without \"keep-\" are deprecated, do not suggest them\n- improved tox configuration\n- remove usage of unittest.mock, always use mock from pypi\n- use entrypoints instead of scripts, for better use of the wheel format and\n  modern installs\n- add requirements.d/development.txt and modify tox.ini\n- use travis-ci for testing based on Linux and (new) OS X\n- use coverage.py, pytest-cov and codecov.io for test coverage support\n\nI forgot to list some stuff already implemented in 0.23.0, here they are:\n\nNew features:\n\n- efficient archive list from manifest, meaning a big speedup for slow\n  repo connections and \"list <repo>\", \"delete <repo>\", \"prune\" (attic #242,\n  attic #167)\n- big speedup for chunks cache sync (esp. for slow repo connections), fixes #18\n- hashindex: improve error messages\n\nOther changes:\n\n- explicitly specify binary mode to open binary files\n- some easy micro optimizations\n\n\nVersion 0.23.0 (2015-06-11)\n---------------------------\n\nIncompatible changes (compared to attic, fork related):\n\n- changed sw name and cli command to \"borg\", updated docs\n- package name (and name in urls) uses \"borgbackup\" to have fewer collisions\n- changed repo / cache internal magic strings from ATTIC* to BORG*,\n  changed cache location to .cache/borg/ - this means that it currently won't\n  accept attic repos (see issue #21 about improving that)\n\nBug fixes:\n\n- avoid defect python-msgpack releases, fixes attic #171, fixes attic #185\n- fix traceback when trying to do unsupported passphrase change, fixes attic #189\n- datetime does not like the year 10.000, fixes attic #139\n- fix \"info\" all archives stats, fixes attic #183\n- fix parsing with missing microseconds, fixes attic #282\n- fix misleading hint the fuse ImportError handler gave, fixes attic #237\n- check unpacked data from RPC for tuple type and correct length, fixes attic #127\n- fix Repository._active_txn state when lock upgrade fails\n- give specific path to xattr.is_enabled(), disable symlink setattr call that\n  always fails\n- fix test setup for 32bit platforms, partial fix for attic #196\n- upgraded versioneer, PEP440 compliance, fixes attic #257\n\nNew features:\n\n- less memory usage: add global option --no-cache-files\n- check --last N (only check the last N archives)\n- check: sort archives in reverse time order\n- rename repo::oldname newname (rename repository)\n- create -v output more informative\n- create --progress (backup progress indicator)\n- create --timestamp (utc string or reference file/dir)\n- create: if \"-\" is given as path, read binary from stdin\n- extract: if --stdout is given, write all extracted binary data to stdout\n- extract --sparse (simple sparse file support)\n- extra debug information for 'fread failed'\n- delete <repo> (deletes whole repo + local cache)\n- FUSE: reflect deduplication in allocated blocks\n- only allow whitelisted RPC calls in server mode\n- normalize source/exclude paths before matching\n- use posix_fadvise not to spoil the OS cache, fixes attic #252\n- toplevel error handler: show tracebacks for better error analysis\n- sigusr1 / sigint handler to print current file infos - attic PR #286\n- RPCError: include the exception args we get from remote\n\nOther changes:\n\n- source: misc. cleanups, pep8, style\n- docs and faq improvements, fixes, updates\n- cleanup crypto.pyx, make it easier to adapt to other AES modes\n- do os.fsync like recommended in the python docs\n- source: Let chunker optionally work with os-level file descriptor.\n- source: Linux: remove duplicate os.fsencode calls\n- source: refactor _open_rb code a bit, so it is more consistent / regular\n- source: refactor indicator (status) and item processing\n- source: use py.test for better testing, flake8 for code style checks\n- source: fix tox >=2.0 compatibility (test runner)\n- pypi package: add python version classifiers, add FreeBSD to platforms\n\n\nAttic Changelog\n---------------\n\nHere you can see the full list of changes between each Attic release until Borg\nforked from Attic:\n\nVersion 0.17\n~~~~~~~~~~~~\n\n(bugfix release, released on X)\n\n- Fix hashindex ARM memory alignment issue (#309)\n- Improve hashindex error messages (#298)\n\nVersion 0.16\n~~~~~~~~~~~~\n\n(bugfix release, released on May 16, 2015)\n\n- Fix typo preventing the security confirmation prompt from working (#303)\n- Improve handling of systems with improperly configured file system encoding (#289)\n- Fix \"All archives\" output for attic info. (#183)\n- More user friendly error message when repository key file is not found (#236)\n- Fix parsing of iso 8601 timestamps with zero microseconds (#282)\n\nVersion 0.15\n~~~~~~~~~~~~\n\n(bugfix release, released on Apr 15, 2015)\n\n- xattr: Be less strict about unknown/unsupported platforms (#239)\n- Reduce repository listing memory usage (#163).\n- Fix BrokenPipeError for remote repositories (#233)\n- Fix incorrect behavior with two character directory names (#265, #268)\n- Require approval before accessing relocated/moved repository (#271)\n- Require approval before accessing previously unknown unencrypted repositories (#271)\n- Fix issue with hash index files larger than 2GB.\n- Fix Python 3.2 compatibility issue with noatime open() (#164)\n- Include missing pyx files in dist files (#168)\n\nVersion 0.14\n~~~~~~~~~~~~\n\n(feature release, released on Dec 17, 2014)\n\n- Added support for stripping leading path segments (#95)\n  \"attic extract --strip-segments X\"\n- Add workaround for old Linux systems without acl_extended_file_no_follow (#96)\n- Add MacPorts' path to the default openssl search path (#101)\n- HashIndex improvements, eliminates unnecessary IO on low memory systems.\n- Fix \"Number of files\" output for attic info. (#124)\n- limit create file permissions so files aren't read while restoring\n- Fix issue with empty xattr values (#106)\n\nVersion 0.13\n~~~~~~~~~~~~\n\n(feature release, released on Jun 29, 2014)\n\n- Fix sporadic \"Resource temporarily unavailable\" when using remote repositories\n- Reduce file cache memory usage (#90)\n- Faster AES encryption (utilizing AES-NI when available)\n- Experimental Linux, OS X and FreeBSD ACL support (#66)\n- Added support for backup and restore of BSDFlags (OSX, FreeBSD) (#56)\n- Fix bug where xattrs on symlinks were not correctly restored\n- Added cachedir support. CACHEDIR.TAG compatible cache directories\n  can now be excluded using ``--exclude-caches`` (#74)\n- Fix crash on extreme mtime timestamps (year 2400+) (#81)\n- Fix Python 3.2 specific lockf issue (EDEADLK)\n\nVersion 0.12\n~~~~~~~~~~~~\n\n(feature release, released on April 7, 2014)\n\n- Python 3.4 support (#62)\n- Various documentation improvements a new style\n- ``attic mount`` now supports mounting an entire repository not only\n  individual archives (#59)\n- Added option to restrict remote repository access to specific path(s):\n  ``attic serve --restrict-to-path X`` (#51)\n- Include \"all archives\" size information in \"--stats\" output. (#54)\n- Added ``--stats`` option to ``attic delete`` and ``attic prune``\n- Fixed bug where ``attic prune`` used UTC instead of the local time zone\n  when determining which archives to keep.\n- Switch to SI units (Power of 1000 instead 1024) when printing file sizes\n\nVersion 0.11\n~~~~~~~~~~~~\n\n(feature release, released on March 7, 2014)\n\n- New \"check\" command for repository consistency checking (#24)\n- Documentation improvements\n- Fix exception during \"attic create\" with repeated files (#39)\n- New \"--exclude-from\" option for attic create/extract/verify.\n- Improved archive metadata deduplication.\n- \"attic verify\" has been deprecated. Use \"attic extract --dry-run\" instead.\n- \"attic prune --hourly|daily|...\" has been deprecated.\n  Use \"attic prune --keep-hourly|daily|...\" instead.\n- Ignore xattr errors during \"extract\" if not supported by the filesystem. (#46)\n\nVersion 0.10\n~~~~~~~~~~~~\n\n(bugfix release, released on Jan 30, 2014)\n\n- Fix deadlock when extracting 0 sized files from remote repositories\n- \"--exclude\" wildcard patterns are now properly applied to the full path\n  not just the file name part (#5).\n- Make source code endianness agnostic (#1)\n\nVersion 0.9\n~~~~~~~~~~~\n\n(feature release, released on Jan 23, 2014)\n\n- Remote repository speed and reliability improvements.\n- Fix sorting of segment names to ignore NFS left over files. (#17)\n- Fix incorrect display of time (#13)\n- Improved error handling / reporting. (#12)\n- Use fcntl() instead of flock() when locking repository/cache. (#15)\n- Let ssh figure out port/user if not specified so we don't override .ssh/config (#9)\n- Improved libcrypto path detection (#23).\n\nVersion 0.8.1\n~~~~~~~~~~~~~\n\n(bugfix release, released on Oct 4, 2013)\n\n- Fix segmentation fault issue.\n\nVersion 0.8\n~~~~~~~~~~~\n\n(feature release, released on Oct 3, 2013)\n\n- Fix xattr issue when backing up sshfs filesystems (#4)\n- Fix issue with excessive index file size (#6)\n- Support access of read only repositories.\n- New syntax to enable repository encryption:\n    attic init --encryption=\"none|passphrase|keyfile\".\n- Detect and abort if repository is older than the cache.\n\n\nVersion 0.7\n~~~~~~~~~~~\n\n(feature release, released on Aug 5, 2013)\n\n- Ported to FreeBSD\n- Improved documentation\n- Experimental: Archives mountable as FUSE filesystems.\n- The \"user.\" prefix is no longer stripped from xattrs on Linux\n\n\nVersion 0.6.1\n~~~~~~~~~~~~~\n\n(bugfix release, released on July 19, 2013)\n\n- Fixed an issue where mtime was not always correctly restored.\n\n\nVersion 0.6\n~~~~~~~~~~~\n\nFirst public release on July 9, 2013\n"
  },
  {
    "path": "docs/changes_1.x.rst",
    "content": ".. _important_notes_1x:\n\nImportant notes 1.x\n===================\n\nThis section provides information about security and corruption issues.\n\n.. _archives_tam_vuln:\n\nPre-1.2.5 archives spoofing vulnerability (CVE-2023-36811)\n----------------------------------------------------------\n\nA flaw in the cryptographic authentication scheme in Borg allowed an attacker to\nfake archives and potentially indirectly cause backup data loss in the repository.\n\nThe attack requires an attacker to be able to\n\n1. insert files (with no additional headers) into backups\n2. gain write access to the repository\n\nThis vulnerability does not disclose plaintext to the attacker, nor does it\naffect the authenticity of existing archives.\n\nCreating plausible fake archives may be feasible for empty or small archives,\nbut is unlikely for large archives.\n\nThe fix enforces checking the TAM authentication tag of archives at critical\nplaces. Borg now considers archives without TAM as garbage or an attack.\n\nWe are not aware of others having discovered, disclosed or exploited this vulnerability.\n\nBelow, if we speak of borg 1.2.6, we mean a borg version >= 1.2.6 **or** a\nborg version that has the relevant security patches for this vulnerability applied\n(could be also an older version in that case).\n\nSteps you must take to upgrade a repository (this applies to all kinds of repos\nno matter what encryption mode they use, including \"none\"):\n\n1. Upgrade all clients using this repository to borg 1.2.6.\n   Note: it is not required to upgrade a server, except if the server-side borg\n   is also used as a client (and not just for \"borg serve\").\n\n   Do **not** run ``borg check`` with borg 1.2.6 before completing the upgrade steps:\n\n   - ``borg check`` would complain about archives without a valid archive TAM.\n   - ``borg check --repair`` would remove such archives!\n2. Run: ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg info --debug <repo> 2>&1 | grep TAM | grep -i manifest``\n\n   a) If you get \"TAM-verified manifest\", continue with 3.\n   b) If you get \"Manifest TAM not found and not required\", run\n      ``borg upgrade --tam --force <repository>`` *on every client*.\n\n3. Run: ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg list --consider-checkpoints --format='{name} {time} tam:{tam}{NL}' <repo>``\n\n   \"tam:verified\" means that the archive has a valid TAM authentication.\n   \"tam:none\" is expected as output for archives created by borg <1.0.9.\n   \"tam:none\" is also expected for archives resulting from a borg rename\n   or borg recreate operation (see #7791).\n   \"tam:none\" could also come from archives created by an attacker.\n   You should verify that \"tam:none\" archives are authentic and not malicious\n   (== have good content, have correct timestamp, can be extracted successfully).\n   In case you find crappy/malicious archives, you must delete them before proceeding.\n   In low-risk, trusted environments, you may decide on your own risk to skip step 3\n   and just trust in everything being OK.\n\n4. If there are no tam:none archives left at this point, you can skip this step.\n   Run ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg upgrade --archives-tam <repo>``.\n   This will unconditionally add a correct archive TAM to all archives not having one.\n   ``borg check`` would consider TAM-less or invalid-TAM archives as garbage or a potential attack.\n   To see that all archives now are \"tam:verified\" run: ``borg list --consider-checkpoints --format='{name} {time} tam:{tam}{NL}' <repo>``\n\n5. Please note that you should never use BORG_WORKAROUNDS=ignore_invalid_archive_tam\n   for normal production operations - it is only needed once to get the archives in a\n   repository into a good state. All archives have a valid TAM now.\n\nVulnerability timeline:\n\n* 2023-06-13: Vulnerability discovered during code review by Thomas Waldmann\n* 2023-06-13...: Work on fixing the issue, upgrade procedure, docs.\n* 2023-06-30: CVE was assigned via Github CNA\n* 2023-06-30 .. 2023-08-29: Fixed issue, code review, docs, testing.\n* 2023-08-30: Released fixed version 1.2.5 (broken upgrade procedure for some repos)\n* 2023-08-31: Released fixed version 1.2.6 (fixes upgrade procedure)\n\n.. _hashindex_set_bug:\n\nPre-1.1.11 potential index corruption / data loss issue\n-------------------------------------------------------\n\nA bug was discovered in our hashtable code, see issue #4829.\nThe code is used for the client-side chunks cache and the server-side repo index.\n\nAlthough borg uses the hashtables very heavily, the index corruption did not\nhappen too frequently, because it needed specific conditions to happen.\n\nData loss required even more specific conditions, so it should be rare (and\nalso detectable via borg check).\n\nYou might be affected if borg crashed with / complained about:\n\n- AssertionError: Corrupted segment reference count - corrupted index or hints\n- ObjectNotFound: Object with key ... not found in repository ...\n- Index mismatch for key b'...'. (..., ...) != (-1, -1)\n- ValueError: stats_against: key contained in self but not in master_index.\n\nAdvised procedure to fix any related issue in your indexes/caches:\n\n- install fixed borg code (on client AND server)\n- for all of your clients and repos remove the cache by:\n\n  borg delete --cache-only YOURREPO\n\n  (later, the cache will be re-built automatically)\n- for all your repos, rebuild the repo index by:\n\n  borg check --repair YOURREPO\n\n  This will also check all archives and detect if there is any data-loss issue.\n\nAffected branches / releases:\n\n- fd06497 introduced the bug into 1.1-maint branch - it affects all borg 1.1.x since 1.1.0b4.\n- fd06497 introduced the bug into master branch - it affects all borg 1.2.0 alpha releases.\n- c5cd882 introduced the bug into 1.0-maint branch - it affects all borg 1.0.x since 1.0.11rc1.\n\nThe bug was fixed by:\n\n- 701159a fixes the bug in 1.1-maint branch - will be released with borg 1.1.11.\n- fa63150 fixes the bug in master branch - will be released with borg 1.2.0a8.\n- 7bb90b6 fixes the bug in 1.0-maint branch. Branch is EOL, no new release is planned as of now.\n\n.. _broken_validator:\n\nPre-1.1.4 potential data corruption issue\n-----------------------------------------\n\nA data corruption bug was discovered in borg check --repair, see issue #3444.\n\nThis is a 1.1.x regression, releases < 1.1 (e.g. 1.0.x) are not affected.\n\nTo avoid data loss, you must not run borg check --repair using an unfixed version\nof borg 1.1.x. The first official release that has the fix is 1.1.4.\n\nPackage maintainers may have applied the fix to updated packages of 1.1.x (x<4)\nthough, see the package maintainer's package changelog to make sure.\n\nIf you never had missing item metadata chunks, the bug has not affected you\neven if you did run borg check --repair with an unfixed version.\n\nWhen borg check --repair tried to repair corrupt archives that miss item metadata\nchunks, the resync to valid metadata in still present item metadata chunks\nmalfunctioned. This was due to a broken validator that considered all (even valid)\nitem metadata as invalid. As they were considered invalid, borg discarded them.\nPractically, that means the affected files, directories or other fs objects were\ndiscarded from the archive.\n\nDue to the malfunction, the process was extremely slow, but if you let it\ncomplete, borg would have created a \"repaired\" archive that has lost a lot of items.\nIf you interrupted borg check --repair because it was so strangely slow (killing\nborg somehow, e.g. Ctrl-C) the transaction was rolled back and no corruption occurred.\n\nThe log message indicating the precondition for the bug triggering looks like:\n\n    item metadata chunk missing [chunk: 001056_bdee87d...a3e50d]\n\nIf you never had that in your borg check --repair runs, you're not affected.\n\nBut if you're unsure or you actually have seen that, better check your archives.\nBy just using \"borg list repo::archive\" you can see if all expected filesystem\nitems are listed.\n\n.. _tam_vuln:\n\nPre-1.0.9 manifest spoofing vulnerability (CVE-2016-10099)\n----------------------------------------------------------\n\nA flaw in the cryptographic authentication scheme in Borg allowed an attacker\nto spoof the manifest. The attack requires an attacker to be able to\n\n1. insert files (with no additional headers) into backups\n2. gain write access to the repository\n\nThis vulnerability does not disclose plaintext to the attacker, nor does it\naffect the authenticity of existing archives.\n\nThe vulnerability allows an attacker to create a spoofed manifest (the list of archives).\nCreating plausible fake archives may be feasible for small archives, but is unlikely\nfor large archives.\n\nThe fix adds a separate authentication tag to the manifest. For compatibility\nwith prior versions this authentication tag is *not* required by default\nfor existing repositories. Repositories created with 1.0.9 and later require it.\n\nSteps you should take:\n\n1. Upgrade all clients to 1.0.9 or later.\n2. Run ``borg upgrade --tam <repository>`` *on every client* for *each* repository.\n3. This will list all archives, including archive IDs, for easy comparison with your logs.\n4. Done.\n\nPrior versions can access and modify repositories with this measure enabled, however,\nto 1.0.9 or later their modifications are indiscernible from an attack and will\nraise an error until the below procedure is followed. We are aware that this can\nbe annoying in some circumstances, but don't see a way to fix the vulnerability\notherwise.\n\nIn case a version prior to 1.0.9 is used to modify a repository where above procedure\nwas completed, and now you get an error message from other clients:\n\n1. ``borg upgrade --tam --force <repository>`` once with *any* client suffices.\n\nThis attack is mitigated by:\n\n- Noting/logging ``borg list``, ``borg info``, or ``borg create --stats``, which\n  contain the archive IDs.\n\nWe are not aware of others having discovered, disclosed or exploited this vulnerability.\n\nVulnerability time line:\n\n* 2016-11-14: Vulnerability and fix discovered during review of cryptography by Marian Beermann (@enkore)\n* 2016-11-20: First patch\n* 2016-12-20: Released fixed version 1.0.9\n* 2017-01-02: CVE was assigned\n* 2017-01-15: Released fixed version 1.1.0b3 (fix was previously only available from source)\n\n.. _attic013_check_corruption:\n\nPre-1.0.9 potential data loss\n-----------------------------\n\nIf you have archives in your repository that were made with attic <= 0.13\n(and later migrated to borg), running borg check would report errors in these\narchives. See issue #1837.\n\nThe reason for this is a invalid (and useless) metadata key that was\nalways added due to a bug in these old attic versions.\n\nIf you run borg check --repair, things escalate quickly: all archive items\nwith invalid metadata will be killed. Due to that attic bug, that means all\nitems in all archives made with these old attic versions.\n\n\nPre-1.0.4 potential repo corruption\n-----------------------------------\n\nSome external errors (like network or disk I/O errors) could lead to\ncorruption of the backup repository due to issue #1138.\n\nA sign that this happened is if \"E\" status was reported for a file that can\nnot be explained by problems with the source file. If you still have logs from\n\"borg create -v --list\", you can check for \"E\" status.\n\nHere is what could cause corruption and what you can do now:\n\n1) I/O errors (e.g. repo disk errors) while writing data to repo.\n\nThis could lead to corrupted segment files.\n\nFix::\n\n    # check for corrupt chunks / segments:\n    borg check -v --repository-only REPO\n\n    # repair the repo:\n    borg check -v --repository-only --repair REPO\n\n    # make sure everything is fixed:\n    borg check -v --repository-only REPO\n\n2) Unreliable network / unreliable connection to the repo.\n\nThis could lead to archive metadata corruption.\n\nFix::\n\n    # check for corrupt archives:\n    borg check -v --archives-only REPO\n\n    # delete the corrupt archives:\n    borg delete --force REPO::CORRUPT_ARCHIVE\n\n    # make sure everything is fixed:\n    borg check -v --archives-only REPO\n\n3) In case you want to do more intensive checking.\n\nThe best check that everything is ok is to run a dry-run extraction::\n\n    borg extract -v --dry-run REPO::ARCHIVE\n\n\n.. _upgradenotes:\n\nUpgrade Notes\n=============\n\nborg 1.1.x to 1.2.x\n-------------------\n\nSome things can be recommended for the upgrade process from borg 1.1.x\n(please also read the important compatibility notes below):\n\n- first upgrade to a recent 1.1.x release - especially if you run some older\n  1.1.* or even 1.0.* borg release.\n- using that, run at least one `borg create` (your normal backup), `prune`\n  and especially a `check` to see everything is in a good state.\n- check the output of `borg check` - if there is anything special, consider\n  a `borg check --repair` followed by another `borg check`.\n- if everything is fine so far (borg check reports no issues), you can consider\n  upgrading to 1.2.x. if not, please first fix any already existing issue.\n- if you want to play safer, first **create a backup of your borg repository**.\n- upgrade to latest borg 1.2.x release (you could use the fat binary from\n  github releases page)\n- borg 1.2.6 has a security fix for the pre-1.2.5 archives spoofing vulnerability\n  (CVE-2023-36811), see details and necessary upgrade procedure described above.\n- run `borg compact --cleanup-commits` to clean up a ton of 17 bytes long files\n  in your repo caused by a borg 1.1 bug\n- run `borg check` again (now with borg 1.2.x) and check if there is anything\n  special.\n- run `borg info` (with borg 1.2.x) to build the local pre12-meta cache (can\n  take significant time, but after that it will be fast) - for more details\n  see below.\n- check the compatibility notes (see below) and adapt your scripts, if needed.\n- if you run into any issues, please check the github issue tracker before\n  posting new issues there or elsewhere.\n\nIf you follow this procedure, you can help avoiding that we get a lot of\n\"borg 1.2\" issue reports that are not really 1.2 issues, but existed before\nand maybe just were not noticed.\n\nCompatibility notes:\n\n- matching of path patterns has been aligned with borg storing relative paths.\n  Borg archives file paths without leading slashes. Previously, include/exclude\n  patterns could contain leading slashes. You should check your patterns and\n  remove leading slashes.\n- dropped support / testing for older Pythons, minimum requirement is 3.8.\n  In case your OS does not provide Python >= 3.8, consider using our binary,\n  which does not need an external Python interpreter. Or continue using\n  borg 1.1.x, which is still supported.\n- freeing repository space only happens when \"borg compact\" is invoked.\n- mount: the default for --numeric-ids is False now (same as borg extract)\n- borg create --noatime is deprecated. Not storing atime is the default behaviour\n  now (use --atime if you want to store the atime).\n- --prefix is deprecated, use -a / --glob-archives, see #6806\n- list: corrected mix-up of \"isomtime\" and \"mtime\" formats.\n  Previously, \"isomtime\" was the default but produced a verbose human format,\n  while \"mtime\" produced a ISO-8601-like format.\n  The behaviours have been swapped (so \"mtime\" is human, \"isomtime\" is ISO-like),\n  and the default is now \"mtime\".\n  \"isomtime\" is now a real ISO-8601 format (\"T\" between date and time, not a space).\n- create/recreate --list: file status for all files used to get announced *AFTER*\n  the file (with borg < 1.2). Now, file status is announced *BEFORE* the file\n  contents are processed. If the file status changes later (e.g. due to an error\n  or a content change), the updated/final file status will be printed again.\n- removed deprecated-since-long stuff (deprecated since):\n\n  - command \"borg change-passphrase\" (2017-02), use \"borg key ...\"\n  - option \"--keep-tag-files\" (2017-01), use \"--keep-exclude-tags\"\n  - option \"--list-format\" (2017-10), use \"--format\"\n  - option \"--ignore-inode\" (2017-09), use \"--files-cache\" w/o \"inode\"\n  - option \"--no-files-cache\" (2017-09), use \"--files-cache=disabled\"\n- removed BORG_HOSTNAME_IS_UNIQUE env var.\n  to use borg you must implement one of these 2 scenarios:\n\n  - 1) the combination of FQDN and result of uuid.getnode() must be unique\n       and stable (this should be the case for almost everybody, except when\n       having duplicate FQDN *and* MAC address or all-zero MAC address)\n  - 2) if you are aware that 1) is not the case for you, you must set\n       BORG_HOST_ID env var to something unique.\n- exit with 128 + signal number, #5161.\n  if you have scripts expecting rc == 2 for a signal exit, you need to update\n  them to check for >= 128.\n\n\n.. _changelog_1x:\n\nChange Log 1.x\n==============\n\nVersion 1.3.0a1 (2022-04-15)\n----------------------------\n\nPlease note:\n\nThis is an alpha release, only for testing - do not use this with production repos.\n\nNew features:\n\n- init: new --encryption=(repokey|keyfile)-[blake2-](aes-ocb|chacha20-poly1305)\n\n  - New, better, faster crypto (see encryption-aead diagram in the docs), #6463.\n  - New AEAD cipher suites: AES-OCB and CHACHA20-POLY1305.\n  - Session keys are derived via HKDF from random session id and master key.\n  - Nonces/MessageIVs are counters starting from 0 for each session.\n  - AAD: chunk id, key type, messageIV, sessionID are now authenticated also.\n  - Solves the potential AES-CTR mode counter management issues of the legacy crypto.\n- init: --key-algorithm=argon2 (new default KDF, older pbkdf2 also still available)\n\n  borg key change-passphrase / change-location keeps the key algorithm unchanged.\n- key change-algorithm: to upgrade existing keys to argon2 or downgrade to pbkdf2.\n\n  We recommend you to upgrade unless you have to keep the key compatible with older versions of borg.\n- key change-location: usable for repokey <-> keyfile location change\n- benchmark cpu: display benchmarks of cpu bound stuff\n- export-tar: new --tar-format=PAX (default: GNU)\n- import-tar/export-tar: can use PAX format for ctime and atime support\n- import-tar/export-tar: --tar-format=BORG: roundtrip ALL item metadata, #5830\n- repository: create and use version 2 repos only for now\n- repository: implement PUT2: header crc32, overall xxh64, #1704\n\nOther changes:\n\n- require python >= 3.9, #6315\n- simplify libs setup, #6482\n- unbundle most bundled 3rd party code, use libs, #6316\n- use libdeflate.crc32 (Linux and all others) or zlib.crc32 (macOS)\n- repository: code cleanups / simplifications\n- internal crypto api: speedups / cleanups / refactorings / modernisation\n- remove \"borg upgrade\" support for \"attic backup\" repos\n- remove PassphraseKey code and borg key migrate-to-repokey command\n- OpenBSD: build borg with OpenSSL (not: LibreSSL), #6474\n- remove support for LibreSSL, #6474\n- remove support for OpenSSL < 1.1.1\n\n\nVersion 1.2.7 (2023-12-02)\n--------------------------\n\nFor upgrade and compatibility hints, please also read the section \"Upgrade Notes\"\nabove.\n\nFixes:\n\n- docs: CVE-2023-36811 upgrade steps: consider checkpoint archives, #7802\n- check/compact: fix spurious reappearance of orphan chunks since borg 1.2, #6687 -\n  this consists of 2 fixes:\n\n  - for existing chunks: check --repair: recreate shadow index, #7897 #6687\n  - for newly created chunks: update shadow index when doing a double-put, #7896 #5661\n\n  If you have experienced issue #6687, you may want to run borg check --repair\n  after upgrading to borg 1.2.7 to recreate the shadow index and get rid of the\n  issue for existing chunks.\n- LockRoster.modify: no KeyError if element was already gone, #7937\n- create --X-from-command: run subcommands with a clean environment, #7916\n- list --sort-by: support \"archive\" as alias of \"name\", #7873\n- fix rc and msg if arg parsing throws an exception, #7885\n\nOther changes:\n\n- support and test on Python 3.12\n- include unistd.h in _chunker.c (fix for Python 3.13)\n- allow msgpack 1.0.6 and 1.0.7\n- TAM issues: show tracebacks, improve borg check logging, #7797\n- replace \"datetime.utcfromtimestamp\" with custom helper to avoid\n  deprecation warnings when using Python 3.12\n- vagrant:\n\n  - use generic/debian9 box, fixes #7579\n  - add VM with debian bookworm / test on OpenSSL 3.0.x.\n- docs:\n\n  - not only attack/unsafe, can also be a fs issue, #7853\n  - point to CVE-2023-36811 upgrade steps from borg 1.1 to 1.2 upgrade steps, #7899\n  - upgrade steps needed for all kinds of repos (including \"none\" encryption mode), #7813\n  - upgrade steps: talk about consequences of borg check, #7816\n  - upgrade steps: remove period that could be interpreted as part of the command\n  - automated-local.rst: use GPT UUID for consistent udev rule\n  - create disk/partition sector backup by disk serial number, #7934\n  - update macOS hint about full disk access\n  - clarify borg prune -a option description, #7871\n  - readthedocs: also build offline docs (HTMLzip), #7835\n  - frontends: add \"check.rebuild_refcounts\" message\n\n\nVersion 1.2.6 (2023-08-31)\n--------------------------\n\nFixes:\n\n- The upgrade procedure docs as published with borg 1.2.5 did not work, if the\n  repository had archives resulting from a borg rename or borg recreate operation.\n\n  The updated docs now use BORG_WORKAROUNDS=ignore_invalid_archive_tam at some\n  places to avoid that issue, #7791.\n\n  See: fix pre-1.2.5 archives spoofing vulnerability (CVE-2023-36811),\n  details and necessary upgrade procedure described above.\n\nOther changes:\n\n- updated 1.2.5 changelog entry: 1.2.5 already has the fix for rename/recreate.\n- remove cython restrictions. recommended is to build with cython 0.29.latest,\n  because borg 1.2.x uses this since years and it is very stable.\n  you can also try to build with cython 3.0.x, there is a good chance that it works.\n  as a 3rd option, we also bundle the `*.c` files cython outputs in the release\n  pypi package, so you can also just use these and not need cython at all.\n\n\nVersion 1.2.5 (2023-08-30)\n--------------------------\n\nFixes:\n\n- Security: fix pre-1.2.5 archives spoofing vulnerability (CVE-2023-36811),\n  see details and necessary upgrade procedure described above.\n- rename/recreate: correctly update resulting archive's TAM, see #7791\n- create: do not try to read parent dir of recursion root, #7746\n- extract: fix false warning about pattern never matching, #4110\n- diff: remove surrogates before output, #7535\n- compact: clear empty directories at end of compact process, #6823\n- create --files-cache=size: fix crash, #7658\n- keyfiles: improve key sanity check, #7561\n- only warn about \"invalid\" chunker params, #7590\n- ProgressIndicatorPercent: fix space computation for wide chars, #3027\n- improve argparse validator error messages\n\nNew features:\n\n- mount: make up volname if not given (macOS), #7690.\n  macFUSE supports a volname mount option to give what finder displays on the\n  desktop / in the directory view. if the user did not specify it, we make\n  something up, because otherwise it would be \"macFUSE Volume 0 (Python)\" and\n  hide the mountpoint directory name.\n- BORG_WORKAROUNDS=authenticated_no_key to extract from authenticated repos\n  without key, #7700\n\nOther changes:\n\n- add `utcnow()` helper function to avoid deprecated `datetime.utcnow()`\n- stay on latest Cython 0.29 (0.29.36) for borg 1.2.x (do not use Cython 3.0 yet)\n- docs:\n\n  - move upgrade notes to own section, see #7546\n  - mount -olocal: how to show mount in finder's sidebar, #5321\n  - list: fix --pattern examples, #7611\n  - improve patterns help\n  - incl./excl. options, path-from-stdin exclusiveness\n  - obfuscation docs: markup fix, note about MAX_DATA_SIZE\n  - --one-file-system: add macOS apfs notes, #4876\n  - improve --one-file-system help string, #5618\n  - rewrite borg check docs\n  - improve the docs for --keep-within, #7687\n  - fix borg init command in environment.rst.inc\n  - 1.1.x upgrade notes: more precise borg upgrade instructions, #3396\n\n- tests:\n\n  - fix repo reopen\n  - avoid long ids in pytest output\n  - check buzhash chunksize distribution, see #7586\n\n\nVersion 1.2.4 (2023-03-24)\n--------------------------\n\nNew features:\n\n- import-tar: add --ignore-zeros to process concatenated tars, #7432.\n- debug id-hash: computes file/chunk content id-hash, #7406\n- diff: --content-only does not show mode/ctime/mtime changes, #7248\n- diff: JSON strings in diff output are now sorted alphabetically\n\nBug fixes:\n\n- xattrs: fix namespace processing on FreeBSD, #6997\n- diff: fix path related bug seen when addressing deferred items.\n- debug get-obj/put-obj: always give chunkid as cli param, see #7290\n  (this is an incompatible change, see also borg debug id-hash)\n- extract: fix mtime when ResourceFork xattr is set (macOS specific), #7234\n- recreate: without --chunker-params, do not re-chunk, #7337\n- recreate: when --target is given, do not detect \"nothing to do\".\n  use case: borg recreate -a src --target dst can be used to make a copy\n  of an archive inside the same repository, #7254.\n- set .hardlink_master for ALL hardlinkable items, #7175\n- locking: fix host, pid, tid order.\n  tid (thread id) must be parsed as hex from lock file name.\n- update development.lock.txt, including a setuptools security fix, #7227\n\nOther changes:\n\n- requirements: allow msgpack 1.0.5 also\n- upgrade Cython to 0.29.33\n- hashindex minor fixes, refactor, tweaks, tests\n- use os.replace not os.rename\n- remove BORG_LIBB2_PREFIX (not used any more)\n- docs:\n\n  - BORG_KEY_FILE: clarify docs, #7444\n  - update FAQ about locale/unicode issues, #6999\n  - improve mount options rendering, #7359\n  - make timestamps in manual pages reproducible\n  - installation: update Fedora in distribution list, #7357\n- tests:\n\n  - fix test_size_on_disk_accurate for large st_blksize, #7250\n  - add same_ts_ns function and use it for relaxed timestamp comparisons\n  - \"auto\" compressor tests: don't assume a specific size,\n    do not assume zlib is better than lz4, #7363\n  - add test for extracted directory mtime\n- vagrant:\n\n  - upgrade local freebsd 12.1 box -> generic/freebsd13 box (13.1)\n  - use pythons > 3.8 which work on freebsd 13.1\n  - pyenv: also install python 3.11.1 for testing\n  - pyenv: use python 3.10.1, 3.10.0 build is broken on freebsd\n\n\nVersion 1.2.3 (2022-12-24)\n--------------------------\n\nFixes:\n\n- create: fix --list --dry-run output for directories, #7209\n- diff/recreate: normalize chunker params before comparing them, #7079\n- check: fix uninitialised variable if repo is completely empty, #7034\n- xattrs: improve error handling, #6988\n- fix args.paths related argparsing, #6994\n- archive.save(): always use metadata from stats (e.g. nfiles, size, ...), #7072\n- tar_filter: recognize .tar.zst as zstd, #7093\n- get_chunker: fix missing sparse=False argument, #7056\n- file_integrity.py: make sure file_fd is always closed on exit\n- repository: cleanup(): close segment before unlinking\n- repository: use os.replace instead of os.rename\n\nOther changes:\n\n- remove python < 3.7 compatibility code\n- do not use version_tuple placeholder in setuptools_scm template\n- CI: fix tox4 passenv issue, #7199\n- vagrant: update to python 3.9.16, use the openbsd 7.1 box\n- misc. test suite and docs fixes / improvements\n- remove deprecated --prefix from docs, #7109\n- Windows: use MSYS2 for Github CI, remove Appveyor CI\n\n\nVersion 1.2.2 (2022-08-20)\n--------------------------\n\nNew features:\n\n- prune/delete --checkpoint-interval=1800 and ctrl-c/SIGINT support, #6284\n\nFixes:\n\n- SaveFile: use a custom mkstemp with mode support, #6933, #6400, #6786.\n  This fixes umask/mode/ACL issues (and also \"chmod not supported\" exceptions\n  seen in 1.2.1) of files updated using SaveFile, e.g. the repo config.\n- hashindex_compact: fix eval order (check idx before use), #5899\n- create --paths-from-(stdin|command): normalize paths, #6778\n- secure_erase: avoid collateral damage, #6768.\n  If a hardlink copy of a repo was made and a new repo config shall be saved,\n  do NOT fill in random garbage before deleting the previous repo config,\n  because that would damage the hardlink copy.\n- list: fix {flags:<WIDTH>} formatting, #6081\n- check: try harder to create the key, #5719\n- misc commands: ctrl-c must not kill other subprocesses, #6912\n\n  - borg create with a remote repo via ssh\n  - borg create --content-from-command\n  - borg create --paths-from-command\n  - (de)compression filter process of import-tar / export-tar\n\nOther changes:\n\n- deprecate --prefix, use -a / --glob-archives, see #6806\n- make setuptools happy (\"package would be ignored\"), #6874\n- fix pyproject.toml to create a fixed _version.py file, compatible with both\n  old and new setuptools_scm version, #6875\n- automate asciinema screencasts\n- CI: test on macOS 12 without fuse / fuse tests\n  (too troublesome on github CI due to kernel extensions needed by macFUSE)\n- tests: fix test_obfuscate byte accounting\n- repository: add debug logging for issue #6687\n- _chunker.c: fix warnings on macOS\n- requirements.lock.txt: use the latest cython 0.29.32\n- docs:\n\n  - add info on man page installation, #6894\n  - update archive_progress json description about \"finished\", #6570\n  - json progress_percent: some values are optional, #4074\n  - FAQ: full quota / full disk, #5960\n  - correct shell syntax for installation using git\n\n\nVersion 1.2.1 (2022-06-06)\n--------------------------\n\nFixes:\n\n- create: skip with warning if opening the parent dir of recursion root fails, #6374\n- create: fix crash. metadata stream can produce all-zero chunks, #6587\n- fix crash when computing stats, escape % chars in archive name, #6500\n- fix transaction rollback: use files cache filename as found in txn.active/, #6353\n- import-tar: kill filter process in case of borg exceptions, #6401 #6681\n- import-tar: fix mtime type bug\n- ensure_dir: respect umask for created directory modes, #6400\n- SaveFile: respect umask for final file mode, #6400\n- check archive: improve error handling for corrupt archive metadata block, make\n  robust_iterator more robust, #4777\n- pre12-meta cache: do not use the cache if want_unique is True, #6612\n- fix scp-style repo url parsing for ip v6 address, #6526\n- mount -o versions: give clear error msg instead of crashing.\n  it does not make sense to request versions view if you only look at 1 archive,\n  but the code shall not crash in that case as it did, but give a clear error msg.\n- show_progress: add finished=true/false to archive_progress json, #6570\n- delete/prune: fix --iec mode output (decimal vs. binary units), #6606\n- info: fix authenticated mode repo to show \"Encrypted: No\", #6462\n- diff: support presence change for blkdev, chrdev and fifo items, #6615\n\nNew features:\n\n- delete: add repository id and location to prompt, #6453\n- borg debug dump-repo-objs --ghost: new --segment=S --offset=O options\n\nOther changes:\n\n- support python 3.11\n- allow msgpack 1.0.4, #6716\n- load_key: no key is same as empty key, #6441\n- give a more helpful error msg for unsupported key formats, #6561\n- better error msg for defect or unsupported repo configs, #6566\n- docs:\n\n  - document borg 1.2 pattern matching behavior change, #6407\n    Make clear that absolute paths always go into the matcher as if they are\n    relative (without leading slash). Adapt all examples accordingly.\n  - authentication primitives: improved security and performance infos\n  - mention BORG_FILES_CACHE_SUFFIX as alternative to BORG_FILES_CACHE_TTL, #5602\n  - FAQ: add a hint about --debug-topic=files_cache\n  - improve borg check --max-duration description\n  - fix values of TAG bytes, #6515\n  - borg compact --cleanup-commits also runs a normal compaction, #6324\n  - virtualization speed tips\n  - recommend umask for passphrase file perms\n  - borg 1.2 is security supported\n  - update link to ubuntu packages, #6485\n  - use --numeric-ids in pull mode docs\n  - remove blake2 docs, blake2 code not bundled any more, #6371\n  - clarify on-disk order and size of segment file log entry fields, #6357\n  - docs building: do not transform --/--- to unicode dashes\n- tests:\n\n  - check that borg does not require pytest for normal usage, fixes #6563\n  - fix OpenBSD symlink mode test failure, #2055\n- vagrant:\n\n  - darwin64: remove fakeroot, #6314\n  - update development.lock.txt\n  - use pyinstaller 4.10 and python 3.9.13 for binary build\n  - upgrade VMCPUS and xdistn from 4 to 16, maybe this speeds up the tests\n- crypto:\n\n  - use hmac.compare_digest instead of ==, #6470\n  - hmac_sha256: replace own cython wrapper code by hmac.digest python stdlib (since py38)\n  - hmac and blake2b minor optimizations and cleanups\n  - removed some unused crypto related code, #6472\n  - avoid losing the key (potential use-after-free). this never could happen in\n    1.2 due to the way we use the code. The issue was discovered in master after\n    other changes, so we also \"fixed\" it here before it bites us.\n- setup / build:\n\n  - add pyproject.toml, fix sys.path, #6466\n  - setuptools_scm: also require it via pyproject.toml\n  - allow extra compiler flags for every extension build\n  - fix misc. C / Cython compiler warnings, deprecation warnings\n  - fix zstd.h include for bundled zstd, #6369\n- source using python 3.8 features: ``pyupgrade --py38-plus ./**/*.py``\n\n\nVersion 1.2.0 (2022-02-22 22:02:22 :-)\n--------------------------------------\n\nPlease note:\n\nThis is the first borg 1.2 release, so be careful and read the notes below.\n\nUpgrade notes:\n\nStrictly taken, nothing special is required for upgrading to 1.2, but some\nthings can be recommended:\n\n- do you already want to upgrade? 1.1.x also will get fixes for a while.\n- be careful, first upgrade your less critical / smaller repos.\n- first upgrade to a recent 1.1.x release - especially if you run some older\n  1.1.* or even 1.0.* borg release.\n- using that, run at least one `borg create` (your normal backup), `prune`\n  and especially a `check` to see everything is in a good state.\n- check the output of `borg check` - if there is anything special, consider\n  a `borg check --repair` followed by another `borg check`.\n- if everything is fine so far (borg check reports no issues), you can consider\n  upgrading to 1.2.0. if not, please first fix any already existing issue.\n- if you want to play safer, first **create a backup of your borg repository**.\n- upgrade to latest borg 1.2.x release (you could use the fat binary from\n  github releases page)\n- run `borg compact --cleanup-commits` to clean up a ton of 17 bytes long files\n  in your repo caused by a borg 1.1 bug\n- run `borg check` again (now with borg 1.2.x) and check if there is anything\n  special.\n- run `borg info` (with borg 1.2.x) to build the local pre12-meta cache (can\n  take significant time, but after that it will be fast) - for more details\n  see below.\n- check the compatibility notes (see below) and adapt your scripts, if needed.\n- if you run into any issues, please check the github issue tracker before\n  posting new issues there or elsewhere.\n\nIf you follow this procedure, you can help avoiding that we get a lot of\n\"borg 1.2\" issue reports that are not really 1.2 issues, but existed before\nand maybe just were not noticed.\n\nCompatibility notes:\n\n- matching of path patterns has been aligned with borg storing relative paths.\n  Borg archives file paths without leading slashes. Previously, include/exclude\n  patterns could contain leading slashes. You should check your patterns and\n  remove leading slashes.\n- dropped support / testing for older Pythons, minimum requirement is 3.8.\n  In case your OS does not provide Python >= 3.8, consider using our binary,\n  which does not need an external Python interpreter. Or continue using\n  borg 1.1.x, which is still supported.\n- freeing repository space only happens when \"borg compact\" is invoked.\n- mount: the default for --numeric-ids is False now (same as borg extract)\n- borg create --noatime is deprecated. Not storing atime is the default behaviour\n  now (use --atime if you want to store the atime).\n- list: corrected mix-up of \"isomtime\" and \"mtime\" formats.\n  Previously, \"isomtime\" was the default but produced a verbose human format,\n  while \"mtime\" produced a ISO-8601-like format.\n  The behaviours have been swapped (so \"mtime\" is human, \"isomtime\" is ISO-like),\n  and the default is now \"mtime\".\n  \"isomtime\" is now a real ISO-8601 format (\"T\" between date and time, not a space).\n- create/recreate --list: file status for all files used to get announced *AFTER*\n  the file (with borg < 1.2). Now, file status is announced *BEFORE* the file\n  contents are processed. If the file status changes later (e.g. due to an error\n  or a content change), the updated/final file status will be printed again.\n- removed deprecated-since-long stuff (deprecated since):\n\n  - command \"borg change-passphrase\" (2017-02), use \"borg key ...\"\n  - option \"--keep-tag-files\" (2017-01), use \"--keep-exclude-tags\"\n  - option \"--list-format\" (2017-10), use \"--format\"\n  - option \"--ignore-inode\" (2017-09), use \"--files-cache\" w/o \"inode\"\n  - option \"--no-files-cache\" (2017-09), use \"--files-cache=disabled\"\n- removed BORG_HOSTNAME_IS_UNIQUE env var.\n  to use borg you must implement one of these 2 scenarios:\n\n  - 1) the combination of FQDN and result of uuid.getnode() must be unique\n       and stable (this should be the case for almost everybody, except when\n       having duplicate FQDN *and* MAC address or all-zero MAC address)\n  - 2) if you are aware that 1) is not the case for you, you must set\n       BORG_HOST_ID env var to something unique.\n- exit with 128 + signal number, #5161.\n  if you have scripts expecting rc == 2 for a signal exit, you need to update\n  them to check for >= 128.\n\nFixes:\n\n- diff: reduce memory consumption, fix is_hardlink_master, #6295\n- compact: fix / improve freeable / freed space log output\n\n  - derive really freed space from quota use before/after, #5679\n  - do not say \"freeable\", but \"maybe freeable\" (based on hint, unsure)\n- fix race conditions in internal SaveFile function, #6306 #6028\n- implement internal safe_unlink (was: truncate_and_unlink) function more safely:\n  usually it does not truncate any more, only under \"disk full\" circumstances\n  and only if there is only one hardlink.\n  see: https://github.com/borgbackup/borg/discussions/6286\n\nOther changes:\n\n- info: use a pre12-meta cache to accelerate stats for borg < 1.2 archives.\n  the first time borg info is invoked on a borg 1.1 repo, it can take a\n  rather long time computing and caching some stats values for 1.1 archives,\n  which borg 1.2 archives have in their archive metadata structure.\n  be patient, esp. if you have lots of old archives.\n  following invocations are much faster due to the cache.\n  related change: add archive name to calc_stats progress display.\n- docs:\n\n  - add borg 1.2 upgrade notes, #6217\n  - link to borg placeholders and borg patterns help\n  - init: explain the encryption modes better\n  - clarify usage of patternfile roots\n  - put import-tar docs into same file as export-tar docs\n  - explain the difference between a path that ends with or without a slash,\n    #6297\n\n\nVersion 1.2.0rc1 (2022-02-05)\n-----------------------------\n\nFixes:\n\n- repo::archive location placeholder expansion fixes, #5826, #5998\n- repository: fix intermediate commits, shall be at end of current segment\n- delete: don't commit if nothing was deleted, avoid cache sync, #6060\n- argument parsing: accept some options only once, #6026\n- disallow overwriting of existing keyfiles on init, #6036\n- if ensure_dir() fails, give more informative error message, #5952\n\nNew features:\n\n- delete --force: do not ask when deleting a repo, #5941\n\nOther changes:\n\n- requirements: exclude broken or incompatible-with-pyinstaller setuptools\n- add a requirements.d/development.lock.txt and use it for vagrant\n- tests:\n\n  - added nonce-related tests\n  - refactor: remove assert_true\n  - vagrant: macos box tuning, netbsd box fixes, #5370, #5922\n- docs:\n\n  - update install docs / requirements docs, #6180\n  - borg mount / FUSE \"versions\" view is not experimental any more\n  - --pattern* is not experimental any more, #6134\n  - impact of deleting path/to/repo/nonce, #5858\n  - key export: add examples, #6204\n  - ~/.config/borg/keys is not used for repokey keys, #6107\n  - excluded parent dir's metadata can't restore\n\n\nVersion 1.2.0b4 (2022-01-23)\n----------------------------\n\nFixes:\n\n- create: fix passing device nodes and symlinks to --paths-from-stdin, #6009\n- create --dry-run: fix display of kept tagfile, #5834\n- check --repair: fix missing parameter in \"did not consistently fail\" msg, #5822\n- fix hardlinkable file type check, #6037\n- list: remove placeholders for shake_* hashes, #6082\n- prune: handle case of calling prune_split when there are no archives, #6015\n- benchmark crud: make sure cleanup of borg-test-data files/dir happens, #5630\n- do not show archive name in repository-related error msgs, #6014\n- prettier error msg (no stacktrace) if exclude file is missing, #5734\n- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979\n- fix pyinstaller detection for dir-mode, #5897\n- atomically create the CACHE_TAG file, #6028\n- deal with the SaveFile/SyncFile race, docs, see #6056 708a5853\n- avoid expanding path into LHS of formatting operation + tests, #6064 #6063\n- repository: quota / compactable computation fixes\n- info: emit repo info even if repo has 0 archives + test, #6120\n\nNew features:\n\n- check --repair: significantly speed up search for next valid object in segment, #6022\n- check: add progress indicator for archive check, #5809\n- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024\n- create: allow --files-cache=size (this is potentially dangerous, use on your own risk), #5686\n- import-tar: implement import-tar to complement export-tar, #2233\n- implement BORG_SELFTEST env variable (can be carefully used to speedup borg hosting), #5871\n- key export: print key if path is '-' or not given, #6092\n- list --format: Add command_line to format keys\n\nOther changes:\n\n- pypi metadata: alpha -> beta\n- require python 3.8+, #5975\n- use pyinstaller 4.7\n- allow msgpack 1.0.3\n- upgrade to bundled xxhash to 0.8.1\n- import-tar / export-tar: tar file related changes:\n\n  - check for short tarfile extensions\n  - add .lz4 and .zstd\n  - fix docs about extensions and decompression commands\n- add github codeql analysis, #6148\n- vagrant:\n\n  - box updates / add new boxes / remove outdated and broken boxes\n  - use Python 3.9.10 (incl. binary builds) and 3.10.0\n  - fix pyenv initialisation, #5798\n  - fix vagrant scp on macOS, #5921\n  - use macfuse instead of osxfuse\n- shell completions:\n\n  - update shell completions to 1.1.17, #5923\n  - remove BORG_LIBC completion, since 9914968 borg no longer uses find_library().\n- docs:\n\n  - fixed readme.rst irc webchat link (we use libera chat now, not freenode)\n  - fix exceptions thrown by `setup.py build_man`\n  - check --repair: recommend checking hw before check --repair, #5855\n  - check --verify-data: clarify and document conflict with --repository-only, #5808\n  - serve: improve ssh forced commands docs, #6083\n  - list: improve docs for `borg list` --format, #6061\n  - list: remove --list-format from borg list\n  - FAQ: fix manifest-timestamp path (inside security dir)\n  - fix the broken link to .nix file\n  - document behavior for filesystems with inconsistent inodes, #5770\n  - clarify user_id vs uid for fuse, #5723\n  - clarify pattern usage with commands, #5176\n  - clarify pp vs. pf pattern type, #5300\n  - update referenced freebsd/macOS versions used for binary build, #5942\n  - pull mode: add some warnings, #5827\n  - clarify \"you will need key and passphrase\" borg init warning, #4622\n  - add missing leading slashes in help patterns, #5857\n  - add info on renaming repositories, #5240\n  - check: add notice about defective hardware, #5753\n  - mention tar --compare (compare archive to fs files), #5880\n  - add note about grandfather-father-son backup retention policy / rotation scheme, #6006\n  - permissions note rewritten to make it less confusing\n  - create github security policy\n  - remove leftovers of BORG_HOSTNAME_IS_UNIQUE\n  - excluded parent dir's metadata can't restore. (#6062)\n  - if parent dir is not extracted, we do not have its metadata\n  - clarify who starts the remote agent\n\n\nVersion 1.2.0b3 (2021-05-12)\n----------------------------\n\nFixes:\n\n- create: fix --progress --log-json, #4360#issuecomment-774580052\n- do not load files cache for commands not using it, #5673\n- fix repeated cache tag file writing bug\n\nNew features:\n\n- create/recreate: print preliminary file status early, #5417\n- create/extract: add --noxattrs and --noacls options, #3955\n- create: verbose files cache logging via --debug-topic=files_cache, #5659\n- mount: implement --numeric-ids (default: False!), #2377\n- diff: add --json-lines option\n- info / create --stats: add --iec option to print sizes in powers of 1024.\n\nOther changes:\n\n- create: add --upload-(ratelimit|buffer), deprecate --remote-* options, #5611\n- create/extract/mount: add --numeric-ids, deprecate --numeric-owner option, #5724\n- config: accept non-int value for max_segment_size / storage_quota\n- use PyInstaller v4.3, #5671\n- vagrant: use Python 3.9.5 to build binaries\n- tox.ini: modernize and enable execution without preinstalling deps\n- cleanup code style checks\n- get rid of distutils, use setuptools+packaging\n- github CI: test on Python 3.10-dev\n- check: missing / healed chunks: always tell chunk ID, #5704\n- docs:\n\n  - remove bad /var/cache exclusion in example commands, #5625\n  - misc. fixes and improvements, esp. for macOS\n  - add unsafe workaround to use an old repo copy, #5722\n\n\nVersion 1.2.0b2 (2021-02-06)\n----------------------------\n\nFixes:\n\n- create: do not recurse into duplicate roots, #5603\n- create: only print stats if not ctrl-c'ed, fixes traceback, #5668\n- extract:\n  improve exception handling when setting xattrs, #5092.\n  emit a warning message giving the path, xattr key and error message.\n  continue trying to restore other xattrs and bsdflags of the same file\n  after an exception with xattr-setting happened.\n- export-tar:\n  fix memory leak with ssh: remote repository, #5568.\n  fix potential memory leak with ssh: remote repository with partial extraction.\n- remove empty shadowed_segments lists, #5275\n- fix bad default: manifest.archives.list(consider_checkpoints=False),\n  fixes tracebacks / KeyErros for missing objects in ChunkIndex, #5668\n\nNew features:\n\n- create: improve sparse file support\n\n  - create --sparse (detect sparse file holes) and file map support,\n    only for the \"fixed\" chunker, #14\n  - detect all-zero chunks in read data in \"buzhash\" and \"fixed\" chunkers\n  - cached_hash: use a small LRU cache to accelerate all-zero chunks hashing\n  - use cached_hash also to generate all-zero replacement chunks\n- create --remote-buffer, add a upload buffer for remote repos, #5574\n- prune: keep oldest archive when retention target not met\n\nOther changes:\n\n- use blake2 from python 3.6+ hashlib\n  (this removes the requirement for libb2 and the bundled blake2 code)\n- also accept msgpack up to 1.0.2.\n  exclude 1.0.1 though, which had some issues (not sure they affect borg).\n- create: add repository location to --stats output, #5491\n- check: debug log the segment filename\n- delete: add a --list switch to borg delete, #5116\n- borg debug dump-hints - implemented to e.g. to look at shadow_index\n- Tab completion support for additional archives for 'borg delete'\n- refactor: have one borg.constants.zero all-zero bytes object\n- refactor shadow_index updating repo.put/delete, #5661, #5636.\n- docs:\n\n  - add another case of attempted hardlink usage\n  - fix description of borg upgrade hardlink usage, #5518\n  - use HTTPS everywhere\n  - add examples for --paths-from-stdin, --paths-from-command, --paths-separator, #5644\n  - fix typos/grammar\n  - update docs for dev environment installation instructions\n  - recommend running tests only on installed versions for setup\n  - add badge with current status of package\n- vagrant:\n\n  - use brew install --cask ..., #5557\n  - use Python 3.9.1 and PyInstaller 4.1 to build the borg binary\n\n\nVersion 1.2.0b1 (2020-12-06)\n----------------------------\n\nFixes:\n\n- BORG_CACHE_DIR crashing borg if empty, atomic handling of\n  recursive directory creation, #5216\n- fix --dry-run and --stats coexistence, #5415\n- allow EIO with warning when trying to hardlink, #4336\n- export-tar: set tar format to GNU_FORMAT explicitly, #5274\n- use --timestamp for {utcnow} and {now} if given, #5189\n- make timestamp helper timezone-aware\n\nNew features:\n\n- create: implement --paths-from-stdin and --paths-from-command, see #5492.\n  These switches read paths to archive from stdin. Delimiter can specified\n  by --paths-delimiter=DELIM. Paths read will be added honoring every\n  option but exclusion options and --one-file-system. borg won't recurse\n  into directories.\n- 'obfuscate' pseudo compressor obfuscates compressed chunk size in repo\n- add pyfuse3 (successor of llfuse) as an alternative lowlevel fuse\n  implementation to llfuse (deprecated), #5407.\n  FUSE implementation can be switched via env var BORG_FUSE_IMPL.\n- allow appending to the files cache filename with BORG_FILES_CACHE_SUFFIX\n- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333\n\nOther changes:\n\n- split recursive directory walking/processing into directory walking and\n  item processing.\n- fix warning by importing setuptools before distutils.\n- debug info: include infos about FUSE implementation, #5546\n- testing:\n\n  - add a test for the hashindex corruption bug, #5531 #4829\n  - move away from travis-ci, use github actions, #5528 #5467\n  - test both on fuse2 and fuse3\n  - upload coverage reports to codecov\n  - fix spurious failure in test_cache_files, #5438\n  - add tests for Location.with_timestamp\n  - tox: add a non-fuse env to the envlist\n- vagrant:\n\n  - use python 3.7.latest and pyinstaller 4.0 for binary creation\n  - pyinstaller: compute basepath from spec file location\n  - vagrant: updates/fixes for archlinux box, #5543\n- docs:\n\n  - \"filename with spaces\" example added to exclude file, #5236\n  - add a hint about sleeping computer, #5301\n  - how to adjust macOS >= Catalina security settings, #5303\n  - process/policy for adding new compression algorithms\n  - updated docs about hacked backup client, #5480\n  - improve ansible deployment docs, make it more generic\n  - how to approach borg speed issues, give speed example, #5371\n  - fix mathematical inaccuracy about chunk size, #5336\n  - add example for excluding content using --pattern cli option\n  - clarify borg create's '--one-file-system' option, #4009\n  - improve docs/FAQ about append-only remote repos, #5497\n  - fix reST markup issues, labels\n  - add infos about contributor retirement status\n\n\nVersion 1.2.0a9 (2020-10-05)\n----------------------------\n\nFixes:\n\n- fix memory leak related to preloading, #5202\n- check --repair: fix potential data loss, #5325\n- persist shadow_index in between borg runs, #4830\n- fix hardlinked CACHEDIR.TAG processing, #4911\n- --read-special: .part files also should be regular files, #5217\n- allow server side enforcing of umask, --umask is for the local borg\n  process only (see docs), #4947\n- exit with 128 + signal number, #5161\n- borg config --list does not show last_segment_checked, #5159\n- locking:\n\n  - fix ExclusiveLock race condition bug, #4923\n  - fix race condition in lock migration, #4953\n  - fix locking on openindiana, #5271\n\nNew features:\n\n- --content-from-command: create archive using stdout of given command, #5174\n- allow key-import + BORG_KEY_FILE to create key files\n- build directory-based binary for macOS to avoid Gatekeeper delays\n\nOther changes:\n\n- upgrade bundled zstd to 1.4.5\n- upgrade bundled xxhash to 0.8.0, #5362\n- if self test fails, also point to OS and hardware, #5334\n- misc. shell completions fixes/updates, rewrite zsh completion\n- prettier error message when archive gets too big, #5307\n- stop relying on `false` exiting with status code 1\n- rephrase some warnings, #5164\n- parseformat: unnecessary calls removed, #5169\n- testing:\n\n  - enable Python3.9 env for test suite and VMs, #5373\n  - drop python 3.5, #5344\n  - misc. vagrant fixes/updates\n  - misc. testing fixes, #5196\n- docs:\n\n  - add ssh-agent pull backup method to doc, #5288\n  - mention double --force in prune docs\n  - update Homebrew install instructions, #5185\n  - better description of how cache and rebuilds of it work\n    and how the workaround applies to that\n  - point to borg create --list item flags in recreate usage, #5165\n  - add a note to create from stdin regarding files cache, #5180\n  - add security faq explaining AES-CTR crypto issues, #5254\n  - clarify --exclude-if-present in recreate, #5193\n  - add socat pull mode, #5150, #900\n  - move content of resources doc page to community project, #2088\n  - explain hash collision, #4884\n  - clarify --recompress option, #5154\n\n\nVersion 1.2.0a8 (2020-04-22)\n----------------------------\n\nFixes:\n\n- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829.\n  Please read and follow the more detailed notes close to the top of this document.\n- fix crash when upgrading erroneous hints file, #4922\n- commit-time free space calc: ignore bad compact map entries, #4796\n- info: if the archive doesn't exist, print a pretty message, #4793\n- --prefix / -P: fix processing, avoid argparse issue, #4769\n- ignore EACCES (errno 13) when hardlinking, #4730\n- add a try catch when formatting the info string, #4818\n- check: do not stumble over invalid item key, #4845\n- update prevalence of env vars to set config and cache paths\n- mount: fix FUSE low linear read speed on large files, #5032\n- extract: fix confusing output of borg extract --list --strip-components, #4934\n- recreate: support --timestamp option, #4745\n- fix ProgressIndicator msgids (JSON output), #4935\n- fuse: set f_namemax in statfs result, #2684\n- accept absolute paths on windows\n- pyinstaller: work around issue with setuptools > 44\n\nNew features:\n\n- chunker speedup (plus regression test)\n- added --consider-checkpoints and related test, #4788\n- added --noflags option, deprecate --nobsdflags option, #4489\n- compact: add --threshold option, #4674\n- mount: add birthtime to FUSE entries\n- support platforms with no os.link, #4901 - if we don't have os.link,\n  we just extract another copy instead of making a hardlink.\n- move sync_file_range to its own extension for better platform compatibility.\n- new --bypass-lock option to bypass locking, e.g. for read-only repos\n- accept absolute paths by removing leading slashes in patterns of all\n  sorts but re: style, #4029\n- delete: new --keep-security-info option\n\nOther changes:\n\n- support msgpack 0.6.2 and 1.0.0, #5065\n- upgrade bundled zstd to 1.4.4\n- upgrade bundled lz4 to 1.9.2\n- upgrade xxhash to 0.7.3\n- require recent enough llfuse for birthtime support, #5064\n- only store compressed data if the result actually is smaller, #4516\n- check: improve error output for matching index size, see #4829\n- ignore --stats when given with --dry-run, but continue, #4373\n- replaced usage of os.statvfs with shutil.disk_usage (better cross-platform support).\n- fuse: remove unneeded version check and compat code, micro opts\n- docs:\n\n  - improve description of path variables\n  - document how to delete data completely, #2929\n  - add FAQ about Borg config dir, #4941\n  - add docs about errors not printed as JSON, #4073\n  - update usage_general.rst.inc\n  - added \"Will move with BORG_CONFIG_DIR variable unless specified.\" to BORG_SECURITY_DIR info.\n  - put BORG_SECURITY_DIR immediately below BORG_CONFIG_DIR (and moved BORG_CACHE_DIR up before them).\n  - add paragraph regarding cache security assumptions, #4900\n  - tell about borg cache security precautions\n  - add FAQ describing difference between a local repo vs. repo on a server.\n  - document how to test exclusion patterns without performing an actual backup\n  - create: tell that \"Calculating size\" time and space needs are caused by --progress\n  - fix/improve documentation for @api decorator, #4674\n  - add a pull backup / push restore how-to, #1552\n  - fix man pages creation, #4752\n  - more general FAQ for backup and retain original paths, #4532\n  - explain difference between --exclude and --pattern, #4118\n  - add FAQ for preventing SSH timeout in extract, #3866\n  - improve password FAQ (decrease pw length, add -w 0 option to base64 to prevent line wrap), #4591\n  - add note about patterns and stored paths, #4160\n  - add upgrade of tools to pip installation how-to, #5090\n  - document one cause of orphaned chunks in check command, #2295\n  - clean up the whole check usage paragraph\n  - FAQ: linked recommended restrictions to ssh public keys on borg servers, #4946\n  - fixed \"doc downplays severity of Nonce reuse issue\", #4883\n  - borg repo restore instructions needed, #3428\n  - new FAQ: A repo is corrupt and must be replaced with an older repo.\n  - clarify borg init's encryption modes\n- native windows port:\n\n  - update README_WINDOWS.rst\n  - updated pyinstaller spec file to support windows builds\n- testing / CI:\n\n  - improved travis config / install script, improved macOS builds\n  - allow osx builds to fail, #4955\n  - Windows 10 build on Appveyor CI\n- vagrant:\n\n  - upgrade pyinstaller to v3.5 + patch\n  - use py369 for binary build, add py380 for tests\n  - fix issue in stretch VM hanging at grub installation\n  - add a debian buster and a ubuntu focal VM\n  - update darwin box to 10.12\n  - upgrade FreeBSD box to 12.1\n  - fix debianoid virtualenv packages\n  - use pyenv in freebsd64 VM\n  - remove the flake8 test\n  - darwin: avoid error if pkg is already installed\n  - debianoid: don't interactively ask questions\n\n\nVersion 1.2.0a7 (2019-09-07)\n----------------------------\n\nFixes:\n\n- slave hardlinks extraction issue, see #4350\n- extract: fix KeyError for \"partial\" extraction, #4607\n- preload chunks for hardlink slaves w/o preloaded master, #4350\n- fix preloading for old remote servers, #4652\n- fix partial extract for hardlinked contentless file types, #4725\n- Repository.open: use stat() to check for repo dir, #4695\n- Repository.check_can_create_repository: use stat() to check, ~ #4695.\n- SecurityManager.known(): check all files, #4614\n- after double-force delete, warn about necessary repair, #4704\n- cope with ANY error when importing pytest into borg.testsuite, #4652\n- fix invalid archive error message\n- setup.py: fix detection of missing Cython\n- filter out selinux xattrs, #4574\n- location arg - should it be optional? #4541\n- enable placeholder usage in --comment, #4559\n- use whitelist approach for borg serve, #4097\n\nNew features:\n\n- minimal native Windows support, see windows readme (work in progress)\n- create: first ctrl-c (SIGINT) triggers checkpoint and abort, #4606\n- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710\n- remove WSL autodetection. if WSL still has this problem, you need to\n  set BORG_WORKAROUNDS=basesyncfile in the borg process environment to\n  work around it.\n- support xxh64 checksum in addition to the hashlib hashes in borg list\n- enable placeholder usage in all extra archive arguments\n- enable placeholder usage in --comment, #4559\n- enable placeholder usage in --glob-archives, #4495\n- ability to use a system-provided version of \"xxhash\"\n- create:\n\n  - changed the default behaviour not to store the atime of fs items. atime is\n    often rather not interesting and fragile - it easily changes even if nothing\n    else has changed and, if stored into the archive, spoils deduplication of\n    the archive metadata stream.\n  - if you give the --noatime option, borg will output a deprecation warning\n    because it is currently ignored / does nothing.\n    Please remove the --noatime option when using borg 1.2.\n  - added a --atime option for storing files' atime into an archive\n\nOther changes:\n\n- argparser: always use REPOSITORY in metavar\n- do not check python/libc for borg serve, #4483\n- small borg compact improvements, #4522\n- compact: log freed space at INFO level\n- tests:\n\n  - tox / travis: add testing on py38-dev\n  - fix broken test that relied on improper zlib assumptions\n  - pure-py msgpack warning shall not make a lot of tests fail, #4558\n  - rename test_mount_hardlinks to test_fuse_mount_hardlinks (master)\n  - vagrant: add up-to-date openindiana box (py35, openssl10)\n  - get rid of confusing coverage warning, #2069\n- docs:\n\n  - reiterate that 'file cache names are absolute' in FAQ,\n    mention bind mount solution, #4738\n  - add restore docs, #4670\n  - updated docs to cover use of temp directory on remote, #4545\n  - add a push-style example to borg-create(1), #4613\n  - timestamps in the files cache are now usually ctime, #4583\n  - benchmark crud: clarify that space is used until compact\n  - update documentation of borg create,\n    corrects a mention of borg 1.1 as a future version.\n  - fix osxfuse github link in installation docs\n  - how to supply a passphrase, use crypto devices, #4549\n  - extract: document limitation \"needs empty destination\",  #4598\n  - update macOS Brew link\n  - add note about software for automating backup\n  - compact: improve docs,\n  - README: new URL for funding options\n\n\nVersion 1.2.0a6 (2019-04-22)\n----------------------------\n\nFixes:\n\n- delete / prune: consider part files correctly for stats, #4507\n- fix \"all archives\" stats considering part files, #4329\n- create: only run stat_simple_attrs() once\n- create: --stats does not work with --dry-run, exit with error msg, #4373\n- give \"invalid repo\" error msg if repo config not found, #4411\n\nNew features:\n\n- display msgpack version as part of sysinfo (e.g. in tracebacks)\n\nOther changes:\n\n- docs:\n\n  - sdd \"SSH Configuration\" section, #4493, #3988, #636, #4485\n  - better document borg check --max-duration, #4473\n  - sorted commands help in multiple steps, #4471\n- testing:\n\n  - travis: use py 3.5.3 and 3.6.7 on macOS to get a pyenv-based python\n    build with openssl 1.1\n  - vagrant: use py 3.5.3 and 3.6.8 on darwin64 VM to build python and\n    borg with openssl 1.1\n  - pytest: -v and default XDISTN to 1, #4481\n\n\nVersion 1.2.0a5 (2019-03-21)\n----------------------------\n\nFixes:\n\n- warn if a file has changed while being backed up, #1750\n- lrucache: regularly remove old FDs, #4427\n- borg command shall terminate with rc 2 for ImportErrors, #4424\n- make freebsd xattr platform code api compatible with linux, #3952\n\nOther changes:\n\n- major setup code refactoring (especially how libraries like openssl, liblz4,\n  libzstd, libb2 are discovered and how it falls back to code bundled with\n  borg), new: uses pkg-config now (and needs python \"pkgconfig\" package\n  installed), #1925\n\n  if you are a borg package maintainer, please try packaging this\n  (see comments in setup.py).\n- Vagrantfile: add zstd, reorder, build env vars, #4444\n- travis: install script improvements\n- update shell completions\n- docs:\n\n  - add a sample logging.conf in docs/misc, #4380\n  - fix spelling errors\n  - update requirements / install docs, #4374\n\n\nVersion 1.2.0a4 (2019-03-11)\n----------------------------\n\nFixes:\n\n- do not use O_NONBLOCK for special files, like FIFOs, block and char devices\n  when using --read-special. fixes backing up FIFOs. fixes to test. #4394\n- more LibreSSL build fixes: LibreSSL has HMAC_CTX_free and HMAC_CTX_new\n\nNew features:\n\n- check: incremental repo check (only checks crc32 for segment entries), #1657\n  borg check --repository-only --max-duration SECONDS ...\n- delete: timestamp for borg delete --info added, #4359\n\nOther changes:\n\n- redo stale lock handling, #3986\n  drop BORG_HOSTNAME_IS_UNIQUE (please use BORG_HOST_ID if needed).\n  borg now always assumes it has a unique host id - either automatically\n  from fqdn plus uuid.getnode() or overridden via BORG_HOST_ID.\n- docs:\n\n  - added Alpine Linux to distribution list\n  - elaborate on append-only mode docs\n- vagrant:\n\n  - darwin: new 10.12 box\n  - freebsd: new 12.0 box\n  - openbsd: new 6.4 box\n  - misc. updates / fixes\n\n\nVersion 1.2.0a3 (2019-02-26)\n----------------------------\n\nFixes:\n\n- LibreSSL build fixes, #4403\n- dummy ACL/xattr code fixes (used by OpenBSD and others), #4403\n- create: fix openat/statat issues for root directory, #4405\n\n\nVersion 1.2.0a2 and earlier (2019-02-24)\n----------------------------------------\n\nNew features:\n\n- compact: \"borg compact\" needs to be used to free repository space by\n  compacting the segments (reading sparse segments, rewriting still needed\n  data to new segments, deleting the sparse segments).\n  Borg < 1.2 invoked compaction automatically at the end of each repository\n  writing command.\n  Borg >= 1.2 does not do that any more to give better speed, more control,\n  more segment file stability (== less stuff moving to newer segments) and\n  more robustness.\n  See the docs about \"borg compact\" for more details.\n- \"borg compact --cleanup-commits\" is to cleanup the tons of 17byte long\n  commit-only segment files caused by borg 1.1.x issue #2850.\n  Invoke this once after upgrading (the server side) borg to 1.2.\n  Compaction now automatically removes unneeded commit-only segment files.\n- prune: Show which rule was applied to keep archive, #2886\n- add fixed blocksize chunker (see --chunker-params docs), #1086\n\nFixes:\n\n- avoid stale filehandle issues, #3265\n- use more FDs, avoid race conditions on active fs, #906, #908, #1038\n- add O_NOFOLLOW to base flags, #908\n- compact:\n\n  - require >10% freeable space in a segment, #2985\n  - repository compaction now automatically removes unneeded 17byte\n    commit-only segments, #2850\n- make swidth available on all posix platforms, #2667\n\nOther changes:\n\n- repository: better speed and less stuff moving around by using separate\n  segment files for manifest DELETEs and PUTs, #3947\n- use pyinstaller v3.3.1 to build binaries\n- update bundled zstd code to 1.3.8, #4210\n- update bundled lz4 code to 1.8.3, #4209\n- msgpack:\n\n  - switch to recent \"msgpack\" pypi pkg name, #3890\n  - wrap msgpack to avoid future compat complications, #3632, #2738\n  - support msgpack 0.6.0 and 0.6.1, #4220, #4308\n\n- llfuse: modernize / simplify llfuse version requirements\n- code refactorings / internal improvements:\n\n  - include size/csize/nfiles[_parts] stats into archive, #3241\n  - calc_stats: use archive stats metadata, if available\n  - crypto: refactored crypto to use an AEAD style API\n  - crypto: new AES-OCB, CHACHA20-POLY1305\n  - create: use less syscalls by not using a python file obj, #906, #3962\n  - diff: refactor the diff functionality to new ItemDiff class, #2475\n  - archive: create FilesystemObjectProcessors class\n  - helpers: make a package, split into smaller modules\n  - xattrs: move to platform package, use cython instead ctypes, #2495\n  - xattrs/acls/bsdflags: misc. code/api optimizations\n  - FUSE: separate creation of filesystem from implementation of llfuse funcs, #3042\n  - FUSE: use unpacker.tell() instead of deprecated write_bytes, #3899\n  - setup.py: move build_man / build_usage code to setup_docs.py\n  - setup.py: update to use a newer Cython/setuptools API for compiling .pyx -> .c, #3788\n  - use python 3.5's os.scandir / os.set_blocking\n  - multithreading preparations (not used yet):\n\n    - item.to_optr(), Item.from_optr()\n    - fix chunker holding the GIL during blocking I/O\n  - C code portability / basic MSC compatibility, #4147, #2677\n- testing:\n\n  - vagrant: new VMs for linux/bsd/darwin, most with OpenSSL 1.1 and py36\n\n\nVersion 1.1.18 (2022-06-05)\n---------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux).\n  If WSL still has a problem with sync_file_range, you need to set\n  BORG_WORKAROUNDS=basesyncfile in the borg process environment to\n  work around the WSL issue.\n- 1.1.14 changes return codes due to a bug fix:\n  In case you have scripts expecting rc == 2 for a signal exit, you need to\n  update them to check for >= 128 (as documented since long).\n- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now.\n- 1.1.17 install_requires the \"packaging\" pypi package now.\n\nNew features:\n\n- check --repair: significantly speed up search for next valid object in segment, #6022\n- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024\n- key export: display key if path is '-' or not given, #6092\n- list --format: add command_line to format keys, #6108\n\nFixes:\n\n- check: improve error handling for corrupt archive metadata block,\n  make robust_iterator more robust, #4777\n- diff: support presence change for blkdev, chrdev and fifo items, #6483\n- diff: reduce memory consumption, fix is_hardlink_master\n- init: disallow overwriting of existing keyfiles\n- info: fix authenticated mode repo to show \"Encrypted: No\", #6462\n- info: emit repo info even if repo has 0 archives, #6120\n- list: remove placeholders for shake_* hashes, #6082\n- mount -o versions: give clear error msg instead of crashing\n- show_progress: add finished=true/false to archive_progress json, #6570\n- fix hardlinkable file type check, #6037\n- do not show archive name in error msgs referring to the repository, #6023\n- prettier error msg (no stacktrace) if exclude file is missing, #5734\n- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979\n- atomically create the CACHE_TAG file, #6028\n- deal with the SaveFile/SyncFile race, docs, see #6176 5c5b59bc9\n- avoid expanding path into LHS of formatting operation + tests, #6064 #6063\n- repository: quota / compactable computation fixes, #6119.\n  This is mainly to keep the repo code in sync with borg 1.2. As borg 1.1\n  compacts immediately, there was not really an issue with this in 1.1.\n- fix transaction rollback: use files cache filename as found in txn.active, #6353\n- do not load files cache for commands not using it, fixes #5673\n- fix scp repo url parsing for ip v6 addrs, #6526\n- repo::archive location placeholder expansion fixes, #5826, #5998\n\n  - use expanded location for log output\n  - support placeholder expansion for BORG_REPO env var\n- respect umask for created directory and file modes, #6400\n- safer truncate_and_unlink implementation\n\nOther changes:\n\n- upgrade bundled xxhash code to 0.8.1\n- fix xxh64 related build (setup.py and post-0.8.1 patch for static_assert).\n  The patch was required to build the bundled xxhash code on FreeBSD, see\n  https://github.com/Cyan4973/xxHash/pull/670\n- msgpack build: remove endianness macro, #6105\n- update and fix shell completions\n- fuse: remove unneeded version check and compat code\n- delete --force: do not ask when deleting a repo, #5941\n- delete: don't commit if nothing was deleted, avoid cache sync, #6060\n- delete: add repository id and location to prompt\n- compact segments: improve freeable / freed space log output, #5679\n- if ensure_dir() fails, give more informative error message, #5952\n- load_key: no key is same as empty key, #6441\n- better error msg for defect or unsupported repo configs, #6566\n- use hmac.compare_digest instead of ==, #6470\n- implement more standard hashindex.setdefault behaviour\n- remove stray punctuation from secure-erase message\n- add development.lock.txt, use a real python 3.5 to generate frozen reqs\n- setuptools 60.7.0 breaks pyinstaller, #6246\n- setup.py clean2 was added to work around some setuptools customizability limitation.\n- allow extra compiler flags for every extension build\n- C code: make switch fallthrough explicit\n- Cython code: fix \"useless trailing comma\" cython warnings\n- requirements.lock.txt: use the latest cython 0.29.30\n- fix compilation warnings: ‘PyUnicode_AsUnicode’ is deprecated\n- docs:\n\n  - ~/.config/borg/keys is not used for repokey keys, #6107\n  - excluded parent dir's metadata can't restore, #6062\n  - permissions note rewritten to make it less confusing, #5490\n  - add note about grandfather-father-son backup retention policy / rotation scheme\n  - clarify who starts the remote agent (borg serve)\n  - test/improve pull backup docs, #5903\n  - document the socat pull mode described in #900 #515ß\n  - borg serve: improve ssh forced commands docs, #6083\n  - improve docs for borg list --format, #6080\n  - fix the broken link to .nix file\n  - clarify pattern usage with commands, #5176\n  - clarify user_id vs uid for fuse, #5723\n  - fix binary build freebsd/macOS version, #5942\n  - FAQ: fix manifest-timestamp path, #6016\n  - remove duplicate faq entries, #5926\n  - fix sphinx warnings, #5919\n  - virtualisation speed tips\n  - fix values of TAG bytes, #6515\n  - recommend umask for passphrase file perms\n  - update link to ubuntu packages, #6485\n  - clarify on-disk order and size of log entry fields, #6357\n  - do not transform --/--- to unicode dashes\n  - improve linking inside docs, link to borg_placeholders, link to borg_patterns\n  - use same phrasing in misc. help texts\n  - borg init: explain the encryption modes better\n  - explain the difference between a path that ends with or without a slash, #6297\n  - clarify usage of patternfile roots, #6242\n  - borg key export: add examples\n  - updates about features not experimental any more: FUSE \"versions\" view, --pattern*, #6134\n  - fix/update cygwin package requirements\n  - impact of deleting path/to/repo/nonce, #5858\n  - warn about tampered server nonce\n  - mention BORG_FILES_CACHE_SUFFIX as alternative to BORG_FILES_CACHE_TTL, #5602\n  - add a troubleshooting note about \"is not a valid repository\" to the FAQ\n- vagrant / CI / testing:\n\n  - misc. fixes and updates, new python versions\n  - macOS on github: re-enable fuse2 testing by downgrading to older macOS, #6099\n  - fix OpenBSD symlink mode test failure, #2055\n  - use the generic/openbsd6 box\n  - strengthen the test: we can read data w/o nonces\n  - add tests for path/to/repo/nonce deletion\n  - darwin64: backport some tunings from master\n  - darwin64: remove fakeroot, #6314\n  - darwin64: fix vagrant scp, #5921\n  - darwin64: use macfuse instead of osxfuse\n  - add ubuntu \"jammy\" 22.04 LTS VM\n  - adapt memory for openindiana64 and darwin64\n\n\nVersion 1.1.17 (2021-07-12)\n---------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux).\n  If WSL still has a problem with sync_file_range, you need to set\n  BORG_WORKAROUNDS=basesyncfile in the borg process environment to\n  work around the WSL issue.\n- 1.1.14 changes return codes due to a bug fix:\n  In case you have scripts expecting rc == 2 for a signal exit, you need to\n  update them to check for >= 128 (as documented since long).\n- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now.\n- 1.1.17 install_requires the \"packaging\" pypi package now.\n\nFixes:\n\n- pyinstaller dir-mode: fix pyi detection / LIBPATH treatment, #5897\n- handle crash due to kill stale lock race, #5828\n- fix BORG_CACHE_DIR crashing borg if empty, #5216\n- create --dry-run: fix display of kept tagfile, #5834\n- fix missing parameter in \"did not consistently fail\" msg, #5822\n- missing / healed chunks: always tell chunk ID, #5704\n- benchmark: make sure cleanup happens even on exceptions, #5630\n\nNew features:\n\n- implement BORG_SELFTEST env variable, #5871.\n  this can be used to accelerate borg startup a bit. not recommended for\n  normal usage, but borg mass hosters with a lot of borg invocations can\n  save some resources with this. on my laptop, this saved ~100ms cpu time\n  (sys+user) per borg command invocation.\n- implement BORG_LIBC env variable to give the libc filename, #5870.\n  you can use this if a borg does not find your libc.\n- check: add progress indicator for archive check.\n- allow --files-cache=size (not recommended, make sure you know what you do)\n\nOther changes:\n\n- Python 3.10 now officially supported!\n  we test on py310-dev on github CI since a while and now also on the vagrant\n  machines, so it should work ok.\n- github CI: test on py310 (again)\n- get rid of distutils, use packaging and setuptools.\n  distutils is deprecated and gives warnings on py 3.10.\n- setup.py: rename \"clean\" to \"clean2\" to avoid shadowing the \"clean\" command.\n- remove libc filename fallback for the BSDs (there is no \"usual\" name)\n- cleanup flake8 checks, fix some pep8 violations.\n- docs building: replace deprecated function \".add_stylesheet()\" for Sphinx 4 compatibility\n- docs:\n\n  - add a hint on sleeping computer and ssh connections, #5301\n  - update the documentation on hacked backup client, #5480\n  - improve docs/FAQ about append-only remote repos, #5497\n  - complement the documentation for pattern files and exclude files, #5520\n  - \"filename with spaces\" example added to exclude file, #5236\n    note: no whitespace escaping needed, processed by borg.\n  - add info on renaming repositories, #5240\n  - clarify borg check --verify-data, #5808\n  - add notice about defective hardware to check documentation, #5753\n  - add paragraph added in #5855 to utility documentation source\n  - add missing leading slashes in help patterns, #5857\n  - clarify \"you will need key and passphrase\" borg init warning, #4622\n  - pull mode: add some warnings, #5827\n  - mention tar --compare (compare archive to fs files), #5880\n  - fix typos, backport of #5597\n- vagrant:\n\n  - add py3.7.11 for binary build, also add 3.10-dev.\n  - use latest Cython 0.29.23 for py310 compat fixes.\n  - more RAM for openindiana upgrade plan resolver, it just hangs (swaps?) if\n    there is too little RAM.\n  - fix install_pyenv to adapt to recent changes in pyenv (same as in master now).\n  - use generic/netbsd9 box, copied from master branch.\n\n\nVersion 1.1.16 (2021-03-23)\n---------------------------\n\nFixes:\n\n- setup.py: add special openssl prefix for Apple M1 compatibility\n- do not recurse into duplicate roots, #5603\n- remove empty shadowed_segments lists, #5275, #5614\n- fix libpython load error when borg fat binary / dir-based binary is invoked\n  via a symlink by upgrading pyinstaller to v4.2, #5688\n- config: accept non-int value (like 500M or 100G) for max_segment_size or\n  storage_quota, #5639.\n  please note: when setting a non-int value for this in a repo config,\n  using the repo will require borg >= 1.1.16.\n\nNew features:\n\n- bundled msgpack: drop support for old buffer protocol to support Python 3.10\n- verbose files cache logging via --debug-topic=files_cache, #5659.\n  Use this if you suspect that borg does not detect unmodified files as expected.\n- create/extract: add --noxattrs and --noacls option, #3955.\n  when given with borg create, borg will not get xattrs / ACLs from input files\n  (and thus, it will not archive xattrs / ACLs). when given with borg extract,\n  borg will not read xattrs / ACLs from archive and will not set xattrs / ACLs\n  on extracted files.\n- diff: add --json-lines option, #3765\n- check: debug log segment filename\n- borg debug dump-hints\n\nOther changes:\n\n- Tab completion support for additional archives for 'borg delete'\n- repository: deduplicate code of put and delete, no functional change\n- tests: fix result order issue (sporadic test failure on openindiana)\n- vagrant:\n\n  - upgrade pyinstaller to v4.2, #5671\n  - avoid grub-install asking interactively for device\n  - remove the xenial box\n  - update freebsd box to 12.1\n- docs:\n\n  - update macOS install instructions, #5677\n  - use macFUSE (not osxfuse) for Apple M1 compatibility\n  - update docs for dev environment installation instructions, #5643\n  - fix grammar in faq\n  - recommend running tests only on installed versions for setup\n  - add link back to git-installation\n  - remove /var/cache exclusion in example commands, #5625.\n    This is generally a poor idea and shouldn't be promoted through examples.\n  - add repology.org badge with current packaging status\n  - explain hash collision\n  - add unsafe workaround to use an old repo copy, #5722\n\n\nVersion 1.1.15 (2020-12-25)\n---------------------------\n\nFixes:\n\n- extract:\n\n  - improve exception handling when setting xattrs, #5092.\n  - emit a warning message giving the path, xattr key and error message.\n  - continue trying to restore other xattrs and bsdflags of the same file\n    after an exception with xattr-setting happened.\n- export-tar:\n\n  - set tar format to GNU_FORMAT explicitly, #5274\n  - fix memory leak with ssh: remote repository, #5568\n  - fix potential memory leak with ssh: remote repository with partial extraction\n- create: fix --dry-run and --stats coexistence, #5415\n- use --timestamp for {utcnow} and {now} if given, #5189\n\nNew features:\n\n- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333\n- allow appending the files cache filename with BORG_FILES_CACHE_SUFFIX env var\n\nOther changes:\n\n- drop python 3.4 support, minimum requirement is 3.5 now.\n- enable using libxxhash instead of bundled xxh64 code\n- update llfuse requirements (1.3.8)\n- set cython language_level in some files to fix warnings\n- allow EIO with warning when trying to hardlink\n- PropDict: fail early if internal_dict is not a dict\n- update shell completions\n- tests / CI\n\n  - add a test for the hashindex corruption bug, #5531 #4829\n  - fix spurious failure in test_cache_files, #5438\n  - added a github ci workflow\n  - reduce testing on travis, no macOS, no py3x-dev, #5467\n  - travis: use newer dists, native py on dist\n- vagrant:\n\n  - remove jessie and trusty boxes, #5348 #5383\n  - pyinstaller 4.0, build on py379\n  - binary build on stretch64, #5348\n  - remove easy_install based pip installation\n- docs:\n\n  - clarify '--one-file-system' for btrfs, #5391\n  - add example for excluding content using the --pattern cmd line arg\n  - complement the documentation for pattern files and exclude files, #5524\n  - made ansible playbook more generic, use package instead of pacman. also\n    change state from \"latest\" to \"present\".\n  - complete documentation on append-only remote repos, #5497\n  - internals: rather talk about target size than statistics, #5336\n  - new compression algorithm policy, #1633 #5505\n  - faq: add a hint on sleeping computer, #5301\n  - note requirements for full disk access on macOS Catalina, #5303\n  - fix/improve description of borg upgrade hardlink usage, #5518\n- modernize 1.1 code:\n\n  - drop code/workarounds only needed to support Python 3.4\n  - remove workaround for pre-release py37 argparse bug\n  - removed some outdated comments/docstrings\n  - requirements: remove some restrictions, lock on current versions\n\n\nVersion 1.1.14 (2020-10-07)\n---------------------------\n\nFixes:\n\n- check --repair: fix potential data loss when interrupting it, #5325\n- exit with 128 + signal number (as documented) when borg is killed by a signal, #5161\n- fix hardlinked CACHEDIR.TAG processing, #4911\n- create --read-special: .part files also should be regular files, #5217\n- llfuse dependency: choose least broken 1.3.6/1.3.7.\n  1.3.6 is broken on python 3.9, 1.3.7 is broken on FreeBSD.\n\nOther changes:\n\n- upgrade bundled xxhash to 0.7.4\n- self test: if it fails, also point to OS and hardware, #5334\n- pyinstaller: compute basepath from spec file location\n- prettier error message when archive gets too big, #5307\n- check/recreate are not \"experimental\" any more (but still potentially dangerous):\n\n  - recreate: remove extra confirmation\n  - rephrase some warnings, update docs, #5164\n- shell completions:\n\n  - misc. updates / fixes\n  - support repositories in fish tab completion, #5256\n  - complete $BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING\n  - rewrite zsh completion:\n\n    - completion for almost all optional and positional arguments\n    - completion for Borg environment variables (parameters)\n- use \"allow/deny list\" instead of \"white/black list\" wording\n- declare \"allow_cache_wipe\" marker in setup.cfg to avoid pytest warning\n- vagrant / tests:\n\n  - misc. fixes / updates\n  - use python 3.5.10 for binary build\n  - build directory-based binaries additionally to the single file binaries\n  - add libffi-dev, required to build python\n  - use cryptography<3.0, more recent versions break the jessie box\n  - test on python 3.9\n  - do brew update with /dev/null redirect to avoid \"too much log output\" on travis-ci\n- docs:\n\n  - add ssh-agent pull backup method docs, #5288\n  - how to approach borg speed issues, #5371\n  - mention double --force in prune docs\n  - update Homebrew install instructions, #5185\n  - better description of how cache and rebuilds of it work\n  - point to borg create --list item flags in recreate usage, #5165\n  - add security faq explaining AES-CTR crypto issues, #5254\n  - add a note to create from stdin regarding files cache, #5180\n  - fix borg.1 manpage generation regression, #5211\n  - clarify how exclude options work in recreate, #5193\n  - add section for retired contributors\n  - hint about not misusing private email addresses of contributors for borg support\n\n\nVersion 1.1.13 (2020-06-06)\n---------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux).\n  If WSL still has a problem with sync_file_range, you need to set\n  BORG_WORKAROUNDS=basesyncfile in the borg process environment to\n  work around the WSL issue.\n\nFixes:\n\n- rebuilt using a current Cython version, compatible with python 3.8, #5214\n\n\nVersion 1.1.12 (2020-06-06)\n---------------------------\n\nFixes:\n\n- fix preload-related memory leak, #5202.\n- mount / borgfs (FUSE filesystem):\n\n  - fix FUSE low linear read speed on large files, #5067\n  - fix crash on old llfuse without birthtime attrs, #5064 - accidentally\n    we required llfuse >= 1.3. Now also old llfuse works again.\n  - set f_namemax in statfs result, #2684\n- update precedence of env vars to set config and cache paths, #4894\n- correctly calculate compression ratio, taking header size into account, too\n\nNew features:\n\n- --bypass-lock option to bypass locking with read-only repositories\n\nOther changes:\n\n- upgrade bundled zstd to 1.4.5\n- travis: adding comments and explanations to Travis config / install script,\n  improve macOS builds.\n- tests: test_delete_force: avoid sporadic test setup issues, #5196\n- misc. vagrant fixes\n- the binary for macOS is now built on macOS 10.12\n- the binaries for Linux are now built on Debian 8 \"Jessie\", #3761\n- docs:\n\n  - PlaceholderError not printed as JSON, #4073\n  - \"How important is Borg config?\", #4941\n  - make Sphinx warnings break docs build, #4587\n  - some markup / warning fixes\n  - add \"updating borgbackup.org/releases\" to release checklist, #4999\n  - add \"rendering docs\" to release checklist, #5000\n  - clarify borg init's encryption modes\n  - add note about patterns and stored paths, #4160\n  - add upgrade of tools to pip installation how-to\n  - document one cause of orphaned chunks in check command, #2295\n  - linked recommended restrictions to ssh public keys on borg servers in faq, #4946\n\n\nVersion 1.1.11 (2020-03-08)\n---------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux).\n  If WSL still has a problem with sync_file_range, you need to set\n  BORG_WORKAROUNDS=basesyncfile in the borg process environment to\n  work around the WSL issue.\n\nFixes:\n\n- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829.\n  Please read and follow the more detailed notes close to the top of this document.\n- upgrade bundled xxhash to 0.7.3, #4891.\n  0.7.2 is the minimum requirement for correct operations on ARMv6 in non-fixup\n  mode, where unaligned memory accesses cause bus errors.\n  0.7.3 adds some speedups and libxxhash 0.7.3 even has a pkg-config file now.\n- upgrade bundled lz4 to 1.9.2\n- upgrade bundled zstd to 1.4.4\n- fix crash when upgrading erroneous hints file, #4922\n- extract:\n\n  - fix KeyError for \"partial\" extraction, #4607\n  - fix \"partial\" extract for hardlinked contentless file types, #4725\n  - fix preloading for old (0.xx) remote servers, #4652\n  - fix confusing output of borg extract --list --strip-components, #4934\n- delete: after double-force delete, warn about necessary repair, #4704\n- create: give invalid repo error msg if repo config not found, #4411\n- mount: fix FUSE mount missing st_birthtime, #4763 #4767\n- check: do not stumble over invalid item key, #4845\n- info: if the archive doesn't exist, print a pretty message, #4793\n- SecurityManager.known(): check all files, #4614\n- Repository.open: use stat() to check for repo dir, #4695\n- Repository.check_can_create_repository: use stat() to check, #4695\n- fix invalid archive error message\n- fix optional/non-optional location arg, #4541\n- commit-time free space calc: ignore bad compact map entries, #4796\n- ignore EACCES (errno 13) when hardlinking the old config, #4730\n- --prefix / -P: fix processing, avoid argparse issue, #4769\n\nNew features:\n\n- enable placeholder usage in all extra archive arguments\n- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710\n- recreate: support --timestamp option, #4745\n- support platforms without os.link (e.g. Android with Termux), #4901.\n  if we don't have os.link, we just extract another copy instead of making a hardlink.\n- support linux platforms without sync_file_range (e.g. Android 7 with Termux), #4905\n\nOther:\n\n- ignore --stats when given with --dry-run, but continue, #4373\n- add some ProgressIndicator msgids to code / fix docs, #4935\n- elaborate on \"Calculating size\" message\n- argparser: always use REPOSITORY in metavar, also use more consistent help phrasing.\n- check: improve error output for matching index size, see #4829\n- docs:\n\n  - changelog: add advisory about hashindex_set bug #4829\n  - better describe BORG_SECURITY_DIR, BORG_CACHE_DIR, #4919\n  - infos about cache security assumptions, #4900\n  - add FAQ describing difference between a local repo vs. repo on a server.\n  - document how to test exclusion patterns without performing an actual backup\n  - timestamps in the files cache are now usually ctime, #4583\n  - fix bad reference to borg compact (does not exist in 1.1), #4660\n  - create: borg 1.1 is not future any more\n  - extract: document limitation \"needs empty destination\", #4598\n  - how to supply a passphrase, use crypto devices, #4549\n  - fix osxfuse github link in installation docs\n  - add example of exclude-norecurse rule in help patterns\n  - update macOS Brew link\n  - add note about software for automating backups, #4581\n  - AUTHORS: mention copyright+license for bundled msgpack\n  - fix various code blocks in the docs, #4708\n  - updated docs to cover use of temp directory on remote, #4545\n  - add restore docs, #4670\n  - add a pull backup / push restore how-to, #1552\n  - add FAQ how to retain original paths, #4532\n  - explain difference between --exclude and --pattern, #4118\n  - add FAQs for SSH connection issues, #3866\n  - improve password FAQ, #4591\n  - reiterate that 'file cache names are absolute' in FAQ\n- tests:\n\n  - cope with ANY error when importing pytest into borg.testsuite, #4652\n  - fix broken test that relied on improper zlib assumptions\n  - test_fuse: filter out selinux xattrs, #4574\n- travis / vagrant:\n\n  - misc python versions removed / changed (due to openssl 1.1 compatibility)\n    or added (3.7 and 3.8, for better borg compatibility testing)\n  - binary building is on python 3.5.9 now\n- vagrant:\n\n  - add new boxes: ubuntu 18.04 and 20.04, debian 10\n  - update boxes: openindiana, darwin, netbsd\n  - remove old boxes: centos 6\n  - darwin: updated osxfuse to 3.10.4\n  - use debian/ubuntu pip/virtualenv packages\n  - rather use python 3.6.2 than 3.6.0, fixes coverage/sqlite3 issue\n  - use requirements.d/development.lock.txt to avoid compat issues\n- travis:\n\n  - darwin: backport some install code / order from master\n  - remove deprecated keyword \"sudo\" from travis config\n  - allow osx builds to fail, #4955\n    this is due to travis-ci frequently being so slow that the OS X builds\n    just fail because they exceed 50 minutes and get killed by travis.\n\n\nVersion 1.1.10 (2019-05-16)\n---------------------------\n\nFixes:\n\n- extract: hang on partial extraction with ssh: repo, when hardlink master\n  is not matched/extracted and borg hangs on related slave hardlink, #4350\n- lrucache: regularly remove old FDs, #4427\n- avoid stale filehandle issues, #3265\n- freebsd: make xattr platform code api compatible with linux, #3952\n- use whitelist approach for borg serve, #4097\n- borg command shall terminate with rc 2 for ImportErrors, #4424\n- create: only run stat_simple_attrs() once, this increases\n  backup with lots of unchanged files performance by ~ 5%.\n- prune: fix incorrect borg prune --stats output with --dry-run, #4373\n- key export: emit user-friendly error if repo key is exported to a directory,\n  #4348\n\nNew features:\n\n- bundle latest supported msgpack-python release (0.5.6), remove msgpack-python\n  from setup.py install_requires - by default we use the bundled code now.\n  optionally, we still support using an external msgpack (see hints in\n  setup.py), but this requires solid requirements management within\n  distributions and is not recommended.\n  borgbackup will break if you upgrade msgpack to an unsupported version.\n- display msgpack version as part of sysinfo (e.g. in tracebacks)\n- timestamp for borg delete --info added, #4359\n- enable placeholder usage in --comment and --glob-archives, #4559, #4495\n\nOther:\n\n- serve: do not check python/libc for borg serve, #4483\n- shell completions: borg diff second archive\n- release scripts: signing binaries with Qubes OS support\n- testing:\n\n  - vagrant: upgrade openbsd box to 6.4\n  - travis-ci: lock test env to py 3.4 compatible versions, #4343\n  - get rid of confusing coverage warning, #2069\n  - rename test_mount_hardlinks to test_fuse_mount_hardlinks,\n    so both can be excluded by \"not test_fuse\".\n  - pure-py msgpack warning shall not make a lot of tests fail, #4558\n- docs:\n\n  - add \"SSH Configuration\" section to \"borg serve\", #3988, #636, #4485\n  - README: new URL for funding options\n  - add a sample logging.conf in docs/misc, #4380\n  - elaborate on append-only mode docs, #3504\n  - installation: added Alpine Linux to distribution list, #4415\n  - usage.html: only modify window.location when redirecting, #4133\n  - add msgpack license to docs/3rd_party/msgpack\n- vagrant / binary builds:\n\n  - use python 3.5.7 for builds\n  - use osxfuse 3.8.3\n\n\nVersion 1.1.9 (2019-02-10)\n--------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n\nFixes:\n\n- security fix: configure FUSE with \"default_permissions\", #3903\n  \"default_permissions\" is now enforced by borg by default to let the\n  kernel check uid/gid/mode based permissions.\n  \"ignore_permissions\" can be given not to enforce \"default_permissions\".\n- make \"hostname\" short, even on misconfigured systems, #4262\n- fix free space calculation on macOS (and others?), #4289\n- config: quit with error message when no key is provided, #4223\n- recover_segment: handle too small segment files correctly, #4272\n- correctly release memoryview, #4243\n- avoid diaper pattern in configparser by opening files, #4263\n- add \"# cython: language_level=3\" directive to .pyx files, #4214\n- info: consider part files for \"This archive\" stats, #3522\n- work around Microsoft WSL issue #645 (sync_file_range), #1961\n\nNew features:\n\n- add --rsh command line option to complement BORG_RSH env var, #1701\n- init: --make-parent-dirs parent1/parent2/repo_dir, #4235\n\nOther:\n\n- add archive name to check --repair output, #3447\n- check for unsupported msgpack versions\n- shell completions:\n\n  - new shell completions for borg 1.1.9\n  - more complete shell completions for borg mount -o\n  - added shell completions for borg help\n  - option arguments for zsh tab completion\n- docs:\n\n  - add FAQ regarding free disk space check, #3905\n  - update BORG_PASSCOMMAND example and clarify variable expansion, #4249\n  - FAQ regarding change of compression settings, #4222\n  - add note about BSD flags to changelog, #4246\n  - improve logging in example automation script\n  - add note about files changing during backup, #4081\n  - work around the backslash issue, #4280\n  - update release workflow using twine (docs, scripts), #4213\n  - add warnings on repository copies to avoid future problems, #4272\n- tests:\n\n  - fix the homebrew 1.9 issues on travis-ci, #4254\n  - fix duplicate test method name, #4311\n\n\nVersion 1.1.8 (2018-12-09)\n--------------------------\n\nFixes:\n\n- enforce storage quota if set by serve-command, #4093\n- invalid locations: give err msg containing parsed location, #4179\n- list repo: add placeholders for hostname and username, #4130\n- on linux, symlinks can't have ACLs, so don't try to set any, #4044\n\nNew features:\n\n- create: added PATH::archive output on INFO log level\n- read a passphrase from a file descriptor specified in the\n  BORG_PASSPHRASE_FD environment variable.\n\nOther:\n\n- docs:\n\n  - option --format is required for some expensive-to-compute values for json\n\n    borg list by default does not compute expensive values except when\n    they are needed. whether they are needed is determined by the format,\n    in standard mode as well as in --json mode.\n  - tell that our binaries are x86/x64 amd/intel, bauerj has ARM\n  - fixed wrong archive name pattern in CRUD benchmark help\n  - fixed link to cachedir spec in docs, #4140\n- tests:\n\n  - stop using fakeroot on travis, avoids sporadic EISDIR errors, #2482\n  - xattr key names must start with \"user.\" on linux\n  - fix code so flake8 3.6 does not complain\n  - explicitly convert environment variable to str, #4136\n  - fix DeprecationWarning: Flags not at the start of the expression, #4137\n  - support pytest4, #4172\n- vagrant:\n\n  - use python 3.5.6 for builds\n\n\nVersion 1.1.7 (2018-08-11)\n--------------------------\n\nCompatibility notes:\n\n- added support for Python 3.7\n\nFixes:\n\n- cache lock: use lock_wait everywhere to fix infinite wait, see #3968\n- don't archive tagged dir when recursing an excluded dir, #3991\n- py37 argparse: work around bad default in py 3.7.0a/b/rc, #3996\n- py37 remove loggerDict.clear() from tearDown method, #3805\n- some fixes for bugs which likely did not result in problems in practice:\n\n  - fixed logic bug in platform module API version check\n  - fixed xattr/acl function prototypes, added missing ones\n\nNew features:\n\n- init: add warning to store both key and passphrase at safe place(s)\n- BORG_HOST_ID env var to work around all-zero MAC address issue, #3985\n- borg debug dump-repo-objs --ghost (dump everything from segment files,\n  including deleted or superseded objects or commit tags)\n- borg debug search-repo-objs (search in repo objects for hex bytes or strings)\n\nOther changes:\n\n- add Python 3.7 support\n- updated shell completions\n- call socket.gethostname only once\n- locking: better logging, add some asserts\n- borg debug dump-repo-objs:\n\n  - filename layout improvements\n  - use repository.scan() to get on-disk order\n- docs:\n\n  - update installation instructions for macOS\n  - added instructions to install fuse via homebrew\n  - improve diff docs\n  - added note that checkpoints inside files requires 1.1+\n  - add link to tempfile module\n  - remove row/column-spanning from docs source, #4000 #3990\n- tests:\n\n  - fetch less data via os.urandom\n  - add py37 env for tox\n  - travis: add 3.7, remove 3.6-dev (we test with -dev in master)\n- vagrant / binary builds:\n\n  - use osxfuse 3.8.2\n  - use own (uptodate) openindiana box\n\n\nVersion 1.1.6 (2018-06-11)\n--------------------------\n\nCompatibility notes:\n\n- 1.1.6 changes:\n\n  - also allow msgpack-python 0.5.6.\n\nFixes:\n\n- fix borg exception handling on ENOSPC error with xattrs, #3808\n- prune: fix/improve overall progress display\n- borg config repo ... does not need cache/manifest/key, #3802\n- debug dump-repo-objs should not depend on a manifest obj\n- pypi package:\n\n  - include .coveragerc, needed by tox.ini\n  - fix package long description, #3854\n\nNew features:\n\n- mount: add uid, gid, umask mount options\n- delete:\n\n  - only commit once, #3823\n  - implement --dry-run, #3822\n- check:\n\n  - show progress while rebuilding missing manifest, #3787\n  - more --repair output\n- borg config --list <repo>, #3612\n\nOther changes:\n\n- update msgpack requirement, #3753\n- update bundled zstd to 1.3.4, #3745\n- update bundled lz4 code to 1.8.2, #3870\n- docs:\n\n  - describe what BORG_LIBZSTD_PREFIX does\n  - fix and deduplicate encryption quickstart docs, #3776\n- vagrant:\n\n  - FUSE for macOS: upgrade 3.7.1 to 3.8.0\n  - exclude macOS High Sierra upgrade on the darwin64 machine\n  - remove borgbackup.egg-info dir in fs_init (after rsync)\n  - use pyenv-based build/test on jessie32/62\n  - use local 32 and 64bit debian jessie boxes\n  - use \"vagrant\" as username for new xenial box\n- travis OS X: use xcode 8.3 (not broken)\n\n\nVersion 1.1.5 (2018-04-01)\n--------------------------\n\nCompatibility notes:\n\n- 1.1.5 changes:\n\n  - require msgpack-python >= 0.4.6 and < 0.5.0.\n    0.5.0+ dropped python 3.4 testing and also caused some other issues because\n    the python package was renamed to msgpack and emitted some FutureWarning.\n\nFixes:\n\n- create --list: fix that it was never showing M status, #3492\n- create: fix timing for first checkpoint (read files cache early, init\n  checkpoint timer after that), see #3394\n- extract: set rc=1 when extracting damaged files with all-zero replacement\n  chunks or with size inconsistencies, #3448\n- diff: consider an empty file as different to a non-existing file, #3688\n- files cache: improve exception handling, #3553\n- ignore exceptions in scandir_inorder() caused by an implicit stat(),\n  also remove unneeded sort, #3545\n- fixed tab completion problem where a space is always added after path even\n  when it shouldn't\n- build: do .h file content checks in binary mode, fixes build issue for\n  non-ascii header files on pure-ascii locale platforms, #3544 #3639\n- borgfs: fix patterns/paths processing, #3551\n- config: add some validation, #3566\n- repository config: add validation for max_segment_size, #3592\n- set cache previous_location on load instead of save\n- remove platform.uname() call which caused library mismatch issues, #3732\n- add exception handler around deprecated platform.linux_distribution() call\n- use same datetime object for {now} and {utcnow}, #3548\n\nNew features:\n\n- create: implement --stdin-name, #3533\n- add chunker_params to borg archive info (--json)\n- BORG_SHOW_SYSINFO=no to hide system information from exceptions\n\nOther changes:\n\n- updated zsh completions for borg 1.1.4\n- files cache related code cleanups\n- be more helpful when parsing invalid --pattern values, #3575\n- be more clear in secure-erase warning message, #3591\n- improve getpass user experience, #3689\n- docs build: unicode problem fixed when using a py27-based sphinx\n- docs:\n\n  - security: explicitly note what happens OUTSIDE the attack model\n  - security: add note about combining compression and encryption\n  - security: describe chunk size / proximity issue, #3687\n  - quickstart: add note about permissions, borg@localhost, #3452\n  - quickstart: add introduction to repositories & archives, #3620\n  - recreate --recompress: add missing metavar, clarify description, #3617\n  - improve logging docs, #3549\n  - add an example for --pattern usage, #3661\n  - clarify path semantics when matching, #3598\n  - link to offline documentation from README, #3502\n  - add docs on how to verify a signed release with GPG, #3634\n  - chunk seed is generated per repository (not: archive)\n  - better formatting of CPU usage documentation, #3554\n  - extend append-only repo rollback docs, #3579\n- tests:\n\n  - fix erroneously skipped zstd compressor tests, #3606\n  - skip a test if argparse is broken, #3705\n- vagrant:\n\n  - xenial64 box now uses username 'vagrant', #3707\n  - move cleanup steps to fs_init, #3706\n  - the boxcutter wheezy boxes are 404, use local ones\n  - update to Python 3.5.5 (for binary builds)\n\n\nVersion 1.1.4 (2017-12-31)\n--------------------------\n\nCompatibility notes:\n\n- When upgrading from borg 1.0.x to 1.1.x, please note:\n\n  - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1.\n  - borg upgrade: you do not need to and you also should not run it.\n  - borg might ask some security-related questions once after upgrading.\n    You can answer them either manually or via environment variable.\n    One known case is if you use unencrypted repositories, then it will ask\n    about a unknown unencrypted repository one time.\n  - your first backup with 1.1.x might be significantly slower (it might\n    completely read, chunk, hash a lot files) - this is due to the\n    --files-cache mode change (and happens every time you change mode).\n    You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible\n    mode (but that is less safe for detecting changed files than the default).\n    See the --files-cache docs for details.\n- borg 1.1.4 changes:\n\n  - zstd compression is new in borg 1.1.4, older borg can't handle it.\n  - new minimum requirements for the compression libraries - if the required\n    versions (header and lib) can't be found at build time, bundled code will\n    be used:\n\n    - added requirement: libzstd >= 1.3.0 (bundled: 1.3.2)\n    - updated requirement: liblz4 >= 1.7.0 / r129 (bundled: 1.8.0)\n\nFixes:\n\n- check: data corruption fix: fix for borg check --repair malfunction, #3444.\n  See the more detailed notes close to the top of this document.\n- delete: also delete security dir when deleting a repo, #3427\n- prune: fix building the \"borg prune\" man page, #3398\n- init: use given --storage-quota for local repo, #3470\n- init: properly quote repo path in output\n- fix startup delay with dns-only own fqdn resolving, #3471\n\nNew features:\n\n- added zstd compression. try it!\n- added placeholder {reverse-fqdn} for fqdn in reverse notation\n- added BORG_BASE_DIR environment variable, #3338\n\nOther changes:\n\n- list help topics when invalid topic is requested\n- fix lz4 deprecation warning, requires lz4 >= 1.7.0 (r129)\n- add parens for C preprocessor macro argument usages (did not cause malfunction)\n- exclude broken pytest 3.3.0 release\n- updated fish/bash completions\n- init: more clear exception messages for borg create, #3465\n- docs:\n\n  - add auto-generated docs for borg config\n  - don't generate HTML docs page for borgfs, #3404\n  - docs update for lz4 b2 zstd changes\n  - add zstd to compression help, readme, docs\n  - update requirements and install docs about bundled lz4 and zstd\n- refactored build of the compress and crypto.low_level extensions, #3415:\n\n  - move some lib/build related code to setup_{zstd,lz4,b2}.py\n  - bundle lz4 1.8.0 (requirement: >= 1.7.0 / r129)\n  - bundle zstd 1.3.2 (requirement: >= 1.3.0)\n  - blake2 was already bundled\n  - rename BORG_LZ4_PREFIX env var to BORG_LIBLZ4_PREFIX for better consistency:\n    we also have BORG_LIBB2_PREFIX and BORG_LIBZSTD_PREFIX now.\n  - add prefer_system_lib* = True settings to setup.py - by default the build\n    will prefer a shared library over the bundled code, if library and headers\n    can be found and meet the minimum requirements.\n\n\nVersion 1.1.3 (2017-11-27)\n--------------------------\n\nFixes:\n\n- Security Fix for CVE-2017-15914: Incorrect implementation of access controls\n  allows remote users to override repository restrictions in Borg servers.\n  A user able to access a remote Borg SSH server is able to circumvent access\n  controls post-authentication.\n  Affected releases: 1.1.0, 1.1.1, 1.1.2. Releases 1.0.x are NOT affected.\n- crc32: deal with unaligned buffer, add tests - this broke borg on older ARM\n  CPUs that can not deal with unaligned 32bit memory accesses and raise a bus\n  error in such cases. the fix might also improve performance on some CPUs as\n  all 32bit memory accesses by the crc32 code are properly aligned now. #3317\n- mount: fixed support of --consider-part-files and do not show .borg_part_N\n  files by default in the mounted FUSE filesystem. #3347\n- fixed cache/repo timestamp inconsistency message, highlight that information\n  is obtained from security dir (deleting the cache will not bypass this error\n  in case the user knows this is a legitimate repo).\n- borgfs: don't show sub-command in borgfs help, #3287\n- create: show an error when --dry-run and --stats are used together, #3298\n\nNew features:\n\n- mount: added exclusion group options and paths, #2138\n\n  Reused some code to support similar options/paths as borg extract offers -\n  making good use of these to mount only a smaller subset of dirs/files can\n  speed up mounting a lot and also will consume way less memory.\n\n  borg mount [options] repo_or_archive mountpoint path [paths...]\n\n  paths: you can just give some \"root paths\" (like for borg extract) to\n  only partially populate the FUSE filesystem.\n\n  new options: --exclude[-from], --pattern[s-from], --strip-components\n- create/extract: support st_birthtime on platforms supporting it, #3272\n- add \"borg config\" command for querying/setting/deleting config values, #3304\n\nOther changes:\n\n- clean up and simplify packaging (only package committed files, do not install\n  .c/.h/.pyx files)\n- docs:\n\n  - point out tuning options for borg create, #3239\n  - add instructions for using ntfsclone, zerofree, #81\n  - move image backup-related FAQ entries to a new page\n  - clarify key aliases for borg list --format, #3111\n  - mention break-lock in checkpointing FAQ entry, #3328\n  - document sshfs rename workaround, #3315\n  - add FAQ about removing files from existing archives\n  - add FAQ about different prune policies\n  - usage and man page for borgfs, #3216\n  - clarify create --stats duration vs. wall time, #3301\n  - clarify encrypted key format for borg key export, #3296\n  - update release checklist about security fixes\n  - document good and problematic option placements, fix examples, #3356\n  - add note about using --nobsdflags to avoid speed penalty related to\n    bsdflags, #3239\n  - move most of support section to www.borgbackup.org\n\n\nVersion 1.1.2 (2017-11-05)\n--------------------------\n\nFixes:\n\n- fix KeyError crash when talking to borg server < 1.0.7, #3244\n- extract: set bsdflags last (include immutable flag), #3263\n- create: don't do stat() call on excluded-norecurse directory, fix exception\n  handling for stat() call, #3209\n- create --stats: do not count data volume twice when checkpointing, #3224\n- recreate: move chunks_healthy when excluding hardlink master, #3228\n- recreate: get rid of chunks_healthy when rechunking (does not match), #3218\n- check: get rid of already existing not matching chunks_healthy metadata, #3218\n- list: fix stdout broken pipe handling, #3245\n- list/diff: remove tag-file options (not used), #3226\n\nNew features:\n\n- bash, zsh and fish shell auto-completions, see scripts/shell_completions/\n- added BORG_CONFIG_DIR env var, #3083\n\nOther changes:\n\n- docs:\n\n  - clarify using a blank passphrase in keyfile mode\n  - mention \"!\" (exclude-norecurse) type in \"patterns\" help\n  - document to first heal before running borg recreate to re-chunk stuff,\n    because that will have to get rid of chunks_healthy metadata.\n  - more than 23 is not supported for CHUNK_MAX_EXP, #3115\n  - borg does not respect nodump flag by default any more\n  - clarify same-filesystem requirement for borg upgrade, #2083\n  - update / rephrase cygwin / WSL status, #3174\n  - improve docs about --stats, #3260\n- vagrant: openindiana new clang package\n\nAlready contained in 1.1.1 (last minute fix):\n\n- arg parsing: fix fallback function, refactor, #3205. This is a fixup\n  for #3155, which was broken on at least python <= 3.4.2.\n\n\nVersion 1.1.1 (2017-10-22)\n--------------------------\n\nCompatibility notes:\n\n- The deprecated --no-files-cache is not a global/common option any more,\n  but only available for borg create (it is not needed for anything else).\n  Use --files-cache=disabled instead of --no-files-cache.\n- The nodump flag (\"do not back up this file\") is not honoured any more by\n  default because this functionality (esp. if it happened by error or\n  unexpected) was rather confusing and unexplainable at first to users.\n  If you want that \"do not back up NODUMP-flagged files\" behaviour, use:\n  borg create --exclude-nodump ...\n- If you are on Linux and do not need bsdflags archived, consider using\n  ``--nobsdflags`` with ``borg create`` to avoid additional syscalls and\n  speed up backup creation.\n\nFixes:\n\n- borg recreate: correctly compute part file sizes. fixes cosmetic, but\n  annoying issue as borg check complains about size inconsistencies of part\n  files in affected archives. you can solve that by running borg recreate on\n  these archives, see also #3157.\n- bsdflags support: do not open BLK/CHR/LNK files, avoid crashes and\n  slowness, #3130\n- recreate: don't crash on attic archives w/o time_end, #3109\n- don't crash on repository filesystems w/o hardlink support, #3107\n- don't crash in first part of truncate_and_unlink, #3117\n- fix server-side IndexError crash with clients < 1.0.7, #3192\n- don't show traceback if only a global option is given, show help, #3142\n- cache: use SaveFile for more safety, #3158\n- init: fix wrong encryption choices in command line parser, fix missing\n  \"authenticated-blake2\", #3103\n- move --no-files-cache from common to borg create options, #3146\n- fix detection of non-local path (failed on ..filename), #3108\n- logging with fileConfig: set json attr on \"borg\" logger, #3114\n- fix crash with relative BORG_KEY_FILE, #3197\n- show excluded dir with \"x\" for tagged dirs / caches, #3189\n\nNew features:\n\n- create: --nobsdflags and --exclude-nodump options, #3160\n- extract: --nobsdflags option, #3160\n\nOther changes:\n\n- remove annoying hardlinked symlinks warning, #3175\n- vagrant: use self-made FreeBSD 10.3 box, #3022\n- travis: don't brew update, hopefully fixes #2532\n- docs:\n\n  - readme: -e option is required in borg 1.1\n  - add example showing --show-version --show-rc\n  - use --format rather than --list-format (deprecated) in example\n  - update docs about hardlinked symlinks limitation\n\n\nVersion 1.1.0 (2017-10-07)\n--------------------------\n\nCompatibility notes:\n\n- borg command line: do not put options in between positional arguments\n\n  This sometimes works (e.g. it worked in borg 1.0.x), but can easily stop\n  working if we make positional arguments optional (like it happened for\n  borg create's \"paths\" argument in 1.1). There are also places in borg 1.0\n  where we do that, so it doesn't work there in general either. #3356\n\n  Good: borg create -v --stats repo::archive path\n  Good: borg create repo::archive path -v --stats\n  Bad:  borg create repo::archive -v --stats path\n\nFixes:\n\n- fix LD_LIBRARY_PATH restoration for subprocesses, #3077\n- \"auto\" compression: make sure expensive compression is actually better,\n  otherwise store lz4 compressed data we already computed.\n\nOther changes:\n\n- docs:\n\n  - FAQ: we do not implement futile attempts of ETA / progress displays\n  - manpage: fix typos, update homepage\n  - implement simple \"issue\" role for manpage generation, #3075\n\n\nVersion 1.1.0rc4 (2017-10-01)\n-----------------------------\n\nCompatibility notes:\n\n- A borg server >= 1.1.0rc4 does not support borg clients 1.1.0b3-b5. #3033\n- The files cache is now controlled differently and has a new default mode:\n\n  - the files cache now uses ctime by default for improved file change\n    detection safety. You can still use mtime for more speed and less safety.\n  - --ignore-inode is deprecated (use --files-cache=... without \"inode\")\n  - --no-files-cache is deprecated (use --files-cache=disabled)\n\nNew features:\n\n- --files-cache - implement files cache mode control, #911\n  You can now control the files cache mode using this option:\n  --files-cache={ctime,mtime,size,inode,rechunk,disabled}\n  (only some combinations are supported). See the docs for details.\n\nFixes:\n\n- remote progress/logging: deal with partial lines, #2637\n- remote progress: flush json mode output\n- fix subprocess environments, #3050 (and more)\n\nOther changes:\n\n- remove client_supports_log_v3 flag, #3033\n- exclude broken Cython 0.27(.0) in requirements, #3066\n- vagrant:\n\n  - upgrade to FUSE for macOS 3.7.1\n  - use Python 3.5.4 to build the binaries\n- docs:\n\n  - security: change-passphrase only changes the passphrase, #2990\n  - fixed/improved borg create --compression examples, #3034\n  - add note about metadata dedup and --no[ac]time, #2518\n  - twitter account @borgbackup now, better visible, #2948\n  - simplified rate limiting wrapper in FAQ\n\n\nVersion 1.1.0rc3 (2017-09-10)\n-----------------------------\n\nNew features:\n\n- delete: support naming multiple archives, #2958\n\nFixes:\n\n- repo cleanup/write: invalidate cached FDs, #2982\n- fix datetime.isoformat() microseconds issues, #2994\n- recover_segment: use mmap(), lower memory needs, #2987\n\nOther changes:\n\n- with-lock: close segment file before invoking subprocess\n- keymanager: don't depend on optional readline module, #2976\n- docs:\n\n  - fix macOS keychain integration command\n  - show/link new screencasts in README, #2936\n  - document utf-8 locale requirement for json mode, #2273\n- vagrant: clean up shell profile init, user name, #2977\n- test_detect_attic_repo: don't test mount, #2975\n- add debug logging for repository cleanup\n\n\nVersion 1.1.0rc2 (2017-08-28)\n-----------------------------\n\nCompatibility notes:\n\n- list: corrected mix-up of \"isomtime\" and \"mtime\" formats. Previously,\n  \"isomtime\" was the default but produced a verbose human format,\n  while \"mtime\" produced a ISO-8601-like format.\n  The behaviours have been swapped (so \"mtime\" is human, \"isomtime\" is ISO-like),\n  and the default is now \"mtime\".\n  \"isomtime\" is now a real ISO-8601 format (\"T\" between date and time, not a space).\n\nNew features:\n\n- None.\n\nFixes:\n\n- list: fix weird mixup of mtime/isomtime\n- create --timestamp: set start time, #2957\n- ignore corrupt files cache, #2939\n- migrate locks to child PID when daemonize is used\n- fix exitcode of borg serve, #2910\n- only compare contents when chunker params match, #2899\n- umount: try fusermount, then try umount, #2863\n\nOther changes:\n\n- JSON: use a more standard ISO 8601 datetime format, #2376\n- cache: write_archive_index: truncate_and_unlink on error, #2628\n- detect non-upgraded Attic repositories, #1933\n- delete various nogil and threading related lines\n- coala / pylint related improvements\n- docs:\n\n  - renew asciinema/screencasts, #669\n  - create: document exclusion through nodump, #2949\n  - minor formatting fixes\n  - tar: tarpipe example\n  - improve \"with-lock\" and \"info\" docs, #2869\n  - detail how to use macOS/GNOME/KDE keyrings for repo passwords, #392\n- travis: only short-circuit docs-only changes for pull requests\n- vagrant:\n\n  - netbsd: bash is already installed\n  - fix netbsd version in PKG_PATH\n  - add exe location to PATH when we build an exe\n\n\nVersion 1.1.0rc1 (2017-07-24)\n-----------------------------\n\nCompatibility notes:\n\n- delete: removed short option for --cache-only\n\nNew features:\n\n- support borg list repo --format {comment} {bcomment} {end}, #2081\n- key import: allow reading from stdin, #2760\n\nFixes:\n\n- with-lock: avoid creating segment files that might be overwritten later, #1867\n- prune: fix checkpoints processing with --glob-archives\n- FUSE: versions view: keep original file extension at end, #2769\n- fix --last, --first: do not accept values <= 0,\n  fix reversed archive ordering with --last\n- include testsuite data (attic.tar.gz) when installing the package\n- use limited unpacker for outer key, for manifest (both security precautions),\n  #2174 #2175\n- fix bashism in shell scripts, #2820, #2816\n- cleanup endianness detection, create _endian.h,\n  fixes build on alpine linux, #2809\n- fix crash with --no-cache-sync (give known chunk size to chunk_incref), #2853\n\nOther changes:\n\n- FUSE: versions view: linear numbering by archive time\n- split up interval parsing from filtering for --keep-within, #2610\n- add a basic .editorconfig, #2734\n- use archive creation time as mtime for FUSE mount, #2834\n- upgrade FUSE for macOS (osxfuse) from 3.5.8 to 3.6.3, #2706\n- hashindex: speed up by replacing modulo with \"if\" to check for wraparound\n- coala checker / pylint: fixed requirements and .coafile, more ignores\n- borg upgrade: name backup directories as 'before-upgrade', #2811\n- add .mailmap\n- some minor changes suggested by lgtm.com\n- docs:\n\n  - better explanation of the --ignore-inode option relevance, #2800\n  - fix openSUSE command and add openSUSE section\n  - simplify ssh authorized_keys file using \"restrict\", add legacy note, #2121\n  - mount: show usage of archive filters\n  - mount: add repository example, #2462\n  - info: update and add examples, #2765\n  - prune: include example\n  - improved style / formatting\n  - improved/fixed segments_per_dir docs\n  - recreate: fix wrong \"remove unwanted files\" example\n  - reference list of status chars in borg recreate --filter description\n  - update source-install docs about doc build dependencies, #2795\n  - cleanup installation docs\n  - file system requirements, update segs per dir\n  - fix checkpoints/parts reference in FAQ, #2859\n- code:\n\n  - hashindex: don't pass side effect into macro\n  - crypto low_level: don't mutate local bytes()\n  - use dash_open function to open file or \"-\" for stdin/stdout\n  - archiver: argparse cleanup / refactoring\n  - shellpattern: add match_end arg\n- tests: added some additional unit tests, some fixes, #2700 #2710\n- vagrant: fix setup of cygwin, add Debian 9 \"stretch\"\n- travis: don't perform full travis build on docs-only changes, #2531\n\n\nVersion 1.1.0b6 (2017-06-18)\n----------------------------\n\nCompatibility notes:\n\n- Running \"borg init\" via a \"borg serve --append-only\" server will *not* create\n  an append-only repository anymore. Use \"borg init --append-only\" to initialize\n  an append-only repository.\n\n- Repositories in the \"repokey\" and \"repokey-blake2\" modes with an empty passphrase\n  are now treated as unencrypted repositories for security checks (e.g.\n  BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).\n\n  Previously there would be no prompts nor messages if an unknown repository\n  in one of these modes with an empty passphrase was encountered. This would\n  allow an attacker to swap a repository, if one assumed that the lack of\n  password prompts was due to a set BORG_PASSPHRASE.\n\n  Since the \"trick\" does not work if BORG_PASSPHRASE is set, this does generally\n  not affect scripts.\n\n- Repositories in the \"authenticated\" mode are now treated as the unencrypted\n  repositories they are.\n\n- The client-side temporary repository cache now holds unencrypted data for better speed.\n\n- borg init: removed the short form of --append-only (-a).\n\n- borg upgrade: removed the short form of --inplace (-i).\n\nNew features:\n\n- reimplemented the RepositoryCache, size-limited caching of decrypted repo\n  contents, integrity checked via xxh64. #2515\n- reduced space usage of chunks.archive.d. Existing caches are migrated during\n  a cache sync. #235 #2638\n- integrity checking using xxh64 for important files used by borg, #1101:\n\n  - repository: index and hints files\n  - cache: chunks and files caches, chunks.archive.d\n- improve cache sync speed, #1729\n- create: new --no-cache-sync option\n- add repository mandatory feature flags infrastructure, #1806\n- Verify most operations against SecurityManager. Location, manifest timestamp\n  and key types are now checked for almost all non-debug commands. #2487\n- implement storage quotas, #2517\n- serve: add --restrict-to-repository, #2589\n- BORG_PASSCOMMAND: use external tool providing the key passphrase, #2573\n- borg export-tar, #2519\n- list: --json-lines instead of --json for archive contents, #2439\n- add --debug-profile option (and also \"borg debug convert-profile\"), #2473\n- implement --glob-archives/-a, #2448\n- normalize authenticated key modes for better naming consistency:\n\n  - rename \"authenticated\" to \"authenticated-blake2\" (uses blake2b)\n  - implement \"authenticated\" mode (uses hmac-sha256)\n\nFixes:\n\n- hashindex: read/write indices >2 GiB on 32bit systems, better error\n  reporting, #2496\n- repository URLs: implement IPv6 address support and also more informative\n  error message when parsing fails.\n- mount: check whether llfuse is installed before asking for passphrase, #2540\n- mount: do pre-mount checks before opening repository, #2541\n- FUSE:\n\n  - fix crash if empty (None) xattr is read, #2534\n  - fix read(2) caching data in metadata cache\n  - fix negative uid/gid crash (fix crash when mounting archives\n    of external drives made on cygwin), #2674\n  - redo ItemCache, on top of object cache\n  - use decrypted cache\n  - remove unnecessary normpaths\n- serve: ignore --append-only when initializing a repository (borg init), #2501\n- serve: fix incorrect type of exception_short for Errors, #2513\n- fix --exclude and --exclude-from recursing into directories, #2469\n- init: don't allow creating nested repositories, #2563\n- --json: fix encryption[mode] not being the cmdline name\n- remote: propagate Error.traceback correctly\n- fix remote logging and progress, #2241\n\n  - implement --debug-topic for remote servers\n  - remote: restore \"Remote:\" prefix (as used in 1.0.x)\n  - rpc negotiate: enable v3 log protocol only for supported clients\n  - fix --progress and logging in general for remote\n- fix parse_version, add tests, #2556\n- repository: truncate segments (and also some other files) before unlinking, #2557\n- recreate: keep timestamps as in original archive, #2384\n- recreate: if single archive is not processed, exit 2\n- patterns: don't recurse with ! / --exclude for pf:, #2509\n- cache sync: fix n^2 behaviour in lookup_name\n- extract: don't write to disk with --stdout (affected non-regular-file items), #2645\n- hashindex: implement KeyError, more tests\n\nOther changes:\n\n- remote: show path in PathNotAllowed\n- consider repokey w/o passphrase == unencrypted, #2169\n- consider authenticated mode == unencrypted, #2503\n- restrict key file names, #2560\n- document follow_symlinks requirements, check libc, use stat and chown\n  with follow_symlinks=False, #2507\n- support common options on the main command, #2508\n- support common options on mid-level commands (e.g. borg *key* export)\n- make --progress a common option\n- increase DEFAULT_SEGMENTS_PER_DIR to 1000\n- chunker: fix invalid use of types (function only used by tests)\n- chunker: don't do uint32_t >> 32\n- FUSE:\n\n  - add instrumentation (--debug and SIGUSR1/SIGINFO)\n  - reduced memory usage for repository mounts by lazily instantiating archives\n  - improved archive load times\n- info: use CacheSynchronizer & HashIndex.stats_against (better performance)\n- docs:\n\n  - init: document --encryption as required\n  - security: OpenSSL usage\n  - security: used implementations; note python libraries\n  - security: security track record of OpenSSL and msgpack\n  - patterns: document denial of service (regex, wildcards)\n  - init: note possible denial of service with \"none\" mode\n  - init: document SHA extension is supported in OpenSSL and thus SHA is\n    faster on AMD Ryzen than blake2b.\n  - book: use A4 format, new builder option format.\n  - book: create appendices\n  - data structures: explain repository compaction\n  - data structures: add chunk layout diagram\n  - data structures: integrity checking\n  - data structures: demingle cache and repo index\n  - Attic FAQ: separate section for attic stuff\n  - FAQ: I get an IntegrityError or similar - what now?\n  - FAQ: Can I use Borg on SMR hard drives?, #2252\n  - FAQ: specify \"using inline shell scripts\"\n  - add systemd warning regarding placeholders, #2543\n  - xattr: document API\n  - add docs/misc/borg-data-flow data flow chart\n  - debugging facilities\n  - README: how to help the project, #2550\n  - README: add bountysource badge, #2558\n  - fresh new theme + tweaking\n  - logo: vectorized (PDF and SVG) versions\n  - frontends: use headlines - you can link to them\n  - mark --pattern, --patterns-from as experimental\n  - highlight experimental features in online docs\n  - remove regex based pattern examples, #2458\n  - nanorst for \"borg help TOPIC\" and --help\n  - split deployment\n  - deployment: hosting repositories\n  - deployment: automated backups to a local hard drive\n  - development: vagrant, windows10 requirements\n  - development: update docs remarks\n  - split usage docs, #2627\n  - usage: avoid bash highlight, [options] instead of <options>\n  - usage: add benchmark page\n  - helpers: truncate_and_unlink doc\n  - don't suggest to leak BORG_PASSPHRASE\n  - internals: columnize rather long ToC [webkit fixup]\n    internals: manifest & feature flags\n  - internals: more HashIndex details\n  - internals: fix ASCII art equations\n  - internals: edited obj graph related sections a bit\n  - internals: layers image + description\n  - fix way too small figures in pdf\n  - index: disable syntax highlight (bash)\n  - improve options formatting, fix accidental block quotes\n\n- testing / checking:\n\n  - add support for using coala, #1366\n  - testsuite: add ArchiverCorruptionTestCase\n  - do not test logger name, #2504\n  - call setup_logging after destroying logging config\n  - testsuite.archiver: normalise pytest.raises vs. assert_raises\n  - add test for preserved intermediate folder permissions, #2477\n  - key: add round-trip test\n  - remove attic dependency of the tests, #2505\n  - enable remote tests on cygwin\n  - tests: suppress tar's future timestamp warning\n  - cache sync: add more refcount tests\n  - repository: add tests, including corruption tests\n\n- vagrant:\n\n  - control VM cpus and pytest workers via env vars VMCPUS and XDISTN\n  - update cleaning workdir\n  - fix openbsd shell\n  - add OpenIndiana\n\n- packaging:\n\n  - binaries: don't bundle libssl\n  - setup.py clean to remove compiled files\n  - fail in borg package if version metadata is very broken (setuptools_scm)\n\n- repo / code structure:\n\n  - create borg.algorithms and borg.crypto packages\n  - algorithms: rename crc32 to checksums\n  - move patterns to module, #2469\n  - gitignore: complete paths for src/ excludes\n  - cache: extract CacheConfig class\n  - implement IntegrityCheckedFile + Detached variant, #2502 #1688\n  - introduce popen_with_error_handling to handle common user errors\n\n\nVersion 1.1.0b5 (2017-04-30)\n----------------------------\n\nCompatibility notes:\n\n- BORG_HOSTNAME_IS_UNIQUE is now on by default.\n- removed --compression-from feature\n- recreate: add --recompress flag, unify --always-recompress and\n  --recompress\n\nFixes:\n\n- catch exception for os.link when hardlinks are not supported, #2405\n- borg rename / recreate: expand placeholders, #2386\n- generic support for hardlinks (files, devices, FIFOs), #2324\n- extract: also create parent dir for device files, if needed, #2358\n- extract: if a hardlink master is not in the to-be-extracted subset,\n  the \"x\" status was not displayed for it, #2351\n- embrace y2038 issue to support 32bit platforms: clamp timestamps to int32,\n  #2347\n- verify_data: fix IntegrityError handling for defect chunks, #2442\n- allow excluding parent and including child, #2314\n\nOther changes:\n\n- refactor compression decision stuff\n- change global compression default to lz4 as well, to be consistent\n  with --compression defaults.\n- placeholders: deny access to internals and other unspecified stuff\n- clearer error message for unrecognized placeholder\n- more clear exception if borg check does not help, #2427\n- vagrant: upgrade FUSE for macOS to 3.5.8, #2346\n- linux binary builds: get rid of glibc 2.13 dependency, #2430\n- docs:\n\n  - placeholders: document escaping\n  - serve: env vars in original commands are ignored\n  - tell what kind of hardlinks we support\n  - more docs about compression\n  - LICENSE: use canonical formulation\n    (\"copyright holders and contributors\" instead of \"author\")\n  - document borg init behaviour via append-only borg serve, #2440\n  - be clear about what buzhash is used for, #2390\n  - add hint about chunker params, #2421\n  - clarify borg upgrade docs, #2436\n  - FAQ to explain warning when running borg check --repair, #2341\n  - repository file system requirements, #2080\n  - pre-install considerations\n  - misc. formatting / crossref fixes\n- tests:\n\n  - enhance travis setuptools_scm situation\n  - add extra test for the hashindex\n  - fix invalid param issue in benchmarks\n\nThese belong to 1.1.0b4 release, but did not make it into changelog by then:\n\n- vagrant: increase memory for parallel testing\n- lz4 compress: lower max. buffer size, exception handling\n- add docstring to do_benchmark_crud\n- patterns help: mention path full-match in intro\n\n\nVersion 1.1.0b4 (2017-03-27)\n----------------------------\n\nCompatibility notes:\n\n- init: the --encryption argument is mandatory now (there are several choices)\n- moved \"borg migrate-to-repokey\" to \"borg key migrate-to-repokey\".\n- \"borg change-passphrase\" is deprecated, use \"borg key change-passphrase\"\n  instead.\n- the --exclude-if-present option now supports tagging a folder with any\n  filesystem object type (file, folder, etc), instead of expecting only files\n  as tags, #1999\n- the --keep-tag-files option has been deprecated in favor of the new\n  --keep-exclude-tags, to account for the change mentioned above.\n- use lz4 compression by default, #2179\n\nNew features:\n\n- JSON API to make developing frontends and automation easier\n  (see :ref:`json_output`)\n\n  - add JSON output to commands: `borg create/list/info --json ...`.\n  - add --log-json option for structured logging output.\n  - add JSON progress information, JSON support for confirmations (yes()).\n- add two new options --pattern and --patterns-from as discussed in #1406\n- new path full match pattern style (pf:) for very fast matching, #2334\n- add 'debug dump-manifest' and 'debug dump-archive' commands\n- add 'borg benchmark crud' command, #1788\n- new 'borg delete --force --force' to delete severely corrupted archives, #1975\n- info: show utilization of maximum archive size, #1452\n- list: add dsize and dcsize keys, #2164\n- paperkey.html: Add interactive html template for printing key backups.\n- key export: add qr html export mode\n- securely erase config file (which might have old encryption key), #2257\n- archived file items: add size to metadata, 'borg extract' and 'borg check' do\n  check the file size for consistency, FUSE uses precomputed size from Item.\n\nFixes:\n\n- fix remote speed regression introduced in 1.1.0b3, #2185\n- fix regression handling timestamps beyond 2262 (revert bigint removal),\n  introduced in 1.1.0b3, #2321\n- clamp (nano)second values to unproblematic range, #2304\n- hashindex: rebuild hashtable if we have too little empty buckets\n  (performance fix), #2246\n- Location regex: fix bad parsing of wrong syntax\n- ignore posix_fadvise errors in repository.py, #2095\n- borg rpc: use limited msgpack.Unpacker (security precaution), #2139\n- Manifest: Make sure manifest timestamp is strictly monotonically increasing.\n- create: handle BackupOSError on a per-path level in one spot\n- create: clarify -x option / meaning of \"same filesystem\"\n- create: don't create hard link refs to failed files\n- archive check: detect and fix missing all-zero replacement chunks, #2180\n- files cache: update inode number when --ignore-inode is used, #2226\n- fix decompression exceptions crashing ``check --verify-data`` and others\n  instead of reporting integrity error, #2224 #2221\n- extract: warning for unextracted big extended attributes, #2258, #2161\n- mount: umount on SIGINT/^C when in foreground\n- mount: handle invalid hard link refs\n- mount: fix huge RAM consumption when mounting a repository (saves number of\n  archives * 8 MiB), #2308\n- hashindex: detect mingw byte order #2073\n- hashindex: fix wrong skip_hint on hashindex_set when encountering tombstones,\n  the regression was introduced in #1748\n- fix ChunkIndex.__contains__ assertion  for big-endian archs\n- fix borg key/debug/benchmark crashing without subcommand, #2240\n- Location: accept //servername/share/path\n- correct/refactor calculation of unique/non-unique chunks\n- extract: fix missing call to ProgressIndicator.finish\n- prune: fix error msg, it is --keep-within, not --within\n- fix \"auto\" compression mode bug (not compressing), #2331\n- fix symlink item fs size computation, #2344\n\nOther changes:\n\n- remote repository: improved async exception processing, #2255 #2225\n- with --compression auto,C, only use C if lz4 achieves at least 3% compression\n- PatternMatcher: only normalize path once, #2338\n- hashindex: separate endian-dependent defs from endian detection\n- migrate-to-repokey: ask using canonical_path() as we do everywhere else.\n- SyncFile: fix use of fd object after close\n- make LoggedIO.close_segment reentrant\n- creating a new segment: use \"xb\" mode, #2099\n- redo key_creator, key_factory, centralise key knowledge, #2272\n- add return code functions, #2199\n- list: only load cache if needed\n- list: files->items, clarifications\n- list: add \"name\" key for consistency with info cmd\n- ArchiveFormatter: add \"start\" key for compatibility with \"info\"\n- RemoteRepository: account rx/tx bytes\n- setup.py build_usage/build_man/build_api fixes\n- Manifest.in: simplify, exclude .so, .dll and .orig, #2066\n- FUSE: get rid of chunk accounting, st_blocks = ceil(size / blocksize).\n- tests:\n\n  - help python development by testing 3.6-dev\n  - test for borg delete --force\n- vagrant:\n\n  - freebsd: some fixes, #2067\n  - darwin64: use osxfuse 3.5.4 for tests / to build binaries\n  - darwin64: improve VM settings\n  - use python 3.5.3 to build binaries, #2078\n  - upgrade pyinstaller from 3.1.1+ to 3.2.1\n  - pyinstaller: use fixed AND freshly compiled bootloader, #2002\n  - pyinstaller: automatically builds bootloader if missing\n- docs:\n\n  - create really nice man pages\n  - faq: mention --remote-ratelimit in bandwidth limit question\n  - fix caskroom link, #2299\n  - docs/security: reiterate that RPC in Borg does no networking\n  - docs/security: counter tracking, #2266\n  - docs/development: update merge remarks\n  - address SSH batch mode in docs, #2202 #2270\n  - add warning about running build_usage on Python >3.4, #2123\n  - one link per distro in the installation page\n  - improve --exclude-if-present and --keep-exclude-tags, #2268\n  - improve automated backup script in doc, #2214\n  - improve remote-path description\n  - update docs for create -C default change (lz4)\n  - document relative path usage, #1868\n  - document snapshot usage, #2178\n  - corrected some stuff in internals+security\n  - internals: move toctree to after the introduction text\n  - clarify metadata kind, manifest ops\n  - key enc: correct / clarify some stuff, link to internals/security\n  - datas: enc: 1.1.x mas different MACs\n  - datas: enc: correct factual error -- no nonce involved there.\n  - make internals.rst an index page and edit it a bit\n  - add \"Cryptography in Borg\" and \"Remote RPC protocol security\" sections\n  - document BORG_HOSTNAME_IS_UNIQUE, #2087\n  - FAQ by categories as proposed by @anarcat in #1802\n  - FAQ: update Which file types, attributes, etc. are *not* preserved?\n  - development: new branching model for git repository\n  - development: define \"ours\" merge strategy for auto-generated files\n  - create: move --exclude note to main doc\n  - create: move item flags to main doc\n  - fix examples using borg init without -e/--encryption\n  - list: don't print key listings in fat (html + man)\n  - remove Python API docs (were very incomplete, build problems on RTFD)\n  - added FAQ section about backing up root partition\n\n\nVersion 1.1.0b3 (2017-01-15)\n----------------------------\n\nCompatibility notes:\n\n- borg init: removed the default of \"--encryption/-e\", #1979\n  This was done so users do a informed decision about -e mode.\n\nBug fixes:\n\n- borg recreate: don't rechunkify unless explicitly told so\n- borg info: fixed bug when called without arguments, #1914\n- borg init: fix free space check crashing if disk is full, #1821\n- borg debug delete/get obj: fix wrong reference to exception\n- fix processing of remote ~/ and ~user/ paths (regressed since 1.1.0b1), #1759\n- posix platform module: only build / import on non-win32 platforms, #2041\n\nNew features:\n\n- new CRC32 implementations that are much faster than the zlib one used previously, #1970\n- add blake2b key modes (use blake2b as MAC). This links against system libb2,\n  if possible, otherwise uses bundled code\n- automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var\n  to enable stale lock killing. If set, stale locks in both cache and\n  repository are deleted. #562 #1253\n- borg info <repo>: print general repo information, #1680\n- borg check --first / --last / --sort / --prefix, #1663\n- borg mount --first / --last / --sort / --prefix, #1542\n- implement \"health\" item formatter key, #1749\n- BORG_SECURITY_DIR to remember security related infos outside the cache.\n  Key type, location and manifest timestamp checks now survive cache\n  deletion. This also means that you can now delete your cache and avoid\n  previous warnings, since Borg can still tell it's safe.\n- implement BORG_NEW_PASSPHRASE, #1768\n\nOther changes:\n\n- borg recreate:\n\n  - remove special-cased --dry-run\n  - update --help\n  - remove bloat: interruption blah, autocommit blah, resuming blah\n  - re-use existing checkpoint functionality\n  - archiver tests: add check_cache tool - lints refcounts\n\n- fixed cache sync performance regression from 1.1.0b1 onwards, #1940\n- syncing the cache without chunks.archive.d\n  now avoids any merges and is thus faster, #1940\n- borg check --verify-data: faster due to linear on-disk-order scan\n- borg debug-xxx commands removed, we use \"debug xxx\" subcommands now, #1627\n- improve metadata handling speed\n- shortcut hashindex_set by having hashindex_lookup hint about address\n- improve / add progress displays, #1721\n- check for index vs. segment files object count mismatch\n- make RPC protocol more extensible: use named parameters.\n- RemoteRepository: misc. code cleanups / refactors\n- clarify cache/repository README file\n\n- docs:\n\n  - quickstart: add a comment about other (remote) filesystems\n  - quickstart: only give one possible ssh url syntax, all others are\n    documented in usage chapter.\n  - mention file://\n  - document repo URLs / archive location\n  - clarify borg diff help, #980\n  - deployment: synthesize alternative --restrict-to-path example\n  - improve cache / index docs, esp. files cache docs, #1825\n  - document using \"git merge 1.0-maint -s recursive -X rename-threshold=20%\"\n    for avoiding troubles when merging the 1.0-maint branch into master.\n\n- tests:\n\n  - FUSE tests: catch ENOTSUP on freebsd\n  - FUSE tests: test troublesome xattrs last\n  - fix byte range error in test, #1740\n  - use monkeypatch to set env vars, but only on pytest based tests.\n  - point XDG_*_HOME to temp dirs for tests, #1714\n  - remove all BORG_* env vars from the outer environment\n\n\nVersion 1.1.0b2 (2016-10-01)\n----------------------------\n\nBug fixes:\n\n- fix incorrect preservation of delete tags, leading to \"object count mismatch\"\n  on borg check, #1598. This only occurred with 1.1.0b1 (not with 1.0.x) and is\n  normally fixed by running another borg create/delete/prune.\n- fix broken --progress for double-cell paths (e.g. CJK), #1624\n- borg recreate: also catch SIGHUP\n- FUSE:\n\n  - fix hardlinks in versions view, #1599\n  - add parameter check to ItemCache.get to make potential failures more clear\n\nNew features:\n\n- Archiver, RemoteRepository: add --remote-ratelimit (send data)\n- borg help compression, #1582\n- borg check: delete chunks with integrity errors, #1575, so they can be\n  \"repaired\" immediately and maybe healed later.\n- archives filters concept (refactoring/unifying older code)\n\n  - covers --first/--last/--prefix/--sort-by options\n  - currently used for borg list/info/delete\n\nOther changes:\n\n- borg check --verify-data slightly tuned (use get_many())\n- change {utcnow} and {now} to ISO-8601 format (\"T\" date/time separator)\n- repo check: log transaction IDs, improve object count mismatch diagnostic\n- Vagrantfile: use TW's fresh-bootloader pyinstaller branch\n- fix module names in api.rst\n- hashindex: bump api_version\n\n\nVersion 1.1.0b1 (2016-08-28)\n----------------------------\n\nNew features:\n\n- new commands:\n\n  - borg recreate: re-create existing archives, #787 #686 #630 #70, also see\n    #757, #770.\n\n    - selectively remove files/dirs from old archives\n    - re-compress data\n    - re-chunkify data, e.g. to have upgraded Attic / Borg 0.xx archives\n      deduplicate with Borg 1.x archives or to experiment with chunker-params.\n  - borg diff: show differences between archives\n  - borg with-lock: execute a command with the repository locked, #990\n- borg create:\n\n  - Flexible compression with pattern matching on path/filename,\n    and LZ4 heuristic for deciding compressibility, #810, #1007\n  - visit files in inode order (better speed, esp. for large directories and rotating disks)\n  - in-file checkpoints, #1217\n  - increased default checkpoint interval to 30 minutes (was 5 minutes), #896\n  - added uuid archive format tag, #1151\n  - save mountpoint directories with --one-file-system, makes system restore easier, #1033\n  - Linux: added support for some BSD flags, #1050\n  - add 'x' status for excluded paths, #814\n\n    - also means files excluded via UF_NODUMP, #1080\n- borg check:\n\n  - will not produce the \"Checking segments\" output unless new --progress option is passed, #824.\n  - --verify-data to verify data cryptographically on the client, #975\n- borg list, #751, #1179\n\n  - removed {formatkeys}, see \"borg list --help\"\n  - --list-format is deprecated, use --format instead\n  - --format now also applies to listing archives, not only archive contents, #1179\n  - now supports the usual [PATH [PATHS…]] syntax and excludes\n  - new keys: csize, num_chunks, unique_chunks, NUL\n  - supports guaranteed_available hashlib hashes\n    (to avoid varying functionality depending on environment),\n    which includes the SHA1 and SHA2 family as well as MD5\n- borg prune:\n\n  - to visualize the \"thinning out\" better, we now list all archives in\n    reverse time order. rephrase and reorder help text.\n  - implement --keep-last N via --keep-secondly N, also --keep-minutely.\n    assuming that there is not more than 1 backup archive made in 1s,\n    --keep-last N and --keep-secondly N are equivalent, #537\n  - cleanup checkpoints except the latest, #1008\n- borg extract:\n\n  - added --progress, #1449\n  - Linux: limited support for BSD flags, #1050\n- borg info:\n\n  - output is now more similar to borg create --stats, #977\n- borg mount:\n\n  - provide \"borgfs\" wrapper for borg mount, enables usage via fstab, #743\n  - \"versions\" mount option - when used with a repository mount, this gives\n    a merged, versioned view of the files in all archives, #729\n- repository:\n\n  - added progress information to commit/compaction phase (often takes some time when deleting/pruning), #1519\n  - automatic recovery for some forms of repository inconsistency, #858\n  - check free space before going forward with a commit, #1336\n  - improved write performance (esp. for rotating media), #985\n\n    - new IO code for Linux\n    - raised default segment size to approx 512 MiB\n  - improved compaction performance, #1041\n  - reduced client CPU load and improved performance for remote repositories, #940\n\n- options that imply output (--show-rc, --show-version, --list, --stats,\n  --progress) don't need -v/--info to have that output displayed, #865\n- add archive comments (via borg (re)create --comment), #842\n- borg list/prune/delete: also output archive id, #731\n- --show-version: shows/logs the borg version, #725\n- added --debug-topic for granular debug logging, #1447\n- use atomic file writing/updating for configuration and key files, #1377\n- BORG_KEY_FILE environment variable, #1001\n- self-testing module, #970\n\n\nBug fixes:\n\n- list: fixed default output being produced if --format is given with empty parameter, #1489\n- create: fixed overflowing progress line with CJK and similar characters, #1051\n- prune: fixed crash if --prefix resulted in no matches, #1029\n- init: clean up partial repo if passphrase input is aborted, #850\n- info: quote cmdline arguments that have spaces in them\n- fix hardlinks failing in some cases for extracting subtrees, #761\n\nOther changes:\n\n- replace stdlib hmac with OpenSSL, zero-copy decrypt (10-15% increase in\n  performance of hash-lists and extract).\n- improved chunker performance, #1021\n- open repository segment files in exclusive mode (fail-safe), #1134\n- improved error logging, #1440\n- Source:\n\n  - pass meta-data around, #765\n  - move some constants to new constants module\n  - better readability and fewer errors with namedtuples, #823\n  - moved source tree into src/ subdirectory, #1016\n  - made borg.platform a package, #1113\n  - removed dead crypto code, #1032\n  - improved and ported parts of the test suite to py.test, #912\n  - created data classes instead of passing dictionaries around, #981, #1158, #1161\n  - cleaned up imports, #1112\n- Docs:\n\n  - better help texts and sphinx reproduction of usage help:\n\n    - Group options\n    - Nicer list of options in Sphinx\n    - Deduplicate 'Common options' (including --help)\n  - chunker: added some insights by \"Voltara\", #903\n  - clarify what \"deduplicated size\" means\n  - fix / update / add package list entries\n  - added a SaltStack usage example, #956\n  - expanded FAQ\n  - new contributors in AUTHORS!\n- Tests:\n\n  - vagrant: add ubuntu/xenial 64bit - this box has still some issues\n  - ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945\n\n\nVersion 1.0.13 (2019-02-15)\n---------------------------\n\nPlease note: this is very likely the last 1.0.x release, please upgrade to 1.1.x.\n\nBug fixes:\n\n- security fix: configure FUSE with \"default_permissions\", #3903.\n  \"default_permissions\" is now enforced by borg by default to let the\n  kernel check uid/gid/mode based permissions.\n  \"ignore_permissions\" can be given not to enforce \"default_permissions\".\n- xattrs: fix borg exception handling on ENOSPC error, #3808.\n\nNew features:\n\n- Read a passphrase from a file descriptor specified in the\n  BORG_PASSPHRASE_FD environment variable.\n\nOther changes:\n\n- acl platform code: fix acl set return type\n- xattr:\n\n  - add linux {list,get,set}xattr ctypes prototypes\n  - fix darwin flistxattr ctypes prototype\n- testing / travis-ci:\n\n  - fix the homebrew 1.9 issues on travis-ci, #4254\n  - travis OS X: use xcode 8.3 (not broken)\n  - tox.ini: lock requirements\n  - unbreak 1.0-maint on travis, fixes #4123\n- vagrant:\n\n  - misc. fixes\n  - FUSE for macOS: upgrade 3.7.1 to 3.8.3\n  - Python: upgrade 3.5.5 to 3.5.6\n- docs:\n\n  - Update installation instructions for macOS\n  - update release workflow using twine (docs, scripts), #4213\n\nVersion 1.0.12 (2018-04-08)\n---------------------------\n\nBug fixes:\n\n- repository: cleanup/write: invalidate cached FDs, tests\n- serve: fix exitcode, #2910\n- extract: set bsdflags last (include immutable flag), #3263\n- create --timestamp: set start time, #2957\n- create: show excluded dir with \"x\" for tagged dirs / caches, #3189\n- migrate locks to child PID when daemonize is used\n- Buffer: fix wrong thread-local storage use, #2951\n- fix detection of non-local path, #3108\n- fix LDLP restoration for subprocesses, #3077\n- fix subprocess environments (xattr module's fakeroot version check,\n  borg umount, BORG_PASSCOMMAND), #3050\n- remote: deal with partial lines, #2637\n- get rid of datetime.isoformat, use safe parse_timestamp to parse\n  timestamps, #2994\n- build: do .h file content checks in binary mode, fixes build issue for\n  non-ascii header files on pure-ascii locale platforms, #3544 #3639\n- remove platform.uname() call which caused library mismatch issues, #3732\n- add exception handler around deprecated platform.linux_distribution() call\n\nOther changes:\n\n- require msgpack-python >= 0.4.6 and < 0.5.0, see #3753\n- add parens for C preprocessor macro argument usages (did not cause\n  malfunction)\n- ignore corrupt files cache, #2939\n- replace \"modulo\" with \"if\" to check for wraparound in hashmap\n- keymanager: don't depend on optional readline module, #2980\n- exclude broken pytest 3.3.0 release\n- exclude broken Cython 0.27(.0) release, #3066\n- flake8: add some ignores\n- docs:\n\n  - create: document exclusion through nodump\n  - document good and problematic option placements, fix examples, #3356\n  - update docs about hardlinked symlinks limitation\n  - faq: we do not implement futile attempts of ETA / progress displays\n  - simplified rate limiting wrapper in FAQ\n  - twitter account @borgbackup, #2948\n  - add note about metadata dedup and --no[ac]time, #2518\n  - change-passphrase only changes the passphrase, #2990\n  - clarify encrypted key format for borg key export, #3296\n  - document sshfs rename workaround, #3315\n  - update release checklist about security fixes\n  - docs about how to verify a signed release, #3634\n  - chunk seed is generated per /repository/\n- vagrant:\n\n  - use FUSE for macOS 3.7.1 to build the macOS binary\n  - use python 3.5.5 to build the binaries\n  - add exe location to PATH when we build an exe\n  - use https pypi url for wheezy\n  - netbsd: bash is already installed\n  - netbsd: fix netbsd version in PKG_PATH\n  - use self-made FreeBSD 10.3 box, #3022\n  - backport fs_init (including related updates) from 1.1\n  - the boxcutter wheezy boxes are 404, use local ones\n- travis:\n\n  - don't perform full Travis build on docs-only changes, #2531\n  - only short-circuit docs-only changes for pull requests\n\n\nVersion 1.0.11 (2017-07-21)\n---------------------------\n\nBug fixes:\n\n- use limited unpacker for outer key (security precaution), #2174\n- fix paperkey import bug\n\nOther changes:\n\n- change --checkpoint-interval default from 600s to 1800s, #2841.\n  this improves efficiency for big repositories a lot.\n- docs: fix OpenSUSE command and add OpenSUSE section\n- tests: add tests for split_lstring and paperkey\n- vagrant:\n\n  - fix openbsd shell\n  - backport cpu/ram setup from master\n  - add stretch64 VM\n\nVersion 1.0.11rc1 (2017-06-27)\n------------------------------\n\nBug fixes:\n\n- performance: rebuild hashtable if we have too few empty buckets, #2246.\n  this fixes some sporadic, but severe performance breakdowns.\n- Archive: allocate zeros when needed, #2308\n  fixes huge memory usage of mount (8 MiB × number of archives)\n- IPv6 address support\n  also: Location: more informative exception when parsing fails\n- borg single-file binary: use pyinstaller v3.2.1, #2396\n  this fixes that the prelink cronjob on some distros kills the\n  borg binary by stripping away parts of it.\n- extract:\n\n  - warning for unextracted big extended attributes, #2258\n  - also create parent dir for device files, if needed.\n  - don't write to disk with --stdout, #2645\n- archive check: detect and fix missing all-zero replacement chunks, #2180\n- fix (de)compression exceptions, #2224 #2221\n- files cache: update inode number, #2226\n- borg rpc: use limited msgpack.Unpacker (security precaution), #2139\n- Manifest: use limited msgpack.Unpacker (security precaution), #2175\n- Location: accept //servername/share/path\n- fix ChunkIndex.__contains__ assertion  for big-endian archs (harmless)\n- create: handle BackupOSError on a per-path level in one spot\n- fix error msg, there is no --keep-last in borg 1.0.x, #2282\n- clamp (nano)second values to unproblematic range, #2304\n- fuse / borg mount:\n\n  - fix st_blocks to be an integer (not float) value\n  - fix negative uid/gid crash (they could come into archives e.g. when\n    backing up external drives under cygwin), #2674\n  - fix crash if empty (None) xattr is read\n  - do pre-mount checks before opening repository\n  - check llfuse is installed before asking for passphrase\n- borg rename: expand placeholders, #2386\n- borg serve: fix forced command lines containing BORG_* env vars\n- fix error msg, it is --keep-within, not --within\n- fix borg key/debug/benchmark crashing without subcommand, #2240\n- chunker: fix invalid use of types, don't do uint32_t >> 32\n- document follow_symlinks requirements, check libc, #2507\n\nNew features:\n\n- added BORG_PASSCOMMAND environment variable, #2573\n- add minimal version of in repository mandatory feature flags, #2134\n\n  This should allow us to make sure older borg versions can be cleanly\n  prevented from doing operations that are no longer safe because of\n  repository format evolution. This allows more fine grained control than\n  just incrementing the manifest version. So for example a change that\n  still allows new archives to be created but would corrupt the repository\n  when an old version tries to delete an archive or check the repository\n  would add the new feature to the check and delete set but leave it out\n  of the write set.\n- borg delete --force --force to delete severely corrupted archives, #1975\n\nOther changes:\n\n- embrace y2038 issue to support 32bit platforms\n- be more clear that this is a \"beyond repair\" case, #2427\n- key file names: limit to 100 characters and remove colons from host name\n- upgrade FUSE for macOS to 3.5.8, #2346\n- split up parsing and filtering for --keep-within, better error message, #2610\n- docs:\n\n  - fix caskroom link, #2299\n  - address SSH batch mode, #2202 #2270\n  - improve remote-path description\n  - document snapshot usage, #2178\n  - document relative path usage, #1868\n  - one link per distro in the installation page\n  - development: new branching model in git repository\n  - kill api page\n  - added FAQ section about backing up root partition\n  - add bountysource badge, #2558\n  - create empty docs.txt requirements, #2694\n  - README: how to help the project\n  - note -v/--verbose requirement on affected options, #2542\n  - document borg init behaviour via append-only borg serve, #2440\n  - be clear about what buzhash is used for (chunking) and want it is not\n    used for (deduplication)- also say already in the readme that we use a\n    cryptohash for dedupe, so people don't worry, #2390\n  - add hint about chunker params to borg upgrade docs, #2421\n  - clarify borg upgrade docs, #2436\n  - quickstart: delete problematic BORG_PASSPHRASE use, #2623\n  - faq: specify \"using inline shell scripts\"\n  - document pattern denial of service, #2624\n- tests:\n\n  - remove attic dependency of the tests, #2505\n  - travis:\n\n    - enhance travis setuptools_scm situation\n    - install fakeroot for Linux\n  - add test for borg delete --force\n  - enable remote tests on cygwin (the cygwin issue that caused these tests\n    to break was fixed in cygwin at least since cygwin 2.8, maybe even since\n    2.7.0).\n  - remove skipping the noatime tests on GNU/Hurd, #2710\n  - fix borg import issue, add comment, #2718\n  - include attic.tar.gz when installing the package\n    also: add include_package_data=True\n\nVersion 1.0.10 (2017-02-13)\n---------------------------\n\nBug fixes:\n\n- Manifest timestamps are now monotonically increasing,\n  this fixes issues when the system clock jumps backwards\n  or is set inconsistently across computers accessing the same repository, #2115\n- Fixed testing regression in 1.0.10rc1 that lead to a hard dependency on\n  py.test >= 3.0, #2112\n\nNew features:\n\n- \"key export\" can now generate a printable HTML page with both a QR code and\n  a human-readable \"paperkey\" representation (and custom text) through the\n  ``--qr-html`` option.\n\n  The same functionality is also available through `paperkey.html <paperkey.html>`_,\n  which is the same HTML page generated by ``--qr-html``. It works with existing\n  \"key export\" files and key files.\n\nOther changes:\n\n- docs:\n\n  - language clarification - \"borg create --one-file-system\" option does not respect\n    mount points, but considers different file systems instead, #2141\n- setup.py: build_api: sort file list for determinism\n\n\nVersion 1.0.10rc1 (2017-01-29)\n------------------------------\n\nBug fixes:\n\n- borg serve: fix transmission data loss of pipe writes, #1268\n  This affects only the cygwin platform (not Linux, BSD, OS X).\n- Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992\n- When running out of buffer memory when reading xattrs, only skip the\n  current file, #1993\n- Fixed \"borg upgrade --tam\" crashing with unencrypted repositories. Since\n  :ref:`the issue <tam_vuln>` is not relevant for unencrypted repositories,\n  it now does nothing and prints an error, #1981.\n- Fixed change-passphrase crashing with unencrypted repositories, #1978\n- Fixed \"borg check repo::archive\" indicating success if \"archive\" does not exist, #1997\n- borg check: print non-exit-code warning if --last or --prefix aren't fulfilled\n- fix bad parsing of wrong repo location syntax\n- create: don't create hard link refs to failed files,\n  mount: handle invalid hard link refs, #2092\n- detect mingw byte order, #2073\n- creating a new segment: use \"xb\" mode, #2099\n- mount: umount on SIGINT/^C when in foreground, #2082\n\nOther changes:\n\n- binary: use fixed AND freshly compiled pyinstaller bootloader, #2002\n- xattr: ignore empty names returned by llistxattr(2) et al\n- Enable the fault handler: install handlers for the SIGSEGV, SIGFPE, SIGABRT,\n  SIGBUS and SIGILL signals to dump the Python traceback.\n- Also print a traceback on SIGUSR2.\n- borg change-passphrase: print key location (simplify making a backup of it)\n- officially support Python 3.6 (setup.py: add Python 3.6 qualifier)\n- tests:\n\n  - vagrant / travis / tox: add Python 3.6 based testing\n  - vagrant: fix openbsd repo, #2042\n  - vagrant: fix the freebsd64 machine, #2037 #2067\n  - vagrant: use python 3.5.3 to build binaries, #2078\n  - vagrant: use osxfuse 3.5.4 for tests / to build binaries\n    vagrant: improve darwin64 VM settings\n  - travis: fix osxfuse install (fixes OS X testing on Travis CI)\n  - travis: require succeeding OS X tests, #2028\n  - travis: use latest pythons for OS X based testing\n  - use pytest-xdist to parallelize testing\n  - fix xattr test race condition, #2047\n  - setup.cfg: fix pytest deprecation warning, #2050\n- docs:\n\n  - language clarification - VM backup FAQ\n  - borg create: document how to back up stdin, #2013\n  - borg upgrade: fix incorrect title levels\n  - add CVE numbers for issues fixed in 1.0.9, #2106\n- fix typos (taken from Debian package patch)\n- remote: include data hexdump in \"unexpected RPC data\" error message\n- remote: log SSH command line at debug level\n- API_VERSION: use numberspaces, #2023\n- remove .github from pypi package, #2051\n- add pip and setuptools to requirements file, #2030\n- SyncFile: fix use of fd object after close (cosmetic)\n- Manifest.in: simplify, exclude \\*.{so,dll,orig}, #2066\n- ignore posix_fadvise errors in repository.py, #2095\n  (works around issues with docker on ARM)\n- make LoggedIO.close_segment reentrant, avoid reentrance\n\n\nVersion 1.0.9 (2016-12-20)\n--------------------------\n\nSecurity fixes:\n\n- A flaw in the cryptographic authentication scheme in Borg allowed an attacker\n  to spoof the manifest. See :ref:`tam_vuln` above for the steps you should\n  take.\n\n  CVE-2016-10099 was assigned to this vulnerability.\n- borg check: When rebuilding the manifest (which should only be needed very rarely)\n  duplicate archive names would be handled on a \"first come first serve\" basis,\n  potentially opening an attack vector to replace archives.\n\n  Example: were there 2 archives named \"foo\" in a repo (which can not happen\n  under normal circumstances, because borg checks if the name is already used)\n  and a \"borg check\" recreated a (previously lost) manifest, the first of the\n  archives it encountered would be in the manifest. The second archive is also\n  still in the repo, but not referenced in the manifest, in this case. If the\n  second archive is the \"correct\" one (and was previously referenced from the\n  manifest), it looks like it got replaced by the first one. In the manifest,\n  it actually got replaced. Both remain in the repo but the \"correct\" one is no\n  longer accessible via normal means - the manifest.\n\n  CVE-2016-10100 was assigned to this vulnerability.\n\nBug fixes:\n\n- borg check:\n\n  - rebuild manifest if it's corrupted\n  - skip corrupted chunks during manifest rebuild\n- fix TypeError in integrity error handler, #1903, #1894\n- fix location parser for archives with @ char (regression introduced in 1.0.8), #1930\n- fix wrong duration/timestamps if system clock jumped during a create\n- fix progress display not updating if system clock jumps backwards\n- fix checkpoint interval being incorrect if system clock jumps\n\nOther changes:\n\n- docs:\n\n  - add python3-devel as a dependency for cygwin-based installation\n  - clarify extract is relative to current directory\n  - FAQ: fix link to changelog\n  - markup fixes\n- tests:\n\n  - test_get\\_(cache|keys)_dir: clean env state, #1897\n  - get back pytest's pretty assertion failures, #1938\n- setup.py build_usage:\n\n  - fixed build_usage not processing all commands\n  - fixed build_usage not generating includes for debug commands\n\n\nVersion 1.0.9rc1 (2016-11-27)\n-----------------------------\n\nBug fixes:\n\n- files cache: fix determination of newest mtime in backup set (which is\n  used in cache cleanup and led to wrong \"A\" [added] status for unchanged\n  files in next backup), #1860.\n\n- borg check:\n\n  - fix incorrectly reporting attic 0.13 and earlier archives as corrupt\n  - handle repo w/o objects gracefully and also bail out early if repo is\n    *completely* empty, #1815.\n- fix tox/pybuild in 1.0-maint\n- at xattr module import time, loggers are not initialized yet\n\nNew features:\n\n- borg umount <mountpoint>\n  exposed already existing umount code via the CLI api, so users can use it,\n  which is more consistent than using borg to mount and fusermount -u (or\n  umount) to un-mount, #1855.\n- implement borg create --noatime --noctime, fixes #1853\n\nOther changes:\n\n- docs:\n\n  - display README correctly on PyPI\n  - improve cache / index docs, esp. files cache docs, fixes #1825\n  - different pattern matching for --exclude, #1779\n  - datetime formatting examples for {now} placeholder, #1822\n  - clarify passphrase mode attic repo upgrade, #1854\n  - clarify --umask usage, #1859\n  - clarify how to choose PR target branch\n  - clarify prune behavior for different archive contents, #1824\n  - fix PDF issues, add logo, fix authors, headings, TOC\n  - move security verification to support section\n  - fix links in standalone README (:ref: tags)\n  - add link to security contact in README\n  - add FAQ about security\n  - move fork differences to FAQ\n  - add more details about resource usage\n- tests: skip remote tests on cygwin, #1268\n- travis:\n\n  - allow OS X failures until the brew cask osxfuse issue is fixed\n  - caskroom osxfuse-beta gone, it's osxfuse now (3.5.3)\n- vagrant:\n\n  - upgrade OSXfuse / FUSE for macOS to 3.5.3\n  - remove llfuse from tox.ini at a central place\n  - do not try to install llfuse on centos6\n  - fix FUSE test for darwin, #1546\n  - add windows virtual machine with cygwin\n  - Vagrantfile cleanup / code deduplication\n\n\nVersion 1.0.8 (2016-10-29)\n--------------------------\n\nBug fixes:\n\n- RemoteRepository: Fix busy wait in call_many, #940\n\nNew features:\n\n- implement borgmajor/borgminor/borgpatch placeholders, #1694\n  {borgversion} was already there (full version string). With the new\n  placeholders you can now also get e.g. 1 or 1.0 or 1.0.8.\n\nOther changes:\n\n- avoid previous_location mismatch, #1741\n\n  due to the changed canonicalization for relative paths in PR #1711 / #1655\n  (implement /./ relpath hack), there would be a changed repo location warning\n  and the user would be asked if this is ok. this would break automation and\n  require manual intervention, which is unwanted.\n\n  thus, we automatically fix the previous_location config entry, if it only\n  changed in the expected way, but still means the same location.\n\n- docs:\n\n  - deployment.rst: do not use bare variables in ansible snippet\n  - add clarification about append-only mode, #1689\n  - setup.py: add comment about requiring llfuse, #1726\n  - update usage.rst / api.rst\n  - repo url / archive location docs + typo fix\n  - quickstart: add a comment about other (remote) filesystems\n\n- vagrant / tests:\n\n  - no chown when rsyncing (fixes boxes w/o vagrant group)\n  - fix FUSE permission issues on linux/freebsd, #1544\n  - skip FUSE test for borg binary + fakeroot\n  - ignore security.selinux xattrs, fixes tests on centos, #1735\n\n\nVersion 1.0.8rc1 (2016-10-17)\n-----------------------------\n\nBug fixes:\n\n- fix signal handling (SIGINT, SIGTERM, SIGHUP), #1620 #1593\n  Fixes e.g. leftover lock files for quickly repeated signals (e.g. Ctrl-C\n  Ctrl-C) or lost connections or systemd sending SIGHUP.\n- progress display: adapt formatting to narrow screens, do not crash, #1628\n- borg create --read-special - fix crash on broken symlink, #1584.\n  also correctly processes broken symlinks. before this regressed to a crash\n  (5b45385) a broken symlink would've been skipped.\n- process_symlink: fix missing backup_io()\n  Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting\n  dirents and dispatching to process_symlink.\n- yes(): abort on wrong answers, saying so, #1622\n- fixed exception borg serve raised when connection was closed before repository\n  was opened. Add an error message for this.\n- fix read-from-closed-FD issue, #1551\n  (this seems not to get triggered in 1.0.x, but was discovered in master)\n- hashindex: fix iterators (always raise StopIteration when exhausted)\n  (this seems not to get triggered in 1.0.x, but was discovered in master)\n- enable relative paths in ssh:// repo URLs, via /./relpath hack, #1655\n- allow repo paths with colons, #1705\n- update changed repo location immediately after acceptance, #1524\n- fix debug get-obj / delete-obj crash if object not found and remote repo,\n  #1684\n- pyinstaller: use a spec file to build borg.exe binary, exclude osxfuse dylib\n  on Mac OS X (avoids mismatch lib <-> driver), #1619\n\nNew features:\n\n- add \"borg key export\" / \"borg key import\" commands, #1555, so users are able\n  to back up / restore their encryption keys more easily.\n\n  Supported formats are the keyfile format used by borg internally and a\n  special \"paper\" format with by line checksums for printed backups. For the\n  paper format, the import is an interactive process which checks each line as\n  soon as it is input.\n- add \"borg debug-refcount-obj\" to determine a repo objects' referrer counts,\n  #1352\n\nOther changes:\n\n- add \"borg debug ...\" subcommands\n  (borg debug-* still works, but will be removed in borg 1.1)\n- setup.py: Add subcommand support to build_usage.\n- remote: change exception message for unexpected RPC data format to indicate\n  dataflow direction.\n- improved messages / error reporting:\n\n  - IntegrityError: add placeholder for message, so that the message we give\n    appears not only in the traceback, but also in the (short) error message,\n    #1572\n  - borg.key: include chunk id in exception msgs, #1571\n  - better messages for cache newer than repo, #1700\n- vagrant (testing/build VMs):\n\n  - upgrade OSXfuse / FUSE for macOS to 3.5.2\n  - update Debian Wheezy boxes, #1686\n  - openbsd / netbsd: use own boxes, fixes misc rsync installation and\n    FUSE/llfuse related testing issues, #1695 #1696 #1670 #1671 #1728\n- docs:\n\n  - add docs for \"key export\" and \"key import\" commands, #1641\n  - fix inconsistency in FAQ (pv-wrapper).\n  - fix second block in \"Easy to use\" section not showing on GitHub, #1576\n  - add bestpractices badge\n  - link reference docs and faq about BORG_FILES_CACHE_TTL, #1561\n  - improve borg info --help, explain size infos, #1532\n  - add release signing key / security contact to README, #1560\n  - add contribution guidelines for developers\n  - development.rst: add sphinx_rtd_theme to the sphinx install command\n  - adjust border color in borg.css\n  - add debug-info usage help file\n  - internals.rst: fix typos\n  - setup.py: fix build_usage to always process all commands\n  - added docs explaining multiple --restrict-to-path flags, #1602\n  - add more specific warning about write-access debug commands, #1587\n  - clarify FAQ regarding backup of virtual machines, #1672\n- tests:\n\n  - work around FUSE xattr test issue with recent fakeroot\n  - simplify repo/hashindex tests\n  - travis: test FUSE-enabled borg, use trusty to have a recent FUSE\n  - re-enable FUSE tests for RemoteArchiver (no deadlocks any more)\n  - clean env for pytest based tests, #1714\n  - fuse_mount contextmanager: accept any options\n\n\nVersion 1.0.7 (2016-08-19)\n--------------------------\n\nSecurity fixes:\n\n- borg serve: fix security issue with remote repository access, #1428\n  If you used e.g. --restrict-to-path /path/client1/ (with or without trailing\n  slash does not make a difference), it acted like a path prefix match using\n  /path/client1 (note the missing trailing slash) - the code then also allowed\n  working in e.g. /path/client13 or /path/client1000.\n\n  As this could accidentally lead to major security/privacy issues depending on\n  the paths you use, the behaviour was changed to be a strict directory match.\n  That means --restrict-to-path /path/client1 (with or without trailing slash\n  does not make a difference) now uses /path/client1/ internally (note the\n  trailing slash here!) for matching and allows precisely that path AND any\n  path below it. So, /path/client1 is allowed, /path/client1/repo1 is allowed,\n  but not /path/client13 or /path/client1000.\n\n  If you willingly used the undocumented (dangerous) previous behaviour, you\n  may need to rearrange your --restrict-to-path paths now. We are sorry if\n  that causes work for you, but we did not want a potentially dangerous\n  behaviour in the software (not even using a for-backwards-compat option).\n\nBug fixes:\n\n- fixed repeated LockTimeout exceptions when borg serve tried to write into\n  a already write-locked repo (e.g. by a borg mount), #502 part b)\n  This was solved by the fix for #1220 in 1.0.7rc1 already.\n- fix cosmetics + file leftover for \"not a valid borg repository\", #1490\n- Cache: release lock if cache is invalid, #1501\n- borg extract --strip-components: fix leak of preloaded chunk contents\n- Repository, when a InvalidRepository exception happens:\n\n  - fix spurious, empty lock.roster\n  - fix repo not closed cleanly\n\nNew features:\n\n- implement borg debug-info, fixes #1122\n  (just calls already existing code via cli, same output as below tracebacks)\n\nOther changes:\n\n- skip the O_NOATIME test on GNU Hurd, fixes #1315\n  (this is a very minor issue and the GNU Hurd project knows the bug)\n- document using a clean repo to test / build the release\n\n\nVersion 1.0.7rc2 (2016-08-13)\n-----------------------------\n\nBug fixes:\n\n- do not write objects to repository that are bigger than the allowed size,\n  borg will reject reading them, #1451.\n\n  Important: if you created archives with many millions of files or\n  directories, please verify if you can open them successfully,\n  e.g. try a \"borg list REPO::ARCHIVE\".\n- lz4 compression: dynamically enlarge the (de)compression buffer, the static\n  buffer was not big enough for archives with extremely many items, #1453\n- larger item metadata stream chunks, raise archive item limit by 8x, #1452\n- fix untracked segments made by moved DELETEs, #1442\n\n  Impact: Previously (metadata) segments could become untracked when deleting data,\n  these would never be cleaned up.\n- extended attributes (xattrs) related fixes:\n\n  - fixed a race condition in xattrs querying that led to the entire file not\n    being backed up (while logging the error, exit code = 1), #1469\n  - fixed a race condition in xattrs querying that led to a crash, #1462\n  - raise OSError including the error message derived from errno, deal with\n    path being a integer FD\n\nOther changes:\n\n- print active env var override by default, #1467\n- xattr module: refactor code, deduplicate, clean up\n- repository: split object size check into too small and too big\n- add a transaction_id assertion, so borg init on a broken (inconsistent)\n  filesystem does not look like a coding error in borg, but points to the\n  real problem.\n- explain confusing TypeError caused by compat support for old servers, #1456\n- add forgotten usage help file from build_usage\n- refactor/unify buffer code into helpers.Buffer class, add tests\n- docs:\n\n  - document archive limitation, #1452\n  - improve prune examples\n\n\nVersion 1.0.7rc1 (2016-08-05)\n-----------------------------\n\nBug fixes:\n\n- fix repo lock deadlocks (related to lock upgrade), #1220\n- catch unpacker exceptions, resync, #1351\n- fix borg break-lock ignoring BORG_REPO env var, #1324\n- files cache performance fixes (fixes unnecessary re-reading/chunking/\n  hashing of unmodified files for some use cases):\n\n  - fix unintended file cache eviction, #1430\n  - implement BORG_FILES_CACHE_TTL, update FAQ, raise default TTL from 10\n    to 20, #1338\n- FUSE:\n\n  - cache partially read data chunks (performance), #965, #966\n  - always create a root dir, #1125\n- use an OrderedDict for helptext, making the build reproducible, #1346\n- RemoteRepository init: always call close on exceptions, #1370 (cosmetic)\n- ignore stdout/stderr broken pipe errors (cosmetic), #1116\n\nNew features:\n\n- better borg versions management support (useful esp. for borg servers\n  wanting to offer multiple borg versions and for clients wanting to choose\n  a specific server borg version), #1392:\n\n  - add BORG_VERSION environment variable before executing \"borg serve\" via ssh\n  - add new placeholder {borgversion}\n  - substitute placeholders in --remote-path\n\n- borg init --append-only option (makes using the more secure append-only mode\n  more convenient. when used remotely, this requires 1.0.7+ also on the borg\n  server), #1291.\n\nOther changes:\n\n- Vagrantfile:\n\n  - darwin64: upgrade to FUSE for macOS 3.4.1 (aka osxfuse), #1378\n  - xenial64: use user \"ubuntu\", not \"vagrant\" (as usual), #1331\n- tests:\n\n  - fix FUSE tests on OS X, #1433\n- docs:\n\n  - FAQ: add backup using stable filesystem names recommendation\n  - FAQ about glibc compatibility added, #491, glibc-check improved\n  - FAQ: 'A' unchanged file; remove ambiguous entry age sentence.\n  - OS X: install pkg-config to build with FUSE support, fixes #1400\n  - add notes about shell/sudo pitfalls with env. vars, #1380\n  - added platform feature matrix\n- implement borg debug-dump-repo-objs\n\n\nVersion 1.0.6 (2016-07-12)\n--------------------------\n\nBug fixes:\n\n- Linux: handle multiple LD_PRELOAD entries correctly, #1314, #1111\n- Fix crash with unclear message if the libc is not found, #1314, #1111\n\nOther changes:\n\n- tests:\n\n  - Fixed O_NOATIME tests for Solaris and GNU Hurd, #1315\n  - Fixed sparse file tests for (file) systems not supporting it, #1310\n- docs:\n\n  - Fixed syntax highlighting, #1313\n  - misc docs: added data processing overview picture\n\n\nVersion 1.0.6rc1 (2016-07-10)\n-----------------------------\n\nNew features:\n\n- borg check --repair: heal damaged files if missing chunks re-appear (e.g. if\n  the previously missing chunk was added again in a later backup archive),\n  #148. (*) Also improved logging.\n\nBug fixes:\n\n- sync_dir: silence fsync() failing with EINVAL, #1287\n  Some network filesystems (like smbfs) don't support this and we use this in\n  repository code.\n- borg mount (FUSE):\n\n  - fix directories being shadowed when contained paths were also specified,\n    #1295\n  - raise I/O Error (EIO) on damaged files (unless -o allow_damaged_files is\n    used), #1302. (*)\n- borg extract: warn if a damaged file is extracted, #1299. (*)\n- Added some missing return code checks (ChunkIndex._add, hashindex_resize).\n- borg check: fix/optimize initial hash table size, avoids resize of the table.\n\nOther changes:\n\n- tests:\n\n  - add more FUSE tests, #1284\n  - deduplicate FUSE (u)mount code\n  - fix borg binary test issues, #862\n- docs:\n\n  - changelog: added release dates to older borg releases\n  - fix some sphinx (docs generator) warnings, #881\n\nNotes:\n\n(*) Some features depend on information (chunks_healthy list) added to item\nmetadata when a file with missing chunks was \"repaired\" using all-zero\nreplacement chunks. The chunks_healthy list is generated since borg 1.0.4,\nthus borg can't recognize such \"repaired\" (but content-damaged) files if the\nrepair was done with an older borg version.\n\n\nVersion 1.0.5 (2016-07-07)\n--------------------------\n\nBug fixes:\n\n- borg mount: fix FUSE crash in xattr code on Linux introduced in 1.0.4, #1282\n\nOther changes:\n\n- backport some FAQ entries from master branch\n- add release helper scripts\n- Vagrantfile:\n\n  - centos6: no FUSE, don't build binary\n  - add xz for redhat-like dists\n\n\nVersion 1.0.4 (2016-07-07)\n--------------------------\n\nNew features:\n\n- borg serve --append-only, #1168\n  This was included because it was a simple change (append-only functionality\n  was already present via repository config file) and makes better security now\n  practically usable.\n- BORG_REMOTE_PATH environment variable, #1258\n  This was included because it was a simple change (--remote-path cli option\n  was already present) and makes borg much easier to use if you need it.\n- Repository: cleanup incomplete transaction on \"no space left\" condition.\n  In many cases, this can avoid a 100% full repo filesystem (which is very\n  problematic as borg always needs free space - even to delete archives).\n\nBug fixes:\n\n- Fix wrong handling and reporting of OSErrors in borg create, #1138.\n  This was a serious issue: in the context of \"borg create\", errors like\n  repository I/O errors (e.g. disk I/O errors, ssh repo connection errors)\n  were handled badly and did not lead to a crash (which would be good for this\n  case, because the repo transaction would be incomplete and trigger a\n  transaction rollback to clean up).\n  Now, error handling for source files is cleanly separated from every other\n  error handling, so only problematic input files are logged and skipped.\n- Implement fail-safe error handling for borg extract.\n  Note that this isn't nearly as critical as the borg create error handling\n  bug, since nothing is written to the repo. So this was \"merely\" misleading\n  error reporting.\n- Add missing error handler in directory attr restore loop.\n- repo: make sure write data hits disk before the commit tag (#1236) and also\n  sync the containing directory.\n- FUSE: getxattr fail must use errno.ENOATTR, #1126\n  (fixes Mac OS X Finder malfunction: \"zero bytes\" file length, access denied)\n- borg check --repair: do not lose information about the good/original chunks.\n  If we do not lose the original chunk IDs list when \"repairing\" a file\n  (replacing missing chunks with all-zero chunks), we have a chance to \"heal\"\n  the file back into its original state later, in case the chunks re-appear\n  (e.g. in a fresh backup). Healing is not implemented yet, see #148.\n- fixes for --read-special mode:\n\n  - ignore known files cache, #1241\n  - fake regular file mode, #1214\n  - improve symlinks handling, #1215\n- remove passphrase from subprocess environment, #1105\n- Ignore empty index file (will trigger index rebuild), #1195\n- add missing placeholder support for --prefix, #1027\n- improve exception handling for placeholder replacement\n- catch and format exceptions in arg parsing\n- helpers: fix \"undefined name 'e'\" in exception handler\n- better error handling for missing repo manifest, #1043\n- borg delete:\n\n  - make it possible to delete a repo without manifest\n  - borg delete --forced allows one to delete corrupted archives, #1139\n- borg check:\n\n  - make borg check work for empty repo\n  - fix resync and msgpacked item qualifier, #1135\n  - rebuild_manifest: fix crash if 'name' or 'time' key were missing.\n  - better validation of item metadata dicts, #1130\n  - better validation of archive metadata dicts\n- close the repo on exit - even if rollback did not work, #1197.\n  This is rather cosmetic, it avoids repo closing in the destructor.\n\n- tests:\n\n  - fix sparse file test, #1170\n  - flake8: ignore new F405, #1185\n  - catch \"invalid argument\" on cygwin, #257\n  - fix sparseness assertion in test prep, #1264\n\nOther changes:\n\n- make borg build/work on OpenSSL 1.0 and 1.1, #1187\n- docs / help:\n\n  - fix / clarify prune help, #1143\n  - fix \"patterns\" help formatting\n  - add missing docs / help about placeholders\n  - resources: rename atticmatic to borgmatic\n  - document sshd settings, #545\n  - more details about checkpoints, add split trick, #1171\n  - support docs: add freenode web chat link, #1175\n  - add prune visualization / example, #723\n  - add note that Fnmatch is default, #1247\n  - make clear that lzma levels > 6 are a waste of cpu cycles\n  - add a \"do not edit\" note to auto-generated files, #1250\n  - update cygwin installation docs\n- repository interoperability with borg master (1.1dev) branch:\n\n  - borg check: read item metadata keys from manifest, #1147\n  - read v2 hints files, #1235\n  - fix hints file \"unknown version\" error handling bug\n- tests: add tests for format_line\n- llfuse: update version requirement for freebsd\n- Vagrantfile:\n\n  - use openbsd 5.9, #716\n  - do not install llfuse on netbsd (broken)\n  - update OSXfuse to version 3.3.3\n  - use Python 3.5.2 to build the binaries\n- glibc compatibility checker: scripts/glibc_check.py\n- add .eggs to .gitignore\n\n\nVersion 1.0.3 (2016-05-20)\n--------------------------\n\nBug fixes:\n\n- prune: avoid that checkpoints are kept and completed archives are deleted in\n  a prune run), #997\n- prune: fix commandline argument validation - some valid command lines were\n  considered invalid (annoying, but harmless), #942\n- fix capabilities extraction on Linux (set xattrs last, after chown()), #1069\n- repository: fix commit tags being seen in data\n- when probing key files, do binary reads. avoids crash when non-borg binary\n  files are located in borg's key files directory.\n- handle SIGTERM and make a clean exit - avoids orphan lock files.\n- repository cache: don't cache large objects (avoid using lots of temp. disk\n  space), #1063\n\nOther changes:\n\n- Vagrantfile: OS X: update osxfuse / install lzma package, #933\n- setup.py: add check for platform_darwin.c\n- setup.py: on freebsd, use a llfuse release that builds ok\n- docs / help:\n\n  - update readthedocs URLs, #991\n  - add missing docs for \"borg break-lock\", #992\n  - borg create help: add some words to about the archive name\n  - borg create help: document format tags, #894\n\n\nVersion 1.0.2 (2016-04-16)\n--------------------------\n\nBug fixes:\n\n- fix malfunction and potential corruption on (nowadays rather rare) big-endian\n  architectures or bi-endian archs in (rare) BE mode. #886, #889\n\n  cache resync / index merge was malfunctioning due to this, potentially\n  leading to data loss. borg info had cosmetic issues (displayed wrong values).\n\n  note: all (widespread) little-endian archs (like x86/x64) or bi-endian archs\n  in (widespread) LE mode (like ARMEL, MIPSEL, ...) were NOT affected.\n- add overflow and range checks for 1st (special) uint32 of the hashindex\n  values, switch from int32 to uint32.\n- fix so that refcount will never overflow, but just stick to max. value after\n  a overflow would have occurred.\n- borg delete: fix --cache-only for broken caches, #874\n\n  Makes --cache-only idempotent: it won't fail if the cache is already deleted.\n- fixed borg create --one-file-system erroneously traversing into other\n  filesystems (if starting fs device number was 0), #873\n- workaround a bug in Linux fadvise FADV_DONTNEED, #907\n\nOther changes:\n\n- better test coverage for hashindex, incl. overflow testing, checking correct\n  computations so endianness issues would be discovered.\n- reproducible doc for ProgressIndicator*,  make the build reproducible.\n- use latest llfuse for vagrant machines\n- docs:\n\n  - use /path/to/repo in examples, fixes #901\n  - fix confusing usage of \"repo\" as archive name (use \"arch\")\n\n\nVersion 1.0.1 (2016-04-08)\n--------------------------\n\nNew features:\n\nUsually there are no new features in a bugfix release, but these were added\ndue to their high impact on security/safety/speed or because they are fixes\nalso:\n\n- append-only mode for repositories, #809, #36 (see docs)\n- borg create: add --ignore-inode option to make borg detect unmodified files\n  even if your filesystem does not have stable inode numbers (like sshfs and\n  possibly CIFS).\n- add options --warning, --error, --critical for missing log levels, #826.\n  it's not recommended to suppress warnings or errors, but the user may decide\n  this on his own.\n  note: --warning is not given to borg serve so a <= 1.0.0 borg will still\n  work as server (it is not needed as it is the default).\n  do not use --error or --critical when using a <= 1.0.0 borg server.\n\nBug fixes:\n\n- fix silently skipping EIO, #748\n- add context manager for Repository (avoid orphan repository locks), #285\n- do not sleep for >60s while waiting for lock, #773\n- unpack file stats before passing to FUSE\n- fix build on illumos\n- don't try to back up doors or event ports (Solaris and derivatives)\n- remove useless/misleading libc version display, #738\n- test suite: reset exit code of persistent archiver, #844\n- RemoteRepository: clean up pipe if remote open() fails\n- Remote: don't print tracebacks for Error exceptions handled downstream, #792\n- if BORG_PASSPHRASE is present but wrong, don't prompt for password, but fail\n  instead, #791\n- ArchiveChecker: move \"orphaned objects check skipped\" to INFO log level, #826\n- fix capitalization, add ellipses, change log level to debug for 2 messages,\n  #798\n\nOther changes:\n\n- update llfuse requirement, llfuse 1.0 works\n- update OS / dist packages on build machines, #717\n- prefer showing --info over -v in usage help, #859\n- docs:\n\n  - fix cygwin requirements (gcc-g++)\n  - document how to debug / file filesystem issues, #664\n  - fix reproducible build of api docs\n  - RTD theme: CSS !important overwrite, #727\n  - Document logo font. Recreate logo png. Remove GIMP logo file.\n\n\nVersion 1.0.0 (2016-03-05)\n--------------------------\n\nThe major release number change (0.x -> 1.x) indicates bigger incompatible\nchanges, please read the compatibility notes, adapt / test your scripts and\ncheck your backup logs.\n\nCompatibility notes:\n\n- drop support for python 3.2 and 3.3, require 3.4 or 3.5, #221 #65 #490\n  note: we provide binaries that include python 3.5.1 and everything else\n  needed. they are an option in case you are stuck with < 3.4 otherwise.\n- change encryption to be on by default (using \"repokey\" mode)\n- moved keyfile keys from ~/.borg/keys to ~/.config/borg/keys,\n  you can either move them manually or run \"borg upgrade <REPO>\"\n- remove support for --encryption=passphrase,\n  use borg migrate-to-repokey to switch to repokey mode, #97\n- remove deprecated --compression <number>,\n  use --compression zlib,<number> instead\n  in case of 0, you could also use --compression none\n- remove deprecated --hourly/daily/weekly/monthly/yearly\n  use --keep-hourly/daily/weekly/monthly/yearly instead\n- remove deprecated --do-not-cross-mountpoints,\n  use --one-file-system instead\n- disambiguate -p option, #563:\n\n  - -p now is same as --progress\n  - -P now is same as --prefix\n- remove deprecated \"borg verify\",\n  use \"borg extract --dry-run\" instead\n- cleanup environment variable semantics, #355\n  the environment variables used to be \"yes sayers\" when set, this was\n  conceptually generalized to \"automatic answerers\" and they just give their\n  value as answer (as if you typed in that value when being asked).\n  See the \"usage\" / \"Environment Variables\" section of the docs for details.\n- change the builtin default for --chunker-params, create 2MiB chunks, #343\n  --chunker-params new default: 19,23,21,4095 - old default: 10,23,16,4095\n\n  one of the biggest issues with borg < 1.0 (and also attic) was that it had a\n  default target chunk size of 64kiB, thus it created a lot of chunks and thus\n  also a huge chunk management overhead (high RAM and disk usage).\n\n  please note that the new default won't change the chunks that you already\n  have in your repository. the new big chunks do not deduplicate with the old\n  small chunks, so expect your repo to grow at least by the size of every\n  changed file and in the worst case (e.g. if your files cache was lost / is\n  not used) by the size of every file (minus any compression you might use).\n\n  in case you want to see a much lower resource usage immediately (RAM / disk)\n  for chunks management, it might be better to start with a new repo than\n  to continue in the existing repo (with an existing repo, you have to wait\n  until all archives with small chunks get pruned to see a lower resource\n  usage).\n\n  if you used the old --chunker-params default value (or if you did not use\n  --chunker-params option at all) and you'd like to continue using small\n  chunks (and you accept the huge resource usage that comes with that), just\n  use explicitly borg create --chunker-params=10,23,16,4095.\n- archive timestamps: the 'time' timestamp now refers to archive creation\n  start time (was: end time), the new 'time_end' timestamp refers to archive\n  creation end time. This might affect prune if your backups take a long time.\n  if you give a timestamp via cli, this is stored into 'time'. therefore it now\n  needs to mean archive creation start time.\n\nNew features:\n\n- implement password roundtrip, #695\n\nBug fixes:\n\n- remote end does not need cache nor keys directories, do not create them, #701\n- added retry counter for passwords, #703\n\nOther changes:\n\n- fix compiler warnings, #697\n- docs:\n\n  - update README.rst to new changelog location in docs/changes.rst\n  - add Teemu to AUTHORS\n  - changes.rst: fix old chunker params, #698\n  - FAQ: how to limit bandwidth\n\n\nVersion 1.0.0rc2 (2016-02-28)\n-----------------------------\n\nNew features:\n\n- format options for location: user, pid, fqdn, hostname, now, utcnow, user\n- borg list --list-format\n- borg prune -v --list enables the keep/prune list output, #658\n\nBug fixes:\n\n- fix _open_rb noatime handling, #657\n- add a simple archivename validator, #680\n- borg create --stats: show timestamps in localtime, use same labels/formatting\n  as borg info, #651\n- llfuse compatibility fixes (now compatible with: 0.40, 0.41, 0.42)\n\nOther changes:\n\n- it is now possible to use \"pip install borgbackup[fuse]\" to\n  install the llfuse dependency automatically, using the correct version requirement\n  for it. you still need to care about having installed the FUSE / build\n  related OS package first, though, so that building llfuse can succeed.\n- Vagrant: drop Ubuntu Precise (12.04) - does not have Python >= 3.4\n- Vagrant: use pyinstaller v3.1.1 to build binaries\n- docs:\n\n  - borg upgrade: add to docs that only LOCAL repos are supported\n  - borg upgrade also handles borg 0.xx -> 1.0\n  - use pip extras or requirements file to install llfuse\n  - fix order in release process\n  - updated usage docs and other minor / cosmetic fixes\n  - verified borg examples in docs, #644\n  - freebsd dependency installation and FUSE configuration, #649\n  - add example how to restore a raw device, #671\n  - add a hint about the dev headers needed when installing from source\n  - add examples for delete (and handle delete after list, before prune), #656\n  - update example for borg create -v --stats (use iso datetime format), #663\n  - added example to BORG_RSH docs\n  - \"connection closed by remote\": add FAQ entry and point to issue #636\n\n\nVersion 1.0.0rc1 (2016-02-07)\n-----------------------------\n\nNew features:\n\n- borg migrate-to-repokey (\"passphrase\" -> \"repokey\" encryption key mode)\n- implement --short for borg list REPO, #611\n- implement --list for borg extract (consistency with borg create)\n- borg serve: overwrite client's --restrict-to-path with ssh forced command's\n  option value (but keep everything else from the client commandline), #544\n- use $XDG_CONFIG_HOME/keys for keyfile keys (~/.config/borg/keys), #515\n- \"borg upgrade\" moves the keyfile keys to the new location\n- display both archive creation start and end time in \"borg info\", #627\n\n\nBug fixes:\n\n- normalize trailing slashes for the repository path, #606\n- Cache: fix exception handling in __init__, release lock, #610\n\nOther changes:\n\n- suppress unneeded exception context (PEP 409), simpler tracebacks\n- removed special code needed to deal with imperfections / incompatibilities /\n  missing stuff in py 3.2/3.3, simplify code that can be done simpler in 3.4\n- removed some version requirements that were kept on old versions because\n  newer did not support py 3.2 any more\n- use some py 3.4+ stdlib code instead of own/openssl/pypi code:\n\n  - use os.urandom instead of own cython openssl RAND_bytes wrapper, #493\n  - use hashlib.pbkdf2_hmac from py stdlib instead of own openssl wrapper\n  - use hmac.compare_digest instead of == operator (constant time comparison)\n  - use stat.filemode instead of homegrown code\n  - use \"mock\" library from stdlib, #145\n  - remove borg.support (with non-broken argparse copy), it is ok in 3.4+, #358\n- Vagrant: copy CHANGES.rst as symlink, #592\n- cosmetic code cleanups, add flake8 to tox/travis, #4\n- docs / help:\n\n  - make \"borg -h\" output prettier, #591\n  - slightly rephrase prune help\n  - add missing example for --list option of borg create\n  - quote exclude line that includes an asterisk to prevent shell expansion\n  - fix dead link to license\n  - delete Ubuntu Vivid, it is not supported anymore (EOL)\n  - OS X binary does not work for older OS X releases, #629\n  - borg serve's special support for forced/original ssh commands, #544\n  - misc. updates and fixes\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Documentation build configuration file, created by\n# sphinx-quickstart on Sat Sep 10 18:18:25 2011.\n#\n# This file is execfile()d with the current directory set to its containing directory.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\nimport sys\nimport os\n\nsys.path.insert(0, os.path.abspath(\"../src\"))\n\nfrom borg import __version__ as sw_version\n\n# -- General configuration -----------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be extensions\n# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.\nextensions = []\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# The suffix of source filenames.\nsource_suffix = \".rst\"\n\n# The encoding of source files.\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = \"index\"\n\n# General information about the project.\nproject = \"Borg - Deduplicating Archiver\"\ncopyright = \"2010-2014 Jonas Borgström, 2015-2025 The Borg Collective (see AUTHORS file)\"\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nsplit_char = \"+\" if \"+\" in sw_version else \"-\"\nversion = sw_version.split(split_char)[0]\n# The full version, including alpha/beta/rc tags.\nrelease = version\n\nsuppress_warnings = [\"image.nonlocal_uri\"]\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n# language = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n# today = ''\n# Else, today_fmt is used as the format for a strftime call.\ntoday_fmt = \"%Y-%m-%d\"\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = [\"_build\"]\n\n# The reST default role (used for this markup: `text`) to use for all documents.\n# default_role = None\n\n# The Borg docs contain no or very little Python docs.\n# Thus, the primary domain is RST.\nprimary_domain = \"rst\"\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = \"sphinx\"\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n\n# -- Options for HTML output ---------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages. See the documentation for\n# a list of built-in themes.\nimport guzzle_sphinx_theme\n\nhtml_theme_path = guzzle_sphinx_theme.html_theme_path()\nhtml_theme = \"guzzle_sphinx_theme\"\n\n\ndef set_rst_settings(app):\n    app.env.settings.update({\"field_name_limit\": 0, \"option_limit\": 0})\n\n\ndef setup(app):\n    app.setup_extension(\"sphinxcontrib.jquery\")\n    app.add_css_file(\"css/borg.css\")\n    app.connect(\"builder-inited\", set_rst_settings)\n\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\nhtml_theme_options = {\"project_nav_name\": \"Borg %s\" % version}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = ['_themes']\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n# html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\nhtml_logo = \"_static/logo.svg\"\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\nhtml_favicon = \"_static/favicon.ico\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"borg_theme\"]\n\nhtml_extra_path = [\"../src/borg/paperkey.html\"]\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\nhtml_last_updated_fmt = \"%Y-%m-%d\"\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\nhtml_use_smartypants = True\nsmartquotes_action = \"qe\"  # no D in there means \"do not transform -- and ---\"\n\n# Custom sidebar templates, maps document names to template names.\nhtml_sidebars = {\"**\": [\"logo-text.html\", \"versionselector.html\", \"searchbox.html\", \"globaltoc.html\"]}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n# html_domain_indices = True\n\n# If false, no index is generated.\nhtml_use_index = False\n\n# If true, the index is split into individual pages for each letter.\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\nhtml_show_sourcelink = False\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\nhtml_show_sphinx = False\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\nhtml_show_copyright = False\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = \"borgdoc\"\n\n\n# -- Options for LaTeX output --------------------------------------------------\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title, author, documentclass [howto/manual]).\nlatex_documents = [(\"book\", \"Borg.tex\", \"Borg Documentation\", \"The Borg Collective\", \"manual\")]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\nlatex_logo = \"_static/logo.pdf\"\n\nlatex_elements = {\"papersize\": \"a4paper\", \"pointsize\": \"10pt\", \"figure_align\": \"H\"}\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\nlatex_show_urls = \"footnote\"\n\n# Additional stuff for the LaTeX preamble.\n# latex_preamble = ''\n\n# Documents to append as an appendix to all manuals.\nlatex_appendices = [\"support\", \"resources\", \"changes\", \"authors\"]\n\n# If false, no module index is generated.\n# latex_domain_indices = True\n\n\n# -- Options for manual page output --------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (\n        \"usage\",\n        \"borg\",\n        \"BorgBackup is a deduplicating backup program with optional compression and authenticated encryption.\",\n        [\"The Borg Collective (see AUTHORS file)\"],\n        1,\n    )\n]\n\nextensions = [\n    \"sphinx.ext.extlinks\",\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.todo\",\n    \"sphinx.ext.coverage\",\n    \"sphinx.ext.viewcode\",\n    \"sphinxcontrib.jquery\",  # jquery is not included anymore by default\n    \"guzzle_sphinx_theme\",  # register the theme as an extension to generate a sitemap.xml\n]\n\nextlinks = {\"issue\": (\"https://github.com/borgbackup/borg/issues/%s\", \"#%s\")}\n"
  },
  {
    "path": "docs/deployment/automated-local.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n\nAutomated backups to a local hard drive\n=======================================\n\nThis guide shows how to automate backups to a hard drive directly connected\nto your computer. If a backup hard drive is connected, backups are automatically\nstarted, and the drive shut-down and disconnected when they are done.\n\nThis guide is written for a Linux-based operating system and makes use of\nsystemd and udev.\n\nOverview\n--------\n\nA udev rule is created to trigger on the addition of block devices. The rule contains a tag\nthat triggers systemd to start a one-shot service. The one-shot service executes a script in\nthe standard systemd service environment, which automatically captures stdout/stderr and\nlogs it to the journal.\n\nThe script mounts the added block device if it is a registered backup drive and creates\nbackups on it. When done, it optionally unmounts the filesystem and spins the drive down,\nso that it may be physically disconnected.\n\nConfiguring the system\n----------------------\n\nFirst, create the ``/etc/backups`` directory (as root).\nAll configuration goes into this directory.\n\nFind out the ID of the partition table of your backup disk (here assumed to be /dev/sdz)::\n\n    lsblk --fs -o +PTUUID /dev/sdz\n\nThen, create ``/etc/backups/80-backup.rules`` with the following content (all on one line)::\n\n    ACTION==\"add\", SUBSYSTEM==\"block\", ENV{ID_PART_TABLE_UUID}==\"<the PTUUID you just noted>\", TAG+=\"systemd\", ENV{SYSTEMD_WANTS}+=\"automatic-backup.service\"\n\nThe \"systemd\" tag in conjunction with the SYSTEMD_WANTS environment variable has systemd\nlaunch the \"automatic-backup\" service, which we will create next, as the\n``/etc/backups/automatic-backup.service`` file:\n\n.. code-block:: ini\n\n    [Service]\n    Type=oneshot\n    ExecStart=/etc/backups/run.sh\n\nNow, create the main backup script, ``/etc/backups/run.sh``. Below is a template;\nmodify it to suit your needs (e.g., more backup sets, dumping databases, etc.).\n\n.. code-block:: bash\n\n    #!/bin/bash -ue\n\n    # The udev rule is not terribly accurate and may trigger our service before\n    # the kernel has finished probing partitions. Sleep for a bit to ensure\n    # the kernel is done.\n    #\n    # This can be avoided by using a more precise udev rule, e.g. matching\n    # a specific hardware path and partition.\n    sleep 5\n\n    #\n    # Script configuration\n    #\n\n    # The backup partition is mounted there\n    MOUNTPOINT=/mnt/backup\n\n    # This is the location of the Borg repository\n    TARGET=$MOUNTPOINT/borg-backups/backup.borg\n\n    # Archive name schema\n    DATE=$(date --iso-8601)-$(hostname)\n\n    # This is the file that will later contain UUIDs of registered backup drives\n    DISKS=/etc/backups/backup.disks\n\n    # Find whether the connected block device is a backup drive\n    for uuid in $(lsblk --noheadings --list --output uuid)\n    do\n            if grep --quiet --fixed-strings $uuid $DISKS; then\n                    break\n            fi\n            uuid=\n    done\n\n    if [ ! $uuid ]; then\n            echo \"No backup disk found, exiting\"\n            exit 0\n    fi\n\n    echo \"Disk $uuid is a backup disk\"\n    partition_path=/dev/disk/by-uuid/$uuid\n    # Mount filesystem if not already done. This assumes that if something is already\n    # mounted at $MOUNTPOINT, it is the backup drive. It will not find the drive if\n    # it was mounted somewhere else.\n    findmnt $MOUNTPOINT >/dev/null || mount $partition_path $MOUNTPOINT\n    drive=$(lsblk --inverse --noheadings --list --paths --output name $partition_path | head --lines 1)\n    echo \"Drive path: $drive\"\n\n    #\n    # Create backups\n    #\n\n    # Options for borg create\n    BORG_OPTS=\"--stats --one-file-system --compression lz4\"\n\n    # Set BORG_PASSPHRASE or BORG_PASSCOMMAND somewhere around here, using export,\n    # if encryption is used.\n\n    # Because no one can answer these questions non-interactively, it is better to\n    # fail quickly instead of hanging.\n    export BORG_RELOCATED_REPO_ACCESS_IS_OK=no\n    export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no\n\n    # Log Borg version\n    borg --version\n\n    echo \"Starting backup for $DATE\"\n\n    # This is just an example, change it however you see fit\n    borg create $BORG_OPTS \\\n      --exclude root/.cache \\\n      --exclude var/lib/docker/devicemapper \\\n      $TARGET::$DATE-$$-system \\\n      / /boot\n\n    # /home is often a separate partition/filesystem.\n    # Even if it is not (add --exclude /home above), it probably makes sense\n    # to have /home in a separate archive.\n    borg create $BORG_OPTS \\\n      --exclude 'sh:home/*/.cache' \\\n      $TARGET::$DATE-$$-home \\\n      /home/\n\n    echo \"Completed backup for $DATE\"\n\n    # Just to be completely paranoid\n    sync\n\n    if [ -f /etc/backups/autoeject ]; then\n            umount $MOUNTPOINT\n            hdparm -Y $drive\n    fi\n\n    if [ -f /etc/backups/backup-suspend ]; then\n            systemctl suspend\n    fi\n\nCreate the ``/etc/backups/autoeject`` file to have the script automatically eject the drive\nafter creating the backup. Rename the file to something else (e.g., ``/etc/backups/autoeject-no``)\nwhen you want to do something with the drive after creating backups (e.g., running checks).\n\nCreate the ``/etc/backups/backup-suspend`` file if the machine should suspend after completing\nthe backup. Don't forget to disconnect the device physically before resuming,\notherwise you'll enter a cycle. You can also add an option to power down instead.\n\nCreate an empty ``/etc/backups/backup.disks`` file, in which you will register your backup drives.\n\nFinally, enable the udev rules and services:\n\n.. code-block:: bash\n\n    ln -s /etc/backups/80-backup.rules /etc/udev/rules.d/80-backup.rules\n    ln -s /etc/backups/automatic-backup.service /etc/systemd/system/automatic-backup.service\n    systemctl daemon-reload\n    udevadm control --reload\n\nAdding backup hard drives\n-------------------------\n\nConnect your backup hard drive. Format it, if not done already.\nFind the UUID of the filesystem on which backups should be stored::\n\n    lsblk -o+uuid,label\n\nRecord the UUID in the ``/etc/backups/backup.disks`` file.\n\nMount the drive at /mnt/backup.\n\nInitialize a Borg repository at the location indicated by ``TARGET``::\n\n    borg init --encryption ... /mnt/backup/borg-backups/backup.borg\n\nUnmount and reconnect the drive, or manually start the ``automatic-backup`` service\nto start the first backup::\n\n    systemctl start --no-block automatic-backup\n\nSee backup logs using journalctl::\n\n    journalctl -fu automatic-backup [-n number-of-lines]\n\nSecurity considerations\n-----------------------\n\nThe script as shown above will mount any filesystem with a UUID listed in\n``/etc/backups/backup.disks``. The UUID check is a safety/annoyance-reduction\nmechanism to keep the script from blowing up whenever a random USB thumb drive is connected.\nIt is not meant as a security mechanism. Mounting filesystems and reading repository\ndata exposes additional attack surfaces (kernel filesystem drivers,\npossibly userspace services, and Borg itself). On the other hand, someone\nstanding right next to your computer can attempt a lot of attacks, most of which\nare easier to do than, e.g., exploiting filesystems (installing a physical keylogger,\nDMA attacks, stealing the machine, ...).\n\nBorg ensures that backups are not created on random drives that \"just happen\"\nto contain a Borg repository. If an unknown unencrypted repository is encountered,\nthen the script aborts (BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no).\n\nBackups are only created on hard drives that contain a Borg repository that is\neither known (by ID) to your machine or you are using encryption and the\npassphrase of the repository has to match the passphrase supplied to Borg.\n"
  },
  {
    "path": "docs/deployment/central-backup-server.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n.. _central-backup-server:\n\nCentral repository server with Ansible or Salt\n==============================================\n\nThis section gives an example of how to set up a Borg repository server for multiple\nclients.\n\nMachines\n--------\n\nThis section uses multiple machines, referred to by their\nrespective fully qualified domain names (FQDNs).\n\n* The backup server: `backup01.srv.local`\n* The clients:\n\n  - John Doe's desktop: `johndoe.clnt.local`\n  - Web server 01: `web01.srv.local`\n  - Application server 01: `app01.srv.local`\n\nUser and group\n--------------\n\nThe repository server should have a single UNIX user for all the clients.\nRecommended user and group with additional settings:\n\n* User: `backup`\n* Group: `backup`\n* Shell: `/bin/bash` (or another shell capable of running the `borg serve` command)\n* Home: `/home/backup`\n\nMost clients should initiate a backup as the root user to capture all\nusers, groups, and permissions (e.g., when backing up `/home`).\n\nFolders\n-------\n\nThe following directory layout is suggested on the repository server:\n\n* User home directory, /home/backup\n* Repositories path (storage pool): /home/backup/repos\n* Clients’ restricted paths (`/home/backup/repos/<client fqdn>`):\n\n  - johndoe.clnt.local: `/home/backup/repos/johndoe.clnt.local`\n  - web01.srv.local: `/home/backup/repos/web01.srv.local`\n  - app01.srv.local: `/home/backup/repos/app01.srv.local`\n\nRestrictions\n------------\n\nBorg is instructed to restrict clients into their own paths:\n``borg serve --restrict-to-path /home/backup/repos/<client fqdn>``\n\nThe client will be able to access any file or subdirectory inside of ``/home/backup/repos/<client fqdn>``\nbut no other directories. You can allow a client to access several separate directories by passing multiple\n``--restrict-to-path`` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/<client fqdn> --restrict-to-path /home/backup/repos/<other client fqdn>``,\nwhich could make sense if multiple machines belong to one person which should then have access to all the\nbackups of their machines.\n\nOnly one SSH key per client is allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and\n``app01.srv.local``. They will access the backup under a single UNIX user account as\n``backup@backup01.srv.local``. Every key in ``$HOME/.ssh/authorized_keys`` has a\nforced command and restrictions applied, as shown below:\n\n::\n\n  command=\"cd /home/backup/repos/<client fqdn>;\n           borg serve --restrict-to-path /home/backup/repos/<client fqdn>\",\n           restrict <keytype> <key> <host>\n\n.. note:: The text shown above needs to be written on a single line!\n\nThe options added to the key perform the following:\n\n1. Change working directory\n2. Run ``borg serve`` restricted to the client base path\n3. Restrict ssh and do not allow stuff which imposes a security risk\n\nBecause of the ``cd`` command, the server automatically changes the current\nworking directory. The client then does not need to know the absolute\nor relative remote repository path and can directly access the repositories at\n``ssh://<user>@<host>/./<repo>``.\n\n.. note:: The setup above ignores all client-given command line parameters\n          that are normally appended to the `borg serve` command.\n\nClient\n------\n\nThe client needs to initialize the `pictures` repository like this:\n\n::\n\n borg init ssh://backup@backup01.srv.local/./pictures\n\nOr with the full path (this should not be used in practice; it is only for demonstration purposes).\nThe server automatically changes the current working directory to the `<client fqdn>` directory.\n\n::\n\n  borg init ssh://backup@backup01.srv.local/home/backup/repos/johndoe.clnt.local/pictures\n\nWhen `johndoe.clnt.local` tries to access a path outside its restriction, the following error is raised.\nJohn Doe tries to back up into the web01 path:\n\n::\n\n  borg init ssh://backup@backup01.srv.local/home/backup/repos/web01.srv.local/pictures\n\n::\n\n  ~~~ SNIP ~~~\n  Remote: borg.remote.PathNotAllowed: /home/backup/repos/web01.srv.local/pictures\n  ~~~ SNIP ~~~\n  Repository path not allowed\n\nAnsible\n-------\n\nAnsible takes care of all the system-specific commands to add the user, create the\nfolder, install and configure software.\n\n::\n\n  - hosts: backup01.srv.local\n    vars:\n      user: backup\n      group: backup\n      home: /home/backup\n      pool: \"{{ home }}/repos\"\n      auth_users:\n        - host: johndoe.clnt.local\n          key: \"{{ lookup('file', '/path/to/keys/johndoe.clnt.local.pub') }}\"\n        - host: web01.clnt.local\n          key: \"{{ lookup('file', '/path/to/keys/web01.clnt.local.pub') }}\"\n        - host: app01.clnt.local\n          key: \"{{ lookup('file', '/path/to/keys/app01.clnt.local.pub') }}\"\n    tasks:\n    - package: name=borg state=present\n    - group: name=\"{{ group }}\" state=present\n    - user: name=\"{{ user }}\" shell=/bin/bash home=\"{{ home }}\" createhome=yes group=\"{{ group }}\" groups= state=present\n    - file: path=\"{{ home }}\" owner=\"{{ user }}\" group=\"{{ group }}\" mode=0700 state=directory\n    - file: path=\"{{ home }}/.ssh\" owner=\"{{ user }}\" group=\"{{ group }}\" mode=0700 state=directory\n    - file: path=\"{{ pool }}\" owner=\"{{ user }}\" group=\"{{ group }}\" mode=0700 state=directory\n    - authorized_key: user=\"{{ user }}\"\n                      key=\"{{ item.key }}\"\n                      key_options='command=\"cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}\",restrict'\n      with_items: \"{{ auth_users }}\"\n    - file: path=\"{{ home }}/.ssh/authorized_keys\" owner=\"{{ user }}\" group=\"{{ group }}\" mode=0600 state=file\n    - file: path=\"{{ pool }}/{{ item.host }}\" owner=\"{{ user }}\" group=\"{{ group }}\" mode=0700 state=directory\n      with_items: \"{{ auth_users }}\"\n\nSalt\n----\n\nThis is a configuration similar to the one above, configured to be deployed with\nSalt running on a Debian system.\n\n::\n\n  Install borg backup from pip:\n    pkg.installed:\n      - pkgs:\n        - python3\n        - python3-dev\n        - python3-pip\n        - python-virtualenv\n        - libssl-dev\n        - openssl\n        - libacl1-dev\n        - libacl1\n        - build-essential\n        - libfuse-dev\n        - fuse\n        - pkg-config\n    pip.installed:\n      - pkgs: [\"borgbackup\"]\n      - bin_env: /usr/bin/pip3\n\n  Setup backup user:\n    user.present:\n      - name: backup\n      - fullname: Backup User\n      - home: /home/backup\n      - shell: /bin/bash\n  # CAUTION!\n  # If you change the ssh command= option below, it won't necessarily get pushed to the backup\n  # server correctly unless you delete the ~/.ssh/authorized_keys file and re-create it!\n  {% for host in backupclients %}\n  Give backup access to {{host}}:\n    ssh_auth.present:\n      - user: backup\n      - source: salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub\n      - options:\n        - command=\"cd /home/backup/repos/{{host}}; borg serve --restrict-to-path /home/backup/repos/{{host}}\"\n        - restrict\n  {% endfor %}\n\n\nEnhancements\n------------\n\nAs this section only describes a simple and effective setup, it could be further\nenhanced when supporting (a limited set) of client supplied commands. A wrapper\nfor starting `borg serve` could be written. Or borg itself could be enhanced to\nautodetect it runs under SSH by checking the `SSH_ORIGINAL_COMMAND` environment\nvariable. This is left open for future improvements.\n\nWhen extending ssh autodetection in borg no external wrapper script is necessary\nand no other interpreter or application has to be deployed.\n\nSee also\n--------\n\n* `SSH Daemon manpage <https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8>`_\n* `Ansible <https://docs.ansible.com>`_\n* `Salt <https://docs.saltstack.com/>`_\n"
  },
  {
    "path": "docs/deployment/hosting-repositories.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n.. _hosting_repositories:\n\nHosting repositories\n====================\n\nThis section shows how to provide repository storage securely for users.\n\nRepositories are accessed through SSH. Each user of the service should\nhave their own login, which is only able to access that user's files.\nTechnically, it is possible to have multiple users share one login;\nhowever, separating them is better. Separate logins increase isolation\nand provide an additional layer of security and safety for both the\nprovider and the users.\n\nFor example, if a user manages to breach ``borg serve``, they can\nonly damage their own data (assuming that the system does not have further\nvulnerabilities).\n\nUse the standard directory structure of the operating system. Each user\nis assigned a home directory, and that user's repositories reside in their\nhome directory.\n\nThe following ``~user/.ssh/authorized_keys`` file is the most important\npiece for a correct deployment. It allows the user to log in via\ntheir public key (which must be provided by the user), and restricts\nSSH access to safe operations only.\n\n::\n\n  command=\"borg serve --restrict-to-repository /home/<user>/repository\",restrict\n  <key type> <key> <key host>\n\n.. note:: The text shown above needs to be written on a **single** line!\n\n.. warning::\n\n    If this file should be automatically updated (e.g. by a web console),\n    pay **utmost attention** to sanitizing user input. Strip all whitespace\n    around the user-supplied key, ensure that it **only** contains ASCII\n    with no control characters and that it consists of three parts separated\n    by a single space. Ensure that no newlines are contained within the key.\n\nThe ``restrict`` keyword enables all restrictions, i.e. disables port, agent\nand X11 forwarding, as well as disabling PTY allocation and execution of ~/.ssh/rc.\nIf any future restriction capabilities are added to authorized_keys\nfiles they will be included in this set.\n\nThe ``command`` keyword forces execution of the specified command\nupon login. This must be ``borg serve``. The ``--restrict-to-repository``\noption permits access to exactly **one** repository. It can be given\nmultiple times to permit access to more than one repository.\n\nThe repository may not exist yet; it can be initialized by the user,\nwhich allows for encryption.\n\nRefer to the `sshd(8) <https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8>`_\nman page for more details on SSH options.\nSee also :ref:`borg_serve`\n"
  },
  {
    "path": "docs/deployment/image-backup.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n\nBacking up entire disk images\n=============================\n\nBacking up disk images can still be efficient with Borg because its `deduplication`_\ntechnique makes sure only the modified parts of the file are stored. Borg also has\noptional simple sparse file support for extraction.\n\nIt is of utmost importance to pin down the disk you want to back up.\nUse the disk's SERIAL for that.\nUse:\n\n.. code-block:: bash\n\n    # You can find the short disk serial by:\n    # udevadm info --query=property --name=nvme1n1 | grep ID_SERIAL_SHORT | cut -d '=' -f 2\n    export BORG_REPO=/path/to/repo\n    DISK_SERIAL=\"7VS0224F\"\n    DISK_ID=$(readlink -f /dev/disk/by-id/*\"${DISK_SERIAL}\") # Returns /dev/nvme1n1\n\n    mapfile -t PARTITIONS < <(lsblk -o NAME,TYPE -p -n -l \"$DISK_ID\" | awk '$2 == \"part\" {print $1}')\n    echo \"Partitions of $DISK_ID:\"\n    echo \"${PARTITIONS[@]}\"\n    echo \"Disk Identifier: $DISK_ID\"\n\n    # Use the following line to perform a Borg backup for the full disk:\n    # borg create --read-special disk-backup \"$DISK_ID\"\n\n    # Use the following to perform a Borg backup for all partitions of the disk\n    # borg create --read-special partitions-backup \"${PARTITIONS[@]}\"\n\n    # Example output:\n    # Partitions of /dev/nvme1n1:\n    # /dev/nvme1n1p1\n    # /dev/nvme1n1p2\n    # /dev/nvme1n1p3\n    # Disk Identifier: /dev/nvme1n1\n    # borg create --read-special disk-backup /dev/nvme1n1\n    # borg create --read-special partitions-backup /dev/nvme1n1p1 /dev/nvme1n1p2 /dev/nvme1n1p3\n\n\n\n\nDecreasing the size of image backups\n------------------------------------\n\nDisk images are as large as the full disk when uncompressed and might not get much\nsmaller post-deduplication after heavy use because virtually all filesystems do not\nactually delete file data on disk but instead delete the filesystem entries referencing\nthe data. Therefore, if a disk nears capacity and files are deleted again, the change\nwill barely decrease the space it takes up when compressed and deduplicated. Depending\non the filesystem, there are several ways to decrease the size of a disk image:\n\nUsing ntfsclone (NTFS, i.e. Windows VMs)\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n``ntfsclone`` can only operate on filesystems with the journal cleared (i.e. turned-off\nmachines), which somewhat limits its utility in the case of VM snapshots. However, when\nit can be used, its special image format is even more efficient than just zeroing and\ndeduplicating. For backup, save the disk header and the contents of each partition::\n\n    HEADER_SIZE=$(sfdisk -lo Start $DISK | grep -A1 -P 'Start$' | tail -n1 | xargs echo)\n    PARTITIONS=$(sfdisk -lo Device,Type $DISK | sed -e '1,/Device\\s*Type/d')\n    dd if=$DISK count=$HEADER_SIZE | borg create --repo repo hostname-partinfo -\n    echo \"$PARTITIONS\" | grep NTFS | cut -d' ' -f1 | while read x; do\n        PARTNUM=$(echo $x | grep -Eo \"[0-9]+$\")\n        ntfsclone -so - $x | borg create --repo repo hostname-part$PARTNUM -\n    done\n    # to back up non-NTFS partitions as well:\n    echo \"$PARTITIONS\" | grep -v NTFS | cut -d' ' -f1 | while read x; do\n        PARTNUM=$(echo $x | grep -Eo \"[0-9]+$\")\n        borg create --read-special --repo repo hostname-part$PARTNUM $x\n    done\n\nRestoration is a similar process::\n\n    borg extract --stdout --repo repo hostname-partinfo | dd of=$DISK && partprobe\n    PARTITIONS=$(sfdisk -lo Device,Type $DISK | sed -e '1,/Device\\s*Type/d')\n    borg list --format {archive}{NL} repo | grep 'part[0-9]*$' | while read x; do\n        PARTNUM=$(echo $x | grep -Eo \"[0-9]+$\")\n        PARTITION=$(echo \"$PARTITIONS\" | grep -E \"$DISKp?$PARTNUM\" | head -n1)\n        if echo \"$PARTITION\" | cut -d' ' -f2- | grep -q NTFS; then\n            borg extract --stdout --repo repo $x | ntfsclone -rO $(echo \"$PARTITION\" | cut -d' ' -f1) -\n        else\n            borg extract --stdout --repo repo $x | dd of=$(echo \"$PARTITION\" | cut -d' ' -f1)\n        fi\n    done\n\n.. note::\n\n   When backing up a disk image (as opposed to a real block device), mount it as\n   a loopback image to use the above snippets::\n\n       DISK=$(losetup -Pf --show /path/to/disk/image)\n       # do backup as shown above\n       losetup -d $DISK\n\nUsing zerofree (ext2, ext3, ext4)\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n``zerofree`` works similarly to ntfsclone in that it zeros out unused chunks of the FS,\nexcept it works in place, zeroing the original partition. This makes the backup process\na bit simpler::\n\n    sfdisk -lo Device,Type $DISK | sed -e '1,/Device\\s*Type/d' | grep Linux | cut -d' ' -f1 | xargs -n1 zerofree\n    borg create --read-special --repo repo hostname-disk $DISK\n\nBecause the partitions were zeroed in place, restoration is only one command::\n\n    borg extract --stdout --repo repo hostname-disk | dd of=$DISK\n\n.. note:: The \"traditional\" way to zero out space on a partition, especially one already\n          mounted, is simply to ``dd`` from ``/dev/zero`` to a temporary file and delete\n          it. This is ill-advised for the reasons mentioned in the ``zerofree`` man page:\n\n          - it is slow.\n          - it makes the disk image (temporarily) grow to its maximal extent.\n          - it (temporarily) uses all free space on the disk, so other concurrent write actions may fail.\n\nVirtual machines\n----------------\n\nIf you use non-snapshotting backup tools like Borg to back up virtual machines, then\nthe VMs should be turned off for the duration of the backup. Backing up live VMs can\n(and will) result in corrupted or inconsistent backup contents: a VM image is just a\nregular file to Borg with the same issues as regular files when it comes to concurrent\nreading and writing from the same file.\n\nFor backing up live VMs use filesystem snapshots on the VM host, which establishes\ncrash-consistency for the VM images. This means that with most filesystems (that\nare journaling) the FS will always be fine in the backup (but may need a journal\nreplay to become accessible).\n\nUsually this does not mean that file *contents* on the VM are consistent, since file\ncontents are normally not journaled. Notable exceptions are ext4 in data=journal mode,\nZFS and btrfs (unless nodatacow is used).\n\nApplications designed with crash-consistency in mind (most relational databases like\nPostgreSQL, SQLite etc. but also for example Borg repositories) should always be able\nto recover to a consistent state from a backup created with crash-consistent snapshots\n(even on ext4 with data=writeback or XFS). Other applications may require a lot of work\nto reach application-consistency; it's a broad and complex issue that cannot be explained\nin entirety here.\n\nHypervisor snapshots capturing most of the VM's state can also be used for backups and\ncan be a better alternative to pure filesystem-based snapshots of the VM's disk, since\nno state is lost. Depending on the application this can be the easiest and most reliable\nway to create application-consistent backups.\n\nBorg does not intend to address these issues due to their huge complexity and\nplatform/software dependency. Combining Borg with the mechanisms provided by the platform\n(snapshots, hypervisor features) will be the best approach to start tackling them.\n"
  },
  {
    "path": "docs/deployment/non-root-user.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n.. _non_root_user:\n\n================================\nBacking up using a non-root user\n================================\n\nThis section describes how to run Borg as a non-root user and still be able to\nback up every file on the system.\n\nNormally, Borg is run as the root user to bypass all filesystem permissions and\nbe able to read all files. However, in theory this also allows Borg to modify or\ndelete files on your system (for example, in case of a bug).\n\nTo eliminate this possibility, we can run Borg as a non-root user and give it read-only\npermissions to all files on the system.\n\n\nUsing Linux capabilities inside a systemd service\n=================================================\n\nOne way to do so is to use Linux `capabilities\n<https://man7.org/linux/man-pages/man7/capabilities.7.html>`_ within a systemd\nservice.\n\nLinux capabilities allow us to grant parts of the root user’s privileges to\na non-root user. This works on a per-thread level and does not grant permissions\nto the non-root user as a whole.\n\nFor this, we need to run the backup script from a systemd service and use the `AmbientCapabilities\n<https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#AmbientCapabilities=>`_\noption added in systemd 229.\n\nA very basic unit file would look like this:\n\n::\n\n    [Unit]\n    Description=Borg Backup\n\n    [Service]\n    Type=oneshot\n    User=borg\n    ExecStart=/usr/local/sbin/backup.sh\n\n    AmbientCapabilities=CAP_DAC_READ_SEARCH\n\nThe ``CAP_DAC_READ_SEARCH`` capability gives Borg read-only access to all files and directories on the system.\n\nThis service can then be started manually using ``systemctl start``, a systemd timer or other methods.\n\nRestore considerations\n======================\n\nUse the root user when restoring files. If you use the non-root user, ``borg extract`` will\nchange ownership of all restored files to the non-root user. Using ``borg mount`` will not allow the\nnon-root user to access files it would not be able to access on the system itself.\n\nOther than that, you can use the same restore process you would use when running the backup as root.\n\n.. warning::\n\n    When using a local repository and running Borg commands as root, make sure to use only commands that do not\n    modify the repository itself, such as extract or mount. Modifying the repository as root will break it for the\n    non-root user, since some files inside the repository will then be owned by root.\n"
  },
  {
    "path": "docs/deployment/pull-backup.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n.. _pull_backup:\n\n=======================\nBacking up in pull mode\n=======================\n\nTypically the Borg client connects to a backup server using SSH as a transport\nwhen initiating a backup. This is referred to as push mode.\n\nHowever, if you require the backup server to initiate the connection, or prefer\nit to initiate the backup run, one of the following workarounds is required to\nallow such a pull-mode setup.\n\nA common use case for pull mode is to back up a remote server to a local personal\ncomputer.\n\nSSHFS\n=====\n\nAssume you have a pull backup system set up with Borg, where a backup server\npulls data from the target via SSHFS. In this mode, the backup client's filesystem\nis mounted remotely on the backup server. Pull mode is even possible if\nthe SSH connection must be established by the client via a remote tunnel. Other\nnetwork file systems like NFS or SMB could be used as well, but SSHFS is very\nsimple to set up and probably the most secure one.\n\nThere are some restrictions caused by SSHFS. For example, unless you define UID\nand GID mappings when mounting via ``sshfs``, owners and groups of the mounted\nfilesystem will probably change, and you may not have access to those files if\nBorg is not run with root privileges.\n\nSSHFS is a FUSE filesystem and uses the SFTP protocol, so there may also be\nunsupported features that the actual implementations of SSHFS, libfuse, and\nSFTP on the backup server do not support, like filename encodings, ACLs, xattrs,\nor flags. Therefore, there is no guarantee that you can restore a system\ncompletely in every aspect from such a backup.\n\n.. warning::\n\n    To mount the client's root filesystem you will need root access to the\n    client. This contradicts the usual threat model of Borg, where\n    clients do not need to trust the backup server (data is encrypted). In pull\n    mode the server (when logged in as root) could cause unlimited damage to the\n    client. Therefore, pull mode should be used only with servers you fully\n    trust!\n\n.. warning::\n\n    Additionally, while chrooted into the client's root filesystem,\n    code from the client will be executed. Therefore, you should do this only when\n    you fully trust the client.\n\n.. warning::\n\n    The chroot method was chosen to get the right user and group name-id\n    mappings, assuming they only come from files (/etc/passwd and group).\n    This assumption might be wrong, e.g. if users/groups also come from\n    ldap or other providers.\n    Thus, it might be better to use ``--numeric-ids`` and not archive any\n    user or group names (but just the numeric IDs) and not use chroot.\n\nCreating a backup\n-----------------\n\nGenerally, in a pull backup situation there is no direct way for borg to know\nthe client's original UID:GID name mapping of files, because Borg would use\n``/etc/passwd`` and ``/etc/group`` of the backup server to map the names. To\nderive the right names, Borg needs to have access to the client's passwd and\ngroup files and use them in the backup process.\n\nThe solution to this problem is chrooting into an sshfs mounted directory. In\nthis example the whole client root file system is mounted. We use the\nstand-alone BorgBackup executable and copy it into the mounted file system to\nmake Borg available after entering chroot; this can be skipped if Borg is\nalready installed on the client.\n\n::\n\n    # Mount client root file system.\n    mkdir /tmp/sshfs\n    sshfs root@host:/ /tmp/sshfs\n    # Mount BorgBackup repository inside it.\n    mkdir /tmp/sshfs/borgrepo\n    mount --bind /path/to/repo /tmp/sshfs/borgrepo\n    # Make borg executable available.\n    cp /usr/local/bin/borg /tmp/sshfs/usr/local/bin/borg\n    # Mount important system directories and enter chroot.\n    cd /tmp/sshfs\n    for i in dev proc sys; do mount --bind /$i $i; done\n    chroot /tmp/sshfs\n\nNow we are on the backup system but inside a chroot with the client's root file\nsystem. We have a copy of Borg binary in ``/usr/local/bin`` and the repository\nin ``/borgrepo``. Borg will back up the client's user/group names, and we can\ncreate the backup, retaining the original paths, excluding the repository:\n\n::\n\n    borg create --exclude borgrepo --files-cache ctime,size --repo /borgrepo archive  /\n\nFor the sake of simplicity only ``borgrepo`` is excluded here. You may want to\nset up an exclude file with additional files and folders to be excluded. Also\nnote that we have to modify Borg's file change detection behaviour – SSHFS\ncannot guarantee stable inode numbers, so we have to supply the\n``--files-cache`` option.\n\nFinally, we need to exit chroot, unmount all the stuff and clean up:\n\n::\n\n    exit # exit chroot\n    rm /tmp/sshfs/usr/local/bin/borg\n    cd /tmp/sshfs\n    for i in dev proc sys borgrepo; do umount ./$i; done\n    rmdir borgrepo\n    cd ~\n    umount /tmp/sshfs\n    rmdir /tmp/sshfs\n\nThanks to secuser on IRC for this how-to!\n\nRestore methods\n---------------\n\nThe counterpart of a pull backup is a push restore. Depending on the type of\nrestore – full restore or partial restore – there are different methods to make\nsure the correct IDs are restored.\n\nPartial restore\n~~~~~~~~~~~~~~~\n\nIn case of a partial restore, using the archived UIDs/GIDs might lead to wrong\nresults if the name-to-ID mapping on the target system has changed compared to\nbackup time (might be the case e.g. for a fresh OS install).\n\nThe workaround again is chrooting into an sshfs mounted directory, so Borg is\nable to map the user/group names of the backup files to the actual IDs on the\nclient. This example is similar to the backup above – only the Borg command is\ndifferent:\n\n::\n\n    # Mount client root file system.\n    mkdir /tmp/sshfs\n    sshfs root@host:/ /tmp/sshfs\n    # Mount BorgBackup repository inside it.\n    mkdir /tmp/sshfs/borgrepo\n    mount --bind /path/to/repo /tmp/sshfs/borgrepo\n    # Make borg executable available.\n    cp /usr/local/bin/borg /tmp/sshfs/usr/local/bin/borg\n    # Mount important system directories and enter chroot.\n    cd /tmp/sshfs\n    for i in dev proc sys; do mount --bind /$i $i; done\n    chroot /tmp/sshfs\n\nNow we can run\n\n::\n\n    borg extract --repo /borgrepo archive PATH\n\nto restore whatever we like partially. Finally, do the clean-up:\n\n::\n\n    exit # exit chroot\n    rm /tmp/sshfs/usr/local/bin/borg\n    cd /tmp/sshfs\n    for i in dev proc sys borgrepo; do umount ./$i; done\n    rmdir borgrepo\n    cd ~\n    umount /tmp/sshfs\n    rmdir /tmp/sshfs\n\nFull restore\n~~~~~~~~~~~~\n\nWhen doing a full restore, we restore all files (including the ones containing\nthe ID-to-name mapping, ``/etc/passwd`` and ``/etc/group``). Everything will be\nconsistent automatically if we restore the numeric IDs stored in the archive. So\nthere is no need for a chroot environment; we just mount the client file system\nand extract a backup, utilizing the ``--numeric-ids`` option:\n\n::\n\n    sshfs root@host:/ /mnt/sshfs\n    cd /mnt/sshfs\n    borg extract --numeric-ids --repo /path/to/repo archive\n    cd ~\n    umount /mnt/sshfs\n\nSimple (lossy) full restore\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nUsing ``borg export-tar`` it is possible to stream a backup to the client and\ndirectly extract it without the need of mounting with SSHFS:\n\n::\n\n    borg export-tar --repo /path/to/repo archive - | ssh root@host 'tar -C / -x'\n\nNote that in this scenario the tar format is the limiting factor – it cannot\nrestore all the advanced features that BorgBackup supports. See\n:ref:`borg_export-tar` for limitations.\n\nsocat\n=====\n\nIn this setup a SSH connection from the backup server to the client is\nestablished that uses SSH reverse port forwarding to tunnel data\ntransparently between UNIX domain sockets on the client and server and the socat\ntool to connect these with the borg client and server processes, respectively.\n\nThe program socat has to be available on the backup server and on the client\nto be backed up.\n\nWhen **pushing** a backup the borg client (holding the data to be backed up)\nconnects to the backup server via ssh, starts ``borg serve`` on the backup\nserver and communicates via standard input and output (transported via SSH)\nwith the process on the backup server.\n\nWith the help of socat this process can be reversed. The backup server will\ncreate a connection to the client (holding the data to be backed up) and will\n**pull** the data.\n\nIn the following example *borg-server* connects to *borg-client* to pull a backup.\n\nTo provide a secure setup sockets should be stored in ``/run/borg``, only\naccessible to the users that run the backup process. So on both systems,\n*borg-server* and *borg-client* the folder ``/run/borg`` has to be created::\n\n   sudo mkdir -m 0700 /run/borg\n\nOn *borg-server* the socket file is opened by the user running the ``borg\nserve`` process writing to the repository\nso the user has to have read and write permissions on ``/run/borg``::\n\n   borg-server:~$ sudo chown borgs /run/borg\n\nOn *borg-client* the socket file is created by ssh, so the user used to connect\nto *borg-client* has to have read and write permissions on ``/run/borg``::\n\n   borg-client:~$ sudo chown borgc /run/borg\n\nOn *borg-server*, we have to start the command ``borg serve`` and make its\nstandard input and output available to a unix socket::\n\n   borg-server:~$ socat UNIX-LISTEN:/run/borg/reponame.sock,fork EXEC:\"borg serve --restrict-to-path /path/to/repo\"\n\nSocat will wait until a connection is opened. Then socat will execute the\ncommand given, redirecting Standard Input and Output to the unix socket. The\noptional arguments for ``borg serve`` are not necessary but a sane default.\n\n.. note::\n   When used in production you may also use systemd socket-based activation\n   instead of socat on the server side. You would wrap the ``borg serve`` command\n   in a `service unit`_ and configure a matching `socket unit`_\n   to start the service whenever a client connects to the socket.\n\n   .. _service unit: https://www.freedesktop.org/software/systemd/man/systemd.service.html\n   .. _socket unit: https://www.freedesktop.org/software/systemd/man/systemd.socket.html\n\nNow we need a way to access the unix socket on *borg-client* (holding the\ndata to be backed up), as we created the unix socket on *borg-server*\nOpening a SSH connection from the *borg-server* to the *borg-client* with reverse port\nforwarding can do this for us::\n\n   borg-server:~$ ssh -R /run/borg/reponame.sock:/run/borg/reponame.sock borgc@borg-client\n\n.. note::\n\n   As the default value of OpenSSH for ``StreamLocalBindUnlink`` is ``no``, the\n   socket file created by sshd is not removed. Trying to connect a second time,\n   will print a short warning, and the forwarding does **not** take place::\n\n      Warning: remote port forwarding failed for listen path /run/borg/reponame.sock\n\n   When you are done, you have to remove the socket file manually, otherwise\n   you may see an error like this when trying to execute borg commands::\n\n      Remote: YYYY/MM/DD HH:MM:SS socat[XXX] E connect(5, AF=1 \"/run/borg/reponame.sock\", 13): Connection refused\n      Connection closed by remote host. Is borg working on the server?\n\n\nWhen a process opens the socket on *borg-client*, SSH will forward all\ndata to the socket on *borg-server*.\n\nThe next step is to tell borg on *borg-client* to use the unix socket to communicate with the\n``borg serve`` command on *borg-server* via the socat socket instead of SSH::\n\n   borg-client:~$ export BORG_RSH=\"sh -c 'exec socat STDIO UNIX-CONNECT:/run/borg/reponame.sock'\"\n\nThe default value for ``BORG_RSH`` is ``ssh``. By default Borg uses SSH to create\nthe connection to the backup server. Therefore Borg parses the repo URL\nand adds the server name (and other arguments) to the SSH command. Those\narguments can not be handled by socat. We wrap the command with ``sh`` to\nignore all arguments intended for the SSH command.\n\nAll Borg commands can now be executed on *borg-client*. For example to create a\nbackup execute the ``borg create`` command::\n\n   borg-client:~$ borg create --repo ssh://borg-server/path/to/repo archive /path_to_backup\n\nWhen automating backup creation, the\ninteractive ssh session may seem inappropriate. An alternative way of creating\na backup may be the following command::\n\n   borg-server:~$ ssh \\\n      -R /run/borg/reponame.sock:/run/borg/reponame.sock \\\n      borgc@borg-client \\\n      borg create \\\n      --rsh \"sh -c 'exec socat STDIO UNIX-CONNECT:/run/borg/reponame.sock'\" \\\n      --repo ssh://borg-server/path/to/repo archive /path_to_backup \\\n      ';' rm /run/borg/reponame.sock\n\nThis command also automatically removes the socket file after the ``borg\ncreate`` command is done.\n\nssh-agent\n=========\n\nIn this scenario *borg-server* initiates an SSH connection to *borg-client* and forwards the authentication\nagent connection.\n\nAfter that, it works similar to the push mode:\n*borg-client* initiates another SSH connection back to *borg-server* using the forwarded authentication agent\nconnection to authenticate itself, starts ``borg serve`` and communicates with it.\n\nUsing this method requires ssh access of user *borgs* to *borgc@borg-client*, where:\n\n* *borgs* is the user on the server side with read/write access to local borg repository.\n* *borgc* is the user on the client side with read access to files meant to be backed up.\n\nApplying this method for automated backup operations\n----------------------------------------------------\n\nAssume that the borg-client host is untrusted.\nTherefore we do some effort to prevent a hostile user on the borg-client side to do something harmful.\nIn case of a fully trusted borg-client the method could be simplified.\n\nPreparing the server side\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDo this once for each client on *borg-server* to allow *borgs* to connect itself on *borg-server* using a\ndedicated ssh key:\n\n::\n\n  borgs@borg-server$ install -m 700 -d ~/.ssh/\n  borgs@borg-server$ ssh-keygen -N '' -t rsa  -f ~/.ssh/borg-client_key\n  borgs@borg-server$ { echo -n 'command=\"borg serve --restrict-to-repo ~/repo\",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys\n  borgs@borg-server$ chmod 600 ~/.ssh/authorized_keys\n\n``install -m 700 -d ~/.ssh/``\n\n  Create directory ~/.ssh with correct permissions if it does not exist yet.\n\n``ssh-keygen -N '' -t rsa  -f ~/.ssh/borg-client_key``\n\n  Create an ssh key dedicated to communication with borg-client.\n\n.. note::\n  Another more complex approach is using a unique ssh key for each pull operation.\n  This is more secure as it guarantees that the key will not be used for other purposes.\n\n``{ echo -n 'command=\"borg serve --restrict-to-repo ~/repo\",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys``\n\n  Add borg-client's ssh public key to ~/.ssh/authorized_keys with forced command and restricted mode.\n  The borg client is restricted to use one repo at the specified path.\n\n``chmod 600 ~/.ssh/authorized_keys``\n\n  Fix permissions of ~/.ssh/authorized_keys.\n\nPull operation\n~~~~~~~~~~~~~~\n\nInitiating borg command execution from *borg-server* (e.g. init)::\n\n  borgs@borg-server$ (\n    eval $(ssh-agent) > /dev/null\n    ssh-add -q ~/.ssh/borg-client_key\n    echo 'your secure borg key passphrase' | \\\n      ssh -A -o StrictHostKeyChecking=no borgc@borg-client \"BORG_PASSPHRASE=\\$(cat) borg --rsh 'ssh -o StrictHostKeyChecking=no' init --encryption repokey ssh://borgs@borg-server/~/repo\"\n    kill \"${SSH_AGENT_PID}\"\n  )\n\nParentheses around commands are needed to avoid interference with a possibly already running ssh-agent.\nParentheses are not needed when using a dedicated bash process.\n\n``eval $(ssh-agent) > /dev/null``\n\n  Run the SSH agent in the background and export related environment variables to the current bash session.\n\n``ssh-add -q ~/.ssh/borg-client_key``\n\n  Load the SSH private key dedicated to communication with the borg-client into the SSH agent.\n  Look at ``man 1 ssh-add`` for a more detailed explanation.\n\n.. note::\n  Care needs to be taken when loading keys into the SSH agent. Users on the *borg-client* having read/write permissions\n  to the agent's UNIX-domain socket (at least borgc and root in our case) can access the agent on *borg-server* through\n  the forwarded connection and can authenticate using any of the identities loaded into the agent\n  (look at ``man 1 ssh`` for more detailed explanation). Therefore there are some security considerations:\n\n  * Private keys loaded into the agent must not be used to enable access anywhere else.\n  * The keys meant to be loaded into the agent must be specified explicitly, not from default locations.\n  * The *borg-client*'s entry in *borgs@borg-server:~/.ssh/authorized_keys* must be as restrictive as possible.\n\n``echo 'your secure borg key passphrase' | ssh -A -o StrictHostKeyChecking=no borgc@borg-client \"BORG_PASSPHRASE=\\$(cat) borg --rsh 'ssh -o StrictHostKeyChecking=no' init --encryption repokey ssh://borgs@borg-server/~/repo\"``\n\n  Run the *borg init* command on *borg-client*.\n\n  *ssh://borgs@borg-server/~/repo* refers to the repository *repo* within borgs's home directory on *borg-server*.\n\n  *StrictHostKeyChecking=no* is used to add host keys automatically to *~/.ssh/known_hosts* without user intervention.\n\n``kill \"${SSH_AGENT_PID}\"``\n\n  Kill ssh-agent with loaded keys when it is not needed anymore.\n\nRemote forwarding\n=================\n\nThe standard ssh client allows to create tunnels to forward local ports to a remote server (local forwarding) and also\nto allow remote ports to be forwarded to local ports (remote forwarding).\n\nThis remote forwarding can be used to allow remote backup clients to access the backup server even if the backup server\ncannot be reached by the backup client.\n\nThis can even be used in cases where neither the backup server can reach the backup client and the backup client cannot\nreach the backup server, but some intermediate host can access both.\n\nA schematic approach is as follows\n\n::\n\n      Backup Server (backup@mybackup)          Intermediate Machine (john@myinter)              Backup Client (bob@myclient)\n\n                                              1. Establish SSH remote forwarding  ----------->  SSH listen on local port\n\n                                                                                                2. Starting ``borg create`` establishes\n                                              3. SSH forwards to intermediate machine  <------- SSH connection to the local port\n      4. Receives backup connection <-------  and further on to backup server\n      via SSH\n\nSo for the backup client the backup is done via SSH to a local port and for the backup server there is a normal backup\nperformed via ssh.\n\nIn order to achieve this, the following commands can be used to create the remote port forwarding:\n\n1. On machine ``myinter``\n\n``ssh bob@myclient -v -C -R 8022:mybackup:22 -N``\n\nThis will listen for ssh-connections on port ``8022`` on ``myclient`` and forward connections to port 22 on ``mybackup``.\n\nYou can also remove the need for machine ``myinter`` and create the port forwarding on the backup server directly by\nusing ``localhost`` instead of ``mybackup``\n\n2. On machine ``myclient``\n\n``borg create -v --progress --stats ssh://backup@localhost:8022/home/backup/repos/myclient /``\n\nMake sure to use port ``8022`` and ``localhost`` for the repository as this instructs borg on ``myclient`` to use the\nremote forwarded ssh connection.\n\nSSH Keys\n--------\n\nIf you want to automate backups when using this method, the ssh ``known_hosts`` and ``authorized_keys`` need to be set up\nto allow connections.\n\nSecurity Considerations\n-----------------------\n\nOpening up SSH access this way can pose a security risk as it effectively opens remote access to your\nbackup server on the client even if it is located outside of your company network.\n\nTo reduce the chances of compromise, you should configure a forced command in ``authorized_keys`` to prevent\nanyone from performing any other action on the backup server.\n\nThis can be done e.g. by adding the following in ``$HOME/.ssh/authorized_keys`` on ``mybackup`` with proper\npath and client-fqdn:\n\n::\n\n  command=\"cd /home/backup/repos/<client fqdn>;borg serve --restrict-to-path /home/backup/repos/<client fqdn>\"\n\n\nAll the additional security considerations for borg should be applied, see :ref:`central-backup-server` for some additional\nhints.\n\nMore information\n----------------\n\nSee `remote forwarding`_ and the `ssh man page`_ for more information about remote forwarding.\n\n   .. _remote forwarding: https://linuxize.com/post/how-to-setup-ssh-tunneling/\n   .. _ssh man page: https://manpages.debian.org/testing/manpages-de/ssh.1.de.html\n"
  },
  {
    "path": "docs/deployment.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: none\n\nDeployment\n==========\n\nThis chapter details deployment strategies for the following scenarios.\n\n.. toctree::\n   :titlesonly:\n\n   deployment/central-backup-server\n   deployment/hosting-repositories\n   deployment/automated-local\n   deployment/image-backup\n   deployment/pull-backup\n   deployment/non-root-user\n"
  },
  {
    "path": "docs/development.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: bash\n.. _development:\n\nDevelopment\n===========\n\nThis chapter will get you started with Borg development.\n\nBorg is written in Python (with a little bit of Cython and C for\nthe performance-critical parts).\n\nContributions\n-------------\n\n... are welcome!\n\nSome guidance for contributors:\n\n- Discuss changes on the GitHub issue tracker, on IRC or on the mailing list.\n\n- Make your PRs on the ``master`` branch (see `Branching Model`_ for details and exceptions).\n\n- Do clean changesets:\n\n  - Focus on some topic, resist changing anything else.\n  - Do not do style changes mixed with functional changes.\n  - Try to avoid refactorings mixed with functional changes.\n  - If you need to fix something after commit/push:\n\n    - If there are ongoing reviews: do a fixup commit you can\n      squash into the bad commit later.\n    - If there are no ongoing reviews or you did not push the\n      bad commit yet: amend the commit to include your fix or\n      merge the fixup commit before pushing.\n  - Have a nice, clear, typo-free commit comment.\n  - If you fixed an issue, refer to it in your commit comment.\n  - Follow the style guide (see below).\n\n- If you write new code, please add tests and docs for it.\n\n- Run the tests, fix any issues that come up.\n\n- Make a pull request on GitHub.\n\n- Wait for review by other developers.\n\nBranching model\n---------------\n\nBorg development happens on the ``master`` branch and uses GitHub pull\nrequests (if you don't have GitHub or don't want to use it you can\nsend smaller patches via the borgbackup mailing list to the maintainers).\n\nStable releases are maintained on maintenance branches named ``x.y-maint``, e.g.,\nthe maintenance branch of the 1.4.x series is ``1.4-maint``.\n\nMost PRs should be filed against the ``master`` branch. Only if an\nissue affects **only** a particular maintenance branch a PR should be\nfiled against it directly.\n\nWhile discussing/reviewing a PR it will be decided whether the\nchange should be applied to maintenance branches. Each maintenance\nbranch has a corresponding *backport/x.y-maint* label, which will then\nbe applied.\n\nChanges that are typically considered for backporting:\n\n- Data loss, corruption and inaccessibility fixes.\n- Security fixes.\n- Forward-compatibility improvements.\n- Documentation corrections.\n\n.. rubric:: Maintainer part\n\nFrom time to time a maintainer will backport the changes for a\nmaintenance branch, typically before a release or if enough changes\nwere collected:\n\n1. Notify others that you're doing this to avoid duplicate work.\n2. Branch a backporting branch off the maintenance branch.\n3. Cherry pick and backport the changes from each labelled PR, remove\n   the label for each PR you've backported.\n\n   To preserve authorship metadata, do not follow the ``git cherry-pick``\n   instructions to use ``git commit`` after resolving conflicts. Instead,\n   stage conflict resolutions and run ``git cherry-pick --continue``,\n   much like using ``git rebase``.\n\n   To avoid merge issues (a cherry pick is a form of merge), use\n   these options (similar to the ``git merge`` options used previously,\n   the ``-x`` option adds a reference to the original commit)::\n\n     git cherry-pick --strategy recursive -X rename-threshold=5% -x\n\n4. Make a PR of the backporting branch against the maintenance branch\n   for backport review. Mention the backported PRs in this PR, e.g.:\n\n       Includes changes from #2055 #2057 #2381\n\n   This way GitHub will automatically show in these PRs where they\n   were backported.\n\n.. rubric:: Historic model\n\nPreviously (until release 1.0.10) Borg used a `\"merge upwards\"\n<https://git-scm.com/docs/gitworkflows#_merging_upwards>`_ model where\nmost minor changes and fixes were committed to a maintenance branch\n(e.g. 1.0-maint), and the maintenance branch(es) were regularly merged\nback into the main development branch. This became more and more\ntroublesome due to merges growing more conflict-heavy and error-prone.\n\nHow to submit a pull request\n----------------------------\n\nIn order to contribute to Borg, you will need to fork the ``borgbackup/borg``\nmain repository to your own Github repository. Then clone your Github repository\nto your local machine. The instructions for forking and cloning a repository\ncan be found there:\n`<https://docs.github.com/en/get-started/quickstart/fork-a-repo>`_ .\n\nMake sure you also fetched the git tags, because without them, ``setuptools-scm``\nwill run into issues determining the correct borg version. Check if ``git tag``\nshows a lot of release tags (version numbers).\nIf it does not, use ``git fetch --tags`` to fetch them.\n\nTo work on your contribution, you first need to decide which branch your pull\nrequest should be against. Often, this might be master branch (esp. for big /\nrisky contributions), but it could be also a maintenance branch like e.g.\n1.4-maint (esp. for small fixes that should go into next maintenance release,\ne.g. 1.4.x).\n\nStart by checking out the appropriate branch:\n::\n\n    git checkout master\n\nIt is best practice for a developer to keep local ``master`` branch as an\nup-to-date copy of the upstream ``master`` branch and always do own work in a\nseparate feature or bugfix branch.\nThis is useful to be able to rebase own branches onto the upstream branches\nthey were branched from, if necessary.\n\nThis also applies to other upstream branches (like e.g. ``1.4-maint``), not\nonly to ``master``.\n\nThus, create a new branch now:\n::\n\n    git checkout -b MYCONTRIB-master  # choose an appropriate own branch name\n\nNow, work on your contribution in that branch. Use these git commands:\n::\n\n    git status   # is there anything that needs to be added?\n    git add ...  # if so, add it\n    git commit   # finally, commit it. use a descriptive comment.\n\nThen push the changes to your Github repository:\n::\n\n    git push --set-upstream origin MYCONTRIB-master\n\nFinally, make a pull request on ``borgbackup/borg`` Github repository against\nthe appropriate branch (e.g. ``master``) so that your changes can be reviewed.\n\nWhat to do if work was accidentally started in wrong branch\n-----------------------------------------------------------\n\nIf you accidentally worked in ``master`` branch, check out the ``master``\nbranch and make sure there are no uncommitted changes. Then, create a feature\nbranch from that, so that your contribution is in a feature branch.\n::\n\n    git checkout master\n    git checkout -b MYCONTRIB-master\n\nNext, check out the ``master`` branch again. Find the commit hash of the last\ncommit that was made before you started working on your contribution and perform\na hard reset.\n::\n\n    git checkout master\n    git log\n    git reset --hard THATHASH\n\nThen, update the local ``master`` branch with changes made in the upstream\nrepository.\n::\n\n    git pull borg master\n\nRebase feature branch onto updated master branch\n------------------------------------------------\n\nAfter updating the local ``master`` branch from upstream, the feature branch\ncan be checked out and rebased onto (the now up-to-date) ``master`` branch.\n::\n\n    git checkout MYCONTRIB-master\n    git rebase -i master\n\nNext, check if there are any commits that exist in the feature branch\nbut not in the ``master`` branch and vice versa. If there are no\nconflicts or after resolving them, push your changes to your Github repository.\n::\n\n    git log\n    git diff master\n    git push -f\n\nCode and issues\n---------------\n\nCode is stored on GitHub, in the `Borgbackup organization\n<https://github.com/borgbackup/borg/>`_. `Issues\n<https://github.com/borgbackup/borg/issues>`_ and `pull requests\n<https://github.com/borgbackup/borg/pulls>`_ should be sent there as\nwell. See also the :ref:`support` section for more details.\n\nStyle guide / Automated Code Formatting\n---------------------------------------\n\nWe use `black`_ for automatically formatting the code.\n\nIf you work on the code, it is recommended that you run black **before each commit**\n(so that new code is always using the desired formatting and no additional commits\nare required to fix the formatting).\n::\n\n    pip install -r requirements.d/codestyle.txt     # everybody use same black version\n    black --check .                                 # only check, don't change\n    black .                                         # reformat the code\n\n\nThe CI workflows will check the code formatting and will fail if it is not formatted correctly.\n\nWhen (mass-)reformatting existing code, we need to avoid ruining `git blame`, so please\nfollow their `guide about avoiding ruining git blame`_:\n\n.. _black: https://black.readthedocs.io/\n.. _guide about avoiding ruining git blame: https://black.readthedocs.io/en/stable/guides/introducing_black_to_your_project.html#avoiding-ruining-git-blame\n\nContinuous Integration\n----------------------\n\nAll pull requests go through `GitHub Actions`_, which runs the tests on misc.\nPython versions and on misc. platforms as well as some additional checks.\n\n.. _GitHub Actions: https://github.com/borgbackup/borg/actions\n\nOutput and Logging\n------------------\nWhen writing logger calls, always use correct log level (debug only for\ndebugging, info for informative messages, warning for warnings, error for\nerrors, critical for critical errors/states).\n\nWhen directly talking to the user (e.g. Y/N questions), do not use logging,\nbut directly output to stderr (not: stdout, it could be connected to a pipe).\n\nTo control the amount and kinds of messages output emitted at info level, use\nflags like ``--stats`` or ``--list``, then create a topic logger for messages\ncontrolled by that flag.  See ``_setup_implied_logging()`` in\n``borg/archiver.py`` for the entry point to topic logging.\n\nBuilding a development environment\n----------------------------------\n\nFirst, just install borg into a virtual env :ref:`as described before <git-installation>`.\n\nTo install some additional packages needed for running the tests, activate your\nvirtual env and run::\n\n  pip install -r requirements.d/development.lock.txt\n\n\nThis project utilizes pre-commit to format and lint code before it is committed.\nAlthough pre-commit is installed when running the command above, the pre-commit hooks\nwill have to be installed separately. Run this command to install the pre-commit hooks::\n\n  pre-commit install\n\nRunning the tests\n-----------------\n\nThe tests are in the borg/testsuite package.\n\nTo run all the tests, you need to have fakeroot installed. If you do not have\nfakeroot, you still will be able to run most tests, just leave away the\n``fakeroot -u`` from the given command lines.\n\nTo run the test suite use the following command::\n\n  fakeroot -u tox  # run all tests\n\nSome more advanced examples::\n\n  # verify a changed tox.ini (run this after any change to tox.ini):\n  fakeroot -u tox --recreate\n\n  fakeroot -u tox -e py313  # run all tests, but only on python 3.13\n\n  fakeroot -u tox borg.testsuite.locking  # only run 1 test module\n\n  fakeroot -u tox borg.testsuite.locking -- -k '\"not Timer\"'  # exclude some tests\n\n  fakeroot -u tox borg.testsuite -- -v  # verbose py.test\n\nImportant notes:\n\n- When using ``--`` to give options to py.test, you MUST also give ``borg.testsuite[.module]``.\n\nRunning the tests (using the pypi package)\n------------------------------------------\n\nSince borg 1.4, it is also possible to run the tests without a development\nenvironment, using the borgbackup dist package (downloaded from pypi.org or\ngithub releases page):\n::\n\n    # optional: create and use a virtual env:\n    python3 -m venv env\n    . env/bin/activate\n\n    # install packages\n    pip install borgbackup\n    pip install pytest pytest-benchmark\n\n    # run the tests\n    pytest -v -rs --benchmark-skip --pyargs borg.testsuite\n\nAdding a compression algorithm\n------------------------------\n\nIf you want to add a new compression algorithm, please refer to :issue:`1633`\nand leave a post there in order to discuss about the proposal.\n\nDocumentation\n-------------\n\nGenerated files\n~~~~~~~~~~~~~~~\n\nUsage documentation (found in ``docs/usage/``) and man pages\n(``docs/man/``) are generated automatically from the command line\nparsers declared in the program and their documentation, which is\nembedded in the program (see archiver.py). These are committed to git\nfor easier use by packagers downstream.\n\nWhen a command is added, a command line flag changed, added or removed,\nthe usage docs need to be rebuilt as well::\n\n  python scripts/make.py build_usage\n  python scripts/make.py build_man\n\nHowever, we prefer to do this as part of our :ref:`releasing`\npreparations, so it is generally not necessary to update these when\nsubmitting patches that change something about the command line.\n\nBuilding the docs with Sphinx\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe documentation (in reStructuredText format, .rst) is in docs/.\n\nTo build the html version of it, you need to have Sphinx installed\n(in your Borg virtualenv with Python 3)::\n\n  pip install -r requirements.d/docs.txt\n\nNow run::\n\n  cd docs/\n  make html\n\nThen point a web browser at docs/_build/html/index.html.\n\nThe website is updated automatically by ReadTheDocs through GitHub web hooks on the\nmain repository.\n\nUsing Vagrant\n-------------\n\nWe use Vagrant for the automated creation of testing environments and borgbackup\nstandalone binaries for various platforms.\n\nFor better security, there is no automatic sync in the VM to host direction.\nThe plugin `vagrant-scp` is useful to copy stuff from the VMs to the host.\n\nThe \"windows10\" box requires the `reload` plugin (``vagrant plugin install vagrant-reload``).\n\nUsage::\n\n   # To create and provision the VM:\n   vagrant up OS\n   # same, but use 6 VM cpus and 12 workers for pytest:\n   VMCPUS=6 XDISTN=12 vagrant up OS\n   # To create an ssh session to the VM:\n   vagrant ssh OS\n   # To execute a command via ssh in the VM:\n   vagrant ssh OS -c \"command args\"\n   # To shut down the VM:\n   vagrant halt OS\n   # To shut down and destroy the VM:\n   vagrant destroy OS\n   # To copy files from the VM (in this case, the generated binary):\n   vagrant scp OS:/vagrant/borg/borg.exe .\n\nUsing Podman\n------------\n\nmacOS-based developers (and others who prefer containers) can run the Linux test suite locally using Podman.\n\nPrerequisites:\n\n-   Install Podman (e.g., ``brew install podman``).\n-   Initialize the Podman machine, only once: ``podman machine init``.\n-   Start the Podman machine, before using it: ``podman machine start``.\n\nUsage::\n\n    # Open an interactive shell in the container (default if no command given):\n    ./scripts/linux-run\n\n    # Run the default tox environment:\n    ./scripts/linux-run tox\n\n    # Run a specific tox environment:\n    ./scripts/linux-run tox -e py311-pyfuse3\n\n    # Pass arguments to pytest (e.g., run specific tests):\n    ./scripts/linux-run tox -e py313-pyfuse3 -- -k mount\n\n    # Switch base image (temporarily):\n    ./scripts/linux-run --image python:3.11-bookworm tox\n\nResource Usage\n~~~~~~~~~~~~~~\n\nThe default Podman VM uses 2GB RAM and half your CPUs.\nFor heavy tests (parallel execution), this might be tight.\n\n-   **Check usage:** Run ``podman stats`` in another terminal while tests are running.\n-   **Increase resources:**\n\n    ::\n\n        podman machine stop\n        podman machine set --cpus 6 --memory 4096\n        podman machine start\n\n\nCreating standalone binaries\n----------------------------\n\nMake sure you have everything built and installed (including fuse stuff).\nWhen using the Vagrant VMs, pyinstaller will already be installed.\n\nWith virtual env activated::\n\n  pip install pyinstaller  # or git checkout master\n  pyinstaller -F -n borg-PLATFORM borg/__main__.py\n  for file in dist/borg-*; do gpg --armor --detach-sign $file; done\n\nIf you encounter issues, see also our `Vagrantfile` for details.\n\n.. note:: Standalone binaries built with pyinstaller are supposed to\n          work on same OS, same architecture (x86 32bit, amd64 64bit)\n          without external dependencies.\n\n.. _releasing:\n\nCreating a new release\n----------------------\n\nChecklist:\n\n- Make sure all issues for this milestone are closed or moved to the\n  next milestone.\n- Check if there are any pending fixes for security issues.\n- Find and fix any low hanging fruit left on the issue tracker.\n- Check that GitHub Actions CI is happy.\n- Update ``CHANGES.rst``, based on ``git log $PREVIOUS_RELEASE..``.\n- Check version number of upcoming release in ``CHANGES.rst``.\n- Render ``CHANGES.rst`` via ``make html`` and check for markup errors.\n- Verify that ``MANIFEST.in``, ``pyproject.toml`` and ``setup.py`` are complete.\n- Run these commands, check git status for files that might need to be added, and commit::\n\n    python scripts/make.py build_usage\n    python scripts/make.py build_man\n\n- Tag the release::\n\n    git tag -s -m \"tagged/signed release X.Y.Z\" X.Y.Z\n\n- Push the release PR branch to GitHub, make a pull request.\n- Also push the release tag.\n- Create a clean repo and use it for the following steps::\n\n    git clone borg borg-clean\n\n  This makes sure no uncommitted files get into the release archive.\n  It will also reveal uncommitted required files.\n  Moreover, it makes sure the vagrant machines only get committed files and\n  do a fresh start based on that.\n- Optional: run tox and/or binary builds on all supported platforms via vagrant,\n  check for test failures. This is now optional as we do platform testing and\n  binary building on GitHub.\n- Create sdist, sign it, upload release to (test) PyPi:\n\n  ::\n\n    scripts/sdist-sign X.Y.Z\n    scripts/upload-pypi X.Y.Z test\n    scripts/upload-pypi X.Y.Z\n\n  Note: the signature is not uploaded to PyPi any more, but we upload it to\n  github releases.\n- When GitHub CI looks good on the release PR, merge it and then check \"Actions\":\n  GitHub will create binary assets after the release PR is merged within the\n  CI testing of the merge. Check the \"Upload binaries\" step on Ubuntu (AMD/Intel\n  and ARM64) and macOS (Intel and ARM64), fetch the ZIPs with the binaries.\n- Unpack the ZIPs and test the binaries, upload the binaries to the GitHub\n  release page (borg-OS-SPEC-ARCH-gh and borg-OS-SPEC-ARCH-gh.tgz).\n\n- Close the release milestone on GitHub.\n- `Update borgbackup.org\n  <https://github.com/borgbackup/borgbackup.github.io/pull/53/files>`_ with the\n  new version number and release date.\n- Announce on:\n\n  - Mailing list.\n  - Mastodon / BlueSky / X (aka Twitter).\n  - IRC channel (change ``/topic``).\n\n- Create a GitHub release, include:\n\n  - pypi dist package and signature\n  - Standalone binaries (see above for how to create them).\n\n    - For macOS binaries **with** FUSE support, document the macFUSE version\n      in the README of the binaries. macFUSE uses a kernel extension that needs\n      to be compatible with the code contained in the binary.\n  - A link to ``CHANGES.rst``.\n"
  },
  {
    "path": "docs/faq.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: none\n.. _faq:\n\nFrequently asked questions\n==========================\n\nUsage & Limitations\n###################\n\nWhat is the difference between a repository on an external hard drive and a repository on a server?\n---------------------------------------------------------------------------------------------------\n\nIf Borg is running in client/server mode, the client uses SSH as a transport to\ntalk to a remote agent, which is another Borg process (Borg is also installed on\nthe server) started automatically by the client. The Borg server performs\nstorage-related, low-level repository operations (list, load, and store objects),\nwhile the Borg client does the high-level stuff: deduplication, encryption,\ncompression, dealing with archives, backups, restores, etc., which reduces the\namount of data that goes over the network.\n\nWhen Borg is writing to a repo on a locally mounted remote filesystem, e.g.\nSSHFS, the Borg client can only perform filesystem operations and has no agent\nrunning on the remote side, so *every* operation needs to go over the network,\nwhich is slower.\n\nCan I back up from multiple servers into a single repository?\n-------------------------------------------------------------\n\nYes, you can! Even simultaneously.\n\nCan I back up to multiple swapped backup targets?\n--------------------------------------------------\n\nIt is possible to swap your backup disks if each backup medium is assigned its\nown repository by creating a new one with :ref:`borg_repo-create`.\n\nCan I copy or synchronize my repo to another location?\n------------------------------------------------------\n\nIf you want to have redundant backup repositories (preferably at separate\nlocations), the recommended way to do that is like this:\n\n- ``borg repo-create repo1 --encryption=X``\n- ``borg repo-create repo2 --encryption=X --other-repo=repo1``\n- Optionally, create a snapshot to have stable and identical input data for both borg create runs.\n- client machine ---borg create---> repo1\n- client machine ---borg create---> repo2\n\nThis will create distinct (different repository ID) but related repositories.\nRelated means using the same chunker secret and the same id_key, thus producing\nthe same chunks / the same chunk IDs if the input data is the same.\n\nThe two independent borg create invocations mean there is no error propagation\nfrom repo1 to repo2 when done like that.\n\nAn alternative is to use ``borg transfer`` to copy backup archives\nfrom repo1 to repo2. This is likely a bit more efficient and the archives would be identical,\nbut it may suffer from potential error propagation.\n\nWarning: Using Borg with multiple repositories that have identical repository IDs (such as\ncreating 1:1 repository copies) is not supported and can lead to various issues,\nfor example cache coherency issues, malfunction, or data corruption.\n\n\"this is either an attack or unsafe\" warning\n--------------------------------------------\n\nAbout the warning:\n\n  Cache or information obtained from the security directory is newer than the\n  repository — this is either an attack or unsafe (multiple repositories with the same ID)\n\n\"unsafe\": If not following the advice from the previous section, you can easily\nrun into this by yourself by restoring an older copy of your repository.\n\n\"attack\": An attacker may have replaced your repo with an older copy, trying to\ntrigger AES counter reuse and break your repo encryption.\n\nBorg users have also reported that file system issues (e.g., hardware issues or I/O errors causing\nthe file system to become read-only) can cause this warning, see :issue:`7853`.\n\nIf you decide to ignore this and accept unsafe operation for this repository,\nyou could delete the manifest-timestamp and the local cache:\n\n::\n\n  borg config id   # shows the REPO_ID\n  rm ~/.config/borg/security/REPO_ID/manifest-timestamp\n  borg repo-delete --cache-only\n\nThis is an unsafe and unsupported way to use Borg. You have been warned.\n\nWhich file types, attributes, etc. are *not* preserved?\n-------------------------------------------------------\n\n    * UNIX domain sockets (because it does not make sense - they are\n      meaningless without the running process that created them and the process\n      needs to recreate them in any case). So, don't panic if your backup\n      misses a UDS!\n    * The precise on-disk (or rather: not-on-disk) representation of the holes\n      in a sparse file.\n      Archive creation has no special support for sparse files, holes are\n      backed up as (deduplicated and compressed) runs of zero bytes.\n      Archive extraction has optional support to extract all-zero chunks as\n      holes in a sparse file.\n    * Some filesystem specific attributes, like btrfs NOCOW, see :ref:`platforms`.\n\nAre there other known limitations?\n----------------------------------\n\n- borg extract supports restoring only into an empty destination. After extraction,\n  the destination will have exactly the contents of the extracted archive.\n  If you extract into a non-empty destination, borg will (for example) not\n  remove files which are in the destination, but not in the archive.\n  See :issue:`4598` for a workaround and more details.\n\n.. _interrupted_backup:\n\nIf a backup stops mid-way, does the already-backed-up data stay there?\n----------------------------------------------------------------------\n\nYes, the data transferred into the repo stays there - just avoid running\n``borg compact`` before you completed the backup, because that would remove\nchunks that were already transferred to the repo, but not (yet) referenced\nby an archive.\n\nIf a backup was interrupted, you normally do not need to do anything special,\njust invoke ``borg create`` as you always do. You may use the same archive name\nas in previous attempt or a different one (e.g. if you always include the\ncurrent datetime), it does not matter.\n\nBorg always does full single-pass backups, so it will start again\nfrom the beginning - but it will be much faster, because some of the data was\nalready stored into the repo, so it does not need to get transmitted and stored\nagain.\n\n\nHow can I back up huge file(s) over an unstable connection?\n-----------------------------------------------------------\n\nYes. For more details, see :ref:`interrupted_backup`.\n\nHow can I restore huge file(s) over an unstable connection?\n-----------------------------------------------------------\n\nTry using ``borg mount`` and ``rsync`` (or a similar tool that supports\nresuming a partial file copy from what's already copied).\n\nMy machine goes to sleep causing `Broken pipe`\n----------------------------------------------\n\nWhile backing up your data over the network, your machine should not go to sleep.\nOn Linux you can use `systemd-inhibit` to avoid that. On macOS you can use `caffeinate`.\n\n``systemd-inhibit borg create ...``\n\n``caffeinate -i borg create ...``\n\nHow can I compare contents of an archive to my local filesystem?\n-----------------------------------------------------------------\n\nYou can instruct ``export-tar`` to send a tar stream to the stdout, and\nthen use ``tar`` to perform the comparison:\n\n::\n\n    borg export-tar archive-name - | tar --compare -f - -C /path/to/compare/to\n\n\nCan Borg add redundancy to the backup data to deal with hardware malfunction?\n-----------------------------------------------------------------------------\n\nNo, it can't. While that at first sounds like a good idea to defend against\nsome defect HDD sectors or SSD flash blocks, dealing with this in a\nreliable way needs a lot of low-level storage layout information and\ncontrol which we do not have (and also can't get, even if we wanted).\n\nSo, if you need that, consider RAID or a filesystem that offers redundant\nstorage or just make backups to different locations / different hardware.\n\nSee also :issue:`225`.\n\nCan Borg verify data integrity of a backup archive?\n---------------------------------------------------\n\nYes, if you want to detect accidental data damage (like bit rot), use the\n``check`` operation. It will notice corruption using CRCs and hashes.\nIf you want to be able to detect malicious tampering also, use an encrypted\nrepo. It will then be able to check using CRCs and HMACs.\n\n.. _faq-integrityerror:\n\nI get an IntegrityError or similar - what now?\n----------------------------------------------\n\nA single error does not necessarily indicate bad hardware or a Borg\nbug. All hardware exhibits a bit error rate (BER). Hard drives are typically\nspecified as exhibiting fewer than one error every 12 to 120 TB\n(one bit error in 10e14 to 10e15 bits). The specification is often called\n*unrecoverable read error rate* (URE rate).\n\nApart from these very rare errors there are two main causes of errors:\n\n(i) Defective hardware: described below.\n(ii) Bugs in software (Borg, operating system, libraries):\n     Ensure software is up to date.\n     Check whether the issue is caused by any fixed bugs described in\n     :ref:`important_notes`.\n\n.. rubric:: Finding defective hardware\n\n.. note::\n\n   Hardware diagnostics are operating system dependent and do not\n   apply universally. The commands shown apply for popular Unix-like\n   systems. Refer to your operating system's manual.\n\nChecking hard drives\n  Find the drive containing the repository and use *findmnt*, *mount* or *lsblk*\n  to learn the device path (typically */dev/...*) of the drive.\n  Then, smartmontools can retrieve self-diagnostics of the drive in question::\n\n      # smartctl -a /dev/sdSomething\n\n  The *Offline_Uncorrectable*, *Current_Pending_Sector* and *Reported_Uncorrect*\n  attributes indicate data corruption. A high *UDMA_CRC_Error_Count* usually\n  indicates a bad cable.\n\n  I/O errors logged by the system (refer to the system journal or\n  dmesg) can point to issues as well. I/O errors only affecting the\n  file system easily go unnoticed, since they are not reported to\n  applications (e.g. Borg), while these errors can still corrupt data.\n\n  Drives can corrupt some sectors in one event, while remaining\n  reliable otherwise. Conversely, drives can fail completely with no\n  advance warning. If in doubt, copy all data from the drive in\n  question to another drive -- just in case it fails completely.\n\n  If any of these are suspicious, a self-test is recommended::\n\n      # smartctl -t long /dev/sdSomething\n\n  Running ``fsck`` if not done already might yield further insights.\n\nChecking memory\n  Intermittent issues, such as ``borg check`` finding errors\n  inconsistently between runs, are frequently caused by bad memory.\n\n  Run memtest86+ (or an equivalent memory tester) to verify that\n  the memory subsystem is operating correctly.\n\nChecking processors\n  Processors rarely cause errors. If they do, they are usually overclocked\n  or otherwise operated outside their specifications. We do not recommend to\n  operate hardware outside its specifications for productive use.\n\n  Tools to verify correct processor operation include Prime95 (mprime), linpack,\n  and stress-ng.\n\n.. rubric:: Repairing a damaged repository\n\nWith any defective hardware found and replaced, the damage done to the repository\nneeds to be ascertained and fixed.\n\n:ref:`borg_check` provides diagnostics and ``--repair`` options for repositories with\nissues. We recommend to first run without ``--repair`` to assess the situation.\nIf the found issues and proposed repairs seem right, re-run \"check\" with ``--repair`` enabled.\n\nHow probable is it to get a hash collision problem?\n---------------------------------------------------\n\nIf you noticed, there are some issues (:issue:`170` (**warning: hell**) and :issue:`4884`)\nabout the probability of a chunk having the same hash as another chunk, making the file\ncorrupted because it grabbed the wrong chunk. This is called the `Birthday Problem\n<https://en.wikipedia.org/wiki/Birthday_problem>`_.\n\nThere is a lot of probability in here so, I can give you my interpretation of\nsuch math but it's honestly better that you read it yourself and grab your own\nresolution from that.\n\nAssuming that all your chunks have a size of :math:`2^{21}` bytes (approximately 2.1 MB)\nand we have a \"perfect\" hash algorithm, we can think that the probability of collision\nwould be of :math:`p^2/2^{n+1}` then, using SHA-256 (:math:`n=256`) and for example\nwe have 1000 million chunks (:math:`p=10^9`) (1000 million chunks would be about 2100TB).\nThe probability would be around 0.0000000000000000000000000000000000000000000000000000000000043.\n\nA mass-murderer space rock happens about once every 30 million years on average.\nThis leads to a probability of such an event occurring in the next second to about :math:`10^{-15}`.\nThat's **45** orders of magnitude more probable than the SHA-256 collision. Briefly stated,\nif you find SHA-256 collisions scary then your priorities are wrong. This example was grabbed from\n`this SO answer <https://stackoverflow.com/a/4014407/13359375>`_, it's great honestly.\n\nStill, the real question is whether Borg tries not to make this happen?\n\nWell... previously it did not check anything until there was a feature added which saves the size\nof the chunks too, so the size of the chunks is compared to the size that you got with the\nhash and if the check says there is a mismatch it will raise an exception instead of corrupting\nthe file. This doesn't save us from everything but reduces the chances of corruption.\nThere are other ways of trying to escape this but it would affect performance so much that\nit wouldn't be worth it and it would contradict Borg's design, so if you don't want this to\nhappen, simply don't use Borg.\n\nWhy is the time elapsed in the archive stats different from wall clock time?\n----------------------------------------------------------------------------\n\nBorg needs to write the time elapsed into the archive metadata before finalizing\nthe archive and saving the files cache.\nThis means when Borg is run with e.g. the ``time`` command, the duration shown\nin the archive stats may be shorter than the full time the command runs for.\n\nHow do I configure different prune policies for different directories?\n----------------------------------------------------------------------\n\nSay you want to prune ``/var/log`` faster than the rest of\n``/``. How do we implement that? The answer is to back up to different\narchive *series* and then implement different prune policies for the\ndifferent series. For example, you could have a script that does::\n\n    borg create --exclude var/log main /\n    borg create logs /var/log\n\nThen you would have two different prune calls with different policies::\n\n    borg prune --verbose --list -d 30 main\n    borg prune --verbose --list -d 7  logs\n\nThis will keep 7 days of logs and 30 days of everything else.\n\nHow do I remove files from an existing backup?\n----------------------------------------------\n\nA file is only removed from a BorgBackup repository if all archives that contain\nthe file are deleted and the corresponding data chunks are removed from the\nrepository. There are two ways how to remove files from a repository.\n\n1. Use :ref:`borg_delete` to remove all archives that contain the files. This\nwill of course delete everything in the archive, not only some files.\n\n2. If you really want to remove only some specific files, you can run the\n:ref:`borg_recreate` command to rewrite all archives with a different\n``--exclude`` pattern. See the examples in the manpage for more information.\n\nFinally, run :ref:`borg_compact` to delete the data chunks from the repository.\n\nCan I safely change the compression level or algorithm?\n--------------------------------------------------------\n\nThe compression level and algorithm don't affect deduplication. Chunk ID hashes\nare calculated *before* compression. New compression settings\nwill only be applied to new chunks, not existing chunks. So it's safe\nto change them.\n\nUse ``borg repo-compress`` to efficiently recompress a complete repository.\n\nWhy is backing up an unmodified FAT filesystem slow on Linux?\n-------------------------------------------------------------\n\nBy default, the files cache used by BorgBackup considers the inode of files.\nWhen an inode number changes compared to the last backup, it hashes the file\nagain. The ``vfat`` kernel driver does not produce stable inode numbers by\ndefault.  One way to achieve stable inode numbering is mounting the filesystem\nusing ``nfs=nostale_ro``. Doing so implies mounting the filesystem read-only.\nAnother option is to not consider inode numbers in the files cache by passing\n``--files-cache=ctime,size``.\n\nWhy are backups slow on a Linux server that is a member of a Windows domain?\n----------------------------------------------------------------------------\n\nIf a Linux server is a member of a Windows domain, username to userid resolution might be\nperformed via ``winbind`` without caching, which can slow down backups significantly.\nYou can use e.g. ``nscd`` to add caching and improve the speed.\n\nSecurity\n########\n\n.. _home_config_borg:\n\nHow important is the borg config directory?\n-------------------------------------------\n\nThe borg config directory (``~/.config/borg`` on Linux,\n``~/Library/Application Support/borg`` on macOS,\n``C:\\Users\\<user>\\AppData\\Roaming\\borg`` on Windows -- see :ref:`env_vars`)\nhas content that you should take care of:\n\n``keys`` subdirectory\n  All your borg keyfile keys are stored in this directory. Please note that borg\n  repokey keys are stored inside the repository. In any case, you MUST make sure\n  to have an independent backup of the borg keys, see :ref:`borg_key_export` for\n  more details.\n\nMake sure that only you have access to the borg config directory.\n\n\nNote about creating multiple keyfile repositories at the same path\n------------------------------------------------------------------\n\nIf you create a new keyfile-encrypted repository at the same filesystem\npath multiple times (for example, when a previous repository at that path\nwas moved away or unmounted), Borg will not overwrite or reuse the existing\nkey file in your keys directory. Instead, it creates a new key file by\nappending a numeric suffix to the base name (e.g., .2, .3, ...).\n\nThis means you may see multiple key files like (example paths for Linux):\n\n- ~/.config/borg/keys/home_user_backup\n- ~/.config/borg/keys/home_user_backup.2\n- ~/.config/borg/keys/home_user_backup.3\n\nEach of these corresponds to a distinct repository created at the same\npath at different times. This behavior avoids accidental key reuse or\noverwrite.\n\n.. _home_data_borg:\n\nHow important is the borg data directory?\n-----------------------------------------\n\nThe borg data directory (``~/.local/share/borg`` on Linux,\n``~/Library/Application Support/borg`` on macOS,\n``C:\\Users\\<user>\\AppData\\Local\\borg`` on Windows -- see :ref:`env_vars`)\nhas content that you should take care of:\n\n``security`` subdirectory\n  Each directory here represents one Borg repository by its ID and contains the last known status.\n  If a repository's status is different from this information at the beginning of BorgBackup\n  operation, Borg outputs warning messages and asks for confirmation, so make sure you do not lose\n  or manipulate these files. However, apart from those warnings, a loss of these files can be\n  recovered.\n\nMake sure that only you have access to the Borg data directory.\n\n.. _cache_security:\n\nDo I need to take security precautions regarding the cache?\n-----------------------------------------------------------\n\nThe cache contains a lot of metadata information about the files in\nyour repositories and it is not encrypted.\n\nHowever, the assumption is that the cache is being stored on the very\nsame system which also contains the original files which are being\nbacked up. So someone with access to the cache files would also have\naccess the original files anyway.\n\nThe Internals section contains more details about :ref:`cache`. If you ever need to move the cache\nto a different location, this can be achieved by using the appropriate :ref:`env_vars`.\n\nHow can I specify the encryption passphrase programmatically?\n-------------------------------------------------------------\n\nThere are several ways to specify a passphrase without human intervention:\n\nSetting ``BORG_PASSPHRASE``\n  The passphrase can be specified using the ``BORG_PASSPHRASE`` environment variable.\n  This is often the simplest option, but can be insecure if the script that sets it\n  is world-readable.\n\n  .. _password_env:\n  .. note:: Be careful how you set the environment; using the ``env``\n          command, a ``system()`` call or using inline shell scripts\n          (e.g. ``BORG_PASSPHRASE=hunter2 borg ...``)\n          might expose the credentials in the process list directly\n          and they will be readable to all users on a system. Using\n          ``export`` in a shell script file should be safe, however, as\n          the environment of a process is `accessible only to that\n          user\n          <https://security.stackexchange.com/questions/14000/environment-variable-accessibility-in-linux/14009#14009>`_.\n\nUsing ``BORG_PASSCOMMAND`` with a file of proper permissions\n  Another option is to create a file with a password in it in your home\n  directory and use permissions to keep anyone else from reading it. For\n  example, first create a key::\n\n    (umask 0077; head -c 32 /dev/urandom | base64 -w 0 > ~/.borg-passphrase)\n\n  Then in an automated script one can put::\n\n    export BORG_PASSCOMMAND=\"cat $HOME/.borg-passphrase\"\n\n  and Borg will automatically use that passphrase.\n\nUsing keyfile-based encryption with a blank passphrase\n  It is possible to encrypt your repository in ``keyfile`` mode instead of the default\n  ``repokey`` mode and use a blank passphrase for the key file (simply press Enter twice\n  when ``borg repo-create`` asks for the password). See :ref:`encrypted_repos`\n  for more details.\n\nUsing ``BORG_PASSCOMMAND`` with macOS Keychain\n  macOS has a native manager for secrets (such as passphrases) which is safer\n  than just using a file as it is encrypted at rest and unlocked manually\n  (fortunately, the login keyring automatically unlocks when you log in). With\n  the built-in ``security`` command, you can access it from the command line,\n  making it useful for ``BORG_PASSCOMMAND``.\n\n  First generate a passphrase and use ``security`` to save it to your login\n  (default) keychain::\n\n    security add-generic-password -D secret -U -a $USER -s borg-passphrase -w $(head -c 32 /dev/urandom | base64 -w 0)\n\n  In your backup script retrieve it in the ``BORG_PASSCOMMAND``::\n\n    export BORG_PASSCOMMAND=\"security find-generic-password -a $USER -s borg-passphrase -w\"\n\nUsing ``BORG_PASSCOMMAND`` with GNOME Keyring\n  GNOME also has a keyring daemon that can be used to store a Borg passphrase.\n  First ensure ``libsecret-tools``, ``gnome-keyring`` and ``libpam-gnome-keyring``\n  are installed. If ``libpam-gnome-keyring`` wasn't already installed, ensure it\n  runs on login::\n\n    sudo sh -c \"echo session optional pam_gnome_keyring.so auto_start >> /etc/pam.d/login\"\n    sudo sh -c \"echo password optional pam_gnome_keyring.so >> /etc/pam.d/passwd\"\n    # you may need to relogin afterwards to activate the login keyring\n\n  Then add a secret to the login keyring::\n\n    head -c 32 /dev/urandom | base64 -w 0 | secret-tool store borg-repository repo-name --label=\"Borg Passphrase\"\n\n  If a dialog box pops up prompting you to pick a password for a new keychain, use your\n  login password. If there is a checkbox for automatically unlocking on login, check it\n  to allow backups without any user intervention whatsoever.\n\n  Once the secret is saved, retrieve it in a backup script using ``BORG_PASSCOMMAND``::\n\n    export BORG_PASSCOMMAND=\"secret-tool lookup borg-repository repo-name\"\n\n  .. note:: For this to unlock the keychain automatically it must be run\n    in the ``dbus`` session of an unlocked terminal; for example, running a backup\n    script as a ``cron`` job might not work unless you also ``export DISPLAY=:0``\n    so ``secret-tool`` can pick up your open session. `It gets even more complicated`__\n    when you are running the tool as a different user (e.g. running a backup as root\n    with the password stored in the user keyring).\n\n__ https://github.com/borgbackup/borg/pull/2837#discussion_r127641330\n\nUsing ``BORG_PASSCOMMAND`` with KWallet\n  KDE also has a keychain feature in the form of KWallet. The command-line tool\n  ``kwalletcli`` can be used to store and retrieve secrets. Ensure ``kwalletcli``\n  is installed, generate a passphrase, and store it in your \"wallet\"::\n\n    head -c 32 /dev/urandom | base64 -w 0 | kwalletcli -Pe borg-passphrase -f Passwords\n\n  Once the secret is saved, retrieve it in a backup script using ``BORG_PASSCOMMAND``::\n\n    export BORG_PASSCOMMAND=\"kwalletcli -e borg-passphrase -f Passwords\"\n\nWhen backing up to remote encrypted repos, is encryption done locally?\n----------------------------------------------------------------------\n\nYes, file and directory metadata and data is locally encrypted, before\nleaving the local machine. We do not mean the transport layer encryption\nby that, but the data/metadata itself. Transport layer encryption (e.g.\nwhen ssh is used as a transport) applies additionally.\n\nWhen backing up to remote servers, do I have to trust the remote server?\n------------------------------------------------------------------------\n\nYes and No.\n\nNo, as far as data confidentiality is concerned - if you use encryption,\nall your files/dirs data and metadata are stored in their encrypted form\ninto the repository.\n\nYes, as an attacker with access to the remote server could delete (or\notherwise make unavailable) all your backups on that server.\n\nHow can I protect against a hacked backup client?\n-------------------------------------------------\n\nAssume you back up your backup client machine C to the backup server S and\nC gets hacked. In a simple push setup, the attacker could then use borg on\nC to delete all backups residing on S.\n\nThese are your options to protect against that:\n\n- Use a pull-mode setup using ``ssh -R``, see :ref:`pull_backup` for more information.\n- Mount C's filesystem on another machine and then create a backup of it.\n- Do not give C filesystem-level access to S.\n\nSee :ref:`hosting_repositories` for a detailed protection guide.\n\nHow can I protect against a hacked backup server?\n-------------------------------------------------\n\nJust in case you got the impression that pull-mode backups are way more safe\nthan push-mode, you also need to consider the case that your backup server S\ngets hacked. In case S has access to a lot of clients C, that might bring you\ninto even bigger trouble than a hacked backup client in the previous FAQ entry.\n\nThese are your options to protect against that:\n\n- Use the standard push-mode setup (see also previous FAQ entry).\n- Mount (the repo part of) S's filesystem on C.\n- Do not give S file-system level access to C.\n- Have your backup server at a well protected place (maybe not reachable from\n  the internet), configure it safely, apply security updates, monitor it, ...\n\nHow can I protect against theft, sabotage, lightning, fire, ...?\n----------------------------------------------------------------\n\nIn general: if your only backup medium is nearby the backed-up machine and\nalways connected, you can easily get into trouble: they likely share the same\nfate if something goes really wrong.\n\nThus:\n\n- have multiple backup media\n- have media disconnected from network, power, computer\n- have media at another place\n- have a relatively recent backup on your media\n\nHow do I report a security issue with Borg?\n-------------------------------------------\n\nSend a private email to the :ref:`security contact <security-contact>`\nif you think you have discovered a security issue.\nPlease disclose security issues responsibly.\n\nCommon issues\n#############\n\nCommand error: ... needed to match precisely one archive, but matched N.\n------------------------------------------------------------------------\n\nThe command wanted one archive to work with. But the parameters you gave either\ndidn't match anything or they matched multiple archives.\n\nSee :ref:`archive_specification` about how to refer to an archive.\n\n/path/to/repo is not a valid repository. Check repo config.\n-----------------------------------------------------------\n\nThere can be many causes of this error. E.g. you have incorrectly specified the repository path.\n\nYou will also get this error if you try to access a repository with a key that uses the argon2 key algorithm using an old version of borg.\nWe recommend upgrading to the latest stable version and trying again. We are sorry. We should have thought about forward\ncompatibility and implemented a more helpful error message.\n\nWhy am I seeing idle borg serve processes on the repo server?\n-------------------------------------------------------------\n\nPlease see the next question.\n\nWhy does Borg disconnect or hang when backing up to a remote server?\n--------------------------------------------------------------------\n\nCommunication with the remote server (using an ssh: repo URL) happens via an SSH\nconnection. This can lead to some issues that would not occur during a local backup:\n\n- Since Borg does not send data all the time, the connection may get closed, leading\n  to errors like \"connection closed by remote\".\n- On the other hand, network issues may lead to a dysfunctional connection\n  that is only detected after some time by the server, leading to stale ``borg serve``\n  processes and locked repositories.\n\nTo fix such problems, please apply these :ref:`SSH settings <ssh_configuration>` so that\nkeep-alive requests are sent regularly.\n\nHow can I deal with my very unstable SSH connection?\n----------------------------------------------------\n\nIf you have issues with lost connections during long-running borg commands, you\ncould try to work around:\n\n- Make partial extracts like ``borg extract PATTERN`` to do multiple\n  smaller extraction runs that complete before your connection has issues.\n- Try using ``borg mount MOUNTPOINT`` and ``rsync -avH`` from\n  ``MOUNTPOINT`` to your desired extraction directory. If the connection breaks\n  down, just repeat that over and over again until rsync does not find anything\n  to do any more. Due to the way borg mount works, this might be less efficient\n  than borg extract for bigger volumes of data.\n\nCan I back up my root partition (/) with Borg?\n----------------------------------------------\n\nBacking up your entire root partition works just fine, but remember to\nexclude directories that make no sense to back up, such as /dev, /proc,\n/sys, /tmp and /run, and to use ``--one-file-system`` if you only want to\nback up the root partition (and not any mounted devices e.g.).\n\nIf it crashes with a UnicodeError, what can I do?\n-------------------------------------------------\n\nCheck if your encoding is set correctly. For most POSIX-like systems, try::\n\n  export LANG=en_US.UTF-8  # or similar, important is correct charset\n\nIf that does not help:\n\n- check for typos, check if you really used ``export``.\n- check if you have set ``LC_ALL`` - if so, try not setting it.\n- check if you generated the respective locale via ``locale-gen``.\n\nI can't extract non-ascii filenames by giving them on the commandline!?\n-----------------------------------------------------------------------\n\nThis might be due to different ways to represent some characters in unicode\nor due to other non-ascii encoding issues.\n\nIf you run into that, try this:\n\n- avoid the non-ascii characters on the commandline by e.g. extracting\n  the parent directory (or even everything)\n- mount the repo using FUSE and use some file manager\n\n.. _expected_performance:\n\nWhat's the expected backup performance?\n---------------------------------------\n\nCompared to simply copying files (e.g. with ``rsync``), Borg has more work to do.\nThis can make creation of the first archive slower, but saves time\nand disk space on subsequent runs. Here what Borg does when you run ``borg create``:\n\n- Borg chunks the file (using the relatively expensive buzhash algorithm)\n- It then computes the \"id\" of the chunk (hmac-sha256 (slow, except\n  if your CPU has sha256 acceleration) or blake2b (fast, in software))\n- Then it checks whether this chunk is already in the repo (local hashtable lookup,\n  fast). If so, the processing of the chunk is completed here. Otherwise it needs to\n  process the chunk:\n- Compresses (the default lz4 is super fast)\n- Encrypts and authenticates (AES-OCB, usually fast if your CPU has AES acceleration as usual\n  since about 10y, or chacha20-poly1305, fast pure-software crypto)\n- Transmits to repo. If the repo is remote, this usually involves an SSH connection\n  (does its own encryption / authentication).\n- Stores the chunk into a key/value store (the key is the chunk id, the value\n  is the data). While doing that, it computes XXH64 of the data (repo low-level\n  checksum, used by borg check --repository).\n\nSubsequent backups are usually very fast if most files are unchanged and only\na few are new or modified. The high performance on unchanged files primarily depends\nonly on a few factors (like FS recursion + metadata reading performance and the\nfiles cache working as expected) and much less on other factors.\n\nE.g., for this setup:\n\n- server grade machine (4C/8T 2013 Xeon, 64GB RAM, 2x good 7200RPM disks)\n- local zfs filesystem (mirrored) containing the backup source data\n- repository is remote (does not matter much for unchanged files)\n- backup job runs while machine is otherwise idle\n\nThe observed performance is that Borg can process about\n**1 million unchanged files (and a few small changed ones) in 4 minutes!**\n\nIf you are seeing much less than that in similar circumstances, read the next\nfew FAQ entries below.\n\n.. _slow_backup:\n\nWhy is my backup so slow?\n--------------------------\n\nIf you feel your Borg backup is too slow somehow, here is what you can do:\n\n- Make sure Borg has enough RAM (depends on how big your repo is / how many\n  files you have)\n- Use one of the blake2 modes for --encryption except if you positively know\n  your CPU (and openssl) accelerates sha256 (then stay with hmac-sha256).\n- Don't use any expensive compression. The default is lz4 and super fast.\n  Uncompressed is often slower than lz4.\n- Just wait. You can also interrupt it and start it again as often as you like,\n  it will converge against a valid \"completed\" state. It is starting\n  from the beginning each time, but it is still faster then as it does not store\n  data into the repo which it already has there.\n- If you don’t need additional file attributes, you can disable them with ``--noflags``,\n  ``--noacls``, ``--noxattrs``. This can lead to noticeable performance improvements\n  when your backup consists of many small files.\n\nTo see what files have changed and take more time processing, you can also add\n``--list --filter=AME --stats`` to your ``borg create`` call to produce more log output,\nincluding a file list (with file status characters) and also some statistics at\nthe end of the backup.\n\nThen you do the backup and look at the log output:\n\n- stats: Do you really have little changes or are there more changes than you thought?\n  In the stats you can see the overall volume of changed data, which needed to be\n  added to the repo. If that is a lot, that can be the reason why it is slow.\n- ``A`` status (\"added\") in the file list:\n  If you see that often, you have a lot of new files (files that Borg did not find\n  in the files cache). If you think there is something wrong with that (the file was there\n  already in the previous backup), please read the FAQ entries below.\n- ``M`` status (\"modified\") in the file list:\n  If you see that often, Borg thinks that a lot of your files might be modified\n  (Borg found them in the files cache, but the metadata read from the filesystem did\n  not match the metadata stored in the files cache).\n  In such a case, Borg will need to process the files' contents completely, which is\n  much slower than processing unmodified files (Borg does not read their contents!).\n  The metadata values used in this comparison are determined by the ``--files-cache`` option\n  and could be e.g. size, ctime and inode number (see the ``borg create`` docs for more\n  details and potential issues).\n  You can use the ``stat`` command on files to look at fs metadata manually to debug if\n  there is any unexpected change triggering the ``M`` status.\n  Also, the ``--debug-topic=files_cache`` option of ``borg create`` provides a lot of debug\n  output helping to analyse why the files cache does not give its expected high performance.\n\nWhen borg runs inside a virtual machine, there are some more things to look at:\n\nSome hypervisors (e.g. kvm on older proxmox) give some broadly compatible CPU type to the\nVM (usually to ease migration between VM hosts of potentially different hardware CPUs).\n\nIt is broadly compatible because they leave away modern CPU features that could be\nnot present in older or other CPUs, e.g. hardware acceleration for AES crypto, for\nsha2 hashes, for (P)CLMUL(QDQ) computations useful for crc32.\n\nSo, basically you pay for compatibility with bad performance. If you prefer better\nperformance, you should try to use the x86-64-v2-AES VCPU or the \"host\" VCPU,\nexposing misc. hw acceleration features to the VM which runs borg.\n\nOn Linux, check ``/proc/cpuinfo`` for the CPU flags inside the VM.\nFor kvm check the docs about \"Host model\" and \"Host passthrough\".\n\nSee also the next few FAQ entries for more details.\n\n.. _a_status_oddity:\n\nI am seeing 'A' (added) status for an unchanged file!?\n------------------------------------------------------\n\nThe files cache is used to determine whether Borg already\n\"knows\" / has backed up a file and if so, to skip the file from\nchunking. It intentionally *excludes* files that have a timestamp\nwhich is the same as the newest timestamp in the created archive.\n\nSo, if you see an 'A' status for unchanged file(s), they are likely the files\nwith the most recent timestamp in that archive.\n\nThis is expected: it is to avoid data loss with files that are backed up from\na snapshot and that are immediately changed after the snapshot (but within\ntimestamp granularity time, so the timestamp would not change). Without the code that\nremoves these files from the files cache, the change that happened right after\nthe snapshot would not be contained in the next backup as Borg would\nthink the file is unchanged.\n\nThis does not affect deduplication, the file will be chunked, but as the chunks\nwill often be the same and already stored in the repo (except in the above\nmentioned rare condition), it will just re-use them as usual and not store new\ndata chunks.\n\nIf you want to avoid unnecessary chunking, just create or touch a small or\nempty file in your backup source file set (so that one has the latest timestamp,\nnot your 50GB VM disk image) and, if you do snapshots, do the snapshot after\nthat.\n\nSince only the files cache is used in the display of files status,\nthose files are reported as being added when, really, chunks are\nalready used.\n\nBy default, ctime (change time) is used for the timestamps to have a rather\nsafe change detection (see also the --files-cache option).\n\nFurthermore, pathnames used as key into the files cache are **as archived**,\nso make sure these are always the same (see ``borg list``).\n\n.. _always_chunking:\n\nIt always chunks all my files, even unchanged ones!\n---------------------------------------------------\n\nBorg maintains a files cache where it remembers the timestamps, size and\ninode of files. When Borg does a new backup and starts processing a\nfile, it first looks whether the file has changed (compared to the values\nstored in the files cache). If the values are the same, the file is assumed\nunchanged and thus its contents won't get chunked (again).\n\nThe files cache is stored separately (using a different filename suffix) per\narchive series, thus using always the same name for the archive is strongly\nrecommended. The \"rebuild files cache from previous archive in repo\" feature\nalso depends on that.\nAlternatively, there is also BORG_FILES_CACHE_SUFFIX which can be used to\nmanually set a custom suffix (if you can't just use the same archive name).\n\nAnother possible reason is that files don't always have the same path -\nborg uses the paths as seen in the archive when using ``borg list``.\n\nIt is possible for some filesystems, such as ``mergerfs`` or network filesystems,\nto return inconsistent inode numbers across runs, causing borg to consider them changed.\nA workaround is to set the option ``--files-cache=ctime,size`` to exclude the inode\nnumber comparison from the files cache check so that files with different inode\nnumbers won't be treated as modified.\n\nUsing a pure-python msgpack! This will result in lower performance.\n-------------------------------------------------------------------\n\nborg uses `msgpack` to serialize/deserialize data.\n\n`msgpack` has 2 implementations:\n\n- a fast one (C code compiled into a platform specific binary), and\n- a slow pure-python one.\n\nThe slow one is used if it can't successfully import the fast one.\n\nIf you use the pyinstaller-made borg \"fat binary\" which we offer on github\nreleases, it could be that you downloaded a binary that does not match the\n(g)libc on your system.\n\nBinaries made for an older glibc than the one you have on your system usually\njust work, but the opposite is not necessarily the case and can lead to misc.\nissues - like failing to load the fast msgpack code or not working at all.\n\nSo: try a binary made for an older glibc.\n\nIf you see this without using a \"fat binary\" from us, it usually means that\nmsgpack is not built / installed correctly. It could be also that the platform\nis not fully supported (so the python code works, but there is no fast binary\ncode).\n\nIs there a way to limit bandwidth with Borg?\n--------------------------------------------\n\nTo limit upload (i.e. :ref:`borg_create`) bandwidth, use the\n``--remote-ratelimit`` option.\n\nThere is no built-in way to limit *download*\n(i.e. :ref:`borg_extract`) bandwidth, but limiting download bandwidth\ncan be accomplished with pipeviewer_:\n\nCreate a wrapper script:  /usr/local/bin/pv-wrapper\n\n::\n\n    #!/bin/sh\n        ## -q, --quiet              do not output any transfer information at all\n        ## -L, --rate-limit RATE    limit transfer to RATE bytes per second\n    RATE=307200\n    pv -q -L $RATE  | \"$@\"\n\nAdd BORG_RSH environment variable to use pipeviewer wrapper script with ssh.\n\n::\n\n    export BORG_RSH='/usr/local/bin/pv-wrapper ssh'\n\nNow Borg will be bandwidth limited. The nice thing about ``pv`` is that you can\nchange rate-limit on the fly:\n\n::\n\n    pv -R $(pidof pv) -L 102400\n\n.. _pipeviewer: https://www.ivarch.com/programs/pv.shtml\n\n\nHow can I avoid unwanted base directories getting stored into archives?\n-----------------------------------------------------------------------\n\nPossible use cases:\n\n- Another file system is mounted and you want to back it up with original paths.\n- You have created a BTRFS snapshot in a ``/.snapshots`` directory for backup.\n\nTo achieve this, run ``borg create`` within the mountpoint/snapshot directory:\n\n::\n\n    # Example: Some file system mounted in /mnt/rootfs.\n    cd /mnt/rootfs\n    borg create rootfs_backup .\n\nAnother way (without changing the directory) is to use the slashdot hack:\n\n::\n\n    borg create rootfs_backup /mnt/rootfs/./\n\n\nI am having troubles with some network/FUSE/special filesystem, why?\n--------------------------------------------------------------------\n\nBorg is doing nothing special in the filesystem, it only uses very\ncommon and compatible operations (even the locking is just \"rename\").\n\nSo, if you are encountering issues like slowness, corruption or malfunction\nwhen using a specific filesystem, please try if you can reproduce the issues\nwith a local (non-network) and proven filesystem (like ext4 on Linux).\n\nIf you can't reproduce the issue then, you maybe have found an issue within\nthe filesystem code you used (not with Borg). For this case, it is\nrecommended that you talk to the developers / support of the network fs and\nmaybe open an issue in their issue tracker. Do not file an issue in the\nBorg issue tracker.\n\nIf you can reproduce the issue with the proven filesystem, please file an\nissue in the Borg issue tracker about that.\n\n\nWhy does running 'borg check --repair' warn about data loss?\n------------------------------------------------------------\n\nRepair usually works for recovering data in a corrupted archive. However,\nit's impossible to predict all modes of corruption. In some very rare\ninstances, such as malfunctioning storage hardware, additional repo\ncorruption may occur. If you can't afford to lose the repo, it's strongly\nrecommended that you perform repair on a copy of the repo.\n\nIn other words, the warning is there to emphasize that Borg:\n  - Will perform automated routines that modify your backup repository\n  - Might not actually fix the problem you are experiencing\n  - Might, in very rare cases, further corrupt your repository\n\nIn the case of malfunctioning hardware, such as a drive or USB hub\ncorrupting data when read or written, it's best to diagnose and fix the\ncause of the initial corruption before attempting to repair the repo. If\nthe corruption is caused by a one time event such as a power outage,\nrunning `borg check --repair` will fix most problems.\n\n\nWhy isn't there more progress / ETA information displayed?\n----------------------------------------------------------\n\nSome borg runs take quite a bit, so it would be nice to see a progress display,\nmaybe even including a ETA (expected time of \"arrival\" [here rather \"completion\"]).\n\nFor some functionality, this can be done: if the total amount of work is more or\nless known, we can display progress. So check if there is a ``--progress`` option.\n\nBut sometimes, the total amount is unknown (e.g. for ``borg create`` we just do\na single pass over the filesystem, so we do not know the total file count or data\nvolume before reaching the end). Adding another pass just to determine that would\ntake additional time and could be incorrect, if the filesystem is changing.\n\nEven if the fs does not change and we knew count and size of all files, we still\ncould not compute the ``borg create`` ETA as we do not know the amount of changed\nchunks, how the bandwidth of source and destination or system performance might\nfluctuate.\n\nYou see, trying to display ETA would be futile. The borg developers prefer to\nrather not implement progress / ETA display than doing futile attempts.\n\nSee also: https://xkcd.com/612/\n\n\nWhy am I getting 'Operation not permitted' errors when backing up on sshfs?\n---------------------------------------------------------------------------\n\nBy default, ``sshfs`` is not entirely POSIX-compliant when renaming files due to\na technicality in the SFTP protocol. Fortunately, it also provides a workaround_\nto make it behave correctly::\n\n    sshfs -o workaround=rename user@host:dir /mnt/dir\n\n.. _workaround: https://unix.stackexchange.com/a/123236\n\n\nHow do I rename a repository?\n-----------------------------\n\nThere is nothing special that needs to be done, you can simply rename the\ndirectory that corresponds to the repository. However, the next time borg\ninteracts with the repository (i.e, via ``borg list``), depending on the value\nof ``BORG_RELOCATED_REPO_ACCESS_IS_OK``, borg may warn you that the repository\nhas been moved. You will be given a prompt to confirm you are OK with this.\n\nIf ``BORG_RELOCATED_REPO_ACCESS_IS_OK`` is unset, borg will interactively ask for\neach repository whether it's OK.\n\nIt may be useful to set ``BORG_RELOCATED_REPO_ACCESS_IS_OK=yes`` to avoid the\nprompts when renaming multiple repositories or in a non-interactive context\nsuch as a script. See :doc:`deployment` for an example.\n\n\nMy backup disk is full, what can I do?\n--------------------------------------\n\nBorg cannot work if you really have zero free space on the backup disk, so the\nfirst thing you must do is deleting some files to regain free disk space. See\n:ref:`about_free_space` for further details.\n\nSome Borg commands that do not change the repository might work under disk-full\nconditions, but generally this should be avoided. If your backup disk is already\nfull when Borg starts a write command like `borg create`, it will abort\nimmediately and the repository will stay as-is.\n\n\nMiscellaneous\n#############\n\nmacOS: borg mounts not shown in Finder's side bar\n-------------------------------------------------\n\nhttps://github.com/macfuse/macfuse/wiki/Mount-options#local\n\nRead the above first and use this on your own risk::\n\n    borg mount -olocal REPO MOUNTPOINT\n\n\nRequirements for the borg single-file binary, esp. (g)libc?\n-----------------------------------------------------------\n\nWe try to build the binary on old, but still supported systems - to keep the\nminimum requirement for the (g)libc low. The (g)libc can't be bundled into\nthe binary as it needs to fit your kernel and OS, but Python and all other\nrequired libraries will be bundled into the binary.\n\nIf your system fulfills the minimum (g)libc requirement (see the README that\nis released with the binary), there should be no problem. If you are slightly\nbelow the required version, maybe just try. Due to the dynamic loading (or not\nloading) of some shared libraries, it might still work depending on what\nlibraries are actually loaded and used.\n\nIn the borg git repository, there is scripts/glibc_check.py that can determine\n(based on the symbols' versions they want to link to) whether a set of given\n(Linux) binaries works with a given glibc version.\n"
  },
  {
    "path": "docs/global.rst.inc",
    "content": ".. highlight:: bash\n.. |package_dirname| replace:: borgbackup-|version|\n.. |package_filename| replace:: |package_dirname|.tar.gz\n.. |package_url| replace:: https://pypi.org/project/borgbackup/#files\n.. |git_url| replace:: https://github.com/borgbackup/borg.git\n.. _github: https://github.com/borgbackup/borg\n.. _issue tracker: https://github.com/borgbackup/borg/issues\n.. _deduplication: https://en.wikipedia.org/wiki/Data_deduplication\n.. _AES: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard\n.. _HMAC-SHA256: https://en.wikipedia.org/wiki/HMAC\n.. _SHA256: https://en.wikipedia.org/wiki/SHA-256\n.. _PBKDF2: https://en.wikipedia.org/wiki/PBKDF2\n.. _argon2: https://en.wikipedia.org/wiki/Argon2\n.. _ACL: https://en.wikipedia.org/wiki/Access_control_list\n.. _libacl: https://savannah.nongnu.org/projects/acl/\n.. _libattr: https://savannah.nongnu.org/projects/attr/\n.. _libxxhash: https://github.com/Cyan4973/xxHash\n.. _liblz4: https://github.com/Cyan4973/lz4\n.. _libzstd: https://github.com/facebook/zstd\n.. _OpenSSL: https://www.openssl.org/\n.. _`Python 3`: https://www.python.org/\n.. _Buzhash: https://en.wikipedia.org/wiki/Buzhash\n.. _msgpack: https://msgpack.org/\n.. _`msgpack-python`: https://pypi.org/project/msgpack-python/\n.. _llfuse: https://pypi.org/project/llfuse/\n.. _mfusepy: https://pypi.org/project/mfusepy/\n.. _pyfuse3: https://pypi.org/project/pyfuse3/\n.. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace\n.. _Cython: https://cython.org/\n.. _virtualenv: https://pypi.org/project/virtualenv/\n.. _python-xxhash: https://github.com/ifduyue/python-xxhash/\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: none\n\nBorg Documentation\n==================\n\n.. include:: ../README.rst\n\n.. When you add an element here, do not forget to add it to book.rst.\n\n.. toctree::\n   :maxdepth: 2\n\n   installation\n   quickstart\n   usage\n   deployment\n   faq\n   support\n   changes\n   changes_1.x\n   changes_0.x\n   internals\n   development\n   authors\n"
  },
  {
    "path": "docs/installation.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: bash\n.. _installation:\n\nInstallation\n============\n\nThere are different ways to install Borg:\n\n- :ref:`distribution-package` - easy and fast if a package is\n  available from your distribution.\n- :ref:`pyinstaller-binary` - easy and fast, we provide a ready-to-use binary file\n  that comes bundled with all dependencies.\n- :ref:`source-install`, either:\n\n  - :ref:`windows-binary` - builds a binary file for Windows using MSYS2.\n  - :ref:`pip-installation` - installing a source package with pip needs\n    more installation steps and requires all dependencies with\n    development headers and a compiler.\n  - :ref:`git-installation`  - for developers and power users who want to\n    have the latest code or use revision control (each release is\n    tagged).\n\n.. _distribution-package:\n\nDistribution Package\n--------------------\n\nSome distributions might offer a ready-to-use ``borgbackup``\npackage which can be installed with the package manager.\n\n.. important:: Those packages may not be up to date with the latest\n               Borg releases. Before submitting a bug\n               report, check the package version and compare that to\n               our latest release then review :doc:`changes` to see if\n               the bug has been fixed. Report bugs to the package\n               maintainer rather than directly to Borg if the\n               package is out of date in the distribution.\n\n.. keep this list in alphabetical order\n\n============ ============================================= =======\nDistribution Source                                        Command\n============ ============================================= =======\nAlpine Linux `Alpine repository`_                          ``apk add borgbackup``\nArch Linux   `[extra]`_                                    ``pacman -S borg``\nDebian       `Debian packages`_                            ``apt install borgbackup``\nGentoo       `ebuild`_                                     ``emerge borgbackup``\nGNU Guix     `GNU Guix`_                                   ``guix package --install borg``\nFedora/RHEL  `Fedora official repository`_                 ``dnf install borgbackup``\nFreeBSD      `FreeBSD ports`_                              ``cd /usr/ports/archivers/py-borgbackup && make install clean``\nmacOS        `Homebrew`_                                   | ``brew install borgbackup`` (official formula, **no** FUSE support)\n                                                           | **or**\n                                                           | ``brew install --cask macfuse`` (`private Tap`_, FUSE support)\n                                                           | ``brew install borgbackup/tap/borgbackup-fuse``\nMageia       `cauldron`_                                   ``urpmi borgbackup``\nNetBSD       `pkgsrc`_                                     ``pkg_add py-borgbackup``\nNixOS        `.nix file`_                                  ``nix-env -i borgbackup``\nOpenBSD      `OpenBSD ports`_                              ``pkg_add borgbackup``\nOpenIndiana  `OpenIndiana hipster repository`_             ``pkg install borg``\nopenSUSE     `openSUSE official repository`_               ``zypper in borgbackup``\nRaspbian     `Raspbian testing`_                           ``apt install borgbackup``\nUbuntu       `Ubuntu packages`_, `Ubuntu PPA`_             ``apt install borgbackup``\n============ ============================================= =======\n\n.. _Alpine repository: https://pkgs.alpinelinux.org/packages?name=borgbackup\n.. _[extra]: https://www.archlinux.org/packages/?name=borg\n.. _Debian packages: https://packages.debian.org/search?keywords=borgbackup&searchon=names&exact=1&suite=all&section=all\n.. _Fedora official repository: https://packages.fedoraproject.org/pkgs/borgbackup/borgbackup/\n.. _FreeBSD ports: https://www.freshports.org/archivers/py-borgbackup/\n.. _ebuild: https://packages.gentoo.org/packages/app-backup/borgbackup\n.. _GNU Guix: https://www.gnu.org/software/guix/package-list.html#borg\n.. _pkgsrc: https://pkgsrc.se/sysutils/py-borgbackup\n.. _cauldron: https://madb.mageia.org/package/show/application/0/release/cauldron/name/borgbackup\n.. _.nix file: https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/backup/borgbackup/default.nix\n.. _OpenBSD ports: https://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/sysutils/borgbackup/\n.. _OpenIndiana hipster repository: https://pkg.openindiana.org/hipster/en/search.shtml?token=borg&action=Search\n.. _openSUSE official repository: https://software.opensuse.org/package/borgbackup\n.. _Homebrew: https://formulae.brew.sh/formula/borgbackup\n.. _private Tap: https://github.com/borgbackup/homebrew-tap\n.. _Raspbian testing: https://archive.raspbian.org/raspbian/pool/main/b/borgbackup/\n.. _Ubuntu packages: https://launchpad.net/ubuntu/+source/borgbackup\n.. _Ubuntu PPA: https://launchpad.net/~costamagnagianfranco/+archive/ubuntu/borgbackup\n\nPlease ask package maintainers to build a package or, if you can package/\nsubmit it yourself, please help us with that! See :issue:`105` on\nGitHub to follow up on packaging efforts.\n\n**Current status of package in the repositories**\n\n.. start-badges\n\n|Packaging status|\n\n.. |Packaging status| image:: https://repology.org/badge/vertical-allrepos/borgbackup.svg\n        :alt: Packaging status\n        :target: https://repology.org/project/borgbackup/versions\n\n.. end-badges\n\n.. _pyinstaller-binary:\n\nStandalone Binary\n-----------------\n\n.. note:: Releases are signed with an OpenPGP key, see\n          :ref:`security-contact` for more instructions.\n\nBorg x86/x64 AMD/Intel compatible binaries (generated with `pyinstaller`_)\nare available on the releases_ page for the following platforms:\n\n* **Linux**: glibc >= 2.28 (ok for most supported Linux releases).\n  Older glibc releases are untested and may not work.\n* **macOS**: 10.12 or newer (To avoid signing issues, download the file via\n  command line **or** remove the ``quarantine`` attribute after downloading:\n  ``$ xattr -dr com.apple.quarantine borg-macosx64.tgz``)\n* **FreeBSD**: 12.1 (unknown whether it works for older releases)\n\nARM binaries are built by Johann Bauer, see: https://borg.bauerj.eu/\n\nTo install such a binary, just drop it into a directory in your ``PATH``,\nmake borg readable and executable for its users and then you can run ``borg``::\n\n    sudo cp borg-linux64 /usr/local/bin/borg\n    sudo chown root:root /usr/local/bin/borg\n    sudo chmod 755 /usr/local/bin/borg\n\nOptionally you can create a symlink to have ``borgfs`` available, which is an\nalias for ``borg mount``::\n\n    ln -s /usr/local/bin/borg /usr/local/bin/borgfs\n\nNote that the binary uses /tmp to unpack Borg with all dependencies. It will\nfail if /tmp has not enough free space or is mounted with the ``noexec``\noption. You can change the temporary directory by setting the ``TEMP``\nenvironment variable before running Borg.\n\nIf a new version is released, you will have to download it manually and replace\nthe old version using the same steps as shown above.\n\n.. _pyinstaller: https://www.pyinstaller.org\n.. _releases: https://github.com/borgbackup/borg/releases\n\n.. _source-install:\n\nFrom Source\n-----------\n\n.. note::\n\n  Some older Linux systems (like RHEL/CentOS 5) and Python interpreter binaries\n  compiled to be able to run on such systems (like Python installed via Anaconda)\n  might miss functions required by Borg.\n\n  This issue will be detected early and Borg will abort with a fatal error.\n\nDependencies\n~~~~~~~~~~~~\n\nTo install Borg from a source package (including pip), you have to install the\nfollowing dependencies first. For the libraries you will also need their\ndevelopment header files (sometimes in a separate `-dev` or `-devel` package).\n\n* `Python 3`_ >= 3.10.0\n* OpenSSL_ >= 1.1.1 (LibreSSL will not work)\n* libacl_ (which depends on libattr_)\n* liblz4_ >= 1.7.0 (r129)\n* libffi (required for argon2-cffi-bindings)\n* pkg-config (cli tool) - Borg uses this to discover header and library\n  locations automatically. Alternatively, you can also point to them via some\n  environment variables, see setup.py.\n* Some other Python dependencies, pip will automatically install them for you.\n* Optionally, if you wish to mount an archive as a FUSE filesystem, you need\n  a FUSE implementation for Python:\n\n  - mfusepy_ >= 3.1.0 (for fuse 2 and fuse 3, use `pip install borgbackup[mfusepy]`), or\n  - pyfuse3_ >= 3.1.1 (for fuse 3, use `pip install borgbackup[pyfuse3]`), or\n  - llfuse_ >= 1.3.8 (for fuse 2, use `pip install borgbackup[llfuse]`).\n  - Additionally, your OS will need to have FUSE support installed\n    (e.g. a package `fuse` for fuse 2 or a package `fuse3` for fuse 3 support).\n* Optionally, if you wish to use S3/B2 Backend:\n  - borgstore[s3] ~= 0.3.0 (use `pip install borgbackup[s3]`)\n* Optionally, if you wish to use SFTP Backend:\n  - borgstore[sftp] ~= 0.3.0 (use `pip install borgbackup[sftp]`)\n\nIf you have troubles finding the right package names, have a look at the\ndistribution specific sections below or the Vagrantfile in the git repository,\nwhich contains installation scripts for a number of operating systems.\n\nIn the following, the steps needed to install the dependencies are listed for a\nselection of platforms. If your distribution is not covered by these\ninstructions, try to use your package manager to install the dependencies.\n\nAfter you have installed the dependencies, you can proceed with steps outlined\nunder :ref:`pip-installation`.\n\nArch Linux\n++++++++++\n\nInstall the runtime and build dependencies::\n\n    pacman -S python python-pip python-virtualenv openssl acl lz4 base-devel\n    pacman -S fuse2     # needed for llfuse\n    pacman -S fuse3     # needed for pyfuse3\n\nNote that Arch Linux specifically doesn't support\n`partial upgrades <https://wiki.archlinux.org/title/Partial_upgrade>`__,\nso in case some packages cannot be retrieved from the repo, run with ``pacman -Syu``.\n\nDebian / Ubuntu\n+++++++++++++++\n\nInstall the dependencies with development headers::\n\n    sudo apt-get install python3 python3-dev python3-pip python3-virtualenv \\\n    libacl1-dev \\\n    libssl-dev \\\n    liblz4-dev \\\n    libffi-dev \\\n    build-essential pkg-config\n    sudo apt-get install libfuse-dev fuse    # needed for llfuse\n    sudo apt-get install libfuse3-dev fuse3  # needed for pyfuse3\n\nIn case you get complaints about permission denied on ``/etc/fuse.conf``: on\nUbuntu this means your user is not in the ``fuse`` group. Add yourself to that\ngroup, log out and log in again.\n\nFedora\n++++++\n\nInstall the dependencies with development headers::\n\n    sudo dnf install python3 python3-devel python3-pip python3-virtualenv \\\n    libacl-devel \\\n    openssl-devel \\\n    lz4-devel \\\n    libffi-devel \\\n    pkgconf\n    sudo dnf install gcc gcc-c++ redhat-rpm-config\n    sudo dnf install fuse-devel fuse         # needed for llfuse\n    sudo dnf install fuse3-devel fuse3       # needed for pyfuse3\n\nopenSUSE Tumbleweed / Leap\n++++++++++++++++++++++++++\n\nInstall the dependencies automatically using zypper::\n\n    sudo zypper source-install --build-deps-only borgbackup\n\nAlternatively, you can enumerate all build dependencies in the command line::\n\n    sudo zypper install python3 python3-devel \\\n    libacl-devel openssl-devel liblz4-devel \\\n    libffi-devel \\\n    python3-Cython python3-Sphinx python3-msgpack-python python3-pkgconfig pkgconf \\\n    python3-pytest python3-setuptools python3-setuptools_scm \\\n    python3-sphinx_rtd_theme gcc gcc-c++\n    sudo zypper install python3-llfuse  # llfuse\n\nmacOS\n+++++\n\nWhen installing borgbackup via Homebrew_, the basic dependencies are installed automatically.\n\nFor FUSE support to mount the backup archives, you need macFUSE, which is available\nvia `github <https://github.com/osxfuse/osxfuse/releases/latest>`__, or Homebrew::\n\n    brew install --cask macfuse\n\nWhen installing Borg via ``pip``, be sure to install the ``llfuse`` extra,\nsince macFUSE only supports FUSE API v2. Also, since Homebrew won't link\nthe installed ``openssl`` formula, point pkg-config to the correct path::\n\n    PKG_CONFIG_PATH=\"/usr/local/opt/openssl@1.1/lib/pkgconfig\" pip install borgbackup[llfuse]\n\nWhen working from a borg git repo workdir, you can install dependencies using the\nBrewfile::\n\n    brew install python@3.11  # can be any supported python3 version\n    brew bundle install  # install requirements from borg repo's ./Brewfile\n    pip3 install virtualenv\n\nBe aware that for all recent macOS releases you must authorize full disk access.\nIt is no longer sufficient to run borg backups as root. If you have not yet\ngranted full disk access, and you run Borg backup from cron, you will see\nmessages such as::\n\n    /Users/you/Pictures/Photos Library.photoslibrary: scandir: [Errno 1] Operation not permitted:\n\nTo fix this problem, you should grant full disk access to cron, and to your\nTerminal application. More information `can be found here\n<https://osxdaily.com/2020/04/27/fix-cron-permissions-macos-full-disk-access/>`__.\n\nFreeBSD\n++++++++\n\nListed below are packages you will need to install Borg, its dependencies,\nand commands to make FUSE work for using the mount command.\n\n::\n\n     pkg install -y python3 pkgconf\n     pkg install openssl\n     pkg install liblz4\n     pkg install fusefs-libs  # needed for llfuse\n     pkg install -y git\n     python3 -m ensurepip # to install pip for Python3\n     To use the mount command:\n     echo 'fuse_load=\"YES\"' >> /boot/loader.conf\n     echo 'vfs.usermount=1' >> /etc/sysctl.conf\n     kldload fuse\n     sysctl vfs.usermount=1\n\n.. _windows_deps:\n\nWindows\n+++++++\n\n.. note::\n    Running under Windows is experimental.\n\n.. warning::\n    This script needs to be run in the UCRT64 environment in MSYS2.\n\nInstall the dependencies with the provided script::\n\n    ./scripts/msys2-install-deps\n\n.. _msys2_path_translation:\n\nMSYS2 Path Translation\n++++++++++++++++++++++\n\nWhen running Borg within an MSYS2 environment, the shell\nautomatically translates POSIX-style paths (like ``/tmp`` or ``/C/Users``) to\nWindows paths (like ``C:\\msys64\\tmp`` or ``C:\\Users``) before they reach the\nBorg process.\n\nThis behavior can result in absolute Windows paths being stored in your backups,\nwhich may not be what you intended if you use POSIX paths for portability.\n\nTo disable this automatic translation for Borg, you can use environment variables\nto exclude everything from conversion. Similarly, MSYS2 also translates\nenvironment variables that look like paths. To disable this generally for Borg,\nset both variables::\n\n    export MSYS2_ARG_CONV_EXCL=\"*\"\n    export MSYS2_ENV_CONV_EXCL=\"*\"\n\nFor more details, see the `MSYS2 documentation on filesystem paths <https://www.msys2.org/docs/filesystem-paths/>`__.\n\nWindows 10's Linux Subsystem\n++++++++++++++++++++++++++++\n\n.. note::\n    Running under Windows 10's Linux Subsystem is experimental and has not been tested much yet.\n\nJust follow the Ubuntu Linux installation steps. You can omit the FUSE stuff, it won't work anyway.\n\n\nCygwin\n++++++\n\n.. note::\n    Running under Cygwin is experimental and has not been tested much yet.\n\nUse the Cygwin installer to install the dependencies::\n\n    python39 python39-devel\n    python39-setuptools python39-pip python39-wheel python39-virtualenv\n    libssl-devel liblz4-devel\n    binutils gcc-g++ git make openssh\n\nMake sure to use a virtual environment to avoid confusions with any Python installed on Windows.\n\n.. _windows-binary:\n\nBuilding a binary on Windows\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. note::\n    This is experimental.\n\n.. warning::\n    This needs to be run in the UCRT64 environment in MSYS2.\n\nEnsure to install the dependencies as described within :ref:`Dependencies: Windows <windows_deps>`.\n\n::\n\n    # Needed for setuptools < 70.2.0 to work - https://www.msys2.org/docs/python/#known-issues\n    # export SETUPTOOLS_USE_DISTUTILS=stdlib\n    pip install -e .\n    pyinstaller -y scripts/borg.exe.spec\n\nA standalone executable will be created in ``dist/borg.exe``.\n\n.. _pip-installation:\n\nUsing pip\n~~~~~~~~~\n\nVirtualenv_ can be used to build and install Borg without affecting\nthe system Python or requiring root access.  Using a virtual environment is\noptional, but recommended except for the most simple use cases.\n\nEnsure to install the dependencies as described within :ref:`source-install`.\n\n.. note::\n    If you install into a virtual environment, you need to **activate** it\n    first (``source borg-env/bin/activate``), before running ``borg``.\n    Alternatively, symlink ``borg-env/bin/borg`` into some directory that is in\n    your ``PATH`` so you can run ``borg``.\n\nThis will use ``pip`` to install the latest release from PyPi::\n\n    virtualenv --python=python3 borg-env\n    source borg-env/bin/activate\n\n    # might be required if your tools are outdated\n    pip install -U pip setuptools wheel\n\n    # install Borg + Python dependencies into virtualenv\n    pip install borgbackup\n    # or alternatively (if you want FUSE support):\n    pip install borgbackup[llfuse]  # to use llfuse\n    pip install borgbackup[pyfuse3]  # to use pyfuse3\n\nTo upgrade Borg to a new version later, run the following after\nactivating your virtual environment::\n\n    pip install -U borgbackup  # or ... borgbackup[llfuse/pyfuse3]\n\nWhen doing manual pip installation, man pages are not automatically\ninstalled.  You can run these commands to install the man pages\nlocally::\n\n    # get borg from github\n    git clone https://github.com/borgbackup/borg.git borg\n\n    # Install the files with proper permissions\n    install -D -m 0644 borg/docs/man/borg*.1* $HOME/.local/share/man/man1/borg.1\n\n    # Update the man page cache\n    mandb\n\n.. _git-installation:\n\nUsing git\n~~~~~~~~~\n\nThis uses latest, unreleased development code from git.\nWhile we try not to break master, there are no guarantees on anything.\n\nEnsure to install the dependencies as described within :ref:`source-install`.\n\nVersion metadata is obtained dynamically at install time using ``setuptools-scm``.\nPlease ensure that your git repo either has correct tags, or provide the version\nmanually using the ``SETUPTOOLS_SCM_PRETEND_VERSION`` environment variable.\n\n::\n\n    # get borg from github\n    git clone https://github.com/borgbackup/borg.git\n\n    # create a virtual environment\n    virtualenv --python=$(which python3) borg-env\n    source borg-env/bin/activate   # always before using!\n\n    # install borg dependencies into virtualenv\n    cd borg\n    pip install -r requirements.d/development.lock.txt\n    pip install -r requirements.d/docs.txt  # optional, to build the docs\n\n    # set a borg version if setuptools-scm fails to do so automatically\n    export SETUPTOOLS_SCM_PRETEND_VERSION=\n\n    # install borg into virtualenv\n    pip install -e .           # in-place editable mode\n    or\n    pip install -e .[pyfuse3]  # in-place editable mode, use pyfuse3\n    or\n    pip install -e .[llfuse]   # in-place editable mode, use llfuse\n\n    # optional: run all the tests, on all installed Python versions\n    # requires fakeroot, available through your package manager\n    fakeroot -u tox --skip-missing-interpreters\n\nBy default the system installation of python will be used.\nIf you need to use a different version of Python you can install this using ``pyenv``:\n\n::\n\n    ...\n    # create a virtual environment\n    pyenv install 3.10.0  # minimum, preferably use something more recent!\n    pyenv global 3.10.0\n    pyenv local 3.10.0\n    virtualenv --python=${pyenv which python} borg-env\n    source borg-env/bin/activate   # always before using!\n    ...\n\n.. note:: As a developer or power user, you should always use a virtual environment.\n"
  },
  {
    "path": "docs/internals/data-structures.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n\n.. _data-structures:\n\nData structures and file formats\n================================\n\nThis page documents the internal data structures and storage\nmechanisms of Borg. It is partly based on mailing list\ndiscussions and also on static code analysis.\n\n.. todo:: Clarify terms, perhaps create a glossary.\n          ID (client?) vs. key (repository?),\n          chunks (blob of data in repo?) vs. object (blob of data in repo, referred to from another object?),\n\n.. _repository:\n\nRepository\n----------\n\nBorg stores its data in a `Repository`, which is a key-value store and has\nthe following structure:\n\nconfig/\n  readme\n    simple text object telling that this is a Borg repository\n  id\n    the unique repository ID encoded as hexadecimal number text\n  version\n    the repository version encoded as decimal number text\n  manifest\n    some data about the repository, binary\n  last-key-checked\n    repository check progress (partial checks, full checks' checkpointing),\n    path of last object checked as text\n  space-reserve.N\n    purely random binary data to reserve space, e.g. for disk-full emergencies\n\nThere is a list of pointers to archive objects in this directory:\n\narchives/\n  0000... .. ffff...\n\nThe actual data is stored into a nested directory structure, using the full\nobject ID as name. Each (encrypted and compressed) object is stored separately.\n\ndata/\n  00/ .. ff/\n    00/ .. ff/\n      0000... .. ffff...\n\nkeys/\n  repokey\n    When using encryption in repokey mode, the encrypted, passphrase protected\n    key is stored here as a base64 encoded text.\n\nlocks/\n  used by the locking system to manage shared and exclusive locks.\n\n\nKeys\n~~~~\n\nRepository object IDs (which are used as key into the key-value store) are\nbyte strings of fixed length (256-bit, 32 bytes), computed like this::\n\n  key = id = id_hash(plaintext_data)  # plain = not encrypted, not compressed, not obfuscated\n\nThe id_hash function depends on the :ref:`encryption mode <borg_repo-create>`.\n\nAs the id / key is used for deduplication, id_hash must be a cryptographically\nstrong hash or MAC.\n\nRepository objects\n~~~~~~~~~~~~~~~~~~\n\nEach repository object is stored separately, under its ID into data/xx/yy/xxyy...\n\nA repo object has a structure like this:\n\n* 32-bit meta size\n* 32-bit data size\n* 64-bit xxh64(meta)\n* 64-bit xxh64(data)\n* meta\n* data\n\nThe size and xxh64 hashes can be used for server-side corruption checks without\nneeding to decrypt anything (which would require the borg key).\n\nThe overall size of repository objects varies from very small (a small source\nfile will be stored as a single repository object) to medium (big source files will\nbe cut into medium-sized chunks of some MB).\n\nMetadata and data are separately encrypted and authenticated (depending on\nthe user's choices).\n\nSee :ref:`data-encryption` for a graphic outlining the anatomy of the\nencryption.\n\nRepo object metadata\n~~~~~~~~~~~~~~~~~~~~\n\nMetadata is a MessagePack-encoded (and encrypted/authenticated) dict with:\n\n- ctype (compression type 0..255)\n- clevel (compression level 0..255)\n- csize (overall compressed (and maybe obfuscated) data size)\n- psize (only when obfuscated: payload size without the obfuscation trailer)\n- size (uncompressed size of the data)\n\nHaving this separately encrypted metadata makes it more efficient to query\nthe metadata without having to read, transfer and decrypt the (usually much\nbigger) data part.\n\nThe compression `ctype` and `clevel` is explained in :ref:`data-compression`.\n\n\nCompaction\n~~~~~~~~~~\n\n``borg compact`` is used to free repository space. It will:\n\n- list all object IDs present in the repository\n- read all archives and determine which object IDs are in use\n- remove all unused objects from the repository\n- inform / warn about anything remarkable it found:\n\n  - warn about IDs used, but not present (data loss!)\n  - inform about IDs that reappeared that were previously lost\n- compute statistics about:\n\n  - compression and deduplication factors\n  - repository space usage and space freed\n\n\nThe object graph\n----------------\n\nOn top of the simple key-value store offered by the Repository_,\nBorg builds a much more sophisticated data structure that is essentially\na completely encrypted object graph. Objects, such as archives_, are referenced\nby their chunk ID, which is cryptographically derived from their contents.\nMore on how this helps security in :ref:`security_structural_auth`.\n\n.. figure:: object-graph.png\n    :figwidth: 100%\n    :width: 100%\n\n.. _manifest:\n\nThe manifest\n~~~~~~~~~~~~\n\nCompared to borg 1.x:\n\n- the manifest moved from object ID 0 to config/manifest\n- the archives list has been moved from the manifest to archives/*\n\nThe manifest is rewritten each time an archive is created, deleted,\nor modified. It looks like this:\n\n.. code-block:: python\n\n    {\n        'version': 1,\n        'timestamp': '2017-05-05T12:42:23.042864',\n        'item_keys': ['acl_access', 'acl_default', ...],\n        'config': {},\n        'archives': {\n            '2017-05-05-system-backup': {\n                'id': b'<32 byte binary object ID>',\n                'time': '2017-05-05T12:42:22.942864',\n            },\n        },\n    }\n\nThe *version* field can be either 1 or 2. The versions differ in the\nway feature flags are handled, described below.\n\nThe *timestamp* field is used to avoid logical replay attacks where\nthe server just resets the repository to a previous state.\n\n*item_keys* is a list containing all Item_ keys that may be encountered in\nthe repository. It is used by *borg check*, which verifies that all keys\nin all items are a subset of these keys. Thus, an older version of *borg check*\nsupporting this mechanism can correctly detect keys introduced in later versions.\n\n*config* is a general-purpose location for additional metadata. All versions\nof Borg preserve its contents.\n\nFeature flags\n+++++++++++++\n\nFeature flags are used to add features to data structures without causing\ncorruption if older versions are used to access or modify them. The main issues\nto consider for a feature flag oriented design are flag granularity,\nflag storage, and cache_ invalidation.\n\nFeature flags are divided in approximately three categories, detailed below.\nDue to the nature of ID-based deduplication, write (i.e. creating archives) and\nread access are not symmetric; it is possible to create archives referencing\nchunks that are not readable with the current feature set. The third\ncategory are operations that require accurate reference counts, for example\narchive deletion and check.\n\nAs the manifest is always updated and always read, it is the ideal place to store\nfeature flags, comparable to the super-block of a file system. The only problem\nis to recover from a lost manifest, i.e. how is it possible to detect which feature\nflags are enabled, if there is no manifest to tell. This issue is left open at this time,\nbut is not expected to be a major hurdle; it doesn't have to be handled efficiently, it just\nneeds to be handled.\n\nLastly, cache_ invalidation is handled by noting which feature\nflags were and which were not understood while manipulating a cache.\nThis allows borg to detect whether the cache needs to be invalidated,\ni.e. rebuilt from scratch. See `Cache feature flags`_ below.\n\nThe *config* key stores the feature flags enabled on a repository:\n\n.. code-block:: python\n\n    config = {\n        'feature_flags': {\n            'read': {\n                'mandatory': ['some_feature'],\n            },\n            'check': {\n                'mandatory': ['other_feature'],\n            }\n            'write': ...,\n            'delete': ...\n        },\n    }\n\nThe top-level distinction for feature flags is the operation the client intends\nto perform,\n\n| the *read* operation includes extraction and listing of archives,\n| the *write* operation includes creating new archives,\n| the *delete* (archives) operation,\n| the *check* operation requires full understanding of everything in the repository.\n|\n\nThese are weakly set-ordered; *check* will include everything required for *delete*,\n*delete* will likely include *write* and *read*. However, *read* may require more\nfeatures than *write* (due to ID-based deduplication, *write* does not necessarily\nrequire reading/understanding repository contents).\n\nEach operation can contain several sets of feature flags. Only one set,\nthe *mandatory* set is currently defined.\n\nUpon reading the manifest, the Borg client has already determined which operation\nshould be performed. If feature flags are found in the manifest, the set\nof feature flags supported by the client is compared to the mandatory set\nfound in the manifest. If any unsupported flags are found (i.e. the mandatory set is\nnot a subset of the features supported by the Borg client used), the operation\nis aborted with a *MandatoryFeatureUnsupported* error:\n\n    Unsupported repository feature(s) {'some_feature'}. A newer version of borg is required to access this repository.\n\nOlder Borg releases do not have this concept and do not perform feature flags checks.\nThese can be locked out with manifest version 2. Thus, the only difference between\nmanifest versions 1 and 2 is that the latter is only accepted by Borg releases\nimplementing feature flags.\n\nTherefore, as soon as any mandatory feature flag is enabled in a repository,\nthe manifest version must be switched to version 2 in order to lock out all\nBorg releases unaware of feature flags.\n\n.. _Cache feature flags:\n.. rubric:: Cache feature flags\n\n`The cache`_ does not have its separate set of feature flags. Instead, Borg stores\nwhich flags were used to create or modify a cache.\n\nAll mandatory manifest features from all operations are gathered in one set.\nThen, two sets of features are computed;\n\n- those features that are supported by the client and mandated by the manifest\n  are added to the *mandatory_features* set,\n- the *ignored_features* set comprised of those features mandated by the manifest,\n  but not supported by the client.\n\nBecause the client previously checked compliance with the mandatory set of features\nrequired for the particular operation it is executing, the *mandatory_features* set\nwill contain all necessary features required for using the cache safely.\n\nConversely, the *ignored_features* set contains only those features which were not\nrelevant to operating the cache. Otherwise, the client would not pass the feature\nset test against the manifest.\n\nWhen opening a cache and the *mandatory_features* set is not a subset of the features\nsupported by the client, the cache is wiped out and rebuilt,\nsince a client not supporting a mandatory feature that the cache was built with\nwould be unable to update it correctly.\nThe assumption behind this behaviour is that any of the unsupported features could have\nbeen reflected in the cache and there is no way for the client to discern whether\nthat is the case.\nMeanwhile, it may not be practical for every feature to have clients using it track\nwhether the feature had an impact on the cache.\nTherefore, the cache is wiped.\n\nWhen opening a cache and the intersection of *ignored_features* and the features\nsupported by the client contains any elements, i.e. the client possesses features\nthat the previous client did not have and those new features are enabled in the repository,\nthe cache is wiped out and rebuilt.\n\nWhile the former condition likely requires no tweaks, the latter condition is formulated\nin an especially conservative way to play it safe. It seems likely that specific features\nmight be exempted from the latter condition.\n\n.. rubric:: Defined feature flags\n\nCurrently no feature flags are defined.\n\nFrom currently planned features, some examples follow,\nthese may/may not be implemented and purely serve as examples.\n\n- A mandatory *read* feature could be using a different encryption scheme (e.g. session keys).\n  This may not be mandatory for the *write* operation - reading data is not strictly required for\n  creating an archive.\n- Any additions to the way chunks are referenced (e.g. to support larger archives) would\n  become a mandatory *delete* and *check* feature; *delete* implies knowing correct\n  reference counts, so all object references need to be understood. *check* must\n  discover the entire object graph as well, otherwise the \"orphan chunks check\"\n  could delete data still in use.\n\n.. _archive:\n\nArchives\n~~~~~~~~\n\nEach archive is an object referenced by an entry below archives/.\nThe archive object itself does not store any of the data contained in the\narchive it describes.\n\nInstead, it contains a list of chunks which form a msgpacked stream of items_.\nThe archive object itself further contains some metadata:\n\n* *version*\n* *name*, which might differ from the name set in the archives/* object.\n  When :ref:`borg_check` rebuilds the manifest (e.g. if it was corrupted) and finds\n  more than one archive object with the same name, it adds a counter to the name\n  in archives/*, but leaves the *name* field of the archives as they were.\n* *item_ptrs*, a list of \"pointer chunk\" IDs.\n  Each \"pointer chunk\" contains a list of chunk IDs of item metadata.\n* *command_line*, the command line which was used to create the archive\n* *hostname*\n* *username*\n* *time* and *time_end* are the start and end timestamps, respectively\n* *comment*, a user-specified archive comment\n* *chunker_params* are the :ref:`chunker-params <chunker-params>` used for creating the archive.\n  This is used by :ref:`borg_recreate` to determine whether a given archive needs rechunking.\n* Some other pieces of information related to recreate.\n\n.. _item:\n\nItems\n~~~~~\n\nEach item represents a file, directory or other file system item and is stored as a\ndictionary created by the ``Item`` class that contains:\n\n* path\n* list of data chunks (size: count * ~40B)\n* user\n* group\n* uid\n* gid\n* mode (item type + permissions)\n* source (for symlinks)\n* hlid (for hardlinks)\n* rdev (for device files)\n* mtime, atime, ctime, birthtime in nanoseconds\n* xattrs\n* acl (various OS-dependent fields)\n* flags\n\nAll items are serialized using msgpack and the resulting byte stream\nis fed into the same chunker algorithm as used for regular file data\nand turned into deduplicated chunks. The reference to these chunks is then added\nto the archive metadata. To achieve a finer granularity on this metadata\nstream, we use different chunker params for this chunker, which result in\nsmaller chunks.\n\nA chunk is stored as an object as well, of course.\n\n.. _chunks:\n.. _chunker_details:\n\nChunks\n~~~~~~\n\nBorg has these chunkers:\n\n- \"fixed\": a simple, low cpu overhead, fixed blocksize chunker, optionally\n  supporting a header block of different size.\n- \"buzhash\": variable, content-defined blocksize, uses a rolling hash\n  computed by the Buzhash_ algorithm.\n- \"buzhash64\": similar to \"buzhash\", but improved 64bit implementation\n\nFor some more general usage hints see also ``--chunker-params``.\n\n\"fixed\" chunker\n+++++++++++++++\n\nThe fixed chunker triggers (chunks) at even-spaced offsets, e.g. every 4MiB,\nproducing chunks of same block size (the last chunk is not required to be\nfull-size).\n\nOptionally, it supports processing a differently sized \"header\" first, before\nit starts to cut chunks of the desired block size.\nThe default is not to have a differently sized header.\n\n``borg create --chunker-params fixed,BLOCK_SIZE[,HEADER_SIZE]``\n\n- BLOCK_SIZE: no default value, multiple of the system page size (usually 4096\n  bytes) recommended. E.g.: 4194304 would cut 4MiB sized chunks.\n- HEADER_SIZE: optional, defaults to 0 (no header).\n\nThe fixed chunker also supports processing sparse files (reading only the ranges\nwith data and seeking over the empty hole ranges).\n\n``borg create --sparse --chunker-params fixed,BLOCK_SIZE[,HEADER_SIZE]``\n\n\"buzhash\" chunker\n+++++++++++++++++\n\nThe buzhash chunker triggers (chunks) when the last HASH_MASK_BITS bits of the\nhash are zero, producing chunks with a target size of 2^HASH_MASK_BITS bytes.\n\nBuzhash is **only** used for cutting the chunks at places defined by the\ncontent, the buzhash value is **not** used as the deduplication criteria (we\nuse a cryptographically strong hash/MAC over the chunk contents for this, the\nid_hash).\n\nThe idea of content-defined chunking is assigning every byte where a\ncut *could* be placed a hash. The hash is based on some number of bytes\n(the window size) before the byte in question. Chunks are cut\nwhere the hash satisfies some condition\n(usually \"n numbers of trailing/leading zeroes\"). This causes chunks to be cut\nin the same location relative to the file's contents, even if bytes are inserted\nor removed before/after a cut, as long as the bytes within the window stay the same.\nThis results in a high chance that a single cluster of changes to a file will only\nresult in 1-2 new chunks, aiding deduplication.\n\nUsing normal hash functions this would be extremely slow,\nrequiring hashing approximately ``window size * file size`` bytes.\nA rolling hash is used instead, which allows to add a new input byte and\ncompute a new hash as well as *remove* a previously added input byte\nfrom the computed hash. This makes the cost of computing a hash for each\ninput byte largely independent of the window size.\n\nBorg defines minimum and maximum chunk sizes (CHUNK_MIN_EXP and CHUNK_MAX_EXP, respectively)\nwhich narrows down where cuts may be made, greatly reducing the amount of data\nthat is actually hashed for content-defined chunking.\n\n``borg create --chunker-params buzhash,CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE``\ncan be used to tune the chunker parameters, the default is:\n\n- CHUNK_MIN_EXP = 19 (minimum chunk size = 2^19 B = 512 kiB)\n- CHUNK_MAX_EXP = 23 (maximum chunk size = 2^23 B = 8 MiB)\n- HASH_MASK_BITS = 21 (target chunk size ~= 2^21 B = 2 MiB)\n- HASH_WINDOW_SIZE = 4095 [B] (`0xFFF`) (must be an odd number)\n\nThe buzhash table is altered by XORing it with a seed randomly generated once\nfor the repository, and stored encrypted in the keyfile. This is to prevent\nchunk size based fingerprinting attacks on your encrypted repo contents (to\nguess what files you have based on a specific set of chunk sizes).\n\n\"buzhash64\" chunker\n+++++++++++++++++++\n\nSimilar to \"buzhash\", but using 64bit wide hash values.\n\nThe buzhash table is cryptographically derived from secret key material.\n\nThese changes should improve resistance against attacks and also solve\nsome of the issues of the original (32bit / XORed table) implementation.\n\n.. _cache:\n\nThe cache\n---------\n\nThe **files cache** is stored in ``cache/files.<SUFFIX>`` and is used at backup\ntime to quickly determine whether a given file is unchanged and we have all its\nchunks.\n\nIn memory, the files cache is a key -> value mapping (a Python *dict*) and contains:\n\n* key: id_hash of the encoded path (same path as seen in archive)\n* value:\n\n  - age (0 [newest], ..., BORG_FILES_CACHE_TTL - 1)\n  - file inode number\n  - file size\n  - file ctime_ns\n  - file mtime_ns\n  - list of chunk (id, size) tuples representing the file's contents\n\nTo determine whether a file has not changed, cached values are looked up via\nthe key in the mapping and compared to the current file attribute values.\n\nIf the file's size, timestamp and inode number is still the same, it is\nconsidered not to have changed. In that case, we check that all file content\nchunks are (still) present in the repository (we check that via the chunks\ncache).\n\nIf everything is matching and all chunks are present, the file is not read /\nchunked / hashed again (but still a file metadata item is written to the\narchive, made from fresh file metadata read from the filesystem). This is\nwhat makes borg so fast when processing unchanged files.\n\nIf there is a mismatch or a chunk is missing, the file is read / chunked /\nhashed. Chunks already present in repo won't be transferred to repo again.\n\nThe inode number is stored and compared to make sure we distinguish between\ndifferent files, as a single path may not be unique across different\narchives in different setups.\n\nNot all filesystems have stable inode numbers. If that is the case, borg can\nbe told to ignore the inode number in the check via --files-cache.\n\nThe age value is used for cache management. If a file is \"seen\" in a backup\nrun, its age is reset to 0, otherwise its age is incremented by one.\nIf a file was not seen in BORG_FILES_CACHE_TTL backups, its cache entry is\nremoved.\n\nThe files cache is a python dictionary, storing python objects, which\ngenerates a lot of overhead.\n\nBorg can also work without using the files cache (saves memory if you have a\nlot of files or not much RAM free), then all files are assumed to have changed.\nThis is usually much slower than with files cache.\n\nThe on-disk format of the files cache is a stream of msgpacked tuples (key, value).\nLoading the files cache involves reading the file, one msgpack object at a time,\nunpacking it, and msgpacking the value (in an effort to save memory).\n\nThe **chunks cache** is not persisted to disk, but dynamically built in memory\nby querying the existing object IDs from the repository.\nIt is used to determine whether we already have a specific chunk.\n\nThe chunks cache is a key -> value mapping and contains:\n\n* key:\n\n  - chunk id_hash\n* value:\n\n  - reference count (always MAX_VALUE as we do not refcount anymore)\n  - size (0 for prev. existing objects, we can't query their plaintext size)\n\nThe chunks cache is a HashIndex_.\n\n.. _cache-memory-usage:\n\nIndexes / Caches memory usage\n-----------------------------\n\nHere is the estimated memory usage of Borg - it's complicated::\n\n  chunk_size ~= 2 ^ HASH_MASK_BITS  (for buzhash chunker, BLOCK_SIZE for fixed chunker)\n  chunk_count ~= total_file_size / chunk_size\n\n  chunks_cache_usage = chunk_count * 40\n\n  files_cache_usage = total_file_count * 240 + chunk_count * 165\n\n  mem_usage ~= chunks_cache_usage + files_cache_usage\n             = chunk_count * 205 + total_file_count * 240\n\nDue to the hashtables, the best/usual/worst cases for memory allocation can\nbe estimated like that::\n\n  mem_allocation = mem_usage / load_factor  # l_f = 0.25 .. 0.75\n\n  mem_allocation_peak = mem_allocation * (1 + growth_factor)  # g_f = 1.1 .. 2\n\nAll units are Bytes.\n\nIt is assuming every chunk is referenced exactly once (if you have a lot of\nduplicate chunks, you will have fewer chunks than estimated above).\n\nIt is also assuming that typical chunk size is 2^HASH_MASK_BITS (if you have\na lot of files smaller than this statistical medium chunk size, you will have\nmore chunks than estimated above, because 1 file is at least 1 chunk).\n\nThe chunks cache and files cache are all implemented as hash tables.\nA hash table must have a significant amount of unused entries to be fast -\nthe so-called load factor gives the used/unused elements ratio.\n\nWhen a hash table gets full (load factor getting too high), it needs to be\ngrown (allocate new, bigger hash table, copy all elements over to it, free old\nhash table) - this will lead to short-time peaks in memory usage each time this\nhappens. Usually does not happen for all hashtables at the same time, though.\nFor small hash tables, we start with a growth factor of 2, which comes down to\n~1.1x for big hash tables.\n\nE.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB.\n\na) with ``create --chunker-params buzhash,10,23,16,4095`` (custom):\n\n  mem_usage  =  2.8GiB\n\nb) with ``create --chunker-params buzhash,19,23,21,4095`` (default):\n\n  mem_usage  =  0.31GiB\n\n.. note:: There is also the ``--files-cache=disabled`` option to disable the files cache.\n   You'll save some memory, but it will need to read / chunk all the files as\n   it can not skip unmodified files then.\n\nHashIndex\n---------\n\nThe chunks cache is implemented as a hash table, with\nonly one slot per bucket, spreading hash collisions to the following\nbuckets. As a consequence the hash is just a start position for a linear\nsearch. If a key is looked up that is not in the table, then the hash table\nis searched from the start position (the hash) until the first empty\nbucket is reached.\n\nThis particular mode of operation is open addressing with linear probing.\n\nWhen the hash table is filled to 75%, its size is grown. When it's\nemptied to 25%, its size is shrunken. Operations on it have a variable\ncomplexity between constant and linear with low factor, and memory overhead\nvaries between 33% and 300%.\n\nIf an element is deleted, and the slot behind the deleted element is not empty,\nthen the element will leave a tombstone, a bucket marked as deleted. Tombstones\nare only removed by insertions using the tombstone's bucket, or by resizing\nthe table. They present the same load to the hash table as a real entry,\nbut do not count towards the regular load factor.\n\nThus, if the number of empty slots becomes too low (recall that linear probing\nfor an element not in the index stops at the first empty slot), the hash table\nis rebuilt. The maximum *effective* load factor, i.e. including tombstones, is 93%.\n\nData in a HashIndex is always stored in little-endian format, which increases\nefficiency for almost everyone, since basically no one uses big-endian processors\nany more.\n\nHashIndex does not use a hashing function, because all keys (save manifest) are\noutputs of a cryptographic hash or MAC and thus already have excellent distribution.\nThus, HashIndex simply uses the first 32 bits of the key as its \"hash\".\n\nThe format is easy to read and write, because the buckets array has the same layout\nin memory and on disk. Only the header formats differ. The on-disk header is\n``struct HashHeader``:\n\n- First, the HashIndex magic, the eight byte ASCII string \"BORG_IDX\".\n- Second, the signed 32-bit number of entries (i.e. buckets which are not deleted and not empty).\n- Third, the signed 32-bit number of buckets, i.e. the length of the buckets array\n  contained in the file, and the modulus for index calculation.\n- Fourth, the signed 8-bit length of keys.\n- Fifth, the signed 8-bit length of values. This has to be at least four bytes.\n\nAll fields are packed.\n\nThe HashIndex is *not* a general purpose data structure.\nThe value size must be at least 4 bytes, and these first bytes are used for in-band\nsignalling in the data structure itself.\n\nThe constant MAX_VALUE (defined as 2**32-1025 = 4294966271) defines the valid range for\nthese 4 bytes when interpreted as an uint32_t from 0 to MAX_VALUE (inclusive).\nThe following reserved values beyond MAX_VALUE are currently in use (byte order is LE):\n\n- 0xffffffff marks empty buckets in the hash table\n- 0xfffffffe marks deleted buckets in the hash table\n\nHashIndex is implemented in C and wrapped with Cython in a class-based interface.\nThe Cython wrapper checks every passed value against these reserved values and\nraises an AssertionError if they are used.\n\n.. _data-encryption:\n\nEncryption\n----------\n\n.. seealso:: The :ref:`borgcrypto` section for an in-depth review.\n\nAEAD modes\n~~~~~~~~~~\n\nFor new repositories, borg only uses modern AEAD ciphers: AES-OCB or CHACHA20-POLY1305.\n\nFor each borg invocation, a new sessionkey is derived from the borg key material\nand the 48bit IV starts from 0 again (both ciphers internally add a 32bit counter\nto our IV, so we'll just count up by 1 per chunk).\n\nThe encryption layout is best seen at the bottom of this diagram:\n\n.. figure:: encryption-aead.png\n    :figwidth: 100%\n    :width: 100%\n\nNo special IV/counter management is needed here due to the use of session keys.\n\nA 48 bit IV is way more than needed: If you only backed up 4kiB chunks (2^12B),\nthe IV would \"limit\" the data encrypted in one session to 2^(12+48)B == 2.3 exabytes,\nmeaning you would run against other limitations (RAM, storage, time) way before that.\nIn practice, chunks are usually bigger, for big files even much bigger, giving an\neven higher limit.\n\nLegacy modes\n~~~~~~~~~~~~\n\nOld repositories (which used AES-CTR mode) are supported read-only to be able to\n``borg transfer`` their archives to new repositories (which use AEAD modes).\n\nAES-CTR mode is not supported for new repositories and the related code will be\nremoved in a future release.\n\nBoth modes\n~~~~~~~~~~\n\nEncryption keys (and other secrets) are kept either in a key file on the client\n('keyfile' mode) or in the repository under keys/repokey ('repokey' mode).\nIn both cases, the secrets are generated from random and then encrypted by a\nkey derived from your passphrase (this happens on the client before the key\nis stored into the keyfile or as repokey).\n\nThe passphrase is passed through the ``BORG_PASSPHRASE`` environment variable\nor prompted for interactive usage.\n\n.. _key_files:\n\nKey files\n---------\n\n.. seealso:: The :ref:`key_encryption` section for an in-depth review of the key encryption.\n\nWhen initializing a repository with one of the \"keyfile\" encryption modes,\nBorg creates an associated key file in the keys subdirectory of the borg config\ndirectory (see :ref:`env_vars` for platform-specific default paths).\n\nThe same key is also used in the \"repokey\" modes, which store it in the repository.\n\nThe internal data structure is as follows:\n\nversion\n  currently always an integer, 2\n\nrepository_id\n  the ``id`` field in the ``config`` ``INI`` file of the repository.\n\ncrypt_key\n  the initial key material used for the AEAD crypto (512 bits)\n\nid_key\n  the key used to MAC the plaintext chunk data to compute the chunk's id\n\nchunk_seed\n  the seed for the buzhash chunking table (signed 32 bit integer)\n\nThese fields are packed using msgpack_. The utf-8 encoded passphrase\nis processed with argon2_ to derive a 256 bit key encryption key (KEK).\n\nThen the KEK is used to encrypt and authenticate the packed data using\nthe chacha20-poly1305 AEAD cipher.\n\nThe result is stored in a another msgpack_ formatted as follows:\n\nversion\n  currently always an integer, 1\n\nsalt\n  random 256 bits salt used to process the passphrase\n\nargon2_*\n  some parameters for the argon2 kdf\n\nalgorithm\n  the algorithms used to process the passphrase\n  (currently the string ``argon2 chacha20-poly1305``)\n\ndata\n  The encrypted, packed fields.\n\nThe resulting msgpack_ is then encoded using base64 and written to the\nkey file, wrapped using the standard ``textwrap`` module with a header.\nThe header is a single line with a MAGIC string, a space and a hexadecimal\nrepresentation of the repository id.\n\n.. _data-compression:\n\nCompression\n-----------\n\nBorg supports the following compression methods, each identified by a ctype value\nin the range between 0 and 255 (and augmented by a clevel 0..255 value for the\ncompression level):\n\n- none (no compression, pass through data 1:1), identified by 0x00\n- lz4 (low compression, but super fast), identified by 0x01\n- zstd (level 1-22 offering a wide range: level 1 is lower compression and high\n  speed, level 22 is higher compression and lower speed) - identified by 0x03\n- zlib (level 0-9, level 0 is no compression [but still adding zlib overhead],\n  level 1 is low, level 9 is high compression), identified by 0x05\n- lzma (level 0-9, level 0 is low, level 9 is high compression), identified\n  by 0x02.\n\nThe type byte is followed by a byte indicating the compression level.\n\nSpeed:  none > lz4 > zlib > lzma, lz4 > zstd\nCompression: lzma > zlib > lz4 > none, zstd > lz4\n\nBe careful, higher compression levels might use a lot of resources (CPU/memory).\n\nThe overall speed of course also depends on the speed of your target storage.\nIf that is slow, using a higher compression level might yield better overall\nperformance. You need to experiment a bit. Maybe just watch your CPU load, if\nthat is relatively low, increase compression until 1 core is 70-100% loaded.\n\nEven if your target storage is rather fast, you might see interesting effects:\nwhile doing no compression at all (none) is a operation that takes no time, it\nlikely will need to store more data to the storage compared to using lz4.\nThe time needed to transfer and store the additional data might be much more\nthan if you had used lz4 (which is super fast, but still might compress your\ndata about 2:1). This is assuming your data is compressible (if you back up\nalready compressed data, trying to compress them at backup time is usually\npointless).\n\nCompression is applied after deduplication, thus using different compression\nmethods in one repo does not influence deduplication.\n\nSee ``borg create --help`` about how to specify the compression level and its default.\n\nLock files (fslocking)\n----------------------\n\nBorg uses filesystem locks to get (exclusive or shared) access to the cache.\n\nThe locking system is based on renaming a temporary directory\nto `lock.exclusive` (for\nexclusive locks). Inside this directory, there is a file indicating\nhostname, process id and thread id of the lock holder.\n\nThere is also a json file `lock.roster` that keeps a directory of all shared\nand exclusive lockers.\n\nIf the process is able to rename a temporary directory (with the\nhost/process/thread identifier prepared inside it) in the resource directory\nto `lock.exclusive`, it has the lock for it. If renaming fails\n(because this directory already exists and its host/process/thread identifier\ndenotes a thread on the host which is still alive), lock acquisition fails.\n\nThe cache lock is usually in `~/.cache/borg/REPOID/lock.*`.\n\nLocks (storelocking)\n--------------------\n\nTo implement locking based on ``borgstore``, borg stores objects below locks/.\n\nThe objects contain:\n\n- a timestamp when lock was created (or refreshed)\n- host / process / thread information about lock owner\n- lock type: exclusive or shared\n\nUsing that information, borg implements:\n\n- lock auto-expiry: if a lock is old and has not been refreshed in time,\n  it will be automatically ignored and deleted. the primary purpose of this\n  is to get rid of stale locks by borg processes on other machines.\n- lock auto-removal if the owner process is dead. the primary purpose of this\n  is to quickly get rid of stale locks by borg processes on the same machine.\n\nBreaking the locks\n------------------\n\nIn case you run into troubles with the locks, you can use the ``borg break-lock``\ncommand after you first have made sure that no Borg process is\nrunning on any machine that accesses this resource. Be very careful, the cache\nor repository might get damaged if multiple processes use it at the same time.\n\nIf there is an issue just with the repository lock, it will usually resolve\nautomatically (see above), just retry later.\n\n\nChecksumming data structures\n----------------------------\n\nAs detailed in the previous sections, Borg generates and stores various files\ncontaining important meta data, such as the files cache.\n\nData corruption in the files cache could create incorrect archives, e.g. due\nto wrong object IDs or sizes in the files cache.\n\nTherefore, Borg calculates checksums when writing these files and tests checksums\nwhen reading them. Checksums are generally 64-bit XXH64 hashes.\nThe canonical xxHash representation is used, i.e. big-endian.\nChecksums are stored as hexadecimal ASCII strings.\n\nFor compatibility, checksums are not required and absent checksums do not trigger errors.\nThe mechanisms have been designed to avoid false-positives when various Borg\nversions are used alternately on the same repositories.\n\nChecksums are a data safety mechanism. They are not a security mechanism.\n\n.. rubric:: Choice of algorithm\n\nXXH64 has been chosen for its high speed on all platforms, which avoids performance\ndegradation in CPU-limited parts (e.g. cache synchronization).\nUnlike CRC32, it neither requires hardware support (crc32c or CLMUL)\nnor vectorized code nor large, cache-unfriendly lookup tables to achieve good performance.\nThis simplifies deployment of it considerably (cf. src/borg/algorithms/crc32...).\n\nFurther, XXH64 is a non-linear hash function and thus has a \"more or less\" good\nchance to detect larger burst errors, unlike linear CRCs where the probability\nof detection decreases with error size.\n\nThe 64-bit checksum length is considered sufficient for the file sizes typically\nchecksummed (individual files up to a few GB, usually less).\nxxHash was expressly designed for data blocks of these sizes.\n\nLower layer — file_integrity\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThere is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile)\nwrapping a file-like object, performing streaming calculation and comparison\nof checksums.\nChecksum errors are signalled by raising an exception at the earliest possible\nmoment (borg.crypto.file_integrity.FileIntegrityError).\n\n.. rubric:: Calculating checksums\n\nBefore feeding the checksum algorithm any data, the file name (i.e. without any path)\nis mixed into the checksum, since the name encodes the context of the data for Borg.\n\nThe various indices used by Borg have separate header and main data parts.\nIntegrityCheckedFile allows borg to checksum them independently, which avoids\neven reading the data when the header is corrupted. When a part is signalled,\nthe length of the part name is mixed into the checksum state first (encoded\nas an ASCII string via `%10d` printf format), then the name of the part\nis mixed in as an UTF-8 string. Lastly, the current position (length)\nin the file is mixed in as well.\n\nThe checksum state is not reset at part boundaries.\n\nA final checksum is always calculated in the same way as the parts described above,\nafter seeking to the end of the file. The final checksum cannot prevent code\nfrom processing corrupted data during reading, however, it prevents use of the\ncorrupted data.\n\n.. rubric:: Serializing checksums\n\nAll checksums are compiled into a simple JSON structure called *integrity data*:\n\n.. code-block:: json\n\n    {\n        \"algorithm\": \"XXH64\",\n        \"digests\": {\n            \"HashHeader\": \"eab6802590ba39e3\",\n            \"final\": \"e2a7f132fc2e8b24\"\n        }\n    }\n\nThe *algorithm* key notes the used algorithm. When reading, integrity data containing\nan unknown algorithm is not inspected further.\n\nThe *digests* key contains a mapping of part names to their digests.\n\nIntegrity data is generally stored by the upper layers, introduced below. An exception\nis the DetachedIntegrityCheckedFile, which automatically writes and reads it from\na \".integrity\" file next to the data file.\n\nUpper layer\n~~~~~~~~~~~\n\n.. rubric:: Main cache files: chunks and files cache\n\nThe integrity data of the ``files`` cache is stored in the cache ``config``.\n\nThe ``[integrity]`` section is used:\n\n.. code-block:: none\n\n    [cache]\n    version = 1\n    repository = 3c4...e59\n    manifest = 10e...21c\n    timestamp = 2017-06-01T21:31:39.699514\n    key_type = 2\n    previous_location = /path/to/repo\n\n    [integrity]\n    manifest = 10e...21c\n    files = {\"algorithm\": \"XXH64\", \"digests\": {\"HashHeader\": \"eab...39e3\", \"final\": \"e2a...b24\"}}\n\nThe manifest ID is duplicated in the integrity section due to the way all Borg\nversions handle the config file. Instead of creating a \"new\" config file from\nan internal representation containing only the data understood by Borg,\nthe config file is read in entirety (using the Python ConfigParser) and modified.\nThis preserves all sections and values not understood by the Borg version\nmodifying it.\n\nThus, if an older versions uses a cache with integrity data, it would preserve\nthe integrity section and its contents. If a integrity-aware Borg version\nwould read this cache, it would incorrectly report checksum errors, since\nthe older version did not update the checksums.\n\nHowever, by duplicating the manifest ID in the integrity section, it is\neasy to tell whether the checksums concern the current state of the cache.\n\nIntegrity errors are fatal in these files, terminating the program,\nand are not automatically corrected at this time.\n\n\nHardLinkManager and the hlid concept\n------------------------------------\n\nDealing with hard links needs some extra care, implemented in borg within the HardLinkManager\nclass:\n\n- At archive creation time, fs items with st_nlink > 1 indicate that they are a member of\n  a group of hardlinks all pointing to the same inode. For such fs items, the archived item\n  includes a hlid attribute (hardlink id), which is computed like H(st_dev, st_ino). Thus,\n  if archived items have the same hlid value, they pointed to the same inode and form a\n  group of hardlinks. Besides that, nothing special is done for any member of the group\n  of hardlinks, meaning that e.g. for regular files, each archived item will have a\n  chunks list.\n- At extraction time, the presence of a hlid attribute indicates that there might be more\n  hardlinks coming, pointing to the same content (inode), thus borg will remember the \"hlid\n  to extracted path\" mapping, so it will know the correct path for extracting (hardlinking)\n  the next hardlink of that group / with the same hlid.\n- This symmetric approach (each item has all the information, e.g. the chunks list)\n  simplifies dealing with such items a lot, especially for partial extraction, for the\n  FUSE filesystem, etc.\n- This is different from the asymmetric approach of old borg versions (< 2.0) and also from\n  tar which have the concept of a main item (first hardlink, has the content) and content-less\n  secondary items with by-name back references for each subsequent hardlink, causing lots\n  of complications when dealing with them.\n"
  },
  {
    "path": "docs/internals/frontends.rst",
    "content": ".. include:: ../global.rst.inc\n.. highlight:: none\n\n.. _json_output:\n\nAll about JSON: How to develop frontends\n========================================\n\nBorg does not have a public API on the Python level. That does not keep you from writing :code:`import borg`,\nbut does mean that there are no release-to-release guarantees on what you might find in that package, not\neven for point releases (1.1.x), and there is no documentation beyond the code and the internals documents.\n\nBorg does on the other hand provide an API on a command-line level. In other words, a frontend should\n(for example) create a backup archive by invoking :ref:`borg_create`, provide command-line parameters/options\nas needed, and parse JSON output from Borg.\n\nImportant: JSON output is expected to be UTF-8, but currently borg depends on the locale being configured\nfor that (must be a UTF-8 locale and *not* \"C\" or \"ascii\"), so that Python will choose to encode to UTF-8.\nThe same applies to any inputs read by borg, they are expected to be UTF-8 encoded also.\n\nOn POSIX systems, you can usually set environment vars to choose a UTF-8 locale:\n\n::\n\n    export LANG=en_US.UTF-8\n    export LC_CTYPE=en_US.UTF-8\n\n\nAnother way to get Python's stdin/stdout/stderr streams to use UTF-8 encoding (without having\na UTF-8 locale / LANG / LC_CTYPE) is:\n\n::\n\n    export PYTHONIOENCODING=utf-8\n\n\nSee :issue:`2273` for more details.\n\n\nDealing with non-unicode byte sequences and JSON limitations\n------------------------------------------------------------\n\nPaths on POSIX systems can have arbitrary bytes in them (except 0x00 which is used as string terminator in C).\n\nNowadays, UTF-8 encoded paths (which decode to valid unicode) are the usual thing, but a lot of systems\nstill have paths from the past, when other, non-unicode codings were used. Especially old Samba shares often\nhave wild mixtures of misc. encodings, sometimes even very broken stuff.\n\nborg deals with such non-unicode paths (\"with funny/broken characters\") by decoding such byte sequences using\nUTF-8 coding and \"surrogateescape\" error handling mode, which maps invalid bytes to special unicode code points\n(surrogate escapes). When encoding such a unicode string back to a byte sequence, the original byte sequence\nwill be reproduced exactly.\n\nJSON should only contain valid unicode text without any surrogate escapes, so we can't just directly have a\nsurrogate-escaped path in JSON (\"path\" is only one example, this also affects other text-like content).\n\nBorg deals with this situation like this (since borg 2.0):\n\nFor a valid unicode path (no surrogate escapes), the JSON will only have \"path\": path.\n\nFor a non-unicode path (with surrogate escapes), the JSON will have 2 entries:\n\n- \"path\": path_approximation (pure valid unicode, all invalid bytes will show up as \"?\")\n- \"path_b64\": path_bytes_base64_encoded (if you decode the base64, you get the original path byte string)\n\nJSON users need to pick whatever suits their needs best. The suggested procedure (shown for \"path\") is:\n\n- check if there is a \"path_b64\" key.\n- if it is there, you will know that the original bytes path did not cleanly UTF-8-decode into unicode (has\n  some invalid bytes) and that the string given by the \"path\" key is only an approximation, but not the precise\n  path. if you need precision, you must base64-decode the value of \"path_b64\" and deal with the arbitrary byte\n  string you'll get. if an approximation is fine, use the value of the \"path\" key.\n- if it is not there, the value of the \"path\" key is all you need (the original bytes path is its UTF-8 encoding).\n\n\nLogging\n-------\n\nEspecially for graphical frontends it is important to be able to convey and reformat progress information\nin meaningful ways. The ``--log-json`` option turns the stderr stream of Borg into a stream of JSON lines,\nwhere each line is a JSON object. The *type* key of the object determines its other contents.\n\n.. warning:: JSON logging requires successful argument parsing. Even with ``--log-json`` specified, a\n    parsing error will be printed in plain text, because logging set-up happens after all arguments are\n    parsed.\n\nThe following types are in use. Progress information is governed by the usual rules for progress information,\nit is not produced unless ``--progress`` is specified.\n\narchive_progress\n    Output during operations creating archives (:ref:`borg_create` and :ref:`borg_recreate`).\n    The following keys exist, each represents the current progress.\n\n    original_size\n        Original size of data processed so far (before compression and deduplication, may be empty/absent)\n    compressed_size\n        Compressed size (may be empty/absent)\n    deduplicated_size\n        Deduplicated size (may be empty/absent)\n    nfiles\n        Number of (regular) files processed so far (may be empty/absent)\n    path\n        Current path (may be empty/absent)\n    time\n        Unix timestamp (float)\n    finished\n        boolean indicating whether the operation has finished, only the last object for an *operation*\n        can have this property set to *true*.\n\nprogress_message\n    A message-based progress information with no concrete progress information, just a message\n    saying what is currently being worked on.\n\n    operation\n        unique, opaque integer ID of the operation\n    :ref:`msgid <msgid>`\n        Message ID of the operation (may be *null*)\n    finished\n        boolean indicating whether the operation has finished, only the last object for an *operation*\n        can have this property set to *true*.\n    message\n        current progress message (may be empty/absent)\n    time\n        Unix timestamp (float)\n\nprogress_percent\n    Absolute progress information with defined end/total and current value.\n\n    operation\n        unique, opaque integer ID of the operation\n    :ref:`msgid <msgid>`\n        Message ID of the operation (may be *null*)\n    finished\n        boolean indicating whether the operation has finished, only the last object for an *operation*\n        can have this property set to *true*.\n    message\n        A formatted progress message, this will include the percentage and perhaps other information\n        (absent for finished == true)\n    current\n        Current value (always less-or-equal to *total*, absent for finished == true)\n    info\n        Array that describes the current item, may be *null*, contents depend on *msgid*\n        (absent for finished == true)\n    total\n        Total value (absent for finished == true)\n    time\n        Unix timestamp (float)\n\nfile_status\n    This is only output by :ref:`borg_create` and :ref:`borg_recreate` if ``--list`` is specified. The usual\n    rules for the file listing applies, including the ``--filter`` option.\n\n    status\n        Single-character status as for regular list output\n    path\n        Path of the file system object\n\nlog_message\n    Any regular log output invokes this type. Regular log options and filtering applies to these as well.\n\n    time\n        Unix timestamp (float)\n    levelname\n        Upper-case log level name (also called severity). Defined levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL\n    name\n        Name of the emitting entity\n    message\n        Formatted log message\n    :ref:`msgid <msgid>`\n        Message ID, may be *null* or absent\n\nSee Prompts_ for the types used by prompts.\n\n.. rubric:: Examples (reformatted, each object would be on exactly one line)\n.. highlight:: json\n\n:ref:`borg_extract` progress::\n\n    {\"message\": \"100.0% Extracting: src/borgbackup.egg-info/entry_points.txt\",\n     \"current\": 13000228, \"total\": 13004993, \"info\": [\"src/borgbackup.egg-info/entry_points.txt\"],\n     \"operation\": 1, \"msgid\": \"extract\", \"type\": \"progress_percent\", \"finished\": false}\n    {\"message\": \"100.0% Extracting: src/borgbackup.egg-info/SOURCES.txt\",\n     \"current\": 13004993, \"total\": 13004993, \"info\": [\"src/borgbackup.egg-info/SOURCES.txt\"],\n     \"operation\": 1, \"msgid\": \"extract\", \"type\": \"progress_percent\", \"finished\": false}\n    {\"operation\": 1, \"msgid\": \"extract\", \"type\": \"progress_percent\", \"finished\": true}\n\n:ref:`borg_create` file listing with progress::\n\n    {\"original_size\": 0, \"compressed_size\": 0, \"deduplicated_size\": 0, \"nfiles\": 0, \"type\": \"archive_progress\", \"path\": \"src\"}\n    {\"type\": \"file_status\", \"status\": \"U\", \"path\": \"src/borgbackup.egg-info/entry_points.txt\"}\n    {\"type\": \"file_status\", \"status\": \"U\", \"path\": \"src/borgbackup.egg-info/SOURCES.txt\"}\n    {\"type\": \"file_status\", \"status\": \"d\", \"path\": \"src/borgbackup.egg-info\"}\n    {\"type\": \"file_status\", \"status\": \"d\", \"path\": \"src\"}\n    {\"original_size\": 13176040, \"compressed_size\": 11386863, \"deduplicated_size\": 503, \"nfiles\": 277, \"type\": \"archive_progress\", \"path\": \"\"}\n\nInternal transaction progress::\n\n    {\"message\": \"Saving files cache\", \"operation\": 2, \"msgid\": \"cache.commit\", \"type\": \"progress_message\", \"finished\": false}\n    {\"message\": \"Saving cache config\", \"operation\": 2, \"msgid\": \"cache.commit\", \"type\": \"progress_message\", \"finished\": false}\n    {\"message\": \"Saving chunks cache\", \"operation\": 2, \"msgid\": \"cache.commit\", \"type\": \"progress_message\", \"finished\": false}\n    {\"operation\": 2, \"msgid\": \"cache.commit\", \"type\": \"progress_message\", \"finished\": true}\n\nA debug log message::\n\n    {\"message\": \"35 self tests completed in 0.08 seconds\",\n     \"type\": \"log_message\", \"created\": 1488278449.5575905, \"levelname\": \"DEBUG\", \"name\": \"borg.archiver\"}\n\nPrompts\n-------\n\nPrompts assume a JSON form as well when the ``--log-json`` option is specified. Responses\nare still read verbatim from *stdin*, while prompts are JSON messages printed to *stderr*,\njust like log messages.\n\nPrompts use the *question_prompt* and *question_prompt_retry* types for the prompt itself,\nand *question_invalid_answer*, *question_accepted_default*, *question_accepted_true*,\n*question_accepted_false* and *question_env_answer* types for information about\nprompt processing.\n\nThe *message* property contains the same string displayed regularly in the same situation,\nwhile the *msgid* property may contain a msgid_, typically the name of the\nenvironment variable that can be used to override the prompt. It is the same for all JSON\nmessages pertaining to the same prompt.\n\n.. rubric:: Examples (reformatted, each object would be on exactly one line)\n.. highlight:: none\n\nProviding an invalid answer::\n\n    {\"type\": \"question_prompt\", \"msgid\": \"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\",\n     \"message\": \"... Type 'YES' if you understand this and want to continue: \"}\n    incorrect answer  # input on stdin\n    {\"type\": \"question_invalid_answer\", \"msgid\": \"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\", \"is_prompt\": false,\n     \"message\": \"Invalid answer, aborting.\"}\n\nProviding a false (negative) answer::\n\n    {\"type\": \"question_prompt\", \"msgid\": \"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\",\n     \"message\": \"... Type 'YES' if you understand this and want to continue: \"}\n    NO  # input on stdin\n    {\"type\": \"question_accepted_false\", \"msgid\": \"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\",\n     \"message\": \"Aborting.\", \"is_prompt\": false}\n\nProviding a true (affirmative) answer::\n\n    {\"type\": \"question_prompt\", \"msgid\": \"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\",\n     \"message\": \"... Type 'YES' if you understand this and want to continue: \"}\n    YES  # input on stdin\n    # no further output, just like the prompt without --log-json\n\nPassphrase prompts\n------------------\n\nPassphrase prompts should be handled differently. Use the environment variables *BORG_PASSPHRASE*\nand *BORG_NEW_PASSPHRASE* (see :ref:`env_vars` for reference) to pass passphrases to Borg, don't\nuse the interactive passphrase prompts.\n\nWhen setting a new passphrase (:ref:`borg_repo-create`, :ref:`borg_key_change-passphrase`) normally\nBorg prompts whether it should display the passphrase. This can be suppressed by setting\nthe environment variable *BORG_DISPLAY_PASSPHRASE* to *no*.\n\nWhen \"confronted\" with an unknown repository, where the application does not know whether\nthe repository is encrypted, the following algorithm can be followed to detect encryption:\n\n1. Set *BORG_PASSPHRASE* to gibberish (for example a freshly generated UUID4, which cannot\n   possibly be the passphrase)\n2. Invoke ``borg list repository ...``\n3. If this fails, due the repository being encrypted and the passphrase obviously being\n   wrong, you'll get an error with the *PassphraseWrong* msgid.\n\n   The repository is encrypted, for further access the application will need the passphrase.\n\n4. If this does not fail, then the repository is not encrypted.\n\nStandard output\n---------------\n\n*stdout* is different and more command-dependent than logging. Commands like :ref:`borg_info`, :ref:`borg_create`\nand :ref:`borg_list` implement a ``--json`` option which turns their regular output into a single JSON object.\n\nSome commands, like :ref:`borg_list` and :ref:`borg_diff`, can produce *a lot* of JSON. Since many JSON implementations\ndon't support a streaming mode of operation, which is pretty much required to deal with this amount of JSON, these\ncommands implement a ``--json-lines`` option which generates output in the `JSON lines <https://jsonlines.org/>`_ format,\nwhich is simply a number of JSON objects separated by new lines.\n\nDates are formatted according to ISO 8601 in local time. No explicit time zone is specified *at this time*\n(subject to change). The equivalent strftime format string is '%Y-%m-%dT%H:%M:%S.%f',\ne.g. ``2017-08-07T12:27:20.123456``.\n\nThe root object of '--json' output will contain at least a *repository* key with an object containing:\n\nid\n    The ID of the repository, normally 64 hex characters\nlocation\n    Canonicalized repository path, thus this may be different from what is specified on the command line\nlast_modified\n    Date when the repository was last modified by the Borg client\n\nThe *encryption* key, if present, contains:\n\nmode\n    Textual encryption mode name (same as :ref:`borg_repo-create` ``--encryption`` names)\nkeyfile\n    Path to the local key file used for access. Depending on *mode* this key may be absent.\n\nThe *cache* key, if present, contains:\n\npath\n    Path to the local repository cache\nstats\n    Object containing cache stats:\n\n    total_chunks\n        Number of chunks\n    total_unique_chunks\n        Number of unique chunks\n    total_size\n        Total uncompressed size of all chunks multiplied with their reference counts\n    unique_size\n        Uncompressed size of all chunks\n\n.. highlight: json\n\nExample *borg info* output::\n\n    {\n        \"cache\": {\n            \"path\": \"/home/user/.cache/borg/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n            \"stats\": {\n                \"total_chunks\": 511533,\n                \"total_size\": 22635749792,\n                \"total_unique_chunks\": 54892,\n                \"unique_size\": 2449675468\n            }\n        },\n        \"encryption\": {\n            \"mode\": \"repokey\"\n        },\n        \"repository\": {\n            \"id\": \"0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n            \"last_modified\": \"2017-08-07T12:27:20.789123\",\n            \"location\": \"/home/user/testrepo\"\n        },\n        \"security_dir\": \"/home/user/.config/borg/security/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n        \"archives\": []\n    }\n\nArchive formats\n+++++++++++++++\n\n:ref:`borg_info` uses an extended format for archives, which is more expensive to retrieve, while\n:ref:`borg_list` uses a simpler format that is faster to retrieve. Either return archives in an\narray under the *archives* key, while :ref:`borg_create` returns a single archive object under the\n*archive* key.\n\nBoth formats contain a *name* key with the archive name, the *id* key with the hexadecimal archive ID,\nand the *start* key with the start timestamp.\n\n*borg info* and *borg create* further have:\n\nend\n    End timestamp\nduration\n    Duration in seconds between start and end in seconds (float)\nstats\n    Archive statistics (freshly calculated, this is what makes \"info\" more expensive)\n\n    original_size\n        Size of files and metadata before compression\n    compressed_size\n        Size after compression\n    deduplicated_size\n        Deduplicated size (against the current repository, not when the archive was created)\n    nfiles\n        Number of regular files in the archive\ncommand_line\n    Array of strings of the command line that created the archive\n\n    The note about paths from above applies here as well.\nchunker_params\n    The chunker parameters the archive has been created with.\n\n:ref:`borg_info` further has:\n\nhostname\n    Hostname of the creating host\nusername\n    Name of the creating user\ncomment\n    Archive comment, if any\n\nSome keys/values are more expensive to compute than others (e.g. because it requires opening the archive,\nnot just the manifest). To optimize for speed, `borg list repo` does not determine these values except\nwhen they are requested. The `--format` option is used for that (for normal mode as well as for `--json`\nmode), so, to have the comment included in the json output, you will need:\n\n::\n\n    borg list repo --format \"{name}{comment}\" --json`\n\n\nExample of a simple archive listing (``borg list --last 1 --json``)::\n\n    {\n        \"archives\": [\n            {\n                \"id\": \"80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a\",\n                \"name\": \"host-system-backup-2017-02-27\",\n                \"start\": \"2017-08-07T12:27:20.789123\"\n            }\n        ],\n        \"encryption\": {\n            \"mode\": \"repokey\"\n        },\n        \"repository\": {\n            \"id\": \"0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n            \"last_modified\": \"2017-08-07T12:27:20.789123\",\n            \"location\": \"/home/user/repository\"\n        }\n    }\n\nThe same archive with more information (``borg info --last 1 --json``)::\n\n    {\n        \"archives\": [\n            {\n                \"chunker_params\": [\n                    \"buzhash\",\n                    13,\n                    23,\n                    16,\n                    4095\n                ],\n                \"command_line\": [\n                    \"/home/user/.local/bin/borg\",\n                    \"create\",\n                    \"/home/user/repository\",\n                    \"...\"\n                ],\n                \"comment\": \"\",\n                \"duration\": 5.641542,\n                \"end\": \"2017-02-27T12:27:20.789123\",\n                \"hostname\": \"host\",\n                \"id\": \"80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a\",\n                \"name\": \"host-system-backup-2017-02-27\",\n                \"start\": \"2017-02-27T12:27:20.789123\",\n                \"stats\": {\n                    \"compressed_size\": 1880961894,\n                    \"deduplicated_size\": 2791,\n                    \"nfiles\": 53669,\n                    \"original_size\": 2400471280\n                },\n                \"username\": \"user\"\n            }\n        ],\n        \"cache\": {\n            \"path\": \"/home/user/.cache/borg/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n            \"stats\": {\n                \"total_chunks\": 511533,\n                \"total_size\": 22635749792,\n                \"total_unique_chunks\": 54892,\n                \"unique_size\": 2449675468\n            }\n        },\n        \"encryption\": {\n            \"mode\": \"repokey\"\n        },\n        \"repository\": {\n            \"id\": \"0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23\",\n            \"last_modified\": \"2017-08-07T12:27:20.789123\",\n            \"location\": \"/home/user/repository\"\n        }\n    }\n\nFile listings\n+++++++++++++\n\nEach archive item (file, directory, ...) is described by one object in the :ref:`borg_list` output.\nRefer to the *borg list* documentation for the available keys and their meaning.\n\nExample (excerpt) of ``borg list --json-lines``::\n\n    {\"type\": \"d\", \"mode\": \"drwxr-xr-x\", \"user\": \"user\", \"group\": \"user\", \"uid\": 1000, \"gid\": 1000, \"path\": \"linux\", \"target\": \"\", \"flags\": null, \"mtime\": \"2017-02-27T12:27:20.023407\", \"size\": 0}\n    {\"type\": \"d\", \"mode\": \"drwxr-xr-x\", \"user\": \"user\", \"group\": \"user\", \"uid\": 1000, \"gid\": 1000, \"path\": \"linux/baz\", \"target\": \"\", \"flags\": null, \"mtime\": \"2017-02-27T12:27:20.585407\", \"size\": 0}\n\n\nArchive Differencing\n++++++++++++++++++++\n\nEach archive difference item (file contents, user/group/mode) output by :ref:`borg_diff` is represented by an *ItemDiff* object.\nThe properties of an *ItemDiff* object are:\n\npath:\n    The filename/path of the *Item* (file, directory, symlink).\n\nchanges:\n    A list of *Change* objects describing the changes made to the item in the two archives. For example,\n    there will be two changes if the contents of a file are changed, and its ownership are changed.\n\nThe *Change* object can contain a number of properties depending on the type of change that occurred.\nIf a 'property' is not required for the type of change, it is not output.\nThe possible properties of a *Change* object are:\n\ntype:\n  The **type** property is always present. It identifies the type of change and will be one of these values:\n\n  - *modified* - file contents changed.\n  - *added* - the file was added.\n  - *removed* - the file was removed.\n  - *added directory* - the directory was added.\n  - *removed directory* - the directory was removed.\n  - *added link* - the symlink was added.\n  - *removed link* - the symlink was removed.\n  - *changed link* - the symlink target was changed.\n  - *mode* - the file/directory/link mode was changed. Note - this could indicate a change from a\n    file/directory/link type to a different type (file/directory/link), such as -- a file is deleted and replaced\n    with a directory of the same name.\n  - *owner* - user and/or group ownership changed.\n\nsize:\n    If **type** == '*added*' or '*removed*', then **size** provides the size of the added or removed file.\n\nadded:\n    If **type** == '*modified*' and chunk ids can be compared, then **added** and **removed** indicate the amount\n    of data 'added' and 'removed'. If chunk ids can not be compared, then **added** and **removed** properties are\n    not provided and the only information available is that the file contents were modified.\n\nremoved:\n    See **added** property.\n\nold_mode:\n    If **type** == '*mode*', then **old_mode** and **new_mode** provide the mode and permissions changes.\n\nnew_mode:\n    See **old_mode** property.\n\nold_user:\n    If **type** == '*owner*', then **old_user**, **new_user**, **old_group** and **new_group** provide the user\n    and group ownership changes.\n\nold_group:\n    See **old_user** property.\n\nnew_user:\n    See **old_user** property.\n\nnew_group:\n    See **old_user** property.\n\n\nExample (excerpt) of ``borg diff --json-lines``::\n\n    {\"path\": \"file1\", \"changes\": [{\"path\": \"file1\", \"changes\": [{\"type\": \"modified\", \"added\": 17, \"removed\": 5}, {\"type\": \"mode\", \"old_mode\": \"-rw-r--r--\", \"new_mode\": \"-rwxr-xr-x\"}]}]}\n    {\"path\": \"file2\", \"changes\": [{\"type\": \"modified\", \"added\": 135, \"removed\": 252}]}\n    {\"path\": \"file4\", \"changes\": [{\"type\": \"added\", \"size\": 0}]}\n    {\"path\": \"file3\", \"changes\": [{\"type\": \"removed\", \"size\": 0}]}\n\n\n.. _msgid:\n\nMessage IDs\n-----------\n\nMessage IDs are strings that essentially give a log message or operation a name, without actually using the\nfull text, since texts change more frequently. Message IDs are unambiguous and reduce the need to parse\nlog messages.\n\nAssigned message IDs and related error RCs (exit codes) are:\n\n.. See scripts/errorlist.py; this is slightly edited.\n\nErrors\n    Error rc: 2 traceback: no\n        Error: {}\n    ErrorWithTraceback rc: 2 traceback: yes\n        Error: {}\n\n    Buffer.MemoryLimitExceeded rc: 2 traceback: no\n        Requested buffer size {} is above the limit of {}.\n    EfficientCollectionQueue.SizeUnderflow rc: 2 traceback: no\n        Could not pop_front first {} elements, collection only has {} elements..\n    RTError rc: 2 traceback: no\n        Runtime Error: {}\n\n    CancelledByUser rc: 3 traceback: no\n        Cancelled by user.\n\n    CommandError rc: 4 traceback: no\n        Command Error: {}\n    PlaceholderError rc: 5 traceback: no\n        Formatting Error: \"{}\".format({}): {}({})\n    InvalidPlaceholder rc: 6 traceback: no\n        Invalid placeholder \"{}\" in string: {}\n\n    Repository.AlreadyExists rc: 10 traceback: no\n        A repository already exists at {}.\n    Repository.CheckNeeded rc: 12 traceback: yes\n        Inconsistency detected. Please run \"borg check {}\".\n    Repository.DoesNotExist rc: 13 traceback: no\n        Repository {} does not exist.\n    Repository.InsufficientFreeSpaceError rc: 14 traceback: no\n        Insufficient free space to complete transaction (required: {}, available: {}).\n    Repository.InvalidRepository rc: 15 traceback: no\n        {} is not a valid repository. Check repo config.\n    Repository.InvalidRepositoryConfig rc: 16 traceback: no\n        {} does not have a valid configuration. Check repo config [{}].\n    Repository.ObjectNotFound rc: 17 traceback: yes\n        Object with key {} not found in repository {}.\n    Repository.ParentPathDoesNotExist rc: 18 traceback: no\n        The parent path of the repo directory [{}] does not exist.\n    Repository.PathAlreadyExists rc: 19 traceback: no\n        There is already something at {}.\n    Repository.PathPermissionDenied rc: 21 traceback: no\n        Permission denied to {}.\n\n    MandatoryFeatureUnsupported rc: 25 traceback: no\n        Unsupported repository feature(s) {}. A newer version of borg is required to access this repository.\n    NoManifestError rc: 26 traceback: no\n        Repository has no manifest.\n    UnsupportedManifestError rc: 27 traceback: no\n        Unsupported manifest envelope. A newer version is required to access this repository.\n\n    Archive.AlreadyExists rc: 30 traceback: no\n        Archive {} already exists\n    Archive.DoesNotExist rc: 31 traceback: no\n        Archive {} does not exist\n    Archive.IncompatibleFilesystemEncodingError rc: 32 traceback: no\n        Failed to encode filename \"{}\" into file system encoding \"{}\". Consider configuring the LANG environment variable.\n\n    KeyfileInvalidError rc: 40 traceback: no\n        Invalid key data for repository {} found in {}.\n    KeyfileMismatchError rc: 41 traceback: no\n        Mismatch between repository {} and key file {}.\n    KeyfileNotFoundError rc: 42 traceback: no\n        No key file for repository {} found in {}.\n    NotABorgKeyFile rc: 43 traceback: no\n        This file is not a borg key backup, aborting.\n    RepoKeyNotFoundError rc: 44 traceback: no\n        No key entry found in the config of repository {}.\n    RepoIdMismatch rc: 45 traceback: no\n        This key backup seems to be for a different backup repository, aborting.\n    UnencryptedRepo rc: 46 traceback: no\n        Key management not available for unencrypted repositories.\n    UnknownKeyType rc: 47 traceback: no\n        Key type {0} is unknown.\n    UnsupportedPayloadError rc: 48 traceback: no\n        Unsupported payload type {}. A newer version is required to access this repository.\n    UnsupportedKeyFormatError rc: 49 traceback:no\n        Your borg key is stored in an unsupported format. Try using a newer version of borg.\n\n\n    NoPassphraseFailure rc: 50 traceback: no\n        can not acquire a passphrase: {}\n    PasscommandFailure rc: 51 traceback: no\n        passcommand supplied in BORG_PASSCOMMAND failed: {}\n    PassphraseWrong rc: 52 traceback: no\n        passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect.\n    PasswordRetriesExceeded rc: 53 traceback: no\n        exceeded the maximum password retries\n\n    Cache.CacheInitAbortedError rc: 60 traceback: no\n        Cache initialization aborted\n    Cache.EncryptionMethodMismatch rc: 61 traceback: no\n        Repository encryption method changed since last access, refusing to continue\n    Cache.RepositoryAccessAborted rc: 62 traceback: no\n        Repository access aborted\n    Cache.RepositoryIDNotUnique rc: 63 traceback: no\n        Cache is newer than repository - do you have multiple, independently updated repos with same ID?\n    Cache.RepositoryReplay rc: 64 traceback: no\n        Cache, or information obtained from the security directory is newer than repository - this is either an attack or unsafe (multiple repos with same ID)\n\n    LockError rc: 70 traceback: no\n        Failed to acquire the lock {}.\n    LockErrorT rc: 71 traceback: yes\n        Failed to acquire the lock {}.\n    LockFailed rc: 72 traceback: yes\n        Failed to create/acquire the lock {} ({}).\n    LockTimeout rc: 73 traceback: no\n        Failed to create/acquire the lock {} (timeout).\n    NotLocked rc: 74 traceback: yes\n        Failed to release the lock {} (was not locked).\n    NotMyLock rc: 75 traceback: yes\n        Failed to release the lock {} (was/is locked, but not by me).\n\n    ConnectionClosed rc: 80 traceback: no\n        Connection closed by remote host\n    ConnectionClosedWithHint rc: 81 traceback: no\n        Connection closed by remote host. {}\n    InvalidRPCMethod rc: 82 traceback: no\n        RPC method {} is not valid\n    PathNotAllowed rc: 83 traceback: no\n        Repository path not allowed: {}\n    RemoteRepository.RPCServerOutdated rc: 84 traceback: no\n        Borg server is too old for {}. Required version {}\n    UnexpectedRPCDataFormatFromClient rc: 85 traceback: no\n        Borg {}: Got unexpected RPC data format from client.\n    UnexpectedRPCDataFormatFromServer rc: 86 traceback: no\n        Got unexpected RPC data format from server:\n        {}\n    ConnectionBrokenWithHint rc: 87 traceback: no\n        Connection to remote host is broken. {}\n\n    IntegrityError rc: 90 traceback: yes\n        Data integrity error: {}\n    FileIntegrityError rc: 91 traceback: yes\n        File failed integrity check: {}\n    DecompressionError rc: 92 traceback: yes\n        Decompression error: {}\n\n\nWarnings\n    BorgWarning rc: 1\n        Warning: {}\n    BackupWarning rc: 1\n        {}: {}\n\n    FileChangedWarning rc: 100\n        {}: file changed while we backed it up\n    IncludePatternNeverMatchedWarning rc: 101\n        Include pattern '{}' never matched.\n    BackupError rc: 102\n        {}: backup error\n    BackupRaceConditionError rc: 103\n        {}: file type or inode changed while we backed it up (race condition, skipped file)\n    BackupOSError rc: 104\n        {}: {}\n    BackupPermissionError rc: 105\n        {}: {}\n    BackupIOError rc: 106\n        {}: {}\n    BackupFileNotFoundError rc: 107\n        {}: {}\n\nOperations\n    - cache.begin_transaction\n    - cache.download_chunks, appears with ``borg create --no-cache-sync``\n    - cache.commit\n    - cache.sync\n\n      *info* is one string element, the name of the archive currently synced.\n    - repository.compact_segments\n    - repository.replay_segments\n    - repository.check\n    - check.verify_data\n    - check.rebuild_manifest\n    - check.rebuild_refcounts\n    - extract\n\n      *info* is one string element, the name of the path currently extracted.\n    - extract.permissions\n    - archive.delete\n    - archive.calc_stats\n    - prune\n    - upgrade.convert_segments\n\nPrompts\n    BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK\n        For \"Warning: Attempting to access a previously unknown unencrypted repository\"\n    BORG_RELOCATED_REPO_ACCESS_IS_OK\n        For \"Warning: The repository at location ... was previously located at ...\"\n    BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\n        For \"This is a potentially dangerous function...\" (check --repair)\n    BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\n        For \"You requested to DELETE the repository completely *including* all archives it contains:\"\n"
  },
  {
    "path": "docs/internals/security.rst",
    "content": ".. include:: ../global.rst.inc\n\n.. somewhat surprisingly the \"bash\" highlighter gives nice results with\n   the pseudo-code notation used in the \"Encryption\" section.\n\n.. highlight:: bash\n\n========\nSecurity\n========\n\n.. _borgcrypto:\n\nCryptography in Borg\n====================\n\n.. _attack_model:\n\nAttack model\n------------\n\nThe attack model of Borg is that the environment of the client process\n(e.g. ``borg create``) is trusted and the repository (server) is not. The\nattacker has any and all access to the repository, including interactive\nmanipulation (man-in-the-middle) for remote repositories.\n\nFurthermore, the client environment is assumed to be persistent across\nattacks (practically this means that the security database cannot be\ndeleted between attacks).\n\nUnder these circumstances Borg guarantees that the attacker cannot\n\n1. modify the data of any archive without the client detecting the change\n2. rename or add an archive without the client detecting the change\n3. recover plain-text data\n4. recover definite (heuristics based on access patterns are possible)\n   structural information such as the object graph (which archives\n   refer to what chunks)\n\nThe attacker can always impose a denial of service by definition (they could\nblock connections to the repository, or delete it partly or entirely).\n\n\n.. _security_structural_auth:\n\nStructural Authentication\n-------------------------\n\nBorg is fundamentally based on an object graph structure (see :ref:`internals`),\nwhere the root objects are the archives.\n\nBorg follows the `Horton principle`_, which states that\nnot only the message must be authenticated, but also its meaning (often\nexpressed through context), because every object used is referenced by a\nparent object through its object ID up to the archive list entry. The object ID in\nBorg is a MAC of the object's plaintext, therefore this ensures that\nan attacker cannot change the context of an object without forging the MAC.\n\nIn other words, the object ID itself only authenticates the plaintext of the\nobject and not its context or meaning. The latter is established by a different\nobject referring to an object ID, thereby assigning a particular meaning to\nan object. For example, an archive item contains a list of object IDs that\nrepresent packed file metadata. On their own, it's not clear that these objects\nwould represent what they do, but by the archive item referring to them\nin a particular part of its own data structure assigns this meaning.\n\nThis results in a directed acyclic graph of authentication from the archive\nlist entry to the data chunks of individual files.\n\nAbove used to be all for borg 1.x and was the reason why it needed the\ntertiary authentication mechanism (TAM) for manifest and archives.\n\nborg 2 now stores the ro_type (\"meaning\") of a repo object's data into that\nobject's metadata (like e.g.: manifest vs. archive vs. user file content data).\nWhen loading data from the repo, borg verifies that the type of object it got\nmatches the type it wanted. borg 2 does not use TAMs any more.\n\nAs both the object's metadata and data are AEAD encrypted and also bound to\nthe object ID (via giving the ID as AAD), there is no way an attacker (without\naccess to the borg key) could change the type of the object or move content\nto a different object ID.\n\nThis effectively 'anchors' each archive to the key, which is controlled by the\nclient, thereby anchoring the DAG starting from the archives list entry,\nmaking it impossible for an attacker to add or modify any part of the\nDAG without Borg being able to detect the tampering.\n\nPlease note that removing an archive by removing an entry from archives/*\nis possible and is done by ``borg delete`` and ``borg prune`` within their\nnormal operation. An attacker could also remove some entries there, but, due to\nencryption, would not know what exactly they are removing. An attacker with\nrepository access could also remove other parts of the repository or the whole\nrepository, so there is not much point in protecting against archive removal.\n\nThe borg 1.x way of having the archives list within the manifest chunk was\nproblematic as it required a read-modify-write operation on the manifest,\nrequiring a lock on the repository. We want to try less locking and more\nparallelism in future.\n\nPassphrase notes\n----------------\n\nNote that when using BORG_PASSPHRASE the attacker cannot swap the *entire*\nrepository against a new repository with e.g. repokey mode and no passphrase,\nbecause Borg will abort access when BORG_PASSPHRASE is incorrect.\n\nHowever, interactively a user might not notice this kind of attack\nimmediately, if she assumes that the reason for the absent passphrase\nprompt is a set BORG_PASSPHRASE. See issue :issue:`2169` for details.\n\n.. _security_encryption:\n\nEncryption\n----------\n\nAEAD modes\n~~~~~~~~~~\n\nModes: --encryption (repokey|keyfile)-[blake2-](aes-ocb|chacha20-poly1305)\n\nSupported: borg 2.0+\n\nEncryption with these modes is based on AEAD ciphers (authenticated encryption\nwith associated data) and session keys.\n\nDepending on the chosen mode (see :ref:`borg_repo-create`) different AEAD ciphers are used:\n\n- AES-256-OCB - super fast, single-pass algorithm IF you have hw accelerated AES.\n- chacha20-poly1305 - very fast, purely software based AEAD cipher.\n\nThe chunk ID is derived via a MAC over the plaintext (mac key taken from borg key):\n\n- HMAC-SHA256 - super fast IF you have hw accelerated SHA256 (see section \"Encryption\" below).\n- Blake2b - very fast, purely software based algorithm.\n\nFor each borg invocation, a new session id is generated by `os.urandom`_.\n\nFrom that session id, the initial key material (ikm, taken from the borg key)\nand an application and cipher specific salt, borg derives a session key using a\n\"one-step KDF\" based on just sha256.\n\nFor each session key, IVs (nonces) are generated by a counter which increments for\neach encrypted message.\n\nSession::\n\n    sessionid = os.urandom(24)\n    domain = \"borg-session-key-CIPHERNAME\"\n    sessionkey = sha256(crypt_key + sessionid + domain)\n    message_iv = 0\n\nEncryption::\n\n    id = MAC(id_key, data)\n    compressed = compress(data)\n\n    header = type-byte || 00h || message_iv || sessionid\n    aad = id || header\n    message_iv++\n    encrypted, auth_tag = AEAD_encrypt(session_key, message_iv, compressed, aad)\n    authenticated = header || auth_tag || encrypted\n\nDecryption::\n\n    # Given: input *authenticated* data and a *chunk-id* to assert\n    type-byte, past_message_iv, past_sessionid, auth_tag, encrypted = SPLIT(authenticated)\n\n    ASSERT(type-byte is correct)\n\n    domain = \"borg-session-key-CIPHERNAME\"\n    past_key = sha256(crypt_key + past_sessionid + domain)\n\n    decrypted = AEAD_decrypt(past_key, past_message_iv, authenticated)\n\n    decompressed = decompress(decrypted)\n\nNotable:\n\n- More modern and often faster AEAD ciphers instead of self-assembled stuff.\n- Due to the usage of session keys, IVs (nonces) do not need special care here as\n  they did for the legacy encryption modes.\n- The id is now also input into the authentication tag computation.\n  This strongly associates the id with the written data (== associates the key with\n  the value). When later reading the data for some id, authentication will only\n  succeed if what we get was really written by us for that id.\n\n\nLegacy modes\n~~~~~~~~~~~~\n\nModes: --encryption (repokey|keyfile)-[blake2]\n\nSupported: borg < 2.0\n\nThese were the AES-CTR based modes in previous borg versions.\n\nborg 2.0 does not support creating new repos using these modes,\nbut ``borg transfer`` can still read such existing repos.\n\n\n.. _key_encryption:\n\nOffline key security\n--------------------\n\nBorg cannot secure the key material while it is running, because the keys\nare needed in plain to decrypt/encrypt repository objects.\n\nFor offline storage of the encryption keys they are encrypted with a\nuser-chosen passphrase.\n\nA 256 bit key encryption key (KEK) is derived from the passphrase\nusing argon2_ with a random 256 bit salt. The KEK is then used\nto Encrypt-*then*-MAC a packed representation of the keys using the\nchacha20-poly1305 AEAD cipher and a constant IV == 0.\nThe ciphertext is then converted to base64.\n\nThis base64 blob (commonly referred to as *keyblob*) is then stored in\nthe key file or in the repository config (keyfile and repokey modes\nrespectively).\n\nThe use of a constant IV is secure because an identical passphrase will\nresult in a different derived KEK for every key encryption due to the salt.\n\n\n.. seealso::\n\n   Refer to the :ref:`key_files` section for details on the format.\n\n\nImplementations used\n--------------------\n\nWe do not implement cryptographic primitives ourselves, but rely\non widely used libraries providing them:\n\n- AES-OCB and CHACHA20-POLY1305 from OpenSSL 1.1 are used,\n  which is also linked into the static binaries we provide.\n  We think this is not an additional risk, since we don't ever\n  use OpenSSL's networking, TLS or X.509 code, but only their\n  primitives implemented in libcrypto.\n- SHA-256, SHA-512 and BLAKE2b from Python's hashlib_ standard library module are used.\n- HMAC and a constant-time comparison from Python's hmac_ standard library module are used.\n- argon2 is used via argon2-cffi.\n\n.. _Horton principle: https://en.wikipedia.org/wiki/Horton_Principle\n.. _length extension: https://en.wikipedia.org/wiki/Length_extension_attack\n.. _hashlib: https://docs.python.org/3/library/hashlib.html\n.. _hmac: https://docs.python.org/3/library/hmac.html\n.. _os.urandom: https://docs.python.org/3/library/os.html#os.urandom\n\nRemote RPC protocol security\n============================\n\n.. note:: This section could be further expanded / detailed.\n\nThe RPC protocol is fundamentally based on msgpack'd messages exchanged\nover an encrypted SSH channel (the system's SSH client is used for this\nby piping data from/to it).\n\nThis means that the authorization and transport security properties\nare inherited from SSH and the configuration of the SSH client and the\nSSH server -- Borg RPC does not contain *any* networking\ncode. Networking is done by the SSH client running in a separate\nprocess, Borg only communicates over the standard pipes (stdout,\nstderr and stdin) with this process. This also means that Borg doesn't\nhave to use a SSH client directly (or SSH at all). For example,\n``sudo`` or ``qrexec`` could be used as an intermediary.\n\nBy using the system's SSH client and not implementing a\n(cryptographic) network protocol Borg sidesteps many security issues\nthat would normally impact distributing statically linked / standalone\nbinaries.\n\nThe remainder of this section will focus on the security of the RPC\nprotocol within Borg.\n\nThe assumed worst-case a server can inflict to a client is a\ndenial of repository service.\n\nThe situation where a server can create a general DoS on the client\nshould be avoided, but might be possible by e.g. forcing the client to\nallocate large amounts of memory to decode large messages (or messages\nthat merely indicate a large amount of data follows). The RPC protocol\ncode uses a limited msgpack Unpacker to prohibit this.\n\nWe believe that other kinds of attacks, especially critical vulnerabilities\nlike remote code execution are inhibited by the design of the protocol:\n\n1. The server cannot send requests to the client on its own accord,\n   it only can send responses. This avoids \"unexpected inversion of control\"\n   issues.\n2. msgpack serialization does not allow embedding or referencing code that\n   is automatically executed. Incoming messages are unpacked by the msgpack\n   unpacker into native Python data structures (like tuples and dictionaries),\n   which are then passed to the rest of the program.\n\n   Additional verification of the correct form of the responses could be implemented.\n3. Remote errors are presented in two forms:\n\n   1. A simple plain-text *stderr* channel. A prefix string indicates the kind of message\n      (e.g. WARNING, INFO, ERROR), which is used to suppress it according to the\n      log level selected in the client.\n\n      A server can send arbitrary log messages, which may confuse a user. However,\n      log messages are only processed when server requests are in progress, therefore\n      the server cannot interfere / confuse with security critical dialogue like\n      the password prompt.\n   2. Server-side exceptions passed over the main data channel. These follow the\n      general pattern of server-sent responses and are sent instead of response data\n      for a request.\n\nThe msgpack implementation used (msgpack-python) has a good security track record,\na large test suite and no issues found by fuzzing. It is based on the msgpack-c implementation,\nsharing the unpacking engine and some support code. msgpack-c has a good track record as well.\nSome issues [#]_ in the past were located in code not included in msgpack-python.\nBorg does not use msgpack-c.\n\n.. [#] - `MessagePack fuzzing <https://blog.gypsyengineer.com/fun/msgpack-fuzzing.html>`_\n       - `Fixed integer overflow and EXT size problem <https://github.com/msgpack/msgpack-c/pull/547>`_\n       - `Fixed array and map size overflow <https://github.com/msgpack/msgpack-c/pull/550>`_\n\nUsing OpenSSL\n=============\n\nBorg uses the OpenSSL library for most cryptography (see `Implementations used`_ above).\nOpenSSL is bundled with static releases, thus the bundled copy is not updated with system\nupdates.\n\nOpenSSL is a large and complex piece of software and has had its share of vulnerabilities,\nhowever, it is important to note that Borg links against ``libcrypto`` **not** ``libssl``.\nlibcrypto is the low-level cryptography part of OpenSSL,\nwhile libssl implements TLS and related protocols.\n\nThe latter is not used by Borg (cf. `Remote RPC protocol security`_, Borg itself does not implement\nany network access) and historically contained most vulnerabilities, especially critical ones.\nThe static binaries released by the project contain neither libssl nor the Python ssl/_ssl modules.\n\nCompression and Encryption\n==========================\n\nCombining encryption with compression can be insecure in some contexts (e.g. online protocols).\n\nThere was some discussion about this in :issue:`1040` and for Borg some developers\nconcluded this is no problem at all, some concluded this is hard and extremely slow to exploit\nand thus no problem in practice.\n\nNo matter what, there is always the option not to use compression if you are worried about this.\n\n\nFingerprinting\n==============\n\nStored chunk sizes\n------------------\n\nA borg repository does not hide the size of the chunks it stores (size\ninformation is needed to operate the repository).\n\nThe chunks stored in the repo are the (compressed, encrypted and authenticated)\noutput of the chunker. The sizes of these stored chunks are influenced by the\ncompression, encryption and authentication.\n\nbuzhash and buzhash64 chunker\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe buzhash chunkers chunk according to the input data, the chunker's\nparameters and secret key material (which all influence the chunk boundary\npositions).\n\nSecret key material:\n\n- \"buzhash\": chunker seed (32bits), used for XORing the hardcoded buzhash table\n- \"buzhash64\": bh64_key (256bits) is derived from ID key, used to cryptographically\n  generate the table.\n\nSmall files below some specific threshold (default: 512 KiB) result in only one\nchunk (identical content / size as the original file), bigger files result in\nmultiple chunks.\n\nfixed chunker\n~~~~~~~~~~~~~\n\nThis chunker yields fixed sized chunks, with optional support of a differently\nsized header chunk. The last chunk is not required to have the full block size\nand is determined by the input file size.\n\nWithin our attack model, an attacker possessing a specific set of files which\nhe assumes that the victim also possesses (and backups into the repository)\ncould try a brute force fingerprinting attack based on the chunk sizes in the\nrepository to prove his assumption.\n\nTo make this more difficult, borg has an ``obfuscate`` pseudo compressor, that\nwill take the output of the normal compression step and tries to obfuscate\nthe size of that output. Of course, it can only **add** to the size, not reduce\nit. Thus, the optional usage of this mechanism comes at a cost: it will make\nyour repository larger (ranging from a few percent larger [cheap] to ridiculously\nlarger [expensive], depending on the algorithm/params you wisely choose).\n\nThe output of the compressed-size obfuscation step will then be encrypted and\nauthenticated, as usual. Of course, using that obfuscation would not make any\nsense without encryption. Thus, the additional data added by the obfuscator\nare just 0x00 bytes, which is good enough because after encryption it will\nlook like random anyway.\n\nTo summarize, this is making size-based fingerprinting difficult:\n\n- user-selectable chunker algorithm (and parametrization)\n- for the buzhash chunker: secret, random per-repo chunker seed\n- user-selectable compression algorithm (and level)\n- optional ``obfuscate`` pseudo compressor with different choices\n  of algorithm and parameters\n\nSecret key usage against fingerprinting\n---------------------------------------\n\nBorg uses the borg key also for chunking and chunk ID generation to protect against fingerprinting.\nAs usual for borg's attack model, the attacker is assumed to have access to a borg repository.\n\nThe borg key includes a secret random chunk_seed which (together with the chunking algorithm)\ndetermines the cutting places and thereby the length of the chunks cut. Because the attacker trying\na chunk length fingerprinting attack would use a different chunker secret than the borg setup being\nattacked, they would not be able to determine the set of chunk lengths for a known set of files.\n\nThe borg key also includes a secret random id_key. The chunk ID generation is not just using a simple\ncryptographic hash like sha256 (because that would be insecure as an attacker could see the hashes of\nsmall files that result only in 1 chunk in the repository). Instead, borg uses keyed hash (a MAC,\ne.g. HMAC-SHA256) to compute the chunk ID from the content and the secret id_key. Thus, an attacker\ncan't compute the same chunk IDs for a known set of small files to determine whether these are stored\nin the attacked repository.\n\nStored chunk proximity\n----------------------\n\nBorg does not try to obfuscate order / proximity of files it discovers by\nrecursing through the filesystem. For performance reasons, we sort directory\ncontents in file inode order (not in file name alphabetical order), so order\nfingerprinting is not useful for an attacker.\n\nBut, when new files are close to each other (when looking at recursion /\nscanning order), the resulting chunks will be also stored close to each other\nin the resulting repository segment file(s).\n\nThis might leak additional information for the chunk size fingerprinting\nattack (see above).\n"
  },
  {
    "path": "docs/internals.rst",
    "content": ".. include:: global.rst.inc\n.. _internals:\n\nInternals\n=========\n\nThe internals chapter describes and analyzes most of the inner workings\nof Borg.\n\nBorg uses a low-level, key-value store, the :ref:`repository`, and\nimplements a more complex data structure on top of it, which is made\nup of the :ref:`manifest <manifest>`, :ref:`archives <archive>`,\n:ref:`items <item>` and data :ref:`chunks`.\n\nEach repository can hold multiple :ref:`archives <archive>`, which\nrepresent individual backups that contain a full archive of the files\nspecified when the backup was performed.\n\nDeduplication is performed globally across all data in the repository\n(multiple backups and even multiple hosts), both on data and file\nmetadata, using :ref:`chunks` created by the chunker using the\nBuzhash_ algorithm (\"buzhash\" and \"buzhash64\" chunker) or a simpler\nfixed block size algorithm (\"fixed\" chunker).\n\nTo perform the repository-wide deduplication, a hash of each\nchunk is checked against the :ref:`chunks cache <cache>`, which is a\nhash table of all chunks that already exist.\n\n.. figure:: internals/structure.png\n    :figwidth: 100%\n    :width: 100%\n\n    Layers in Borg. At the very top, commands are implemented, using\n    a data access layer provided by the Archive and Item classes.\n    The \"key\" object provides both compression and authenticated\n    encryption used by the data access layer. The \"key\" object represents\n    the sole trust boundary in Borg.\n    The lowest layer is the repository, either accessed directly\n    (Repository) or remotely (RemoteRepository).\n\n.. toctree::\n    :caption: Internals contents\n\n    internals/security\n    internals/data-structures\n    internals/frontends\n"
  },
  {
    "path": "docs/introduction.rst",
    "content": "Introduction\n============\n\n.. This shim is here to fix the structure in the PDF\n   rendering. Without this stub, the elements in the toctree of\n   index.rst show up a level below the README file included.\n\n.. include:: ../README.rst\n"
  },
  {
    "path": "docs/man/borg-analyze.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-analyze\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-analyze \\- Analyzes archives.\n.SH SYNOPSIS\n.sp\nborg [common options] analyze [options]\n.SH DESCRIPTION\n.sp\nAnalyze archives to find \\(dqhot spots\\(dq.\n.sp\n\\fBborg analyze\\fP relies on the usual archive matching options to select the\narchives that should be considered for analysis (e.g. \\fB\\-a series_name\\fP).\nThen it iterates over all matching archives, over all contained files, and\ncollects information about chunks stored in all directories it encounters.\n.sp\nIt considers chunk IDs and their plaintext sizes (we do not have the compressed\nsize in the repository easily available) and adds up the sizes of added and removed\nchunks per direct parent directory, and outputs a list of \\(dqdirectory: size\\(dq.\n.sp\nYou can use that list to find directories with a lot of \\(dqactivity\\(dq — maybe\nsome of these are temporary or cache directories you forgot to exclude.\n.sp\nTo avoid including these unwanted directories in your backups, you can carefully\nexclude them in \\fBborg create\\fP (for future backups) or use \\fBborg recreate\\fP\nto recreate existing archives without them.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-benchmark-cpu.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-benchmark-cpu\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-benchmark-cpu \\- Benchmark CPU-bound operations.\n.SH SYNOPSIS\n.sp\nborg [common options] benchmark cpu [options]\n.SH DESCRIPTION\n.sp\nThis command benchmarks miscellaneous CPU\\-bound Borg operations.\n.sp\nIt creates input data in memory, runs the operation and then displays throughput.\nTo reduce outside influence on the timings, please make sure to run this with:\n.INDENT 0.0\n.IP \\(bu 2\nan otherwise as idle as possible machine\n.IP \\(bu 2\nenough free memory so there will be no slow down due to paging activity\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-json\nformat output as JSON\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-benchmark-crud.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-benchmark-crud\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-benchmark-crud \\- Benchmark Create, Read, Update, Delete for archives.\n.SH SYNOPSIS\n.sp\nborg [common options] benchmark crud [options] PATH\n.SH DESCRIPTION\n.sp\nThis command benchmarks borg CRUD (create, read, update, delete) operations.\n.sp\nIt creates input data below the given PATH and backs up this data into the given REPO.\nThe REPO must already exist (it could be a fresh empty repo or an existing repo, the\ncommand will create / read / update / delete some archives named borg\\-benchmark\\-crud* there.\n.sp\nMake sure you have free space there; you will need about 1 GB each (+ overhead).\n.sp\nIf your repository is encrypted and borg needs a passphrase to unlock the key, use:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nBORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nMeasurements are done with different input file sizes and counts.\nThe file contents are very artificial (either all zero or all random),\nthus the measurement results do not necessarily reflect performance with real data.\nAlso, due to the kind of content used, no compression is used in these benchmarks.\n.INDENT 0.0\n.TP\n.B C\\- == borg create (1st archive creation, no compression, do not use files cache)\nC\\-Z\\- == all\\-zero files. full dedup, this is primarily measuring reader/chunker/hasher.\nC\\-R\\- == random files. no dedup, measuring throughput through all processing stages.\n.TP\n.B R\\- == borg extract (extract archive, dry\\-run, do everything, but do not write files to disk)\nR\\-Z\\- == all zero files. Measuring heavily duplicated files.\nR\\-R\\- == random files. No duplication here, measuring throughput through all processing\nstages, except writing to disk.\n.TP\n.B U\\- == borg create (2nd archive creation of unchanged input files, measure files cache speed)\nThe throughput value is kind of virtual here, it does not actually read the file.\nU\\-Z\\- == needs to check the 2 all\\-zero chunks\\(aq existence in the repo.\nU\\-R\\- == needs to check existence of a lot of different chunks in the repo.\n.TP\n.B D\\- == borg delete archive (delete last remaining archive, measure deletion + compaction)\nD\\-Z\\- == few chunks to delete / few segments to compact/remove.\nD\\-R\\- == many chunks to delete / many segments to compact/remove.\n.UNINDENT\n.sp\nPlease note that there might be quite some variance in these measurements.\nTry multiple measurements and having a otherwise idle machine (and network, if you use it).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B PATH\npath where to create benchmark input data\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-json\\-lines\nFormat output as JSON Lines.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-benchmark.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-benchmark\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-benchmark \\- benchmark command\n.SH SYNOPSIS\n.nf\nborg [common options] benchmark crud ...\nborg [common options] benchmark cpu ...\n.fi\n.sp\n.SH DESCRIPTION\n.sp\nThese commands do various benchmarks.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-benchmark\\-crud(1)\\fP, \\fIborg\\-benchmark\\-cpu(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-break-lock.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-break-lock\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-break-lock \\- Breaks the repository lock (for example, if it was left by a dead Borg process).\n.SH SYNOPSIS\n.sp\nborg [common options] break\\-lock [options]\n.SH DESCRIPTION\n.sp\nThis command breaks the repository and cache locks.\nUse with care and only when no Borg process (on any machine) is\ntrying to access the cache or the repository.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-check.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-check\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-check \\- Checks repository consistency.\n.SH SYNOPSIS\n.sp\nborg [common options] check [options]\n.SH DESCRIPTION\n.sp\nThe check command verifies the consistency of a repository and its archives.\nIt consists of two major steps:\n.INDENT 0.0\n.IP 1. 3\nChecking the consistency of the repository itself. This includes checking\nthe file magic headers, and both the metadata and data of all objects in\nthe repository. The read data is checked by size and hash. Bit rot and other\ntypes of accidental damage can be detected this way. Running the repository\ncheck can be split into multiple partial checks using \\fB\\-\\-max\\-duration\\fP\\&.\nWhen checking an \\%<ssh://> remote repository, please note that the checks run on\nthe server and do not cause significant network traffic.\n.IP 2. 3\nChecking consistency and correctness of the archive metadata and optionally\narchive data (requires \\fB\\-\\-verify\\-data\\fP). This includes ensuring that the\nrepository manifest exists, the archive metadata chunk is present, and that\nall chunks referencing files (items) in the archive exist. This requires\nreading archive and file metadata, but not data. To scan for archives whose\nentries were lost from the archive directory, pass \\fB\\-\\-find\\-lost\\-archives\\fP\\&.\nIt requires reading all data and is hence very time\\-consuming.\nTo additionally cryptographically verify the file (content) data integrity,\npass \\fB\\-\\-verify\\-data\\fP, which is even more time\\-consuming.\n.sp\nWhen checking archives of a remote repository, archive checks run on the client\nmachine because they require decrypting data and therefore the encryption key.\n.UNINDENT\n.sp\nBoth steps can also be run independently. Pass \\fB\\-\\-repository\\-only\\fP to run the\nrepository checks only, or pass \\fB\\-\\-archives\\-only\\fP to run the archive checks\nonly.\n.sp\nThe \\fB\\-\\-max\\-duration\\fP option can be used to split a long\\-running repository\ncheck into multiple partial checks. After the given number of seconds, the check\nis interrupted. The next partial check will continue where the previous one\nstopped, until the full repository has been checked. Assuming a complete check\nwould take 7 hours, then running a daily check with \\fB\\-\\-max\\-duration=3600\\fP\n(1 hour) would result in one full repository check per week. Doing a full\nrepository check aborts any previous partial check; the next partial check will\nrestart from the beginning. With partial repository checks you can run neither\narchive checks, nor enable repair mode. Consequently, if you want to use\n\\fB\\-\\-max\\-duration\\fP you must also pass \\fB\\-\\-repository\\-only\\fP, and must not pass\n\\fB\\-\\-archives\\-only\\fP, nor \\fB\\-\\-repair\\fP\\&.\n.sp\n\\fBWarning:\\fP Please note that partial repository checks (i.e., running with\n\\fB\\-\\-max\\-duration\\fP) can only perform non\\-cryptographic checksum checks on the\nrepository files. Enabling partial repository checks excludes archive checks\nfor the same reason. Therefore, partial checks may be useful only with very large\nrepositories where a full check would take too long.\n.sp\nThe \\fB\\-\\-verify\\-data\\fP option will perform a full integrity verification (as\nopposed to checking just the xxh64) of data, which means reading the\ndata from the repository, decrypting and decompressing it. It is a complete\ncryptographic verification and hence very time\\-consuming, but will detect any\naccidental and malicious corruption. Tamper\\-resistance is only guaranteed for\nencrypted repositories against attackers without access to the keys. You cannot\nuse \\fB\\-\\-verify\\-data\\fP with \\fB\\-\\-repository\\-only\\fP\\&.\n.sp\nThe \\fB\\-\\-find\\-lost\\-archives\\fP option will also scan the whole repository, but\ntells Borg to search for lost archive metadata. If Borg encounters any archive\nmetadata that does not match an archive directory entry (including\nsoft\\-deleted archives), it means that an entry was lost.\nUnless \\fBborg compact\\fP is called, these archives can be fully restored with\n\\fB\\-\\-repair\\fP\\&. Please note that \\fB\\-\\-find\\-lost\\-archives\\fP must read a lot of\ndata from the repository and is thus very time\\-consuming. You cannot use\n\\fB\\-\\-find\\-lost\\-archives\\fP with \\fB\\-\\-repository\\-only\\fP\\&.\n.SS About repair mode\n.sp\nThe check command is a read\\-only task by default. If any corruption is found,\nBorg will report the issue and proceed with checking. To actually repair the\nissues found, pass \\fB\\-\\-repair\\fP\\&.\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\n\\fB\\-\\-repair\\fP is a \\fBPOTENTIALLY DANGEROUS FEATURE\\fP and might lead to data\nloss! This does not just include data that was previously lost anyway, but\nmight include more data for kinds of corruption it is not capable of\ndealing with. \\fBBE VERY CAREFUL!\\fP\n.UNINDENT\n.UNINDENT\n.sp\nPursuant to the previous warning it is also highly recommended to test the\nreliability of the hardware running Borg with stress testing software. This\nespecially includes storage and memory testers. Unreliable hardware might lead\nto additional data loss.\n.sp\nIt is highly recommended to create a backup of your repository before running\nin repair mode (i.e. running it with \\fB\\-\\-repair\\fP).\n.sp\nRepair mode will attempt to fix any corruptions found. Fixing corruptions does\nnot mean recovering lost data: Borg cannot magically restore data lost due to\ne.g. a hardware failure. Repairing a repository means sacrificing some data\nfor the sake of the repository as a whole and the remaining data. Hence it is,\nby definition, a potentially lossy task.\n.sp\nIn practice, repair mode hooks into both the repository and archive checks:\n.INDENT 0.0\n.IP 1. 3\nWhen checking the repository\\(aqs consistency, repair mode removes corrupted\nobjects from the repository after it did a 2nd try to read them correctly.\n.IP 2. 3\nWhen checking the consistency and correctness of archives, repair mode might\nremove whole archives from the manifest if their archive metadata chunk is\ncorrupt or lost. Borg will also report files that reference missing chunks.\n.UNINDENT\n.sp\nIf \\fB\\-\\-repair \\-\\-find\\-lost\\-archives\\fP is given, previously lost entries will\nbe recreated in the archive directory. This is only possible before\n\\fBborg compact\\fP would remove the archives\\(aq data completely.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-repository\\-only\nonly perform repository checks\n.TP\n.B  \\-\\-archives\\-only\nonly perform archive checks\n.TP\n.B  \\-\\-verify\\-data\nperform cryptographic archive data integrity verification (conflicts with \\fB\\-\\-repository\\-only\\fP)\n.TP\n.B  \\-\\-repair\nattempt to repair any inconsistencies found\n.TP\n.B  \\-\\-find\\-lost\\-archives\nattempt to find lost archives\n.TP\n.BI \\-\\-max\\-duration \\ SECONDS\nperform only a partial repository check for at most SECONDS seconds (default: unlimited)\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-common.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-common\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-common \\- Common options of Borg commands\n.SH SYNOPSIS\n.INDENT 0.0\n.TP\n.B  \\-h\\fP,\\fB  \\-\\-help\nshow this help message and exit\n.TP\n.B  \\-\\-critical\nwork on log level CRITICAL\n.TP\n.B  \\-\\-error\nwork on log level ERROR\n.TP\n.B  \\-\\-warning\nwork on log level WARNING (default)\n.TP\n.B  \\-\\-info\\fP,\\fB  \\-v\\fP,\\fB  \\-\\-verbose\nwork on log level INFO\n.TP\n.B  \\-\\-debug\nenable debug output, work on log level DEBUG\n.TP\n.BI \\-\\-debug\\-topic \\ TOPIC\nenable TOPIC debugging (can be specified multiple times). The logger path is borg.debug.<TOPIC> if TOPIC is not fully qualified.\n.TP\n.B  \\-p\\fP,\\fB  \\-\\-progress\nshow progress information\n.TP\n.B  \\-\\-iec\nformat using IEC units (1KiB = 1024B)\n.TP\n.B  \\-\\-log\\-json\nOutput one JSON object per log line instead of formatted text.\n.TP\n.BI \\-\\-lock\\-wait \\ SECONDS\nwait at most SECONDS for acquiring a repository/cache lock (default: 10).\n.TP\n.B  \\-\\-show\\-version\nshow/log the borg version\n.TP\n.B  \\-\\-show\\-rc\nshow/log the return code (rc)\n.TP\n.BI \\-\\-umask \\ M\nset umask to M (local only, default: 0077)\n.TP\n.BI \\-\\-remote\\-path \\ PATH\nuse PATH as borg executable on the remote (default: \\(dqborg\\(dq)\n.TP\n.BI \\-\\-upload\\-ratelimit \\ RATE\nset network upload rate limit in kiByte/s (default: 0=unlimited)\n.TP\n.BI \\-\\-upload\\-buffer \\ UPLOAD_BUFFER\nset network upload buffer size in MiB. (default: 0=no buffer)\n.TP\n.BI \\-\\-debug\\-profile \\ FILE\nWrite execution profile in Borg format into FILE. For local use a Python\\-compatible file can be generated by suffixing FILE with \\(dq.pyprof\\(dq.\n.TP\n.BI \\-\\-rsh \\ RSH\nUse this command to connect to the \\(aqborg serve\\(aq process (default: \\(aqssh\\(aq)\n.TP\n.BI \\-\\-socket \\ PATH\nUse UNIX DOMAIN (IPC) socket at PATH for client/server communication with socket: protocol.\n.TP\n.BI \\-r \\ REPO\\fR,\\fB \\ \\-\\-repo \\ REPO\nrepository to use\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-compact.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-compact\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-compact \\- Collects garbage in the repository.\n.SH SYNOPSIS\n.sp\nborg [common options] compact [options]\n.SH DESCRIPTION\n.sp\nFree repository space by deleting unused chunks.\n.sp\n\\fBborg compact\\fP analyzes all existing archives to determine which repository\nobjects are actually used (referenced). It then deletes all unused objects\nfrom the repository to free space.\n.sp\nUnused objects may result from:\n.INDENT 0.0\n.IP \\(bu 2\nuse of \\fBborg delete\\fP or \\fBborg prune\\fP\n.IP \\(bu 2\ninterrupted backups (consider retrying the backup before running compact)\n.IP \\(bu 2\nbackups of source files that encountered an I/O error mid\\-transfer and were skipped\n.IP \\(bu 2\ncorruption of the repository (e.g., the archives directory lost entries; see notes below)\n.UNINDENT\n.sp\nYou usually do not want to run \\fBborg compact\\fP after every write operation, but\neither regularly (e.g., once a month, possibly together with \\fBborg check\\fP) or\nwhen disk space needs to be freed.\n.sp\n\\fBImportant:\\fP\n.sp\nAfter compacting, it is no longer possible to use \\fBborg undelete\\fP to recover\npreviously soft\\-deleted archives.\n.sp\n\\fBborg compact\\fP might also delete data from archives that were \\(dqlost\\(dq due to\narchives directory corruption. Such archives could potentially be restored with\n\\fBborg check \\-\\-find\\-lost\\-archives [\\-\\-repair]\\fP, which is slow. You therefore\nmight not want to do that unless there are signs of lost archives (e.g., when\nseeing fatal errors when creating backups or when archives are missing in\n\\fBborg repo\\-list\\fP).\n.sp\nWhen using the \\fB\\-\\-stats\\fP option, borg will internally list all repository\nobjects to determine their existence and stored size. It will build a fresh\nchunks index from that information and cache it in the repository. For some\ntypes of repositories, this might be very slow. It will tell you the sum of\nstored object sizes, before and after compaction.\n.sp\nWithout \\fB\\-\\-stats\\fP, borg will rely on the cached chunks index to determine\nexisting object IDs (but there is no stored size information in the index,\nthus it cannot compute before/after compaction size statistics).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change the repository\n.TP\n.B  \\-s\\fP,\\fB  \\-\\-stats\nprint statistics (might be much slower)\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Compact segments and free repository disk space\n$ borg compact\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-completion.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-completion\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-completion \\- Output shell completion script for the given shell.\n.SH SYNOPSIS\n.sp\nborg [common options] completion [options] SHELL\n.SH DESCRIPTION\n.sp\nThis command prints a shell completion script for the given shell.\n.sp\nPlease note that for some dynamic completions (like archive IDs), the shell\ncompletion script will call borg to query the repository. This will work best\nif that call can be made without prompting for user input, so you may want to\nset BORG_REPO and BORG_PASSPHRASE environment variables.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B SHELL\nshell to generate completion for (one of: %(choices)s)\n.UNINDENT\n.SH EXAMPLES\n.sp\nTo activate completion in your current shell session, evaluate the output\nof this command. To enable it persistently, add the corresponding line to\nyour shell\\(aqs startup file.\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Bash (in ~/.bashrc)\neval \\(dq$(borg completion bash)\\(dq\n\n# Zsh (in ~/.zshrc)\neval \\(dq$(borg completion zsh)\\(dq\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-compression.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-compression\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-compression \\- Details regarding compression\n.SH DESCRIPTION\n.sp\nIt is no problem to mix different compression methods in one repository,\ndeduplication is done on the source data chunks (not on the compressed\nor encrypted data).\n.sp\nIf some specific chunk was once compressed and stored into the repository, creating\nanother backup that also uses this chunk will not change the stored chunk.\nSo if you use different compression specs for the backups, whichever stores a\nchunk first determines its compression. See also \\fBborg recreate\\fP\\&.\n.sp\nCompression is lz4 by default. If you want something else, you have to specify what you want.\n.sp\nValid compression specifiers are:\n.INDENT 0.0\n.TP\n.B none\nDo not compress.\n.TP\n.B lz4\nUse lz4 compression. Very high speed, very low compression. (default)\n.TP\n.B zstd[,L]\nUse zstd (\\(dqzstandard\\(dq) compression, a modern wide\\-range algorithm.\nIf you do not explicitly give the compression level L (ranging from 1\nto 22), it will use level 3.\n.TP\n.B zlib[,L]\nUse zlib (\\(dqgz\\(dq) compression. Medium speed, medium compression.\nIf you do not explicitly give the compression level L (ranging from 0\nto 9), it will use level 6.\nGiving level 0 (means \\(dqno compression\\(dq, but still has zlib protocol\noverhead) is usually pointless, you better use \\(dqnone\\(dq compression.\n.TP\n.B lzma[,L]\nUse lzma (\\(dqxz\\(dq) compression. Low speed, high compression.\nIf you do not explicitly give the compression level L (ranging from 0\nto 9), it will use level 6.\nGiving levels above 6 is pointless and counterproductive because it does\nnot compress better due to the buffer size used by borg \\- but it wastes\nlots of CPU cycles and RAM.\n.TP\n.B auto,C[,L]\nUse a built\\-in heuristic to decide per chunk whether to compress or not.\nThe heuristic tries with lz4 whether the data is compressible.\nFor incompressible data, it will not use compression (uses \\(dqnone\\(dq).\nFor compressible data, it uses the given C[,L] compression \\- with C[,L]\nbeing any valid compression specifier. This can be helpful for media files\nwhich often cannot be compressed much more.\n.TP\n.B obfuscate,SPEC,C[,L]\nUse compressed\\-size obfuscation to make fingerprinting attacks based on\nthe observable stored chunk size more difficult. Note:\n.INDENT 7.0\n.IP \\(bu 2\nYou must combine this with encryption, or it won\\(aqt make any sense.\n.IP \\(bu 2\nYour repo size will be bigger, of course.\n.IP \\(bu 2\nA chunk is limited by the constant \\fBMAX_DATA_SIZE\\fP (cur. ~20MiB).\n.UNINDENT\n.sp\nThe SPEC value determines how the size obfuscation works:\n.sp\n\\fIRelative random reciprocal size variation\\fP (multiplicative)\n.sp\nSize will increase by a factor, relative to the compressed data size.\nSmaller factors are used often, larger factors rarely.\n.sp\nAvailable factors:\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\n1:     0.01 ..        100\n2:     0.1  ..      1,000\n3:     1    ..     10,000\n4:    10    ..    100,000\n5:   100    ..  1,000,000\n6: 1,000    .. 10,000,000\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nExample probabilities for SPEC \\fB1\\fP:\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\n90   %  0.01 ..   0.1\n 9   %  0.1  ..   1\n 0.9 %  1    ..  10\n 0.09% 10    .. 100\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fIRandomly sized padding up to the given size\\fP (additive)\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\n110: 1kiB (2 ^ (SPEC \\- 100))\n\\&...\n120: 1MiB\n\\&...\n123: 8MiB (max.)\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fIPadmé padding\\fP (deterministic)\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\n250: pads to sums of powers of 2, max 12% overhead\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nUses the Padmé algorithm to deterministically pad the compressed size to a sum of\npowers of 2, limiting overhead to 12%. See \\%<https://\\:lbarman\\:.ch/\\:blog/\\:padme/> for details.\n.UNINDENT\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-\\-compression lz4 \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression zstd \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression zstd,10 \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression zlib \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression zlib,1 \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression auto,lzma,6 \\-\\-repo REPO ARCHIVE data\nborg create \\-\\-compression auto,lzma ...\nborg create \\-\\-compression obfuscate,110,none ...\nborg create \\-\\-compression obfuscate,3,auto,zstd,10 ...\nborg create \\-\\-compression obfuscate,2,zstd,6 ...\nborg create \\-\\-compression obfuscate,250,zstd,3 ...\n.EE\n.UNINDENT\n.UNINDENT\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-config.1",
    "content": ".\\\" Man page generated from reStructuredText.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"BORG-CONFIG\" 1 \"2024-07-19\" \"\" \"borg backup tool\"\n.SH NAME\nborg-config \\- get, set, and delete values in a repository or cache config file\n.SH SYNOPSIS\n.sp\nborg [common options] config [options] [NAME] [VALUE]\n.SH DESCRIPTION\n.sp\nThis command gets and sets options in a local repository or cache config file.\nFor security reasons, this command only works on local repositories.\n.sp\nTo delete a config value entirely, use \\fB\\-\\-delete\\fP\\&. To list the values\nof the configuration file or the default values, use \\fB\\-\\-list\\fP\\&.  To get an existing\nkey, pass only the key name. To set a key, pass both the key name and\nthe new value. Keys can be specified in the format \\(dqsection.name\\(dq or\nsimply \\(dqname\\(dq; the section will default to \\(dqrepository\\(dq and \\(dqcache\\(dq for\nthe repo and cache configs, respectively.\n.sp\nBy default, borg config manipulates the repository config file. Using \\fB\\-\\-cache\\fP\nedits the repository cache\\(aqs config file instead.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nname of config key\n.TP\n.B VALUE\nnew value for key\n.UNINDENT\n.SS optional arguments\n.INDENT 0.0\n.TP\n.B  \\-c\\fP,\\fB  \\-\\-cache\nget and set values from the repo cache\n.TP\n.B  \\-d\\fP,\\fB  \\-\\-delete\ndelete the key from the config file\n.TP\n.B  \\-l\\fP,\\fB  \\-\\-list\nlist the configuration of the repo\n.UNINDENT\n.SH EXAMPLES\n.sp\n\\fBNOTE:\\fP\n.INDENT 0.0\n.INDENT 3.5\nThe repository & cache config files are some of the only directly manipulable\nparts of a repository that aren\\(aqt versioned or backed up, so be careful when\nmaking changes!\n.UNINDENT\n.UNINDENT\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.nf\n.ft C\n# find cache directory\n$ cd ~/.cache/borg/$(borg config id)\n\n# reserve some space\n$ borg config additional_free_space 2G\n\n# make a repo append\\-only\n$ borg config append_only 1\n.ft P\n.fi\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH AUTHOR\nThe Borg Collective\n.\\\" Generated by docutils manpage writer.\n.\n"
  },
  {
    "path": "docs/man/borg-create.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-create\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-create \\- Creates a new archive.\n.SH SYNOPSIS\n.sp\nborg [common options] create [options] NAME [PATH...]\n.SH DESCRIPTION\n.sp\nThis command creates a backup archive containing all files found while recursively\ntraversing all specified paths. Paths are added to the archive as they are given,\nwhich means that if relative paths are desired, the command must be run from the correct\ndirectory.\n.sp\nThe slashdot hack in paths (recursion roots) is triggered by using \\fB/./\\fP:\n\\fB/this/gets/stripped/./this/gets/archived\\fP means to process that fs object, but\nstrip the prefix on the left side of \\fB\\&./\\fP from the archived items (in this case,\n\\fBthis/gets/archived\\fP will be the path in the archived item).\n.sp\nWhen specifying \\(aq\\-\\(aq as a path, borg will read data from standard input and create a\nfile named \\(aqstdin\\(aq in the created archive from that data. In some cases, it is more\nappropriate to use \\-\\-content\\-from\\-command. See the section \\fIReading from stdin\\fP\nbelow for details.\n.sp\nThe archive will consume almost no disk space for files or parts of files that\nhave already been stored in other archives.\n.sp\nThe \\fB\\-\\-tags\\fP option can be used to add a list of tags to the new archive.\n.sp\nThe archive name does not need to be unique; you can and should use the same\nname for a series of archives. The unique archive identifier is its ID (hash),\nand you can abbreviate the ID as long as it is unique.\n.sp\nIn the archive name, you may use the following placeholders:\n{now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.\n.sp\nBackup speed is increased by not reprocessing files that are already part of\nexisting archives and were not modified. The detection of unmodified files is\ndone by comparing multiple file metadata values with previous values kept in\nthe files cache.\n.sp\nThis comparison can operate in different modes as given by \\fB\\-\\-files\\-cache\\fP:\n.INDENT 0.0\n.IP \\(bu 2\nctime,size,inode (default)\n.IP \\(bu 2\nmtime,size,inode (default behaviour of borg versions older than 1.1.0rc4)\n.IP \\(bu 2\nctime,size (ignore the inode number)\n.IP \\(bu 2\nmtime,size (ignore the inode number)\n.IP \\(bu 2\nrechunk,ctime (all files are considered modified \\- rechunk, cache ctime)\n.IP \\(bu 2\nrechunk,mtime (all files are considered modified \\- rechunk, cache mtime)\n.IP \\(bu 2\ndisabled (disable the files cache, all files considered modified \\- rechunk)\n.UNINDENT\n.sp\ninode number: better safety, but often unstable on network filesystems\n.sp\nNormally, detecting file modifications will take inode information into\nconsideration to improve the reliability of file change detection.\nThis is problematic for files located on sshfs and similar network file\nsystems which do not provide stable inode numbers, such files will always\nbe considered modified. You can use modes without \\fIinode\\fP in this case to\nimprove performance, but reliability of change detection might be reduced.\n.sp\nctime vs. mtime: safety vs. speed\n.INDENT 0.0\n.IP \\(bu 2\nctime is a rather safe way to detect changes to a file (metadata and contents)\nas it cannot be set from userspace. But a metadata\\-only change will already\nupdate the ctime, so there might be some unnecessary chunking/hashing even\nwithout content changes. Some filesystems do not support ctime (change time).\nE.g. doing a chown or chmod to a file will change its ctime.\n.IP \\(bu 2\nmtime usually works and only updates if file contents were changed. But mtime\ncan be arbitrarily set from userspace, e.g., to set mtime back to the same value\nit had before a content change happened. This can be used maliciously as well as\nwell\\-meant, but in both cases mtime\\-based cache modes can be problematic.\n.UNINDENT\n.INDENT 0.0\n.TP\n.B The \\fB\\-\\-files\\-changed\\fP option controls how Borg detects if a file has changed during backup:\n.INDENT 7.0\n.IP \\(bu 2\nctime (default on POSIX): Use ctime to detect changes. This is the safest option.\nNot supported on Windows (ctime is file creation time there).\n.IP \\(bu 2\nmtime (default on Windows): Use mtime to detect changes.\n.IP \\(bu 2\ndisabled: Disable the \\(dqfile has changed while we backed it up\\(dq detection completely.\nThis is not recommended unless you know what you\\(aqre doing, as it could lead to\ninconsistent backups if files change during the backup process.\n.UNINDENT\n.UNINDENT\n.sp\nThe mount points of filesystems or filesystem snapshots should be the same for every\ncreation of a new archive to ensure fast operation. This is because the file cache that\nis used to determine changed files quickly uses absolute filenames.\nIf this is not possible, consider creating a bind mount to a stable location.\n.sp\nThe \\fB\\-\\-progress\\fP option shows (from left to right) Original and (uncompressed)\ndeduplicated size (O and U respectively), then the Number of files (N) processed so far,\nfollowed by the currently processed path.\n.sp\nWhen using \\fB\\-\\-stats\\fP, you will get some statistics about how much data was\nadded \\- the \\(dqThis Archive\\(dq deduplicated size there is most interesting as that is\nhow much your repository will grow. Please note that the \\(dqAll archives\\(dq stats refer to\nthe state after creation. Also, the \\fB\\-\\-stats\\fP and \\fB\\-\\-dry\\-run\\fP options are mutually\nexclusive because the data is not actually compressed and deduplicated during a dry run.\n.sp\nFor more help on include/exclude patterns, see the \\fIborg_patterns\\fP command output.\n.sp\nFor more help on placeholders, see the \\fIborg_placeholders\\fP command output.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.TP\n.B PATH\npaths to archive\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not create a backup archive\n.TP\n.B  \\-s\\fP,\\fB  \\-\\-stats\nprint statistics for the created archive\n.TP\n.B  \\-\\-list\noutput a verbose list of items (files, dirs, ...)\n.TP\n.BI \\-\\-filter \\ STATUSCHARS\nonly display items with the given status characters (see description)\n.TP\n.B  \\-\\-json\noutput stats as JSON. Implies \\fB\\-\\-stats\\fP\\&.\n.TP\n.BI \\-\\-stdin\\-name \\ NAME\nuse NAME in archive for stdin data (default: \\(aqstdin\\(aq)\n.TP\n.BI \\-\\-stdin\\-user \\ USER\nset user USER in archive for stdin data (default: do not store user/uid)\n.TP\n.BI \\-\\-stdin\\-group \\ GROUP\nset group GROUP in archive for stdin data (default: do not store group/gid)\n.TP\n.BI \\-\\-stdin\\-mode \\ M\nset mode to M in archive for stdin data (default: 0660)\n.TP\n.B  \\-\\-content\\-from\\-command\ninterpret PATH as a command and store its stdout. See also the section \\(aqReading from stdin\\(aq below.\n.TP\n.B  \\-\\-paths\\-from\\-stdin\nread DELIM\\-separated list of paths to back up from stdin. All control is external: it will back up all files given \\- no more, no less.\n.TP\n.B  \\-\\-paths\\-from\\-command\ninterpret PATH as command and treat its output as \\fB\\-\\-paths\\-from\\-stdin\\fP\n.TP\n.B  \\-\\-paths\\-from\\-shell\\-command\ninterpret PATH as shell command and treat its output as \\fB\\-\\-paths\\-from\\-stdin\\fP\n.TP\n.BI \\-\\-paths\\-delimiter \\ DELIM\nset path delimiter for \\fB\\-\\-paths\\-from\\-stdin\\fP and \\fB\\-\\-paths\\-from\\-command\\fP (default: \\fB\\en\\fP)\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.B  \\-\\-exclude\\-caches\nexclude directories that contain a CACHEDIR.TAG file (\\%<https://\\:www\\:.bford\\:.info/\\:cachedir/\\:spec\\:.html>)\n.TP\n.BI \\-\\-exclude\\-if\\-present \\ NAME\nexclude directories that are tagged by containing a filesystem object with the given NAME\n.TP\n.B  \\-\\-keep\\-exclude\\-tags\nif tag objects are specified with \\fB\\-\\-exclude\\-if\\-present\\fP, do not omit the tag objects themselves from the backup archive\n.UNINDENT\n.SS Filesystem options\n.INDENT 0.0\n.TP\n.B  \\-x\\fP,\\fB  \\-\\-one\\-file\\-system\nstay in the same file system and do not store mount points of other file systems \\- this might behave different from your expectations, see the description below.\n.TP\n.B  \\-\\-numeric\\-ids\nonly store numeric user and group identifiers\n.TP\n.B  \\-\\-atime\ndo store atime into archive\n.TP\n.B  \\-\\-noctime\ndo not store ctime into archive\n.TP\n.B  \\-\\-nobirthtime\ndo not store birthtime (creation date) into archive\n.TP\n.B  \\-\\-noflags\ndo not read and store flags (e.g. NODUMP, IMMUTABLE) into archive\n.TP\n.B  \\-\\-noacls\ndo not read and store ACLs into archive\n.TP\n.B  \\-\\-noxattrs\ndo not read and store xattrs into archive\n.TP\n.B  \\-\\-sparse\ndetect sparse holes in input (supported only by fixed chunker)\n.TP\n.BI \\-\\-files\\-cache \\ MODE\noperate files cache in MODE. default: ctime,size,inode\n.TP\n.BI \\-\\-files\\-changed \\ MODE\nspecify how to detect if a file has changed during backup (ctime, mtime, disabled). default: ctime (on Windows: mtime, because ctime is file creation time there).\n.TP\n.B  \\-\\-read\\-special\nopen and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files.\n.UNINDENT\n.SS Archive options\n.INDENT 0.0\n.TP\n.BI \\-\\-comment \\ COMMENT\nadd a comment text to the archive\n.TP\n.BI \\-\\-timestamp \\ TIMESTAMP\nmanually specify the archive creation date/time (yyyy\\-mm\\-ddThh:mm:ss[(+|\\-)HH:MM] format, (+|\\-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n.TP\n.BI \\-\\-chunker\\-params \\ PARAMS\nspecify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095\n.TP\n.BI \\-C \\ COMPRESSION\\fR,\\fB \\ \\-\\-compression \\ COMPRESSION\nselect compression algorithm, see the output of the \\(dqborg help compression\\(dq command for details.\n.TP\n.BI \\-\\-hostname \\ HOSTNAME\nexplicitly set hostname for the archive\n.TP\n.BI \\-\\-username \\ USERNAME\nexplicitly set username for the archive\n.TP\n.BI \\-\\-tags \\ TAG\nadd tags to archive (comma\\-separated or multiple arguments)\n.UNINDENT\n.SH EXAMPLES\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nArchive series and performance: In Borg 2, archives that share the same NAME form an \\(dqarchive series\\(dq.\nThe files cache is maintained per series. For best performance on repeated backups, reuse the same\nNAME every time you run \\fBborg create\\fP for the same dataset (e.g. always use \\fBmy\\-documents\\fP).\nFrequently changing the NAME (for example by embedding date/time like \\fBmy\\-documents\\-2025\\-11\\-10\\fP)\nprevents cache reuse and forces Borg to re\\-scan and re\\-chunk files, which can make incremental\nbackups vastly slower. Only vary the NAME if you intentionally want to start a new series.\n.sp\nIf you must vary the archive name but still want cache reuse across names, see the advanced\nknobs described in \\fIupgradenotes2\\fP (\\fBBORG_FILES_CACHE_SUFFIX\\fP and \\fBBORG_FILES_CACHE_TTL\\fP),\nbut the recommended approach is to keep a stable NAME per series.\n.UNINDENT\n.UNINDENT\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Backup ~/Documents into an archive named \\(dqmy\\-documents\\(dq\n$ borg create my\\-documents ~/Documents\n\n# same, but list all files as we process them\n$ borg create \\-\\-list my\\-documents ~/Documents\n\n# Backup /mnt/disk/docs, but strip path prefix using the slashdot hack\n$ borg create \\-\\-repo /path/to/repo docs /mnt/disk/./docs\n\n# Backup ~/Documents and ~/src but exclude pyc files\n$ borg create my\\-files                \\e\n    ~/Documents                       \\e\n    ~/src                             \\e\n    \\-\\-exclude \\(aq*.pyc\\(aq\n\n# Backup home directories excluding image thumbnails (i.e. only\n# /home/<one directory>/.thumbnails is excluded, not /home/*/*/.thumbnails etc.)\n$ borg create my\\-files /home \\-\\-exclude \\(aqsh:home/*/.thumbnails\\(aq\n\n# Back up the root filesystem into an archive named \\(dqroot\\-archive\\(dq\n# Use zlib compression (good, but slow) — default is LZ4 (fast, low compression ratio)\n$ borg create \\-C zlib,6 \\-\\-one\\-file\\-system root\\-archive /\n\n# Backup into an archive name like FQDN\\-root\n$ borg create \\(aq{fqdn}\\-root\\(aq /\n\n# Back up a remote host locally (\\(dqpull\\(dq style) using SSHFS\n$ mkdir sshfs\\-mount\n$ sshfs root@example.com:/ sshfs\\-mount\n$ cd sshfs\\-mount\n$ borg create example.com\\-root .\n$ cd ..\n$ fusermount \\-u sshfs\\-mount\n\n# Make a big effort in fine\\-grained deduplication (big chunk management\n# overhead, needs a lot of RAM and disk space; see the formula in the internals docs):\n$ borg create \\-\\-chunker\\-params buzhash,10,23,16,4095 small /smallstuff\n\n# Backup a raw device (must not be active/in use/mounted at that time)\n$ borg create \\-\\-read\\-special \\-\\-chunker\\-params fixed,4194304 my\\-sdx /dev/sdX\n\n# Backup a sparse disk image (must not be active/in use/mounted at that time)\n$ borg create \\-\\-sparse \\-\\-chunker\\-params fixed,4194304 my\\-disk my\\-disk.raw\n\n# No compression (none)\n$ borg create \\-\\-compression none arch ~\n\n# Super fast, low compression (lz4, default)\n$ borg create arch ~\n\n# Less fast, higher compression (zlib, N = 0..9)\n$ borg create \\-\\-compression zlib,N arch ~\n\n# Even slower, even higher compression (lzma, N = 0..9)\n$ borg create \\-\\-compression lzma,N arch ~\n\n# Only compress compressible data with lzma,N (N = 0..9)\n$ borg create \\-\\-compression auto,lzma,N arch ~\n\n# Use the short hostname and username as the archive name\n$ borg create \\(aq{hostname}\\-{user}\\(aq ~\n\n# Back up relative paths by moving into the correct directory first\n$ cd /home/user/Documents\n# The root directory of the archive will be \\(dqprojectA\\(dq\n$ borg create \\(aqdaily\\-projectA\\(aq projectA\n\n# Use external command to determine files to archive\n# Use \\-\\-paths\\-from\\-stdin with find to back up only files less than 1 MB in size\n$ find ~ \\-size \\-1000k | borg create \\-\\-paths\\-from\\-stdin small\\-files\\-only\n# Use \\-\\-paths\\-from\\-command with find to back up files from only a given user\n$ borg create \\-\\-paths\\-from\\-command joes\\-files \\-\\- find /srv/samba/shared \\-user joe\n# Use \\-\\-paths\\-from\\-shell\\-command with find to back up a few files from only a given user \\-\n# BE VERY CAREFUL AND ONLY USE TRUSTED INPUT FOR THE SHELL COMMAND!\n$ borg create \\-\\-paths\\-from\\-shell\\-command some\\-of\\-joes\\-files \\-\\- \\(dqfind /srv/samba/shared \\-user joe | head\\(dq\n# Use \\-\\-paths\\-from\\-stdin with \\-\\-paths\\-delimiter (for example, for filenames with newlines in them)\n$ find ~ \\-size \\-1000k \\-print0 | borg create \\e\n    \\-\\-paths\\-from\\-stdin \\e\n    \\-\\-paths\\-delimiter \\(dq\\e0\\(dq \\e\n    smallfiles\\-handle\\-newline\n.EE\n.UNINDENT\n.UNINDENT\n.SH NOTES\n.sp\nThe \\fB\\-\\-exclude\\fP patterns are not like tar. In tar \\fB\\-\\-exclude\\fP .bundler/gems will\nexclude foo/.bundler/gems. In borg it will not, you need to use \\fB\\-\\-exclude\\fP\n\\(aq*/.bundler/gems\\(aq to get the same effect.\n.sp\nIn addition to using \\fB\\-\\-exclude\\fP patterns, it is possible to use\n\\fB\\-\\-exclude\\-if\\-present\\fP to specify the name of a filesystem object (e.g. a file\nor folder name) which, when contained within another folder, will prevent the\ncontaining folder from being backed up.  By default, the containing folder and\nall of its contents will be omitted from the backup.  If, however, you wish to\nonly include the objects specified by \\fB\\-\\-exclude\\-if\\-present\\fP in your backup,\nand not include any other contents of the containing folder, this can be enabled\nthrough using the \\fB\\-\\-keep\\-exclude\\-tags\\fP option.\n.sp\nThe \\fB\\-x\\fP or \\fB\\-\\-one\\-file\\-system\\fP option excludes directories, that are mountpoints (and everything in them).\nIt detects mountpoints by comparing the device number from the output of \\fBstat()\\fP of the directory and its\nparent directory. Specifically, it excludes directories for which \\fBstat()\\fP reports a device number different\nfrom the device number of their parent.\nIn general: be aware that there are directories with device number different from their parent, which the kernel\ndoes not consider a mountpoint and also the other way around.\nLinux examples for this are bind mounts (possibly same device number, but always a mountpoint) and ALL\nsubvolumes of a btrfs (different device number from parent but not necessarily a mountpoint).\nmacOS examples are the apfs mounts of a typical macOS installation.\nTherefore, when using \\fB\\-\\-one\\-file\\-system\\fP, you should double\\-check that the backup works as intended.\n.SS Item flags\n.sp\n\\fB\\-\\-list\\fP outputs a list of all files, directories and other\nfile system items it considered (no matter whether they had content changes\nor not). For each item, it prefixes a single\\-letter flag that indicates type\nand/or status of the item.\n.sp\nIf you are interested only in a subset of that output, you can give e.g.\n\\fB\\-\\-filter=AME\\fP and it will only show regular files with A, M or E status (see\nbelow).\n.sp\nA uppercase character represents the status of a regular file relative to the\n\\(dqfiles\\(dq cache (not relative to the repo \\-\\- this is an issue if the files cache\nis not used). Metadata is stored in any case and for \\(aqA\\(aq and \\(aqM\\(aq also new data\nchunks are stored. For \\(aqU\\(aq all data chunks refer to already existing chunks.\n.INDENT 0.0\n.IP \\(bu 2\n\\(aqA\\(aq = regular file, added (see also \\fIa_status_oddity\\fP in the FAQ)\n.IP \\(bu 2\n\\(aqM\\(aq = regular file, modified\n.IP \\(bu 2\n\\(aqU\\(aq = regular file, unchanged\n.IP \\(bu 2\n\\(aqC\\(aq = regular file, it changed while we backed it up\n.IP \\(bu 2\n\\(aqE\\(aq = regular file, an error happened while accessing/reading \\fIthis\\fP file\n.UNINDENT\n.sp\nA lowercase character means a file type other than a regular file,\nborg usually just stores their metadata:\n.INDENT 0.0\n.IP \\(bu 2\n\\(aqd\\(aq = directory\n.IP \\(bu 2\n\\(aqb\\(aq = block device\n.IP \\(bu 2\n\\(aqc\\(aq = char device\n.IP \\(bu 2\n\\(aqh\\(aq = regular file, hard link (to already seen inodes)\n.IP \\(bu 2\n\\(aqs\\(aq = symlink\n.IP \\(bu 2\n\\(aqf\\(aq = fifo\n.UNINDENT\n.sp\nOther flags used include:\n.INDENT 0.0\n.IP \\(bu 2\n\\(aq+\\(aq = included, item would be backed up (if not in dry\\-run mode)\n.IP \\(bu 2\n\\(aq\\-\\(aq = excluded, item would not be / was not backed up\n.IP \\(bu 2\n\\(aqi\\(aq = backup data was read from standard input (stdin)\n.IP \\(bu 2\n\\(aq?\\(aq = missing status code (if you see this, please file a bug report!)\n.UNINDENT\n.SS Reading backup data from stdin\n.sp\nThere are two methods to read from stdin. Either specify \\fB\\-\\fP as path and\npipe directly to borg:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nbackup\\-vm \\-\\-id myvm \\-\\-stdout | borg create \\-\\-repo REPO ARCHIVE \\-\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nOr use \\fB\\-\\-content\\-from\\-command\\fP to have Borg manage the execution of the\ncommand and piping. If you do so, the first PATH argument is interpreted\nas command to execute and any further arguments are treated as arguments\nto the command:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-\\-content\\-from\\-command \\-\\-repo REPO ARCHIVE \\-\\- backup\\-vm \\-\\-id myvm \\-\\-stdout\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fB\\-\\-\\fP is used to ensure \\fB\\-\\-id\\fP and \\fB\\-\\-stdout\\fP are \\fBnot\\fP considered\narguments to \\fBborg\\fP but rather \\fBbackup\\-vm\\fP\\&.\n.sp\nThe difference between the two approaches is that piping to borg creates an\narchive even if the command piping to borg exits with a failure. In this case,\n\\fBone can end up with truncated output being backed up\\fP\\&. Using\n\\fB\\-\\-content\\-from\\-command\\fP, in contrast, borg is guaranteed to fail without\ncreating an archive should the command fail. The command is considered failed\nwhen it returned a non\\-zero exit code.\n.sp\nReading from stdin yields just a stream of data without file metadata\nassociated with it, and the files cache is not needed at all. So it is\nsafe to disable it via \\fB\\-\\-files\\-cache disabled\\fP and speed up backup\ncreation a bit.\n.sp\nBy default, the content read from stdin is stored in a file called \\(aqstdin\\(aq.\nUse \\fB\\-\\-stdin\\-name\\fP to change the name.\n.SS Feeding all file paths from externally\n.sp\nUsually, you give a starting path (recursion root) to borg and then borg\nautomatically recurses, finds and backs up all fs objects contained in\nthere (optionally considering include/exclude rules).\n.sp\nIf you need more control and you want to give every single fs object path\nto borg (maybe implementing your own recursion or your own rules), you can use\n\\fB\\-\\-paths\\-from\\-stdin\\fP, \\fB\\-\\-paths\\-from\\-command\\fP or \\fB\\-\\-paths\\-from\\-shell\\-command\\fP\n(with the latter two, borg will fail to create an archive should the command fail).\n.sp\nBorg supports paths with the slashdot hack to strip path prefixes here also.\nSo, be careful not to unintentionally trigger that.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-delete(1)\\fP, \\fIborg\\-prune(1)\\fP, \\fIborg\\-check(1)\\fP, \\fIborg\\-patterns(1)\\fP, \\fIborg\\-placeholders(1)\\fP, \\fIborg\\-compression(1)\\fP, \\fIborg\\-repo\\-create(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-delete.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-delete\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-delete \\- Deletes archives.\n.SH SYNOPSIS\n.sp\nborg [common options] delete [options] [NAME]\n.SH DESCRIPTION\n.sp\nThis command soft\\-deletes archives from the repository.\n.sp\nImportant:\n.INDENT 0.0\n.IP \\(bu 2\nThe delete command will only mark archives for deletion (\\(dqsoft\\-deletion\\(dq),\nrepository disk space is \\fBnot\\fP freed until you run \\fBborg compact\\fP\\&.\n.IP \\(bu 2\nYou can use \\fBborg undelete\\fP to undelete archives, but only until\nyou run \\fBborg compact\\fP\\&.\n.UNINDENT\n.sp\nWhen in doubt, use \\fB\\-\\-dry\\-run \\-\\-list\\fP to see what would be deleted.\n.sp\nYou can delete multiple archives by specifying a match pattern using\nthe \\fB\\-\\-match\\-archives PATTERN\\fP option (for more information on these\npatterns, see \\fIborg_patterns\\fP).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change the repository\n.TP\n.B  \\-\\-list\noutput a verbose list of archives\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Delete all backup archives named \\(dqkenny\\-files\\(dq:\n$ borg delete \\-a kenny\\-files\n# Actually free disk space:\n$ borg compact\n\n# Delete a specific backup archive using its unique archive ID prefix\n$ borg delete aid:d34db33f\n\n# Delete all archives whose names begin with the machine\\(aqs hostname followed by \\(dq\\-\\(dq\n$ borg delete \\-a \\(aqsh:{hostname}\\-*\\(aq\n\n# Delete all archives whose names contain \\(dq\\-2012\\-\\(dq\n$ borg delete \\-a \\(aqsh:*\\-2012\\-*\\(aq\n\n# See what would be deleted if delete was run without \\-\\-dry\\-run\n$ borg delete \\-\\-list \\-\\-dry\\-run \\-a \\(aqsh:*\\-May\\-*\\(aq\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-compact(1)\\fP, \\fIborg\\-repo\\-delete(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-diff.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-diff\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-diff \\- Finds differences between two archives.\n.SH SYNOPSIS\n.sp\nborg [common options] diff [options] ARCHIVE1 ARCHIVE2 [PATH...]\n.SH DESCRIPTION\n.sp\nThis command finds differences (file contents, metadata) between ARCHIVE1 and ARCHIVE2.\n.sp\nFor more help on include/exclude patterns, see the output of the \\fIborg_patterns\\fP command.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B ARCHIVE1\nARCHIVE1 name\n.TP\n.B ARCHIVE2\nARCHIVE2 name\n.TP\n.B PATH\npaths of items inside the archives to compare; patterns are supported.\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-numeric\\-ids\nonly consider numeric user and group identifiers\n.TP\n.B  \\-\\-same\\-chunker\\-params\noverride the check of chunker parameters\n.TP\n.BI \\-\\-format \\ FORMAT\nspecify format for differences between archives (default: \\(dq{change} {path}{NL}\\(dq)\n.TP\n.B  \\-\\-json\\-lines\nFormat output as JSON Lines.\n.TP\n.B  \\-\\-sort\\-by\nSort output by comma\\-separated fields (e.g., \\(aq>size_added,path\\(aq).\n.TP\n.B  \\-\\-content\\-only\nOnly compare differences in content (exclude metadata differences)\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg diff archive1 archive2\n    +17 B      \\-5 B [\\-rw\\-r\\-\\-r\\-\\- \\-> \\-rwxr\\-xr\\-x] file1\n   +135 B    \\-252 B file2\nadded           0 B file4\nremoved         0 B file3\n\n$ borg diff archive1 archive2\n{\\(dqpath\\(dq: \\(dqfile1\\(dq, \\(dqchanges\\(dq: [{\\(dqtype\\(dq: \\(dqmodified\\(dq, \\(dqadded\\(dq: 17, \\(dqremoved\\(dq: 5}, {\\(dqtype\\(dq: \\(dqmode\\(dq, \\(dqold_mode\\(dq: \\(dq\\-rw\\-r\\-\\-r\\-\\-\\(dq, \\(dqnew_mode\\(dq: \\(dq\\-rwxr\\-xr\\-x\\(dq}]}\n{\\(dqpath\\(dq: \\(dqfile2\\(dq, \\(dqchanges\\(dq: [{\\(dqtype\\(dq: \\(dqmodified\\(dq, \\(dqadded\\(dq: 135, \\(dqremoved\\(dq: 252}]}\n{\\(dqpath\\(dq: \\(dqfile4\\(dq, \\(dqchanges\\(dq: [{\\(dqtype\\(dq: \\(dqadded\\(dq, \\(dqsize\\(dq: 0}]}\n{\\(dqpath\\(dq: \\(dqfile3\\(dq, \\(dqchanges\\(dq: [{\\(dqtype\\(dq: \\(dqremoved\\(dq, \\(dqsize\\(dq: 0}]}\n\n\n# Use \\-\\-sort\\-by with a comma\\-separated list; sorts apply stably from last to first.\n# Here: primary by net size change descending, tie\\-breaker by path ascending\n$ borg diff \\-\\-sort\\-by=\\(dq>size_diff,path\\(dq archive1 archive2\n    +17 B      \\-5 B [\\-rw\\-r\\-\\-r\\-\\- \\-> \\-rwxr\\-xr\\-x] file1\nremoved         0 B file3\nadded           0 B file4\n   +135 B    \\-252 B file2\n.EE\n.UNINDENT\n.UNINDENT\n.SH NOTES\n.SS The FORMAT specifier syntax\n.sp\nThe \\fB\\-\\-format\\fP option uses Python\\(aqs format string syntax \\%<https://\\:docs\\:.python\\:.org/\\:3\\:.10/\\:library/\\:string\\:.html#\\:formatstrings>\\&.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg diff \\-\\-format \\(aq{content:30} {path}{NL}\\(aq ArchiveFoo ArchiveBar\nmodified:  +4.1 kB  \\-1.0 kB    file\\-diff\n\\&...\n\n# {VAR:<NUMBER} \\- pad to NUMBER columns left\\-aligned.\n# {VAR:>NUMBER} \\- pad to NUMBER columns right\\-aligned.\n$ borg diff \\-\\-format \\(aq{content:>30} {path}{NL}\\(aq ArchiveFoo ArchiveBar\n   modified:  +4.1 kB  \\-1.0 kB file\\-diff\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThe following keys are always available:\n\\- NEWLINE: OS dependent line separator\n\\- NL: alias of NEWLINE\n\\- NUL: NUL character for creating print0 / xargs \\-0 like output\n\\- SPACE: space character\n\\- TAB: tab character\n\\- CR: carriage return character\n\\- LF: line feed character\n.sp\nKeys available only when showing differences between archives:\n.INDENT 0.0\n.IP \\(bu 2\npath: archived file path\n.IP \\(bu 2\nchange: all available changes\n.IP \\(bu 2\ncontent: file content change\n.IP \\(bu 2\nmode: file mode change\n.IP \\(bu 2\ntype: file type change\n.IP \\(bu 2\nowner: file owner (user/group) change\n.IP \\(bu 2\ngroup: file group change\n.IP \\(bu 2\nuser: file user change\n.IP \\(bu 2\nlink: file link change\n.IP \\(bu 2\ndirectory: file directory change\n.IP \\(bu 2\nblkdev: file block device change\n.IP \\(bu 2\nchrdev: file character device change\n.IP \\(bu 2\nfifo: file fifo change\n.IP \\(bu 2\nmtime: file modification time change\n.IP \\(bu 2\nctime: file change time change\n.IP \\(bu 2\nisomtime: file modification time change (ISO 8601)\n.IP \\(bu 2\nisoctime: file creation time change (ISO 8601)\n.UNINDENT\n.SS What is compared\n.sp\nFor each matching item in both archives, Borg reports:\n.INDENT 0.0\n.IP \\(bu 2\nContent changes: total added/removed bytes within files. If chunker parameters are comparable,\nBorg compares chunk IDs quickly; otherwise, it compares the content.\n.IP \\(bu 2\nMetadata changes: user, group, mode, and other metadata shown inline, like\n\\(dq[old_mode \\-> new_mode]\\(dq for mode changes. Use \\fB\\-\\-content\\-only\\fP to suppress metadata changes.\n.IP \\(bu 2\nAdded/removed items: printed as \\(dqadded SIZE path\\(dq or \\(dqremoved SIZE path\\(dq.\n.UNINDENT\n.SS Output formats\n.sp\nThe default (text) output shows one line per changed path, e.g.:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n+135 B    \\-252 B [ \\-rw\\-r\\-\\-r\\-\\- \\-> \\-rwxr\\-xr\\-x ] path/to/file\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nJSON Lines output (\\fB\\-\\-json\\-lines\\fP) prints one JSON object per changed path, e.g.:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n{\\(dqpath\\(dq: \\(dqPATH\\(dq, \\(dqchanges\\(dq: [\n    {\\(dqtype\\(dq: \\(dqmodified\\(dq, \\(dqadded\\(dq: BYTES, \\(dqremoved\\(dq: BYTES},\n    {\\(dqtype\\(dq: \\(dqmode\\(dq, \\(dqold_mode\\(dq: \\(dq\\-rw\\-r\\-\\-r\\-\\-\\(dq, \\(dqnew_mode\\(dq: \\(dq\\-rwxr\\-xr\\-x\\(dq},\n    {\\(dqtype\\(dq: \\(dqadded\\(dq, \\(dqsize\\(dq: SIZE},\n    {\\(dqtype\\(dq: \\(dqremoved\\(dq, \\(dqsize\\(dq: SIZE}\n]}\n.EE\n.UNINDENT\n.UNINDENT\n.SS Sorting\n.sp\nUse \\fB\\-\\-sort\\-by FIELDS\\fP where FIELDS is a comma\\-separated list of fields.\nSorts are applied stably from last to first in the given list. Prepend \\(dq>\\(dq for\ndescending, \\(dq<\\(dq (or no prefix) for ascending, for example \\fB\\-\\-sort\\-by=\\(dq>size_added,path\\(dq\\fP\\&.\nSupported fields include:\n.INDENT 0.0\n.IP \\(bu 2\npath: the item path\n.IP \\(bu 2\nsize_added: total bytes added for the item content\n.IP \\(bu 2\nsize_removed: total bytes removed for the item content\n.IP \\(bu 2\nsize_diff: size_added \\- size_removed (net content change)\n.IP \\(bu 2\nsize: size of the item as stored in ARCHIVE2 (0 for removed items)\n.IP \\(bu 2\nuser, group, uid, gid, ctime, mtime: taken from the item state in ARCHIVE2 when present\n.IP \\(bu 2\nctime_diff, mtime_diff: timestamp difference (ARCHIVE2 \\- ARCHIVE1)\n.UNINDENT\n.SS Performance considerations\n.sp\ndiff automatically detects whether the archives were created with the same chunker\nparameters. If so, only chunk IDs are compared, which is very fast.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-export-tar.1",
    "content": "'\\\" t\n.\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-export-tar\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-export-tar \\- Export archive contents as a tarball\n.SH SYNOPSIS\n.sp\nborg [common options] export\\-tar [options] NAME FILE [PATH...]\n.SH DESCRIPTION\n.sp\nThis command creates a tarball from an archive.\n.sp\nWhen giving \\(aq\\-\\(aq as the output FILE, Borg will write a tar stream to standard output.\n.sp\nBy default (\\fB\\-\\-tar\\-filter=auto\\fP) Borg will detect whether the FILE should be compressed\nbased on its file extension and pipe the tarball through an appropriate filter\nbefore writing it to FILE:\n.INDENT 0.0\n.IP \\(bu 2\n\\&.tar.gz or .tgz: gzip\n.IP \\(bu 2\n\\&.tar.bz2 or .tbz: bzip2\n.IP \\(bu 2\n\\&.tar.xz or .txz: xz\n.IP \\(bu 2\n\\&.tar.zstd or .tar.zst: zstd\n.IP \\(bu 2\n\\&.tar.lz4: lz4\n.UNINDENT\n.sp\nAlternatively, a \\fB\\-\\-tar\\-filter\\fP program may be explicitly specified. It should\nread the uncompressed tar stream from stdin and write a compressed/filtered\ntar stream to stdout.\n.sp\nDepending on the \\fB\\-\\-tar\\-format\\fP option, these formats are created:\n.TS\nbox center;\nl|l|l.\nT{\n\\-\\-tar\\-format\nT}\tT{\nSpecification\nT}\tT{\nMetadata\nT}\n_\nT{\nBORG\nT}\tT{\nBORG specific, like PAX\nT}\tT{\nall as supported by borg\nT}\n_\nT{\nPAX\nT}\tT{\nPOSIX.1\\-2001 (pax) format\nT}\tT{\nGNU + atime/ctime/mtime ns\n+ xattrs\nT}\n_\nT{\nGNU\nT}\tT{\nGNU tar format\nT}\tT{\nmtime s, no atime/ctime,\nno ACLs/xattrs/bsdflags\nT}\n.TE\n.sp\nA \\fB\\-\\-sparse\\fP option (as found in borg extract) is not supported.\n.sp\nBy default the entire archive is extracted but a subset of files and directories\ncan be selected by passing a list of \\fBPATHs\\fP as arguments.\nThe file selection can further be restricted by using the \\fB\\-\\-exclude\\fP option.\n.sp\nFor more help on include/exclude patterns, see the \\fIborg_patterns\\fP command output.\n.sp\n\\fB\\-\\-progress\\fP can be slower than no progress display, since it makes one additional\npass over the archive metadata.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.TP\n.B FILE\noutput tar file. \\(dq\\-\\(dq to write to stdout instead.\n.TP\n.B PATH\npaths to extract; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-tar\\-filter\nfilter program to pipe data through\n.TP\n.B  \\-\\-list\noutput verbose list of items (files, dirs, ...)\n.TP\n.BI \\-\\-tar\\-format \\ FMT\nselect tar format: BORG, PAX or GNU\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.BI \\-\\-strip\\-components \\ NUMBER\nRemove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-extract.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-extract\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-extract \\- Extracts archive contents.\n.SH SYNOPSIS\n.sp\nborg [common options] extract [options] NAME [PATH...]\n.SH DESCRIPTION\n.sp\nThis command extracts the contents of an archive.\n.sp\nBy default, the entire archive is extracted, but a subset of files and directories\ncan be selected by passing a list of \\fBPATH\\fP arguments. The default interpretation\nfor the paths to extract is \\fIpp:\\fP which is a literal path\\-prefix match. If you want\nto use e.g. a wildcard, you must select a different pattern style such as \\fIsh:\\fP or\n\\fIfm:\\fP\\&. See \\fIborg_patterns\\fP for more information.\n.sp\nThe file selection can be further restricted by using the \\fB\\-\\-exclude\\fP option.\nFor more help on include/exclude patterns, see the \\fIborg_patterns\\fP command output.\n.sp\nBy using \\fB\\-\\-dry\\-run\\fP, you can do all extraction steps except actually writing the\noutput data: reading metadata and data chunks from the repository, checking the hash/HMAC,\ndecrypting, and decompressing.\n.sp\n\\fB\\-\\-progress\\fP can be slower than no progress display, since it makes one additional\npass over the archive metadata.\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nCurrently, extract always writes into the current working directory (\\(dq.\\(dq),\nso make sure you \\fBcd\\fP to the right place before calling \\fBborg extract\\fP\\&.\n.sp\nWhen parent directories are not extracted (because of using file/directory selection\nor any other reason), Borg cannot restore parent directories\\(aq metadata, e.g., owner,\ngroup, permissions, etc.\n.UNINDENT\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.TP\n.B PATH\npaths to extract; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-list\noutput a verbose list of items (files, dirs, ...)\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not actually change any files\n.TP\n.B  \\-\\-numeric\\-ids\nonly use numeric user and group identifiers\n.TP\n.B  \\-\\-noflags\ndo not extract/set flags (e.g. NODUMP, IMMUTABLE)\n.TP\n.B  \\-\\-noacls\ndo not extract/set ACLs\n.TP\n.B  \\-\\-noxattrs\ndo not extract/set xattrs\n.TP\n.B  \\-\\-stdout\nwrite all extracted data to stdout\n.TP\n.B  \\-\\-sparse\ncreate holes in the output sparse file from all\\-zero chunks\n.TP\n.B  \\-\\-continue\ncontinue a previously interrupted extraction of the same archive\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.BI \\-\\-strip\\-components \\ NUMBER\nRemove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Extract entire archive\n$ borg extract my\\-files\n\n# Extract entire archive and list files while processing\n$ borg extract \\-\\-list my\\-files\n\n# Verify whether an archive could be successfully extracted, but do not write files to disk\n$ borg extract \\-\\-dry\\-run my\\-files\n\n# Extract the \\(dqsrc\\(dq directory\n$ borg extract my\\-files home/USERNAME/src\n\n# Extract the \\(dqsrc\\(dq directory but exclude object files\n$ borg extract my\\-files home/USERNAME/src \\-\\-exclude \\(aq*.o\\(aq\n\n# Extract only the C files\n$ borg extract my\\-files \\(aqsh:home/USERNAME/src/*.c\\(aq\n\n# Restore a raw device (must not be active/in use/mounted at that time)\n$ borg extract \\-\\-stdout my\\-sdx | dd of=/dev/sdx bs=10M\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-mount(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-import-tar.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-import-tar\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-import-tar \\- Create a backup archive from a tarball\n.SH SYNOPSIS\n.sp\nborg [common options] import\\-tar [options] NAME TARFILE\n.SH DESCRIPTION\n.sp\nThis command creates a backup archive from a tarball.\n.sp\nWhen giving \\(aq\\-\\(aq as path, Borg will read a tar stream from standard input.\n.sp\nBy default (\\-\\-tar\\-filter=auto) Borg will detect whether the file is compressed\nbased on its file extension and pipe the file through an appropriate filter:\n.INDENT 0.0\n.IP \\(bu 2\n\\&.tar.gz or .tgz: gzip \\-d\n.IP \\(bu 2\n\\&.tar.bz2 or .tbz: bzip2 \\-d\n.IP \\(bu 2\n\\&.tar.xz or .txz: xz \\-d\n.IP \\(bu 2\n\\&.tar.zstd or .tar.zst: zstd \\-d\n.IP \\(bu 2\n\\&.tar.lz4: lz4 \\-d\n.UNINDENT\n.sp\nAlternatively, a \\-\\-tar\\-filter program may be explicitly specified. It should\nread compressed data from stdin and output an uncompressed tar stream on\nstdout.\n.sp\nMost documentation of borg create applies. Note that this command does not\nsupport excluding files.\n.sp\nA \\fB\\-\\-sparse\\fP option (as found in borg create) is not supported.\n.sp\nAbout tar formats and metadata conservation or loss, please see \\fBborg export\\-tar\\fP\\&.\n.sp\nimport\\-tar reads these tar formats:\n.INDENT 0.0\n.IP \\(bu 2\nBORG: borg specific (PAX\\-based)\n.IP \\(bu 2\nPAX: POSIX.1\\-2001\n.IP \\(bu 2\nGNU: GNU tar\n.IP \\(bu 2\nPOSIX.1\\-1988 (ustar)\n.IP \\(bu 2\nUNIX V7 tar\n.IP \\(bu 2\nSunOS tar with extended attributes\n.UNINDENT\n.sp\nTo import multiple tarballs into a single archive, they can be simply\nconcatenated (e.g. using \\(dqcat\\(dq) into a single file, and imported with an\n\\fB\\-\\-ignore\\-zeros\\fP option to skip through the stop markers between them.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.TP\n.B TARFILE\ninput tar file. \\(dq\\-\\(dq to read from stdin instead.\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-tar\\-filter\nfilter program to pipe data through\n.TP\n.B  \\-s\\fP,\\fB  \\-\\-stats\nprint statistics for the created archive\n.TP\n.B  \\-\\-list\noutput verbose list of items (files, dirs, ...)\n.TP\n.BI \\-\\-filter \\ STATUSCHARS\nonly display items with the given status characters\n.TP\n.B  \\-\\-json\noutput stats as JSON (implies \\-\\-stats)\n.TP\n.B  \\-\\-ignore\\-zeros\nignore zero\\-filled blocks in the input tarball\n.UNINDENT\n.SS Archive options\n.INDENT 0.0\n.TP\n.BI \\-\\-comment \\ COMMENT\nadd a comment text to the archive\n.TP\n.BI \\-\\-timestamp \\ TIMESTAMP\nmanually specify the archive creation date/time (yyyy\\-mm\\-ddThh:mm:ss[(+|\\-)HH:MM] format, (+|\\-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n.TP\n.BI \\-\\-chunker\\-params \\ PARAMS\nspecify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095\n.TP\n.BI \\-C \\ COMPRESSION\\fR,\\fB \\ \\-\\-compression \\ COMPRESSION\nselect compression algorithm, see the output of the \\(dqborg help compression\\(dq command for details.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Export as an uncompressed tar archive\n$ borg export\\-tar Monday Monday.tar\n\n# Import an uncompressed tar archive\n$ borg import\\-tar Monday Monday.tar\n\n# Exclude some file types and compress using gzip\n$ borg export\\-tar Monday Monday.tar.gz \\-\\-exclude \\(aq*.so\\(aq\n\n# Use a higher compression level with gzip\n$ borg export\\-tar \\-\\-tar\\-filter=\\(dqgzip \\-9\\(dq Monday Monday.tar.gz\n\n# Copy an archive from repoA to repoB\n$ borg \\-r repoA export\\-tar \\-\\-tar\\-format=BORG archive \\- | borg \\-r repoB import\\-tar archive \\-\n\n# Export a tar, but instead of storing it on disk, upload it to a remote site using curl\n$ borg export\\-tar Monday \\- | curl \\-\\-data\\-binary @\\- https://somewhere/to/POST\n\n# Remote extraction via \\(aqtarpipe\\(aq\n$ borg export\\-tar Monday \\- | ssh somewhere \\(dqcd extracted; tar x\\(dq\n.EE\n.UNINDENT\n.UNINDENT\n.SS Archives transfer script\n.sp\nOutputs a script that copies all archives from repo1 to repo2:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nfor N I T in \\(gaborg list \\-\\-format=\\(aq{archive} {id} {time:%Y\\-%m\\-%dT%H:%M:%S}{NL}\\(aq\\(ga\ndo\n  echo \\(dqborg \\-r repo1 export\\-tar \\-\\-tar\\-format=BORG aid:$I \\- | borg \\-r repo2 import\\-tar \\-\\-timestamp=$T $N \\-\\(dq\ndone\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nKept:\n.INDENT 0.0\n.IP \\(bu 2\narchive name, archive timestamp\n.IP \\(bu 2\narchive contents (all items with metadata and data)\n.UNINDENT\n.sp\nLost:\n.INDENT 0.0\n.IP \\(bu 2\nsome archive metadata (like the original command line, execution time, etc.)\n.UNINDENT\n.sp\nPlease note:\n.INDENT 0.0\n.IP \\(bu 2\nall data goes over that pipe, again and again for every archive\n.IP \\(bu 2\nthe pipe is dumb, there is no data or transfer time reduction there due to deduplication\n.IP \\(bu 2\nmaybe add compression\n.IP \\(bu 2\npipe over ssh for remote transfer\n.IP \\(bu 2\nno special sparse file support\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-info.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-info\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-info \\- Show archive details such as disk space used\n.SH SYNOPSIS\n.sp\nborg [common options] info [options] [NAME]\n.SH DESCRIPTION\n.sp\nThis command displays detailed information about the specified archive.\n.sp\nPlease note that the deduplicated sizes of the individual archives do not add\nup to the deduplicated size of the repository (\\(dqall archives\\(dq), because the two\nmean different things:\n.sp\nThis archive / deduplicated size = amount of data stored ONLY for this archive\n= unique chunks of this archive.\nAll archives / deduplicated size = amount of data stored in the repository\n= all chunks in the repository.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-json\nformat output as JSON\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg info aid:f7dea078\nArchive name: source\\-backup\nArchive fingerprint: f7dea0788dfc026cc2be1c0f5b94beb4e4084eb3402fc40c38d8719b1bf2d943\nComment:\nHostname: mba2020\nUsername: tw\nTime (start): Sat, 2022\\-06\\-25 20:51:40\nTime (end): Sat, 2022\\-06\\-25 20:51:40\nDuration: 0.03 seconds\nCommand line: /usr/bin/borg \\-r path/to/repo create source\\-backup src\nUtilization of maximum supported archive size: 0%\nNumber of files: 244\nOriginal size: 13.80 MB\nDeduplicated size: 531 B\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-list(1)\\fP, \\fIborg\\-diff(1)\\fP, \\fIborg\\-repo\\-info(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-key-change-algorithm.1",
    "content": ".\\\" Man page generated from reStructuredText.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"BORG-KEY-CHANGE-ALGORITHM\" 1 \"2022-06-26\" \"\" \"borg backup tool\"\n.SH NAME\nborg-key-change-algorithm \\- Change repository key algorithm\n.SH SYNOPSIS\n.sp\nborg [common options] key change\\-algorithm [options] ALGORITHM\n.SH DESCRIPTION\n.sp\nChange the algorithm we use to encrypt and authenticate the borg key.\n.sp\nImportant: In a \\fIrepokey\\fP mode (e.g. repokey\\-blake2) all users share the same key.\nIn this mode upgrading to \\fIargon2\\fP will make it impossible to access the repo for users who use an old version of borg.\nWe recommend upgrading to the latest stable version.\n.sp\nImportant: In a \\fIkeyfile\\fP mode (e.g. keyfile\\-blake2) each user has their own key (in \\fB~/.config/borg/keys\\fP).\nIn this mode this command will only change the key used by the current user.\nIf you want to upgrade to \\fIargon2\\fP to strengthen security, you will have to upgrade each user\\(aqs key individually.\n.sp\nYour repository is encrypted and authenticated with a key that is randomly generated by \\fBborg init\\fP\\&.\nThe key is encrypted and authenticated with your passphrase.\n.sp\nWe currently support two choices:\n.INDENT 0.0\n.IP 1. 3\nargon2 \\- recommended. This algorithm is used by default when initialising a new repository.\nThe key encryption key is derived from your passphrase via argon2\\-id.\nArgon2 is considered more modern and secure than pbkdf2.\n.IP 2. 3\npbkdf2 \\- the legacy algorithm. Use this if you want to access your repo via old versions of borg.\nThe key encryption key is derived from your passphrase via PBKDF2\\-HMAC\\-SHA256.\n.UNINDENT\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.nf\n.ft C\n# Upgrade an existing key to argon2\nborg key change\\-algorithm /path/to/repo argon2\n# Downgrade to pbkdf2 \\- use this if upgrading borg is not an option\nborg key change\\-algorithm /path/to/repo pbkdf2\n.ft P\n.fi\n.UNINDENT\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B ALGORITHM\nselect key algorithm\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH AUTHOR\nThe Borg Collective\n.\\\" Generated by docutils manpage writer.\n.\n"
  },
  {
    "path": "docs/man/borg-key-change-location.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-key-change-location\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-key-change-location \\- Changes the repository key location.\n.SH SYNOPSIS\n.sp\nborg [common options] key change\\-location [options] KEY_LOCATION\n.SH DESCRIPTION\n.sp\nChange the location of a Borg key. The key can be stored at different locations:\n.INDENT 0.0\n.IP \\(bu 2\nkeyfile: locally, usually in the home directory\n.IP \\(bu 2\nrepokey: inside the repository (in the repository config)\n.UNINDENT\n.sp\nPlease note:\n.sp\nThis command does NOT change the crypto algorithms, just the key location,\nthus you must ONLY give the key location (keyfile or repokey).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B KEY_LOCATION\nselect key location\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-keep\nkeep the key also at the current location (default: remove it)\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-key-change-passphrase.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-key-change-passphrase\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-key-change-passphrase \\- Changes the repository key file passphrase.\n.SH SYNOPSIS\n.sp\nborg [common options] key change\\-passphrase [options]\n.SH DESCRIPTION\n.sp\nThe key files used for repository encryption are optionally passphrase\nprotected. This command can be used to change this passphrase.\n.sp\nPlease note that this command only changes the passphrase, but not any\nsecret protected by it (like e.g. encryption/MAC keys or chunker seed).\nThus, changing the passphrase after passphrase and borg key got compromised\ndoes not protect future (nor past) backups to the same repository.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Create a key file protected repository\n$ borg repo\\-create \\-\\-encryption=keyfile\\-aes\\-ocb \\-v\nInitializing repository at \\(dq/path/to/repo\\(dq\nEnter new passphrase:\nEnter same passphrase again:\nRemember your passphrase. Your data will be inaccessible without it.\nKey in \\(dq/root/.config/borg/keys/mnt_backup\\(dq created.\nKeep this key safe. Your data will be inaccessible without it.\nSynchronizing chunks cache...\nArchives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0.\nDone.\n\n# Change key file passphrase\n$ borg key change\\-passphrase \\-v\nEnter passphrase for key /root/.config/borg/keys/mnt_backup:\nEnter new passphrase:\nEnter same passphrase again:\nRemember your passphrase. Your data will be inaccessible without it.\nKey updated\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nThe key file paths shown above are the defaults for Linux (\\fB~/.config/borg/keys/\\fP).\nOn macOS, key files are stored in \\fB~/Library/Application Support/borg/keys/\\fP\\&.\nOn Windows, they are stored in \\fBC:\\eUsers\\e<user>\\eAppData\\eRoaming\\eborg\\ekeys\\e\\fP\\&.\nSee \\fIenv_vars\\fP for details.\n.UNINDENT\n.UNINDENT\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Import a previously\\-exported key into the specified\n# key file (creating or overwriting the output key)\n# (keyfile repositories only)\n$ BORG_KEY_FILE=/path/to/output\\-key borg key import /path/to/exported\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nFully automated using environment variables:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ BORG_NEW_PASSPHRASE=old borg repo\\-create \\-\\-encryption=repokey\\-aes\\-ocb\n# now \\(dqold\\(dq is the current passphrase.\n$ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change\\-passphrase\n# now \\(dqnew\\(dq is the current passphrase.\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-key-export.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-key-export\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-key-export \\- Exports the repository key for backup.\n.SH SYNOPSIS\n.sp\nborg [common options] key export [options] [PATH]\n.SH DESCRIPTION\n.sp\nThis command backs up the borg key.\n.sp\nIf repository encryption is used, the repository is inaccessible\nwithout the borg key (and the passphrase that protects the borg key).\nIf a repository is not encrypted, but authenticated, the borg key is\nstill needed to access the repository normally.\n.sp\nFor repositories using \\fBkeyfile\\fP encryption the key is kept locally\non the system that is capable of doing backups. To guard against loss\nor corruption of this key, the key needs to be backed up independently\nof the main data backup.\n.sp\nFor repositories using \\fBrepokey\\fP encryption or \\fBauthenticated\\fP mode\nthe key is kept in the repository. A backup is thus not strictly needed,\nbut guards against the repository becoming inaccessible if the key is\ncorrupted or lost.\n.sp\nNote that the backup produced does not include the passphrase itself\n(i.e. the exported key stays encrypted). In order to regain access to a\nrepository, one needs both the exported key and the original passphrase.\nKeep the exported key and the passphrase at safe places.\n.sp\nThere are three backup formats. The normal backup format is suitable for\ndigital storage as a file. The \\fB\\-\\-paper\\fP backup format is optimized\nfor printing and typing in while importing, with per line checks to\nreduce problems with manual input. The \\fB\\-\\-qr\\-html\\fP creates a printable\nHTML template with a QR code and a copy of the \\fB\\-\\-paper\\fP\\-formatted key.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B PATH\nwhere to store the backup\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-paper\nCreate an export suitable for printing and later type\\-in\n.TP\n.B  \\-\\-qr\\-html\nCreate an HTML file suitable for printing and later type\\-in or QR scan\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg key export > encrypted\\-key\\-backup\nborg key export \\-\\-paper > encrypted\\-key\\-backup.txt\nborg key export \\-\\-qr\\-html > encrypted\\-key\\-backup.html\n# Or pass the output file as an argument instead of redirecting stdout:\nborg key export encrypted\\-key\\-backup\nborg key export \\-\\-paper encrypted\\-key\\-backup.txt\nborg key export \\-\\-qr\\-html encrypted\\-key\\-backup.html\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-key\\-import(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-key-import.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-key-import\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-key-import \\- Imports the repository key from backup.\n.SH SYNOPSIS\n.sp\nborg [common options] key import [options] [PATH]\n.SH DESCRIPTION\n.sp\nThis command restores a key previously backed up with the export command.\n.sp\nIf the \\fB\\-\\-paper\\fP option is given, the import will be an interactive\nprocess in which each line is checked for plausibility before\nproceeding to the next line. For this format PATH must not be given.\n.sp\nFor repositories using keyfile encryption, the key file which \\fBborg key\nimport\\fP writes to depends on several factors. If the \\fBBORG_KEY_FILE\\fP\nenvironment variable is set and non\\-empty, \\fBborg key import\\fP creates\nor overwrites that file named by \\fB$BORG_KEY_FILE\\fP\\&. Otherwise, \\fBborg\nkey import\\fP searches in the \\fB$BORG_KEYS_DIR\\fP directory for a key file\nassociated with the repository. If a key file is found in\n\\fB$BORG_KEYS_DIR\\fP, \\fBborg key import\\fP overwrites it; otherwise, \\fBborg\nkey import\\fP creates a new key file in \\fB$BORG_KEYS_DIR\\fP\\&.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B PATH\npath to the backup (\\(aq\\-\\(aq to read from stdin)\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-paper\ninteractively import from a backup done with \\fB\\-\\-paper\\fP\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-key\\-export(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-key-migrate-to-repokey.1",
    "content": ".\\\" Man page generated from reStructuredText.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"BORG-KEY-MIGRATE-TO-REPOKEY\" 1 \"2022-02-19\" \"\" \"borg backup tool\"\n.SH NAME\nborg-key-migrate-to-repokey \\- Migrate passphrase -> repokey\n.SH SYNOPSIS\n.sp\nborg [common options] key migrate\\-to\\-repokey [options] [REPOSITORY]\n.SH DESCRIPTION\n.sp\nThis command migrates a repository from passphrase mode (removed in Borg 1.0)\nto repokey mode.\n.sp\nYou will be first asked for the repository passphrase (to open it in passphrase\nmode). This is the same passphrase as you used to use for this repo before 1.0.\n.sp\nIt will then derive the different secrets from this passphrase.\n.sp\nThen you will be asked for a new passphrase (twice, for safety). This\npassphrase will be used to protect the repokey (which contains these same\nsecrets in encrypted form). You may use the same passphrase as you used to\nuse, but you may also use a different one.\n.sp\nAfter migrating to repokey mode, you can change the passphrase at any time.\nBut please note: the secrets will always stay the same and they could always\nbe derived from your (old) passphrase\\-mode passphrase.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.sp\nREPOSITORY\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH AUTHOR\nThe Borg Collective\n.\\\" Generated by docutils manpage writer.\n.\n"
  },
  {
    "path": "docs/man/borg-key.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-key\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-key \\- Manage the keyfile or repokey of a repository\n.SH SYNOPSIS\n.nf\nborg [common options] key export ...\nborg [common options] key import ...\nborg [common options] key change\\-passphrase ...\nborg [common options] key change\\-location ...\n.fi\n.sp\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-key\\-export(1)\\fP, \\fIborg\\-key\\-import(1)\\fP, \\fIborg\\-key\\-change\\-passphrase(1)\\fP, \\fIborg\\-key\\-change\\-location(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-list.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-list\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-list \\- List archive contents.\n.SH SYNOPSIS\n.sp\nborg [common options] list [options] NAME [PATH...]\n.SH DESCRIPTION\n.sp\nThis command lists the contents of an archive.\n.sp\nFor more help on include/exclude patterns, see the output of \\fIborg_patterns\\fP\\&.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.TP\n.B PATH\npaths to list; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-short\nonly print file/directory names, nothing else\n.TP\n.BI \\-\\-format \\ FORMAT\nspecify format for file listing (default: \\(dq{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\\(dq)\n.TP\n.B  \\-\\-json\\-lines\nFormat output as JSON Lines. The form of \\fB\\-\\-format\\fP is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text.\n.TP\n.BI \\-\\-depth \\ N\nonly list files up to the specified directory depth\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg list root\\-2016\\-02\\-15\ndrwxr\\-xr\\-x root   root          0 Mon, 2016\\-02\\-15 17:44:27 .\ndrwxrwxr\\-x root   root          0 Mon, 2016\\-02\\-15 19:04:49 bin\n\\-rwxr\\-xr\\-x root   root    1029624 Thu, 2014\\-11\\-13 00:08:51 bin/bash\nlrwxrwxrwx root   root          0 Fri, 2015\\-03\\-27 20:24:26 bin/bzcmp \\-> bzdiff\n\\-rwxr\\-xr\\-x root   root       2140 Fri, 2015\\-03\\-27 20:24:22 bin/bzdiff\n\\&...\n\n$ borg list root\\-2016\\-02\\-15 \\-\\-pattern \\(dq\\- bin/ba*\\(dq\ndrwxr\\-xr\\-x root   root          0 Mon, 2016\\-02\\-15 17:44:27 .\ndrwxrwxr\\-x root   root          0 Mon, 2016\\-02\\-15 19:04:49 bin\nlrwxrwxrwx root   root          0 Fri, 2015\\-03\\-27 20:24:26 bin/bzcmp \\-> bzdiff\n\\-rwxr\\-xr\\-x root   root       2140 Fri, 2015\\-03\\-27 20:24:22 bin/bzdiff\n\\&...\n\n$ borg list archiveA \\-\\-format=\\(dq{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}\\(dq\ndrwxrwxr\\-x user   user          0 Sun, 2015\\-02\\-01 11:00:00 .\ndrwxrwxr\\-x user   user          0 Sun, 2015\\-02\\-01 11:00:00 code\ndrwxrwxr\\-x user   user          0 Sun, 2015\\-02\\-01 11:00:00 code/myproject\n\\-rw\\-rw\\-r\\-\\- user   user    1416192 Sun, 2015\\-02\\-01 11:00:00 code/myproject/file.ext\n\\-rw\\-rw\\-r\\-\\- user   user    1416192 Sun, 2015\\-02\\-01 11:00:00 code/myproject/file.text\n\\&...\n\n$ borg list archiveA \\-\\-pattern \\(aq+ re:\\e.ext$\\(aq \\-\\-pattern \\(aq\\- re:^.*$\\(aq\n\\-rw\\-rw\\-r\\-\\- user   user    1416192 Sun, 2015\\-02\\-01 11:00:00 code/myproject/file.ext\n\\&...\n\n$ borg list archiveA \\-\\-pattern \\(aq+ re:.ext$\\(aq \\-\\-pattern \\(aq\\- re:^.*$\\(aq\n\\-rw\\-rw\\-r\\-\\- user   user    1416192 Sun, 2015\\-02\\-01 11:00:00 code/myproject/file.ext\n\\-rw\\-rw\\-r\\-\\- user   user    1416192 Sun, 2015\\-02\\-01 11:00:00 code/myproject/file.text\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.SH NOTES\n.SS The FORMAT specifier syntax\n.sp\nThe \\fB\\-\\-format\\fP option uses Python\\(aqs format string syntax \\%<https://\\:docs\\:.python\\:.org/\\:3\\:.10/\\:library/\\:string\\:.html#\\:formatstrings>\\&.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg list \\-\\-format \\(aq{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\\(aq ArchiveFoo\n\\-rw\\-rw\\-r\\-\\- user   user       1024 Thu, 2021\\-12\\-09 10:22:17 file\\-foo\n\\&...\n\n# {VAR:<NUMBER} \\- pad to NUMBER columns left\\-aligned.\n# {VAR:>NUMBER} \\- pad to NUMBER columns right\\-aligned.\n$ borg list \\-\\-format \\(aq{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}\\(aq ArchiveFoo\n\\-rw\\-rw\\-r\\-\\-   user   user 1024     Thu, 2021\\-12\\-09 10:22:17 file\\-foo\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThe following keys are always available:\n\\- NEWLINE: OS dependent line separator\n\\- NL: alias of NEWLINE\n\\- NUL: NUL character for creating print0 / xargs \\-0 like output\n\\- SPACE: space character\n\\- TAB: tab character\n\\- CR: carriage return character\n\\- LF: line feed character\n.sp\nKeys available only when listing files in an archive:\n.INDENT 0.0\n.IP \\(bu 2\ntype: file type (file, dir, symlink, ...)\n.IP \\(bu 2\nmode: file mode (as in stat)\n.IP \\(bu 2\nuid: user id of file owner\n.IP \\(bu 2\ngid: group id of file owner\n.IP \\(bu 2\nuser: user name of file owner\n.IP \\(bu 2\ngroup: group name of file owner\n.IP \\(bu 2\npath: file path\n.IP \\(bu 2\ntarget: link target for symlinks\n.IP \\(bu 2\nhlid: hard link identity (same if hardlinking same fs object)\n.IP \\(bu 2\ninode: inode number\n.IP \\(bu 2\nflags: file flags\n.IP \\(bu 2\nsize: file size\n.IP \\(bu 2\nnum_chunks: number of chunks in this file\n.IP \\(bu 2\nmtime: file modification time\n.IP \\(bu 2\nctime: file change time\n.IP \\(bu 2\natime: file access time\n.IP \\(bu 2\nisomtime: file modification time (ISO 8601 format)\n.IP \\(bu 2\nisoctime: file change time (ISO 8601 format)\n.IP \\(bu 2\nisoatime: file access time (ISO 8601 format)\n.IP \\(bu 2\nfingerprint: Fingerprint of the file content (may have false negatives), format: H(conditions)\\-H(chunk_ids)\n.IP \\(bu 2\nblake2b\n.IP \\(bu 2\nblake2s\n.IP \\(bu 2\nmd5\n.IP \\(bu 2\nsha1\n.IP \\(bu 2\nsha224\n.IP \\(bu 2\nsha256\n.IP \\(bu 2\nsha384\n.IP \\(bu 2\nsha3_224\n.IP \\(bu 2\nsha3_256\n.IP \\(bu 2\nsha3_384\n.IP \\(bu 2\nsha3_512\n.IP \\(bu 2\nsha512\n.IP \\(bu 2\nxxh64: XXH64 checksum of this file (note: this is NOT a cryptographic hash!)\n.IP \\(bu 2\narchiveid: internal ID of the archive\n.IP \\(bu 2\narchivename: name of the archive\n.IP \\(bu 2\nextra: prepends {target} with \\(dq \\-> \\(dq for soft links and \\(dq link to \\(dq for hard links\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-info(1)\\fP, \\fIborg\\-diff(1)\\fP, \\fIborg\\-prune(1)\\fP, \\fIborg\\-patterns(1)\\fP, \\fIborg\\-repo\\-list(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-match-archives.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-match-archives\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-match-archives \\- Details regarding match-archives\n.SH DESCRIPTION\n.sp\nThe \\fB\\-\\-match\\-archives\\fP option matches a given pattern against the list of all archives\nin the repository. It can be given multiple times.\n.sp\nThe patterns can have a prefix of:\n.INDENT 0.0\n.IP \\(bu 2\nname: pattern match on the archive name (default)\n.IP \\(bu 2\naid: prefix match on the archive id (only one result allowed)\n.IP \\(bu 2\nuser: exact match on the username who created the archive\n.IP \\(bu 2\nhost: exact match on the hostname where the archive was created\n.IP \\(bu 2\ntags: match on the archive tags\n.UNINDENT\n.sp\nIn case of a name pattern match,\nit uses pattern styles similar to the ones described by \\fBborg help patterns\\fP:\n.INDENT 0.0\n.TP\n.B Identical match pattern, selector \\fBid:\\fP (default)\nSimple string match, must fully match exactly as given.\n.TP\n.B Shell\\-style patterns, selector \\fBsh:\\fP\nMatch like on the shell, wildcards like \\fI*\\fP and \\fI?\\fP work.\n.TP\n.B Regular expressions \\%<https://\\:docs\\:.python\\:.org/\\:3/\\:library/\\:re\\:.html>, selector \\fBre:\\fP\nFull regular expression support.\nThis is very powerful, but can also get rather complicated.\n.UNINDENT\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# name match, id: style\nborg delete \\-\\-match\\-archives \\(aqid:archive\\-with\\-crap\\(aq\nborg delete \\-a \\(aqid:archive\\-with\\-crap\\(aq  # same, using short option\nborg delete \\-a \\(aqarchive\\-with\\-crap\\(aq  # same, because \\(aqid:\\(aq is the default\n\n# name match, sh: style\nborg delete \\-a \\(aqsh:home\\-kenny\\-*\\(aq\n\n# name match, re: style\nborg delete \\-a \\(aqre:pc[123]\\-home\\-(user1|user2)\\-2022\\-09\\-.*\\(aq\n\n# archive id prefix match:\nborg delete \\-a \\(aqaid:d34db33f\\(aq\n\n# host or user match\nborg delete \\-a \\(aquser:kenny\\(aq\nborg delete \\-a \\(aqhost:kenny\\-pc\\(aq\n\n# tags match\nborg delete \\-a \\(aqtags:TAG1\\(aq \\-a \\(aqtags:TAG2\\(aq\n.EE\n.UNINDENT\n.UNINDENT\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-mount.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-mount\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-mount \\- Mounts an archive or an entire repository as a FUSE filesystem.\n.SH SYNOPSIS\n.sp\nborg [common options] mount [options] MOUNTPOINT [PATH...]\n.SH DESCRIPTION\n.sp\nThis command mounts a repository or an archive as a FUSE filesystem.\nThis can be useful for browsing or restoring individual files.\n.sp\nWhen restoring, take into account that the current FUSE implementation does\nnot support special fs flags and ACLs.\n.sp\nWhen mounting a repository, the top directories will be named like the\narchives and the directory structure below these will be loaded on\\-demand from\nthe repository when entering these directories, so expect some delay.\n.sp\nCare should be taken, as Borg backs up symlinks as\\-is. When an archive\nor repository is mounted, it is possible to “jump” outside the mount point\nby following a symlink. If this happens, files or directories (or versions of them)\nthat are not part of the archive or repository may appear to be within the mount point.\n.sp\nUnless the \\fB\\-\\-foreground\\fP option is given, the command will run in the\nbackground until the filesystem is \\fBunmounted\\fP\\&.\n.sp\nPerformance tips:\n.INDENT 0.0\n.IP \\(bu 2\nWhen doing a \\(dqwhole repository\\(dq mount:\ndo not enter archive directories if not needed; this avoids on\\-demand loading.\n.IP \\(bu 2\nOnly mount a specific archive, not the whole repository.\n.IP \\(bu 2\nOnly mount specific paths in a specific archive, not the complete archive.\n.UNINDENT\n.sp\nThe command \\fBborgfs\\fP provides a wrapper for \\fBborg mount\\fP\\&. This can also be\nused in fstab entries:\n\\fB/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0\\fP\n.sp\nTo allow a regular user to use fstab entries, add the \\fBuser\\fP option:\n\\fB/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0\\fP\n.sp\nFor FUSE configuration and mount options, see the mount.fuse(8) manual page.\n.sp\nBorg\\(aqs default behavior is to use the archived user and group names of each\nfile and map them to the system\\(aqs respective user and group IDs.\nAlternatively, using \\fBnumeric\\-ids\\fP will instead use the archived user and\ngroup IDs without any mapping.\n.sp\nThe \\fBuid\\fP and \\fBgid\\fP mount options (implemented by Borg) can be used to\noverride the user and group IDs of all files (i.e., \\fBborg mount \\-o\nuid=1000,gid=1000\\fP).\n.sp\nThe man page references \\fBuser_id\\fP and \\fBgroup_id\\fP mount options\n(implemented by FUSE) which specify the user and group ID of the mount owner\n(also known as the user who does the mounting). It is set automatically by libfuse (or\nthe filesystem if libfuse is not used). However, you should not specify these\nmanually. Unlike the \\fBuid\\fP and \\fBgid\\fP mount options, which affect all files,\n\\fBuser_id\\fP and \\fBgroup_id\\fP affect the user and group ID of the mounted\n(base) directory.\n.sp\nAdditional mount options supported by Borg:\n.INDENT 0.0\n.IP \\(bu 2\n\\fBversions\\fP: when used with a repository mount, this gives a merged, versioned\nview of the files in the archives. EXPERIMENTAL; layout may change in the future.\n.IP \\(bu 2\n\\fBallow_damaged_files\\fP: by default, damaged files (where chunks are missing)\nwill return EIO (I/O error) when trying to read the related parts of the file.\nSet this option to replace the missing parts with all\\-zero bytes.\n.IP \\(bu 2\n\\fBignore_permissions\\fP: for security reasons the \\fBdefault_permissions\\fP mount\noption is internally enforced by Borg. \\fBignore_permissions\\fP can be given to\nnot enforce \\fBdefault_permissions\\fP\\&.\n.UNINDENT\n.sp\nThe BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is intended for advanced users\nto tweak performance. It sets the number of cached data chunks; additional\nmemory usage can be up to ~8 MiB times this number. The default is the number\nof CPU cores.\n.sp\nWhen the daemonized process receives a signal or crashes, it does not unmount.\nUnmounting in these cases could cause an active rsync or similar process\nto delete data unintentionally.\n.sp\nWhen running in the foreground, ^C/SIGINT cleanly unmounts the filesystem,\nbut other signals or crashes do not.\n.sp\nDebugging:\n.sp\n\\fBborg mount\\fP usually daemonizes and the daemon process sends stdout/stderr\nto /dev/null. Thus, you need to either use \\fB\\-f / \\-\\-foreground\\fP to make it stay\nin the foreground and not daemonize, or use \\fBBORG_LOGGING_CONF\\fP to reconfigure\nthe logger to output to a file.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B MOUNTPOINT\nwhere to mount the filesystem\n.TP\n.B PATH\npaths to extract; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-f\\fP,\\fB  \\-\\-foreground\nstay in foreground, do not daemonize\n.TP\n.B  \\-o\nextra mount options\n.TP\n.B  \\-\\-numeric\\-ids\nuse numeric user and group identifiers from archives\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.BI \\-\\-strip\\-components \\ NUMBER\nRemove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-umount(1)\\fP, \\fIborg\\-extract(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-patterns.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-patterns\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-patterns \\- Details regarding patterns\n.SH DESCRIPTION\n.sp\nWhen specifying one or more file paths in a Borg command that supports\npatterns for the respective option or argument, you can apply the\npatterns described here to include only desired files and/or exclude\nunwanted ones. Patterns can be used\n.INDENT 0.0\n.IP \\(bu 2\nfor \\fB\\-\\-exclude\\fP option,\n.IP \\(bu 2\nin the file given with \\fB\\-\\-exclude\\-from\\fP option,\n.IP \\(bu 2\nfor \\fB\\-\\-pattern\\fP option,\n.IP \\(bu 2\nin the file given with \\fB\\-\\-patterns\\-from\\fP option and\n.IP \\(bu 2\nfor \\fBPATH\\fP arguments that explicitly support them.\n.UNINDENT\n.sp\nThe path/filenames used as input for the pattern matching start with the\ncurrently active recursion root. You usually give the recursion root(s)\nwhen invoking borg and these can be either relative or absolute paths.\n.sp\nBe careful, your patterns must match the archived paths:\n.INDENT 0.0\n.IP \\(bu 2\nArchived paths never start with a leading slash (\\(aq/\\(aq), nor with \\(aq.\\(aq, nor with \\(aq..\\(aq.\n.INDENT 2.0\n.IP \\(bu 2\nWhen you back up absolute paths like \\fB/home/user\\fP, the archived\npaths start with \\fBhome/user\\fP\\&.\n.IP \\(bu 2\nWhen you back up relative paths like \\fB\\&./src\\fP, the archived paths\nstart with \\fBsrc\\fP\\&.\n.IP \\(bu 2\nWhen you back up relative paths like \\fB\\&../../src\\fP, the archived paths\nstart with \\fBsrc\\fP\\&.\n.IP \\(bu 2\nOn native Windows, archived absolute paths look like \\fBC/Windows/System32\\fP\\&.\n.UNINDENT\n.UNINDENT\n.sp\nBorg supports different pattern styles. To define a non\\-default\nstyle for a specific pattern, prefix it with two characters followed\nby a colon \\(aq:\\(aq (i.e. \\fBfm:path/*\\fP, \\fBsh:path/**\\fP).\n.sp\nNote: Windows users must only use forward slashes in patterns, not backslashes.\n.sp\nThe default pattern style for \\fB\\-\\-exclude\\fP differs from \\fB\\-\\-pattern\\fP, see below.\n.INDENT 0.0\n.TP\n.B Fnmatch \\%<https://\\:docs\\:.python\\:.org/\\:3/\\:library/\\:fnmatch\\:.html>, selector \\fBfm:\\fP\nThis is the default style for \\fB\\-\\-exclude\\fP and \\fB\\-\\-exclude\\-from\\fP\\&.\nThese patterns use a variant of shell pattern syntax, with \\(aq*\\(aq matching\nany number of characters, \\(aq?\\(aq matching any single character, \\(aq[...]\\(aq\nmatching any single character specified, including ranges, and \\(aq[!...]\\(aq\nmatching any character not specified. For the purpose of these patterns,\nthe path separator (forward slash \\(aq/\\(aq) is not treated specially.\nWrap meta\\-characters in brackets for a literal\nmatch (i.e. \\fB[?]\\fP to match the literal character \\(aq?\\(aq). For a path\nto match a pattern, the full path must match, or it must match\nfrom the start of the full path to just before a path separator. Except\nfor the root path, paths will never end in the path separator when\nmatching is attempted.  Thus, if a given pattern ends in a path\nseparator, a \\(aq*\\(aq is appended before matching is attempted. A leading\npath separator is always removed.\n.TP\n.B Shell\\-style patterns, selector \\fBsh:\\fP\nThis is the default style for \\fB\\-\\-pattern\\fP and \\fB\\-\\-patterns\\-from\\fP\\&.\nLike fnmatch patterns these are similar to shell patterns. The difference\nis that the pattern may include \\fB**/\\fP for matching zero or more directory\nlevels, \\fB*\\fP for matching zero or more arbitrary characters with the\nexception of any path separator, \\fB{}\\fP containing comma\\-separated\nalternative patterns. A leading path separator is always removed.\n.TP\n.B Regular expressions \\%<https://\\:docs\\:.python\\:.org/\\:3/\\:library/\\:re\\:.html>, selector \\fBre:\\fP\nUnlike shell patterns, regular expressions are not required to match the full\npath and any substring match is sufficient. It is strongly recommended to\nanchor patterns to the start (\\(aq^\\(aq), to the end (\\(aq$\\(aq) or both.\n.TP\n.B Path prefix, selector \\fBpp:\\fP\nThis pattern style is useful to match whole subdirectories. The pattern\n\\fBpp:root/somedir\\fP matches \\fBroot/somedir\\fP and everything therein.\nA leading path separator is always removed.\n.TP\n.B Path full\\-match, selector \\fBpf:\\fP\nThis pattern style is (only) useful to match full paths.\nThis is kind of a pseudo pattern as it cannot have any variable or\nunspecified parts \\- the full path must be given. \\fBpf:root/file.ext\\fP\nmatches \\fBroot/file.ext\\fP only. A leading path separator is always\nremoved.\n.sp\nImplementation note: this is implemented via very time\\-efficient O(1)\nhashtable lookups (this means you can have huge amounts of such patterns\nwithout impacting performance much).\nDue to that, this kind of pattern does not respect any context or order.\nIf you use such a pattern to include a file, it will always be included\n(if the directory recursion encounters it).\nOther include/exclude patterns that would normally match will be ignored.\nSame logic applies for exclude.\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\n\\fBre:\\fP, \\fBsh:\\fP and \\fBfm:\\fP patterns are all implemented on top of\nthe Python SRE engine. It is very easy to formulate patterns for each\nof these types which requires an inordinate amount of time to match\npaths. If untrusted users are able to supply patterns, ensure they\ncannot supply \\fBre:\\fP patterns. Further, ensure that \\fBsh:\\fP and\n\\fBfm:\\fP patterns only contain a handful of wildcards at most.\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\n\\fBWindows path handling\\fP: All paths in Borg archives use forward slashes (\\fB/\\fP)\nas path separators, regardless of the platform. When creating archives on Windows,\nbackslashes from filesystem paths are automatically converted to forward slashes.\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\n\\fBWindows reserved characters\\fP: On Windows, when extracting archives created on\nPOSIX systems, paths may contain characters that are reserved from being used in\nfile or directory names (like: \\fB< > : \\(dq \\e | ? *\\fP).\nThese are replaced by characters in the unicode private use area (\\fBU+F0xx\\fP) like\nthe CIFS mapchars feature also does it. It won\\(aqt be pretty, but at least it works.\n.UNINDENT\n.UNINDENT\n.sp\nExclusions can be passed via the command line option \\fB\\-\\-exclude\\fP\\&. When used\nfrom within a shell, the patterns should be quoted to protect them from\nexpansion.\n.sp\nPatterns matching special characters, e.g. whitespace, within a shell may\nrequire adjustments, such as putting quotation marks around the arguments.\nExample:\nUsing bash, the following command line option would match and exclude \\(dqitem name\\(dq:\n\\fB\\-\\-pattern=\\(aq\\-path/item name\\(aq\\fP\nNote that when patterns are used within a pattern file directly read by borg,\ne.g. when using \\fB\\-\\-exclude\\-from\\fP or \\fB\\-\\-patterns\\-from\\fP, there is no shell\ninvolved and thus no quotation marks are required.\n.sp\nThe \\fB\\-\\-exclude\\-from\\fP option permits loading exclusion patterns from a text\nfile with one pattern per line. Lines empty or starting with the hash sign\n\\(aq#\\(aq after removing whitespace on both ends are ignored. The optional style\nselector prefix is also supported for patterns loaded from a file. Due to\nwhitespace removal, paths with whitespace at the beginning or end can only be\nexcluded using regular expressions.\n.sp\nTo test your exclusion patterns without performing an actual backup you can\nrun \\fBborg create \\-\\-list \\-\\-dry\\-run ...\\fP\\&.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Exclude a directory anywhere in the tree named \\(ga\\(gasteamapps/common\\(ga\\(ga\n# (and everything below it), regardless of where it appears:\n$ borg create \\-e \\(aqsh:**/steamapps/common/**\\(aq archive /\n\n# Exclude the contents of \\(ga\\(ga/home/user/.cache\\(ga\\(ga:\n$ borg create \\-e \\(aqsh:home/user/.cache/**\\(aq archive /home/user\n$ borg create \\-e home/user/.cache/ archive /home/user\n\n# The file \\(aq/home/user/.cache/important\\(aq is *not* backed up:\n$ borg create \\-e home/user/.cache/ archive / /home/user/.cache/important\n\n# Exclude \\(aq/home/user/file.o\\(aq but not \\(aq/home/user/file.odt\\(aq:\n$ borg create \\-e \\(aq*.o\\(aq archive /\n\n# Exclude \\(aq/home/user/junk\\(aq and \\(aq/home/user/subdir/junk\\(aq but\n# not \\(aq/home/user/importantjunk\\(aq or \\(aq/etc/junk\\(aq:\n$ borg create \\-e \\(aqhome/*/junk\\(aq archive /\n\n# The contents of directories in \\(aq/home\\(aq are not backed up when their name\n# ends in \\(aq.tmp\\(aq\n$ borg create \\-\\-exclude \\(aqre:^home/[^/]+\\e.tmp/\\(aq archive /\n\n# Load exclusions from file\n$ cat >exclude.txt <<EOF\n# Comment line\nhome/*/junk\n*.tmp\nfm:aa:something/*\nre:^home/[^/]+\\e.tmp/\nsh:home/*/.thumbnails\n# Example with spaces, no need to escape as it is processed by borg\nsome file with spaces.txt\nEOF\n$ borg create \\-\\-exclude\\-from exclude.txt archive /\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nA more general and easier to use way to define filename matching patterns\nexists with the \\fB\\-\\-pattern\\fP and \\fB\\-\\-patterns\\-from\\fP options. Using\nthese, you may specify the backup roots, default pattern styles and\npatterns for inclusion and exclusion.\n.INDENT 0.0\n.TP\n.B Root path prefix \\fBR\\fP\nA recursion root path starts with the prefix \\fBR\\fP, followed by a path\n(a plain path, not a file pattern). Use this prefix to have the root\npaths in the patterns file rather than as command line arguments.\n.TP\n.B Pattern style prefix \\fBP\\fP (only useful within patterns files)\nTo change the default pattern style, use the \\fBP\\fP prefix, followed by\nthe pattern style abbreviation (\\fBfm\\fP, \\fBpf\\fP, \\fBpp\\fP, \\fBre\\fP, \\fBsh\\fP).\nAll patterns following this line in the same patterns file will use this\nstyle until another style is specified or the end of the file is reached.\nWhen the current patterns file is finished, the default pattern style will\nreset.\n.TP\n.B Exclude pattern prefix \\fB\\-\\fP\nUse the prefix \\fB\\-\\fP, followed by a pattern, to define an exclusion.\nThis has the same effect as the \\fB\\-\\-exclude\\fP option.\n.TP\n.B Exclude no\\-recurse pattern prefix \\fB!\\fP\nUse the prefix \\fB!\\fP, followed by a pattern, to define an exclusion\nthat does not recurse into subdirectories. This saves time, but\nprevents include patterns to match any files in subdirectories.\n.TP\n.B Include pattern prefix \\fB+\\fP\nUse the prefix \\fB+\\fP, followed by a pattern, to define inclusions.\nThis is useful to include paths that are covered in an exclude\npattern and would otherwise not be backed up.\n.UNINDENT\n.sp\nThe first matching pattern is used, so if an include pattern matches\nbefore an exclude pattern, the file is backed up. Note that a no\\-recurse\nexclude stops examination of subdirectories so that potential includes\nwill not match \\- use normal excludes for such use cases.\n.sp\nExample:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Define the recursion root\nR /\n# Exclude all iso files in any directory\n\\- **/*.iso\n# Explicitly include all inside etc and root\n+ etc/**\n+ root/**\n# Exclude a specific directory under each user\\(aqs home directories\n\\- home/*/.cache\n# Explicitly include everything in /home\n+ home/**\n# Explicitly exclude some directories without recursing into them\n! re:^(dev|proc|run|sys|tmp)\n# Exclude all other files and directories\n# that are not specifically included earlier.\n\\- **\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fBTip: You can easily test your patterns with \\-\\-dry\\-run and  \\-\\-list\\fP:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg create \\-\\-dry\\-run \\-\\-list \\-\\-patterns\\-from patterns.txt archive\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis will list the considered files one per line, prefixed with a\ncharacter that indicates the action (e.g. \\(aqx\\(aq for excluding, see\n\\fBItem flags\\fP in \\fIborg create\\fP usage docs).\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nIt is possible that a subdirectory or file is matched while its parent\ndirectories are not. In that case, parent directories are not backed\nup and thus their user, group, permission, etc. cannot be restored.\n.UNINDENT\n.UNINDENT\n.sp\nPatterns (\\fB\\-\\-pattern\\fP) and excludes (\\fB\\-\\-exclude\\fP) from the command line are\nconsidered first (in the order of appearance). Then patterns from \\fB\\-\\-patterns\\-from\\fP\nare added. Exclusion patterns from \\fB\\-\\-exclude\\-from\\fP files are appended last.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# back up pics, but not the ones from 2018, except the good ones:\n# note: using = is essential to avoid cmdline argument parsing issues.\nborg create \\-\\-pattern=+pics/2018/good \\-\\-pattern=\\-pics/2018 archive pics\n\n# back up only JPG/JPEG files (case insensitive) in all home directories:\nborg create \\-\\-pattern \\(aq+ re:\\e.jpe?g(?i)$\\(aq archive /home\n\n# back up homes, but exclude big downloads (like .ISO files) or hidden files:\nborg create \\-\\-exclude \\(aqre:\\e.iso(?i)$\\(aq \\-\\-exclude \\(aqsh:home/**/.*\\(aq archive /home\n\n# use a file with patterns (recursion root \\(aq/\\(aq via command line):\nborg create \\-\\-patterns\\-from patterns.lst archive /\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThe patterns.lst file could look like that:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# \\(dqsh:\\(dq pattern style is the default\n# exclude caches\n\\- home/*/.cache\n# include susans home\n+ home/susan\n# also back up this exact file\n+ pf:home/bobby/specialfile.txt\n# don\\(aqt back up the other home directories\n\\- home/*\n# don\\(aqt even look in /dev, /proc, /run, /sys, /tmp (note: would exclude files like /device, too)\n! re:^(dev|proc|run|sys|tmp)\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nYou can specify recursion roots either on the command line or in a patternfile:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# these two commands do the same thing\nborg create \\-\\-exclude home/bobby/junk archive /home/bobby /home/susan\nborg create \\-\\-patterns\\-from patternfile.lst archive\n.EE\n.UNINDENT\n.UNINDENT\n.sp\npatternfile.lst:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# note that excludes use fm: by default and patternfiles use sh: by default.\n# therefore, we need to specify fm: to have the same exact behavior.\nP fm\nR /home/bobby\nR /home/susan\n\\- home/bobby/junk\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis allows you to share the same patterns between multiple repositories\nwithout needing to specify them on the command line.\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-placeholders.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-placeholders\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-placeholders \\- Details regarding placeholders\n.SH DESCRIPTION\n.sp\nRepository URLs, \\fB\\-\\-name\\fP, \\fB\\-a\\fP / \\fB\\-\\-match\\-archives\\fP, \\fB\\-\\-comment\\fP\nand \\fB\\-\\-remote\\-path\\fP values support these placeholders:\n.INDENT 0.0\n.TP\n.B {hostname}\nThe (short) hostname of the machine.\n.TP\n.B {fqdn}\nThe full name of the machine.\n.TP\n.B {reverse\\-fqdn}\nThe full name of the machine in reverse domain name notation.\n.TP\n.B {now}\nThe current local date and time, by default in ISO\\-8601 format.\nYou can also supply your own format string \\%<https://\\:docs\\:.python\\:.org/\\:3\\:.10/\\:library/\\:datetime\\:.html#\\:strftime-and-strptime-behavior>, e.g. {now:%Y\\-%m\\-%d_%H:%M:%S}\n.TP\n.B {utcnow}\nThe current UTC date and time, by default in ISO\\-8601 format.\nYou can also supply your own format string \\%<https://\\:docs\\:.python\\:.org/\\:3\\:.10/\\:library/\\:datetime\\:.html#\\:strftime-and-strptime-behavior>, e.g. {utcnow:%Y\\-%m\\-%d_%H:%M:%S}\n.TP\n.B {user}\nThe user name (or UID, if no name is available) of the user running borg.\n.TP\n.B {pid}\nThe current process ID.\n.TP\n.B {borgversion}\nThe version of borg, e.g.: 1.0.8rc1\n.TP\n.B {borgmajor}\nThe version of borg, only the major version, e.g.: 1\n.TP\n.B {borgminor}\nThe version of borg, only major and minor version, e.g.: 1.0\n.TP\n.B {borgpatch}\nThe version of borg, only major, minor and patch version, e.g.: 1.0.8\n.UNINDENT\n.sp\nIf literal curly braces need to be used, double them for escaping:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-\\-repo /path/to/repo {{literal_text}}\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-\\-repo /path/to/repo {hostname}\\-{user}\\-{utcnow} ...\nborg create \\-\\-repo /path/to/repo {hostname}\\-{now:%Y\\-%m\\-%d_%H:%M:%S%z} ...\nborg prune \\-a \\(aqsh:{hostname}\\-*\\(aq ...\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nsystemd uses a difficult, non\\-standard syntax for command lines in unit files (refer to\nthe \\fIsystemd.unit(5)\\fP manual page).\n.sp\nWhen invoking borg from unit files, pay particular attention to escaping,\nespecially when using the now/utcnow placeholders, since systemd performs its own\n%\\-based variable replacement even in quoted text. To avoid interference from systemd,\ndouble all percent signs (\\fB{hostname}\\-{now:%Y\\-%m\\-%d_%H:%M:%S}\\fP\nbecomes \\fB{hostname}\\-{now:%%Y\\-%%m\\-%%d_%%H:%%M:%%S}\\fP).\n.UNINDENT\n.UNINDENT\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-prune.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-prune\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-prune \\- Prune archives according to specified rules.\n.SH SYNOPSIS\n.sp\nborg [common options] prune [options] [NAME]\n.SH DESCRIPTION\n.sp\nThe prune command prunes a repository by soft\\-deleting all archives not\nmatching any of the specified retention options.\n.sp\nImportant:\n.INDENT 0.0\n.IP \\(bu 2\nThe prune command will only mark archives for deletion (\\(dqsoft\\-deletion\\(dq),\nrepository disk space is \\fBnot\\fP freed until you run \\fBborg compact\\fP\\&.\n.IP \\(bu 2\nYou can use \\fBborg undelete\\fP to undelete archives, but only until\nyou run \\fBborg compact\\fP\\&.\n.UNINDENT\n.sp\nThis command is normally used by automated backup scripts wanting to keep a\ncertain number of historic backups. This retention policy is commonly referred to as\nGFS \\%<https://\\:en\\:.wikipedia\\:.org/\\:wiki/\\:Backup_rotation_scheme#\\:Grandfather-father-son>\n(Grandfather\\-father\\-son) backup rotation scheme.\n.sp\nThe recommended way to use prune is to give the archive series name to it via the\nNAME argument (assuming you have the same name for all archives in a series).\nAlternatively, you can also use \\-\\-match\\-archives (\\-a), then only archives that\nmatch the pattern are considered for deletion and only those archives count\ntowards the totals specified by the rules.\nOtherwise, \\fIall\\fP archives in the repository are candidates for deletion!\nThere is no automatic distinction between archives representing different\ncontents. These need to be distinguished by specifying matching globs.\n.sp\nIf you have multiple series of archives with different data sets (e.g.\nfrom different machines) in one shared repository, use one prune call per\nseries.\n.sp\nThe \\fB\\-\\-keep\\-within\\fP option takes an argument of the form \\(dq<int><char>\\(dq,\nwhere char is \\(dqy\\(dq, \\(dqm\\(dq, \\(dqw\\(dq, \\(dqd\\(dq, \\(dqH\\(dq, \\(dqM\\(dq, or \\(dqS\\(dq.  For example,\n\\fB\\-\\-keep\\-within 2d\\fP means to keep all archives that were created within\nthe past 2 days.  \\(dq1m\\(dq is taken to mean \\(dq31d\\(dq. The archives kept with\nthis option do not count towards the totals specified by any other options.\n.sp\nA good procedure is to thin out more and more the older your backups get.\nAs an example, \\fB\\-\\-keep\\-daily 7\\fP means to keep the latest backup on each day,\nup to 7 most recent days with backups (days without backups do not count).\nThe rules are applied from secondly to yearly, and backups selected by previous\nrules do not count towards those of later rules. The time that each backup\nstarts is used for pruning purposes. Dates and times are interpreted in the local\ntimezone of the system where borg prune runs, and weeks go from Monday to Sunday.\nSpecifying a negative number of archives to keep means that there is no limit.\n.sp\nBorg will retain the oldest archive if any of the secondly, minutely, hourly,\ndaily, weekly, monthly, quarterly, or yearly rules was not otherwise able to\nmeet its retention target. This enables the first chronological archive to\ncontinue aging until it is replaced by a newer archive that meets the retention\ncriteria.\n.sp\nThe \\fB\\-\\-keep\\-13weekly\\fP and \\fB\\-\\-keep\\-3monthly\\fP rules are two different\nstrategies for keeping archives every quarter year.\n.sp\nThe \\fB\\-\\-keep\\-last N\\fP option is doing the same as \\fB\\-\\-keep\\-secondly N\\fP (and it will\nkeep the last N archives under the assumption that you do not create more than one\nbackup archive in the same second).\n.sp\nYou can influence how the \\fB\\-\\-list\\fP output is formatted by using the \\fB\\-\\-short\\fP\noption (less wide output) or by giving a custom format using \\fB\\-\\-format\\fP (see\nthe \\fBborg repo\\-list\\fP description for more details about the format string).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change the repository\n.TP\n.B  \\-\\-list\noutput a verbose list of archives it keeps/prunes\n.TP\n.B  \\-\\-short\nuse a less wide archive part format\n.TP\n.B  \\-\\-list\\-pruned\noutput verbose list of archives it prunes\n.TP\n.B  \\-\\-list\\-kept\noutput verbose list of archives it keeps\n.TP\n.BI \\-\\-format \\ FORMAT\nspecify format for the archive part (default: \\(dq{archive:<36} {time} [{id}]\\(dq)\n.TP\n.BI \\-\\-keep\\-within \\ INTERVAL\nkeep all archives within this time interval\n.TP\n.B  \\-\\-keep\\-last\\fP,\\fB  \\-\\-keep\\-secondly\nnumber of secondly archives to keep\n.TP\n.B  \\-\\-keep\\-minutely\nnumber of minutely archives to keep\n.TP\n.B  \\-H\\fP,\\fB  \\-\\-keep\\-hourly\nnumber of hourly archives to keep\n.TP\n.B  \\-d\\fP,\\fB  \\-\\-keep\\-daily\nnumber of daily archives to keep\n.TP\n.B  \\-w\\fP,\\fB  \\-\\-keep\\-weekly\nnumber of weekly archives to keep\n.TP\n.B  \\-m\\fP,\\fB  \\-\\-keep\\-monthly\nnumber of monthly archives to keep\n.TP\n.B  \\-\\-keep\\-13weekly\nnumber of quarterly archives to keep (13 week strategy)\n.TP\n.B  \\-\\-keep\\-3monthly\nnumber of quarterly archives to keep (3 month strategy)\n.TP\n.B  \\-y\\fP,\\fB  \\-\\-keep\\-yearly\nnumber of yearly archives to keep\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH EXAMPLES\n.sp\nBe careful: prune is a potentially dangerous command that removes backup\narchives.\n.sp\nBy default, prune applies to \\fBall archives in the repository\\fP unless you\nrestrict its operation to a subset of the archives.\n.sp\nThe recommended way to name archives (with \\fBborg create\\fP) is to use the\nidentical archive name within a series of archives. Then you can simply give\nthat name to prune as well, so it operates only on that series of archives.\n.sp\nAlternatively, you can use \\fB\\-a\\fP/\\fB\\-\\-match\\-archives\\fP to match archive names\nand select a subset of them.\nWhen using \\fB\\-a\\fP, be careful to choose a good pattern — for example, do not use a\nprefix \\(dqfoo\\(dq if you do not also want to match \\(dqfoobar\\(dq.\n.sp\nIt is strongly recommended to always run \\fBprune \\-v \\-\\-list \\-\\-dry\\-run ...\\fP\nfirst, so you will see what it would do without it actually doing anything.\n.sp\nDo not forget to run \\fBborg compact \\-v\\fP after prune to actually free disk space.\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Keep 7 end of day and 4 additional end of week archives.\n# Do a dry\\-run without actually deleting anything.\n$ borg prune \\-v \\-\\-list \\-\\-dry\\-run \\-\\-keep\\-daily=7 \\-\\-keep\\-weekly=4\n\n# Similar to the above, but only apply to the archive series named \\(aq{hostname}\\(aq:\n$ borg prune \\-v \\-\\-list \\-\\-keep\\-daily=7 \\-\\-keep\\-weekly=4 \\(aq{hostname}\\(aq\n\n# Similar to the above, but apply to archive names starting with the hostname\n# of the machine followed by a \\(aq\\-\\(aq character:\n$ borg prune \\-v \\-\\-list \\-\\-keep\\-daily=7 \\-\\-keep\\-weekly=4 \\-a \\(aqsh:{hostname}\\-*\\(aq\n\n# Keep 7 end of day, 4 additional end of week archives,\n# and an end of month archive for every month:\n$ borg prune \\-v \\-\\-list \\-\\-keep\\-daily=7 \\-\\-keep\\-weekly=4 \\-\\-keep\\-monthly=\\-1\n\n# Keep all backups in the last 10 days, 4 additional end of week archives,\n# and an end of month archive for every month:\n$ borg prune \\-v \\-\\-list \\-\\-keep\\-within=10d \\-\\-keep\\-weekly=4 \\-\\-keep\\-monthly=\\-1\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThere is also a visualized prune example in \\fBdocs/misc/prune\\-example.txt\\fP\\&.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-compact(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-recreate.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-recreate\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-recreate \\- Recreate archives.\n.SH SYNOPSIS\n.sp\nborg [common options] recreate [options] [PATH...]\n.SH DESCRIPTION\n.sp\nRecreate the contents of existing archives.\n.sp\nRecreate is a potentially dangerous function and might lead to data loss\n(if used wrongly). BE VERY CAREFUL!\n.sp\nImportant: Repository disk space is \\fBnot\\fP freed until you run \\fBborg compact\\fP\\&.\n.sp\n\\fB\\-\\-exclude\\fP, \\fB\\-\\-exclude\\-from\\fP, \\fB\\-\\-exclude\\-if\\-present\\fP, \\fB\\-\\-keep\\-exclude\\-tags\\fP\nand PATH have the exact same semantics as in \\(dqborg create\\(dq, but they only check\nfiles in the archives and not in the local filesystem. If paths are specified,\nthe resulting archives will contain only files from those paths.\n.sp\nNote that all paths in an archive are relative, therefore absolute patterns/paths\nwill \\fInot\\fP match (\\fB\\-\\-exclude\\fP, \\fB\\-\\-exclude\\-from\\fP, PATHs).\n.sp\n\\fB\\-\\-chunker\\-params\\fP will re\\-chunk all files in the archive, this can be\nused to have upgraded Borg 0.xx archives deduplicate with Borg 1.x archives.\n.sp\n\\fBUSE WITH CAUTION.\\fP\nDepending on the paths and patterns given, recreate can be used to\ndelete files from archives permanently.\nWhen in doubt, use \\fB\\-\\-dry\\-run \\-\\-verbose \\-\\-list\\fP to see how patterns/paths are\ninterpreted. See \\fIlist_item_flags\\fP in \\fBborg create\\fP for details.\n.sp\nThe archive being recreated is only removed after the operation completes. The\narchive that is built during the operation exists at the same time at\n\\(dq<ARCHIVE>.recreate\\(dq. The new archive will have a different archive ID.\n.sp\nWith \\fB\\-\\-target\\fP the original archive is not replaced, instead a new archive is created.\n.sp\nWhen rechunking, space usage can be substantial \\- expect\nat least the entire deduplicated size of the archives using the previous\nchunker params.\n.sp\nIf your most recent borg check found missing chunks, please first run another\nbackup for the same data, before doing any rechunking. If you are lucky, that\nwill recreate the missing chunks. Optionally, do another borg check to see\nif the chunks are still missing.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B PATH\npaths to recreate; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-list\noutput verbose list of items (files, dirs, ...)\n.TP\n.BI \\-\\-filter \\ STATUSCHARS\nonly display items with the given status characters (listed in borg create \\-\\-help)\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change anything\n.TP\n.B  \\-s\\fP,\\fB  \\-\\-stats\nprint statistics at end\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.B  \\-\\-exclude\\-caches\nexclude directories that contain a CACHEDIR.TAG file (\\%<https://\\:www\\:.bford\\:.info/\\:cachedir/\\:spec\\:.html>)\n.TP\n.BI \\-\\-exclude\\-if\\-present \\ NAME\nexclude directories that are tagged by containing a filesystem object with the given NAME\n.TP\n.B  \\-\\-keep\\-exclude\\-tags\nif tag objects are specified with \\fB\\-\\-exclude\\-if\\-present\\fP, do not omit the tag objects themselves from the backup archive\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-target \\ TARGET\ncreate a new archive with the name ARCHIVE, do not replace existing archive\n.TP\n.BI \\-\\-comment \\ COMMENT\nadd a comment text to the archive\n.TP\n.BI \\-\\-timestamp \\ TIMESTAMP\nmanually specify the archive creation date/time (yyyy\\-mm\\-ddThh:mm:ss[(+|\\-)HH:MM] format, (+|\\-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n.TP\n.BI \\-C \\ COMPRESSION\\fR,\\fB \\ \\-\\-compression \\ COMPRESSION\nselect compression algorithm, see the output of the \\(dqborg help compression\\(dq command for details.\n.TP\n.BI \\-\\-chunker\\-params \\ PARAMS\nrechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or \\fIdefault\\fP to use the chunker defaults. default: do not rechunk\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Create a backup with fast, low compression\n$ borg create archive /some/files \\-\\-compression lz4\n# Then recompress it — this might take longer, but the backup has already completed,\n# so there are no inconsistencies from a long\\-running backup job.\n$ borg recreate \\-a archive \\-\\-recompress \\-\\-compression zlib,9\n\n# Remove unwanted files from all archives in a repository.\n# Note the relative path for the \\-\\-exclude option — archives only contain relative paths.\n$ borg recreate \\-\\-exclude home/icke/Pictures/drunk_photos\n\n# Change the archive comment\n$ borg create \\-\\-comment \\(dqThis is a comment\\(dq archivename ~\n$ borg info \\-a archivename\nName: archivename\nFingerprint: ...\nComment: This is a comment\n\\&...\n$ borg recreate \\-\\-comment \\(dqThis is a better comment\\(dq \\-a archivename\n$ borg info \\-a archivename\nName: archivename\nFingerprint: ...\nComment: This is a better comment\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-patterns(1)\\fP, \\fIborg\\-placeholders(1)\\fP, \\fIborg\\-compression(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-rename.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-rename\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-rename \\- Rename an existing archive.\n.SH SYNOPSIS\n.sp\nborg [common options] rename [options] OLDNAME NEWNAME\n.SH DESCRIPTION\n.sp\nThis command renames an archive in the repository.\n.sp\nThis results in a different archive ID.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B OLDNAME\nspecify the current archive name\n.TP\n.B NEWNAME\nspecify the new archive name\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg create archivename ~\n$ borg repo\\-list\narchivename                          Mon, 2016\\-02\\-15 19:50:19\n\n$ borg rename archivename newname\n$ borg repo\\-list\nnewname                              Mon, 2016\\-02\\-15 19:50:19\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-compress.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-compress\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-compress \\- Repository (re-)compression.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-compress [options]\n.SH DESCRIPTION\n.sp\nRepository (re\\-)compression (and/or re\\-obfuscation).\n.sp\nReads all chunks in the repository and recompresses them if they are not already\nusing the compression type/level and obfuscation level given via \\fB\\-\\-compression\\fP\\&.\n.sp\nIf the outcome of the chunk processing indicates a change in compression\ntype/level or obfuscation level, the processed chunk is written to the repository.\nPlease note that the outcome might not always be the desired compression\ntype/level \\- if no compression gives a shorter output, that might be chosen.\n.sp\nPlease note that this command can not work in low (or zero) free disk space\nconditions.\n.sp\nIf the \\fBborg repo\\-compress\\fP process receives a SIGINT signal (Ctrl\\-C), the repo\nwill be committed and compacted and borg will terminate cleanly afterwards.\n.sp\nBoth \\fB\\-\\-progress\\fP and \\fB\\-\\-stats\\fP are recommended when \\fBborg repo\\-compress\\fP\nis used interactively.\n.sp\nYou do \\fBnot\\fP need to run \\fBborg compact\\fP after \\fBborg repo\\-compress\\fP\\&.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.BI \\-C \\ COMPRESSION\\fR,\\fB \\ \\-\\-compression \\ COMPRESSION\nselect compression algorithm, see the output of the \\(dqborg help compression\\(dq command for details.\n.TP\n.B  \\-s\\fP,\\fB  \\-\\-stats\nprint statistics\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Recompress repository contents\n$ borg repo\\-compress \\-\\-progress \\-\\-compression=zstd,3\n\n# Recompress and obfuscate repository contents\n$ borg repo\\-compress \\-\\-progress \\-\\-compression=obfuscate,1,zstd,3\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-create.1",
    "content": "'\\\" t\n.\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-create\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-create \\- Creates a new, empty repository.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-create [options]\n.SH DESCRIPTION\n.sp\nThis command creates a new, empty repository. A repository is a \\fBborgstore\\fP store\ncontaining the deduplicated data from zero or more archives.\n.sp\nRepository creation can be quite slow for some kinds of stores (e.g. for \\fBsftp:\\fP) \\-\nthis is due to borgstore pre\\-creating all directories needed, making usage of the\nstore faster.\n.SS Encryption mode TL;DR\n.sp\nThe encryption mode can only be configured when creating a new repository \\- you can\nneither configure it on a per\\-archive basis nor change the mode of an existing repository.\nThis example will likely NOT give optimum performance on your machine (performance\ntips will come below):\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg repo\\-create \\-\\-encryption repokey\\-aes\\-ocb\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nBorg will:\n.INDENT 0.0\n.IP 1. 3\nAsk you to come up with a passphrase.\n.IP 2. 3\nCreate a borg key (which contains some random secrets. See \\fIkey_files\\fP).\n.IP 3. 3\nDerive a \\(dqkey encryption key\\(dq from your passphrase\n.IP 4. 3\nEncrypt and sign the key with the key encryption key\n.IP 5. 3\nStore the encrypted borg key inside the repository directory (in the repo config).\nThis is why it is essential to use a secure passphrase.\n.IP 6. 3\nEncrypt and sign your backups to prevent anyone from reading or forging them unless they\nhave the key and know the passphrase. Make sure to keep a backup of\nyour key \\fBoutside\\fP the repository \\- do not lock yourself out by\n\\(dqleaving your keys inside your car\\(dq (see \\fIborg_key_export\\fP).\nThe encryption is done locally \\- if you use a remote repository, the remote machine\nnever sees your passphrase, your unencrypted key or your unencrypted files.\nChunking and ID generation are also based on your key to improve\nyour privacy.\n.IP 7. 3\nUse the key when extracting files to decrypt them and to verify that the contents of\nthe backups have not been accidentally or maliciously altered.\n.UNINDENT\n.SS Picking a passphrase\n.sp\nMake sure you use a good passphrase. Not too short, not too simple. The real\nencryption / decryption key is encrypted with / locked by your passphrase.\nIf an attacker gets your key, they cannot unlock and use it without knowing the\npassphrase.\n.sp\nBe careful with special or non\\-ASCII characters in your passphrase:\n.INDENT 0.0\n.IP \\(bu 2\nBorg processes the passphrase as Unicode (and encodes it as UTF\\-8),\nso it does not have problems dealing with even the strangest characters.\n.IP \\(bu 2\nBUT: that does not necessarily apply to your OS/VM/keyboard configuration.\n.UNINDENT\n.sp\nSo better use a long passphrase made from simple ASCII characters than one that\nincludes non\\-ASCII stuff or characters that are hard or impossible to enter on\na different keyboard layout.\n.sp\nYou can change your passphrase for existing repositories at any time; it will not affect\nthe encryption/decryption key or other secrets.\n.SS Choosing an encryption mode\n.sp\nDepending on your hardware, hashing and crypto performance may vary widely.\nThe easiest way to find out what is fastest is to run \\fBborg benchmark cpu\\fP\\&.\n.sp\n\\fIrepokey\\fP modes: if you want ease\\-of\\-use and \\(dqpassphrase\\(dq security is good enough \\-\nthe key will be stored in the repository (in \\fBrepo_dir/config\\fP).\n.sp\n\\fIkeyfile\\fP modes: if you want \\(dqpassphrase and having\\-the\\-key\\(dq security \\-\nthe key will be stored in your home directory (in \\fB~/.config/borg/keys\\fP).\n.sp\nThe following table is roughly sorted in order of preference, the better ones are\nin the upper part of the table, in the lower part is the old and/or unsafe(r) stuff:\n.\\\" nanorst: inline-fill\n.\n.TS\nbox center;\nl|l|l|l.\nT{\nMode (K = keyfile or repokey)\nT}\tT{\nID\\-Hash\nT}\tT{\nEncryption\nT}\tT{\nAuthentication\nT}\n_\nT{\nK\\-blake2\\-chacha20\\-poly1305\nT}\tT{\nBLAKE2b\nT}\tT{\nCHACHA20\nT}\tT{\nPOLY1305\nT}\n_\nT{\nK\\-chacha20\\-poly1305\nT}\tT{\nHMAC\\-SHA\\-256\nT}\tT{\nCHACHA20\nT}\tT{\nPOLY1305\nT}\n_\nT{\nK\\-blake2\\-aes\\-ocb\nT}\tT{\nBLAKE2b\nT}\tT{\nAES256\\-OCB\nT}\tT{\nAES256\\-OCB\nT}\n_\nT{\nK\\-aes\\-ocb\nT}\tT{\nHMAC\\-SHA\\-256\nT}\tT{\nAES256\\-OCB\nT}\tT{\nAES256\\-OCB\nT}\n_\nT{\nauthenticated\\-blake2\nT}\tT{\nBLAKE2b\nT}\tT{\nnone\nT}\tT{\nBLAKE2b\nT}\n_\nT{\nauthenticated\nT}\tT{\nHMAC\\-SHA\\-256\nT}\tT{\nnone\nT}\tT{\nHMAC\\-SHA256\nT}\n_\nT{\nnone\nT}\tT{\nSHA\\-256\nT}\tT{\nnone\nT}\tT{\nnone\nT}\n.TE\n.\\\" nanorst: inline-replace\n.\n.sp\n\\fInone\\fP mode uses no encryption and no authentication. You are advised NOT to use this mode\nas it would expose you to all sorts of issues (DoS, confidentiality, tampering, ...) in\ncase of malicious activity in the repository.\n.sp\nIf you do \\fBnot\\fP want to encrypt the contents of your backups, but still want to detect\nmalicious tampering, use an \\fIauthenticated\\fP mode. It is like \\fIrepokey\\fP minus encryption.\nTo normally work with \\fBauthenticated\\fP repositories, you will need the passphrase, but\nthere is an emergency workaround; see \\fBBORG_WORKAROUNDS=authenticated_no_key\\fP docs.\n.SS Creating a related repository\n.sp\nYou can use \\fBborg repo\\-create \\-\\-other\\-repo ORIG_REPO ...\\fP to create a related repository\nthat uses the same secret key material as the given other/original repository.\n.sp\nBy default, only the ID key and chunker secret will be the same (these are important\nfor deduplication) and the AE crypto keys will be newly generated random keys.\n.sp\nOptionally, if you use \\fB\\-\\-copy\\-crypt\\-key\\fP you can also keep the same crypt_key\n(used for authenticated encryption). This might be desired, for example, if you want to have fewer\nkeys to manage.\n.sp\nCreating related repositories is useful, for example, if you want to use \\fBborg transfer\\fP later.\n.SS Creating a related repository for data migration from Borg 1.2 or 1.4\n.sp\nYou can use \\fBborg repo\\-create \\-\\-other\\-repo ORIG_REPO \\-\\-from\\-borg1 ...\\fP to create a related\nrepository that uses the same secret key material as the given other/original repository.\n.sp\nThen use \\fBborg transfer \\-\\-other\\-repo ORIG_REPO \\-\\-from\\-borg1 ...\\fP to transfer the archives.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.BI \\-\\-other\\-repo \\ SRC_REPOSITORY\nreuse the key material from the other repository\n.TP\n.B  \\-\\-from\\-borg1\nother repository is Borg 1.x\n.TP\n.BI \\-e \\ MODE\\fR,\\fB \\ \\-\\-encryption \\ MODE\nselect encryption key mode \\fB(required)\\fP\n.TP\n.B  \\-\\-copy\\-crypt\\-key\ncopy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key).\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Local repository\n$ export BORG_REPO=/path/to/repo\n# Recommended repokey AEAD cryptographic modes\n$ borg repo\\-create \\-\\-encryption=repokey\\-aes\\-ocb\n$ borg repo\\-create \\-\\-encryption=repokey\\-chacha20\\-poly1305\n$ borg repo\\-create \\-\\-encryption=repokey\\-blake2\\-aes\\-ocb\n$ borg repo\\-create \\-\\-encryption=repokey\\-blake2\\-chacha20\\-poly1305\n# No encryption (not recommended)\n$ borg repo\\-create \\-\\-encryption=authenticated\n$ borg repo\\-create \\-\\-encryption=authenticated\\-blake2\n$ borg repo\\-create \\-\\-encryption=none\n\n# Remote repository (accesses a remote Borg via SSH)\n$ export BORG_REPO=ssh://user@hostname/~/backup\n# repokey: stores the encrypted key in <REPO_DIR>/config\n$ borg repo\\-create \\-\\-encryption=repokey\\-aes\\-ocb\n# keyfile: stores the encrypted key in the config dir\\(aqs keys/ subdir\n# (e.g. ~/.config/borg/keys/ on Linux, ~/Library/Application Support/borg/keys/ on macOS)\n$ borg repo\\-create \\-\\-encryption=keyfile\\-aes\\-ocb\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-repo\\-delete(1)\\fP, \\fIborg\\-repo\\-list(1)\\fP, \\fIborg\\-check(1)\\fP, \\fIborg\\-benchmark\\-cpu(1)\\fP, \\fIborg\\-key\\-import(1)\\fP, \\fIborg\\-key\\-export(1)\\fP, \\fIborg\\-key\\-change\\-passphrase(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-delete.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-delete\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-delete \\- Deletes a repository.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-delete [options]\n.SH DESCRIPTION\n.sp\nThis command deletes a complete repository.\n.sp\nWhen you delete a complete repository, the security info and local cache for it\n(if any) are also deleted. Alternatively, you can delete just the local cache\nwith the \\fB\\-\\-cache\\-only\\fP option, or keep the security info with the\n\\fB\\-\\-keep\\-security\\-info\\fP option.\n.sp\nAlways first use \\fB\\-\\-dry\\-run \\-\\-list\\fP to see what would be deleted.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change the repository\n.TP\n.B  \\-\\-list\noutput a verbose list of archives\n.TP\n.B  \\-\\-force\nforce deletion of corrupted archives; use \\fB\\-\\-force \\-\\-force\\fP if a single \\fB\\-\\-force\\fP does not work.\n.TP\n.B  \\-\\-cache\\-only\ndelete only the local cache for the given repository\n.TP\n.B  \\-\\-keep\\-security\\-info\nkeep the local security info when deleting a repository\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# delete the whole repository and the related local cache:\n$ borg repo\\-delete\nYou requested to DELETE the repository completely *including* all archives it contains:\nrepo                                 Mon, 2016\\-02\\-15 19:26:54\nroot\\-2016\\-02\\-15                      Mon, 2016\\-02\\-15 19:36:29\nnewname                              Mon, 2016\\-02\\-15 19:50:19\nType \\(aqYES\\(aq if you understand this and want to continue: YES\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-info.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-info\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-info \\- Show repository information.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-info [options]\n.SH DESCRIPTION\n.sp\nThis command displays detailed information about the repository.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-json\nformat output as JSON\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg repo\\-info\nRepository ID: 0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\nLocation: /Users/tw/w/borg/path/to/repo\nEncrypted: Yes (repokey AES\\-OCB)\nCache: /Users/tw/.cache/borg/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\nSecurity dir: /Users/tw/.config/borg/security/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\nOriginal size: 152.14 MB\nDeduplicated size: 30.38 MB\nUnique chunks: 654\nTotal chunks: 3302\n.EE\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-list.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-list\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-list \\- List the archives contained in a repository.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-list [options]\n.SH DESCRIPTION\n.sp\nThis command lists the archives contained in a repository.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-short\nonly print the archive IDs, nothing else\n.TP\n.BI \\-\\-format \\ FORMAT\nspecify format for archive listing (default: \\(dq{archive:<36} {time} [{id}]{NL}\\(dq)\n.TP\n.B  \\-\\-json\nFormat output as JSON. The form of \\fB\\-\\-format\\fP is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text.\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.B  \\-\\-deleted\nconsider only soft\\-deleted archives.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg repo\\-list\n151b1a57  Mon, 2024\\-09\\-23 22:57:11 +0200  docs             tw          MacBook\\-Pro  this is a comment\n3387a079  Thu, 2024\\-09\\-26 09:07:07 +0200  scripts          tw          MacBook\\-Pro\nca774425  Thu, 2024\\-09\\-26 10:05:23 +0200  scripts          tw          MacBook\\-Pro\nba56c4a5  Thu, 2024\\-09\\-26 10:12:45 +0200  src              tw          MacBook\\-Pro\n7567b79a  Thu, 2024\\-09\\-26 10:15:07 +0200  scripts          tw          MacBook\\-Pro\n21ab3600  Thu, 2024\\-09\\-26 10:15:17 +0200  docs             tw          MacBook\\-Pro\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.SH NOTES\n.SS The FORMAT specifier syntax\n.sp\nThe \\fB\\-\\-format\\fP option uses Python\\(aqs format string syntax \\%<https://\\:docs\\:.python\\:.org/\\:3\\:.10/\\:library/\\:string\\:.html#\\:formatstrings>\\&.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ borg repo\\-list \\-\\-format \\(aq{archive}{NL}\\(aq\nArchiveFoo\nArchiveBar\n\\&...\n\n# {VAR:NUMBER} \\- pad to NUMBER columns.\n# Strings are left\\-aligned, numbers are right\\-aligned.\n# Note: time columns except \\(ga\\(gaisomtime\\(ga\\(ga, \\(ga\\(gaisoctime\\(ga\\(ga and \\(ga\\(gaisoatime\\(ga\\(ga cannot be padded.\n$ borg repo\\-list \\-\\-format \\(aq{archive:36} {time} [{id}]{NL}\\(aq /path/to/repo\nArchiveFoo                           Thu, 2021\\-12\\-09 10:22:28 [0b8e9...3b274]\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThe following keys are always available:\n\\- NEWLINE: OS dependent line separator\n\\- NL: alias of NEWLINE\n\\- NUL: NUL character for creating print0 / xargs \\-0 like output\n\\- SPACE: space character\n\\- TAB: tab character\n\\- CR: carriage return character\n\\- LF: line feed character\n.sp\nKeys available only when listing archives in a repository:\n.INDENT 0.0\n.IP \\(bu 2\narchive: archive name\n.IP \\(bu 2\nname: alias of \\(dqarchive\\(dq\n.IP \\(bu 2\ncomment: archive comment\n.IP \\(bu 2\nid: internal ID of the archive\n.IP \\(bu 2\ntags: archive tags\n.IP \\(bu 2\ntime: nominal time of the archive\n.IP \\(bu 2\nstart: start time of the archive operation\n.IP \\(bu 2\nend: end time of the archive operation\n.IP \\(bu 2\ncommand_line: command line which was used to create the archive\n.IP \\(bu 2\nhostname: hostname of host on which this archive was created\n.IP \\(bu 2\nusername: username of user who created this archive\n.IP \\(bu 2\nsize: size of this archive (data plus metadata, not considering compression and deduplication)\n.IP \\(bu 2\nnfiles: count of files in this archive\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-repo-space.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-repo-space\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-repo-space \\- Manages reserved space in the repository.\n.SH SYNOPSIS\n.sp\nborg [common options] repo\\-space [options]\n.SH DESCRIPTION\n.sp\nThis command manages reserved space in a repository.\n.sp\nBorg cannot work in disk\\-full conditions (it cannot lock a repository and thus cannot\nrun prune/delete or compact operations to free disk space).\n.sp\nTo avoid running into such dead\\-end situations, you can put some objects into a\nrepository that take up disk space. If you ever run into a disk\\-full situation, you\ncan free that space, and then Borg will be able to run normally so you can free more\ndisk space by using \\fBborg prune\\fP/\\fBborg delete\\fP/\\fBborg compact\\fP\\&. After that, do\nnot forget to reserve space again, in case you run into that situation again later.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Create a new repository:\n$ borg repo\\-create ...\n# Reserve approx. 1 GiB of space for emergencies:\n$ borg repo\\-space \\-\\-reserve 1G\n\n# Check the amount of reserved space in the repository:\n$ borg repo\\-space\n\n# EMERGENCY! Free all reserved space to get things back to normal:\n$ borg repo\\-space \\-\\-free\n$ borg prune ...\n$ borg delete ...\n$ borg compact \\-v  # only this actually frees space of deleted archives\n$ borg repo\\-space \\-\\-reserve 1G  # reserve space again for next time\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nReserved space is always rounded up to full reservation blocks of 64 MiB.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.BI \\-\\-reserve \\ SPACE\nAmount of space to reserve (e.g. 100M, 1G). Default: 0.\n.TP\n.B  \\-\\-free\nFree all reserved space. Do not forget to reserve space again later.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-serve.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-serve\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-serve \\- Starts in server mode. This command is usually not used manually.\n.SH SYNOPSIS\n.sp\nborg [common options] serve [options]\n.SH DESCRIPTION\n.sp\nThis command starts a repository server process.\n.sp\n\\fIborg serve\\fP currently supports:\n.INDENT 0.0\n.IP \\(bu 2\nBeing automatically started via SSH when the borg client uses an \\%<ssh://>\\&...\nremote repository. In this mode, \\fIborg serve\\fP will run until that SSH connection\nis terminated.\n.IP \\(bu 2\nBeing started by some other means (not by the borg client) as a long\\-running socket\nserver to be used for borg clients using a socket://... repository (see the \\fI\\-\\-socket\\fP\noption if you do not want to use the default path for the socket and PID file).\n.UNINDENT\n.sp\nPlease note that \\fIborg serve\\fP does not support providing a specific repository via the\n\\fI\\-\\-repo\\fP option or the \\fIBORG_REPO\\fP environment variable. It is always the borg client that\nspecifies the repository to use when communicating with \\fIborg serve\\fP\\&.\n.sp\nThe \\-\\-permissions option enforces repository permissions:\n.INDENT 0.0\n.IP \\(bu 2\n\\fIall\\fP: All permissions are granted. (Default; the permissions system is not used.)\n.IP \\(bu 2\n\\fIno\\-delete\\fP: Allow reading and writing; disallow deleting and overwriting data.\nNew archives can be created; existing archives cannot be deleted. New chunks can\nbe added; existing chunks cannot be deleted or overwritten.\n.IP \\(bu 2\n\\fIwrite\\-only\\fP: Allow writing; disallow reading data.\nNew archives can be created; existing archives cannot be read.\nNew chunks can be added; existing chunks cannot be read, deleted, or overwritten.\n.IP \\(bu 2\n\\fIread\\-only\\fP: Allow reading; disallow writing or deleting data.\nExisting archives can be read, but no archives can be created or deleted.\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.BI \\-\\-restrict\\-to\\-path \\ PATH\nRestrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all subdirectories is granted implicitly; PATH does not need to point directly to a repository.\n.TP\n.BI \\-\\-restrict\\-to\\-repository \\ PATH\nRestrict repository access. Only the repository located at PATH (no subdirectories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike \\fB\\-\\-restrict\\-to\\-path\\fP, subdirectories are not accessible; PATH must point directly to a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there.\n.TP\n.B  \\-\\-permissions\nSet repository permission mode. Overrides BORG_REPO_PERMISSIONS environment variable.\n.UNINDENT\n.SH EXAMPLES\n.sp\n\\fBborg serve\\fP has special support for ssh forced commands (see \\fBauthorized_keys\\fP\nexample below): if the environment variable SSH_ORIGINAL_COMMAND is set it will\nignore some options given on the command line and use the values from the\nvariable instead. This only applies to a carefully controlled allowlist of safe\noptions. This list currently contains:\n.INDENT 0.0\n.IP \\(bu 2\nOptions that control the log level and debug topics printed\nsuch as \\fB\\-\\-verbose\\fP, \\fB\\-\\-info\\fP, \\fB\\-\\-debug\\fP, \\fB\\-\\-debug\\-topic\\fP, etc.\n.IP \\(bu 2\n\\fB\\-\\-lock\\-wait\\fP to allow the client to control how long to wait before\ngiving up and aborting the operation when another process is holding a lock.\n.UNINDENT\n.sp\nEnvironment variables (such as BORG_XXX) contained in the original\ncommand sent by the client are \\fInot\\fP interpreted, but ignored. If BORG_XXX environment\nvariables should be set on the \\fBborg serve\\fP side, then these must be set in system\\-specific\nlocations like \\fB/etc/environment\\fP or in the forced command itself (example below).\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Allow an SSH keypair to run only borg, and only have access to /path/to/repo.\n# Use key options to disable unneeded and potentially dangerous SSH functionality.\n# This will help to secure an automated remote backup system.\n$ cat ~/.ssh/authorized_keys\ncommand=\\(dqborg serve \\-\\-restrict\\-to\\-path /path/to/repo\\(dq,restrict ssh\\-rsa AAAAB3[...]\n\n# Specify repository permissions for an SSH keypair.\n$ cat ~/.ssh/authorized_keys\ncommand=\\(dqborg serve \\-\\-permissions=read\\-only\\(dq,restrict ssh\\-rsa AAAAB3[...]\n\n# Set a BORG_XXX environment variable on the \\(dqborg serve\\(dq side\n$ cat ~/.ssh/authorized_keys\ncommand=\\(dqBORG_XXX=value borg serve [...]\\(dq,restrict ssh\\-rsa [...]\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nThe examples above use the \\fBrestrict\\fP directive and assume a POSIX\ncompliant shell set as the user\\(aqs login shell.\nThis automatically blocks potentially dangerous SSH features, even when\nthey are added in a future update. Thus, this option should be preferred.\n.sp\nIf you are using OpenSSH server < 7.2, however, you must explicitly\nspecify the SSH features to restrict and cannot simply use the \\fBrestrict\\fP option, as it\nwas introduced in v7.2. We recommend using\n\\fBno\\-port\\-forwarding,no\\-X11\\-forwarding,no\\-pty,no\\-agent\\-forwarding,no\\-user\\-rc\\fP\nin this case.\n.UNINDENT\n.UNINDENT\n.sp\nDetails about sshd usage: sshd(8) \\%<https://\\:www\\:.openbsd\\:.org/\\:cgi-bin/\\:man\\:.cgi/\\:OpenBSD-current/\\:man8/\\:sshd.8>\n.SS SSH Configuration\n.sp\n\\fBborg serve\\fP\\(aqs pipes (\\fBstdin\\fP/\\fBstdout\\fP/\\fBstderr\\fP) are connected to the \\fBsshd\\fP process on the server side. In the event that the SSH connection between \\fBborg serve\\fP and the client is disconnected or stuck abnormally (for example, due to a network outage), it can take a long time for \\fBsshd\\fP to notice the client is disconnected. In the meantime, \\fBsshd\\fP continues running, and as a result so does the \\fBborg serve\\fP process holding the lock on the repository. This can cause subsequent \\fBborg\\fP operations on the remote repository to fail with the error: \\fBFailed to create/acquire the lock\\fP\\&.\n.sp\nTo avoid this, it is recommended to perform the following additional SSH configuration:\n.sp\nEither in the client\\-side \\fB~/.ssh/config\\fP file or in the client\\(aqs \\fB/etc/ssh/ssh_config\\fP file:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nHost backupserver\n        ServerAliveInterval 10\n        ServerAliveCountMax 30\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nReplacing \\fBbackupserver\\fP with the hostname, FQDN or IP address of the borg server.\n.sp\nThis will cause the client to send a keepalive to the server every 10 seconds. If 30 consecutive keepalives are sent without a response (a time of 300 seconds), the SSH client process will be terminated, causing the borg process to terminate gracefully.\n.sp\nIn the server\\-side \\fBsshd\\fP configuration file (typically \\fB/etc/ssh/sshd_config\\fP):\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nClientAliveInterval 10\nClientAliveCountMax 30\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis will cause the server to send a keepalive to the client every 10 seconds. If 30 consecutive keepalives are sent without a response (a time of 300 seconds), the server\\(aqs sshd process will be terminated, causing the \\fBborg serve\\fP process to terminate gracefully and release the lock on the repository.\n.sp\nIf you then run borg commands with \\fB\\-\\-lock\\-wait 600\\fP, this gives sufficient time for the borg serve processes to terminate after the SSH connection is torn down after the 300 second wait for the keepalives to fail.\n.sp\nYou may, of course, modify the timeout values demonstrated above to values that suit your environment and use case.\n.sp\nWhen the client is untrusted, it is a good idea to set the backup\nuser\\(aqs shell to a simple implementation (\\fB/bin/sh\\fP is only an example and may or may\nnot be such a simple implementation):\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nchsh \\-s /bin/sh BORGUSER\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nBecause the configured shell is used by OpenSSH \\%<https://\\:www\\:.openssh\\:.com/>\nto execute the command configured through the \\fBauthorized_keys\\fP file\nusing \\fB\\(dq$SHELL\\(dq \\-c \\(dq$COMMAND\\(dq\\fP,\nsetting a minimal shell implementation reduces the attack surface\ncompared to when a feature\\-rich and complex shell implementation is\nused.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-tag.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-tag\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-tag \\- Manage tags.\n.SH SYNOPSIS\n.sp\nborg [common options] tag [options] [NAME]\n.SH DESCRIPTION\n.sp\nManage archive tags.\n.sp\nBorg archives can have a set of tags which can be used for matching archives.\n.sp\nYou can set the tags to a specific set of tags or you can add or remove\ntags from the current set of tags.\n.sp\nUser\\-defined tags must not start with \\fI@\\fP because such tags are considered\nspecial and users are only allowed to use known special tags:\n.sp\n\\fB@PROT\\fP: protects archives against archive deletion or pruning.\n.sp\nPre\\-existing special tags cannot be removed via \\fB\\-\\-set\\fP\\&. You can still use\n\\fB\\-\\-set\\fP, but you must also give pre\\-existing special tags (so they won\\(aqt be\nremoved).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.BI \\-\\-set \\ TAG\nset tags\n.TP\n.BI \\-\\-add \\ TAG\nadd tags\n.TP\n.BI \\-\\-remove \\ TAG\nremove tags\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-transfer.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-transfer\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-transfer \\- archives transfer from other repository, optionally upgrade data format\n.SH SYNOPSIS\n.sp\nborg [common options] transfer [options]\n.SH DESCRIPTION\n.sp\nThis command transfers archives from one repository to another repository.\nOptionally, it can also upgrade the transferred data.\nOptionally, it can also recompress the transferred data.\nOptionally, it can also re\\-chunk the transferred data using different chunker parameters.\n.sp\nIt is easiest (and fastest) to give \\fB\\-\\-compression=COMPRESSION \\-\\-recompress=never\\fP using\nthe same COMPRESSION mode as in the SRC_REPO \\- borg will use that COMPRESSION for metadata (in\nany case) and keep data compressed \\(dqas is\\(dq (saves time as no data compression is needed).\n.sp\nIf you want to globally change compression while transferring archives to the DST_REPO,\ngive \\fB\\-\\-compress=WANTED_COMPRESSION \\-\\-recompress=always\\fP\\&.\n.sp\nThe default is to transfer all archives.\n.sp\nYou could use the misc. archive filter options to limit which archives it will\ntransfer, e.g. using the \\fB\\-a\\fP option. This is recommended for big\nrepositories with multiple data sets to keep the runtime per invocation lower.\n.SS General purpose archive transfer\n.sp\nTransfer borg2 archives into a related other borg2 repository:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# create a related DST_REPO (reusing key material from SRC_REPO), so that\n# chunking and chunk id generation will work in the same way as before.\nborg \\-\\-repo=DST_REPO repo\\-create \\-\\-encryption=DST_ENC \\-\\-other\\-repo=SRC_REPO\n\n# transfer archives from SRC_REPO to DST_REPO\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO \\-\\-dry\\-run  # check what it would do\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO            # do it!\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO \\-\\-dry\\-run  # check! anything left?\n.EE\n.UNINDENT\n.UNINDENT\n.SS Data migration / upgrade from borg 1.x\n.sp\nTo migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar\nto the above, but you need the \\fB\\-\\-from\\-borg1\\fP option:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg \\-\\-repo=DST_REPO repocreate \\-\\-encryption=DST_ENC \\-\\-other\\-repo=SRC_REPO \\-\\-from\\-borg1\n\n# to continue using lz4 compression as you did in SRC_REPO:\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO \\-\\-from\\-borg1 \\e\n     \\-\\-compress=lz4 \\-\\-recompress=never\n\n# alternatively, to recompress everything to zstd,3:\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO \\-\\-from\\-borg1 \\e\n     \\-\\-compress=zstd,3 \\-\\-recompress=always\n\n# to re\\-chunk using different chunker parameters:\nborg \\-\\-repo=DST_REPO transfer \\-\\-other\\-repo=SRC_REPO \\e\n     \\-\\-chunker\\-params=buzhash,19,23,21,4095\n.EE\n.UNINDENT\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change repository, just check\n.TP\n.BI \\-\\-other\\-repo \\ SRC_REPOSITORY\ntransfer archives from the other repository\n.TP\n.B  \\-\\-from\\-borg1\nother repository is borg 1.x\n.TP\n.BI \\-\\-upgrader \\ UPGRADER\nuse the upgrader to convert transferred data (default: no conversion)\n.TP\n.BI \\-C \\ COMPRESSION\\fR,\\fB \\ \\-\\-compression \\ COMPRESSION\nselect compression algorithm, see the output of the \\(dqborg help compression\\(dq command for details.\n.TP\n.BI \\-\\-recompress \\ MODE\nrecompress data chunks according to \\fIMODE\\fP and \\fB\\-\\-compression\\fP\\&. Possible modes are \\fIalways\\fP: recompress unconditionally; and \\fInever\\fP: do not recompress (faster: re\\-uses compressed data chunks w/o change).If no MODE is given, \\fIalways\\fP will be used. Not passing \\-\\-recompress is equivalent to \\(dq\\-\\-recompress never\\(dq.\n.TP\n.BI \\-\\-chunker\\-params \\ PARAMS\nrechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or \\fIdefault\\fP to use the chunker defaults. default: do not rechunk\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# 0. Have Borg 2.0 installed on the client AND server; have a b12 repository copy for testing.\n\n# 1. Create a new \\(dqrelated\\(dq repository:\n# Here, the existing Borg 1.2 repository used repokey\\-blake2 (and AES\\-CTR mode),\n# thus we use repokey\\-blake2\\-aes\\-ocb for the new Borg 2.0 repository.\n# Staying with the same chunk ID algorithm (BLAKE2) and with the same\n# key material (via \\-\\-other\\-repo <oldrepo>) will make deduplication work\n# between old archives (copied with borg transfer) and future ones.\n# The AEAD cipher does not matter (everything must be re\\-encrypted and\n# re\\-authenticated anyway); you could also choose repokey\\-blake2\\-chacha20\\-poly1305.\n# In case your old Borg repository did not use BLAKE2, just remove the \\(dq\\-blake2\\(dq.\n$ borg \\-\\-repo       ssh://borg2@borgbackup/./tests/b20 repo\\-create \\e\n       \\-\\-other\\-repo ssh://borg2@borgbackup/./tests/b12 \\-e repokey\\-blake2\\-aes\\-ocb\n\n# 2. Check what and how much it would transfer:\n$ borg \\-\\-repo       ssh://borg2@borgbackup/./tests/b20 transfer \\-\\-upgrader=From12To20 \\e\n       \\-\\-other\\-repo ssh://borg2@borgbackup/./tests/b12 \\-\\-dry\\-run\n\n# 3. Transfer (copy) archives from the old repository into the new repository (takes time and space!):\n$ borg \\-\\-repo       ssh://borg2@borgbackup/./tests/b20 transfer \\-\\-upgrader=From12To20 \\e\n       \\-\\-other\\-repo ssh://borg2@borgbackup/./tests/b12\n\n# 4. Check whether we have everything (same as step 2):\n$ borg \\-\\-repo       ssh://borg2@borgbackup/./tests/b20 transfer \\-\\-upgrader=From12To20 \\e\n       \\-\\-other\\-repo ssh://borg2@borgbackup/./tests/b12 \\-\\-dry\\-run\n.EE\n.UNINDENT\n.UNINDENT\n.SS Keyfile considerations when upgrading from borg 1.x\n.sp\nIf you are using a \\fBkeyfile\\fP encryption mode (not \\fBrepokey\\fP), borg 2\nmay not automatically find your borg 1.x key file, because the default\nkey file directory has changed on some platforms due to the switch to\nthe platformdirs \\%<https://\\:pypi\\:.org/\\:project/\\:platformdirs/> library.\n.sp\nOn \\fBLinux\\fP, there is typically no change \\-\\- both borg 1.x and borg 2\nuse \\fB~/.config/borg/keys/\\fP\\&.\n.sp\nOn \\fBmacOS\\fP, borg 1.x stored key files in \\fB~/.config/borg/keys/\\fP,\nbut borg 2 defaults to \\fB~/Library/Application Support/borg/keys/\\fP\\&.\n.sp\nOn \\fBWindows\\fP, borg 1.x used XDG\\-style paths (e.g. \\fB~/.config/borg/keys/\\fP),\nwhile borg 2 defaults to \\fBC:\\eUsers\\e<user>\\eAppData\\eRoaming\\eborg\\ekeys\\e\\fP\\&.\n.sp\nIf borg 2 cannot find your key file, you have several options:\n.INDENT 0.0\n.IP 1. 3\n\\fBCopy the key file\\fP from the old location to the new one.\n.IP 2. 3\n\\fBSet BORG_KEYS_DIR\\fP to point to the old key file directory:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\nexport BORG_KEYS_DIR=~/.config/borg/keys\n.EE\n.UNINDENT\n.UNINDENT\n.IP 3. 3\n\\fBSet BORG_KEY_FILE\\fP to point directly to the specific key file:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\nexport BORG_KEY_FILE=~/.config/borg/keys/your_key_file\n.EE\n.UNINDENT\n.UNINDENT\n.IP 4. 3\n\\fBSet BORG_BASE_DIR\\fP to force borg 2 to use the same base directory\nas borg 1.x:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\nexport BORG_BASE_DIR=$HOME\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis makes borg 2 use \\fB$HOME/.config/borg\\fP, \\fB$HOME/.cache/borg\\fP,\netc., matching borg 1.x behaviour on all platforms.\n.UNINDENT\n.sp\nSee \\fIenv_vars\\fP for more details on directory environment variables.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-umount.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-umount\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-umount \\- Unmounts the FUSE filesystem.\n.SH SYNOPSIS\n.sp\nborg [common options] umount [options] MOUNTPOINT\n.SH DESCRIPTION\n.sp\nThis command unmounts a FUSE filesystem that was mounted with \\fBborg mount\\fP\\&.\n.sp\nThis is a convenience wrapper that just calls the platform\\-specific shell\ncommand \\- usually this is either umount or fusermount \\-u.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B MOUNTPOINT\nmountpoint of the filesystem to unmount\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# Mounting the repository shows all archives.\n# Archives are loaded lazily, expect some delay when navigating to an archive\n# for the first time.\n$ borg mount /tmp/mymountpoint\n$ ls /tmp/mymountpoint\nroot\\-2016\\-02\\-14 root\\-2016\\-02\\-15\n$ borg umount /tmp/mymountpoint\n\n# The \\(dqversions view\\(dq merges all archives in the repository\n# and provides a versioned view on files.\n$ borg mount \\-o versions /tmp/mymountpoint\n$ ls \\-l /tmp/mymountpoint/home/user/doc.txt/\ntotal 24\n\\-rw\\-rw\\-r\\-\\- 1 user group 12357 Aug 26 21:19 doc.cda00bc9.txt\n\\-rw\\-rw\\-r\\-\\- 1 user group 12204 Aug 26 21:04 doc.fa760f28.txt\n$ borg umount /tmp/mymountpoint\n\n# Archive filters are supported.\n# These are especially handy for the \\(dqversions view\\(dq,\n# which does not support lazy processing of archives.\n$ borg mount \\-o versions \\-\\-match\\-archives \\(aqsh:*\\-my\\-home\\(aq \\-\\-last 10 /tmp/mymountpoint\n\n# Exclusion options are supported.\n# These can speed up mounting and lower memory needs significantly.\n$ borg mount /path/to/repo /tmp/mymountpoint only/that/path\n$ borg mount \\-\\-exclude \\(aq...\\(aq /tmp/mymountpoint\n.EE\n.UNINDENT\n.UNINDENT\n.SS borgfs\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ echo \\(aq/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0\\(aq >> /etc/fstab\n$ mount /tmp/myrepo\n$ ls /tmp/myrepo\nroot\\-2016\\-02\\-01 root\\-2016\\-02\\-15\n.EE\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\n\\fBborgfs\\fP will be automatically provided if you used a distribution\npackage or \\fBpip\\fP to install Borg. Users of the standalone binary will have\nto manually create a symlink (see \\fIpyinstaller\\-binary\\fP).\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP, \\fIborg\\-mount(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-undelete.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-undelete\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-undelete \\- Undeletes archives.\n.SH SYNOPSIS\n.sp\nborg [common options] undelete [options] [NAME]\n.SH DESCRIPTION\n.sp\nThis command undeletes archives in the repository.\n.sp\nImportant: Undeleting archives is only possible before compacting.\nOnce \\fBborg compact\\fP has run, all disk space occupied only by the\nsoft\\-deleted archives will be freed, and undeleting is no longer\npossible.\n.sp\nWhen in doubt, use \\fB\\-\\-dry\\-run \\-\\-list\\fP to see what would be\nundeleted.\n.sp\nYou can undelete multiple archives by specifying a match pattern using\nthe \\fB\\-\\-match\\-archives PATTERN\\fP option (for more information on these\npatterns, see \\fIborg_patterns\\fP).\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B NAME\nspecify the archive name\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change the repository\n.TP\n.B  \\-\\-list\noutput a verbose list of archives\n.UNINDENT\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-upgrade.1",
    "content": ".\\\" Man page generated from reStructuredText.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"BORG-UPGRADE\" 1 \"2022-06-25\" \"\" \"borg backup tool\"\n.SH NAME\nborg-upgrade \\- upgrade a repository from a previous version\n.SH SYNOPSIS\n.sp\nborg [common options] upgrade [options]\n.SH DESCRIPTION\n.sp\nUpgrade an existing, local Borg repository.\n.SS When you do not need borg upgrade\n.sp\nNot every change requires that you run \\fBborg upgrade\\fP\\&.\n.sp\nYou do \\fBnot\\fP need to run it when:\n.INDENT 0.0\n.IP \\(bu 2\nmoving your repository to a different place\n.IP \\(bu 2\nupgrading to another point release (like 1.0.x to 1.0.y),\nexcept when noted otherwise in the changelog\n.IP \\(bu 2\nupgrading from 1.0.x to 1.1.x,\nexcept when noted otherwise in the changelog\n.UNINDENT\n.SS Borg 1.x.y upgrades\n.sp\nUse \\fBborg upgrade \\-\\-tam REPO\\fP to require manifest authentication\nintroduced with Borg 1.0.9 to address security issues. This means\nthat modifying the repository after doing this with a version prior\nto 1.0.9 will raise a validation error, so perform this upgrade\nonly after updating all clients using the repository to 1.0.9 or newer.\n.sp\nThis upgrade should be done on each client for safety reasons.\n.sp\nIf a repository is accidentally modified with a pre\\-1.0.9 client after\nthis upgrade, use \\fBborg upgrade \\-\\-tam \\-\\-force REPO\\fP to remedy it.\n.sp\nIf you routinely do this you might not want to enable this upgrade\n(which will leave you exposed to the security issue). You can\nreverse the upgrade by issuing \\fBborg upgrade \\-\\-disable\\-tam REPO\\fP\\&.\n.sp\nSee\n\\fI\\%https://borgbackup.readthedocs.io/en/stable/changes.html#pre\\-1\\-0\\-9\\-manifest\\-spoofing\\-vulnerability\\fP\nfor details.\n.SS Borg 0.xx to Borg 1.x\n.sp\nThis currently supports converting Borg 0.xx to 1.0.\n.sp\nCurrently, only LOCAL repositories can be upgraded (issue #465).\n.sp\nPlease note that \\fBborg create\\fP (since 1.0.0) uses bigger chunks by\ndefault than old borg did, so the new chunks won\\(aqt deduplicate\nwith the old chunks in the upgraded repository.\nSee \\fB\\-\\-chunker\\-params\\fP option of \\fBborg create\\fP and \\fBborg recreate\\fP\\&.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS optional arguments\n.INDENT 0.0\n.TP\n.B  \\-n\\fP,\\fB  \\-\\-dry\\-run\ndo not change repository\n.TP\n.B  \\-\\-inplace\nrewrite repository in place, with no chance of going back to older versions of the repository.\n.TP\n.B  \\-\\-force\nForce upgrade\n.TP\n.B  \\-\\-tam\nEnable manifest authentication (in key and cache) (Borg 1.0.9 and later).\n.TP\n.B  \\-\\-disable\\-tam\nDisable manifest authentication (in key and cache).\n.UNINDENT\n.SH EXAMPLES\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.nf\n.ft C\n# Upgrade the borg repository to the most recent version.\n$ borg upgrade \\-v /path/to/repo\nmaking a hardlink copy in /path/to/repo.before\\-upgrade\\-2016\\-02\\-15\\-20:51:55\nopening attic repository with borg and converting\nno key file found for repository\nconverting repo index /path/to/repo/index.0\nconverting 1 segments...\nconverting borg 0.xx to borg current\nno key file found for repository\n.ft P\n.fi\n.UNINDENT\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH AUTHOR\nThe Borg Collective\n.\\\" Generated by docutils manpage writer.\n.\n"
  },
  {
    "path": "docs/man/borg-version.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-version\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-version \\- Displays the Borg client and server versions.\n.SH SYNOPSIS\n.sp\nborg [common options] version [options]\n.SH DESCRIPTION\n.sp\nThis command displays the Borg client and server versions.\n.sp\nIf a local repository is given, the client code directly accesses the repository,\nso the client version is also shown as the server version.\n.sp\nIf a remote repository is given (e.g., ssh:), the remote Borg is queried, and\nits version is displayed as the server version.\n.sp\nExamples:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n# local repository (client uses 1.4.0 alpha version)\n$ borg version /mnt/backup\n1.4.0a / 1.4.0a\n\n# remote repository (client uses 1.4.0 alpha, server uses 1.2.7 release)\n$ borg version ssh://borg@borgbackup:repo\n1.4.0a / 1.2.7\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nDue to the version tuple format used in Borg client/server negotiation, only\na simplified version is displayed (as provided by borg.version.format_version).\n.sp\nYou can also use \\fBborg \\-\\-version\\fP to display a potentially more precise client version.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg-with-lock.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg-with-lock\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg-with-lock \\- Runs a user-specified command with the repository lock held.\n.SH SYNOPSIS\n.sp\nborg [common options] with\\-lock [options] COMMAND [ARGS...]\n.SH DESCRIPTION\n.sp\nThis command runs a user\\-specified command while locking the repository. For example:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\n$ BORG_REPO=/mnt/borgrepo borg with\\-lock rsync \\-av /mnt/borgrepo /somewhere/else/borgrepo\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nIt first tries to acquire the lock (make sure that no other operation is\nrunning in the repository), then executes the given command as a subprocess and waits\nfor its termination, releases the lock, and returns the user command\\(aqs return\ncode as Borg\\(aqs return code.\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nIf you copy a repository with the lock held, the lock will be present in\nthe copy. Before using Borg on the copy from a different host,\nyou need to run \\fBborg break\\-lock\\fP on the copied repository, because\nBorg is cautious and does not automatically remove stale locks made by a different host.\n.UNINDENT\n.UNINDENT\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B COMMAND\ncommand to run\n.TP\n.B ARGS\ncommand arguments\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borg.1",
    "content": "'\\\" t\n.\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borg\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborg \\- deduplicating and encrypting backup tool\n.SH SYNOPSIS\n.sp\nborg [common options] <command> [options] [arguments]\n.SH DESCRIPTION\n.\\\" we don't include the README.rst here since we want to keep this terse.\n.\n.sp\nBorgBackup (short: Borg) is a deduplicating backup program.\nOptionally, it supports compression and authenticated encryption.\n.sp\nThe main goal of Borg is to provide an efficient and secure way to back up data.\nThe data deduplication technique used makes Borg suitable for daily backups\nsince only changes are stored.\nThe authenticated encryption technique makes it suitable for backups to targets not\nfully trusted.\n.sp\nBorg stores a set of files in an \\fIarchive\\fP\\&. A \\fIrepository\\fP is a collection\nof \\fIarchives\\fP\\&. The format of repositories is Borg\\-specific. Borg does not\ndistinguish archives from each other in any way other than their name,\nit does not matter when or where archives were created (e.g., different hosts).\n.SH EXAMPLES\n.SS A step\\-by\\-step example\n.INDENT 0.0\n.IP 1. 3\nBefore a backup can be made, a repository has to be initialized:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo repo\\-create \\-\\-encryption=repokey\\-aes\\-ocb\n.EE\n.UNINDENT\n.UNINDENT\n.IP 2. 3\nBack up the \\fB~/src\\fP and \\fB~/Documents\\fP directories into an archive called\n\\fIdocs\\fP:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo create docs ~/src ~/Documents\n.EE\n.UNINDENT\n.UNINDENT\n.IP 3. 3\nThe next day, create a new archive using the same archive name:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo create \\-\\-stats docs ~/src ~/Documents\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis backup will be much quicker and much smaller, since only new,\nnever\\-before\\-seen data is stored. The \\fB\\-\\-stats\\fP option causes Borg to\noutput statistics about the newly created archive such as the deduplicated\nsize (the amount of unique data not shared with other archives):\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\nRepository: /path/to/repo\nArchive name: docs\nArchive fingerprint: bcd1b53f9b4991b7afc2b339f851b7ffe3c6d030688936fe4552eccc1877718d\nTime (start): Sat, 2022\\-06\\-25 20:21:43\nTime (end):   Sat, 2022\\-06\\-25 20:21:43\nDuration: 0.07 seconds\nUtilization of maximum archive size: 0%\nNumber of files: 699\nOriginal size: 31.14 MB\nDeduplicated size: 502 B\n.EE\n.UNINDENT\n.UNINDENT\n.IP 4. 3\nList all archives in the repository:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo repo\\-list\ndocs                                 Sat, 2022\\-06\\-25 20:21:14 [b80e24d2...b179f298]\ndocs                                 Sat, 2022\\-06\\-25 20:21:43 [bcd1b53f...1877718d]\n.EE\n.UNINDENT\n.UNINDENT\n.IP 5. 3\nList the contents of the first archive:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo list aid:b80e24d2\ndrwxr\\-xr\\-x user   group          0 Mon, 2016\\-02\\-15 18:22:30 home/user/Documents\n\\-rw\\-r\\-\\-r\\-\\- user   group       7961 Mon, 2016\\-02\\-15 18:22:30 home/user/Documents/Important.doc\n\\&...\n.EE\n.UNINDENT\n.UNINDENT\n.IP 6. 3\nRestore the first archive by extracting the files relative to the current directory:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo extract aid:b80e24d2\n.EE\n.UNINDENT\n.UNINDENT\n.IP 7. 3\nDelete the first archive (please note that this does \\fBnot\\fP free repository disk space):\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo delete aid:b80e24d2\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nBe careful if you use an archive NAME (and not an archive ID), as it might match multiple archives.\nAlways use \\fB\\-\\-dry\\-run\\fP and \\fB\\-\\-list\\fP first!\n.IP 8. 3\nRecover disk space by compacting the segment files in the repository:\n.INDENT 3.0\n.INDENT 3.5\n.sp\n.EX\n$ borg \\-r /path/to/repo compact \\-v\n.EE\n.UNINDENT\n.UNINDENT\n.UNINDENT\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nBorg is quiet by default (it defaults to WARNING log level).\nYou can use options like \\fB\\-\\-progress\\fP or \\fB\\-\\-list\\fP to get specific\nreports during command execution.  You can also add the \\fB\\-v\\fP (or\n\\fB\\-\\-verbose\\fP or \\fB\\-\\-info\\fP) option to adjust the log level to INFO to\nget other informational messages.\n.UNINDENT\n.UNINDENT\n.SH NOTES\n.SS Positional Arguments and Options: Order matters\n.sp\nBorg only supports taking options (\\fB\\-s\\fP and \\fB\\-\\-progress\\fP in the example)\neither to the left or to the right of all positional arguments (\\fBrepo::archive\\fP and \\fBpath\\fP\nin the example), but not in between them:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-s \\-\\-progress archive path  # good and preferred\nborg create archive path \\-s \\-\\-progress  # also works\nborg create \\-s archive path \\-\\-progress  # works, but ugly\nborg create archive \\-s \\-\\-progress path  # BAD\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThis is due to a problem in the argparse module: \\%<https://\\:bugs\\:.python\\:.org/\\:issue15112>\n.SS Repository URLs\n.sp\n\\fBLocal filesystem\\fP (or locally mounted network filesystem):\n.sp\n\\fB/path/to/repo\\fP — filesystem path to the repository directory (absolute path)\n.sp\n\\fBpath/to/repo\\fP — filesystem path to the repository directory (relative path)\n.sp\nAlso, paths like \\fB~/path/to/repo\\fP or \\fB~other/path/to/repo\\fP work (this is\nexpanded by your shell).\n.sp\nNote: You may also prepend \\fBfile://\\fP to a filesystem path to use URL style.\n.sp\n\\fBRemote repositories\\fP accessed via SSH \\%<user@\\:host>:\n.sp\n\\fBssh://user@host:port//abs/path/to/repo\\fP — absolute path\n.sp\n\\fBssh://user@host:port/rel/path/to/repo\\fP — path relative to the current directory\n.sp\n\\fBRemote repositories\\fP accessed via SFTP:\n.sp\n\\fBsftp://user@host:port//abs/path/to/repo\\fP — absolute path\n.sp\n\\fBsftp://user@host:port/rel/path/to/repo\\fP — path relative to the current directory\n.sp\nFor SSH and SFTP URLs, the \\fBuser@\\fP and \\fB:port\\fP parts are optional.\n.sp\n\\fBRemote repositories\\fP accessed via rclone:\n.sp\n\\fBrclone:remote:path\\fP — see the rclone docs for more details about \\fBremote:path\\fP\\&.\n.sp\n\\fBRemote repositories\\fP accessed via S3:\n.sp\n\\fB(s3|b2):[(profile|(access_key_id:access_key_secret))@][scheme://hostname[:port]]/bucket/path\\fP — see the boto3 docs for more details about credentials.\n.sp\nIf you are connecting to AWS S3, \\fB[schema://hostname[:port]]\\fP is optional, but \\fBbucket\\fP and \\fBpath\\fP are always required.\n\\fIscheme\\fP is usually \\fIhttps\\fP here, hostname and optional port refer to your S3/B2 server, if that is not Amazon\\(aqs.\n.sp\nNote: There is a known issue with some S3\\-compatible services, e.g., Backblaze B2. If you encounter problems, try using \\fBb2:\\fP instead of \\fBs3:\\fP in the URL.\n.sp\nIf you frequently need the same repository URL, it is a good idea to set the\n\\fBBORG_REPO\\fP environment variable to set a default repository URL:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nexport BORG_REPO=\\(aqssh://user@host:port/rel/path/to/repo\\(aq\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nThen simply omit the \\fB\\-\\-repo\\fP option when you want\nto use the default — it will be read from BORG_REPO.\n.SS Repository Locations / Archive Names\n.sp\nMany commands need to know the repository location; specify it via \\fB\\-r\\fP/\\fB\\-\\-repo\\fP\nor use the \\fBBORG_REPO\\fP environment variable.\n.sp\nCommands that need one or two archive names usually take them as positional arguments.\n.sp\nCommands that work with an arbitrary number of archives usually accept \\fB\\-a ARCH_GLOB\\fP\\&.\n.sp\nArchive names must not contain the \\fB/\\fP (slash) character. For simplicity,\nalso avoid spaces or other characters that have special meaning to the\nshell or in a filesystem (\\fBborg mount\\fP uses the archive name as a directory\nname).\n.SS Logging\n.sp\nBorg writes all log output to stderr by default. However, output on stderr does\nnot necessarily indicate an error. Check the log levels of the messages and the\nreturn code of borg to determine error, warning, or success conditions.\n.sp\nIf you want to capture the log output to a file, just redirect it:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nborg create \\-\\-repo repo archive myfiles 2>> logfile\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nCustom logging configurations can be implemented via BORG_LOGGING_CONF.\n.sp\nThe log level of the built\\-in logging configuration defaults to WARNING.\nThis is because we want Borg to be mostly silent and only output\nwarnings, errors, and critical messages unless output has been requested\nby supplying an option that implies output (e.g., \\fB\\-\\-list\\fP or \\fB\\-\\-progress\\fP).\n.sp\nLog levels: DEBUG < INFO < WARNING < ERROR < CRITICAL\n.sp\nUse \\fB\\-\\-debug\\fP to set the DEBUG log level —\nthis prints debug, info, warning, error, and critical messages.\n.sp\nUse \\fB\\-\\-info\\fP (or \\fB\\-v\\fP or \\fB\\-\\-verbose\\fP) to set the INFO log level —\nthis prints info, warning, error, and critical messages.\n.sp\nUse \\fB\\-\\-warning\\fP (default) to set the WARNING log level —\nthis prints warning, error, and critical messages.\n.sp\nUse \\fB\\-\\-error\\fP to set the ERROR log level —\nthis prints error and critical messages.\n.sp\nUse \\fB\\-\\-critical\\fP to set the CRITICAL log level —\nthis prints only critical messages.\n.sp\nWhile you can set miscellaneous log levels, do not expect every command to\nproduce different output at different log levels — it\\(aqs merely a possibility.\n.sp\n\\fBWarning:\\fP\n.INDENT 0.0\n.INDENT 3.5\nOptions \\fB\\-\\-critical\\fP and \\fB\\-\\-error\\fP are provided for completeness,\ntheir usage is not recommended as you might miss important information.\n.UNINDENT\n.UNINDENT\n.SS Return codes\n.sp\nBorg can exit with the following return codes (rc):\n.TS\nbox center;\nl|l.\nT{\nReturn code\nT}\tT{\nMeaning\nT}\n_\nT{\n0\nT}\tT{\nsuccess (logged as INFO)\nT}\n_\nT{\n1\nT}\tT{\ngeneric warning (operation reached its normal end, but there were warnings —\nyou should check the log; logged as WARNING)\nT}\n_\nT{\n2\nT}\tT{\ngeneric error (like a fatal error or a local/remote exception; the operation\ndid not reach its normal end; logged as ERROR)\nT}\n_\nT{\n3..99\nT}\tT{\nspecific error (enabled by BORG_EXIT_CODES=modern)\nT}\n_\nT{\n100..127\nT}\tT{\nspecific warning (enabled by BORG_EXIT_CODES=modern)\nT}\n_\nT{\n128+N\nT}\tT{\nkilled by signal N (e.g. 137 == kill \\-9)\nT}\n.TE\n.sp\nIf you use \\fB\\-\\-show\\-rc\\fP, the return code is also logged at the indicated\nlevel as the last log entry.\n.sp\nThe modern exit codes (return codes, \\(dqrc\\(dq) are documented here: see \\fImsgid\\fP\\&.\n.SS Environment Variables\n.sp\nBorg uses some environment variables for automation:\n.INDENT 0.0\n.TP\n.B General:\n.INDENT 7.0\n.TP\n.B BORG_REPO\nWhen set, use the value to give the default repository location.\nUse this so you do not need to type \\fB\\-\\-repo /path/to/my/repo\\fP all the time.\n.TP\n.B BORG_OTHER_REPO\nSimilar to BORG_REPO, but gives the default for \\fB\\-\\-other\\-repo\\fP\\&.\n.TP\n.B BORG_PASSPHRASE (and BORG_OTHER_PASSPHRASE)\nWhen set, use the value to answer the passphrase question for encrypted repositories.\nIt is used when a passphrase is needed to access an encrypted repo as well as when a new\npassphrase should be initially set when initializing an encrypted repo.\nSee also BORG_NEW_PASSPHRASE.\n.TP\n.B BORG_PASSCOMMAND (and BORG_OTHER_PASSCOMMAND)\nWhen set, use the standard output of the command (trailing newlines are stripped) to answer the\npassphrase question for encrypted repositories.\nIt is used when a passphrase is needed to access an encrypted repo as well as when a new\npassphrase should be initially set when initializing an encrypted repo. Note that the command\nis executed without a shell. So variables, like \\fB$HOME\\fP will work, but \\fB~\\fP won\\(aqt.\nIf BORG_PASSPHRASE is also set, it takes precedence.\nSee also BORG_NEW_PASSPHRASE.\n.TP\n.B BORG_PASSPHRASE_FD (and BORG_OTHER_PASSPHRASE_FD)\nWhen set, specifies a file descriptor to read a passphrase\nfrom. Programs starting borg may choose to open an anonymous pipe\nand use it to pass a passphrase. This is safer than passing via\nBORG_PASSPHRASE, because on some systems (e.g. Linux) environment\ncan be examined by other processes.\nIf BORG_PASSPHRASE or BORG_PASSCOMMAND are also set, they take precedence.\n.TP\n.B BORG_NEW_PASSPHRASE\nWhen set, use the value to answer the passphrase question when a \\fBnew\\fP passphrase is asked for.\nThis variable is checked first. If it is not set, BORG_PASSPHRASE and BORG_PASSCOMMAND will also\nbe checked.\nMain use case for this is to fully automate \\fBborg change\\-passphrase\\fP\\&.\n.TP\n.B BORG_DISPLAY_PASSPHRASE\nWhen set, use the value to answer the \\(dqdisplay the passphrase for verification\\(dq question when defining a new passphrase for encrypted repositories.\n.TP\n.B BORG_DEBUG_PASSPHRASE\nWhen set to YES, display debugging information that includes passphrases used and passphrase related env vars set.\n.TP\n.B BORG_EXIT_CODES\nWhen set to \\(dqmodern\\(dq, the borg process will return more specific exit codes (rc).\nWhen set to \\(dqlegacy\\(dq, the borg process will return rc 2 for all errors, 1 for all warnings, 0 for success.\nDefault is \\(dqmodern\\(dq.\n.TP\n.B BORG_HOST_ID\nBorg usually computes a host id from the FQDN plus the results of \\fBuuid.getnode()\\fP (which usually returns\na unique id based on the MAC address of the network interface. Except if that MAC happens to be all\\-zero \\- in\nthat case it returns a random value, which is not what we want (because it kills automatic stale lock removal).\nSo, if you have an all\\-zero MAC address or other reasons to better control the host id externally, just set this\nenvironment variable to a unique value. If all your FQDNs are unique, you can just use the FQDN. If not,\nuse \\%<FQDN@\\:uniqueid>\\&.\n.TP\n.B BORG_LOCK_WAIT\nYou can set the default value for the \\fB\\-\\-lock\\-wait\\fP option with this, so\nyou do not need to give it as a command line option.\n.TP\n.B BORG_LOGGING_CONF\nWhen set, use the given filename as INI \\%<https://\\:docs\\:.python\\:.org/\\:3/\\:library/\\:logging\\:.config\\:.html#\\:configuration-file-format>\\-style logging configuration.\nA basic example conf can be found at \\fBdocs/misc/logging.conf\\fP\\&.\n.TP\n.B BORG_RSH\nWhen set, use this command instead of \\fBssh\\fP\\&. This can be used to specify ssh options, such as\na custom identity file \\fBssh \\-i /path/to/private/key\\fP\\&. See \\fBman ssh\\fP for other options. Using\nthe \\fB\\-\\-rsh CMD\\fP command line option overrides the environment variable.\n.TP\n.B BORG_REMOTE_PATH\nWhen set, use the given path as borg executable on the remote (defaults to \\(dqborg\\(dq if unset).\nUsing the \\fB\\-\\-remote\\-path PATH\\fP command line option overrides the environment variable.\n.TP\n.B BORG_REPO_PERMISSIONS\nSet repository permissions, see also: \\fIborg_serve\\fP\n.TP\n.B BORG_FILES_CACHE_SUFFIX\nWhen set to a value at least one character long, instructs borg to use a specifically named\n(based on the suffix) alternative files cache. This can be used to avoid loading and saving\ncache entries for backup sources other than the current sources.\n.TP\n.B BORG_FILES_CACHE_TTL\nWhen set to a numeric value, this determines the maximum \\(dqtime to live\\(dq for the files cache\nentries (default: 2). The files cache is used to determine quickly whether a file is unchanged.\n.TP\n.B BORG_USE_CHUNKS_ARCHIVE\nWhen set to no (default: yes), the \\fBchunks.archive.d\\fP folder will not be used. This reduces\ndisk space usage but slows down cache resyncs.\n.TP\n.B BORG_SHOW_SYSINFO\nWhen set to no (default: yes), system information (like OS, Python version, ...) in\nexceptions is not shown.\nPlease only use for good reasons as it makes issues harder to analyze.\n.TP\n.B BORG_MSGPACK_VERSION_CHECK\nControls whether Borg checks the \\fBmsgpack\\fP version.\nThe default is \\fByes\\fP (strict check). Set to \\fBno\\fP to disable the version check and\nallow any installed \\fBmsgpack\\fP version. Use this at your own risk; malfunctioning or\nincompatible \\fBmsgpack\\fP versions may cause subtle bugs or repository data corruption.\n.TP\n.B BORG_FUSE_IMPL\nChoose the low\\-level FUSE implementation borg shall use for \\fBborg mount\\fP\\&.\nThis is a comma\\-separated list of implementation names, they are tried in the\ngiven order, e.g.:\n.INDENT 7.0\n.IP \\(bu 2\n\\fBmfusepy,pyfuse3,llfuse\\fP: default, first try to load mfusepy, then pyfuse3, then llfuse.\n.IP \\(bu 2\n\\fBllfuse,pyfuse3\\fP: first try to load llfuse, then try to load pyfuse3.\n.IP \\(bu 2\n\\fBmfusepy\\fP: only try to load mfusepy\n.IP \\(bu 2\n\\fBpyfuse3\\fP: only try to load pyfuse3\n.IP \\(bu 2\n\\fBllfuse\\fP: only try to load llfuse\n.IP \\(bu 2\n\\fBnone\\fP: do not try to load an implementation\n.UNINDENT\n.TP\n.B BORG_SELFTEST\nThis can be used to influence borg\\(aqs built\\-in self\\-tests. The default is to execute the tests\nat the beginning of each borg command invocation.\n.sp\nBORG_SELFTEST=disabled can be used to switch off the tests and rather save some time.\nDisabling is not recommended for normal borg users, but large scale borg storage providers can\nuse this to optimize production servers after at least doing a one\\-time test borg (with\nself\\-tests not disabled) when installing or upgrading machines/OS/Borg.\n.TP\n.B BORG_WORKAROUNDS\nA list of comma\\-separated strings that trigger workarounds in borg,\ne.g. to work around bugs in other software.\n.sp\nCurrently known strings are:\n.INDENT 7.0\n.TP\n.B basesyncfile\nUse the more simple BaseSyncFile code to avoid issues with sync_file_range.\nYou might need this to run borg on WSL (Windows Subsystem for Linux) or\nin systemd.nspawn containers on some architectures (e.g. ARM).\nUsing this does not affect data safety, but might result in a more bursty\nwrite\\-to\\-disk behavior (not continuously streaming to disk).\n.TP\n.B retry_erofs\nRetry opening a file without O_NOATIME if opening a file with O_NOATIME\ncaused EROFS. You will need this to make archives from volume shadow copies\nin WSL1 (Windows Subsystem for Linux 1).\n.TP\n.B authenticated_no_key\nWork around a lost passphrase or key for an \\fBauthenticated\\fP mode repository\n(these are only authenticated, but not encrypted).\nIf the key is missing in the repository config, add \\fBkey = anything\\fP there.\n.sp\nThis workaround is \\fBonly\\fP for emergencies and \\fBonly\\fP to extract data\nfrom an affected repository (read\\-only access):\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\nBORG_WORKAROUNDS=authenticated_no_key borg extract \\-\\-repo repo archive\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nAfter you have extracted all data you need, you MUST delete the repository:\n.INDENT 7.0\n.INDENT 3.5\n.sp\n.EX\nBORG_WORKAROUNDS=authenticated_no_key borg delete repo\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nNow you can init a fresh repo. Make sure you do not use the workaround any more.\n.UNINDENT\n.UNINDENT\n.TP\n.B Output formatting:\n.INDENT 7.0\n.TP\n.B BORG_LIST_FORMAT\nGiving the default value for \\fBborg list \\-\\-format=X\\fP\\&.\n.TP\n.B BORG_REPO_LIST_FORMAT\nGiving the default value for \\fBborg repo\\-list \\-\\-format=X\\fP\\&.\n.TP\n.B BORG_PRUNE_FORMAT\nGiving the default value for \\fBborg prune \\-\\-format=X\\fP\\&.\n.UNINDENT\n.TP\n.B Some automatic \\(dqanswerers\\(dq (if set, they automatically answer confirmation questions):\n.INDENT 7.0\n.TP\n.B BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no (or =yes)\nFor \\(dqWarning: Attempting to access a previously unknown unencrypted repository\\(dq\n.TP\n.B BORG_RELOCATED_REPO_ACCESS_IS_OK=no (or =yes)\nFor \\(dqWarning: The repository at location ... was previously located at ...\\(dq\n.TP\n.B BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO (or =YES)\nFor \\(dqThis is a potentially dangerous function...\\(dq (check \\-\\-repair)\n.TP\n.B BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES)\nFor \\(dqYou requested to DELETE the repository completely \\fIincluding\\fP all archives it contains:\\(dq\n.UNINDENT\n.sp\nNote: answers are case sensitive. setting an invalid answer value might either give the default\nanswer or ask you interactively, depending on whether retries are allowed (they by default are\nallowed). So please test your scripts interactively before making them a non\\-interactive script.\n.UNINDENT\n.sp\nDirectories and files:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n\\fBNote:\\fP\n.INDENT 0.0\n.INDENT 3.5\nBorg 2 uses the platformdirs \\%<https://\\:pypi\\:.org/\\:project/\\:platformdirs/> library to determine\ndefault directory locations. This means that default paths are \\fBplatform\\-specific\\fP:\n.INDENT 0.0\n.IP \\(bu 2\n\\fBLinux\\fP: Uses XDG Base Directory Specification paths (e.g., \\fB~/.config/borg\\fP,\n\\fB~/.cache/borg\\fP, \\fB~/.local/share/borg\\fP). XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> variables are honoured.\n.IP \\(bu 2\n\\fBmacOS\\fP: Uses native macOS directories (e.g., \\fB~/Library/Application Support/borg\\fP,\n\\fB~/Library/Caches/borg\\fP). XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> variables are \\fBnot\\fP honoured.\n.IP \\(bu 2\n\\fBWindows\\fP: Uses Windows AppData directories (e.g., \\fBC:\\eUsers\\e<user>\\eAppData\\eRoaming\\eborg\\fP,\n\\fBC:\\eUsers\\e<user>\\eAppData\\eLocal\\eborg\\fP). XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> variables are \\fBnot\\fP honoured.\n.UNINDENT\n.sp\nOn all platforms, you can override each directory individually using the specific environment\nvariables described below. You can also set \\fBBORG_BASE_DIR\\fP to force borg to use\n\\fBBORG_BASE_DIR/.config/borg\\fP, \\fBBORG_BASE_DIR/.cache/borg\\fP, etc., regardless of the platform.\n.UNINDENT\n.UNINDENT\n.sp\nDefault directory locations by platform (when no \\fBBORG_*\\fP environment variables are set):\n.TS\nbox center;\nl|l|l|l.\nT{\nDirectory\nT}\tT{\nLinux\nT}\tT{\nmacOS\nT}\tT{\nWindows\nT}\n_\nT{\nConfig\nT}\tT{\n\\fB~/.config/borg\\fP\nT}\tT{\n\\fB~/Library/Application Support/borg\\fP\nT}\tT{\n\\fB%APPDATA%\\eborg\\fP\nT}\n_\nT{\nCache\nT}\tT{\n\\fB~/.cache/borg\\fP\nT}\tT{\n\\fB~/Library/Caches/borg\\fP\nT}\tT{\n\\fB%LOCALAPPDATA%\\eborg\\eCache\\fP\nT}\n_\nT{\nData\nT}\tT{\n\\fB~/.local/share/borg\\fP\nT}\tT{\n\\fB~/Library/Application Support/borg\\fP\nT}\tT{\n\\fB%LOCALAPPDATA%\\eborg\\fP\nT}\n_\nT{\nRuntime\nT}\tT{\n\\fB/run/user/<uid>/borg\\fP\nT}\tT{\n\\fB~/Library/Caches/TemporaryItems/borg\\fP\nT}\tT{\n\\fB%TEMP%\\eborg\\fP\nT}\n_\nT{\nKeys\nT}\tT{\n\\fB<config_dir>/keys\\fP\nT}\tT{\n\\fB<config_dir>/keys\\fP\nT}\tT{\n\\fB<config_dir>\\ekeys\\fP\nT}\n_\nT{\nSecurity\nT}\tT{\n\\fB<data_dir>/security\\fP\nT}\tT{\n\\fB<data_dir>/security\\fP\nT}\tT{\n\\fB<data_dir>\\esecurity\\fP\nT}\n.TE\n.INDENT 0.0\n.TP\n.B BORG_BASE_DIR\nDefaults to \\fB$HOME\\fP or \\fB~$USER\\fP or \\fB~\\fP (in that order).\nIf you want to move all borg\\-specific folders to a custom path at once, all you need to do is\nto modify \\fBBORG_BASE_DIR\\fP: the other paths for cache, config etc. will adapt accordingly\n(assuming you didn\\(aqt set them to a different custom value).\n.TP\n.B BORG_CACHE_DIR\nDefaults to the platform\\-specific cache directory (see table above).\nIf \\fBBORG_BASE_DIR\\fP is set, defaults to \\fB$BORG_BASE_DIR/.cache/borg\\fP\\&.\nOn Linux, XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> \\fBXDG_CACHE_HOME\\fP is also honoured if \\fBBORG_BASE_DIR\\fP is not set.\nThis directory contains the local cache and might need a lot\nof space for dealing with big repositories. Make sure you\\(aqre aware of the associated\nsecurity aspects of the cache location: \\fIcache_security\\fP\n.TP\n.B BORG_CONFIG_DIR\nDefaults to the platform\\-specific config directory (see table above).\nIf \\fBBORG_BASE_DIR\\fP is set, defaults to \\fB$BORG_BASE_DIR/.config/borg\\fP\\&.\nOn Linux, XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> \\fBXDG_CONFIG_HOME\\fP is also honoured if \\fBBORG_BASE_DIR\\fP is not set.\nThis directory contains all borg configuration directories, see the FAQ\nfor a security advisory about the data in this directory: \\fIhome_config_borg\\fP\n.TP\n.B BORG_DATA_DIR\nDefaults to the platform\\-specific data directory (see table above).\nIf \\fBBORG_BASE_DIR\\fP is set, defaults to \\fB$BORG_BASE_DIR/.local/share/borg\\fP\\&.\nOn Linux, XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> \\fBXDG_DATA_HOME\\fP is also honoured if \\fBBORG_BASE_DIR\\fP is not set.\nThis directory contains all borg data directories, see the FAQ\nfor a security advisory about the data in this directory: \\fIhome_data_borg\\fP\n.TP\n.B BORG_RUNTIME_DIR\nDefaults to the platform\\-specific runtime directory (see table above).\nIf \\fBBORG_BASE_DIR\\fP is set, defaults to \\fB$BORG_BASE_DIR/.cache/borg\\fP\\&.\nOn Linux, XDG env var \\%<https://\\:specifications\\:.freedesktop\\:.org/\\:basedir-spec/\\:0\\:.6/\\:ar01s03\\:.html> \\fBXDG_RUNTIME_DIR\\fP is also honoured if \\fBBORG_BASE_DIR\\fP is not set.\nThis directory contains borg runtime files, like e.g. the socket file.\n.TP\n.B BORG_SECURITY_DIR\nDefaults to \\fB$BORG_DATA_DIR/security\\fP\\&.\nThis directory contains security relevant data.\n.TP\n.B BORG_KEYS_DIR\nDefaults to \\fB$BORG_CONFIG_DIR/keys\\fP\\&.\nThis directory contains keys for encrypted repositories.\n.TP\n.B BORG_KEY_FILE\nWhen set, use the given path as repository key file. Please note that this is only\nfor rather special applications that externally fully manage the key files:\n.INDENT 7.0\n.IP \\(bu 2\nthis setting only applies to the keyfile modes (not to the repokey modes).\n.IP \\(bu 2\nusing a full, absolute path to the key file is recommended.\n.IP \\(bu 2\nall directories in the given path must exist.\n.IP \\(bu 2\nthis setting forces borg to use the key file at the given location.\n.IP \\(bu 2\nthe key file must either exist (for most commands) or will be created (\\fBborg repo\\-create\\fP).\n.IP \\(bu 2\nyou need to give a different path for different repositories.\n.IP \\(bu 2\nyou need to point to the correct key file matching the repository the command will operate on.\n.UNINDENT\n.TP\n.B TMPDIR\nThis is where temporary files are stored (might need a lot of temporary space for some\noperations), see tempfile \\%<https://\\:docs\\:.python\\:.org/\\:3/\\:library/\\:tempfile\\:.html#\\:tempfile\\:.gettempdir> for details.\n.UNINDENT\n.UNINDENT\n.UNINDENT\n.INDENT 0.0\n.TP\n.B Building:\n.INDENT 7.0\n.TP\n.B BORG_OPENSSL_NAME\nDefines the subdirectory name for OpenSSL (setup.py).\n.TP\n.B BORG_OPENSSL_PREFIX\nAdds given OpenSSL header file directory to the default locations (setup.py).\n.TP\n.B BORG_LIBACL_PREFIX\nAdds given prefix directory to the default locations. If an \\(aqinclude/acl/libacl.h\\(aq is found\nBorg will be linked against the system libacl instead of a bundled implementation. (setup.py)\n.TP\n.B BORG_LIBLZ4_PREFIX\nAdds given prefix directory to the default locations. If a \\(aqinclude/lz4.h\\(aq is found Borg\nwill be linked against the system liblz4 instead of a bundled implementation. (setup.py)\n.UNINDENT\n.UNINDENT\n.sp\nPlease note:\n.INDENT 0.0\n.IP \\(bu 2\nBe very careful when using the \\(dqyes\\(dq sayers, the warnings with prompt exist for your / your data\\(aqs security/safety.\n.IP \\(bu 2\nAlso be very careful when putting your passphrase into a script, make sure it has appropriate file permissions (e.g.\nmode 600, root:root).\n.UNINDENT\n.SS Automatically generated Environment Variables (jsonargparse)\n.sp\nBorg uses jsonargparse \\%<https://\\:jsonargparse\\:.readthedocs\\:.io/> with \\fBdefault_env=True\\fP, which means that every\ncommand\\-line option can also be set via an environment variable.\n.sp\nThe environment variable name is derived from the program name (\\fBborg\\fP),\nthe subcommand (if any), and the option name, all converted to uppercase\nwith dashes replaced by underscores.\n.sp\nFor \\fBtop\\-level options\\fP (not specific to a subcommand), the pattern is:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nBORG_<OPTION>\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nFor example, \\fB\\-\\-lock\\-wait\\fP can be set via \\fBBORG_LOCK_WAIT\\fP\\&.\n.sp\nFor \\fBsubcommand options\\fP, the subcommand and option are separated by a\ndouble underscore:\n.INDENT 0.0\n.INDENT 3.5\n.sp\n.EX\nBORG_<SUBCOMMAND>__<OPTION>\n.EE\n.UNINDENT\n.UNINDENT\n.sp\nFor example, \\fBborg create \\-\\-comment\\fP can be set via \\fBBORG_CREATE__COMMENT\\fP\\&.\n.SS File systems\n.sp\nWe recommend using a reliable, scalable journaling filesystem for the\nrepository, e.g., zfs, btrfs, ext4, apfs.\n.sp\nBorg now uses the \\fBborgstore\\fP package to implement the key/value store it\nuses for the repository.\n.sp\nIt currently uses the \\fBfile:\\fP store (posixfs backend) either with a local\ndirectory or via SSH and a remote \\fBborg serve\\fP agent using borgstore on the\nremote side.\n.sp\nThis means that it will store each chunk into a separate filesystem file\n(for more details, see the \\fBborgstore\\fP project).\n.sp\nThis has some pros and cons (compared to legacy Borg 1.x segment files):\n.sp\nPros:\n.INDENT 0.0\n.IP \\(bu 2\nSimplicity and better maintainability of the Borg code.\n.IP \\(bu 2\nSometimes faster, less I/O, better scalability: e.g., borg compact can just\nremove unused chunks by deleting a single file and does not need to read\nand rewrite segment files to free space.\n.IP \\(bu 2\nIn the future, easier to adapt to other kinds of storage:\nborgstore\\(aqs backends are quite simple to implement.\n\\fBsftp:\\fP and \\fBrclone:\\fP backends already exist, others might be easy to add.\n.IP \\(bu 2\nParallel repository access with less locking is easier to implement.\n.UNINDENT\n.sp\nCons:\n.INDENT 0.0\n.IP \\(bu 2\nThe repository filesystem will have to deal with a large number of files (there\nare provisions in borgstore against having too many files in a single directory\nby using a nested directory structure).\n.IP \\(bu 2\nGreater filesystem space overhead (depends on the allocation block size — modern\nfilesystems like zfs are rather clever here, using a variable block size).\n.IP \\(bu 2\nSometimes slower, due to less sequential and more random access operations.\n.UNINDENT\n.SS Units\n.sp\nTo display quantities, Borg takes care of respecting the\nusual conventions of scale. Disk sizes are displayed in decimal \\%<https://\\:en\\:.wikipedia\\:.org/\\:wiki/\\:Decimal>, using powers of ten (so\n\\fBkB\\fP means 1000 bytes). For memory usage, binary prefixes \\%<https://\\:en\\:.wikipedia\\:.org/\\:wiki/\\:Binary_prefix> are used, and are\nindicated using the IEC binary prefixes \\%<https://\\:en\\:.wikipedia\\:.org/\\:wiki/\\:IEC_80000-13#\\:Prefixes_for_binary_multiples>,\nusing powers of two (so \\fBKiB\\fP means 1024 bytes).\n.SS Date and Time\n.sp\nWe format date and time in accordance with ISO 8601, that is: YYYY\\-MM\\-DD and\nHH:MM:SS (24\\-hour clock).\n.sp\nFor more information, see: \\%<https://\\:xkcd\\:.com/\\:1179/>\n.sp\nUnless otherwise noted, we display local date and time.\nInternally, we store and process date and time as UTC.\nTIMESPAN\n.sp\nSome options accept a TIMESPAN parameter, which can be given as a number of\nyears (e.g. \\fB2y\\fP), months (e.g. \\fB12m\\fP), weeks (e.g. \\fB2w\\fP),\ndays (e.g. \\fB7d\\fP), hours (e.g. \\fB8H\\fP), minutes (e.g. \\fB30M\\fP),\nor seconds (e.g. \\fB150S\\fP).\n.SS Resource Usage\n.sp\nBorg might use significant resources depending on the size of the data set it is dealing with.\n.sp\nIf you use Borg in a client/server way (with an SSH repository),\nthe resource usage occurs partly on the client and partly on the\nserver.\n.sp\nIf you use Borg as a single process (with a filesystem repository),\nall resource usage occurs in that one process, so add up client and\nserver to get the approximate resource usage.\n.INDENT 0.0\n.TP\n.B CPU client:\n.INDENT 7.0\n.IP \\(bu 2\n\\fBborg create:\\fP chunking, hashing, compression, encryption (high CPU usage)\n.IP \\(bu 2\n\\fBchunks cache sync:\\fP quite heavy on CPU, doing lots of hash table operations\n.IP \\(bu 2\n\\fBborg extract:\\fP decryption, decompression (medium to high CPU usage)\n.IP \\(bu 2\n\\fBborg check:\\fP similar to extract, but depends on options given\n.IP \\(bu 2\n\\fBborg prune/borg delete archive:\\fP low to medium CPU usage\n.IP \\(bu 2\n\\fBborg delete repo:\\fP done on the server\n.UNINDENT\n.sp\nIt will not use more than 100% of one CPU core as the code is currently single\\-threaded.\nEspecially higher zlib and lzma compression levels use significant amounts\nof CPU cycles. Crypto might be cheap on the CPU (if hardware\\-accelerated) or\nexpensive (if not).\n.TP\n.B CPU server:\nIt usually does not need much CPU; it just deals with the key/value store\n(repository) and uses the repository index for that.\n.sp\nborg check: the repository check computes the checksums of all chunks\n(medium CPU usage)\nborg delete repo: low CPU usage\n.TP\n.B CPU (only for client/server operation):\nWhen using Borg in a client/server way with an ssh\\-type repository, the SSH\nprocesses used for the transport layer will need some CPU on the client and\non the server due to the crypto they are doing — especially if you are pumping\nlarge amounts of data.\n.TP\n.B Memory (RAM) client:\nThe chunks index and the files index are read into memory for performance\nreasons. Might need large amounts of memory (see below).\nCompression, especially lzma compression with high levels, might need substantial\namounts of memory.\n.TP\n.B Memory (RAM) server:\nThe server process will load the repository index into memory. Might need\nconsiderable amounts of memory, but less than on the client (see below).\n.TP\n.B Chunks index (client only):\nProportional to the number of data chunks in your repo. Lots of chunks\nin your repo imply a big chunks index.\nIt is possible to tweak the chunker parameters (see create options).\n.TP\n.B Files index (client only):\nProportional to the number of files in your last backups. Can be switched\noff (see create options), but the next backup might be much slower if you do.\nThe speed benefit of using the files cache is proportional to file size.\n.TP\n.B Repository index (server only):\nProportional to the number of data chunks in your repo. Lots of chunks\nin your repo imply a big repository index.\nIt is possible to tweak the chunker parameters (see create options) to\ninfluence the number of chunks created.\n.TP\n.B Temporary files (client):\nReading data and metadata from a FUSE\\-mounted repository will consume up to\nthe size of all deduplicated, small chunks in the repository. Big chunks\nwill not be locally cached.\n.TP\n.B Temporary files (server):\nA non\\-trivial amount of data will be stored in the remote temporary directory\nfor each client that connects to it. For some remotes, this can fill the\ndefault temporary directory in /tmp. This can be mitigated by ensuring the\n$TMPDIR, $TEMP, or $TMP environment variable is properly set for the sshd\nprocess.\nFor some OSes, this can be done by setting the correct value in the\n\\&.bashrc (or equivalent login config file for other shells); however, in\nother cases it may be necessary to first enable \\fBPermitUserEnvironment yes\\fP\nin your \\fBsshd_config\\fP file, then add \\fBenvironment=\\(dqTMPDIR=/my/big/tmpdir\\(dq\\fP\nat the start of the public key to be used in the \\fBauthorized_keys\\fP file.\n.TP\n.B Cache files (client only):\nContains the chunks index and files index (plus a collection of single\\-\narchive chunk indexes), which might need huge amounts of disk space\ndepending on archive count and size — see the FAQ for how to reduce this.\n.TP\n.B Network (only for client/server operation):\nIf your repository is remote, all deduplicated (and optionally compressed/\nencrypted) data has to go over the connection (\\fBssh://\\fP repository URL).\nIf you use a locally mounted network filesystem, some additional copy\noperations used for transaction support also go over the connection. If\nyou back up multiple sources to one target repository, additional traffic\nhappens for cache resynchronization.\n.UNINDENT\n.SS Support for file metadata\n.sp\nBesides regular file and directory structures, Borg can preserve\n.INDENT 0.0\n.IP \\(bu 2\nsymlinks (stored as a symlink; the symlink is not followed)\n.IP \\(bu 2\nspecial files:\n.INDENT 2.0\n.IP \\(bu 2\ncharacter and block device files (restored via mknod(2))\n.IP \\(bu 2\nFIFOs (\\(dqnamed pipes\\(dq)\n.IP \\(bu 2\nspecial file \\fIcontents\\fP can be backed up in \\fB\\-\\-read\\-special\\fP mode.\nBy default, the metadata to create them with mknod(2), mkfifo(2), etc. is stored.\n.UNINDENT\n.IP \\(bu 2\nhard\\-linked regular files, devices, symlinks, FIFOs (considering all items in the same archive)\n.IP \\(bu 2\ntimestamps with nanosecond precision: mtime, atime, ctime\n.IP \\(bu 2\nother timestamps: birthtime (on platforms supporting it)\n.IP \\(bu 2\npermissions:\n.INDENT 2.0\n.IP \\(bu 2\nIDs of owning user and owning group\n.IP \\(bu 2\nnames of owning user and owning group (if the IDs can be resolved)\n.IP \\(bu 2\nUnix Mode/Permissions (u/g/o permissions, suid, sgid, sticky)\n.UNINDENT\n.UNINDENT\n.sp\nOn some platforms additional features are supported:\n.\\\" Yes/No's are grouped by reason/mechanism/reference.\n.\n.TS\nbox center;\nl|l|l|l.\nT{\nPlatform\nT}\tT{\nACLs\n[5]\nT}\tT{\nxattr\n[6]\nT}\tT{\nFlags\n[7]\nT}\n_\nT{\nLinux\nT}\tT{\nYes\nT}\tT{\nYes\nT}\tT{\nYes [1]\nT}\n_\nT{\nmacOS\nT}\tT{\nYes\nT}\tT{\nYes\nT}\tT{\nYes (all)\nT}\n_\nT{\nFreeBSD\nT}\tT{\nYes\nT}\tT{\nYes\nT}\tT{\nYes (all)\nT}\n_\nT{\nOpenBSD\nT}\tT{\nn/a\nT}\tT{\nn/a\nT}\tT{\nYes (all)\nT}\n_\nT{\nNetBSD\nT}\tT{\nn/a\nT}\tT{\nNo [2]\nT}\tT{\nYes (all)\nT}\n_\nT{\nSolaris and derivatives\nT}\tT{\nNo [3]\nT}\tT{\nNo [3]\nT}\tT{\nn/a\nT}\n_\nT{\nWindows (cygwin)\nT}\tT{\nNo [4]\nT}\tT{\nNo\nT}\tT{\nNo\nT}\n.TE\n.sp\nOther Unix\\-like operating systems may work as well, but have not been tested yet.\n.sp\nNote that most platform\\-dependent features also depend on the filesystem.\nFor example, ntfs\\-3g on Linux is not able to convey NTFS ACLs.\n.IP [1] 5\nOnly \\(dqnodump\\(dq, \\(dqimmutable\\(dq, \\(dqcompressed\\(dq and \\(dqappend\\(dq are supported.\nFeature request #618 for more flags.\n.IP [2] 5\nFeature request #1332\n.IP [3] 5\nFeature request #1337\n.IP [4] 5\nCygwin tries to map NTFS ACLs to permissions with varying degrees of success.\n.IP [5] 5\nThe native access control list mechanism of the OS. This normally limits access to\nnon\\-native ACLs. For example, NTFS ACLs are not completely accessible on Linux with ntfs\\-3g.\n.IP [6] 5\nExtended attributes; key\\-value pairs attached to a file, mainly used by the OS.\nThis includes resource forks on macOS.\n.IP [7] 5\nAlso known as \\fIBSD flags\\fP\\&. The Linux set of flags [1] is portable across platforms.\nThe BSDs define additional flags.\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP for common command line options\n.sp\n\\fIborg\\-repo\\-create(1)\\fP, \\fIborg\\-repo\\-delete(1)\\fP, \\fIborg\\-repo\\-list(1)\\fP, \\fIborg\\-repo\\-info(1)\\fP,\n\\fIborg\\-create(1)\\fP, \\fIborg\\-mount(1)\\fP, \\fIborg\\-extract(1)\\fP,\n\\fIborg\\-list(1)\\fP, \\fIborg\\-info(1)\\fP,\n\\fIborg\\-delete(1)\\fP, \\fIborg\\-prune(1)\\fP, \\fIborg\\-compact(1)\\fP,\n\\fIborg\\-recreate(1)\\fP\n.sp\n\\fIborg\\-compression(1)\\fP, \\fIborg\\-patterns(1)\\fP, \\fIborg\\-placeholders(1)\\fP\n.INDENT 0.0\n.IP \\(bu 2\nMain web site \\%<https://\\:www\\:.borgbackup\\:.org/>\n.IP \\(bu 2\nReleases \\%<https://\\:github\\:.com/\\:borgbackup/\\:borg/\\:releases>\n.IP \\(bu 2\nChangelog \\%<https://\\:github\\:.com/\\:borgbackup/\\:borg/\\:blob/\\:master/\\:docs/\\:changes\\:.rst>\n.IP \\(bu 2\nGitHub \\%<https://\\:github\\:.com/\\:borgbackup/\\:borg>\n.IP \\(bu 2\nSecurity contact \\%<https://\\:borgbackup\\:.readthedocs\\:.io/\\:en/\\:latest/\\:support\\:.html#\\:security-contact>\n.UNINDENT\n.SH Author\nThe Borg Collective\n\norphan: \n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man/borgfs.1",
    "content": ".\\\" Man page generated from reStructuredText\n.\\\" by the Docutils 0.22.4 manpage writer.\n.\n.\n.nr rst2man-indent-level 0\n.\n.de1 rstReportMargin\n\\\\$1 \\\\n[an-margin]\nlevel \\\\n[rst2man-indent-level]\nlevel margin: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n-\n\\\\n[rst2man-indent0]\n\\\\n[rst2man-indent1]\n\\\\n[rst2man-indent2]\n..\n.de1 INDENT\n.\\\" .rstReportMargin pre:\n. RS \\\\$1\n. nr rst2man-indent\\\\n[rst2man-indent-level] \\\\n[an-margin]\n. nr rst2man-indent-level +1\n.\\\" .rstReportMargin post:\n..\n.de UNINDENT\n. RE\n.\\\" indent \\\\n[an-margin]\n.\\\" old: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.nr rst2man-indent-level -1\n.\\\" new: \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]\n.in \\\\n[rst2man-indent\\\\n[rst2man-indent-level]]u\n..\n.TH \"borgfs\" \"1\" \"2026-03-15\" \"\" \"borg backup tool\"\n.SH Name\nborgfs \\- Mounts an archive or an entire repository as a FUSE filesystem.\n.SH SYNOPSIS\n.sp\nborgfs [options] MOUNTPOINT [PATH...]\n.SH DESCRIPTION\n.sp\nFor more information, see borg mount \\-\\-help.\n.SH OPTIONS\n.sp\nSee \\fIborg\\-common(1)\\fP for common options of Borg commands.\n.SS arguments\n.INDENT 0.0\n.TP\n.B MOUNTPOINT\nwhere to mount the filesystem\n.TP\n.B PATH\npaths to extract; patterns are supported\n.UNINDENT\n.SS options\n.INDENT 0.0\n.TP\n.B  \\-\\-config\nPath to a configuration file.\n.UNINDENT\n.IP \"System Message: WARNING/2 (docs/borgfs.rst:, line 51)\"\nOption list ends without a blank line; unexpected unindent.\n.sp\n\\-\\-print_config \b[=flags]     Print the configuration after applying all other arguments and exit. The optional flags customizes the output and are one or more keywords separated by comma. The supported flags are: skip_default, skip_null.\n\\-V, \\-\\-version     show version number and exit\n\\-\\-cockpit     Start the Borg TUI\n\\-f, \\-\\-foreground     stay in foreground, do not daemonize\n\\-o      extra mount options\n\\-\\-numeric\\-ids     use numeric user and group identifiers from archives\n.SS Archive filters\n.INDENT 0.0\n.TP\n.BI \\-a \\ PATTERN\\fR,\\fB \\ \\-\\-match\\-archives \\ PATTERN\nonly consider archives matching all patterns. See \\(dqborg help match\\-archives\\(dq.\n.TP\n.BI \\-\\-sort\\-by \\ KEYS\nComma\\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n.TP\n.BI \\-\\-first \\ N\nconsider the first N archives after other filters are applied\n.TP\n.BI \\-\\-last \\ N\nconsider the last N archives after other filters are applied\n.TP\n.BI \\-\\-oldest \\ TIMESPAN\nconsider archives between the oldest archive\\(aqs timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newest \\ TIMESPAN\nconsider archives between the newest archive\\(aqs timestamp and (newest \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-older \\ TIMESPAN\nconsider archives older than (now \\- TIMESPAN), e.g., 7d or 12m.\n.TP\n.BI \\-\\-newer \\ TIMESPAN\nconsider archives newer than (now \\- TIMESPAN), e.g., 7d or 12m.\n.UNINDENT\n.SS Include/Exclude options\n.INDENT 0.0\n.TP\n.BI \\-e \\ PATTERN\\fR,\\fB \\ \\-\\-exclude \\ PATTERN\nexclude paths matching PATTERN\n.TP\n.BI \\-\\-exclude\\-from \\ EXCLUDEFILE\nread exclude patterns from EXCLUDEFILE, one per line\n.TP\n.BI \\-\\-pattern \\ PATTERN\ninclude/exclude paths matching PATTERN\n.TP\n.BI \\-\\-patterns\\-from \\ PATTERNFILE\nread include/exclude patterns from PATTERNFILE, one per line\n.TP\n.BI \\-\\-strip\\-components \\ NUMBER\nRemove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n.UNINDENT\n.SH SEE ALSO\n.sp\n\\fIborg\\-common(1)\\fP\n.SH Author\nThe Borg Collective\n.\\\" End of generated man page.\n"
  },
  {
    "path": "docs/man_intro.rst",
    "content": ":orphan:\n\nSYNOPSIS\n--------\n\nborg [common options] <command> [options] [arguments]\n\nDESCRIPTION\n-----------\n\n.. we don't include the README.rst here since we want to keep this terse.\n\nBorgBackup (short: Borg) is a deduplicating backup program.\nOptionally, it supports compression and authenticated encryption.\n\nThe main goal of Borg is to provide an efficient and secure way to back up data.\nThe data deduplication technique used makes Borg suitable for daily backups\nsince only changes are stored.\nThe authenticated encryption technique makes it suitable for backups to targets not\nfully trusted.\n\nBorg stores a set of files in an *archive*. A *repository* is a collection\nof *archives*. The format of repositories is Borg-specific. Borg does not\ndistinguish archives from each other in any way other than their name,\nit does not matter when or where archives were created (e.g., different hosts).\n\nEXAMPLES\n--------\n\nA step-by-step example\n~~~~~~~~~~~~~~~~~~~~~~\n\n.. include:: quickstart_example.rst.inc\n\nNOTES\n-----\n\n.. include:: usage_general.rst.inc\n\nSEE ALSO\n--------\n\n`borg-common(1)` for common command line options\n\n`borg-repo-create(1)`, `borg-repo-delete(1)`, `borg-repo-list(1)`, `borg-repo-info(1)`,\n`borg-create(1)`, `borg-mount(1)`, `borg-extract(1)`,\n`borg-list(1)`, `borg-info(1)`,\n`borg-delete(1)`, `borg-prune(1)`, `borg-compact(1)`,\n`borg-recreate(1)`\n\n`borg-compression(1)`, `borg-patterns(1)`, `borg-placeholders(1)`\n\n* Main web site https://www.borgbackup.org/\n* Releases https://github.com/borgbackup/borg/releases\n* Changelog https://github.com/borgbackup/borg/blob/master/docs/changes.rst\n* GitHub https://github.com/borgbackup/borg\n* Security contact https://borgbackup.readthedocs.io/en/latest/support.html#security-contact\n"
  },
  {
    "path": "docs/misc/asciinema/README",
    "content": "Do NOT run the examples without isolation (e.g Vagrant) or\nthis code may make undesirable changes to your host.\n\nRunning `vagrant up` in this directory will update the screencasts.\n"
  },
  {
    "path": "docs/misc/asciinema/Vagrantfile",
    "content": "Vagrant.configure(\"2\") do |config|\n  config.vm.box = \"debian/bullseye64\"\n  config.vm.provision \"install dependencies\", type: \"shell\", inline: <<-SHELL\n    apt-get update\n    apt-get install -y wget expect gpg asciinema ssh adduser fuse\n    mkdir -p /wallpaper\n    wget \\\n        --user-agent=\"borgbackup demo screencast\" \\\n        --input-file=/vagrant/sample-wallpapers.txt \\\n        --directory-prefix=/wallpaper\n  SHELL\n  config.vm.provision \"record install\", type: \"shell\", inline: <<-SHELL\n  \tgpg --recv-keys \"6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393\"\n    asciinema rec -c 'expect /vagrant/install.tcl' --overwrite /vagrant/install.json < /dev/null\n  SHELL\n  config.vm.provision \"record basic usage\", type: \"shell\", inline: <<-SHELL\n    # `rm` below allows quick re-exec via:\n    # vagrant provision --provision-with \"record basic usage\"\n    # this is useful when testing changes\n    rm -r /media/backup/borgdemo || true\n    rm -r ~/.ssh/ || true\n    rm -r Wallpaper || true\n    deluser --remove-home borgdemo || true\n\n    # In case we have skipped \"record install\"\n    if [ ! -e /usr/local/bin/borg ] ; then\n      wget https://github.com/borgbackup/borg/releases/download/1.2.1/borg-linux64\n      install --owner root --group root --mode 755 borg-linux64 /usr/local/bin/borg\n    fi\n\n    mkdir -p /media/backup/borgdemo\n    mkdir Wallpaper\n    cp -r /wallpaper Wallpaper/bigcollection\n    cp /wallpaper/Trapper_cabin.jpg Wallpaper/deer.jpg\n\n    adduser --disabled-password borgdemo\n    echo '127.0.0.1 remoteserver.example' >> /etc/hosts\n    ssh-keygen -f ~/.ssh/id_rsa -N ''\n    ssh-keyscan remoteserver.example > ~/.ssh/known_hosts\n    runuser -u borgdemo mkdir ~borgdemo/.ssh\n    runuser -u borgdemo tee ~borgdemo/.ssh/authorized_keys < ~/.ssh/id_rsa.pub\n\n    asciinema rec -c 'expect /vagrant/basic.tcl' --overwrite /vagrant/basic.json < /dev/null\n  SHELL\n  config.vm.provision \"record advanced usage\", type: \"shell\", inline: <<-SHELL\n    rm -r /media/backup/borgdemo || true\n    rm -r Wallpaper || true\n\n    # In case we have skipped \"record install\"\n    if [ ! -e /usr/local/bin/borg ] ; then\n      wget https://github.com/borgbackup/borg/releases/download/1.2.1/borg-linux64\n      install --owner root --group root --mode 755 borg-linux64 /usr/local/bin/borg\n    fi\n\n    mkdir -p /media/backup/borgdemo\n    mkdir Wallpaper\n    cp -r /wallpaper Wallpaper/bigcollection\n    cp /wallpaper/Trapper_cabin.jpg Wallpaper/deer.jpg\n    mkdir -p ~/Downloads/big\n    dd if=/dev/zero of=loopbackfile.img bs=100M count=4\n    losetup /dev/loop0 loopbackfile.img\n\n\t# Make it look as if the adv. usage screencast was recorded after basic usage\n    export BORG_PASSPHRASE='1234'\n    borg init --encryption=repokey /media/backup/borgdemo\n    borg create --compression lz4 /media/backup/borgdemo::backup1 Wallpaper\n    echo \"new nice file\" > Wallpaper/newfile.txt\n    borg create --compression lz4 /media/backup/borgdemo::backup2 Wallpaper\n    mv Wallpaper/bigcollection Wallpaper/bigcollection_NEW\n    borg create --compression lz4 /media/backup/borgdemo::backup3 Wallpaper\n    unset BORG_PASSPHRASE\n\n    asciinema rec -c 'expect /vagrant/advanced.tcl' --overwrite /vagrant/advanced.json < /dev/null\n  SHELL\nend\n"
  },
  {
    "path": "docs/misc/asciinema/advanced.json",
    "content": "{\"version\": 2, \"width\": 80, \"height\": 24, \"timestamp\": 1657143034, \"env\": {\"SHELL\": \"/bin/bash\", \"TERM\": \"vt100\"}}\n[0.768648, \"o\", \"$ #\"]\n[0.819047, \"o\", \" \"]\n[0.842504, \"o\", \"F\"]\n[0.878977, \"o\", \"o\"]\n[0.899264, \"o\", \"r\"]\n[0.943053, \"o\", \" \"]\n[0.978278, \"o\", \"t\"]\n[0.987772, \"o\", \"h\"]\n[1.031408, \"o\", \"e\"]\n[1.056081, \"o\", \" \"]\n[1.086997, \"o\", \"p\"]\n[1.152571, \"o\", \"r\"]\n[1.209285, \"o\", \"o\"]\n[1.255887, \"o\", \" \"]\n[1.26543, \"o\", \"u\"]\n[1.274899, \"o\", \"s\"]\n[1.315128, \"o\", \"e\"]\n[1.40852, \"o\", \"r\"]\n[1.431039, \"o\", \"s\"]\n[1.447279, \"o\", \",\"]\n[1.519005, \"o\", \" \"]\n[1.575052, \"o\", \"h\"]\n[1.58459, \"o\", \"e\"]\n[1.594101, \"o\", \"r\"]\n[1.606992, \"o\", \"e\"]\n[1.748322, \"o\", \" \"]\n[1.87594, \"o\", \"a\"]\n[1.945731, \"o\", \"r\"]\n[2.000125, \"o\", \"e\"]\n[2.024668, \"o\", \" \"]\n[2.203154, \"o\", \"s\"]\n[2.263049, \"o\", \"o\"]\n[2.309367, \"o\", \"m\"]\n[2.33112, \"o\", \"e\"]\n[2.352861, \"o\", \" \"]\n[2.490625, \"o\", \"a\"]\n[2.551042, \"o\", \"d\"]\n[2.583045, \"o\", \"v\"]\n[2.690816, \"o\", \"a\"]\n[2.700252, \"o\", \"n\"]\n[2.759018, \"o\", \"c\"]\n[2.768529, \"o\", \"e\"]\n[2.808619, \"o\", \"d\"]\n[2.855074, \"o\", \" \"]\n[2.884555, \"o\", \"f\"]\n[2.914707, \"o\", \"e\"]\n[2.929256, \"o\", \"a\"]\n[3.129633, \"o\", \"t\"]\n[3.138965, \"o\", \"u\"]\n[3.184356, \"o\", \"r\"]\n[3.311044, \"o\", \"e\"]\n[3.396778, \"o\", \"s\"]\n[3.406265, \"o\", \" \"]\n[3.415762, \"o\", \"o\"]\n[3.471532, \"o\", \"f\"]\n[3.675, \"o\", \" \"]\n[3.757453, \"o\", \"b\"]\n[3.801838, \"o\", \"o\"]\n[3.838019, \"o\", \"r\"]\n[3.85161, \"o\", \"g\"]\n[4.018913, \"o\", \",\"]\n[4.052867, \"o\", \" \"]\n[4.179986, \"o\", \"s\"]\n[4.190396, \"o\", \"o\"]\n[4.372854, \"o\", \" \"]\n[4.481366, \"o\", \"y\"]\n[4.490884, \"o\", \"o\"]\n[4.511015, \"o\", \"u\"]\n[4.555555, \"o\", \" \"]\n[4.630529, \"o\", \"c\"]\n[4.640007, \"o\", \"a\"]\n[4.724636, \"o\", \"n\"]\n[4.910983, \"o\", \" \"]\n[4.938589, \"o\", \"i\"]\n[5.095055, \"o\", \"m\"]\n[5.107011, \"o\", \"p\"]\n[5.214923, \"o\", \"r\"]\n[5.226529, \"o\", \"e\"]\n[5.315333, \"o\", \"s\"]\n[5.471155, \"o\", \"s\"]\n[5.555004, \"o\", \" \"]\n[5.622492, \"o\", \"y\"]\n[5.661749, \"o\", \"o\"]\n[5.707342, \"o\", \"u\"]\n[5.787235, \"o\", \"r\"]\n[5.811207, \"o\", \" \"]\n[5.857214, \"o\", \"f\"]\n[5.88323, \"o\", \"r\"]\n[5.964393, \"o\", \"i\"]\n[5.97392, \"o\", \"e\"]\n[6.021398, \"o\", \"n\"]\n[6.044981, \"o\", \"d\"]\n[6.069829, \"o\", \"s\"]\n[6.106061, \"o\", \".\"]\n[6.147432, \"o\", \" \"]\n[6.351264, \"o\", \";\"]\n[6.361522, \"o\", \")\"]\n[6.392543, \"o\", \"\\r\\n\"]\n[6.39289, \"o\", \"$ \"]\n[6.392956, \"o\", \"#\"]\n[6.412913, \"o\", \" \"]\n[6.425579, \"o\", \"N\"]\n[6.435148, \"o\", \"o\"]\n[6.444729, \"o\", \"t\"]\n[6.539217, \"o\", \"e\"]\n[6.632652, \"o\", \":\"]\n[6.642205, \"o\", \" \"]\n[6.697711, \"o\", \"T\"]\n[6.715209, \"o\", \"h\"]\n[6.7247, \"o\", \"i\"]\n[6.759216, \"o\", \"s\"]\n[6.95973, \"o\", \" \"]\n[7.008323, \"o\", \"s\"]\n[7.019203, \"o\", \"c\"]\n[7.057184, \"o\", \"r\"]\n[7.257712, \"o\", \"e\"]\n[7.338241, \"o\", \"e\"]\n[7.40562, \"o\", \"n\"]\n[7.43121, \"o\", \"c\"]\n[7.572713, \"o\", \"a\"]\n[7.612116, \"o\", \"s\"]\n[7.626855, \"o\", \"t\"]\n[7.66643, \"o\", \" \"]\n[7.727205, \"o\", \"w\"]\n[7.777758, \"o\", \"a\"]\n[7.827163, \"o\", \"s\"]\n[7.858017, \"o\", \" \"]\n[7.907525, \"o\", \"m\"]\n[7.952253, \"o\", \"a\"]\n[8.014485, \"o\", \"d\"]\n[8.0731, \"o\", \"e\"]\n[8.273661, \"o\", \" \"]\n[8.336051, \"o\", \"w\"]\n[8.439208, \"o\", \"i\"]\n[8.448307, \"o\", \"t\"]\n[8.470845, \"o\", \"h\"]\n[8.499491, \"o\", \" \"]\n[8.614968, \"o\", \"b\"]\n[8.816062, \"o\", \"o\"]\n[8.832931, \"o\", \"r\"]\n[8.991667, \"o\", \"g\"]\n[9.021108, \"o\", \" \"]\n[9.039006, \"o\", \"1\"]\n[9.126149, \"o\", \".\"]\n[9.140746, \"o\", \"2\"]\n[9.298244, \"o\", \".\"]\n[9.319537, \"o\", \"1\"]\n[9.49402, \"o\", \" \"]\n[9.533047, \"o\", \"–\"]\n[9.594869, \"o\", \" \"]\n[9.667117, \"o\", \"o\"]\n[9.761801, \"o\", \"l\"]\n[9.923825, \"o\", \"d\"]\n[10.02222, \"o\", \"e\"]\n[10.062303, \"o\", \"r\"]\n[10.15542, \"o\", \" \"]\n[10.1914, \"o\", \"o\"]\n[10.235464, \"o\", \"r\"]\n[10.263389, \"o\", \" \"]\n[10.272554, \"o\", \"n\"]\n[10.282136, \"o\", \"e\"]\n[10.339477, \"o\", \"w\"]\n[10.54022, \"o\", \"e\"]\n[10.615052, \"o\", \"r\"]\n[10.654864, \"o\", \" \"]\n[10.735194, \"o\", \"b\"]\n[10.744688, \"o\", \"o\"]\n[10.786952, \"o\", \"r\"]\n[10.81902, \"o\", \"g\"]\n[11.010552, \"o\", \" \"]\n[11.105492, \"o\", \"v\"]\n[11.133374, \"o\", \"e\"]\n[11.142911, \"o\", \"r\"]\n[11.25048, \"o\", \"s\"]\n[11.327051, \"o\", \"i\"]\n[11.349735, \"o\", \"o\"]\n[11.406042, \"o\", \"n\"]\n[11.415592, \"o\", \"s\"]\n[11.436237, \"o\", \" \"]\n[11.635499, \"o\", \"m\"]\n[11.723838, \"o\", \"a\"]\n[11.788201, \"o\", \"y\"]\n[11.879904, \"o\", \" \"]\n[11.958149, \"o\", \"b\"]\n[12.001343, \"o\", \"e\"]\n[12.126177, \"o\", \"h\"]\n[12.135508, \"o\", \"a\"]\n[12.230989, \"o\", \"v\"]\n[12.264373, \"o\", \"e\"]\n[12.280025, \"o\", \" \"]\n[12.410209, \"o\", \"d\"]\n[12.450914, \"o\", \"i\"]\n[12.539827, \"o\", \"f\"]\n[12.598996, \"o\", \"f\"]\n[12.608261, \"o\", \"e\"]\n[12.617685, \"o\", \"r\"]\n[12.632187, \"o\", \"e\"]\n[12.790989, \"o\", \"n\"]\n[12.826976, \"o\", \"t\"]\n[12.89162, \"o\", \"l\"]\n[12.975071, \"o\", \"y\"]\n[12.986557, \"o\", \".\"]\n[12.999146, \"o\", \"\\r\\n\"]\n[12.999751, \"o\", \"$ \\r\\n$ #\"]\n[13.060331, \"o\", \" \"]\n[13.069688, \"o\", \"F\"]\n[13.099036, \"o\", \"i\"]\n[13.185258, \"o\", \"r\"]\n[13.198792, \"o\", \"s\"]\n[13.209188, \"o\", \"t\"]\n[13.263526, \"o\", \" \"]\n[13.329055, \"o\", \"o\"]\n[13.407171, \"o\", \"f\"]\n[13.608135, \"o\", \" \"]\n[13.626983, \"o\", \"a\"]\n[13.688315, \"o\", \"l\"]\n[13.697764, \"o\", \"l\"]\n[13.715004, \"o\", \",\"]\n[13.892359, \"o\", \" \"]\n[13.901819, \"o\", \"w\"]\n[13.998468, \"o\", \"e\"]\n[14.032986, \"o\", \" \"]\n[14.087043, \"o\", \"c\"]\n[14.125877, \"o\", \"a\"]\n[14.151009, \"o\", \"n\"]\n[14.235155, \"o\", \" \"]\n[14.313567, \"o\", \"u\"]\n[14.335062, \"o\", \"s\"]\n[14.344804, \"o\", \"e\"]\n[14.379354, \"o\", \" \"]\n[14.509748, \"o\", \"s\"]\n[14.652207, \"o\", \"e\"]\n[14.680643, \"o\", \"v\"]\n[14.728477, \"o\", \"e\"]\n[14.788854, \"o\", \"r\"]\n[14.90724, \"o\", \"a\"]\n[14.961741, \"o\", \"l\"]\n[15.162007, \"o\", \" \"]\n[15.227757, \"o\", \"e\"]\n[15.282232, \"o\", \"n\"]\n[15.291691, \"o\", \"v\"]\n[15.417971, \"o\", \"i\"]\n[15.443234, \"o\", \"r\"]\n[15.555707, \"o\", \"o\"]\n[15.611088, \"o\", \"n\"]\n[15.672888, \"o\", \"m\"]\n[15.696164, \"o\", \"e\"]\n[15.778922, \"o\", \"n\"]\n[15.829436, \"o\", \"t\"]\n[15.871693, \"o\", \" \"]\n[15.922998, \"o\", \"v\"]\n[15.957608, \"o\", \"a\"]\n[15.9844, \"o\", \"r\"]\n[16.001013, \"o\", \"i\"]\n[16.082567, \"o\", \"a\"]\n[16.124326, \"o\", \"b\"]\n[16.219488, \"o\", \"l\"]\n[16.242184, \"o\", \"e\"]\n[16.367007, \"o\", \"s\"]\n[16.482485, \"o\", \" \"]\n[16.535034, \"o\", \"f\"]\n[16.582985, \"o\", \"o\"]\n[16.608749, \"o\", \"r\"]\n[16.805253, \"o\", \" \"]\n[16.817763, \"o\", \"b\"]\n[16.827346, \"o\", \"o\"]\n[16.902962, \"o\", \"r\"]\n[17.010552, \"o\", \"g\"]\n[17.019948, \"o\", \".\"]\n[17.035642, \"o\", \"\\r\\n$ #\"]\n[17.045132, \"o\", \" \"]\n[17.055624, \"o\", \"E\"]\n[17.182999, \"o\", \".\"]\n[17.25914, \"o\", \"g\"]\n[17.454545, \"o\", \".\"]\n[17.464707, \"o\", \" \"]\n[17.474183, \"o\", \"w\"]\n[17.503023, \"o\", \"e\"]\n[17.68359, \"o\", \" \"]\n[17.698943, \"o\", \"d\"]\n[17.791032, \"o\", \"o\"]\n[17.801167, \"o\", \" \"]\n[17.831033, \"o\", \"n\"]\n[17.842921, \"o\", \"o\"]\n[17.959138, \"o\", \"t\"]\n[17.96848, \"o\", \" \"]\n[18.145872, \"o\", \"w\"]\n[18.161406, \"o\", \"a\"]\n[18.292528, \"o\", \"n\"]\n[18.320058, \"o\", \"t\"]\n[18.437598, \"o\", \" \"]\n[18.45628, \"o\", \"t\"]\n[18.541798, \"o\", \"o\"]\n[18.62301, \"o\", \" \"]\n[18.661202, \"o\", \"t\"]\n[18.786572, \"o\", \"y\"]\n[18.796032, \"o\", \"p\"]\n[18.807045, \"o\", \"e\"]\n[18.928807, \"o\", \" \"]\n[18.950521, \"o\", \"i\"]\n[18.987044, \"o\", \"n\"]\n[19.075581, \"o\", \" \"]\n[19.0942, \"o\", \"o\"]\n[19.106641, \"o\", \"u\"]\n[19.130251, \"o\", \"r\"]\n[19.330682, \"o\", \" \"]\n[19.341078, \"o\", \"r\"]\n[19.358696, \"o\", \"e\"]\n[19.368058, \"o\", \"p\"]\n[19.377522, \"o\", \"o\"]\n[19.419039, \"o\", \" \"]\n[19.608443, \"o\", \"p\"]\n[19.617959, \"o\", \"a\"]\n[19.694452, \"o\", \"t\"]\n[19.794913, \"o\", \"h\"]\n[19.952455, \"o\", \" \"]\n[20.035038, \"o\", \"a\"]\n[20.083042, \"o\", \"n\"]\n[20.171973, \"o\", \"d\"]\n[20.243033, \"o\", \" \"]\n[20.254983, \"o\", \"p\"]\n[20.291367, \"o\", \"a\"]\n[20.3727, \"o\", \"s\"]\n[20.466977, \"o\", \"s\"]\n[20.558116, \"o\", \"w\"]\n[20.567538, \"o\", \"o\"]\n[20.607054, \"o\", \"r\"]\n[20.666378, \"o\", \"d\"]\n[20.678977, \"o\", \" \"]\n[20.690985, \"o\", \"a\"]\n[20.702998, \"o\", \"g\"]\n[20.744796, \"o\", \"a\"]\n[20.790992, \"o\", \"i\"]\n[20.800385, \"o\", \"n\"]\n[20.881913, \"o\", \" \"]\n[20.950979, \"o\", \"a\"]\n[21.040524, \"o\", \"n\"]\n[21.11104, \"o\", \"d\"]\n[21.275424, \"o\", \" \"]\n[21.463012, \"o\", \"a\"]\n[21.503013, \"o\", \"g\"]\n[21.512508, \"o\", \"a\"]\n[21.57893, \"o\", \"i\"]\n[21.606392, \"o\", \"n\"]\n[21.725122, \"o\", \"…\"]\n[21.769809, \"o\", \"\\r\\n$ e\"]\n[21.803359, \"o\", \"x\"]\n[21.815009, \"o\", \"p\"]\n[21.824309, \"o\", \"o\"]\n[21.89458, \"o\", \"r\"]\n[21.906961, \"o\", \"t\"]\n[21.948228, \"o\", \" \"]\n[21.967744, \"o\", \"B\"]\n[21.977193, \"o\", \"O\"]\n[22.12507, \"o\", \"R\"]\n[22.154999, \"o\", \"G\"]\n[22.202465, \"o\", \"_\"]\n[22.253869, \"o\", \"R\"]\n[22.269407, \"o\", \"E\"]\n[22.294996, \"o\", \"P\"]\n[22.327029, \"o\", \"O\"]\n[22.416502, \"o\", \"=\"]\n[22.617292, \"o\", \"'\"]\n[22.662825, \"o\", \"/\"]\n[22.67222, \"o\", \"m\"]\n[22.707279, \"o\", \"e\"]\n[22.734538, \"o\", \"d\"]\n[22.790948, \"o\", \"i\"]\n[22.810998, \"o\", \"a\"]\n[22.829427, \"o\", \"/\"]\n[23.031009, \"o\", \"b\"]\n[23.043435, \"o\", \"a\"]\n[23.155688, \"o\", \"c\"]\n[23.182287, \"o\", \"k\"]\n[23.303009, \"o\", \"u\"]\n[23.335461, \"o\", \"p\"]\n[23.434866, \"o\", \"/\"]\n[23.444346, \"o\", \"b\"]\n[23.499795, \"o\", \"o\"]\n[23.531013, \"o\", \"r\"]\n[23.661517, \"o\", \"g\"]\n[23.715646, \"o\", \"d\"]\n[23.755832, \"o\", \"e\"]\n[23.765272, \"o\", \"m\"]\n[23.802991, \"o\", \"o\"]\n[23.872678, \"o\", \"'\"]\n[23.903462, \"o\", \"\\r\\n\"]\n[23.903965, \"o\", \"$ e\"]\n[23.913483, \"o\", \"x\"]\n[23.947622, \"o\", \"p\"]\n[23.964186, \"o\", \"o\"]\n[24.011565, \"o\", \"r\"]\n[24.031009, \"o\", \"t\"]\n[24.040478, \"o\", \" \"]\n[24.225827, \"o\", \"B\"]\n[24.241378, \"o\", \"O\"]\n[24.279005, \"o\", \"R\"]\n[24.28844, \"o\", \"G\"]\n[24.410795, \"o\", \"_\"]\n[24.457036, \"o\", \"P\"]\n[24.478998, \"o\", \"A\"]\n[24.537176, \"o\", \"S\"]\n[24.622476, \"o\", \"S\"]\n[24.63584, \"o\", \"P\"]\n[24.645319, \"o\", \"H\"]\n[24.679837, \"o\", \"R\"]\n[24.713232, \"o\", \"A\"]\n[24.747347, \"o\", \"S\"]\n[24.802772, \"o\", \"E\"]\n[24.939031, \"o\", \"=\"]\n[25.063414, \"o\", \"'\"]\n[25.085883, \"o\", \"1\"]\n[25.131182, \"o\", \"2\"]\n[25.146789, \"o\", \"3\"]\n[25.17097, \"o\", \"4\"]\n[25.181406, \"o\", \"'\"]\n[25.255167, \"o\", \"\\r\\n$ #\"]\n[25.455604, \"o\", \" \"]\n[25.515004, \"o\", \"P\"]\n[25.524401, \"o\", \"r\"]\n[25.533815, \"o\", \"o\"]\n[25.551117, \"o\", \"b\"]\n[25.563604, \"o\", \"l\"]\n[25.652944, \"o\", \"e\"]\n[25.662389, \"o\", \"m\"]\n[25.69001, \"o\", \" \"]\n[25.712501, \"o\", \"s\"]\n[25.724796, \"o\", \"o\"]\n[25.874974, \"o\", \"l\"]\n[25.941505, \"o\", \"v\"]\n[25.982985, \"o\", \"e\"]\n[26.010991, \"o\", \"d\"]\n[26.050291, \"o\", \",\"]\n[26.093324, \"o\", \" \"]\n[26.111054, \"o\", \"b\"]\n[26.198394, \"o\", \"o\"]\n[26.286561, \"o\", \"r\"]\n[26.326204, \"o\", \"g\"]\n[26.356889, \"o\", \" \"]\n[26.430982, \"o\", \"w\"]\n[26.44645, \"o\", \"i\"]\n[26.472184, \"o\", \"l\"]\n[26.494997, \"o\", \"l\"]\n[26.530965, \"o\", \" \"]\n[26.607747, \"o\", \"u\"]\n[26.63889, \"o\", \"s\"]\n[26.728184, \"o\", \"e\"]\n[26.746538, \"o\", \" \"]\n[26.776367, \"o\", \"t\"]\n[26.812555, \"o\", \"h\"]\n[26.821996, \"o\", \"i\"]\n[26.838955, \"o\", \"s\"]\n[26.927322, \"o\", \" \"]\n[26.936757, \"o\", \"a\"]\n[26.946151, \"o\", \"u\"]\n[26.995406, \"o\", \"t\"]\n[27.011962, \"o\", \"o\"]\n[27.024437, \"o\", \"m\"]\n[27.117245, \"o\", \"a\"]\n[27.126966, \"o\", \"t\"]\n[27.279335, \"o\", \"i\"]\n[27.288775, \"o\", \"c\"]\n[27.314968, \"o\", \"a\"]\n[27.334924, \"o\", \"l\"]\n[27.371544, \"o\", \"l\"]\n[27.431022, \"o\", \"y\"]\n[27.631831, \"o\", \"…\"]\n[27.65508, \"o\", \" \"]\n[27.668634, \"o\", \":\"]\n[27.678135, \"o\", \")\"]\n[27.687675, \"o\", \"\\r\\n\"]\n[27.688086, \"o\", \"$ #\"]\n[27.70553, \"o\", \" \"]\n[27.736874, \"o\", \"W\"]\n[27.778417, \"o\", \"e\"]\n[27.787855, \"o\", \"'\"]\n[27.847064, \"o\", \"l\"]\n[27.872775, \"o\", \"l\"]\n[27.882286, \"o\", \" \"]\n[28.006994, \"o\", \"u\"]\n[28.131631, \"o\", \"s\"]\n[28.225535, \"o\", \"e\"]\n[28.246953, \"o\", \" \"]\n[28.262315, \"o\", \"t\"]\n[28.370427, \"o\", \"h\"]\n[28.384875, \"o\", \"i\"]\n[28.502982, \"o\", \"s\"]\n[28.703165, \"o\", \" \"]\n[28.7349, \"o\", \"r\"]\n[28.770977, \"o\", \"i\"]\n[28.832334, \"o\", \"g\"]\n[28.842777, \"o\", \"h\"]\n[28.874993, \"o\", \"t\"]\n[28.888544, \"o\", \" \"]\n[28.902979, \"o\", \"a\"]\n[29.076391, \"o\", \"w\"]\n[29.120581, \"o\", \"a\"]\n[29.225466, \"o\", \"y\"]\n[29.235041, \"o\", \"…\"]\n[29.283084, \"o\", \"\\r\\n\"]\n[29.283646, \"o\", \"$ \\r\\n\"]\n[29.284034, \"o\", \"$ #\"]\n[29.383424, \"o\", \"#\"]\n[29.392808, \"o\", \" \"]\n[29.420544, \"o\", \"A\"]\n[29.4309, \"o\", \"D\"]\n[29.440364, \"o\", \"V\"]\n[29.449873, \"o\", \"A\"]\n[29.495251, \"o\", \"N\"]\n[29.639413, \"o\", \"C\"]\n[29.64894, \"o\", \"E\"]\n[29.66553, \"o\", \"D\"]\n[29.746904, \"o\", \" \"]\n[29.758356, \"o\", \"C\"]\n[29.798779, \"o\", \"R\"]\n[29.830964, \"o\", \"E\"]\n[29.843403, \"o\", \"A\"]\n[29.858925, \"o\", \"T\"]\n[29.868203, \"o\", \"I\"]\n[29.899006, \"o\", \"O\"]\n[29.965264, \"o\", \"N\"]\n[30.162763, \"o\", \" \"]\n[30.191018, \"o\", \"#\"]\n[30.201373, \"o\", \"#\"]\n[30.272056, \"o\", \"\\r\\n\"]\n[30.272614, \"o\", \"$ \\r\\n\"]\n[30.272946, \"o\", \"$ #\"]\n[30.293589, \"o\", \" \"]\n[30.33099, \"o\", \"W\"]\n[30.340457, \"o\", \"e\"]\n[30.542977, \"o\", \" \"]\n[30.560565, \"o\", \"c\"]\n[30.572165, \"o\", \"a\"]\n[30.614991, \"o\", \"n\"]\n[30.663027, \"o\", \" \"]\n[30.71139, \"o\", \"a\"]\n[30.783952, \"o\", \"l\"]\n[30.801481, \"o\", \"s\"]\n[30.839011, \"o\", \"o\"]\n[30.852338, \"o\", \" \"]\n[30.861725, \"o\", \"u\"]\n[30.900896, \"o\", \"s\"]\n[30.927412, \"o\", \"e\"]\n[31.014982, \"o\", \" \"]\n[31.055047, \"o\", \"s\"]\n[31.223032, \"o\", \"o\"]\n[31.302948, \"o\", \"m\"]\n[31.339396, \"o\", \"e\"]\n[31.539842, \"o\", \" \"]\n[31.676113, \"o\", \"p\"]\n[31.857625, \"o\", \"l\"]\n[31.916215, \"o\", \"a\"]\n[31.9574, \"o\", \"c\"]\n[31.981896, \"o\", \"e\"]\n[32.088152, \"o\", \"h\"]\n[32.107751, \"o\", \"o\"]\n[32.188877, \"o\", \"l\"]\n[32.272549, \"o\", \"d\"]\n[32.301495, \"o\", \"e\"]\n[32.321059, \"o\", \"r\"]\n[32.408185, \"o\", \"s\"]\n[32.422845, \"o\", \" \"]\n[32.451011, \"o\", \"i\"]\n[32.529593, \"o\", \"n\"]\n[32.593158, \"o\", \" \"]\n[32.632624, \"o\", \"o\"]\n[32.642958, \"o\", \"u\"]\n[32.65631, \"o\", \"r\"]\n[32.838845, \"o\", \" \"]\n[32.898746, \"o\", \"a\"]\n[32.922951, \"o\", \"r\"]\n[32.971394, \"o\", \"c\"]\n[32.982984, \"o\", \"h\"]\n[33.022383, \"o\", \"i\"]\n[33.032879, \"o\", \"v\"]\n[33.051246, \"o\", \"e\"]\n[33.084787, \"o\", \" \"]\n[33.26713, \"o\", \"n\"]\n[33.353287, \"o\", \"a\"]\n[33.366472, \"o\", \"m\"]\n[33.422971, \"o\", \"e\"]\n[33.581748, \"o\", \"…\"]\n[33.713408, \"o\", \"\\r\\n\"]\n[33.713891, \"o\", \"$ b\"]\n[33.867002, \"o\", \"o\"]\n[33.91104, \"o\", \"r\"]\n[33.932041, \"o\", \"g\"]\n[34.075293, \"o\", \" \"]\n[34.084734, \"o\", \"c\"]\n[34.121825, \"o\", \"r\"]\n[34.219301, \"o\", \"e\"]\n[34.288629, \"o\", \"a\"]\n[34.343252, \"o\", \"t\"]\n[34.357744, \"o\", \"e\"]\n[34.435105, \"o\", \" \"]\n[34.444706, \"o\", \"-\"]\n[34.52727, \"o\", \"-\"]\n[34.564482, \"o\", \"s\"]\n[34.574039, \"o\", \"t\"]\n[34.599311, \"o\", \"a\"]\n[34.755245, \"o\", \"t\"]\n[34.771806, \"o\", \"s\"]\n[34.78124, \"o\", \" \"]\n[34.851131, \"o\", \"-\"]\n[34.939052, \"o\", \"-\"]\n[34.951559, \"o\", \"p\"]\n[34.961005, \"o\", \"r\"]\n[34.97031, \"o\", \"o\"]\n[35.12819, \"o\", \"g\"]\n[35.228805, \"o\", \"r\"]\n[35.251403, \"o\", \"e\"]\n[35.276778, \"o\", \"s\"]\n[35.307296, \"o\", \"s\"]\n[35.316668, \"o\", \" \"]\n[35.343066, \"o\", \"-\"]\n[35.367632, \"o\", \"-\"]\n[35.415702, \"o\", \"c\"]\n[35.452671, \"o\", \"o\"]\n[35.574231, \"o\", \"m\"]\n[35.650388, \"o\", \"p\"]\n[35.659845, \"o\", \"r\"]\n[35.735001, \"o\", \"e\"]\n[35.752448, \"o\", \"s\"]\n[35.787031, \"o\", \"s\"]\n[35.796485, \"o\", \"i\"]\n[35.843758, \"o\", \"o\"]\n[35.870949, \"o\", \"n\"]\n[35.958837, \"o\", \" \"]\n[35.970931, \"o\", \"l\"]\n[36.010975, \"o\", \"z\"]\n[36.211339, \"o\", \"4\"]\n[36.220814, \"o\", \" \"]\n[36.303384, \"o\", \":\"]\n[36.312833, \"o\", \":\"]\n[36.392376, \"o\", \"{\"]\n[36.442668, \"o\", \"u\"]\n[36.64288, \"o\", \"s\"]\n[36.690147, \"o\", \"e\"]\n[36.72678, \"o\", \"r\"]\n[36.927231, \"o\", \"}\"]\n[36.963512, \"o\", \"-\"]\n[37.061838, \"o\", \"{\"]\n[37.083465, \"o\", \"n\"]\n[37.14301, \"o\", \"o\"]\n[37.260484, \"o\", \"w\"]\n[37.33903, \"o\", \"}\"]\n[37.428115, \"o\", \" \"]\n[37.44573, \"o\", \"W\"]\n[37.455225, \"o\", \"a\"]\n[37.526439, \"o\", \"l\"]\n[37.535647, \"o\", \"l\"]\n[37.545066, \"o\", \"p\"]\n[37.561417, \"o\", \"a\"]\n[37.570742, \"o\", \"p\"]\n[37.615839, \"o\", \"e\"]\n[37.633475, \"o\", \"r\"]\n[37.77723, \"o\", \"\\r\\n\"]\n[38.728182, \"o\", \"0 B O 0 B C 0 B D 0 N Wallpaper                                                 \\r\"]\n[38.729483, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[38.73014, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[38.73084, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[38.731497, \"o\", \"                                                                                \\r\"]\n[38.740067, \"o\", \"                                                                                \\r\"]\n[38.75526, \"o\", \"Saving files cache                                                              \\r\"]\n[38.756708, \"o\", \"Saving chunks cache                                                             \\r\"]\n[38.757468, \"o\", \"Saving cache config                                                             \\r\"]\n[38.759071, \"o\", \"                                                                                \\r\"]\n[38.761562, \"o\", \"------------------------------------------------------------------------------\"]\n[38.761744, \"o\", \"\\r\\r\\n\"]\n[38.761989, \"o\", \"Repository: /media/backup/borgdemo\"]\n[38.762153, \"o\", \"\\r\\r\\n\"]\n[38.762386, \"o\", \"Archive name: root-2022-07-06T21:31:12\"]\n[38.762674, \"o\", \"\\r\\r\\n\"]\n[38.762747, \"o\", \"Archive fingerprint: 280fe9e3d92e2e61f04f0ef98de32b4d0a797ec9e3b0b29ddb2b0946c4a0236b\"]\n[38.762885, \"o\", \"\\r\\r\\n\"]\n[38.763095, \"o\", \"Time (start): Wed, 2022-07-06 21:31:12\"]\n[38.763338, \"o\", \"\\r\\r\\n\"]\n[38.763535, \"o\", \"Time (end):   Wed, 2022-07-06 21:31:12\"]\n[38.763775, \"o\", \"\\r\\r\\n\"]\n[38.763962, \"o\", \"Duration: 0.02 seconds\"]\n[38.764202, \"o\", \"\\r\\r\\n\"]\n[38.764463, \"o\", \"Number of files: 32\\r\\r\\n\"]\n[38.764682, \"o\", \"Utilization of max. archive size: 0%\"]\n[38.764874, \"o\", \"\\r\\r\\n\"]\n[38.76508, \"o\", \"------------------------------------------------------------------------------\"]\n[38.765384, \"o\", \"\\r\\r\\n                       Original size      Compressed size    Deduplicated size\"]\n[38.765551, \"o\", \"\\r\\r\\n\"]\n[38.765757, \"o\", \"This archive:              401.15 MB            399.73 MB                542 B\"]\n[38.76595, \"o\", \"\\r\\r\\n\"]\n[38.766157, \"o\", \"All archives:                1.60 GB              1.60 GB            399.57 MB\"]\n[38.766321, \"o\", \"\\r\\r\\n\"]\n[38.766523, \"o\", \"\\r\\r\\n\"]\n[38.766796, \"o\", \"                       Unique chunks         Total chunks\"]\n[38.767025, \"o\", \"\\r\\r\\n\"]\n[38.767238, \"o\", \"Chunk index:                     182                  711\"]\n[38.767399, \"o\", \"\\r\\r\\n\"]\n[38.767602, \"o\", \"------------------------------------------------------------------------------\"]\n[38.767957, \"o\", \"\\r\\r\\n\"]\n[38.815857, \"o\", \"$ #\"]\n[38.923502, \"o\", \" \"]\n[39.027, \"o\", \"N\"]\n[39.043393, \"o\", \"o\"]\n[39.095046, \"o\", \"t\"]\n[39.154595, \"o\", \"i\"]\n[39.167017, \"o\", \"c\"]\n[39.219356, \"o\", \"e\"]\n[39.25099, \"o\", \" \"]\n[39.284613, \"o\", \"t\"]\n[39.336021, \"o\", \"h\"]\n[39.439378, \"o\", \"e\"]\n[39.448858, \"o\", \" \"]\n[39.471101, \"o\", \"b\"]\n[39.480542, \"o\", \"a\"]\n[39.530899, \"o\", \"c\"]\n[39.564044, \"o\", \"k\"]\n[39.606444, \"o\", \"u\"]\n[39.645591, \"o\", \"p\"]\n[39.65497, \"o\", \" \"]\n[39.681416, \"o\", \"n\"]\n[39.772835, \"o\", \"a\"]\n[39.782316, \"o\", \"m\"]\n[39.910022, \"o\", \"e\"]\n[40.02652, \"o\", \".\"]\n[40.199614, \"o\", \"\\r\\n$ \\r\\n$ #\"]\n[40.243003, \"o\", \" \"]\n[40.259433, \"o\", \"A\"]\n[40.282203, \"o\", \"n\"]\n[40.299793, \"o\", \"d\"]\n[40.500115, \"o\", \" \"]\n[40.513573, \"o\", \"w\"]\n[40.555786, \"o\", \"e\"]\n[40.640486, \"o\", \" \"]\n[40.700229, \"o\", \"c\"]\n[40.728957, \"o\", \"a\"]\n[40.929419, \"o\", \"n\"]\n[41.12993, \"o\", \" \"]\n[41.256333, \"o\", \"p\"]\n[41.306777, \"o\", \"u\"]\n[41.352942, \"o\", \"t\"]\n[41.471912, \"o\", \" \"]\n[41.545122, \"o\", \"c\"]\n[41.609493, \"o\", \"o\"]\n[41.630075, \"o\", \"m\"]\n[41.645445, \"o\", \"p\"]\n[41.662961, \"o\", \"l\"]\n[41.714345, \"o\", \"e\"]\n[41.72384, \"o\", \"t\"]\n[41.759022, \"o\", \"e\"]\n[41.773394, \"o\", \"l\"]\n[41.782732, \"o\", \"y\"]\n[41.833383, \"o\", \" \"]\n[41.895001, \"o\", \"d\"]\n[41.904433, \"o\", \"i\"]\n[41.926981, \"o\", \"f\"]\n[41.946971, \"o\", \"f\"]\n[41.979006, \"o\", \"e\"]\n[42.03056, \"o\", \"r\"]\n[42.087944, \"o\", \"e\"]\n[42.123277, \"o\", \"n\"]\n[42.143987, \"o\", \"t\"]\n[42.260474, \"o\", \" \"]\n[42.26997, \"o\", \"d\"]\n[42.391003, \"o\", \"a\"]\n[42.415766, \"o\", \"t\"]\n[42.432303, \"o\", \"a\"]\n[42.63498, \"o\", \",\"]\n[42.644225, \"o\", \" \"]\n[42.827016, \"o\", \"w\"]\n[42.845569, \"o\", \"i\"]\n[42.908977, \"o\", \"t\"]\n[42.964603, \"o\", \"h\"]\n[42.974094, \"o\", \" \"]\n[42.98695, \"o\", \"d\"]\n[43.063018, \"o\", \"i\"]\n[43.072434, \"o\", \"f\"]\n[43.107612, \"o\", \"f\"]\n[43.135052, \"o\", \"e\"]\n[43.14441, \"o\", \"r\"]\n[43.214774, \"o\", \"e\"]\n[43.270106, \"o\", \"n\"]\n[43.27948, \"o\", \"t\"]\n[43.294946, \"o\", \" \"]\n[43.319007, \"o\", \"b\"]\n[43.494978, \"o\", \"a\"]\n[43.511473, \"o\", \"c\"]\n[43.570849, \"o\", \"k\"]\n[43.58022, \"o\", \"u\"]\n[43.639716, \"o\", \"p\"]\n[43.654954, \"o\", \" \"]\n[43.715076, \"o\", \"s\"]\n[43.724678, \"o\", \"e\"]\n[43.815095, \"o\", \"t\"]\n[43.854662, \"o\", \"t\"]\n[43.916752, \"o\", \"i\"]\n[43.929157, \"o\", \"n\"]\n[44.019065, \"o\", \"g\"]\n[44.066708, \"o\", \"s\"]\n[44.078303, \"o\", \",\"]\n[44.159135, \"o\", \" \"]\n[44.216838, \"o\", \"i\"]\n[44.307253, \"o\", \"n\"]\n[44.334797, \"o\", \" \"]\n[44.372032, \"o\", \"o\"]\n[44.381394, \"o\", \"u\"]\n[44.390774, \"o\", \"r\"]\n[44.442241, \"o\", \" \"]\n[44.463309, \"o\", \"b\"]\n[44.473591, \"o\", \"a\"]\n[44.49526, \"o\", \"c\"]\n[44.50448, \"o\", \"k\"]\n[44.602902, \"o\", \"u\"]\n[44.631293, \"o\", \"p\"]\n[44.8315, \"o\", \".\"]\n[44.841097, \"o\", \" \"]\n[44.850531, \"o\", \"I\"]\n[44.866947, \"o\", \"t\"]\n[44.930496, \"o\", \" \"]\n[44.951657, \"o\", \"w\"]\n[45.01905, \"o\", \"i\"]\n[45.067479, \"o\", \"l\"]\n[45.076954, \"o\", \"l\"]\n[45.227241, \"o\", \" \"]\n[45.239817, \"o\", \"b\"]\n[45.26112, \"o\", \"e\"]\n[45.290566, \"o\", \" \"]\n[45.303406, \"o\", \"d\"]\n[45.312421, \"o\", \"e\"]\n[45.363008, \"o\", \"d\"]\n[45.382612, \"o\", \"u\"]\n[45.454977, \"o\", \"p\"]\n[45.474444, \"o\", \"l\"]\n[45.483814, \"o\", \"i\"]\n[45.686687, \"o\", \"c\"]\n[45.734982, \"o\", \"a\"]\n[45.909772, \"o\", \"t\"]\n[46.110364, \"o\", \"e\"]\n[46.119831, \"o\", \"d\"]\n[46.154955, \"o\", \",\"]\n[46.31846, \"o\", \" \"]\n[46.342976, \"o\", \"a\"]\n[46.365482, \"o\", \"n\"]\n[46.451798, \"o\", \"y\"]\n[46.493003, \"o\", \"w\"]\n[46.502493, \"o\", \"a\"]\n[46.548, \"o\", \"y\"]\n[46.618954, \"o\", \":\"]\n[46.645599, \"o\", \"\\r\\n\"]\n[46.646017, \"o\", \"$ b\"]\n[46.737468, \"o\", \"o\"]\n[46.862935, \"o\", \"r\"]\n[46.875431, \"o\", \"g\"]\n[46.979005, \"o\", \" \"]\n[47.038989, \"o\", \"c\"]\n[47.071029, \"o\", \"r\"]\n[47.271454, \"o\", \"e\"]\n[47.423014, \"o\", \"a\"]\n[47.478504, \"o\", \"t\"]\n[47.623367, \"o\", \"e\"]\n[47.667711, \"o\", \" \"]\n[47.868353, \"o\", \"-\"]\n[47.927624, \"o\", \"-\"]\n[47.987025, \"o\", \"s\"]\n[48.115443, \"o\", \"t\"]\n[48.146002, \"o\", \"a\"]\n[48.164393, \"o\", \"t\"]\n[48.275588, \"o\", \"s\"]\n[48.33613, \"o\", \" \"]\n[48.345597, \"o\", \"-\"]\n[48.50091, \"o\", \"-\"]\n[48.511465, \"o\", \"p\"]\n[48.524985, \"o\", \"r\"]\n[48.534442, \"o\", \"o\"]\n[48.543773, \"o\", \"g\"]\n[48.624086, \"o\", \"r\"]\n[48.636604, \"o\", \"e\"]\n[48.662972, \"o\", \"s\"]\n[48.672252, \"o\", \"s\"]\n[48.723547, \"o\", \" \"]\n[48.923935, \"o\", \"-\"]\n[49.043023, \"o\", \"-\"]\n[49.062581, \"o\", \"c\"]\n[49.075033, \"o\", \"o\"]\n[49.095001, \"o\", \"m\"]\n[49.174991, \"o\", \"p\"]\n[49.231004, \"o\", \"r\"]\n[49.240489, \"o\", \"e\"]\n[49.274596, \"o\", \"s\"]\n[49.29506, \"o\", \"s\"]\n[49.375731, \"o\", \"i\"]\n[49.474879, \"o\", \"o\"]\n[49.484391, \"o\", \"n\"]\n[49.551834, \"o\", \" \"]\n[49.591234, \"o\", \"z\"]\n[49.600649, \"o\", \"l\"]\n[49.746253, \"o\", \"i\"]\n[49.795017, \"o\", \"b\"]\n[49.913498, \"o\", \",\"]\n[49.922915, \"o\", \"6\"]\n[49.991003, \"o\", \" \"]\n[50.043696, \"o\", \"-\"]\n[50.05315, \"o\", \"-\"]\n[50.119, \"o\", \"e\"]\n[50.187581, \"o\", \"x\"]\n[50.199004, \"o\", \"c\"]\n[50.216452, \"o\", \"l\"]\n[50.386885, \"o\", \"u\"]\n[50.403456, \"o\", \"d\"]\n[50.427008, \"o\", \"e\"]\n[50.47501, \"o\", \" \"]\n[50.490128, \"o\", \"~\"]\n[50.499528, \"o\", \"/\"]\n[50.557048, \"o\", \"D\"]\n[50.589507, \"o\", \"o\"]\n[50.614943, \"o\", \"w\"]\n[50.646175, \"o\", \"n\"]\n[50.772655, \"o\", \"l\"]\n[50.788527, \"o\", \"o\"]\n[50.96277, \"o\", \"a\"]\n[50.972183, \"o\", \"d\"]\n[50.981668, \"o\", \"s\"]\n[51.159808, \"o\", \"/\"]\n[51.169272, \"o\", \"b\"]\n[51.18588, \"o\", \"i\"]\n[51.195007, \"o\", \"g\"]\n[51.395579, \"o\", \" \"]\n[51.598979, \"o\", \":\"]\n[51.637314, \"o\", \":\"]\n[51.687702, \"o\", \"{\"]\n[51.819, \"o\", \"u\"]\n[51.828341, \"o\", \"s\"]\n[51.844681, \"o\", \"e\"]\n[51.854996, \"o\", \"r\"]\n[51.931009, \"o\", \"}\"]\n[51.940417, \"o\", \"-\"]\n[52.082583, \"o\", \"{\"]\n[52.168797, \"o\", \"n\"]\n[52.24168, \"o\", \"o\"]\n[52.442577, \"o\", \"w\"]\n[52.464135, \"o\", \"}\"]\n[52.473611, \"o\", \" \"]\n[52.483012, \"o\", \"~\"]\n[52.510975, \"o\", \"/\"]\n[52.555894, \"o\", \"D\"]\n[52.571294, \"o\", \"o\"]\n[52.593995, \"o\", \"w\"]\n[52.680131, \"o\", \"n\"]\n[52.690653, \"o\", \"l\"]\n[52.733071, \"o\", \"o\"]\n[52.77442, \"o\", \"a\"]\n[52.97502, \"o\", \"d\"]\n[52.999044, \"o\", \"s\"]\n[53.019388, \"o\", \"\\r\\n\"]\n[53.963521, \"o\", \"0 B O 0 B C 0 B D 0 N root/Downloads                                            \\r\"]\n[53.964284, \"o\", \"                                                                                \\r\"]\n[53.964921, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[53.965567, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[53.966217, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[53.96692, \"o\", \"                                                                                \\r\"]\n[53.982311, \"o\", \"Saving files cache                                                              \\r\"]\n[53.983834, \"o\", \"Saving chunks cache                                                             \\r\"]\n[53.984584, \"o\", \"Saving cache config                                                             \\r\"]\n[53.986341, \"o\", \"                                                                                \\r\"]\n[53.989445, \"o\", \"------------------------------------------------------------------------------\"]\n[53.989754, \"o\", \"\\r\\r\\n\"]\n[53.990138, \"o\", \"Repository: /media/backup/borgdemo\"]\n[53.990438, \"o\", \"\\r\\r\\n\"]\n[53.990824, \"o\", \"Archive name: root-2022-07-06T21:31:28\"]\n[53.991138, \"o\", \"\\r\\r\\n\"]\n[53.991518, \"o\", \"Archive fingerprint: d292c218f6c32423a9de2e3fbdf51c8ee865dc83ac78f62624216ebdae7ca55e\"]\n[53.991813, \"o\", \"\\r\\r\\n\"]\n[53.992184, \"o\", \"Time (start): Wed, 2022-07-06 21:31:28\"]\n[53.992478, \"o\", \"\\r\\r\\n\"]\n[53.992851, \"o\", \"Time (end):   Wed, 2022-07-06 21:31:28\"]\n[53.99314, \"o\", \"\\r\\r\\n\"]\n[53.993512, \"o\", \"Duration: 0.01 seconds\"]\n[53.993812, \"o\", \"\\r\\r\\n\"]\n[53.99419, \"o\", \"Number of files: 0\"]\n[53.994479, \"o\", \"\\r\\r\\n\"]\n[53.994893, \"o\", \"Utilization of max. archive size: 0%\"]\n[53.995216, \"o\", \"\\r\\r\\n\"]\n[53.99559, \"o\", \"------------------------------------------------------------------------------\"]\n[53.995881, \"o\", \"\\r\\r\\n\"]\n[53.996319, \"o\", \"                       Original size      Compressed size    Deduplicated size\"]\n[53.99661, \"o\", \"\\r\\r\\n\"]\n[53.996989, \"o\", \"This archive:                  576 B                549 B                549 B\"]\n[53.997279, \"o\", \"\\r\\r\\n\"]\n[53.99766, \"o\", \"All archives:                1.60 GB              1.60 GB            399.57 MB\"]\n[53.997951, \"o\", \"\\r\\r\\n\"]\n[53.998329, \"o\", \"\\r\\r\\n\"]\n[53.998748, \"o\", \"                       Unique chunks         Total chunks\"]\n[53.999041, \"o\", \"\\r\\r\\n\"]\n[53.999436, \"o\", \"Chunk index:                     184                  713\"]\n[53.999731, \"o\", \"\\r\\r\\n\"]\n[54.000115, \"o\", \"------------------------------------------------------------------------------\"]\n[54.000409, \"o\", \"\\r\\r\\n\"]\n[54.049234, \"o\", \"$ \\r\\n\"]\n[54.049753, \"o\", \"$ #\"]\n[54.068119, \"o\", \" \"]\n[54.07753, \"o\", \"O\"]\n[54.086991, \"o\", \"r\"]\n[54.18121, \"o\", \" \"]\n[54.24303, \"o\", \"l\"]\n[54.342987, \"o\", \"e\"]\n[54.371024, \"o\", \"t\"]\n[54.513575, \"o\", \"'\"]\n[54.569014, \"o\", \"s\"]\n[54.769663, \"o\", \" \"]\n[54.847339, \"o\", \"b\"]\n[54.872046, \"o\", \"a\"]\n[54.942801, \"o\", \"c\"]\n[54.962407, \"o\", \"k\"]\n[54.985108, \"o\", \"u\"]\n[55.129751, \"o\", \"p\"]\n[55.191004, \"o\", \" \"]\n[55.200452, \"o\", \"a\"]\n[55.401184, \"o\", \" \"]\n[55.485684, \"o\", \"d\"]\n[55.515004, \"o\", \"e\"]\n[55.533494, \"o\", \"v\"]\n[55.585263, \"o\", \"i\"]\n[55.608998, \"o\", \"c\"]\n[55.661538, \"o\", \"e\"]\n[55.684062, \"o\", \" \"]\n[55.706779, \"o\", \"v\"]\n[55.718956, \"o\", \"i\"]\n[55.741305, \"o\", \"a\"]\n[55.774956, \"o\", \" \"]\n[55.784238, \"o\", \"S\"]\n[55.81504, \"o\", \"T\"]\n[55.834466, \"o\", \"D\"]\n[55.843769, \"o\", \"I\"]\n[55.930083, \"o\", \"N\"]\n[56.09109, \"o\", \".\"]\n[56.25442, \"o\", \"\\r\\n$ s\"]\n[56.27403, \"o\", \"u\"]\n[56.32435, \"o\", \"d\"]\n[56.333722, \"o\", \"o\"]\n[56.535107, \"o\", \" \"]\n[56.611262, \"o\", \"d\"]\n[56.669831, \"o\", \"d\"]\n[56.695163, \"o\", \" \"]\n[56.822363, \"o\", \"i\"]\n[56.848108, \"o\", \"f\"]\n[56.876998, \"o\", \"=\"]\n[56.886385, \"o\", \"/\"]\n[56.90777, \"o\", \"d\"]\n[56.922055, \"o\", \"e\"]\n[56.956174, \"o\", \"v\"]\n[57.06045, \"o\", \"/\"]\n[57.114878, \"o\", \"l\"]\n[57.166361, \"o\", \"o\"]\n[57.187001, \"o\", \"o\"]\n[57.237174, \"o\", \"p\"]\n[57.246641, \"o\", \"0\"]\n[57.256116, \"o\", \" \"]\n[57.329709, \"o\", \"b\"]\n[57.339104, \"o\", \"s\"]\n[57.353578, \"o\", \"=\"]\n[57.551207, \"o\", \"1\"]\n[57.562974, \"o\", \"0\"]\n[57.584427, \"o\", \"M\"]\n[57.751007, \"o\", \" \"]\n[57.884754, \"o\", \"|\"]\n[57.894982, \"o\", \" \"]\n[57.978933, \"o\", \"b\"]\n[57.993342, \"o\", \"o\"]\n[58.02676, \"o\", \"r\"]\n[58.198989, \"o\", \"g\"]\n[58.399364, \"o\", \" \"]\n[58.414833, \"o\", \"c\"]\n[58.442568, \"o\", \"r\"]\n[58.454953, \"o\", \"e\"]\n[58.46519, \"o\", \"a\"]\n[58.55284, \"o\", \"t\"]\n[58.593169, \"o\", \"e\"]\n[58.624302, \"o\", \" \"]\n[58.765138, \"o\", \"-\"]\n[58.847729, \"o\", \"-\"]\n[58.886836, \"o\", \"p\"]\n[58.931277, \"o\", \"r\"]\n[58.944868, \"o\", \"o\"]\n[59.098028, \"o\", \"g\"]\n[59.1514, \"o\", \"r\"]\n[59.25589, \"o\", \"e\"]\n[59.423182, \"o\", \"s\"]\n[59.48791, \"o\", \"s\"]\n[59.591304, \"o\", \" \"]\n[59.666137, \"o\", \"-\"]\n[59.735141, \"o\", \"-\"]\n[59.764108, \"o\", \"s\"]\n[59.883644, \"o\", \"t\"]\n[59.984319, \"o\", \"a\"]\n[59.995915, \"o\", \"t\"]\n[60.087205, \"o\", \"s\"]\n[60.288136, \"o\", \" \"]\n[60.297505, \"o\", \":\"]\n[60.398272, \"o\", \":\"]\n[60.409597, \"o\", \"s\"]\n[60.478088, \"o\", \"p\"]\n[60.555086, \"o\", \"e\"]\n[60.587356, \"o\", \"c\"]\n[60.596772, \"o\", \"i\"]\n[60.650928, \"o\", \"a\"]\n[60.683017, \"o\", \"l\"]\n[60.75472, \"o\", \"b\"]\n[60.767219, \"o\", \"a\"]\n[60.80299, \"o\", \"c\"]\n[60.863321, \"o\", \"k\"]\n[60.942935, \"o\", \"u\"]\n[61.021254, \"o\", \"p\"]\n[61.030747, \"o\", \" \"]\n[61.040178, \"o\", \"-\"]\n[61.111171, \"o\", \"\\r\\n\"]\n[62.112352, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[62.112873, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[62.113327, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[62.113941, \"o\", \"                                                                                \\r\"]\n[62.125566, \"o\", \"8.39 MB O 32.95 kB C 32.95 kB D 0 N stdin                                       \\r\"]\n[62.360058, \"o\", \"67.11 MB O 263.60 kB C 32.95 kB D 0 N stdin                                     \\r\"]\n[62.576735, \"o\", \"125.83 MB O 494.25 kB C 32.95 kB D 0 N stdin                                    \\r\"]\n[62.800828, \"o\", \"184.55 MB O 724.90 kB C 32.95 kB D 0 N stdin                                    \\r\"]\n[63.001059, \"o\", \"234.88 MB O 922.60 kB C 32.95 kB D 0 N stdin                                    \\r\"]\n[63.212902, \"o\", \"285.21 MB O 1.12 MB C 32.95 kB D 0 N stdin                                      \\r\"]\n[63.433125, \"o\", \"335.54 MB O 1.32 MB C 32.95 kB D 0 N stdin                                      \\r\"]\n[63.636129, \"o\", \"385.88 MB O 1.52 MB C 32.95 kB D 0 N stdin                                      \\r\"]\n[63.787663, \"o\", \"40+0 records in\\r\\r\\n40+0 records out\\r\\r\\n419430400 bytes (419 MB, 400 MiB) copied, 2.66153 s, 158 MB/s\\r\\r\\n\"]\n[63.795318, \"o\", \"                                                                                \\r\"]\n[63.815459, \"o\", \"Saving files cache                                                              \\r\"]\n[63.817002, \"o\", \"Saving chunks cache                                                             \\r\"]\n[63.817775, \"o\", \"Saving cache config                                                             \\r\"]\n[63.820066, \"o\", \"                                                                                \\r\"]\n[63.829239, \"o\", \"------------------------------------------------------------------------------\\r\\r\\nRepository: /media/backup/borgdemo\\r\\r\\nArchive name: specialbackup\\r\\r\\nArchive fingerprint: 05e096b07e9031a5d8527f2a4014717c22d9d9005554c6867b608d0f0f670561\\r\\r\\nTime (start): Wed, 2022-07-06 21:31:36\\r\\r\\nTime (end):   Wed, 2022-07-06 21:31:38\\r\\r\\n\"]\n[63.829424, \"o\", \"Duration: 1.74 seconds\\r\\r\\n\"]\n[63.830396, \"o\", \"Number of files: 1\\r\\r\\nUtilization of max. archive size: 0%\\r\\r\\n------------------------------------------------------------------------------\\r\\r\\n                       Original size      Compressed size    Deduplicated size\\r\\r\\nThis archive:              419.43 MB              1.65 MB             33.47 kB\\r\\r\\nAll archives:                2.02 GB              1.60 GB            399.61 MB\\r\\r\\n\\r\\r\\n                       Unique chunks         Total chunks\\r\\r\\n\"]\n[63.830571, \"o\", \"Chunk index:                     187                  765\\r\\r\\n\"]\n[63.831243, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[63.876336, \"o\", \"$ \\r\\n\"]\n[63.876652, \"o\", \"$ #\"]\n[63.910702, \"o\", \" \"]\n[63.92204, \"o\", \"L\"]\n[63.93149, \"o\", \"e\"]\n[63.947129, \"o\", \"t\"]\n[64.147375, \"o\", \"'\"]\n[64.181549, \"o\", \"s\"]\n[64.19927, \"o\", \" \"]\n[64.285772, \"o\", \"c\"]\n[64.317175, \"o\", \"o\"]\n[64.390987, \"o\", \"n\"]\n[64.430118, \"o\", \"t\"]\n[64.462988, \"o\", \"i\"]\n[64.489445, \"o\", \"n\"]\n[64.55958, \"o\", \"u\"]\n[64.759862, \"o\", \"e\"]\n[64.960077, \"o\", \" \"]\n[64.969536, \"o\", \"w\"]\n[65.097873, \"o\", \"i\"]\n[65.145536, \"o\", \"t\"]\n[65.175076, \"o\", \"h\"]\n[65.291736, \"o\", \" \"]\n[65.337115, \"o\", \"s\"]\n[65.397665, \"o\", \"o\"]\n[65.407179, \"o\", \"m\"]\n[65.435062, \"o\", \"e\"]\n[65.503335, \"o\", \" \"]\n[65.518915, \"o\", \"s\"]\n[65.530115, \"o\", \"i\"]\n[65.543504, \"o\", \"m\"]\n[65.606961, \"o\", \"p\"]\n[65.660102, \"o\", \"l\"]\n[65.693965, \"o\", \"e\"]\n[65.895054, \"o\", \" \"]\n[65.965676, \"o\", \"t\"]\n[66.034322, \"o\", \"h\"]\n[66.139148, \"o\", \"i\"]\n[66.148314, \"o\", \"n\"]\n[66.270932, \"o\", \"g\"]\n[66.302594, \"o\", \"s\"]\n[66.371076, \"o\", \":\"]\n[66.400889, \"o\", \"\\r\\n$ #\"]\n[66.466516, \"o\", \"#\"]\n[66.562937, \"o\", \" \"]\n[66.601084, \"o\", \"U\"]\n[66.664675, \"o\", \"S\"]\n[66.747098, \"o\", \"E\"]\n[66.765397, \"o\", \"F\"]\n[66.817143, \"o\", \"U\"]\n[66.827826, \"o\", \"L\"]\n[66.903094, \"o\", \" \"]\n[66.912246, \"o\", \"C\"]\n[66.991072, \"o\", \"O\"]\n[67.004464, \"o\", \"M\"]\n[67.01897, \"o\", \"M\"]\n[67.167193, \"o\", \"A\"]\n[67.231511, \"o\", \"N\"]\n[67.246067, \"o\", \"D\"]\n[67.255524, \"o\", \"S\"]\n[67.391135, \"o\", \" \"]\n[67.545458, \"o\", \"#\"]\n[67.590966, \"o\", \"#\"]\n[67.604457, \"o\", \"\\r\\n$ #\"]\n[67.613944, \"o\", \" \"]\n[67.800648, \"o\", \"Y\"]\n[67.83914, \"o\", \"o\"]\n[67.848506, \"o\", \"u\"]\n[67.954889, \"o\", \" \"]\n[68.026487, \"o\", \"c\"]\n[68.050923, \"o\", \"a\"]\n[68.060401, \"o\", \"n\"]\n[68.136499, \"o\", \" \"]\n[68.145993, \"o\", \"s\"]\n[68.211723, \"o\", \"h\"]\n[68.302823, \"o\", \"o\"]\n[68.391106, \"o\", \"w\"]\n[68.427221, \"o\", \" \"]\n[68.436687, \"o\", \"s\"]\n[68.450242, \"o\", \"o\"]\n[68.487072, \"o\", \"m\"]\n[68.54245, \"o\", \"e\"]\n[68.584131, \"o\", \" \"]\n[68.593457, \"o\", \"i\"]\n[68.711179, \"o\", \"n\"]\n[68.734, \"o\", \"f\"]\n[68.93436, \"o\", \"o\"]\n[68.963047, \"o\", \"r\"]\n[68.987598, \"o\", \"m\"]\n[69.109977, \"o\", \"a\"]\n[69.11933, \"o\", \"t\"]\n[69.142931, \"o\", \"i\"]\n[69.176318, \"o\", \"o\"]\n[69.199092, \"o\", \"n\"]\n[69.399296, \"o\", \" \"]\n[69.44696, \"o\", \"a\"]\n[69.456119, \"o\", \"b\"]\n[69.47174, \"o\", \"o\"]\n[69.505723, \"o\", \"u\"]\n[69.542938, \"o\", \"t\"]\n[69.552441, \"o\", \" \"]\n[69.654686, \"o\", \"a\"]\n[69.680471, \"o\", \"n\"]\n[69.720608, \"o\", \" \"]\n[69.921506, \"o\", \"a\"]\n[69.93091, \"o\", \"r\"]\n[69.940379, \"o\", \"c\"]\n[69.983696, \"o\", \"h\"]\n[69.995146, \"o\", \"i\"]\n[70.00694, \"o\", \"v\"]\n[70.052253, \"o\", \"e\"]\n[70.173065, \"o\", \".\"]\n[70.209802, \"o\", \" \"]\n[70.344882, \"o\", \"Y\"]\n[70.38098, \"o\", \"o\"]\n[70.391455, \"o\", \"u\"]\n[70.400885, \"o\", \" \"]\n[70.436288, \"o\", \"c\"]\n[70.479658, \"o\", \"a\"]\n[70.511128, \"o\", \"n\"]\n[70.532543, \"o\", \" \"]\n[70.564681, \"o\", \"e\"]\n[70.605056, \"o\", \"v\"]\n[70.663093, \"o\", \"e\"]\n[70.710312, \"o\", \"n\"]\n[70.746508, \"o\", \" \"]\n[70.75596, \"o\", \"d\"]\n[70.789044, \"o\", \"o\"]\n[70.874953, \"o\", \" \"]\n[70.8951, \"o\", \"i\"]\n[70.904455, \"o\", \"t\"]\n[70.951104, \"o\", \" \"]\n[70.960345, \"o\", \"w\"]\n[70.990959, \"o\", \"i\"]\n[71.049385, \"o\", \"t\"]\n[71.178649, \"o\", \"h\"]\n[71.199039, \"o\", \"o\"]\n[71.247286, \"o\", \"u\"]\n[71.263928, \"o\", \"t\"]\n[71.341404, \"o\", \" \"]\n[71.400933, \"o\", \"n\"]\n[71.504487, \"o\", \"e\"]\n[71.513992, \"o\", \"e\"]\n[71.702213, \"o\", \"d\"]\n[71.745271, \"o\", \"i\"]\n[71.78307, \"o\", \"n\"]\n[71.792346, \"o\", \"g\"]\n[71.846181, \"o\", \" \"]\n[71.927444, \"o\", \"t\"]\n[71.963354, \"o\", \"o\"]\n[72.167077, \"o\", \" \"]\n[72.367322, \"o\", \"s\"]\n[72.567847, \"o\", \"p\"]\n[72.578326, \"o\", \"e\"]\n[72.608109, \"o\", \"c\"]\n[72.63193, \"o\", \"i\"]\n[72.666017, \"o\", \"f\"]\n[72.681392, \"o\", \"y\"]\n[72.880734, \"o\", \" \"]\n[72.891089, \"o\", \"t\"]\n[72.911506, \"o\", \"h\"]\n[72.964726, \"o\", \"e\"]\n[73.022153, \"o\", \" \"]\n[73.05761, \"o\", \"a\"]\n[73.067139, \"o\", \"r\"]\n[73.178498, \"o\", \"c\"]\n[73.191079, \"o\", \"h\"]\n[73.201342, \"o\", \"i\"]\n[73.237445, \"o\", \"v\"]\n[73.254932, \"o\", \"e\"]\n[73.324386, \"o\", \" \"]\n[73.51517, \"o\", \"n\"]\n[73.627725, \"o\", \"a\"]\n[73.663138, \"o\", \"m\"]\n[73.7273, \"o\", \"e\"]\n[73.790958, \"o\", \":\"]\n[73.872668, \"o\", \"\\r\\n\"]\n[73.873096, \"o\", \"$ b\"]\n[73.926405, \"o\", \"o\"]\n[73.949729, \"o\", \"r\"]\n[73.958792, \"o\", \"g\"]\n[73.993115, \"o\", \" \"]\n[74.002344, \"o\", \"i\"]\n[74.011562, \"o\", \"n\"]\n[74.043878, \"o\", \"f\"]\n[74.088185, \"o\", \"o\"]\n[74.121451, \"o\", \" \"]\n[74.310737, \"o\", \":\"]\n[74.359159, \"o\", \":\"]\n[74.368338, \"o\", \" \"]\n[74.453642, \"o\", \"-\"]\n[74.615093, \"o\", \"-\"]\n[74.698272, \"o\", \"l\"]\n[74.707464, \"o\", \"a\"]\n[74.720773, \"o\", \"s\"]\n[74.776057, \"o\", \"t\"]\n[74.976332, \"o\", \" \"]\n[74.987519, \"o\", \"1\"]\n[75.085858, \"o\", \"\\r\\n\"]\n[76.043124, \"o\", \"Archive name: specialbackup\\r\\r\\n\"]\n[76.043213, \"o\", \"Archive fingerprint: 05e096b07e9031a5d8527f2a4014717c22d9d9005554c6867b608d0f0f670561\\r\\r\\n\"]\n[76.043561, \"o\", \"Comment: \\r\\r\\nHostname: bullseye\\r\\r\\n\"]\n[76.043782, \"o\", \"Username: root\"]\n[76.043834, \"o\", \"\\r\\r\\n\"]\n[76.04424, \"o\", \"Time (start): Wed, 2022-07-06 21:31:36\\r\\r\\n\"]\n[76.044293, \"o\", \"Time (end): Wed, 2022-07-06 21:31:38\\r\\r\\n\"]\n[76.044483, \"o\", \"Duration: 1.74 seconds\"]\n[76.044742, \"o\", \"\\r\\r\\n\"]\n[76.044782, \"o\", \"Number of files: 1\"]\n[76.04487, \"o\", \"\\r\\r\\n\"]\n[76.045065, \"o\", \"Command line: borg create --progress --stats ::specialbackup -\"]\n[76.045356, \"o\", \"\\r\\r\\n\"]\n[76.045395, \"o\", \"Utilization of maximum supported archive size: 0%\"]\n[76.045614, \"o\", \"\\r\\r\\n\"]\n[76.045653, \"o\", \"------------------------------------------------------------------------------\"]\n[76.045739, \"o\", \"\\r\\r\\n\"]\n[76.046056, \"o\", \"                       Original size      Compressed size    Deduplicated size\"]\n[76.046095, \"o\", \"\\r\\r\\n\"]\n[76.046303, \"o\", \"This archive:              419.43 MB              1.65 MB             33.67 kB\"]\n[76.046342, \"o\", \"\\r\\r\\n\"]\n[76.046558, \"o\", \"All archives:                2.02 GB              1.60 GB            399.61 MB\"]\n[76.046597, \"o\", \"\\r\\r\\n\"]\n[76.046854, \"o\", \"\\r\\r\\n\"]\n[76.046895, \"o\", \"                       Unique chunks         Total chunks\"]\n[76.047217, \"o\", \"\\r\\r\\n\"]\n[76.047276, \"o\", \"Chunk index:                     187                  765\"]\n[76.047573, \"o\", \"\\r\\r\\n\"]\n[76.101724, \"o\", \"$ \\r\\n\"]\n[76.10207, \"o\", \"$ #\"]\n[76.110849, \"o\", \" \"]\n[76.25529, \"o\", \"S\"]\n[76.27759, \"o\", \"o\"]\n[76.379077, \"o\", \" \"]\n[76.40714, \"o\", \"l\"]\n[76.452484, \"o\", \"e\"]\n[76.46365, \"o\", \"t\"]\n[76.501988, \"o\", \"'\"]\n[76.516262, \"o\", \"s\"]\n[76.604546, \"o\", \" \"]\n[76.618853, \"o\", \"r\"]\n[76.637128, \"o\", \"e\"]\n[76.702425, \"o\", \"n\"]\n[76.71163, \"o\", \"a\"]\n[76.789956, \"o\", \"m\"]\n[76.799176, \"o\", \"e\"]\n[76.859403, \"o\", \" \"]\n[76.932679, \"o\", \"o\"]\n[76.965959, \"o\", \"u\"]\n[76.989297, \"o\", \"r\"]\n[77.033601, \"o\", \" \"]\n[77.063032, \"o\", \"l\"]\n[77.073168, \"o\", \"a\"]\n[77.09649, \"o\", \"s\"]\n[77.12679, \"o\", \"t\"]\n[77.224094, \"o\", \" \"]\n[77.233311, \"o\", \"a\"]\n[77.287591, \"o\", \"r\"]\n[77.3018, \"o\", \"c\"]\n[77.311098, \"o\", \"h\"]\n[77.320311, \"o\", \"i\"]\n[77.343514, \"o\", \"v\"]\n[77.411844, \"o\", \"e\"]\n[77.442132, \"o\", \":\"]\n[77.518449, \"o\", \"\\r\\n\"]\n[77.518821, \"o\", \"$ b\"]\n[77.568049, \"o\", \"o\"]\n[77.640406, \"o\", \"r\"]\n[77.655604, \"o\", \"g\"]\n[77.76192, \"o\", \" \"]\n[77.771152, \"o\", \"r\"]\n[77.80733, \"o\", \"e\"]\n[77.977601, \"o\", \"n\"]\n[78.025855, \"o\", \"a\"]\n[78.082105, \"o\", \"m\"]\n[78.153352, \"o\", \"e\"]\n[78.278704, \"o\", \" \"]\n[78.393852, \"o\", \":\"]\n[78.411165, \"o\", \":\"]\n[78.422273, \"o\", \"s\"]\n[78.437519, \"o\", \"p\"]\n[78.464756, \"o\", \"e\"]\n[78.491126, \"o\", \"c\"]\n[78.50326, \"o\", \"i\"]\n[78.58451, \"o\", \"a\"]\n[78.685851, \"o\", \"l\"]\n[78.779088, \"o\", \"b\"]\n[78.829332, \"o\", \"a\"]\n[78.867606, \"o\", \"c\"]\n[78.876858, \"o\", \"k\"]\n[78.905176, \"o\", \"u\"]\n[78.933486, \"o\", \"p\"]\n[78.960769, \"o\", \" \"]\n[78.971173, \"o\", \"b\"]\n[79.002474, \"o\", \"a\"]\n[79.081807, \"o\", \"c\"]\n[79.091086, \"o\", \"k\"]\n[79.190364, \"o\", \"u\"]\n[79.231594, \"o\", \"p\"]\n[79.282855, \"o\", \"-\"]\n[79.319118, \"o\", \"b\"]\n[79.376382, \"o\", \"l\"]\n[79.434706, \"o\", \"o\"]\n[79.448864, \"o\", \"c\"]\n[79.467102, \"o\", \"k\"]\n[79.512354, \"o\", \"-\"]\n[79.58843, \"o\", \"d\"]\n[79.667638, \"o\", \"e\"]\n[79.682887, \"o\", \"v\"]\n[79.778153, \"o\", \"i\"]\n[79.850387, \"o\", \"c\"]\n[79.888649, \"o\", \"e\"]\n[79.92093, \"o\", \"\\r\\n\"]\n[80.929653, \"o\", \"$ \\r\\n\"]\n[80.930152, \"o\", \"$ b\"]\n[81.026102, \"o\", \"o\"]\n[81.110369, \"o\", \"r\"]\n[81.134711, \"o\", \"g\"]\n[81.201937, \"o\", \" \"]\n[81.21108, \"o\", \"i\"]\n[81.225431, \"o\", \"n\"]\n[81.35772, \"o\", \"f\"]\n[81.377012, \"o\", \"o\"]\n[81.407296, \"o\", \" \"]\n[81.513619, \"o\", \":\"]\n[81.591079, \"o\", \":\"]\n[81.600193, \"o\", \" \"]\n[81.60944, \"o\", \"-\"]\n[81.661749, \"o\", \"-\"]\n[81.686048, \"o\", \"l\"]\n[81.71232, \"o\", \"a\"]\n[81.852605, \"o\", \"s\"]\n[81.874732, \"o\", \"t\"]\n[82.0751, \"o\", \" \"]\n[82.085265, \"o\", \"1\"]\n[82.101556, \"o\", \"\\r\\n\"]\n[83.06916, \"o\", \"Archive name: backup-block-device\\r\\r\\nArchive fingerprint: c416cbf604ab7dce6512f4d48cb9ea3fc56e311d9c794e2ec0814703a7561551\\r\\r\\nComment: \\r\\r\\nHostname: bullseye\\r\\r\\nUsername: root\\r\\r\\nTime (start): Wed, 2022-07-06 21:31:36\\r\\r\\nTime (end): Wed, 2022-07-06 21:31:38\\r\\r\\nDuration: 1.74 seconds\\r\\r\\nNumber of files: 1\\r\\r\\nCommand line: borg create --progress --stats ::specialbackup -\\r\\r\\nUtilization of maximum supported archive size: 0%\\r\\r\\n------------------------------------------------------------------------------\\r\\r\\n                       Original size      Compressed size    Deduplicated size\\r\\r\\nThis archive:              419.43 MB              1.65 MB             33.69 kB\\r\\r\\nAll archives:                2.02 GB              1.60 GB            399.61 MB\\r\\r\\n\\r\\r\\n                       Unique chunks         Total chunks\\r\\r\\nChunk index:                     187                  765\"]\n[83.069711, \"o\", \"\\r\\r\\n\"]\n[83.116244, \"o\", \"$ \"]\n[83.116346, \"o\", \"\\r\\n\"]\n[83.117641, \"o\", \"$ #\"]\n[83.180969, \"o\", \" \"]\n[83.197222, \"o\", \"A\"]\n[83.373416, \"o\", \" \"]\n[83.387658, \"o\", \"v\"]\n[83.431926, \"o\", \"e\"]\n[83.537164, \"o\", \"r\"]\n[83.548425, \"o\", \"y\"]\n[83.561647, \"o\", \" \"]\n[83.627053, \"o\", \"i\"]\n[83.63615, \"o\", \"m\"]\n[83.671335, \"o\", \"p\"]\n[83.871559, \"o\", \"o\"]\n[83.880773, \"o\", \"r\"]\n[83.918114, \"o\", \"t\"]\n[83.976375, \"o\", \"a\"]\n[83.989643, \"o\", \"n\"]\n[84.067881, \"o\", \"t\"]\n[84.152154, \"o\", \" \"]\n[84.161334, \"o\", \"s\"]\n[84.255576, \"o\", \"t\"]\n[84.299837, \"o\", \"e\"]\n[84.329092, \"o\", \"p\"]\n[84.356372, \"o\", \" \"]\n[84.429601, \"o\", \"i\"]\n[84.438781, \"o\", \"f\"]\n[84.44806, \"o\", \" \"]\n[84.499307, \"o\", \"y\"]\n[84.508554, \"o\", \"o\"]\n[84.5178, \"o\", \"u\"]\n[84.623066, \"o\", \" \"]\n[84.632231, \"o\", \"c\"]\n[84.69749, \"o\", \"h\"]\n[84.757754, \"o\", \"o\"]\n[84.81601, \"o\", \"o\"]\n[84.885254, \"o\", \"s\"]\n[84.901535, \"o\", \"e\"]\n[84.984793, \"o\", \" \"]\n[85.13606, \"o\", \"k\"]\n[85.162302, \"o\", \"e\"]\n[85.171486, \"o\", \"y\"]\n[85.345714, \"o\", \"f\"]\n[85.40499, \"o\", \"i\"]\n[85.414189, \"o\", \"l\"]\n[85.518496, \"o\", \"e\"]\n[85.543521, \"o\", \" \"]\n[85.612764, \"o\", \"m\"]\n[85.621973, \"o\", \"o\"]\n[85.698247, \"o\", \"d\"]\n[85.717486, \"o\", \"e\"]\n[85.917778, \"o\", \" \"]\n[85.96802, \"o\", \"(\"]\n[86.024313, \"o\", \"w\"]\n[86.033549, \"o\", \"h\"]\n[86.084714, \"o\", \"e\"]\n[86.097024, \"o\", \"r\"]\n[86.106266, \"o\", \"e\"]\n[86.235495, \"o\", \" \"]\n[86.284754, \"o\", \"t\"]\n[86.293941, \"o\", \"h\"]\n[86.328215, \"o\", \"e\"]\n[86.337408, \"o\", \" \"]\n[86.346629, \"o\", \"k\"]\n[86.355891, \"o\", \"e\"]\n[86.425154, \"o\", \"y\"]\n[86.456417, \"o\", \"f\"]\n[86.525668, \"o\", \"i\"]\n[86.534847, \"o\", \"l\"]\n[86.617122, \"o\", \"e\"]\n[86.738378, \"o\", \" \"]\n[86.750643, \"o\", \"i\"]\n[86.85383, \"o\", \"s\"]\n[86.912086, \"o\", \" \"]\n[86.969355, \"o\", \"o\"]\n[87.106606, \"o\", \"n\"]\n[87.115827, \"o\", \"l\"]\n[87.172088, \"o\", \"y\"]\n[87.232346, \"o\", \" \"]\n[87.242557, \"o\", \"s\"]\n[87.272829, \"o\", \"a\"]\n[87.312082, \"o\", \"v\"]\n[87.412313, \"o\", \"e\"]\n[87.511544, \"o\", \"d\"]\n[87.711795, \"o\", \" \"]\n[87.781041, \"o\", \"l\"]\n[87.816298, \"o\", \"o\"]\n[87.828552, \"o\", \"c\"]\n[87.904814, \"o\", \"a\"]\n[88.105062, \"o\", \"l\"]\n[88.152317, \"o\", \"l\"]\n[88.287535, \"o\", \"y\"]\n[88.356789, \"o\", \")\"]\n[88.412023, \"o\", \" \"]\n[88.555231, \"o\", \"i\"]\n[88.570459, \"o\", \"s\"]\n[88.624724, \"o\", \" \"]\n[88.634834, \"o\", \"t\"]\n[88.644101, \"o\", \"o\"]\n[88.669362, \"o\", \" \"]\n[88.869623, \"o\", \"e\"]\n[88.933856, \"o\", \"x\"]\n[88.963068, \"o\", \"p\"]\n[89.062317, \"o\", \"o\"]\n[89.199564, \"o\", \"r\"]\n[89.31488, \"o\", \"t\"]\n[89.399071, \"o\", \" \"]\n[89.408282, \"o\", \"y\"]\n[89.451519, \"o\", \"o\"]\n[89.460707, \"o\", \"u\"]\n[89.660976, \"o\", \"r\"]\n[89.670172, \"o\", \" \"]\n[89.691403, \"o\", \"k\"]\n[89.740641, \"o\", \"e\"]\n[89.795877, \"o\", \"y\"]\n[89.816131, \"o\", \"f\"]\n[89.83932, \"o\", \"i\"]\n[89.848581, \"o\", \"l\"]\n[89.867845, \"o\", \"e\"]\n[90.068096, \"o\", \" \"]\n[90.077304, \"o\", \"a\"]\n[90.219558, \"o\", \"n\"]\n[90.317806, \"o\", \"d\"]\n[90.376045, \"o\", \" \"]\n[90.423241, \"o\", \"p\"]\n[90.465504, \"o\", \"o\"]\n[90.527757, \"o\", \"s\"]\n[90.536949, \"o\", \"s\"]\n[90.607156, \"o\", \"i\"]\n[90.642411, \"o\", \"b\"]\n[90.651607, \"o\", \"l\"]\n[90.68693, \"o\", \"y\"]\n[90.88709, \"o\", \" \"]\n[90.919294, \"o\", \"p\"]\n[90.942543, \"o\", \"r\"]\n[91.078836, \"o\", \"i\"]\n[91.102079, \"o\", \"n\"]\n[91.246364, \"o\", \"t\"]\n[91.446583, \"o\", \" \"]\n[91.511009, \"o\", \"i\"]\n[91.578199, \"o\", \"t\"]\n[91.704476, \"o\", \",\"]\n[91.741724, \"o\", \" \"]\n[91.750942, \"o\", \"e\"]\n[91.771117, \"o\", \"t\"]\n[91.782313, \"o\", \"c\"]\n[91.803545, \"o\", \".\"]\n[91.865929, \"o\", \"\\r\\n\"]\n[91.866312, \"o\", \"$ b\"]\n[91.902524, \"o\", \"o\"]\n[91.929281, \"o\", \"r\"]\n[91.938795, \"o\", \"g\"]\n[92.001495, \"o\", \" \"]\n[92.019245, \"o\", \"k\"]\n[92.055794, \"o\", \"e\"]\n[92.148098, \"o\", \"y\"]\n[92.243444, \"o\", \" \"]\n[92.252817, \"o\", \"e\"]\n[92.304265, \"o\", \"x\"]\n[92.313724, \"o\", \"p\"]\n[92.330183, \"o\", \"o\"]\n[92.385758, \"o\", \"r\"]\n[92.438182, \"o\", \"t\"]\n[92.470969, \"o\", \" \"]\n[92.595227, \"o\", \"-\"]\n[92.674856, \"o\", \"-\"]\n[92.687324, \"o\", \"q\"]\n[92.707309, \"o\", \"r\"]\n[92.778481, \"o\", \"-\"]\n[92.920862, \"o\", \"h\"]\n[92.999202, \"o\", \"t\"]\n[93.008599, \"o\", \"m\"]\n[93.062016, \"o\", \"l\"]\n[93.071322, \"o\", \" \"]\n[93.080847, \"o\", \":\"]\n[93.154772, \"o\", \":\"]\n[93.171063, \"o\", \" \"]\n[93.180511, \"o\", \"f\"]\n[93.19507, \"o\", \"i\"]\n[93.223267, \"o\", \"l\"]\n[93.267445, \"o\", \"e\"]\n[93.377017, \"o\", \".\"]\n[93.395205, \"o\", \"h\"]\n[93.404616, \"o\", \"t\"]\n[93.466142, \"o\", \"m\"]\n[93.507241, \"o\", \"l\"]\n[93.614841, \"o\", \" \"]\n[93.634433, \"o\", \" \"]\n[93.676532, \"o\", \"#\"]\n[93.695231, \"o\", \" \"]\n[93.708813, \"o\", \"t\"]\n[93.756113, \"o\", \"h\"]\n[93.930579, \"o\", \"i\"]\n[93.982926, \"o\", \"s\"]\n[94.183278, \"o\", \" \"]\n[94.290766, \"o\", \"c\"]\n[94.303095, \"o\", \"r\"]\n[94.407434, \"o\", \"e\"]\n[94.549651, \"o\", \"a\"]\n[94.615164, \"o\", \"t\"]\n[94.700689, \"o\", \"e\"]\n[94.735132, \"o\", \"s\"]\n[94.874051, \"o\", \" \"]\n[94.888609, \"o\", \"a\"]\n[94.975046, \"o\", \" \"]\n[95.023542, \"o\", \"n\"]\n[95.032912, \"o\", \"i\"]\n[95.131124, \"o\", \"c\"]\n[95.189662, \"o\", \"e\"]\n[95.220861, \"o\", \" \"]\n[95.230334, \"o\", \"H\"]\n[95.239715, \"o\", \"T\"]\n[95.303126, \"o\", \"M\"]\n[95.31252, \"o\", \"L\"]\n[95.508921, \"o\", \",\"]\n[95.535693, \"o\", \" \"]\n[95.588304, \"o\", \"b\"]\n[95.624524, \"o\", \"u\"]\n[95.660455, \"o\", \"t\"]\n[95.863076, \"o\", \" \"]\n[95.951101, \"o\", \"w\"]\n[96.019068, \"o\", \"h\"]\n[96.064404, \"o\", \"e\"]\n[96.104629, \"o\", \"n\"]\n[96.122201, \"o\", \" \"]\n[96.159079, \"o\", \"y\"]\n[96.170341, \"o\", \"o\"]\n[96.194989, \"o\", \"u\"]\n[96.242074, \"o\", \" \"]\n[96.251522, \"o\", \"w\"]\n[96.359109, \"o\", \"a\"]\n[96.518803, \"o\", \"n\"]\n[96.57115, \"o\", \"t\"]\n[96.622541, \"o\", \" \"]\n[96.806376, \"o\", \"s\"]\n[96.818953, \"o\", \"o\"]\n[96.897732, \"o\", \"m\"]\n[96.971108, \"o\", \"e\"]\n[97.067593, \"o\", \"t\"]\n[97.103142, \"o\", \"h\"]\n[97.183278, \"o\", \"i\"]\n[97.239756, \"o\", \"n\"]\n[97.259141, \"o\", \"g\"]\n[97.329703, \"o\", \" \"]\n[97.403076, \"o\", \"s\"]\n[97.412422, \"o\", \"i\"]\n[97.53864, \"o\", \"m\"]\n[97.549121, \"o\", \"p\"]\n[97.604583, \"o\", \"l\"]\n[97.635096, \"o\", \"e\"]\n[97.64439, \"o\", \"r\"]\n[97.675134, \"o\", \"…\"]\n[97.814542, \"o\", \"\\r\\n\"]\n[98.722178, \"o\", \"$ b\"]\n[98.807795, \"o\", \"o\"]\n[98.910336, \"o\", \"r\"]\n[98.919913, \"o\", \"g\"]\n[98.929377, \"o\", \" \"]\n[98.938952, \"o\", \"k\"]\n[98.965665, \"o\", \"e\"]\n[99.11715, \"o\", \"y\"]\n[99.317399, \"o\", \" \"]\n[99.326787, \"o\", \"e\"]\n[99.350517, \"o\", \"x\"]\n[99.359844, \"o\", \"p\"]\n[99.4675, \"o\", \"o\"]\n[99.489996, \"o\", \"r\"]\n[99.589275, \"o\", \"t\"]\n[99.691859, \"o\", \" \"]\n[99.704219, \"o\", \"-\"]\n[99.713707, \"o\", \"-\"]\n[99.743603, \"o\", \"p\"]\n[99.798054, \"o\", \"a\"]\n[99.823767, \"o\", \"p\"]\n[99.885944, \"o\", \"e\"]\n[99.897636, \"o\", \"r\"]\n[99.9767, \"o\", \" \"]\n[100.027386, \"o\", \":\"]\n[100.052192, \"o\", \":\"]\n[100.067352, \"o\", \" \"]\n[100.156712, \"o\", \" \"]\n[100.19786, \"o\", \"#\"]\n[100.212631, \"o\", \" \"]\n[100.287297, \"o\", \"t\"]\n[100.29679, \"o\", \"h\"]\n[100.306215, \"o\", \"i\"]\n[100.359208, \"o\", \"s\"]\n[100.374671, \"o\", \" \"]\n[100.474986, \"o\", \"i\"]\n[100.503798, \"o\", \"s\"]\n[100.541912, \"o\", \" \"]\n[100.626098, \"o\", \"a\"]\n[100.666332, \"o\", \" \"]\n[100.697183, \"o\", \"\\\"\"]\n[100.711032, \"o\", \"m\"]\n[100.737775, \"o\", \"a\"]\n[100.758421, \"o\", \"n\"]\n[100.797571, \"o\", \"u\"]\n[100.806956, \"o\", \"a\"]\n[100.87637, \"o\", \"l\"]\n[100.955112, \"o\", \" \"]\n[100.964372, \"o\", \"i\"]\n[100.975112, \"o\", \"n\"]\n[100.986557, \"o\", \"p\"]\n[101.026035, \"o\", \"u\"]\n[101.08241, \"o\", \"t\"]\n[101.103095, \"o\", \"\\\"\"]\n[101.20461, \"o\", \"-\"]\n[101.223075, \"o\", \"o\"]\n[101.264982, \"o\", \"n\"]\n[101.362331, \"o\", \"l\"]\n[101.485717, \"o\", \"y\"]\n[101.686062, \"o\", \" \"]\n[101.695543, \"o\", \"b\"]\n[101.726369, \"o\", \"a\"]\n[101.926959, \"o\", \"c\"]\n[101.936329, \"o\", \"k\"]\n[101.947079, \"o\", \"u\"]\n[101.956316, \"o\", \"p\"]\n[102.123084, \"o\", \" \"]\n[102.163099, \"o\", \"(\"]\n[102.176215, \"o\", \"b\"]\n[102.22553, \"o\", \"u\"]\n[102.279082, \"o\", \"t\"]\n[102.396342, \"o\", \" \"]\n[102.483081, \"o\", \"i\"]\n[102.567065, \"o\", \"t\"]\n[102.728838, \"o\", \" \"]\n[102.738243, \"o\", \"i\"]\n[102.786531, \"o\", \"s\"]\n[102.867342, \"o\", \" \"]\n[102.923016, \"o\", \"a\"]\n[102.998953, \"o\", \"l\"]\n[103.035273, \"o\", \"s\"]\n[103.087168, \"o\", \"o\"]\n[103.096547, \"o\", \" \"]\n[103.262476, \"o\", \"i\"]\n[103.347847, \"o\", \"n\"]\n[103.415063, \"o\", \"c\"]\n[103.427066, \"o\", \"l\"]\n[103.439044, \"o\", \"u\"]\n[103.482403, \"o\", \"d\"]\n[103.645195, \"o\", \"e\"]\n[103.655053, \"o\", \"d\"]\n[103.771462, \"o\", \" \"]\n[103.799107, \"o\", \"i\"]\n[103.879238, \"o\", \"n\"]\n[103.979692, \"o\", \" \"]\n[103.996193, \"o\", \"t\"]\n[104.057759, \"o\", \"h\"]\n[104.087079, \"o\", \"e\"]\n[104.175264, \"o\", \" \"]\n[104.315076, \"o\", \"-\"]\n[104.405243, \"o\", \"-\"]\n[104.605975, \"o\", \"q\"]\n[104.631634, \"o\", \"r\"]\n[104.735842, \"o\", \"-\"]\n[104.778042, \"o\", \"c\"]\n[104.788574, \"o\", \"o\"]\n[104.827883, \"o\", \"d\"]\n[104.838226, \"o\", \"e\"]\n[104.911691, \"o\", \" \"]\n[104.92405, \"o\", \"o\"]\n[104.988546, \"o\", \"p\"]\n[105.070922, \"o\", \"t\"]\n[105.121393, \"o\", \"i\"]\n[105.204919, \"o\", \"o\"]\n[105.243122, \"o\", \"n\"]\n[105.267738, \"o\", \")\"]\n[105.286747, \"o\", \"\\r\\n\"]\n[106.149824, \"o\", \"To restore key use borg key import --paper /path/to/repo\\r\\r\\n\\r\\r\\nBORG PAPER KEY v1\\r\\r\\nid: 20 / 543b72 bac0b1 5cfa3d / 816b0a 201520 - 52\\r\\r\\n 1: 86a961 6c676f 726974 686da6 736861 323536 - 14\\r\\r\\n 2: a46461 7461da 00de7c 327081 c876c2 86754c - bc\\r\\r\\n 3: dbfb68 784ce2 8047f6 1f7067 063d08 ecbe39 - 60\\r\\r\\n 4: abd2be 2f5165 6a1641 ddbb49 32fc4a c7e391 - 30\\r\\r\\n 5: d90522 c569ed 80d96c 1ae06f 3e50a0 3dbef6 - 0e\\r\\r\\n 6: 499ee6 cacd65 727cc5 cb3ba2 5f7f79 5b852a - 07\\r\\r\\n 7: 7e2b22 2ed3b8 41e1b7 0b7db2 24b1d7 1ba0bb - 80\\r\\r\\n 8: 2967ab 7776fd 5c23c8 5366e9 256824 3d8df1 - d5\\r\\r\\n 9: b2d3d5 c60f1b 5a68b0 7294e9 87fa86 18a987 - b0\\r\\r\\n10: 944388 909b57 df48ce 8a8600 527a8f 637584 - c9\\r\\r\\n11: 170511 8963a1 554fce f2ba95 5a758b 429719 - ee\\r\\r\\n12: 88bee4 9720ba f725de dcdec0 894143 8a7ebc - 29\\r\\r\\n13: a0d2e5 eb0ce8 956541 8cef38 c7194e b8b5fe - e6\\r\\r\\n\"]\n[106.149893, \"o\", \"14: 2b9282 a8c540 ae69e9 0bca48 9b52a4 686173 - ba\\r\\r\\n15: 68da00 207ba4 f1d621 e8edc4 57978b 04fe04 - af\\r\\r\\n16: 45af0a eefb96 051947 106f38 b44520 1002cc - dd\\r\\r\\n17: aa6974 657261 74696f 6e73ce 000186 a0a473 - 15\\r\\r\\n18: 616c74 da0020 738815 8e60fb 2b5cc4 662110 - dd\\r\\r\\n19: 9cdaff 4c7f78 b95f81 57009d ac0c27 f2e160 - 69\\r\\r\\n\"]\n[106.150871, \"o\", \"20: facfa7 766572 73696f 6e01 - 49\\r\\r\\n\"]\n[106.200923, \"o\", \"$ \\r\\n\"]\n[106.201933, \"o\", \"$ #\"]\n[106.311069, \"o\", \"#\"]\n[106.350876, \"o\", \" \"]\n[106.360114, \"o\", \"M\"]\n[106.381845, \"o\", \"A\"]\n[106.448659, \"o\", \"I\"]\n[106.631657, \"o\", \"N\"]\n[106.819891, \"o\", \"T\"]\n[106.829408, \"o\", \"E\"]\n[106.886858, \"o\", \"N\"]\n[106.897164, \"o\", \"A\"]\n[106.921071, \"o\", \"N\"]\n[106.947049, \"o\", \"C\"]\n[106.959083, \"o\", \"E\"]\n[107.007196, \"o\", \" \"]\n[107.083581, \"o\", \"#\"]\n[107.112356, \"o\", \"#\"]\n[107.250885, \"o\", \"\\r\\n\"]\n[107.251287, \"o\", \"$ #\"]\n[107.451883, \"o\", \" \"]\n[107.461255, \"o\", \"S\"]\n[107.470909, \"o\", \"o\"]\n[107.657323, \"o\", \"m\"]\n[107.753465, \"o\", \"e\"]\n[107.801524, \"o\", \"t\"]\n[107.848903, \"o\", \"i\"]\n[107.879027, \"o\", \"m\"]\n[107.88862, \"o\", \"e\"]\n[107.977214, \"o\", \"s\"]\n[108.110259, \"o\", \" \"]\n[108.179155, \"o\", \"b\"]\n[108.351926, \"o\", \"a\"]\n[108.400699, \"o\", \"c\"]\n[108.410203, \"o\", \"k\"]\n[108.419646, \"o\", \"u\"]\n[108.486479, \"o\", \"p\"]\n[108.567209, \"o\", \"s\"]\n[108.767773, \"o\", \" \"]\n[108.803208, \"o\", \"g\"]\n[108.846873, \"o\", \"e\"]\n[108.867075, \"o\", \"t\"]\n[108.977561, \"o\", \" \"]\n[109.136181, \"o\", \"b\"]\n[109.200763, \"o\", \"r\"]\n[109.259374, \"o\", \"o\"]\n[109.278942, \"o\", \"k\"]\n[109.3141, \"o\", \"e\"]\n[109.43095, \"o\", \"n\"]\n[109.46321, \"o\", \" \"]\n[109.47754, \"o\", \"o\"]\n[109.588012, \"o\", \"r\"]\n[109.605532, \"o\", \" \"]\n[109.61509, \"o\", \"w\"]\n[109.715362, \"o\", \"e\"]\n[109.724883, \"o\", \" \"]\n[109.782594, \"o\", \"w\"]\n[109.797213, \"o\", \"a\"]\n[109.808586, \"o\", \"n\"]\n[109.900685, \"o\", \"t\"]\n[109.911137, \"o\", \" \"]\n[110.056022, \"o\", \"a\"]\n[110.099232, \"o\", \" \"]\n[110.182919, \"o\", \"r\"]\n[110.203243, \"o\", \"e\"]\n[110.230058, \"o\", \"g\"]\n[110.385779, \"o\", \"u\"]\n[110.39538, \"o\", \"l\"]\n[110.406829, \"o\", \"a\"]\n[110.454471, \"o\", \"r\"]\n[110.539017, \"o\", \" \"]\n[110.61907, \"o\", \"\\\"\"]\n[110.765729, \"o\", \"c\"]\n[110.96699, \"o\", \"h\"]\n[111.035241, \"o\", \"e\"]\n[111.090908, \"o\", \"c\"]\n[111.121846, \"o\", \"k\"]\n[111.214881, \"o\", \"u\"]\n[111.224693, \"o\", \"p\"]\n[111.268192, \"o\", \"\\\"\"]\n[111.372347, \"o\", \" \"]\n[111.387051, \"o\", \"t\"]\n[111.396725, \"o\", \"h\"]\n[111.406456, \"o\", \"a\"]\n[111.422276, \"o\", \"t\"]\n[111.526997, \"o\", \" \"]\n[111.6099, \"o\", \"e\"]\n[111.755036, \"o\", \"v\"]\n[111.867197, \"o\", \"e\"]\n[111.87702, \"o\", \"r\"]\n[111.919062, \"o\", \"y\"]\n[111.986004, \"o\", \"t\"]\n[112.051883, \"o\", \"h\"]\n[112.171044, \"o\", \"i\"]\n[112.21494, \"o\", \"n\"]\n[112.227013, \"o\", \"g\"]\n[112.265067, \"o\", \" \"]\n[112.28199, \"o\", \"i\"]\n[112.400713, \"o\", \"s\"]\n[112.435962, \"o\", \" \"]\n[112.544903, \"o\", \"o\"]\n[112.557869, \"o\", \"k\"]\n[112.652822, \"o\", \"a\"]\n[112.742943, \"o\", \"y\"]\n[112.943874, \"o\", \"…\"]\n[112.989623, \"o\", \"\\r\\n\"]\n[112.990358, \"o\", \"$ \"]\n[112.990917, \"o\", \"b\"]\n[113.020704, \"o\", \"o\"]\n[113.0326, \"o\", \"r\"]\n[113.062861, \"o\", \"g\"]\n[113.146392, \"o\", \" \"]\n[113.158985, \"o\", \"c\"]\n[113.187026, \"o\", \"h\"]\n[113.230546, \"o\", \"e\"]\n[113.240481, \"o\", \"c\"]\n[113.270414, \"o\", \"k\"]\n[113.298937, \"o\", \" \"]\n[113.42575, \"o\", \"-\"]\n[113.43564, \"o\", \"v\"]\n[113.448634, \"o\", \" \"]\n[113.51099, \"o\", \":\"]\n[113.572617, \"o\", \":\"]\n[113.623082, \"o\", \"\\r\\n\"]\n[114.468125, \"o\", \"Starting repository check\\r\\r\\n\"]\n[114.970867, \"o\", \"finished segment check at segment 29\\r\\r\\n\"]\n[114.973282, \"o\", \"Starting repository index check\\r\\r\\n\"]\n[114.973326, \"o\", \"Index object count match.\\r\\r\\n\"]\n[114.974106, \"o\", \"Finished full repository check, no problems found.\\r\\r\\n\"]\n[114.974507, \"o\", \"Starting archive consistency check...\\r\\r\\n\"]\n[115.054678, \"o\", \"Analyzing archive backup1 (1/6)\\r\\r\\n\"]\n[115.060833, \"o\", \"Analyzing archive backup2 (2/6)\\r\\r\\n\"]\n[115.063603, \"o\", \"Analyzing archive backup3 (3/6)\\r\\r\\n\"]\n[115.066201, \"o\", \"Analyzing archive root-2022-07-06T21:31:12 (4/6)\\r\\r\\n\"]\n[115.068866, \"o\", \"Analyzing archive root-2022-07-06T21:31:28 (5/6)\\r\\r\\n\"]\n[115.069725, \"o\", \"Analyzing archive backup-block-device (6/6)\\r\\r\\n\"]\n[115.070801, \"o\", \"Archive consistency check complete, no problems found.\\r\\r\\n\"]\n[115.13231, \"o\", \"$ \\r\\n\"]\n[115.132383, \"o\", \"$ #\"]\n[115.141656, \"o\", \" \"]\n[115.157976, \"o\", \"N\"]\n[115.18922, \"o\", \"e\"]\n[115.20151, \"o\", \"x\"]\n[115.354755, \"o\", \"t\"]\n[115.495108, \"o\", \" \"]\n[115.504182, \"o\", \"p\"]\n[115.514429, \"o\", \"r\"]\n[115.560709, \"o\", \"o\"]\n[115.700955, \"o\", \"b\"]\n[115.710156, \"o\", \"l\"]\n[115.889298, \"o\", \"e\"]\n[115.900524, \"o\", \"m\"]\n[115.948777, \"o\", \":\"]\n[116.100016, \"o\", \" \"]\n[116.127186, \"o\", \"U\"]\n[116.273465, \"o\", \"s\"]\n[116.28276, \"o\", \"u\"]\n[116.372945, \"o\", \"a\"]\n[116.390155, \"o\", \"l\"]\n[116.399296, \"o\", \"l\"]\n[116.408552, \"o\", \"y\"]\n[116.481705, \"o\", \" \"]\n[116.571974, \"o\", \"y\"]\n[116.583156, \"o\", \"o\"]\n[116.609289, \"o\", \"u\"]\n[116.687464, \"o\", \" \"]\n[116.744699, \"o\", \"d\"]\n[116.753796, \"o\", \"o\"]\n[116.894075, \"o\", \" \"]\n[116.903189, \"o\", \"n\"]\n[117.10336, \"o\", \"o\"]\n[117.11259, \"o\", \"t\"]\n[117.185751, \"o\", \" \"]\n[117.197017, \"o\", \"h\"]\n[117.397254, \"o\", \"a\"]\n[117.406473, \"o\", \"v\"]\n[117.415684, \"o\", \"e\"]\n[117.61584, \"o\", \" \"]\n[117.643157, \"o\", \"i\"]\n[117.652315, \"o\", \"n\"]\n[117.757616, \"o\", \"f\"]\n[117.781874, \"o\", \"i\"]\n[117.791149, \"o\", \"n\"]\n[117.80637, \"o\", \"i\"]\n[117.827596, \"o\", \"t\"]\n[117.854946, \"o\", \"e\"]\n[118.055148, \"o\", \" \"]\n[118.064193, \"o\", \"d\"]\n[118.073447, \"o\", \"i\"]\n[118.088702, \"o\", \"s\"]\n[118.105948, \"o\", \"k\"]\n[118.115157, \"o\", \" \"]\n[118.155353, \"o\", \"s\"]\n[118.272461, \"o\", \"p\"]\n[118.288573, \"o\", \"a\"]\n[118.461837, \"o\", \"c\"]\n[118.474126, \"o\", \"e\"]\n[118.67441, \"o\", \".\"]\n[118.748669, \"o\", \" \"]\n[118.93194, \"o\", \"S\"]\n[119.010054, \"o\", \"o\"]\n[119.085321, \"o\", \" \"]\n[119.094526, \"o\", \"y\"]\n[119.112808, \"o\", \"o\"]\n[119.148083, \"o\", \"u\"]\n[119.291238, \"o\", \" \"]\n[119.300456, \"o\", \"m\"]\n[119.344754, \"o\", \"a\"]\n[119.405028, \"o\", \"y\"]\n[119.48428, \"o\", \" \"]\n[119.508528, \"o\", \"n\"]\n[119.551619, \"o\", \"e\"]\n[119.613877, \"o\", \"e\"]\n[119.638147, \"o\", \"d\"]\n[119.731323, \"o\", \" \"]\n[119.818835, \"o\", \"t\"]\n[119.86185, \"o\", \"o\"]\n[119.947949, \"o\", \" \"]\n[120.135432, \"o\", \"p\"]\n[120.182617, \"o\", \"r\"]\n[120.230827, \"o\", \"u\"]\n[120.240039, \"o\", \"n\"]\n[120.27029, \"o\", \"e\"]\n[120.289402, \"o\", \" \"]\n[120.436641, \"o\", \"y\"]\n[120.471903, \"o\", \"o\"]\n[120.595072, \"o\", \"u\"]\n[120.604288, \"o\", \"r\"]\n[120.631553, \"o\", \" \"]\n[120.66179, \"o\", \"a\"]\n[120.782954, \"o\", \"r\"]\n[120.792152, \"o\", \"c\"]\n[120.992294, \"o\", \"h\"]\n[121.117555, \"o\", \"i\"]\n[121.126807, \"o\", \"v\"]\n[121.161076, \"o\", \"e\"]\n[121.220316, \"o\", \"…\"]\n[121.23366, \"o\", \"\\r\\n$ #\"]\n[121.242855, \"o\", \" \"]\n[121.321154, \"o\", \"Y\"]\n[121.352419, \"o\", \"o\"]\n[121.386668, \"o\", \"u\"]\n[121.456777, \"o\", \" \"]\n[121.506033, \"o\", \"c\"]\n[121.515201, \"o\", \"a\"]\n[121.535296, \"o\", \"n\"]\n[121.55855, \"o\", \" \"]\n[121.588801, \"o\", \"t\"]\n[121.597994, \"o\", \"u\"]\n[121.642252, \"o\", \"n\"]\n[121.651435, \"o\", \"e\"]\n[121.66472, \"o\", \" \"]\n[121.673911, \"o\", \"t\"]\n[121.683098, \"o\", \"h\"]\n[121.692328, \"o\", \"i\"]\n[121.733587, \"o\", \"s\"]\n[121.933824, \"o\", \" \"]\n[121.94297, \"o\", \"i\"]\n[122.023164, \"o\", \"n\"]\n[122.073434, \"o\", \" \"]\n[122.197695, \"o\", \"e\"]\n[122.242944, \"o\", \"v\"]\n[122.267127, \"o\", \"e\"]\n[122.313365, \"o\", \"r\"]\n[122.375621, \"o\", \"y\"]\n[122.480878, \"o\", \" \"]\n[122.49007, \"o\", \"d\"]\n[122.555315, \"o\", \"e\"]\n[122.590572, \"o\", \"t\"]\n[122.601763, \"o\", \"a\"]\n[122.618969, \"o\", \"i\"]\n[122.818095, \"o\", \"l\"]\n[122.938459, \"o\", \".\"]\n[123.01755, \"o\", \" \"]\n[123.064808, \"o\", \"S\"]\n[123.106838, \"o\", \"e\"]\n[123.221128, \"o\", \"e\"]\n[123.310401, \"o\", \" \"]\n[123.319655, \"o\", \"t\"]\n[123.508935, \"o\", \"h\"]\n[123.519109, \"o\", \"e\"]\n[123.71934, \"o\", \" \"]\n[123.728563, \"o\", \"d\"]\n[123.741784, \"o\", \"o\"]\n[123.850044, \"o\", \"c\"]\n[123.989291, \"o\", \"s\"]\n[124.071531, \"o\", \" \"]\n[124.156791, \"o\", \"f\"]\n[124.301032, \"o\", \"o\"]\n[124.31025, \"o\", \"r\"]\n[124.510545, \"o\", \" \"]\n[124.608805, \"o\", \"d\"]\n[124.674988, \"o\", \"e\"]\n[124.684186, \"o\", \"t\"]\n[124.715408, \"o\", \"a\"]\n[124.727663, \"o\", \"i\"]\n[124.805915, \"o\", \"l\"]\n[124.842176, \"o\", \"s\"]\n[125.042443, \"o\", \".\"]\n[125.059688, \"o\", \" \"]\n[125.093771, \"o\", \"H\"]\n[125.102879, \"o\", \"e\"]\n[125.112002, \"o\", \"r\"]\n[125.2784, \"o\", \"e\"]\n[125.440604, \"o\", \" \"]\n[125.468854, \"o\", \"o\"]\n[125.507037, \"o\", \"n\"]\n[125.538304, \"o\", \"l\"]\n[125.547524, \"o\", \"y\"]\n[125.587786, \"o\", \" \"]\n[125.599977, \"o\", \"a\"]\n[125.622256, \"o\", \" \"]\n[125.63238, \"o\", \"s\"]\n[125.641635, \"o\", \"i\"]\n[125.652865, \"o\", \"m\"]\n[125.662088, \"o\", \"p\"]\n[125.671289, \"o\", \"l\"]\n[125.709579, \"o\", \"e\"]\n[125.820831, \"o\", \" \"]\n[125.888074, \"o\", \"e\"]\n[125.943287, \"o\", \"x\"]\n[125.996545, \"o\", \"a\"]\n[126.019798, \"o\", \"m\"]\n[126.028994, \"o\", \"p\"]\n[126.108255, \"o\", \"l\"]\n[126.154535, \"o\", \"e\"]\n[126.203713, \"o\", \":\"]\n[126.247947, \"o\", \"\\r\\n\"]\n[126.248396, \"o\", \"$ b\"]\n[126.257524, \"o\", \"o\"]\n[126.310761, \"o\", \"r\"]\n[126.33483, \"o\", \"g\"]\n[126.346943, \"o\", \" \"]\n[126.40812, \"o\", \"p\"]\n[126.417361, \"o\", \"r\"]\n[126.475452, \"o\", \"u\"]\n[126.484575, \"o\", \"n\"]\n[126.538756, \"o\", \"e\"]\n[126.738889, \"o\", \" \"]\n[126.869013, \"o\", \"-\"]\n[127.069159, \"o\", \"-\"]\n[127.192274, \"o\", \"l\"]\n[127.201392, \"o\", \"i\"]\n[127.211523, \"o\", \"s\"]\n[127.220653, \"o\", \"t\"]\n[127.231777, \"o\", \" \"]\n[127.240901, \"o\", \"-\"]\n[127.320174, \"o\", \"-\"]\n[127.346272, \"o\", \"k\"]\n[127.407534, \"o\", \"e\"]\n[127.445718, \"o\", \"e\"]\n[127.454727, \"o\", \"p\"]\n[127.499843, \"o\", \"-\"]\n[127.554949, \"o\", \"l\"]\n[127.569072, \"o\", \"a\"]\n[127.699194, \"o\", \"s\"]\n[127.715297, \"o\", \"t\"]\n[127.769524, \"o\", \" \"]\n[127.80476, \"o\", \"1\"]\n[127.827003, \"o\", \" \"]\n[127.844259, \"o\", \"-\"]\n[127.981519, \"o\", \"-\"]\n[128.022874, \"o\", \"d\"]\n[128.033089, \"o\", \"r\"]\n[128.084347, \"o\", \"y\"]\n[128.201627, \"o\", \"-\"]\n[128.30488, \"o\", \"r\"]\n[128.344133, \"o\", \"u\"]\n[128.35339, \"o\", \"n\"]\n[128.437824, \"o\", \"\\r\\n\"]\n[129.45361, \"o\", \"Keeping archive (rule: secondly #1):     backup-block-device                  Wed, 2022-07-06 21:31:36 [c416cbf604ab7dce6512f4d48cb9ea3fc56e311d9c794e2ec0814703a7561551]\"]\n[129.454727, \"o\", \"\\r\\r\\n\"]\n[129.455338, \"o\", \"Would prune:                             root-2022-07-06T21:31:28             Wed, 2022-07-06 21:31:28 [d292c218f6c32423a9de2e3fbdf51c8ee865dc83ac78f62624216ebdae7ca55e]\"]\n[129.455722, \"o\", \"\\r\\r\\n\"]\n[129.456283, \"o\", \"Would prune:                             root-2022-07-06T21:31:12             Wed, 2022-07-06 21:31:12 [280fe9e3d92e2e61f04f0ef98de32b4d0a797ec9e3b0b29ddb2b0946c4a0236b]\"]\n[129.456658, \"o\", \"\\r\\r\\n\"]\n[129.457213, \"o\", \"Would prune:                             backup3                              Wed, 2022-07-06 21:30:32 [b715e25edecebd717c06cca04bcc1dc73a73fd87a9b126d1f190a15a068c0f69]\"]\n[129.457618, \"o\", \"\\r\\r\\n\"]\n[129.458159, \"o\", \"Would prune:                             backup2                              Wed, 2022-07-06 21:30:31 [cbd3400dcea913c0611a75681c2e2ee192d7b9f915f6d9e8e77cbabde976c239]\"]\n[129.458583, \"o\", \"\\r\\r\\n\"]\n[129.459193, \"o\", \"Would prune:                             backup1                              Wed, 2022-07-06 21:30:25 [a1f88905bc1f341893c92a4757ab3975b4dcd36ca6669dabc76c5ea073a9095b]\"]\n[129.459581, \"o\", \"\\r\\r\\n\"]\n[129.51897, \"o\", \"$ #\"]\n[129.625084, \"o\", \" \"]\n[129.65835, \"o\", \"W\"]\n[129.681607, \"o\", \"h\"]\n[129.772895, \"o\", \"e\"]\n[129.806128, \"o\", \"n\"]\n[129.834381, \"o\", \" \"]\n[129.938653, \"o\", \"a\"]\n[129.947855, \"o\", \"c\"]\n[130.001121, \"o\", \"t\"]\n[130.022385, \"o\", \"u\"]\n[130.043634, \"o\", \"a\"]\n[130.110916, \"o\", \"l\"]\n[130.174201, \"o\", \"l\"]\n[130.183388, \"o\", \"y\"]\n[130.328666, \"o\", \" \"]\n[130.379911, \"o\", \"e\"]\n[130.398181, \"o\", \"x\"]\n[130.555408, \"o\", \"e\"]\n[130.684651, \"o\", \"c\"]\n[130.717891, \"o\", \"u\"]\n[130.732147, \"o\", \"t\"]\n[130.831375, \"o\", \"i\"]\n[130.917655, \"o\", \"n\"]\n[130.946912, \"o\", \"g\"]\n[130.978155, \"o\", \" \"]\n[130.994409, \"o\", \"i\"]\n[131.003618, \"o\", \"t\"]\n[131.174879, \"o\", \" \"]\n[131.184112, \"o\", \"i\"]\n[131.236365, \"o\", \"n\"]\n[131.270612, \"o\", \" \"]\n[131.27982, \"o\", \"a\"]\n[131.294113, \"o\", \" \"]\n[131.322373, \"o\", \"s\"]\n[131.376617, \"o\", \"c\"]\n[131.40187, \"o\", \"r\"]\n[131.458129, \"o\", \"i\"]\n[131.476372, \"o\", \"p\"]\n[131.505657, \"o\", \"t\"]\n[131.545909, \"o\", \",\"]\n[131.600166, \"o\", \" \"]\n[131.642413, \"o\", \"y\"]\n[131.651619, \"o\", \"o\"]\n[131.675712, \"o\", \"u\"]\n[131.767878, \"o\", \" \"]\n[131.814969, \"o\", \"h\"]\n[131.894152, \"o\", \"a\"]\n[131.951416, \"o\", \"v\"]\n[131.993679, \"o\", \"e\"]\n[132.060965, \"o\", \" \"]\n[132.070207, \"o\", \"t\"]\n[132.115432, \"o\", \"o\"]\n[132.128652, \"o\", \" \"]\n[132.199925, \"o\", \"u\"]\n[132.281178, \"o\", \"s\"]\n[132.306425, \"o\", \"e\"]\n[132.473677, \"o\", \" \"]\n[132.526977, \"o\", \"i\"]\n[132.539148, \"o\", \"t\"]\n[132.739381, \"o\", \" \"]\n[132.75765, \"o\", \"w\"]\n[132.773923, \"o\", \"i\"]\n[132.859107, \"o\", \"t\"]\n[132.871348, \"o\", \"h\"]\n[132.932614, \"o\", \"o\"]\n[132.965873, \"o\", \"u\"]\n[132.990116, \"o\", \"t\"]\n[133.039327, \"o\", \" \"]\n[133.118587, \"o\", \"t\"]\n[133.16384, \"o\", \"h\"]\n[133.197097, \"o\", \"e\"]\n[133.258298, \"o\", \" \"]\n[133.26755, \"o\", \"-\"]\n[133.30881, \"o\", \"-\"]\n[133.356063, \"o\", \"d\"]\n[133.431264, \"o\", \"r\"]\n[133.579521, \"o\", \"y\"]\n[133.598889, \"o\", \"-\"]\n[133.611051, \"o\", \"r\"]\n[133.627291, \"o\", \"u\"]\n[133.682531, \"o\", \"n\"]\n[133.699785, \"o\", \" \"]\n[133.752037, \"o\", \"o\"]\n[133.83033, \"o\", \"p\"]\n[133.958481, \"o\", \"t\"]\n[133.98858, \"o\", \"i\"]\n[134.144897, \"o\", \"o\"]\n[134.211076, \"o\", \"n\"]\n[134.222281, \"o\", \",\"]\n[134.363538, \"o\", \" \"]\n[134.397804, \"o\", \"o\"]\n[134.408005, \"o\", \"f\"]\n[134.608297, \"o\", \" \"]\n[134.626522, \"o\", \"c\"]\n[134.635763, \"o\", \"o\"]\n[134.694895, \"o\", \"u\"]\n[134.774151, \"o\", \"r\"]\n[134.815387, \"o\", \"s\"]\n[134.911629, \"o\", \"e\"]\n[134.955897, \"o\", \".\"]\n[134.9656, \"o\", \"\\r\\n$ \\r\\n\"]\n[134.965766, \"o\", \"$ #\"]\n[134.974883, \"o\", \"#\"]\n[135.032143, \"o\", \" \"]\n[135.212403, \"o\", \"R\"]\n[135.320667, \"o\", \"E\"]\n[135.34591, \"o\", \"S\"]\n[135.355073, \"o\", \"T\"]\n[135.462336, \"o\", \"O\"]\n[135.520591, \"o\", \"R\"]\n[135.564825, \"o\", \"E\"]\n[135.574095, \"o\", \" \"]\n[135.583294, \"o\", \"#\"]\n[135.59252, \"o\", \"#\"]\n[135.76985, \"o\", \"\\r\\n\"]\n[135.77021, \"o\", \"$ \\r\\n\"]\n[135.770371, \"o\", \"$ #\"]\n[135.802643, \"o\", \" \"]\n[135.831898, \"o\", \"W\"]\n[135.841141, \"o\", \"h\"]\n[136.024428, \"o\", \"e\"]\n[136.074669, \"o\", \"n\"]\n[136.083752, \"o\", \" \"]\n[136.092876, \"o\", \"y\"]\n[136.177024, \"o\", \"o\"]\n[136.211126, \"o\", \"u\"]\n[136.308445, \"o\", \" \"]\n[136.317628, \"o\", \"w\"]\n[136.326945, \"o\", \"a\"]\n[136.527133, \"o\", \"n\"]\n[136.545348, \"o\", \"t\"]\n[136.572621, \"o\", \" \"]\n[136.59388, \"o\", \"t\"]\n[136.621124, \"o\", \"o\"]\n[136.74037, \"o\", \" \"]\n[136.759632, \"o\", \"s\"]\n[136.768838, \"o\", \"e\"]\n[136.870121, \"o\", \"e\"]\n[136.933357, \"o\", \" \"]\n[136.998616, \"o\", \"t\"]\n[137.01787, \"o\", \"h\"]\n[137.106114, \"o\", \"e\"]\n[137.256375, \"o\", \" \"]\n[137.284629, \"o\", \"d\"]\n[137.476905, \"o\", \"i\"]\n[137.546163, \"o\", \"f\"]\n[137.746423, \"o\", \"f\"]\n[137.755642, \"o\", \" \"]\n[137.955914, \"o\", \"b\"]\n[137.968135, \"o\", \"e\"]\n[138.083376, \"o\", \"t\"]\n[138.15063, \"o\", \"w\"]\n[138.19896, \"o\", \"e\"]\n[138.259157, \"o\", \"e\"]\n[138.339242, \"o\", \"n\"]\n[138.362365, \"o\", \" \"]\n[138.547758, \"o\", \"t\"]\n[138.574956, \"o\", \"w\"]\n[138.597213, \"o\", \"o\"]\n[138.797472, \"o\", \" \"]\n[138.80677, \"o\", \"a\"]\n[138.83602, \"o\", \"r\"]\n[138.845271, \"o\", \"c\"]\n[138.882571, \"o\", \"h\"]\n[138.932833, \"o\", \"i\"]\n[138.976096, \"o\", \"v\"]\n[139.069357, \"o\", \"e\"]\n[139.101615, \"o\", \"s\"]\n[139.160879, \"o\", \" \"]\n[139.170074, \"o\", \"u\"]\n[139.181311, \"o\", \"s\"]\n[139.254593, \"o\", \"e\"]\n[139.454948, \"o\", \" \"]\n[139.655134, \"o\", \"t\"]\n[139.840393, \"o\", \"h\"]\n[139.894686, \"o\", \"i\"]\n[139.929925, \"o\", \"s\"]\n[140.130188, \"o\", \" \"]\n[140.198433, \"o\", \"c\"]\n[140.233719, \"o\", \"o\"]\n[140.390928, \"o\", \"m\"]\n[140.418192, \"o\", \"m\"]\n[140.432452, \"o\", \"a\"]\n[140.475737, \"o\", \"n\"]\n[140.586824, \"o\", \"d\"]\n[140.757161, \"o\", \".\"]\n[140.833578, \"o\", \"\\r\\n\"]\n[140.834316, \"o\", \"$ #\"]\n[140.843373, \"o\", \" \"]\n[140.964668, \"o\", \"E\"]\n[141.09793, \"o\", \".\"]\n[141.108182, \"o\", \"g\"]\n[141.151418, \"o\", \".\"]\n[141.19968, \"o\", \" \"]\n[141.232918, \"o\", \"w\"]\n[141.433168, \"o\", \"h\"]\n[141.444444, \"o\", \"a\"]\n[141.463703, \"o\", \"t\"]\n[141.490935, \"o\", \" \"]\n[141.51719, \"o\", \"h\"]\n[141.5274, \"o\", \"a\"]\n[141.557672, \"o\", \"p\"]\n[141.711926, \"o\", \"p\"]\n[141.813187, \"o\", \"e\"]\n[141.842439, \"o\", \"n\"]\n[141.974914, \"o\", \"e\"]\n[142.011958, \"o\", \"d\"]\n[142.141211, \"o\", \" \"]\n[142.196474, \"o\", \"b\"]\n[142.205729, \"o\", \"e\"]\n[142.218871, \"o\", \"t\"]\n[142.268159, \"o\", \"w\"]\n[142.281389, \"o\", \"e\"]\n[142.295658, \"o\", \"e\"]\n[142.485918, \"o\", \"n\"]\n[142.499102, \"o\", \" \"]\n[142.59036, \"o\", \"t\"]\n[142.662621, \"o\", \"h\"]\n[142.86285, \"o\", \"e\"]\n[143.053083, \"o\", \" \"]\n[143.109365, \"o\", \"f\"]\n[143.174629, \"o\", \"i\"]\n[143.183817, \"o\", \"r\"]\n[143.264078, \"o\", \"s\"]\n[143.273304, \"o\", \"t\"]\n[143.347578, \"o\", \" \"]\n[143.38783, \"o\", \"t\"]\n[143.397029, \"o\", \"w\"]\n[143.445305, \"o\", \"o\"]\n[143.604558, \"o\", \" \"]\n[143.61982, \"o\", \"b\"]\n[143.662984, \"o\", \"a\"]\n[143.677246, \"o\", \"c\"]\n[143.68947, \"o\", \"k\"]\n[143.718812, \"o\", \"u\"]\n[143.750062, \"o\", \"p\"]\n[143.878314, \"o\", \"s\"]\n[144.014587, \"o\", \"?\"]\n[144.024937, \"o\", \"\\r\\n\"]\n[144.025577, \"o\", \"$ b\"]\n[144.103775, \"o\", \"o\"]\n[144.11397, \"o\", \"r\"]\n[144.314265, \"o\", \"g\"]\n[144.323463, \"o\", \" \"]\n[144.33574, \"o\", \"d\"]\n[144.404002, \"o\", \"i\"]\n[144.413199, \"o\", \"f\"]\n[144.422432, \"o\", \"f\"]\n[144.492721, \"o\", \" \"]\n[144.616987, \"o\", \":\"]\n[144.735188, \"o\", \":\"]\n[144.887429, \"o\", \"b\"]\n[144.913512, \"o\", \"a\"]\n[144.976805, \"o\", \"c\"]\n[145.012882, \"o\", \"k\"]\n[145.050988, \"o\", \"u\"]\n[145.06411, \"o\", \"p\"]\n[145.073236, \"o\", \"1\"]\n[145.122529, \"o\", \" \"]\n[145.172773, \"o\", \"b\"]\n[145.274964, \"o\", \"a\"]\n[145.329206, \"o\", \"c\"]\n[145.340477, \"o\", \"k\"]\n[145.451743, \"o\", \"u\"]\n[145.559999, \"o\", \"p\"]\n[145.587202, \"o\", \"2\"]\n[145.630681, \"o\", \"\\r\\n\"]\n[146.646473, \"o\", \"added          14 B Wallpaper/newfile.txt\"]\n[146.647431, \"o\", \"\\r\\r\\n\"]\n[146.706774, \"o\", \"$ #\"]\n[146.716059, \"o\", \" \"]\n[146.755468, \"o\", \"A\"]\n[146.901789, \"o\", \"h\"]\n[147.035155, \"o\", \",\"]\n[147.070453, \"o\", \" \"]\n[147.132464, \"o\", \"w\"]\n[147.170608, \"o\", \"e\"]\n[147.298155, \"o\", \" \"]\n[147.328469, \"o\", \"a\"]\n[147.358818, \"o\", \"d\"]\n[147.434065, \"o\", \"d\"]\n[147.500279, \"o\", \"e\"]\n[147.523549, \"o\", \"d\"]\n[147.583913, \"o\", \" \"]\n[147.642189, \"o\", \"a\"]\n[147.724483, \"o\", \" \"]\n[147.733697, \"o\", \"f\"]\n[147.744978, \"o\", \"i\"]\n[147.754216, \"o\", \"l\"]\n[147.764569, \"o\", \"e\"]\n[147.773825, \"o\", \",\"]\n[147.807202, \"o\", \" \"]\n[147.892481, \"o\", \"r\"]\n[148.021787, \"o\", \"i\"]\n[148.148093, \"o\", \"g\"]\n[148.228377, \"o\", \"h\"]\n[148.312729, \"o\", \"t\"]\n[148.476959, \"o\", \"…\"]\n[148.677268, \"o\", \"\\r\\n\"]\n[148.678159, \"o\", \"$ \\r\\n\"]\n[148.678976, \"o\", \"$ #\"]\n[148.821721, \"o\", \" \"]\n[148.839132, \"o\", \"T\"]\n[148.848356, \"o\", \"h\"]\n[148.87758, \"o\", \"e\"]\n[148.951243, \"o\", \"r\"]\n[149.035959, \"o\", \"e\"]\n[149.239284, \"o\", \" \"]\n[149.272524, \"o\", \"a\"]\n[149.288132, \"o\", \"r\"]\n[149.299294, \"o\", \"e\"]\n[149.32949, \"o\", \" \"]\n[149.404801, \"o\", \"a\"]\n[149.425843, \"o\", \"l\"]\n[149.459032, \"o\", \"s\"]\n[149.490519, \"o\", \"o\"]\n[149.691261, \"o\", \" \"]\n[149.74465, \"o\", \"o\"]\n[149.775182, \"o\", \"t\"]\n[149.849871, \"o\", \"h\"]\n[149.940396, \"o\", \"e\"]\n[149.97516, \"o\", \"r\"]\n[149.984609, \"o\", \" \"]\n[150.015151, \"o\", \"w\"]\n[150.025493, \"o\", \"a\"]\n[150.179023, \"o\", \"y\"]\n[150.379968, \"o\", \"s\"]\n[150.528559, \"o\", \" \"]\n[150.566363, \"o\", \"t\"]\n[150.766933, \"o\", \"o\"]\n[150.892125, \"o\", \" \"]\n[150.95061, \"o\", \"e\"]\n[151.015184, \"o\", \"x\"]\n[151.191108, \"o\", \"t\"]\n[151.269469, \"o\", \"r\"]\n[151.278897, \"o\", \"a\"]\n[151.344629, \"o\", \"c\"]\n[151.37102, \"o\", \"t\"]\n[151.57479, \"o\", \" \"]\n[151.779091, \"o\", \"t\"]\n[151.807215, \"o\", \"h\"]\n[151.817017, \"o\", \"e\"]\n[151.826812, \"o\", \" \"]\n[151.837693, \"o\", \"d\"]\n[151.870989, \"o\", \"a\"]\n[151.906995, \"o\", \"t\"]\n[151.935865, \"o\", \"a\"]\n[151.951634, \"o\", \".\"]\n[151.961314, \"o\", \"\\r\\n\"]\n[151.961915, \"o\", \"$ \"]\n[151.962215, \"o\", \"#\"]\n[152.087058, \"o\", \" \"]\n[152.291084, \"o\", \"E\"]\n[152.491848, \"o\", \".\"]\n[152.643011, \"o\", \"g\"]\n[152.667003, \"o\", \".\"]\n[152.679008, \"o\", \" \"]\n[152.738984, \"o\", \"a\"]\n[152.750962, \"o\", \"s\"]\n[152.850896, \"o\", \" \"]\n[152.862923, \"o\", \"a\"]\n[153.066925, \"o\", \" \"]\n[153.09013, \"o\", \"t\"]\n[153.20094, \"o\", \"a\"]\n[153.254436, \"o\", \"r\"]\n[153.267051, \"o\", \" \"]\n[153.299031, \"o\", \"a\"]\n[153.342374, \"o\", \"r\"]\n[153.38735, \"o\", \"c\"]\n[153.433121, \"o\", \"h\"]\n[153.4661, \"o\", \"i\"]\n[153.508774, \"o\", \"v\"]\n[153.585774, \"o\", \"e\"]\n[153.630565, \"o\", \".\"]\n[153.675714, \"o\", \"\\r\\n\"]\n[153.676317, \"o\", \"$ \"]\n[153.676694, \"o\", \"b\"]\n[153.762637, \"o\", \"o\"]\n[153.772338, \"o\", \"r\"]\n[153.781992, \"o\", \"g\"]\n[153.912578, \"o\", \" \"]\n[153.92756, \"o\", \"e\"]\n[154.013549, \"o\", \"x\"]\n[154.066538, \"o\", \"p\"]\n[154.076608, \"o\", \"o\"]\n[154.140459, \"o\", \"r\"]\n[154.16366, \"o\", \"t\"]\n[154.364435, \"o\", \"-\"]\n[154.408099, \"o\", \"t\"]\n[154.418034, \"o\", \"a\"]\n[154.428065, \"o\", \"r\"]\n[154.455016, \"o\", \" \"]\n[154.469614, \"o\", \"-\"]\n[154.579003, \"o\", \"-\"]\n[154.599039, \"o\", \"p\"]\n[154.717754, \"o\", \"r\"]\n[154.735811, \"o\", \"o\"]\n[154.763071, \"o\", \"g\"]\n[154.775033, \"o\", \"r\"]\n[154.882988, \"o\", \"e\"]\n[155.083346, \"o\", \"s\"]\n[155.146533, \"o\", \"s\"]\n[155.189637, \"o\", \" \"]\n[155.23149, \"o\", \":\"]\n[155.267181, \"o\", \":\"]\n[155.330171, \"o\", \"b\"]\n[155.462667, \"o\", \"a\"]\n[155.477237, \"o\", \"c\"]\n[155.494642, \"o\", \"k\"]\n[155.503974, \"o\", \"u\"]\n[155.515675, \"o\", \"p\"]\n[155.595249, \"o\", \"2\"]\n[155.604431, \"o\", \" \"]\n[155.614019, \"o\", \"b\"]\n[155.643395, \"o\", \"a\"]\n[155.652943, \"o\", \"c\"]\n[155.691221, \"o\", \"k\"]\n[155.734922, \"o\", \"u\"]\n[155.744399, \"o\", \"p\"]\n[155.753882, \"o\", \".\"]\n[155.802861, \"o\", \"t\"]\n[155.977131, \"o\", \"a\"]\n[155.986236, \"o\", \"r\"]\n[156.087515, \"o\", \".\"]\n[156.136273, \"o\", \"g\"]\n[156.160903, \"o\", \"z\"]\n[156.198554, \"o\", \"\\r\\n\"]\n[157.172127, \"o\", \"Calculating size                                                                \\r\"]\n[157.219086, \"o\", \"  0.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.232012, \"o\", \"  0.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.247489, \"o\", \"  0.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.262907, \"o\", \"  0.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.279081, \"o\", \"  0.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.300524, \"o\", \"  0.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.315831, \"o\", \"  0.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.330977, \"o\", \"  0.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.345515, \"o\", \"  0.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.360863, \"o\", \"  0.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.375449, \"o\", \"  1.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.408876, \"o\", \"  1.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.422884, \"o\", \"  1.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.438354, \"o\", \"  1.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.452712, \"o\", \"  1.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.467172, \"o\", \"  1.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.499915, \"o\", \"  1.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.515169, \"o\", \"  1.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.529275, \"o\", \"  1.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.543338, \"o\", \"  1.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.581406, \"o\", \"  2.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.596978, \"o\", \"  2.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.611373, \"o\", \"  2.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.626227, \"o\", \"  2.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.640994, \"o\", \"  2.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.725886, \"o\", \"  2.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.745778, \"o\", \"  2.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.760489, \"o\", \"  2.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.775759, \"o\", \"  2.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.791933, \"o\", \"  2.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.80658, \"o\", \"  3.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.821733, \"o\", \"  3.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.836702, \"o\", \"  3.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.85165, \"o\", \"  3.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.868112, \"o\", \"  3.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.882944, \"o\", \"  3.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.897961, \"o\", \"  3.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.912838, \"o\", \"  3.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.92886, \"o\", \"  3.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.943857, \"o\", \"  3.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[157.959085, \"o\", \"  4.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.006007, \"o\", \"  4.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.025829, \"o\", \"  4.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.041193, \"o\", \"  4.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.056682, \"o\", \"  4.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.072103, \"o\", \"  4.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.088539, \"o\", \"  4.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.103897, \"o\", \"  4.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.119636, \"o\", \"  4.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.134689, \"o\", \"  4.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.205307, \"o\", \"  5.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.221344, \"o\", \"  5.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.236706, \"o\", \"  5.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.252215, \"o\", \"  5.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.268821, \"o\", \"  5.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.284465, \"o\", \"  5.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.300428, \"o\", \"  5.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.315921, \"o\", \"  5.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.332981, \"o\", \"  5.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.348576, \"o\", \"  5.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.364666, \"o\", \"  6.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.380214, \"o\", \"  6.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.397047, \"o\", \"  6.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.412564, \"o\", \"  6.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.42787, \"o\", \"  6.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.443443, \"o\", \"  6.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.48121, \"o\", \"  6.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.500545, \"o\", \"  6.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.516723, \"o\", \"  6.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.533792, \"o\", \"  6.9% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.548206, \"o\", \"  7.0% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.58743, \"o\", \"  7.1% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.607343, \"o\", \"  7.2% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.622246, \"o\", \"  7.3% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.637241, \"o\", \"  7.4% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.653785, \"o\", \"  7.5% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.669092, \"o\", \"  7.6% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.68441, \"o\", \"  7.7% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.704085, \"o\", \"  7.8% Processing: Wallpaper/bigcollection/Pse...nian_001_OpenCL_45154214_8K.jpg\\r\"]\n[158.744556, \"o\", \"  7.9% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.760259, \"o\", \"  8.0% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.776392, \"o\", \"  8.1% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.793498, \"o\", \"  8.2% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.8327, \"o\", \"  8.3% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.850863, \"o\", \"  8.4% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.866562, \"o\", \"  8.5% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.88326, \"o\", \"  8.6% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.899004, \"o\", \"  8.7% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.918108, \"o\", \"  8.8% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.947114, \"o\", \"  8.9% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.969053, \"o\", \"  9.0% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[158.984936, \"o\", \"  9.1% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.010189, \"o\", \"  9.2% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.028139, \"o\", \"  9.3% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.108276, \"o\", \"  9.4% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.127413, \"o\", \"  9.5% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.142793, \"o\", \"  9.6% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.158279, \"o\", \"  9.7% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.17327, \"o\", \"  9.8% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.189834, \"o\", \"  9.9% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.205214, \"o\", \" 10.0% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.220659, \"o\", \" 10.1% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.236247, \"o\", \" 10.2% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.25289, \"o\", \" 10.3% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.268312, \"o\", \" 10.4% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.284074, \"o\", \" 10.5% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.300031, \"o\", \" 10.6% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.332223, \"o\", \" 10.7% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.34777, \"o\", \" 10.8% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.38522, \"o\", \" 10.9% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.402322, \"o\", \" 11.0% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.419323, \"o\", \" 11.1% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.434845, \"o\", \" 11.2% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.449991, \"o\", \" 11.3% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.465728, \"o\", \" 11.4% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.504174, \"o\", \" 11.5% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.521585, \"o\", \" 11.6% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.53715, \"o\", \" 11.7% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.553172, \"o\", \" 11.8% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.570076, \"o\", \" 11.9% Processing: Wallpaper/bigcollection/Men...bulb_OpenCL_528814521414_8K.jpg\\r\"]\n[159.592397, \"o\", \" 12.0% Processing: Wallpaper/bigcollection/Gre...ng_under_tree_in_foreground.jpg\\r\"]\n[159.613245, \"o\", \" 12.1% Processing: Wallpaper/bigcollection/Gre...ng_under_tree_in_foreground.jpg\\r\"]\n[159.64795, \"o\", \" 12.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.663153, \"o\", \" 12.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.679807, \"o\", \" 12.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.695152, \"o\", \" 12.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.744481, \"o\", \" 12.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.760973, \"o\", \" 12.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.776544, \"o\", \" 12.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.792091, \"o\", \" 12.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.807627, \"o\", \" 13.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.824415, \"o\", \" 13.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.839658, \"o\", \" 13.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.856632, \"o\", \" 13.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.886813, \"o\", \" 13.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.907035, \"o\", \" 13.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.922753, \"o\", \" 13.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.938148, \"o\", \" 13.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.969351, \"o\", \" 13.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[159.986611, \"o\", \" 13.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.002259, \"o\", \" 14.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.018123, \"o\", \" 14.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.066077, \"o\", \" 14.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.0814, \"o\", \" 14.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.098185, \"o\", \" 14.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.11377, \"o\", \" 14.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.185687, \"o\", \" 14.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.204434, \"o\", \" 14.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.220251, \"o\", \" 14.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.236801, \"o\", \" 14.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.25281, \"o\", \" 15.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.269864, \"o\", \" 15.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.284684, \"o\", \" 15.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.301911, \"o\", \" 15.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.318073, \"o\", \" 15.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.339029, \"o\", \" 15.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.356267, \"o\", \" 15.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.372204, \"o\", \" 15.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.388185, \"o\", \" 15.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.404526, \"o\", \" 15.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.421391, \"o\", \" 16.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.447583, \"o\", \" 16.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.466393, \"o\", \" 16.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.485417, \"o\", \" 16.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.519291, \"o\", \" 16.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.53441, \"o\", \" 16.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.551411, \"o\", \" 16.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.567197, \"o\", \" 16.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.606136, \"o\", \" 16.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.621928, \"o\", \" 16.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.638135, \"o\", \" 17.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.653872, \"o\", \" 17.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.669626, \"o\", \" 17.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.694603, \"o\", \" 17.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.712689, \"o\", \" 17.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.756749, \"o\", \" 17.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.772771, \"o\", \" 17.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.789625, \"o\", \" 17.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.805609, \"o\", \" 17.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.821556, \"o\", \" 17.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.838879, \"o\", \" 18.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.871146, \"o\", \" 18.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.889911, \"o\", \" 18.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.905901, \"o\", \" 18.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.925349, \"o\", \" 18.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.941677, \"o\", \" 18.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.978467, \"o\", \" 18.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[160.994806, \"o\", \" 18.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.01088, \"o\", \" 18.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.027933, \"o\", \" 18.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.082066, \"o\", \" 19.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.098727, \"o\", \" 19.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.114477, \"o\", \" 19.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.131563, \"o\", \" 19.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.145861, \"o\", \" 19.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.162872, \"o\", \" 19.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.178381, \"o\", \" 19.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.195497, \"o\", \" 19.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.210998, \"o\", \" 19.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.226604, \"o\", \" 19.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.281901, \"o\", \" 20.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.301155, \"o\", \" 20.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.317754, \"o\", \" 20.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.333414, \"o\", \" 20.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.349715, \"o\", \" 20.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.367718, \"o\", \" 20.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.386536, \"o\", \" 20.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.402073, \"o\", \" 20.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.417741, \"o\", \" 20.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.434854, \"o\", \" 20.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.449254, \"o\", \" 21.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.466199, \"o\", \" 21.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.508892, \"o\", \" 21.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.525813, \"o\", \" 21.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.541559, \"o\", \" 21.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.557149, \"o\", \" 21.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.572844, \"o\", \" 21.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.588465, \"o\", \" 21.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.645283, \"o\", \" 21.8% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.659307, \"o\", \" 21.9% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.676184, \"o\", \" 22.0% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.691533, \"o\", \" 22.1% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.70822, \"o\", \" 22.2% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.723919, \"o\", \" 22.3% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.73965, \"o\", \" 22.4% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.755492, \"o\", \" 22.5% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.772282, \"o\", \" 22.6% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.787919, \"o\", \" 22.7% Processing: Wallpaper/bigcollection/Men...Fold_Box_OpenCL_18915424_8K.jpg\\r\"]\n[161.831364, \"o\", \" 22.8% Processing: Wallpaper/bigcollection/KIFS_OpenCL_54815_5K.jpg             \\r\"]\n[161.8508, \"o\", \" 22.9% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.872417, \"o\", \" 23.0% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.893537, \"o\", \" 23.1% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.91235, \"o\", \" 23.2% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.929587, \"o\", \" 23.3% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.968449, \"o\", \" 23.4% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[161.990515, \"o\", \" 23.5% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[162.00623, \"o\", \" 23.6% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[162.023164, \"o\", \" 23.7% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[162.039545, \"o\", \" 23.8% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[162.05695, \"o\", \" 23.9% Processing: Wallpaper/bigcollection/KIF...penCL_54815_5K.jpg             \\r\"]\n[162.085475, \"o\", \" 24.0% Processing: Wallpaper/bigcollection/ProjectStealth.png                   \\r\"]\n[162.099031, \"o\", \" 24.1% Processing: Wallpaper/bigcollection/Pro...tStealth.png                   \\r\"]\n[162.112845, \"o\", \" 24.2% Processing: Wallpaper/bigcollection/Pro...tStealth.png                   \\r\"]\n[162.131599, \"o\", \" 24.3% Processing: Wallpaper/bigcollection/Pro...tStealth.png                   \\r\"]\n[162.158098, \"o\", \" 24.4% Processing: Wallpaper/bigcollection/KIFS_OpenCL_5434735835_5K.jpg        \\r\"]\n[162.173267, \"o\", \" 24.5% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.244066, \"o\", \" 24.6% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.260231, \"o\", \" 24.7% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.275372, \"o\", \" 24.8% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.290501, \"o\", \" 24.9% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.305736, \"o\", \" 25.0% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.322345, \"o\", \" 25.1% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.337761, \"o\", \" 25.2% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.353173, \"o\", \" 25.3% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.368582, \"o\", \" 25.4% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.383992, \"o\", \" 25.5% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.400764, \"o\", \" 25.6% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.416352, \"o\", \" 25.7% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.432005, \"o\", \" 25.8% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.447552, \"o\", \" 25.9% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.46454, \"o\", \" 26.0% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.480048, \"o\", \" 26.1% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.514279, \"o\", \" 26.2% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.529417, \"o\", \" 26.3% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.546059, \"o\", \" 26.4% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.561448, \"o\", \" 26.5% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.576586, \"o\", \" 26.6% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.611597, \"o\", \" 26.7% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.631237, \"o\", \" 26.8% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.646638, \"o\", \" 26.9% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.66206, \"o\", \" 27.0% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.677412, \"o\", \" 27.1% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.693949, \"o\", \" 27.2% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.724496, \"o\", \" 27.3% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.739752, \"o\", \" 27.4% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.755092, \"o\", \" 27.5% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.781252, \"o\", \" 27.6% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.797449, \"o\", \" 27.7% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.827407, \"o\", \" 27.8% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.847733, \"o\", \" 27.9% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.864316, \"o\", \" 28.0% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.879641, \"o\", \" 28.1% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.896474, \"o\", \" 28.2% Processing: Wallpaper/bigcollection/KIF...penCL_5434735835_5K.jpg        \\r\"]\n[162.95962, \"o\", \" 28.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[162.974269, \"o\", \" 28.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[162.988897, \"o\", \" 28.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.003383, \"o\", \" 28.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.018078, \"o\", \" 28.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.03435, \"o\", \" 28.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.049134, \"o\", \" 28.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.06415, \"o\", \" 29.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.079264, \"o\", \" 29.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.095033, \"o\", \" 29.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.109954, \"o\", \" 29.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.145147, \"o\", \" 29.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.163279, \"o\", \" 29.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.17801, \"o\", \" 29.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.193503, \"o\", \" 29.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.208067, \"o\", \" 29.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.222848, \"o\", \" 29.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.265416, \"o\", \" 30.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.281955, \"o\", \" 30.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.296749, \"o\", \" 30.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.311728, \"o\", \" 30.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.32726, \"o\", \" 30.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.343787, \"o\", \" 30.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.358255, \"o\", \" 30.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.386172, \"o\", \" 30.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.401217, \"o\", \" 30.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.487853, \"o\", \" 30.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.50415, \"o\", \" 31.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.518983, \"o\", \" 31.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.535014, \"o\", \" 31.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.549825, \"o\", \" 31.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.564416, \"o\", \" 31.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.579236, \"o\", \" 31.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.593644, \"o\", \" 31.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.60972, \"o\", \" 31.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.624526, \"o\", \" 31.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.639323, \"o\", \" 31.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.654362, \"o\", \" 32.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.670841, \"o\", \" 32.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.686164, \"o\", \" 32.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.702037, \"o\", \" 32.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.717948, \"o\", \" 32.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.733275, \"o\", \" 32.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.749689, \"o\", \" 32.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.764971, \"o\", \" 32.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.792328, \"o\", \" 32.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.809063, \"o\", \" 32.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.825566, \"o\", \" 33.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.862521, \"o\", \" 33.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.877663, \"o\", \" 33.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.892846, \"o\", \" 33.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.909113, \"o\", \" 33.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.933228, \"o\", \" 33.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.950611, \"o\", \" 33.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[163.965592, \"o\", \" 33.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.010098, \"o\", \" 33.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.026093, \"o\", \" 33.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.041194, \"o\", \" 34.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.05754, \"o\", \" 34.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.072675, \"o\", \" 34.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.087708, \"o\", \" 34.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.105121, \"o\", \" 34.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.205315, \"o\", \" 34.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.221054, \"o\", \" 34.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.236048, \"o\", \" 34.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.251256, \"o\", \" 34.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.266101, \"o\", \" 34.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.282491, \"o\", \" 35.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.297453, \"o\", \" 35.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.312481, \"o\", \" 35.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.327597, \"o\", \" 35.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.342124, \"o\", \" 35.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.358177, \"o\", \" 35.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.373151, \"o\", \" 35.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.38827, \"o\", \" 35.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.403196, \"o\", \" 35.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.419188, \"o\", \" 35.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.434077, \"o\", \" 36.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.449026, \"o\", \" 36.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.463925, \"o\", \" 36.2% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.479708, \"o\", \" 36.3% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.494372, \"o\", \" 36.4% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.509307, \"o\", \" 36.5% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.549395, \"o\", \" 36.6% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.564805, \"o\", \" 36.7% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.579672, \"o\", \" 36.8% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.594262, \"o\", \" 36.9% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.609097, \"o\", \" 37.0% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.625262, \"o\", \" 37.1% Processing: Wallpaper/bigcollection/Gen...ld_Box_OpenCL_4258952414_8K.jpg\\r\"]\n[164.71211, \"o\", \" 37.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.725461, \"o\", \" 37.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.740883, \"o\", \" 37.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.755236, \"o\", \" 37.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.76952, \"o\", \" 37.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.783476, \"o\", \" 37.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.798959, \"o\", \" 37.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.811597, \"o\", \" 37.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.827098, \"o\", \" 38.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.841882, \"o\", \" 38.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.857076, \"o\", \" 38.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.871356, \"o\", \" 38.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.885444, \"o\", \" 38.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.90003, \"o\", \" 38.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.915101, \"o\", \" 38.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.929158, \"o\", \" 38.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.943635, \"o\", \" 38.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[164.95983, \"o\", \" 38.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.016768, \"o\", \" 39.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.031747, \"o\", \" 39.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.045748, \"o\", \" 39.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.060048, \"o\", \" 39.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.074101, \"o\", \" 39.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.089652, \"o\", \" 39.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.104215, \"o\", \" 39.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.118366, \"o\", \" 39.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.132477, \"o\", \" 39.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.146595, \"o\", \" 39.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.161775, \"o\", \" 40.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.216091, \"o\", \" 40.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.235223, \"o\", \" 40.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.250335, \"o\", \" 40.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.264513, \"o\", \" 40.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.27882, \"o\", \" 40.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.29292, \"o\", \" 40.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.308386, \"o\", \" 40.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.325042, \"o\", \" 40.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.339423, \"o\", \" 40.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.354481, \"o\", \" 41.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.370932, \"o\", \" 41.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.386144, \"o\", \" 41.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.406262, \"o\", \" 41.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.439308, \"o\", \" 41.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.457374, \"o\", \" 41.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.471511, \"o\", \" 41.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.485641, \"o\", \" 41.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.499622, \"o\", \" 41.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.540815, \"o\", \" 41.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.555849, \"o\", \" 42.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.5697, \"o\", \" 42.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.584235, \"o\", \" 42.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.598363, \"o\", \" 42.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.613715, \"o\", \" 42.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.679444, \"o\", \" 42.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.693115, \"o\", \" 42.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.707433, \"o\", \" 42.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.721651, \"o\", \" 42.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.739409, \"o\", \" 42.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.753223, \"o\", \" 43.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.767281, \"o\", \" 43.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r\"]\n[165.767825, \"o\", \"l\"]\n[165.808885, \"o\", \"s\"]\n[165.832142, \"o\", \" \"]\n[165.911302, \"o\", \"-\"]\n[165.931544, \"o\", \"l\"]\n[166.132117, \"o\", \"\\r\\n 43.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.4% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.5% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.6% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.7% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.8% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 43.9% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 44.0% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 44.1% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 44.2% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 44.3% Processing: Wallpaper/bigcollection/Man...ale_4D_OpenCL_9648145412_8K.jpg\\r 44.4% Processing: Wallpaper/bigcollection/Man...a\"]\n[166.132282, \"o\", \"le_4D_OpenCL_9648145412_8K.jpg\\r 44.5% Processing: Wallpaper/bigcollection/Trapper_cabin.jpg                    \\r 44.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r 44.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r 44.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r 44.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r 45.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r 45.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.137111, \"o\", \" 45.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.22457, \"o\", \" 45.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.239156, \"o\", \" 45.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.253881, \"o\", \" 45.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.268644, \"o\", \" 45.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.28505, \"o\", \" 45.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.300194, \"o\", \" 45.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.315375, \"o\", \" 45.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.330249, \"o\", \" 46.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.346349, \"o\", \" 46.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.361565, \"o\", \" 46.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.377064, \"o\", \" 46.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.392388, \"o\", \" 46.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.408886, \"o\", \" 46.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.423419, \"o\", \" 46.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.438061, \"o\", \" 46.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.452779, \"o\", \" 46.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.467486, \"o\", \" 46.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.48437, \"o\", \" 47.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.503504, \"o\", \" 47.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.552176, \"o\", \" 47.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.566378, \"o\", \" 47.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.582761, \"o\", \" 47.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.597476, \"o\", \" 47.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.68726, \"o\", \" 47.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.703484, \"o\", \" 47.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.719492, \"o\", \" 47.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.733919, \"o\", \" 47.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.749032, \"o\", \" 48.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.763838, \"o\", \" 48.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.779922, \"o\", \" 48.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.794452, \"o\", \" 48.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.809086, \"o\", \" 48.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.823751, \"o\", \" 48.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.839424, \"o\", \" 48.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.853844, \"o\", \" 48.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.868371, \"o\", \" 48.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.883116, \"o\", \" 48.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.899475, \"o\", \" 49.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.914904, \"o\", \" 49.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.929672, \"o\", \" 49.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.944956, \"o\", \" 49.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.961651, \"o\", \" 49.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.97701, \"o\", \" 49.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[166.992797, \"o\", \" 49.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.027683, \"o\", \" 49.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.044163, \"o\", \" 49.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.059611, \"o\", \" 49.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.074996, \"o\", \" 50.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.105621, \"o\", \" 50.1% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.121448, \"o\", \" 50.2% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.137932, \"o\", \" 50.3% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.15341, \"o\", \" 50.4% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.195728, \"o\", \" 50.5% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.210778, \"o\", \" 50.6% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.227386, \"o\", \" 50.7% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.242559, \"o\", \" 50.8% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.257732, \"o\", \" 50.9% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.272797, \"o\", \" 51.0% Processing: Wallpaper/bigcollection/Man...elbox_-_Variable_8K_6595424.jpg\\r\"]\n[167.309495, \"o\", \" 51.1% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.325907, \"o\", \" 51.2% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.342154, \"o\", \" 51.3% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.359718, \"o\", \" 51.4% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.398424, \"o\", \" 51.5% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.421304, \"o\", \" 51.6% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.437025, \"o\", \" 51.7% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.453464, \"o\", \" 51.8% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.468683, \"o\", \" 51.9% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.484422, \"o\", \" 52.0% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.49948, \"o\", \" 52.1% Processing: Wallpaper/bigcollection/Men...ox_OpenCL_14048152404910_8K.jpg\\r\"]\n[167.544858, \"o\", \" 52.2% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.561287, \"o\", \" 52.3% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.575551, \"o\", \" 52.4% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.589573, \"o\", \" 52.5% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.604085, \"o\", \" 52.6% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.620028, \"o\", \" 52.7% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.63516, \"o\", \" 52.8% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.655216, \"o\", \" 52.9% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.673546, \"o\", \" 53.0% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.712002, \"o\", \" 53.1% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.726797, \"o\", \" 53.2% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.741734, \"o\", \" 53.3% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.756963, \"o\", \" 53.4% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.772975, \"o\", \" 53.5% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.794887, \"o\", \" 53.6% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.820943, \"o\", \" 53.7% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.841302, \"o\", \" 53.8% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.857191, \"o\", \" 53.9% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.878906, \"o\", \" 54.0% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.898181, \"o\", \" 54.1% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.920781, \"o\", \" 54.2% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.940917, \"o\", \" 54.3% Processing: Wallpaper/bigcollection/Sie...er_4D_OpenCL_51241841541_8K.jpg\\r\"]\n[167.971271, \"o\", \" 54.4% Processing: Wallpaper/bigcollection/Airbus_Wing_01798_changed.jpg        \\r\"]\n[167.989545, \"o\", \" 54.5% Processing: Wallpaper/bigcollection/Air..._Wing_01798_changed.jpg        \\r\"]\n[168.003567, \"o\", \" 54.6% Processing: Wallpaper/bigcollection/Air..._Wing_01798_changed.jpg        \\r\"]\n[168.019075, \"o\", \" 54.7% Processing: Wallpaper/bigcollection/Air..._Wing_01798_changed.jpg        \\r\"]\n[168.032985, \"o\", \" 54.8% Processing: Wallpaper/bigcollection/Air..._Wing_01798_changed.jpg        \\r\"]\n[168.066013, \"o\", \" 54.9% Processing: Wallpaper/bigcollection/Holytrinfruitlandpark1b.jpg          \\r\"]\n[168.080007, \"o\", \" 55.0% Processing: Wallpaper/bigcollection/Hol...infruitlandpark1b.jpg          \\r\"]\n[168.095155, \"o\", \" 55.1% Processing: Wallpaper/bigcollection/Hol...infruitlandpark1b.jpg          \\r\"]\n[168.11164, \"o\", \" 55.2% Processing: Wallpaper/bigcollection/Hol...infruitlandpark1b.jpg          \\r\"]\n[168.149798, \"o\", \" 55.3% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.16731, \"o\", \" 55.4% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.182878, \"o\", \" 55.5% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.197377, \"o\", \" 55.6% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.212045, \"o\", \" 55.7% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.251289, \"o\", \" 55.8% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.267957, \"o\", \" 55.9% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.283528, \"o\", \" 56.0% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.299004, \"o\", \" 56.1% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.31406, \"o\", \" 56.2% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.329499, \"o\", \" 56.3% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.345926, \"o\", \" 56.4% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.366048, \"o\", \" 56.5% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.379607, \"o\", \" 56.6% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.400899, \"o\", \" 56.7% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.417018, \"o\", \" 56.8% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.450473, \"o\", \" 56.9% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.464717, \"o\", \" 57.0% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.479182, \"o\", \" 57.1% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.503588, \"o\", \" 57.2% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.524622, \"o\", \" 57.3% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.538213, \"o\", \" 57.4% Processing: Wallpaper/bigcollection/Abo...od_12_OpenCL_45184521485_5K.jpg\\r\"]\n[168.639859, \"o\", \" 57.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.655102, \"o\", \" 57.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.671064, \"o\", \" 57.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.686236, \"o\", \" 57.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.703022, \"o\", \" 57.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.717008, \"o\", \" 58.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.733435, \"o\", \" 58.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.748353, \"o\", \" 58.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.764338, \"o\", \" 58.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.777824, \"o\", \" 58.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.793721, \"o\", \" 58.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.808616, \"o\", \" 58.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.823499, \"o\", \" 58.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.839317, \"o\", \" 58.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.852967, \"o\", \" 58.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.869026, \"o\", \" 59.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.883509, \"o\", \" 59.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.900077, \"o\", \" 59.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.914409, \"o\", \" 59.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.930421, \"o\", \" 59.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.945342, \"o\", \" 59.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[168.999211, \"o\", \" 59.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.016832, \"o\", \" 59.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.031617, \"o\", \" 59.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.046268, \"o\", \" 59.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.061, \"o\", \" 60.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.076713, \"o\", \" 60.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.091634, \"o\", \" 60.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.106464, \"o\", \" 60.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.121532, \"o\", \" 60.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.136652, \"o\", \" 60.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.152763, \"o\", \" 60.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.190922, \"o\", \" 60.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.206963, \"o\", \" 60.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.220276, \"o\", \" 60.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.235989, \"o\", \" 61.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.250363, \"o\", \" 61.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.297467, \"o\", \" 61.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.319013, \"o\", \" 61.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.333854, \"o\", \" 61.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.348863, \"o\", \" 61.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.363703, \"o\", \" 61.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.379802, \"o\", \" 61.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.394659, \"o\", \" 61.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.40965, \"o\", \" 61.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.424664, \"o\", \" 62.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.440895, \"o\", \" 62.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.486794, \"o\", \" 62.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.501244, \"o\", \" 62.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.51593, \"o\", \" 62.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.530869, \"o\", \" 62.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.54712, \"o\", \" 62.6% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.561963, \"o\", \" 62.7% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.576793, \"o\", \" 62.8% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.591753, \"o\", \" 62.9% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.60768, \"o\", \" 63.0% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.643056, \"o\", \" 63.1% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.660544, \"o\", \" 63.2% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.67538, \"o\", \" 63.3% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.691966, \"o\", \" 63.4% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.70663, \"o\", \" 63.5% Processing: Wallpaper/bigcollection/Men...ternion_OpenCL_644289452_8K.jpg\\r\"]\n[169.724173, \"o\", \" 63.6% Processing: Wallpaper/bigcollection/Gabrielsond.jpg                      \\r\"]\n[169.755422, \"o\", \" 63.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.77475, \"o\", \" 63.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.789891, \"o\", \" 63.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.805116, \"o\", \" 64.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.831568, \"o\", \" 64.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.852789, \"o\", \" 64.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.868043, \"o\", \" 64.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.88704, \"o\", \" 64.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.903364, \"o\", \" 64.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.936235, \"o\", \" 64.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.9512, \"o\", \" 64.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.982339, \"o\", \" 64.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[169.999446, \"o\", \" 64.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.015661, \"o\", \" 65.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.03056, \"o\", \" 65.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.092201, \"o\", \" 65.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.106882, \"o\", \" 65.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.123126, \"o\", \" 65.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.1399, \"o\", \" 65.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.155076, \"o\", \" 65.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.169973, \"o\", \" 65.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.186293, \"o\", \" 65.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.201513, \"o\", \" 65.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.21664, \"o\", \" 66.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.231604, \"o\", \" 66.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.248006, \"o\", \" 66.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.263039, \"o\", \" 66.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.355062, \"o\", \" 66.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.369727, \"o\", \" 66.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.384513, \"o\", \" 66.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.40045, \"o\", \" 66.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.415694, \"o\", \" 66.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.430163, \"o\", \" 66.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.44498, \"o\", \" 67.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.461066, \"o\", \" 67.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.475821, \"o\", \" 67.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.490933, \"o\", \" 67.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.50695, \"o\", \" 67.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.523218, \"o\", \" 67.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.538283, \"o\", \" 67.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.553378, \"o\", \" 67.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.568527, \"o\", \" 67.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.584645, \"o\", \" 67.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.599594, \"o\", \" 68.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.614904, \"o\", \" 68.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.629862, \"o\", \" 68.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.659018, \"o\", \" 68.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.678932, \"o\", \" 68.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.693859, \"o\", \" 68.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.716413, \"o\", \" 68.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.734956, \"o\", \" 68.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.763419, \"o\", \" 68.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.781631, \"o\", \" 68.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.796773, \"o\", \" 69.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.813144, \"o\", \" 69.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.837495, \"o\", \" 69.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.854888, \"o\", \" 69.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.869913, \"o\", \" 69.4% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.90722, \"o\", \" 69.5% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.923005, \"o\", \" 69.6% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.937825, \"o\", \" 69.7% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.952976, \"o\", \" 69.8% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.981995, \"o\", \" 69.9% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[170.999233, \"o\", \" 70.0% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[171.013875, \"o\", \" 70.1% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[171.034083, \"o\", \" 70.2% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[171.05009, \"o\", \" 70.3% Processing: Wallpaper/bigcollection/Mix...schwamm_OpenCL_461481542_8K.jpg\\r\"]\n[171.071759, \"o\", \" 70.4% Processing: Wallpaper/bigcollection/Bel..._hair_variant)_(16x9_ratio).jpg\\r\"]\n[171.101, \"o\", \" 70.5% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.120041, \"o\", \" 70.6% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.133117, \"o\", \" 70.7% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.148628, \"o\", \" 70.8% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.170476, \"o\", \" 70.9% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.189523, \"o\", \" 71.0% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.204942, \"o\", \" 71.1% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.231339, \"o\", \" 71.2% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.247225, \"o\", \" 71.3% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.278438, \"o\", \" 71.4% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.294749, \"o\", \" 71.5% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.308543, \"o\", \" 71.6% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.32423, \"o\", \" 71.7% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.343463, \"o\", \" 71.8% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.358279, \"o\", \" 71.9% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.401928, \"o\", \" 72.0% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.417339, \"o\", \" 72.1% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.433765, \"o\", \" 72.2% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.448139, \"o\", \" 72.3% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.463803, \"o\", \" 72.4% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.478013, \"o\", \" 72.5% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.492003, \"o\", \" 72.6% Processing: Wallpaper/bigcollection/Sie..._4D_OpenCL_2154188450481_8K.jpg\\r\"]\n[171.528374, \"o\", \" 72.7% Processing: Wallpaper/bigcollection/Abox_4D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.544588, \"o\", \" 72.8% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.559642, \"o\", \" 72.9% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.573521, \"o\", \" 73.0% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.587608, \"o\", \" 73.1% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.6351, \"o\", \" 73.2% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.652608, \"o\", \" 73.3% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.666452, \"o\", \" 73.4% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.680621, \"o\", \" 73.5% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.69472, \"o\", \" 73.6% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.709747, \"o\", \" 73.7% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.723682, \"o\", \" 73.8% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.738028, \"o\", \" 73.9% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.752058, \"o\", \" 74.0% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.76719, \"o\", \" 74.1% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.790642, \"o\", \" 74.2% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.820514, \"o\", \" 74.3% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.834545, \"o\", \" 74.4% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.849766, \"o\", \" 74.5% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.863903, \"o\", \" 74.6% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.923574, \"o\", \" 74.7% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.942274, \"o\", \" 74.8% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.957763, \"o\", \" 74.9% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.971772, \"o\", \" 75.0% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[171.986089, \"o\", \" 75.1% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.000346, \"o\", \" 75.2% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.015555, \"o\", \" 75.3% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.029527, \"o\", \" 75.4% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.043727, \"o\", \" 75.5% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.057704, \"o\", \" 75.6% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.072116, \"o\", \" 75.7% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.087552, \"o\", \" 75.8% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.102046, \"o\", \" 75.9% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.147613, \"o\", \" 76.0% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.161519, \"o\", \" 76.1% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.176861, \"o\", \" 76.2% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.191243, \"o\", \" 76.3% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.205521, \"o\", \" 76.4% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.219614, \"o\", \" 76.5% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.235435, \"o\", \" 76.6% Processing: Wallpaper/bigcollection/Abo...D_OpenCL_545185481_8K.jpg      \\r\"]\n[172.335066, \"o\", \" 76.7% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.349849, \"o\", \" 76.8% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.365366, \"o\", \" 76.9% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.381936, \"o\", \" 77.0% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.397341, \"o\", \" 77.1% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.412546, \"o\", \" 77.2% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.427934, \"o\", \" 77.3% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.444458, \"o\", \" 77.4% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.459873, \"o\", \" 77.5% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.475137, \"o\", \" 77.6% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.490889, \"o\", \" 77.7% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.507471, \"o\", \" 77.8% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.522782, \"o\", \" 77.9% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.537746, \"o\", \" 78.0% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.553095, \"o\", \" 78.1% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.568446, \"o\", \" 78.2% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.585084, \"o\", \" 78.3% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.600602, \"o\", \" 78.4% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.616157, \"o\", \" 78.5% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.636866, \"o\", \" 78.6% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.656337, \"o\", \" 78.7% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.676685, \"o\", \" 78.8% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.695044, \"o\", \" 78.9% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.795084, \"o\", \" 79.0% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.811192, \"o\", \" 79.1% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.826313, \"o\", \" 79.2% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.841796, \"o\", \" 79.3% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.857509, \"o\", \" 79.4% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.873717, \"o\", \" 79.5% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.888989, \"o\", \" 79.6% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.904218, \"o\", \" 79.7% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.920812, \"o\", \" 79.8% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.936174, \"o\", \" 79.9% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.951402, \"o\", \" 80.0% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.966554, \"o\", \" 80.1% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.981776, \"o\", \" 80.2% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[172.998078, \"o\", \" 80.3% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.017396, \"o\", \" 80.4% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.034055, \"o\", \" 80.5% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.049362, \"o\", \" 80.6% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.065899, \"o\", \" 80.7% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.081107, \"o\", \" 80.8% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.097665, \"o\", \" 80.9% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.111775, \"o\", \" 81.0% Processing: Wallpaper/bigcollection/Sie...nski_4D_OpenCL_485274854_5K.jpg\\r\"]\n[173.148292, \"o\", \" 81.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.165472, \"o\", \" 81.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.181997, \"o\", \" 81.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.198583, \"o\", \" 81.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.216137, \"o\", \" 81.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.245988, \"o\", \" 81.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.262544, \"o\", \" 81.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.278612, \"o\", \" 81.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.313541, \"o\", \" 81.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.333354, \"o\", \" 82.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.349458, \"o\", \" 82.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.366892, \"o\", \" 82.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.381392, \"o\", \" 82.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.416691, \"o\", \" 82.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.433961, \"o\", \" 82.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.451384, \"o\", \" 82.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.467403, \"o\", \" 82.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.516534, \"o\", \" 82.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.531702, \"o\", \" 82.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.547214, \"o\", \" 83.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.564017, \"o\", \" 83.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.578307, \"o\", \" 83.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.595338, \"o\", \" 83.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.610696, \"o\", \" 83.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.627217, \"o\", \" 83.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.653623, \"o\", \" 83.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.669272, \"o\", \" 83.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.684708, \"o\", \" 83.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.727221, \"o\", \" 83.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.746924, \"o\", \" 84.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.762108, \"o\", \" 84.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.777159, \"o\", \" 84.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.792423, \"o\", \" 84.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.80903, \"o\", \" 84.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.82715, \"o\", \" 84.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.847467, \"o\", \" 84.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.922477, \"o\", \" 84.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.939391, \"o\", \" 84.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.956239, \"o\", \" 84.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.971558, \"o\", \" 85.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[173.988201, \"o\", \" 85.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.002262, \"o\", \" 85.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.018968, \"o\", \" 85.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.034114, \"o\", \" 85.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.051034, \"o\", \" 85.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.065118, \"o\", \" 85.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.08173, \"o\", \" 85.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.097284, \"o\", \" 85.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.112741, \"o\", \" 85.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.129045, \"o\", \" 86.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.184503, \"o\", \" 86.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.206856, \"o\", \" 86.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.223262, \"o\", \" 86.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.239863, \"o\", \" 86.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.253836, \"o\", \" 86.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.270586, \"o\", \" 86.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.286007, \"o\", \" 86.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.30157, \"o\", \" 86.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.318006, \"o\", \" 86.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.333644, \"o\", \" 87.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.350304, \"o\", \" 87.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.36464, \"o\", \" 87.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.389727, \"o\", \" 87.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.407212, \"o\", \" 87.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.422833, \"o\", \" 87.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.453296, \"o\", \" 87.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.467395, \"o\", \" 87.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.520985, \"o\", \" 87.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.533525, \"o\", \" 87.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.55016, \"o\", \" 88.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.56416, \"o\", \" 88.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.580874, \"o\", \" 88.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.596654, \"o\", \" 88.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.614557, \"o\", \" 88.4% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.628951, \"o\", \" 88.5% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.645523, \"o\", \" 88.6% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.660816, \"o\", \" 88.7% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.675584, \"o\", \" 88.8% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.704816, \"o\", \" 88.9% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.722559, \"o\", \" 89.0% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.737936, \"o\", \" 89.1% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.756118, \"o\", \" 89.2% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.77368, \"o\", \" 89.3% Processing: Wallpaper/bigcollection/Ver...engerschwamm_OpenCl_6184524.jpg\\r\"]\n[174.799303, \"o\", \" 89.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.832351, \"o\", \" 89.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.845368, \"o\", \" 89.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.863336, \"o\", \" 89.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.879711, \"o\", \" 89.8% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.896108, \"o\", \" 89.9% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.916611, \"o\", \" 90.0% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.939868, \"o\", \" 90.1% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.956109, \"o\", \" 90.2% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.980547, \"o\", \" 90.3% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[174.99835, \"o\", \" 90.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.024687, \"o\", \" 90.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.042584, \"o\", \" 90.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.075226, \"o\", \" 90.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.090554, \"o\", \" 90.8% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.107221, \"o\", \" 90.9% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.148647, \"o\", \" 91.0% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.163581, \"o\", \" 91.1% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.178784, \"o\", \" 91.2% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.193848, \"o\", \" 91.3% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.210235, \"o\", \" 91.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.22529, \"o\", \" 91.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.280281, \"o\", \" 91.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.295515, \"o\", \" 91.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.31194, \"o\", \" 91.8% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.327947, \"o\", \" 91.9% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.342733, \"o\", \" 92.0% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.357877, \"o\", \" 92.1% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.374404, \"o\", \" 92.2% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.388429, \"o\", \" 92.3% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.40491, \"o\", \" 92.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.425357, \"o\", \" 92.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.442092, \"o\", \" 92.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.483321, \"o\", \" 92.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.498389, \"o\", \" 92.8% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.513452, \"o\", \" 92.9% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.530094, \"o\", \" 93.0% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.545353, \"o\", \" 93.1% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.579634, \"o\", \" 93.2% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.593214, \"o\", \" 93.3% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.609788, \"o\", \" 93.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.625037, \"o\", \" 93.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.640266, \"o\", \" 93.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.656135, \"o\", \" 93.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.688247, \"o\", \" 93.8% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.703239, \"o\", \" 93.9% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.718576, \"o\", \" 94.0% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.754452, \"o\", \" 94.1% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.769853, \"o\", \" 94.2% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.770073, \"o\", \"\\r\\n\"]\n[175.784359, \"o\", \" 94.3% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.800622, \"o\", \" 94.4% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.815985, \"o\", \" 94.5% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.837175, \"o\", \" 94.6% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.857703, \"o\", \" 94.7% Processing: Wallpaper/bigcollection/Men...schwamm_OpenCL_955141845_8K.jpg\\r\"]\n[175.942193, \"o\", \" 94.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[175.961827, \"o\", \" 94.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[175.97792, \"o\", \" 95.0% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[175.995471, \"o\", \" 95.1% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.01187, \"o\", \" 95.2% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.028298, \"o\", \" 95.3% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.045597, \"o\", \" 95.4% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.060698, \"o\", \" 95.5% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.078263, \"o\", \" 95.6% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.093876, \"o\", \" 95.7% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.10991, \"o\", \" 95.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.125699, \"o\", \" 95.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.142577, \"o\", \" 96.0% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.158303, \"o\", \" 96.1% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.173966, \"o\", \" 96.2% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.190604, \"o\", \" 96.3% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.206311, \"o\", \" 96.4% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.222587, \"o\", \" 96.5% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.238229, \"o\", \" 96.6% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.259845, \"o\", \" 96.7% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.276799, \"o\", \" 96.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.301942, \"o\", \" 96.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.31995, \"o\", \" 97.0% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.350236, \"o\", \" 97.1% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.37165, \"o\", \" 97.2% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.387226, \"o\", \" 97.3% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.402999, \"o\", \" 97.4% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.418169, \"o\", \" 97.5% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.511296, \"o\", \" 97.6% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.526762, \"o\", \" 97.7% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.542462, \"o\", \" 97.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.558234, \"o\", \" 97.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.575231, \"o\", \" 98.0% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.590746, \"o\", \" 98.1% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.606485, \"o\", \" 98.2% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.622891, \"o\", \" 98.3% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.636901, \"o\", \" 98.4% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.653178, \"o\", \" 98.5% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.667977, \"o\", \" 98.6% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.68731, \"o\", \" 98.7% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.70233, \"o\", \" 98.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.718126, \"o\", \" 98.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.733087, \"o\", \" 99.0% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.748853, \"o\", \" 99.1% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.765928, \"o\", \" 99.2% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.78199, \"o\", \" 99.3% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.797818, \"o\", \" 99.4% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.813269, \"o\", \" 99.5% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.847059, \"o\", \" 99.6% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.865025, \"o\", \" 99.7% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.881058, \"o\", \" 99.8% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.89715, \"o\", \" 99.9% Processing: Wallpaper/bigcollection/Fre...ial_projects._(14168975789).jpg\\r\"]\n[176.913918, \"o\", \"                                                                                \\r\"]\n[176.990837, \"o\", \"$ #\"]\n[177.002124, \"o\", \" \"]\n[177.031029, \"o\", \"Y\"]\n[177.07908, \"o\", \"o\"]\n[177.091476, \"o\", \"u\"]\n[177.155024, \"o\", \" \"]\n[177.179734, \"o\", \"c\"]\n[177.207211, \"o\", \"a\"]\n[177.22278, \"o\", \"n\"]\n[177.27989, \"o\", \" \"]\n[177.292519, \"o\", \"m\"]\n[177.326473, \"o\", \"o\"]\n[177.341919, \"o\", \"u\"]\n[177.40636, \"o\", \"n\"]\n[177.415817, \"o\", \"t\"]\n[177.425138, \"o\", \" \"]\n[177.469067, \"o\", \"a\"]\n[177.499804, \"o\", \"n\"]\n[177.509376, \"o\", \" \"]\n[177.539022, \"o\", \"a\"]\n[177.637345, \"o\", \"r\"]\n[177.665967, \"o\", \"c\"]\n[177.711071, \"o\", \"h\"]\n[177.852891, \"o\", \"i\"]\n[177.875061, \"o\", \"v\"]\n[177.996486, \"o\", \"e\"]\n[178.014829, \"o\", \" \"]\n[178.139564, \"o\", \"o\"]\n[178.153, \"o\", \"r\"]\n[178.340782, \"o\", \" \"]\n[178.431104, \"o\", \"e\"]\n[178.554985, \"o\", \"v\"]\n[178.640138, \"o\", \"e\"]\n[178.653782, \"o\", \"n\"]\n[178.68707, \"o\", \" \"]\n[178.696599, \"o\", \"t\"]\n[178.73106, \"o\", \"h\"]\n[178.785567, \"o\", \"e\"]\n[178.815053, \"o\", \" \"]\n[178.882046, \"o\", \"w\"]\n[178.891573, \"o\", \"h\"]\n[178.922301, \"o\", \"o\"]\n[178.934767, \"o\", \"l\"]\n[178.944116, \"o\", \"e\"]\n[179.14435, \"o\", \" \"]\n[179.153748, \"o\", \"r\"]\n[179.165375, \"o\", \"e\"]\n[179.182938, \"o\", \"p\"]\n[179.19851, \"o\", \"o\"]\n[179.211035, \"o\", \"s\"]\n[179.273267, \"o\", \"i\"]\n[179.282781, \"o\", \"t\"]\n[179.313448, \"o\", \"o\"]\n[179.323019, \"o\", \"r\"]\n[179.334972, \"o\", \"y\"]\n[179.503097, \"o\", \":\"]\n[179.680855, \"o\", \"\\r\\ntotal 821176\\r\\r\\ndrwxr-xr-x 3 root root      4096 Jul  6 21:30 Wallpaper\\r\\r\\ndrwxr-xr-x 3 root root      4096 Jul  6 21:28 Wallpaper.orig\\r\\r\\n-rw------- 1 root root 398438415 Jul  6 21:33 backup.tar.gz\\r\\r\\n-rw-r--r-- 1 root root  22929200 Jun  5 21:42 borg-linux64\\r\\r\\n-rw-r--r-- 1 root root       862 Jun  5 21:42 borg-linux64.asc\\r\\r\\n-rw------- 1 root root     66582 Jul  6 21:32 file.html\\r\\r\\n-rw-r--r-- 1 root root 419430400 Jul  6 21:30 loopbackfile.img\\r\\r\\n$ $ $ m\"]\n[179.746167, \"o\", \"k\"]\n[179.758958, \"o\", \"d\"]\n[179.768486, \"o\", \"i\"]\n[179.791088, \"o\", \"r\"]\n[179.991469, \"o\", \" \"]\n[180.098874, \"o\", \"/\"]\n[180.119267, \"o\", \"t\"]\n[180.260824, \"o\", \"m\"]\n[180.320436, \"o\", \"p\"]\n[180.486884, \"o\", \"/\"]\n[180.510496, \"o\", \"m\"]\n[180.570831, \"o\", \"o\"]\n[180.5904, \"o\", \"u\"]\n[180.623022, \"o\", \"n\"]\n[180.632463, \"o\", \"t\"]\n[180.671542, \"o\", \"\\r\\nb\"]\n[180.774426, \"o\", \"o\"]\n[180.796051, \"o\", \"r\"]\n[180.843047, \"o\", \"g\"]\n[180.861582, \"o\", \" \"]\n[180.909768, \"o\", \"m\"]\n[180.959072, \"o\", \"o\"]\n[181.012766, \"o\", \"u\"]\n[181.050466, \"o\", \"n\"]\n[181.059659, \"o\", \"t\"]\n[181.125752, \"o\", \" \"]\n[181.171124, \"o\", \":\"]\n[181.225894, \"o\", \":\"]\n[181.256531, \"o\", \" \"]\n[181.303266, \"o\", \"/\"]\n[181.315278, \"o\", \"t\"]\n[181.343582, \"o\", \"m\"]\n[181.371377, \"o\", \"p\"]\n[181.439985, \"o\", \"/\"]\n[181.46064, \"o\", \"m\"]\n[181.479426, \"o\", \"o\"]\n[181.56903, \"o\", \"u\"]\n[181.578686, \"o\", \"n\"]\n[181.651202, \"o\", \"t\"]\n[181.70471, \"o\", \"\\r\\nl\"]\n[181.735837, \"o\", \"s\"]\n[181.867009, \"o\", \" \"]\n[181.92209, \"o\", \"-\"]\n[181.947235, \"o\", \"l\"]\n[182.02963, \"o\", \"a\"]\n[182.038794, \"o\", \" \"]\n[182.053892, \"o\", \"/\"]\n[182.062896, \"o\", \"t\"]\n[182.119006, \"o\", \"m\"]\n[182.130552, \"o\", \"p\"]\n[182.211559, \"o\", \"/\"]\n[182.230681, \"o\", \"m\"]\n[182.282454, \"o\", \"o\"]\n[182.291818, \"o\", \"u\"]\n[182.301301, \"o\", \"n\"]\n[182.31049, \"o\", \"t\"]\n[182.368951, \"o\", \"\\r\\n\"]\n[182.369429, \"o\", \"$ b\"]\n[182.413573, \"o\", \"o\"]\n[182.423034, \"o\", \"r\"]\n[182.598887, \"o\", \"g\"]\n[182.751119, \"o\", \" \"]\n[182.783431, \"o\", \"u\"]\n[182.796922, \"o\", \"m\"]\n[182.806458, \"o\", \"o\"]\n[182.843154, \"o\", \"u\"]\n[182.935406, \"o\", \"n\"]\n[183.062912, \"o\", \"t\"]\n[183.099442, \"o\", \" \"]\n[183.113601, \"o\", \"/\"]\n[183.135259, \"o\", \"t\"]\n[183.175463, \"o\", \"m\"]\n[183.247234, \"o\", \"p\"]\n[183.256812, \"o\", \"/\"]\n[183.384711, \"o\", \"m\"]\n[183.427408, \"o\", \"o\"]\n[183.436978, \"o\", \"u\"]\n[183.599101, \"o\", \"n\"]\n[183.632998, \"o\", \"t\"]\n[183.702153, \"o\", \"\\r\\n$ total 4\\r\"]\n[183.702958, \"o\", \"\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:33 .\\r\\r\\ndrwxrwxrwt 10 root root 4096 Jul  6 21:33 ..\\r\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:31 backup-block-device\\r\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:30 backup1\\r\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:30 backup2\\r\\r\\n\"]\n[183.70361, \"o\", \"drwxr-xr-x  1 root root    0 Jul  6 21:30 backup3\\r\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:31 root-2022-07-06T21:31:12\\r\\r\\ndrwxr-xr-x  1 root root    0 Jul  6 21:31 root-2022-07-06T21:31:28\\r\\r\\n$ \\r\\n\"]\n[183.704007, \"o\", \"#\"]\n[183.782891, \"o\", \" \"]\n[183.825479, \"o\", \"T\"]\n[183.938756, \"o\", \"h\"]\n[184.065807, \"o\", \"a\"]\n[184.095759, \"o\", \"t\"]\n[184.106989, \"o\", \"'\"]\n[184.172356, \"o\", \"s\"]\n[184.333635, \"o\", \" \"]\n[184.450955, \"o\", \"i\"]\n[184.482269, \"o\", \"t\"]\n[184.568569, \"o\", \",\"]\n[184.589754, \"o\", \" \"]\n[184.650885, \"o\", \"b\"]\n[184.680299, \"o\", \"u\"]\n[184.730828, \"o\", \"t\"]\n[184.768041, \"o\", \" \"]\n[184.791356, \"o\", \"o\"]\n[184.849649, \"o\", \"f\"]\n[184.932911, \"o\", \" \"]\n[184.966199, \"o\", \"c\"]\n[184.977311, \"o\", \"o\"]\n[184.986748, \"o\", \"u\"]\n[185.020926, \"o\", \"r\"]\n[185.13723, \"o\", \"s\"]\n[185.189548, \"o\", \"e\"]\n[185.230866, \"o\", \" \"]\n[185.331133, \"o\", \"t\"]\n[185.454426, \"o\", \"h\"]\n[185.544653, \"o\", \"e\"]\n[185.586996, \"o\", \"r\"]\n[185.653352, \"o\", \"e\"]\n[185.701637, \"o\", \" \"]\n[185.710915, \"o\", \"i\"]\n[185.7361, \"o\", \"s\"]\n[185.745458, \"o\", \" \"]\n[185.769768, \"o\", \"m\"]\n[185.805076, \"o\", \"o\"]\n[185.886372, \"o\", \"r\"]\n[185.896491, \"o\", \"e\"]\n[185.948792, \"o\", \" \"]\n[185.958167, \"o\", \"t\"]\n[186.029468, \"o\", \"o\"]\n[186.104744, \"o\", \" \"]\n[186.139952, \"o\", \"e\"]\n[186.340348, \"o\", \"x\"]\n[186.349559, \"o\", \"p\"]\n[186.435894, \"o\", \"l\"]\n[186.458035, \"o\", \"o\"]\n[186.506422, \"o\", \"r\"]\n[186.515622, \"o\", \"e\"]\n[186.707953, \"o\", \",\"]\n[186.79823, \"o\", \" \"]\n[186.812457, \"o\", \"s\"]\n[186.821601, \"o\", \"o\"]\n[186.892955, \"o\", \" \"]\n[186.951022, \"o\", \"h\"]\n[186.964333, \"o\", \"a\"]\n[186.973626, \"o\", \"v\"]\n[187.018026, \"o\", \"e\"]\n[187.201321, \"o\", \" \"]\n[187.264622, \"o\", \"a\"]\n[187.302927, \"o\", \" \"]\n[187.314958, \"o\", \"l\"]\n[187.366361, \"o\", \"o\"]\n[187.429373, \"o\", \"o\"]\n[187.446645, \"o\", \"k\"]\n[187.455846, \"o\", \" \"]\n[187.656118, \"o\", \"a\"]\n[187.759311, \"o\", \"t\"]\n[187.959528, \"o\", \" \"]\n[187.98579, \"o\", \"t\"]\n[188.042958, \"o\", \"h\"]\n[188.078209, \"o\", \"e\"]\n[188.278473, \"o\", \" \"]\n[188.289572, \"o\", \"d\"]\n[188.323028, \"o\", \"o\"]\n[188.425315, \"o\", \"c\"]\n[188.43457, \"o\", \"s\"]\n[188.532843, \"o\", \".\"]\n[188.567197, \"o\", \"\\r\\n\"]\n[188.567499, \"o\", \"$ $ \"]\n"
  },
  {
    "path": "docs/misc/asciinema/advanced.tcl",
    "content": "# Configuration for send -h\n# Tries to emulate a human typing\n# Tweak this if typing is too fast or too slow\nset send_human {.05 .1 1 .01 .2}\n\nset script {\n# For the pro users, here are some advanced features of borg, so you can impress your friends. ;)\n# Note: This screencast was made with __BORG_VERSION__ – older or newer borg versions may behave differently.\n\n# First of all, we can use several environment variables for borg.\n# E.g. we do not want to type in our repo path and password again and again…\nexport BORG_REPO='/media/backup/borgdemo'\nexport BORG_PASSPHRASE='1234'\n# Problem solved, borg will use this automatically… :)\n# We'll use this right away…\n\n## ADVANCED CREATION ##\n\n# We can also use some placeholders in our archive name…\nborg create --stats --progress --compression lz4 ::{user}-{now} Wallpaper\n# Notice the backup name.\n\n# And we can put completely different data, with different backup settings, in our backup. It will be deduplicated, anyway:\nborg create --stats --progress --compression zlib,6 --exclude ~/Downloads/big ::{user}-{now} ~/Downloads\n\n# Or let's backup a device via STDIN.\nsudo dd if=/dev/loop0 bs=10M | borg create --progress --stats ::specialbackup -\n\n# Let's continue with some simple things:\n## USEFUL COMMANDS ##\n# You can show some information about an archive. You can even do it without needing to specify the archive name:\nborg info :: --last 1\n\n# So let's rename our last archive:\nborg rename ::specialbackup backup-block-device\n\nborg info :: --last 1\n\n# A very important step if you choose keyfile mode (where the keyfile is only saved locally) is to export your keyfile and possibly print it, etc.\nborg key export --qr-html :: file.html  # this creates a nice HTML, but when you want something simpler…\nborg key export --paper ::  # this is a \"manual input\"-only backup (but it is also included in the --qr-code option)\n\n## MAINTENANCE ##\n# Sometimes backups get broken or we want a regular \"checkup\" that everything is okay…\nborg check -v ::\n\n# Next problem: Usually you do not have infinite disk space. So you may need to prune your archive…\n# You can tune this in every detail. See the docs for details. Here only a simple example:\nborg prune --list --keep-last 1 --dry-run\n# When actually executing it in a script, you have to use it without the --dry-run option, of course.\n\n## RESTORE ##\n\n# When you want to see the diff between two archives use this command.\n# E.g. what happened between the first two backups?\nborg diff ::backup1 backup2\n# Ah, we added a file, right…\n\n# There are also other ways to extract the data.\n# E.g. as a tar archive.\nborg export-tar --progress ::backup2 backup.tar.gz\nls -l\n\n# You can mount an archive or even the whole repository:\nmkdir /tmp/mount\nborg mount :: /tmp/mount\nls -la /tmp/mount\nborg umount /tmp/mount\n\n# That's it, but of course there is more to explore, so have a look at the docs.\n}\n\nset script [string trim $script]\nset script [string map [list __BORG_VERSION__ [exec borg -V]] $script]\nset script [split $script \\n]\n\nset ::env(PS1) \"$ \"\nset stty_init -echo\nspawn -noecho /bin/sh\nforeach line $script {\n\texpect \"$ \"\n\tsend_user -h $line\\n\n\tsend $line\\n\n}\nexpect \"$ \"\n"
  },
  {
    "path": "docs/misc/asciinema/basic.json",
    "content": "{\"version\": 2, \"width\": 80, \"height\": 24, \"timestamp\": 1657142858, \"env\": {\"SHELL\": \"/bin/bash\", \"TERM\": \"vt100\"}}\n[0.901997, \"o\", \"$ #\"]\n[0.911859, \"o\", \" \"]\n[0.945252, \"o\", \"H\"]\n[1.145466, \"o\", \"e\"]\n[1.219717, \"o\", \"r\"]\n[1.32403, \"o\", \"e\"]\n[1.380192, \"o\", \" \"]\n[1.430453, \"o\", \"y\"]\n[1.440629, \"o\", \"o\"]\n[1.477854, \"o\", \"u\"]\n[1.592158, \"o\", \"'\"]\n[1.604339, \"o\", \"l\"]\n[1.667607, \"o\", \"l\"]\n[1.740872, \"o\", \" \"]\n[1.762133, \"o\", \"s\"]\n[1.783381, \"o\", \"e\"]\n[1.823622, \"o\", \"e\"]\n[1.914891, \"o\", \" \"]\n[1.942144, \"o\", \"s\"]\n[2.003381, \"o\", \"o\"]\n[2.012556, \"o\", \"m\"]\n[2.061939, \"o\", \"e\"]\n[2.071228, \"o\", \" \"]\n[2.15549, \"o\", \"b\"]\n[2.326771, \"o\", \"a\"]\n[2.387008, \"o\", \"s\"]\n[2.449353, \"o\", \"i\"]\n[2.474589, \"o\", \"c\"]\n[2.674869, \"o\", \" \"]\n[2.697142, \"o\", \"c\"]\n[2.719411, \"o\", \"o\"]\n[2.753587, \"o\", \"m\"]\n[2.766854, \"o\", \"m\"]\n[2.815101, \"o\", \"a\"]\n[2.830358, \"o\", \"n\"]\n[2.903606, \"o\", \"d\"]\n[2.918861, \"o\", \"s\"]\n[2.982125, \"o\", \" \"]\n[3.008327, \"o\", \"t\"]\n[3.020509, \"o\", \"o\"]\n[3.165753, \"o\", \" \"]\n[3.174937, \"o\", \"s\"]\n[3.204287, \"o\", \"t\"]\n[3.237478, \"o\", \"a\"]\n[3.261594, \"o\", \"r\"]\n[3.338838, \"o\", \"t\"]\n[3.430109, \"o\", \" \"]\n[3.569377, \"o\", \"w\"]\n[3.593565, \"o\", \"o\"]\n[3.670838, \"o\", \"r\"]\n[3.697097, \"o\", \"k\"]\n[3.714347, \"o\", \"i\"]\n[3.724362, \"o\", \"n\"]\n[3.757578, \"o\", \"g\"]\n[3.957829, \"o\", \" \"]\n[4.086061, \"o\", \"w\"]\n[4.096274, \"o\", \"i\"]\n[4.141494, \"o\", \"t\"]\n[4.160749, \"o\", \"h\"]\n[4.180015, \"o\", \" \"]\n[4.245245, \"o\", \"b\"]\n[4.275507, \"o\", \"o\"]\n[4.304763, \"o\", \"r\"]\n[4.465051, \"o\", \"g\"]\n[4.474258, \"o\", \".\"]\n[4.537729, \"o\", \"\\r\\n\"]\n[4.543921, \"o\", \"$ #\"]\n[4.565316, \"o\", \" \"]\n[4.575589, \"o\", \"N\"]\n[4.696831, \"o\", \"o\"]\n[4.738093, \"o\", \"t\"]\n[4.801261, \"o\", \"e\"]\n[4.810475, \"o\", \":\"]\n[4.819702, \"o\", \" \"]\n[4.830927, \"o\", \"T\"]\n[4.855195, \"o\", \"h\"]\n[4.89738, \"o\", \"i\"]\n[4.939628, \"o\", \"s\"]\n[4.948905, \"o\", \" \"]\n[5.013207, \"o\", \"t\"]\n[5.02248, \"o\", \"e\"]\n[5.057704, \"o\", \"a\"]\n[5.135974, \"o\", \"s\"]\n[5.155228, \"o\", \"e\"]\n[5.229419, \"o\", \"r\"]\n[5.341645, \"o\", \" \"]\n[5.350881, \"o\", \"s\"]\n[5.521412, \"o\", \"c\"]\n[5.536707, \"o\", \"r\"]\n[5.545931, \"o\", \"e\"]\n[5.668293, \"o\", \"e\"]\n[5.741519, \"o\", \"n\"]\n[5.783892, \"o\", \"c\"]\n[5.793081, \"o\", \"a\"]\n[5.858403, \"o\", \"s\"]\n[5.910676, \"o\", \"t\"]\n[6.096058, \"o\", \" \"]\n[6.126284, \"o\", \"w\"]\n[6.198546, \"o\", \"a\"]\n[6.277777, \"o\", \"s\"]\n[6.293117, \"o\", \" \"]\n[6.313411, \"o\", \"m\"]\n[6.349414, \"o\", \"a\"]\n[6.358599, \"o\", \"d\"]\n[6.36784, \"o\", \"e\"]\n[6.434105, \"o\", \" \"]\n[6.452354, \"o\", \"w\"]\n[6.481555, \"o\", \"i\"]\n[6.490802, \"o\", \"t\"]\n[6.499992, \"o\", \"h\"]\n[6.519263, \"o\", \" \"]\n[6.682501, \"o\", \"b\"]\n[6.711788, \"o\", \"o\"]\n[6.779036, \"o\", \"r\"]\n[6.788232, \"o\", \"g\"]\n[6.813434, \"o\", \" \"]\n[6.839723, \"o\", \"1\"]\n[6.848893, \"o\", \".\"]\n[7.008256, \"o\", \"2\"]\n[7.068455, \"o\", \".\"]\n[7.077624, \"o\", \"1\"]\n[7.179898, \"o\", \" \"]\n[7.192112, \"o\", \"–\"]\n[7.227365, \"o\", \" \"]\n[7.301542, \"o\", \"o\"]\n[7.364789, \"o\", \"l\"]\n[7.496018, \"o\", \"d\"]\n[7.544279, \"o\", \"e\"]\n[7.694535, \"o\", \"r\"]\n[7.81182, \"o\", \" \"]\n[7.822025, \"o\", \"o\"]\n[7.83126, \"o\", \"r\"]\n[7.988502, \"o\", \" \"]\n[8.019762, \"o\", \"n\"]\n[8.028979, \"o\", \"e\"]\n[8.146233, \"o\", \"w\"]\n[8.301411, \"o\", \"e\"]\n[8.310614, \"o\", \"r\"]\n[8.353893, \"o\", \" \"]\n[8.43015, \"o\", \"b\"]\n[8.444415, \"o\", \"o\"]\n[8.461635, \"o\", \"r\"]\n[8.529875, \"o\", \"g\"]\n[8.652126, \"o\", \" \"]\n[8.661253, \"o\", \"v\"]\n[8.740513, \"o\", \"e\"]\n[8.872782, \"o\", \"r\"]\n[8.884992, \"o\", \"s\"]\n[8.934245, \"o\", \"i\"]\n[8.987505, \"o\", \"o\"]\n[9.016764, \"o\", \"n\"]\n[9.058061, \"o\", \"s\"]\n[9.093246, \"o\", \" \"]\n[9.148492, \"o\", \"m\"]\n[9.157691, \"o\", \"a\"]\n[9.166916, \"o\", \"y\"]\n[9.184181, \"o\", \" \"]\n[9.257366, \"o\", \"b\"]\n[9.288631, \"o\", \"e\"]\n[9.297848, \"o\", \"h\"]\n[9.328139, \"o\", \"a\"]\n[9.379388, \"o\", \"v\"]\n[9.390641, \"o\", \"e\"]\n[9.399821, \"o\", \" \"]\n[9.507092, \"o\", \"d\"]\n[9.519377, \"o\", \"i\"]\n[9.57966, \"o\", \"f\"]\n[9.670911, \"o\", \"f\"]\n[9.689196, \"o\", \"e\"]\n[9.698414, \"o\", \"r\"]\n[9.722689, \"o\", \"e\"]\n[9.741948, \"o\", \"n\"]\n[9.751156, \"o\", \"t\"]\n[9.760393, \"o\", \"l\"]\n[9.769577, \"o\", \"y\"]\n[9.969826, \"o\", \".\"]\n[10.032, \"o\", \"\\r\\n\"]\n[10.038957, \"o\", \"$ #\"]\n[10.048219, \"o\", \" \"]\n[10.057455, \"o\", \"B\"]\n[10.0818, \"o\", \"u\"]\n[10.128106, \"o\", \"t\"]\n[10.237374, \"o\", \" \"]\n[10.295841, \"o\", \"l\"]\n[10.305257, \"o\", \"e\"]\n[10.341361, \"o\", \"t\"]\n[10.417542, \"o\", \"'\"]\n[10.429003, \"o\", \"s\"]\n[10.461357, \"o\", \" \"]\n[10.661763, \"o\", \"s\"]\n[10.671353, \"o\", \"t\"]\n[10.706527, \"o\", \"a\"]\n[10.733207, \"o\", \"r\"]\n[10.750995, \"o\", \"t\"]\n[10.829478, \"o\", \".\"]\n[10.847759, \"o\", \"\\r\\n\"]\n[10.853699, \"o\", \"$ \\r\\n\"]\n[10.857297, \"o\", \"$ #\"]\n[10.974637, \"o\", \" \"]\n[11.021433, \"o\", \"F\"]\n[11.030913, \"o\", \"i\"]\n[11.048388, \"o\", \"r\"]\n[11.106915, \"o\", \"s\"]\n[11.11632, \"o\", \"t\"]\n[11.140345, \"o\", \" \"]\n[11.204803, \"o\", \"o\"]\n[11.313637, \"o\", \"f\"]\n[11.429012, \"o\", \" \"]\n[11.442311, \"o\", \"a\"]\n[11.451818, \"o\", \"l\"]\n[11.529721, \"o\", \"l\"]\n[11.579317, \"o\", \",\"]\n[11.607295, \"o\", \" \"]\n[11.619676, \"o\", \"y\"]\n[11.714946, \"o\", \"o\"]\n[11.729578, \"o\", \"u\"]\n[11.860181, \"o\", \" \"]\n[11.884661, \"o\", \"c\"]\n[11.897179, \"o\", \"a\"]\n[11.97332, \"o\", \"n\"]\n[11.989804, \"o\", \" \"]\n[12.009297, \"o\", \"a\"]\n[12.025623, \"o\", \"l\"]\n[12.069382, \"o\", \"w\"]\n[12.117614, \"o\", \"a\"]\n[12.201573, \"o\", \"y\"]\n[12.251935, \"o\", \"s\"]\n[12.364579, \"o\", \" \"]\n[12.374052, \"o\", \"g\"]\n[12.413268, \"o\", \"e\"]\n[12.422739, \"o\", \"t\"]\n[12.623041, \"o\", \" \"]\n[12.670536, \"o\", \"h\"]\n[12.737348, \"o\", \"e\"]\n[12.821793, \"o\", \"l\"]\n[12.866273, \"o\", \"p\"]\n[12.893446, \"o\", \":\"]\n[12.95329, \"o\", \"\\r\\n\"]\n[12.959698, \"o\", \"$ b\"]\n[12.99345, \"o\", \"o\"]\n[13.060998, \"o\", \"r\"]\n[13.149973, \"o\", \"g\"]\n[13.244305, \"o\", \" \"]\n[13.380017, \"o\", \"h\"]\n[13.389332, \"o\", \"e\"]\n[13.421149, \"o\", \"l\"]\n[13.430532, \"o\", \"p\"]\n[13.514054, \"o\", \"\\r\\n\"]\n[14.368385, \"o\", \"usage: borg [-V] [-h] [--critical] [--error] [--warning] [--info] [--debug]\"]\n[14.369035, \"o\", \"\\r\\r\\n\"]\n[14.369334, \"o\", \"            [--debug-topic TOPIC] [-p] [--iec] [--log-json]\"]\n[14.369527, \"o\", \"\\r\\r\\n\"]\n[14.369709, \"o\", \"            [--lock-wait SECONDS] [--show-version] [--show-rc]\"]\n[14.36989, \"o\", \"\\r\\r\\n\"]\n[14.370079, \"o\", \"            [--umask M] [--remote-path PATH] [--remote-ratelimit RATE]\"]\n[14.370256, \"o\", \"\\r\\r\\n\"]\n[14.370441, \"o\", \"            [--upload-ratelimit RATE] [--remote-buffer UPLOAD_BUFFER]\"]\n[14.370624, \"o\", \"\\r\\r\\n\"]\n[14.370801, \"o\", \"            [--upload-buffer UPLOAD_BUFFER] [--consider-part-files]\"]\n[14.370976, \"o\", \"\\r\\r\\n\"]\n[14.371151, \"o\", \"            [--debug-profile FILE] [--rsh RSH]\"]\n[14.371358, \"o\", \"\\r\\r\\n\"]\n[14.371536, \"o\", \"            <command> ...\"]\n[14.371713, \"o\", \"\\r\\r\\n\"]\n[14.371888, \"o\", \"\\r\\r\\n\"]\n[14.372204, \"o\", \"Borg - Deduplicated Backups\"]\n[14.372752, \"o\", \"\\r\\r\\n\"]\n[14.373316, \"o\", \"\\r\\r\\n\"]\n[14.373744, \"o\", \"optional arguments:\"]\n[14.374157, \"o\", \"\\r\\r\\n\"]\n[14.374567, \"o\", \"  -V, --version         show version number and exit\"]\n[14.374875, \"o\", \"\\r\\r\\n\"]\n[14.375184, \"o\", \"\\r\\r\\n\"]\n[14.375501, \"o\", \"Common options:\"]\n[14.375811, \"o\", \"\\r\\r\\n\"]\n[14.376121, \"o\", \"  -h, --help            show this help message and exit\"]\n[14.376429, \"o\", \"\\r\\r\\n\"]\n[14.376733, \"o\", \"  --critical            work on log level CRITICAL\"]\n[14.377062, \"o\", \"\\r\\r\\n\"]\n[14.37727, \"o\", \"  --error               work on log level ERROR\"]\n[14.37747, \"o\", \"\\r\\r\\n\"]\n[14.377667, \"o\", \"  --warning             work on log level WARNING (default)\"]\n[14.377871, \"o\", \"\\r\\r\\n\"]\n[14.378072, \"o\", \"  --info, -v, --verbose\"]\n[14.378271, \"o\", \"\\r\\r\\n\"]\n[14.378472, \"o\", \"                        work on log level INFO\"]\n[14.378671, \"o\", \"\\r\\r\\n\"]\n[14.378873, \"o\", \"  --debug               enable debug output, work on log level DEBUG\"]\n[14.379071, \"o\", \"\\r\\r\\n\"]\n[14.379314, \"o\", \"  --debug-topic TOPIC   enable TOPIC debugging (can be specified multiple\"]\n[14.379542, \"o\", \"\\r\\r\\n\"]\n[14.379737, \"o\", \"                        times). The logger path is borg.debug.<TOPIC> if TOPIC\"]\n[14.37995, \"o\", \"\\r\\r\\n\"]\n[14.380162, \"o\", \"                        is not fully qualified.\"]\n[14.380372, \"o\", \"\\r\\r\\n\"]\n[14.380593, \"o\", \"  -p, --progress        show progress information\"]\n[14.38081, \"o\", \"\\r\\r\\n\"]\n[14.381071, \"o\", \"  --iec                 format using IEC units (1KiB = 1024B)\"]\n[14.381241, \"o\", \"\\r\"]\n[14.381315, \"o\", \"\\r\\n\"]\n[14.381543, \"o\", \"  --log-json            Output one JSON object per log line instead of\"]\n[14.381761, \"o\", \"\\r\"]\n[14.381844, \"o\", \"\\r\\n\"]\n[14.382072, \"o\", \"                        formatted text.\"]\n[14.382294, \"o\", \"\\r\"]\n[14.382368, \"o\", \"\\r\\n\"]\n[14.382586, \"o\", \"  --lock-wait SECONDS   wait at most SECONDS for acquiring a repository/cache\"]\n[14.382805, \"o\", \"\\r\"]\n[14.382887, \"o\", \"\\r\\n\"]\n[14.383122, \"o\", \"                        lock (default: 1).\"]\n[14.383368, \"o\", \"\\r\"]\n[14.383442, \"o\", \"\\r\\n\"]\n[14.384227, \"o\", \"  --show-version        show/log the borg version\"]\n[14.38446, \"o\", \"\\r\"]\n[14.384533, \"o\", \"\\r\\n\"]\n[14.384765, \"o\", \"  --show-rc             show/log the return code (rc)\"]\n[14.384993, \"o\", \"\\r\"]\n[14.3851, \"o\", \"\\r\\n\"]\n[14.385332, \"o\", \"  --umask M             set umask to M (local only, default: 0077)\"]\n[14.38558, \"o\", \"\\r\"]\n[14.385653, \"o\", \"\\r\\n\"]\n[14.38589, \"o\", \"  --remote-path PATH    use PATH as borg executable on the remote (default:\"]\n[14.386122, \"o\", \"\\r\"]\n[14.386195, \"o\", \"\\r\\n\"]\n[14.386434, \"o\", \"                        \\\"borg\\\")\"]\n[14.386666, \"o\", \"\\r\"]\n[14.386806, \"o\", \"\\r\\n\"]\n[14.387039, \"o\", \"  --remote-ratelimit RATE\"]\n[14.387292, \"o\", \"\\r\"]\n[14.387434, \"o\", \"\\r\\n\"]\n[14.387668, \"o\", \"                        deprecated, use ``--upload-ratelimit`` instead\"]\n[14.387898, \"o\", \"\\r\"]\n[14.388037, \"o\", \"\\r\\n\"]\n[14.388279, \"o\", \"  --upload-ratelimit RATE\"]\n[14.388509, \"o\", \"\\r\"]\n[14.388648, \"o\", \"\\r\\n\"]\n[14.388884, \"o\", \"                        set network upload rate limit in kiByte/s (default:\"]\n[14.389135, \"o\", \"\\r\"]\n[14.389273, \"o\", \"\\r\\n\"]\n[14.389509, \"o\", \"    \"]\n[14.389743, \"o\", \"                    0=unlimited)\"]\n[14.389979, \"o\", \"\\r\"]\n[14.390123, \"o\", \"\\r\\n\"]\n[14.390378, \"o\", \"  --remote-buffer UPLOAD_BUFFER\"]\n[14.390614, \"o\", \"\\r\"]\n[14.390752, \"o\", \"\\r\\n\"]\n[14.390991, \"o\", \"                        deprecated, use ``--upload-buffer`` instead\"]\n[14.391273, \"o\", \"\\r\"]\n[14.391422, \"o\", \"\\r\\n\"]\n[14.391661, \"o\", \"  --upload-buffer UPLOAD_BUFFER\"]\n[14.391918, \"o\", \"\\r\"]\n[14.392057, \"o\", \"\\r\\n\"]\n[14.392288, \"o\", \"                        set network upload buffer size in MiB. (default: 0=no\"]\n[14.39254, \"o\", \"\\r\"]\n[14.392692, \"o\", \"\\r\\n\"]\n[14.392919, \"o\", \"                        buffer)\"]\n[14.393165, \"o\", \"\\r\"]\n[14.393321, \"o\", \"\\r\\n\"]\n[14.39355, \"o\", \"  --consider-part-files\"]\n[14.39378, \"o\", \"\\r\"]\n[14.393956, \"o\", \"\\r\\n\"]\n[14.394208, \"o\", \"                        treat part files like normal files (e.g. to\"]\n[14.394468, \"o\", \"\\r\"]\n[14.39462, \"o\", \"\\r\\n\"]\n[14.394868, \"o\", \"                        list/extract them)\"]\n[14.39511, \"o\", \"\\r\"]\n[14.395272, \"o\", \"\\r\\n\"]\n[14.395511, \"o\", \"  --debug-profile FILE  Write execution profile in Borg format into FILE. For\"]\n[14.395745, \"o\", \"\\r\"]\n[14.395915, \"o\", \"\\r\\n\"]\n[14.397106, \"o\", \"                        local use a Python-compatible file can be generated by\\r\\r\\n                        suffixing FILE with \\\".pyprof\\\".\\r\\r\\n  --rsh RSH             Use this command to connect to the 'borg serve'\\r\\r\\n                        process (default: 'ssh')\\r\\r\\n\\r\\r\\nrequired arguments:\\r\\r\\n  <command>\\r\\r\\n    benchmark           benchmark command\\r\\r\\n    break-lock          break repository and cache locks\\r\\r\\n    check               verify repository\\r\\r\\n    compact             compact segment files / free space in repo\\r\\r\\n    config              get and set configuration values\\r\\r\\n    create              create backup\\r\\r\\n    debug               debugging command (not intended for normal use)\\r\\r\\n    delete              delete archive\\r\\r\\n    diff                find differences in archive contents\\r\\r\\n    export-tar          create tarball from archive\\r\\r\\n    extract             extract archive contents\\r\\r\\n    info                show repository or archive information\\r\\r\\n    init                initialize empty repository\\r\\r\\n    k\"]\n[14.397135, \"o\", \"ey                 manage repository key\\r\\r\\n    list                list archive or repository contents\\r\\r\\n    mount               mount repository\\r\\r\\n    prune               prune archives\\r\\r\\n    recreate            Re-create archives\\r\\r\\n    rename              rename archive\\r\\r\\n    serve               start repository server process\\r\\r\\n    umount              umount repository\\r\\r\\n    upgrade             upgrade repository format\\r\\r\\n    with-lock           run user command with lock held\\r\\r\\n    import-tar          Create a backup archive from a tarball\\r\\r\\n\"]\n[14.44226, \"o\", \"$ #\"]\n[14.467585, \"o\", \" \"]\n[14.48689, \"o\", \"T\"]\n[14.561135, \"o\", \"h\"]\n[14.623399, \"o\", \"e\"]\n[14.66958, \"o\", \"s\"]\n[14.678779, \"o\", \"e\"]\n[14.689048, \"o\", \" \"]\n[14.69935, \"o\", \"a\"]\n[14.768546, \"o\", \"r\"]\n[14.807766, \"o\", \"e\"]\n[15.008011, \"o\", \" \"]\n[15.017332, \"o\", \"a\"]\n[15.104611, \"o\", \" \"]\n[15.204869, \"o\", \"l\"]\n[15.234109, \"o\", \"o\"]\n[15.252376, \"o\", \"t\"]\n[15.366682, \"o\", \" \"]\n[15.375836, \"o\", \"o\"]\n[15.39412, \"o\", \"f\"]\n[15.441422, \"o\", \" \"]\n[15.450632, \"o\", \"c\"]\n[15.52093, \"o\", \"o\"]\n[15.630962, \"o\", \"m\"]\n[15.750214, \"o\", \"m\"]\n[15.759497, \"o\", \"a\"]\n[15.845708, \"o\", \"n\"]\n[15.910964, \"o\", \"d\"]\n[15.957243, \"o\", \"s\"]\n[16.102504, \"o\", \",\"]\n[16.118774, \"o\", \" \"]\n[16.130979, \"o\", \"s\"]\n[16.266236, \"o\", \"o\"]\n[16.312506, \"o\", \" \"]\n[16.384774, \"o\", \"b\"]\n[16.437017, \"o\", \"e\"]\n[16.505236, \"o\", \"t\"]\n[16.612532, \"o\", \"t\"]\n[16.64879, \"o\", \"e\"]\n[16.781204, \"o\", \"r\"]\n[16.833255, \"o\", \" \"]\n[16.92251, \"o\", \"w\"]\n[17.053713, \"o\", \"e\"]\n[17.081033, \"o\", \" \"]\n[17.107308, \"o\", \"s\"]\n[17.185495, \"o\", \"t\"]\n[17.243755, \"o\", \"a\"]\n[17.277997, \"o\", \"r\"]\n[17.292215, \"o\", \"t\"]\n[17.35945, \"o\", \" \"]\n[17.439708, \"o\", \"w\"]\n[17.463952, \"o\", \"i\"]\n[17.506213, \"o\", \"t\"]\n[17.525383, \"o\", \"h\"]\n[17.558644, \"o\", \" \"]\n[17.572897, \"o\", \"a\"]\n[17.636143, \"o\", \" \"]\n[17.741311, \"o\", \"f\"]\n[17.810552, \"o\", \"e\"]\n[17.90177, \"o\", \"w\"]\n[18.005029, \"o\", \":\"]\n[18.014935, \"o\", \"\\r\\n\"]\n[18.020184, \"o\", \"$ #\"]\n[18.063288, \"o\", \" \"]\n[18.072418, \"o\", \"L\"]\n[18.160547, \"o\", \"e\"]\n[18.360844, \"o\", \"t\"]\n[18.561132, \"o\", \"'\"]\n[18.593287, \"o\", \"s\"]\n[18.62653, \"o\", \" \"]\n[18.717781, \"o\", \"c\"]\n[18.826025, \"o\", \"r\"]\n[18.835218, \"o\", \"e\"]\n[18.873402, \"o\", \"a\"]\n[18.882603, \"o\", \"t\"]\n[18.899863, \"o\", \"e\"]\n[18.910079, \"o\", \" \"]\n[18.953439, \"o\", \"a\"]\n[19.11271, \"o\", \" \"]\n[19.133755, \"o\", \"r\"]\n[19.14299, \"o\", \"e\"]\n[19.16526, \"o\", \"p\"]\n[19.232505, \"o\", \"o\"]\n[19.286765, \"o\", \" \"]\n[19.346037, \"o\", \"o\"]\n[19.424305, \"o\", \"n\"]\n[19.481493, \"o\", \" \"]\n[19.616742, \"o\", \"a\"]\n[19.671994, \"o\", \"n\"]\n[19.692266, \"o\", \" \"]\n[19.766499, \"o\", \"e\"]\n[19.775721, \"o\", \"x\"]\n[19.975975, \"o\", \"t\"]\n[20.022241, \"o\", \"e\"]\n[20.05052, \"o\", \"r\"]\n[20.059738, \"o\", \"n\"]\n[20.071001, \"o\", \"a\"]\n[20.082205, \"o\", \"l\"]\n[20.091424, \"o\", \" \"]\n[20.148692, \"o\", \"d\"]\n[20.180965, \"o\", \"r\"]\n[20.236208, \"o\", \"i\"]\n[20.256457, \"o\", \"v\"]\n[20.278716, \"o\", \"e\"]\n[20.301976, \"o\", \"…\"]\n[20.333963, \"o\", \"\\r\\n\"]\n[20.339852, \"o\", \"$ b\"]\n[20.358126, \"o\", \"o\"]\n[20.442379, \"o\", \"r\"]\n[20.455655, \"o\", \"g\"]\n[20.510921, \"o\", \" \"]\n[20.565184, \"o\", \"i\"]\n[20.610429, \"o\", \"n\"]\n[20.619643, \"o\", \"i\"]\n[20.628869, \"o\", \"t\"]\n[20.638114, \"o\", \" \"]\n[20.794374, \"o\", \"-\"]\n[20.810588, \"o\", \"-\"]\n[20.88284, \"o\", \"e\"]\n[20.90811, \"o\", \"n\"]\n[20.940365, \"o\", \"c\"]\n[20.978597, \"o\", \"r\"]\n[21.01085, \"o\", \"y\"]\n[21.041143, \"o\", \"p\"]\n[21.162394, \"o\", \"t\"]\n[21.219624, \"o\", \"i\"]\n[21.242904, \"o\", \"o\"]\n[21.252069, \"o\", \"n\"]\n[21.358325, \"o\", \"=\"]\n[21.369514, \"o\", \"r\"]\n[21.410761, \"o\", \"e\"]\n[21.433035, \"o\", \"p\"]\n[21.535257, \"o\", \"o\"]\n[21.649448, \"o\", \"k\"]\n[21.683731, \"o\", \"e\"]\n[21.77799, \"o\", \"y\"]\n[21.823212, \"o\", \" \"]\n[21.957402, \"o\", \"/\"]\n[22.088666, \"o\", \"m\"]\n[22.236898, \"o\", \"e\"]\n[22.24612, \"o\", \"d\"]\n[22.267377, \"o\", \"i\"]\n[22.314631, \"o\", \"a\"]\n[22.389887, \"o\", \"/\"]\n[22.406138, \"o\", \"b\"]\n[22.464386, \"o\", \"a\"]\n[22.486629, \"o\", \"c\"]\n[22.499854, \"o\", \"k\"]\n[22.569172, \"o\", \"u\"]\n[22.578364, \"o\", \"p\"]\n[22.630618, \"o\", \"/\"]\n[22.642925, \"o\", \"b\"]\n[22.698191, \"o\", \"o\"]\n[22.751434, \"o\", \"r\"]\n[22.780696, \"o\", \"g\"]\n[22.801933, \"o\", \"d\"]\n[22.822192, \"o\", \"e\"]\n[22.907435, \"o\", \"m\"]\n[22.938691, \"o\", \"o\"]\n[22.964601, \"o\", \"\\r\\n\"]\n[23.832422, \"o\", \"Enter new passphrase: \"]\n[25.486059, \"o\", \"\\r\\r\\n\"]\n[25.487658, \"o\", \"Enter same passphrase again: \"]\n[26.856445, \"o\", \"\\r\\r\\n\"]\n[26.857204, \"o\", \"Do you want your passphrase to be displayed for verification? [yN]: \\r\\r\\n\"]\n[26.966484, \"o\", \"\\r\\r\\n\"]\n[26.966685, \"o\", \"By default repositories initialized with this version will produce security\"]\n[26.966829, \"o\", \"\\r\\r\\n\"]\n[26.966962, \"o\", \"errors if written to with an older version (up to and including Borg 1.0.8).\"]\n[26.967109, \"o\", \"\\r\\r\\n\"]\n[26.967228, \"o\", \"\\r\\r\\n\"]\n[26.967349, \"o\", \"If you want to use these older versions, you can disable the check by running:\"]\n[26.967488, \"o\", \"\\r\\r\\n\"]\n[26.967607, \"o\", \"borg upgrade --disable-tam /media/backup/borgdemo\"]\n[26.96775, \"o\", \"\\r\\r\\n\"]\n[26.967866, \"o\", \"\\r\\r\\n\"]\n[26.968011, \"o\", \"See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details about the security implications.\"]\n[26.968147, \"o\", \"\\r\\r\\n\"]\n[26.968375, \"o\", \"\\r\\r\\n\"]\n[26.968503, \"o\", \"IMPORTANT: you will need both KEY AND PASSPHRASE to access this repo!\"]\n[26.968625, \"o\", \"\\r\\r\\n\"]\n[26.968777, \"o\", \"If you used a repokey mode, the key is stored in the repo, but you should back it up separately.\"]\n[26.968901, \"o\", \"\\r\\r\\n\"]\n[26.969071, \"o\", \"Use \\\"borg key export\\\" to export the key, optionally in printable format.\"]\n[26.969224, \"o\", \"\\r\\r\\n\"]\n[26.969358, \"o\", \"Write down the passphrase. Store both at safe place(s).\"]\n[26.969507, \"o\", \"\\r\\r\\n\"]\n[26.969633, \"o\", \"\\r\\r\\n\"]\n[27.013625, \"o\", \"$ #\"]\n[27.13268, \"o\", \" \"]\n[27.147032, \"o\", \"T\"]\n[27.156512, \"o\", \"h\"]\n[27.177304, \"o\", \"i\"]\n[27.223151, \"o\", \"s\"]\n[27.27742, \"o\", \" \"]\n[27.286916, \"o\", \"u\"]\n[27.296388, \"o\", \"s\"]\n[27.325338, \"o\", \"e\"]\n[27.334603, \"o\", \"s\"]\n[27.359038, \"o\", \" \"]\n[27.369472, \"o\", \"t\"]\n[27.499665, \"o\", \"h\"]\n[27.521345, \"o\", \"e\"]\n[27.585341, \"o\", \" \"]\n[27.697371, \"o\", \"r\"]\n[27.706903, \"o\", \"e\"]\n[27.907207, \"o\", \"p\"]\n[27.920745, \"o\", \"o\"]\n[27.976277, \"o\", \"k\"]\n[28.085507, \"o\", \"e\"]\n[28.097114, \"o\", \"y\"]\n[28.293222, \"o\", \" \"]\n[28.302812, \"o\", \"e\"]\n[28.503212, \"o\", \"n\"]\n[28.554895, \"o\", \"c\"]\n[28.568404, \"o\", \"r\"]\n[28.769382, \"o\", \"y\"]\n[28.901385, \"o\", \"p\"]\n[28.961879, \"o\", \"t\"]\n[28.971352, \"o\", \"i\"]\n[29.017917, \"o\", \"o\"]\n[29.167349, \"o\", \"n\"]\n[29.34115, \"o\", \".\"]\n[29.46939, \"o\", \" \"]\n[29.509408, \"o\", \"Y\"]\n[29.518829, \"o\", \"o\"]\n[29.529334, \"o\", \"u\"]\n[29.585778, \"o\", \" \"]\n[29.613184, \"o\", \"m\"]\n[29.622616, \"o\", \"a\"]\n[29.677445, \"o\", \"y\"]\n[29.727828, \"o\", \" \"]\n[29.737091, \"o\", \"l\"]\n[29.807396, \"o\", \"o\"]\n[29.905617, \"o\", \"o\"]\n[29.971877, \"o\", \"k\"]\n[30.151165, \"o\", \" \"]\n[30.24344, \"o\", \"a\"]\n[30.432734, \"o\", \"t\"]\n[30.464016, \"o\", \" \"]\n[30.498308, \"o\", \"\\\"\"]\n[30.508566, \"o\", \"b\"]\n[30.623871, \"o\", \"o\"]\n[30.66712, \"o\", \"r\"]\n[30.81244, \"o\", \"g\"]\n[30.890789, \"o\", \" \"]\n[30.974011, \"o\", \"h\"]\n[31.106339, \"o\", \"e\"]\n[31.119612, \"o\", \"l\"]\n[31.154894, \"o\", \"p\"]\n[31.278174, \"o\", \" \"]\n[31.325429, \"o\", \"i\"]\n[31.338742, \"o\", \"n\"]\n[31.377071, \"o\", \"i\"]\n[31.415261, \"o\", \"t\"]\n[31.494554, \"o\", \"\\\"\"]\n[31.533795, \"o\", \" \"]\n[31.583029, \"o\", \"o\"]\n[31.652322, \"o\", \"r\"]\n[31.85262, \"o\", \" \"]\n[31.891954, \"o\", \"t\"]\n[31.901285, \"o\", \"h\"]\n[31.910553, \"o\", \"e\"]\n[31.959826, \"o\", \" \"]\n[32.090128, \"o\", \"o\"]\n[32.290413, \"o\", \"n\"]\n[32.299633, \"o\", \"l\"]\n[32.336944, \"o\", \"i\"]\n[32.383206, \"o\", \"n\"]\n[32.392447, \"o\", \"e\"]\n[32.476764, \"o\", \" \"]\n[32.485995, \"o\", \"d\"]\n[32.495203, \"o\", \"o\"]\n[32.526555, \"o\", \"c\"]\n[32.587775, \"o\", \" \"]\n[32.597044, \"o\", \"a\"]\n[32.673383, \"o\", \"t\"]\n[32.682665, \"o\", \" \"]\n[32.743954, \"o\", \"h\"]\n[32.823241, \"o\", \"t\"]\n[32.84252, \"o\", \"t\"]\n[32.868801, \"o\", \"p\"]\n[32.895065, \"o\", \"s\"]\n[32.974332, \"o\", \":\"]\n[33.121548, \"o\", \"/\"]\n[33.183875, \"o\", \"/\"]\n[33.284921, \"o\", \"b\"]\n[33.300185, \"o\", \"o\"]\n[33.320424, \"o\", \"r\"]\n[33.368691, \"o\", \"g\"]\n[33.377904, \"o\", \"b\"]\n[33.431174, \"o\", \"a\"]\n[33.471457, \"o\", \"c\"]\n[33.480708, \"o\", \"k\"]\n[33.489935, \"o\", \"u\"]\n[33.522193, \"o\", \"p\"]\n[33.536454, \"o\", \".\"]\n[33.545628, \"o\", \"r\"]\n[33.620889, \"o\", \"e\"]\n[33.689179, \"o\", \"a\"]\n[33.698404, \"o\", \"d\"]\n[33.719676, \"o\", \"t\"]\n[33.768939, \"o\", \"h\"]\n[33.778141, \"o\", \"e\"]\n[33.822397, \"o\", \"d\"]\n[33.917604, \"o\", \"o\"]\n[34.021836, \"o\", \"c\"]\n[34.03105, \"o\", \"s\"]\n[34.231324, \"o\", \".\"]\n[34.274564, \"o\", \"i\"]\n[34.335814, \"o\", \"o\"]\n[34.359041, \"o\", \"/\"]\n[34.431296, \"o\", \" \"]\n[34.440495, \"o\", \"f\"]\n[34.477712, \"o\", \"o\"]\n[34.510921, \"o\", \"r\"]\n[34.603179, \"o\", \" \"]\n[34.628416, \"o\", \"o\"]\n[34.696681, \"o\", \"t\"]\n[34.758901, \"o\", \"h\"]\n[34.836318, \"o\", \"e\"]\n[34.905349, \"o\", \"r\"]\n[34.951592, \"o\", \" \"]\n[34.96081, \"o\", \"m\"]\n[35.073058, \"o\", \"o\"]\n[35.099305, \"o\", \"d\"]\n[35.144574, \"o\", \"e\"]\n[35.22482, \"o\", \"s\"]\n[35.268061, \"o\", \".\"]\n[35.277927, \"o\", \"\\r\\n\"]\n[35.283574, \"o\", \"$ \\r\\n\"]\n[35.287762, \"o\", \"$ #\"]\n[35.32211, \"o\", \" \"]\n[35.385009, \"o\", \"S\"]\n[35.401365, \"o\", \"o\"]\n[35.410748, \"o\", \" \"]\n[35.451919, \"o\", \"n\"]\n[35.461356, \"o\", \"o\"]\n[35.474934, \"o\", \"w\"]\n[35.536488, \"o\", \",\"]\n[35.570013, \"o\", \" \"]\n[35.631873, \"o\", \"l\"]\n[35.661368, \"o\", \"e\"]\n[35.670835, \"o\", \"t\"]\n[35.845321, \"o\", \"'\"]\n[36.021655, \"o\", \"s\"]\n[36.090853, \"o\", \" \"]\n[36.101264, \"o\", \"c\"]\n[36.110857, \"o\", \"r\"]\n[36.120334, \"o\", \"e\"]\n[36.241347, \"o\", \"a\"]\n[36.300965, \"o\", \"t\"]\n[36.339197, \"o\", \"e\"]\n[36.371614, \"o\", \" \"]\n[36.45487, \"o\", \"o\"]\n[36.466427, \"o\", \"u\"]\n[36.496514, \"o\", \"r\"]\n[36.516156, \"o\", \" \"]\n[36.525617, \"o\", \"f\"]\n[36.609341, \"o\", \"i\"]\n[36.628943, \"o\", \"r\"]\n[36.638409, \"o\", \"s\"]\n[36.6533, \"o\", \"t\"]\n[36.742041, \"o\", \" \"]\n[36.823533, \"o\", \"(\"]\n[36.836112, \"o\", \"c\"]\n[36.891684, \"o\", \"o\"]\n[36.915431, \"o\", \"m\"]\n[36.937742, \"o\", \"p\"]\n[36.993129, \"o\", \"r\"]\n[37.038448, \"o\", \"e\"]\n[37.130855, \"o\", \"s\"]\n[37.14035, \"o\", \"s\"]\n[37.223271, \"o\", \"e\"]\n[37.273744, \"o\", \"d\"]\n[37.473926, \"o\", \")\"]\n[37.549739, \"o\", \" \"]\n[37.559237, \"o\", \"b\"]\n[37.70135, \"o\", \"a\"]\n[37.759843, \"o\", \"c\"]\n[37.888108, \"o\", \"k\"]\n[37.985378, \"o\", \"u\"]\n[37.99491, \"o\", \"p\"]\n[38.057312, \"o\", \".\"]\n[38.070071, \"o\", \"\\r\\n\"]\n[38.076517, \"o\", \"$ b\"]\n[38.131758, \"o\", \"o\"]\n[38.176021, \"o\", \"r\"]\n[38.232284, \"o\", \"g\"]\n[38.290526, \"o\", \" \"]\n[38.299745, \"o\", \"c\"]\n[38.373041, \"o\", \"r\"]\n[38.391283, \"o\", \"e\"]\n[38.400486, \"o\", \"a\"]\n[38.422774, \"o\", \"t\"]\n[38.431987, \"o\", \"e\"]\n[38.477263, \"o\", \" \"]\n[38.557434, \"o\", \"-\"]\n[38.571684, \"o\", \"-\"]\n[38.635942, \"o\", \"s\"]\n[38.649196, \"o\", \"t\"]\n[38.696452, \"o\", \"a\"]\n[38.716708, \"o\", \"t\"]\n[38.74095, \"o\", \"s\"]\n[38.795156, \"o\", \" \"]\n[38.971421, \"o\", \"-\"]\n[38.980619, \"o\", \"-\"]\n[39.154846, \"o\", \"p\"]\n[39.259082, \"o\", \"r\"]\n[39.268286, \"o\", \"o\"]\n[39.295549, \"o\", \"g\"]\n[39.375798, \"o\", \"r\"]\n[39.505049, \"o\", \"e\"]\n[39.548298, \"o\", \"s\"]\n[39.55747, \"o\", \"s\"]\n[39.729698, \"o\", \" \"]\n[39.738886, \"o\", \"-\"]\n[39.770147, \"o\", \"-\"]\n[39.780361, \"o\", \"c\"]\n[39.851606, \"o\", \"o\"]\n[39.908862, \"o\", \"m\"]\n[39.9303, \"o\", \"p\"]\n[39.967419, \"o\", \"r\"]\n[40.167693, \"o\", \"e\"]\n[40.182868, \"o\", \"s\"]\n[40.236139, \"o\", \"s\"]\n[40.338402, \"o\", \"i\"]\n[40.387653, \"o\", \"o\"]\n[40.417878, \"o\", \"n\"]\n[40.431077, \"o\", \" \"]\n[40.440317, \"o\", \"l\"]\n[40.471602, \"o\", \"z\"]\n[40.50683, \"o\", \"4\"]\n[40.533153, \"o\", \" \"]\n[40.632398, \"o\", \"/\"]\n[40.765597, \"o\", \"m\"]\n[40.939842, \"o\", \"e\"]\n[41.013163, \"o\", \"d\"]\n[41.068403, \"o\", \"i\"]\n[41.161609, \"o\", \"a\"]\n[41.34683, \"o\", \"/\"]\n[41.35603, \"o\", \"b\"]\n[41.382307, \"o\", \"a\"]\n[41.420551, \"o\", \"c\"]\n[41.449776, \"o\", \"k\"]\n[41.461978, \"o\", \"u\"]\n[41.47121, \"o\", \"p\"]\n[41.505394, \"o\", \"/\"]\n[41.535648, \"o\", \"b\"]\n[41.553906, \"o\", \"o\"]\n[41.563104, \"o\", \"r\"]\n[41.572335, \"o\", \"g\"]\n[41.600598, \"o\", \"d\"]\n[41.621833, \"o\", \"e\"]\n[41.631044, \"o\", \"m\"]\n[41.690289, \"o\", \"o\"]\n[41.842576, \"o\", \":\"]\n[41.9168, \"o\", \":\"]\n[41.926071, \"o\", \"b\"]\n[41.939284, \"o\", \"a\"]\n[41.994532, \"o\", \"c\"]\n[42.006819, \"o\", \"k\"]\n[42.039076, \"o\", \"u\"]\n[42.048303, \"o\", \"p\"]\n[42.060532, \"o\", \"1\"]\n[42.102826, \"o\", \" \"]\n[42.207072, \"o\", \"W\"]\n[42.217211, \"o\", \"a\"]\n[42.226429, \"o\", \"l\"]\n[42.265646, \"o\", \"l\"]\n[42.344914, \"o\", \"p\"]\n[42.410163, \"o\", \"a\"]\n[42.436418, \"o\", \"p\"]\n[42.633625, \"o\", \"e\"]\n[42.64884, \"o\", \"r\"]\n[42.746842, \"o\", \"\\r\\n\"]\n[43.682327, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[45.082307, \"o\", \"\\r\\r\\n\"]\n[45.169184, \"o\", \"0 B O 0 B C 0 B D 0 N Wallpaper                                                 \\r\"]\n[45.187613, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[45.188516, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[45.189235, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[45.189931, \"o\", \"                                                                                \\r\"]\n[45.382861, \"o\", \"18.70 MB O 18.70 MB C 18.70 MB D 0 N Wallpaper/bigcolle...OpenCL_45154214_8K.jpg\\r\"]\n[45.589911, \"o\", \"37.45 MB O 37.36 MB C 37.36 MB D 1 N Wallpaper/bigcolle...CL_528814521414_8K.jpg\\r\"]\n[45.792693, \"o\", \"54.88 MB O 54.62 MB C 54.62 MB D 4 N Wallpaper/bigcolle...OpenCL_18915424_8K.jpg\\r\"]\n[46.015974, \"o\", \"75.03 MB O 74.77 MB C 74.77 MB D 4 N Wallpaper/bigcolle...OpenCL_18915424_8K.jpg\\r\"]\n[46.256392, \"o\", \"96.05 MB O 95.78 MB C 95.78 MB D 6 N Wallpaper/bigcolle...FS_OpenCL_54815_5K.jpg\\r\"]\n[46.475686, \"o\", \"116.05 MB O 115.77 MB C 115.77 MB D 10 N Wallpaper/bigcol...CL_4258952414_8K.jpg\\r\"]\n[46.718361, \"o\", \"138.21 MB O 137.93 MB C 137.93 MB D 10 N Wallpaper/bigcol...CL_4258952414_8K.jpg\\r\"]\n[46.933739, \"o\", \"158.88 MB O 158.61 MB C 158.61 MB D 11 N Wallpaper/bigcol...CL_9648145412_8K.jpg\\r\"]\n[47.134629, \"o\", \"177.87 MB O 177.59 MB C 177.59 MB D 11 N Wallpaper/bigcol...CL_9648145412_8K.jpg\\r\"]\n[47.337235, \"o\", \"195.94 MB O 195.67 MB C 195.67 MB D 14 N Wallpaper/bigcol...iable_8K_6595424.jpg\\r\"]\n[47.56114, \"o\", \"217.13 MB O 216.29 MB C 216.29 MB D 16 N Wallpaper/bigcol...L_51241841541_8K.jpg\\r\"]\n[47.769682, \"o\", \"235.09 MB O 234.13 MB C 234.13 MB D 20 N Wallpaper/bigcol...nCL_644289452_8K.jpg\\r\"]\n[47.975793, \"o\", \"252.58 MB O 251.62 MB C 251.62 MB D 20 N Wallpaper/bigcol...nCL_644289452_8K.jpg\\r\"]\n[48.183523, \"o\", \"270.49 MB O 269.52 MB C 269.52 MB D 22 N Wallpaper/bigcol...nCL_461481542_8K.jpg\\r\"]\n[48.384239, \"o\", \"288.97 MB O 287.80 MB C 287.80 MB D 24 N Wallpaper/bigcol...2154188450481_8K.jpg\\r\"]\n[48.587528, \"o\", \"307.41 MB O 306.10 MB C 306.10 MB D 25 N Wallpaper/bigcol...nCL_545185481_8K.jpg\\r\"]\n[48.798353, \"o\", \"326.35 MB O 324.95 MB C 324.95 MB D 27 N Wallpaper/bigcol...m_OpenCl_6184524.jpg\\r\"]\n[49.018803, \"o\", \"347.31 MB O 345.91 MB C 345.91 MB D 27 N Wallpaper/bigcol...m_OpenCl_6184524.jpg\\r\"]\n[49.234361, \"o\", \"366.64 MB O 365.24 MB C 365.24 MB D 28 N Wallpaper/bigcol...nCL_955141845_8K.jpg\\r\"]\n[49.438542, \"o\", \"384.57 MB O 383.17 MB C 383.17 MB D 29 N Wallpaper/bigcol...s._(14168975789).jpg\\r\"]\n[49.616544, \"o\", \"                                                                                \\r\"]\n[49.707316, \"o\", \"Saving files cache                                                              \\r\"]\n[49.708504, \"o\", \"Saving chunks cache                                                             \\r\"]\n[49.709375, \"o\", \"Saving cache config                                                             \\r\"]\n[49.711438, \"o\", \"                                                                                \\r\"]\n[49.713185, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[49.713432, \"o\", \"Repository: /media/backup/borgdemo\"]\n[49.713781, \"o\", \"\\r\\r\\n\"]\n[49.713824, \"o\", \"Archive name: backup1\"]\n[49.714201, \"o\", \"\\r\\r\\nArchive fingerprint: 4b2f146af02ce3a4df8018411ce46d97fe7eaad0512c969a4bbcd4f113900746\"]\n[49.71452, \"o\", \"\\r\\r\\nTime (start): Wed, 2022-07-06 21:28:23\"]\n[49.714706, \"o\", \"\\r\\r\\n\"]\n[49.715114, \"o\", \"Time (end):   Wed, 2022-07-06 21:28:28\\r\\r\\n\"]\n[49.715288, \"o\", \"Duration: 4.45 seconds\"]\n[49.715701, \"o\", \"\\r\\r\\nNumber of files: 31\"]\n[49.715975, \"o\", \"\\r\\r\\n\"]\n[49.716017, \"o\", \"Utilization of max. archive size: 0%\"]\n[49.716177, \"o\", \"\\r\\r\\n\"]\n[49.716452, \"o\", \"------------------------------------------------------------------------------\"]\n[49.716679, \"o\", \"\\r\\r\\n\"]\n[49.71705, \"o\", \"                       Original size      Compressed size    Deduplicated size\\r\\r\\n\"]\n[49.717685, \"o\", \"This archive:              401.15 MB            399.74 MB            399.55 MB\\r\\r\\nAll archives:              401.15 MB            399.74 MB            399.56 MB\\r\\r\\n\\r\\r\\n                       Unique chunks         Total chunks\\r\\r\\nChunk index:                     208                  209\\r\\r\\n\"]\n[49.718032, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[49.763286, \"o\", \"$ \\r\\n\"]\n[49.769027, \"o\", \"$ #\"]\n[49.889538, \"o\", \" \"]\n[49.907123, \"o\", \"T\"]\n[49.98155, \"o\", \"h\"]\n[50.001932, \"o\", \"a\"]\n[50.077298, \"o\", \"t\"]\n[50.149538, \"o\", \"'\"]\n[50.265474, \"o\", \"s\"]\n[50.328143, \"o\", \" \"]\n[50.353649, \"o\", \"n\"]\n[50.376936, \"o\", \"i\"]\n[50.424241, \"o\", \"c\"]\n[50.433748, \"o\", \"e\"]\n[50.530161, \"o\", \",\"]\n[50.57866, \"o\", \" \"]\n[50.633469, \"o\", \"s\"]\n[50.652226, \"o\", \"o\"]\n[50.7495, \"o\", \" \"]\n[50.8277, \"o\", \"f\"]\n[50.85347, \"o\", \"a\"]\n[50.86547, \"o\", \"r\"]\n[50.897494, \"o\", \".\"]\n[51.013237, \"o\", \"\\r\\n\"]\n[51.019506, \"o\", \"$ #\"]\n[51.045786, \"o\", \" \"]\n[51.195486, \"o\", \"S\"]\n[51.225488, \"o\", \"o\"]\n[51.297491, \"o\", \" \"]\n[51.32406, \"o\", \"l\"]\n[51.333277, \"o\", \"e\"]\n[51.361489, \"o\", \"t\"]\n[51.391215, \"o\", \"'\"]\n[51.400666, \"o\", \"s\"]\n[51.525492, \"o\", \" \"]\n[51.534974, \"o\", \"a\"]\n[51.561475, \"o\", \"d\"]\n[51.575956, \"o\", \"d\"]\n[51.610233, \"o\", \" \"]\n[51.626877, \"o\", \"a\"]\n[51.753372, \"o\", \" \"]\n[51.806885, \"o\", \"n\"]\n[51.837461, \"o\", \"e\"]\n[51.846818, \"o\", \"w\"]\n[51.90324, \"o\", \" \"]\n[51.912612, \"o\", \"f\"]\n[51.945692, \"o\", \"i\"]\n[51.955259, \"o\", \"l\"]\n[51.976908, \"o\", \"e\"]\n[51.989371, \"o\", \"…\"]\n[52.09749, \"o\", \"\\r\\n\"]\n[52.104127, \"o\", \"$ e\"]\n[52.249858, \"o\", \"c\"]\n[52.381469, \"o\", \"h\"]\n[52.409548, \"o\", \"o\"]\n[52.429558, \"o\", \" \"]\n[52.450022, \"o\", \"\\\"\"]\n[52.472755, \"o\", \"n\"]\n[52.494909, \"o\", \"e\"]\n[52.541666, \"o\", \"w\"]\n[52.621531, \"o\", \" \"]\n[52.657476, \"o\", \"n\"]\n[52.666897, \"o\", \"i\"]\n[52.749532, \"o\", \"c\"]\n[52.789582, \"o\", \"e\"]\n[52.857092, \"o\", \" \"]\n[52.901226, \"o\", \"f\"]\n[53.022805, \"o\", \"i\"]\n[53.091341, \"o\", \"l\"]\n[53.143908, \"o\", \"e\"]\n[53.34533, \"o\", \"\\\"\"]\n[53.358764, \"o\", \" \"]\n[53.377334, \"o\", \">\"]\n[53.424655, \"o\", \" \"]\n[53.434072, \"o\", \"W\"]\n[53.469238, \"o\", \"a\"]\n[53.479684, \"o\", \"l\"]\n[53.525369, \"o\", \"l\"]\n[53.569647, \"o\", \"p\"]\n[53.607808, \"o\", \"a\"]\n[53.640682, \"o\", \"p\"]\n[53.657289, \"o\", \"e\"]\n[53.741474, \"o\", \"r\"]\n[53.79733, \"o\", \"/\"]\n[53.933274, \"o\", \"n\"]\n[53.945655, \"o\", \"e\"]\n[53.993338, \"o\", \"w\"]\n[54.002684, \"o\", \"f\"]\n[54.024254, \"o\", \"i\"]\n[54.177552, \"o\", \"l\"]\n[54.228957, \"o\", \"e\"]\n[54.336453, \"o\", \".\"]\n[54.348932, \"o\", \"t\"]\n[54.377374, \"o\", \"x\"]\n[54.389764, \"o\", \"t\"]\n[54.531118, \"o\", \"\\r\\n\"]\n[54.538791, \"o\", \"$ \\r\\n\"]\n[54.541962, \"o\", \"$ b\"]\n[54.570224, \"o\", \"o\"]\n[54.597517, \"o\", \"r\"]\n[54.62934, \"o\", \"g\"]\n[54.680347, \"o\", \" \"]\n[54.708874, \"o\", \"c\"]\n[54.74704, \"o\", \"r\"]\n[54.789626, \"o\", \"e\"]\n[54.827188, \"o\", \"a\"]\n[54.836664, \"o\", \"t\"]\n[54.901889, \"o\", \"e\"]\n[55.102264, \"o\", \" \"]\n[55.128036, \"o\", \"-\"]\n[55.143576, \"o\", \"-\"]\n[55.169344, \"o\", \"s\"]\n[55.195965, \"o\", \"t\"]\n[55.291014, \"o\", \"a\"]\n[55.367836, \"o\", \"t\"]\n[55.388423, \"o\", \"s\"]\n[55.571545, \"o\", \" \"]\n[55.581105, \"o\", \"-\"]\n[55.604771, \"o\", \"-\"]\n[55.617271, \"o\", \"p\"]\n[55.717512, \"o\", \"r\"]\n[55.795014, \"o\", \"o\"]\n[55.889328, \"o\", \"g\"]\n[55.898787, \"o\", \"r\"]\n[56.099465, \"o\", \"e\"]\n[56.191812, \"o\", \"s\"]\n[56.282367, \"o\", \"s\"]\n[56.392067, \"o\", \" \"]\n[56.444626, \"o\", \"-\"]\n[56.453934, \"o\", \"-\"]\n[56.554258, \"o\", \"c\"]\n[56.661366, \"o\", \"o\"]\n[56.672796, \"o\", \"m\"]\n[56.690574, \"o\", \"p\"]\n[56.700005, \"o\", \"r\"]\n[56.76936, \"o\", \"e\"]\n[56.813887, \"o\", \"s\"]\n[57.01435, \"o\", \"s\"]\n[57.046765, \"o\", \"i\"]\n[57.061266, \"o\", \"o\"]\n[57.070634, \"o\", \"n\"]\n[57.143411, \"o\", \" \"]\n[57.169351, \"o\", \"l\"]\n[57.213313, \"o\", \"z\"]\n[57.237415, \"o\", \"4\"]\n[57.437721, \"o\", \" \"]\n[57.556322, \"o\", \"/\"]\n[57.569348, \"o\", \"m\"]\n[57.57873, \"o\", \"e\"]\n[57.588073, \"o\", \"d\"]\n[57.59747, \"o\", \"i\"]\n[57.740032, \"o\", \"a\"]\n[57.833541, \"o\", \"/\"]\n[57.910749, \"o\", \"b\"]\n[57.990587, \"o\", \"a\"]\n[58.03506, \"o\", \"c\"]\n[58.084774, \"o\", \"k\"]\n[58.110399, \"o\", \"u\"]\n[58.119855, \"o\", \"p\"]\n[58.129364, \"o\", \"/\"]\n[58.16133, \"o\", \"b\"]\n[58.272751, \"o\", \"o\"]\n[58.332329, \"o\", \"r\"]\n[58.384694, \"o\", \"g\"]\n[58.541406, \"o\", \"d\"]\n[58.593907, \"o\", \"e\"]\n[58.621372, \"o\", \"m\"]\n[58.640502, \"o\", \"o\"]\n[58.741339, \"o\", \":\"]\n[58.792742, \"o\", \":\"]\n[58.834033, \"o\", \"b\"]\n[58.913401, \"o\", \"a\"]\n[58.922796, \"o\", \"c\"]\n[59.073337, \"o\", \"k\"]\n[59.082618, \"o\", \"u\"]\n[59.101057, \"o\", \"p\"]\n[59.208574, \"o\", \"2\"]\n[59.217983, \"o\", \" \"]\n[59.233577, \"o\", \"W\"]\n[59.301331, \"o\", \"a\"]\n[59.396659, \"o\", \"l\"]\n[59.408194, \"o\", \"l\"]\n[59.437392, \"o\", \"p\"]\n[59.467902, \"o\", \"a\"]\n[59.668385, \"o\", \"p\"]\n[59.68178, \"o\", \"e\"]\n[59.766896, \"o\", \"r\"]\n[59.818201, \"o\", \"\\r\\n\"]\n[60.775328, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[62.286369, \"o\", \"\\r\\r\\n\"]\n[62.373384, \"o\", \"0 B O 0 B C 0 B D 0 N Wallpaper                                                 \\r\"]\n[62.374541, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[62.375102, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[62.37576, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[62.376372, \"o\", \"                                                                                \\r\"]\n[62.390566, \"o\", \"                                                                                \\r\"]\n[62.405895, \"o\", \"Saving files cache                                                              \\r\"]\n[62.407022, \"o\", \"Saving chunks cache                                                             \\r\"]\n[62.407729, \"o\", \"Saving cache config                                                             \\r\"]\n[62.409963, \"o\", \"                                                                                \\r\"]\n[62.41203, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[62.412335, \"o\", \"Repository: /media/backup/borgdemo\"]\n[62.412659, \"o\", \"\\r\\r\\n\"]\n[62.412993, \"o\", \"Archive name: backup2\"]\n[62.413341, \"o\", \"\\r\\r\\n\"]\n[62.413707, \"o\", \"Archive fingerprint: 70d0bd96eb512ca96356308fb4aab162ae7d868c9fe491a36b1d8b00190e8e97\"]\n[62.413751, \"o\", \"\\r\\r\\n\"]\n[62.414127, \"o\", \"Time (start): Wed, 2022-07-06 21:28:40\"]\n[62.414342, \"o\", \"\\r\\r\\n\"]\n[62.414764, \"o\", \"Time (end):   Wed, 2022-07-06 21:28:40\"]\n[62.414975, \"o\", \"\\r\\r\\n\"]\n[62.415392, \"o\", \"Duration: 0.02 seconds\"]\n[62.415608, \"o\", \"\\r\\r\\n\"]\n[62.41596, \"o\", \"Number of files: 32\"]\n[62.416062, \"o\", \"\\r\\r\\n\"]\n[62.416542, \"o\", \"Utilization of max. archive size: 0%\"]\n[62.416767, \"o\", \"\\r\\r\\n\"]\n[62.417159, \"o\", \"------------------------------------------------------------------------------\"]\n[62.417305, \"o\", \"\\r\\r\\n\"]\n[62.417746, \"o\", \"                       Original size      Compressed size    Deduplicated size\"]\n[62.417864, \"o\", \"\\r\\r\\n\"]\n[62.418171, \"o\", \"This archive:              401.15 MB            399.74 MB                604 B\"]\n[62.418576, \"o\", \"\\r\\r\\n\"]\n[62.418965, \"o\", \"All archives:              802.29 MB            799.48 MB            399.57 MB\"]\n[62.419161, \"o\", \"\\r\\r\\n\"]\n[62.419577, \"o\", \"\\r\\r\\n\"]\n[62.419938, \"o\", \"                       Unique chunks         Total chunks\"]\n[62.420131, \"o\", \"\\r\\r\\n\"]\n[62.420581, \"o\", \"Chunk index:                     211                  419\"]\n[62.420775, \"o\", \"\\r\\r\\n\"]\n[62.421247, \"o\", \"------------------------------------------------------------------------------\"]\n[62.421462, \"o\", \"\\r\\r\\n\"]\n[62.4692, \"o\", \"$ \\r\\n\"]\n[62.472497, \"o\", \"$ #\"]\n[62.48179, \"o\", \" \"]\n[62.651124, \"o\", \"W\"]\n[62.660372, \"o\", \"o\"]\n[62.786678, \"o\", \"w\"]\n[62.850953, \"o\", \",\"]\n[62.942261, \"o\", \" \"]\n[62.952488, \"o\", \"t\"]\n[63.093759, \"o\", \"h\"]\n[63.102987, \"o\", \"i\"]\n[63.150301, \"o\", \"s\"]\n[63.217527, \"o\", \" \"]\n[63.226846, \"o\", \"w\"]\n[63.286129, \"o\", \"a\"]\n[63.309429, \"o\", \"s\"]\n[63.364749, \"o\", \" \"]\n[63.425058, \"o\", \"a\"]\n[63.449431, \"o\", \" \"]\n[63.458651, \"o\", \"l\"]\n[63.467865, \"o\", \"o\"]\n[63.500253, \"o\", \"t\"]\n[63.573463, \"o\", \" \"]\n[63.616768, \"o\", \"f\"]\n[63.648075, \"o\", \"a\"]\n[63.692378, \"o\", \"s\"]\n[63.714715, \"o\", \"t\"]\n[63.829065, \"o\", \"e\"]\n[63.938338, \"o\", \"r\"]\n[64.13862, \"o\", \"!\"]\n[64.15888, \"o\", \"\\r\\n\"]\n[64.165067, \"o\", \"$ #\"]\n[64.215376, \"o\", \" \"]\n[64.241616, \"o\", \"N\"]\n[64.252934, \"o\", \"o\"]\n[64.294204, \"o\", \"t\"]\n[64.303462, \"o\", \"i\"]\n[64.332765, \"o\", \"c\"]\n[64.358045, \"o\", \"e\"]\n[64.36728, \"o\", \" \"]\n[64.391572, \"o\", \"t\"]\n[64.411851, \"o\", \"h\"]\n[64.467152, \"o\", \"e\"]\n[64.572418, \"o\", \" \"]\n[64.585626, \"o\", \"\\\"\"]\n[64.594877, \"o\", \"D\"]\n[64.604145, \"o\", \"e\"]\n[64.658491, \"o\", \"d\"]\n[64.747708, \"o\", \"u\"]\n[64.756943, \"o\", \"p\"]\n[64.823255, \"o\", \"l\"]\n[64.914556, \"o\", \"i\"]\n[64.948846, \"o\", \"c\"]\n[64.958142, \"o\", \"a\"]\n[64.985391, \"o\", \"t\"]\n[65.152715, \"o\", \"e\"]\n[65.177085, \"o\", \"d\"]\n[65.259267, \"o\", \" \"]\n[65.309462, \"o\", \"s\"]\n[65.325649, \"o\", \"i\"]\n[65.357957, \"o\", \"z\"]\n[65.528257, \"o\", \"e\"]\n[65.600556, \"o\", \"\\\"\"]\n[65.707856, \"o\", \" \"]\n[65.717101, \"o\", \"f\"]\n[65.750507, \"o\", \"o\"]\n[65.779701, \"o\", \"r\"]\n[65.788939, \"o\", \" \"]\n[65.849319, \"o\", \"\\\"\"]\n[65.942606, \"o\", \"T\"]\n[65.951859, \"o\", \"h\"]\n[65.961183, \"o\", \"i\"]\n[66.115428, \"o\", \"s\"]\n[66.184735, \"o\", \" \"]\n[66.220023, \"o\", \"a\"]\n[66.230285, \"o\", \"r\"]\n[66.279587, \"o\", \"c\"]\n[66.297785, \"o\", \"h\"]\n[66.395105, \"o\", \"i\"]\n[66.408354, \"o\", \"v\"]\n[66.428654, \"o\", \"e\"]\n[66.473928, \"o\", \"\\\"\"]\n[66.483134, \"o\", \"!\"]\n[66.571433, \"o\", \"\\r\\n\"]\n[66.576919, \"o\", \"$ #\"]\n[66.592225, \"o\", \" \"]\n[66.633417, \"o\", \"B\"]\n[66.708708, \"o\", \"o\"]\n[66.753999, \"o\", \"r\"]\n[66.76423, \"o\", \"g\"]\n[66.773413, \"o\", \" \"]\n[66.867707, \"o\", \"r\"]\n[66.877054, \"o\", \"e\"]\n[66.897308, \"o\", \"c\"]\n[66.962672, \"o\", \"o\"]\n[66.971899, \"o\", \"g\"]\n[67.017259, \"o\", \"n\"]\n[67.026475, \"o\", \"i\"]\n[67.035723, \"o\", \"z\"]\n[67.053037, \"o\", \"e\"]\n[67.132276, \"o\", \"d\"]\n[67.14758, \"o\", \" \"]\n[67.156806, \"o\", \"t\"]\n[67.226104, \"o\", \"h\"]\n[67.252425, \"o\", \"a\"]\n[67.307794, \"o\", \"t\"]\n[67.508065, \"o\", \" \"]\n[67.517322, \"o\", \"m\"]\n[67.717495, \"o\", \"o\"]\n[67.797754, \"o\", \"s\"]\n[67.80702, \"o\", \"t\"]\n[67.816244, \"o\", \" \"]\n[67.949494, \"o\", \"f\"]\n[68.062791, \"o\", \"i\"]\n[68.121067, \"o\", \"l\"]\n[68.140367, \"o\", \"e\"]\n[68.276663, \"o\", \"s\"]\n[68.304952, \"o\", \" \"]\n[68.407232, \"o\", \"d\"]\n[68.445403, \"o\", \"i\"]\n[68.474727, \"o\", \"d\"]\n[68.593061, \"o\", \" \"]\n[68.617374, \"o\", \"n\"]\n[68.640634, \"o\", \"o\"]\n[68.649862, \"o\", \"t\"]\n[68.662149, \"o\", \" \"]\n[68.69242, \"o\", \"c\"]\n[68.71764, \"o\", \"h\"]\n[68.726846, \"o\", \"a\"]\n[68.765193, \"o\", \"n\"]\n[68.79645, \"o\", \"g\"]\n[68.893677, \"o\", \"e\"]\n[68.953943, \"o\", \" \"]\n[69.002217, \"o\", \"a\"]\n[69.025452, \"o\", \"n\"]\n[69.127777, \"o\", \"d\"]\n[69.13708, \"o\", \" \"]\n[69.156345, \"o\", \"d\"]\n[69.214641, \"o\", \"e\"]\n[69.300936, \"o\", \"d\"]\n[69.409271, \"o\", \"u\"]\n[69.476576, \"o\", \"p\"]\n[69.596893, \"o\", \"l\"]\n[69.68111, \"o\", \"i\"]\n[69.706158, \"o\", \"c\"]\n[69.728513, \"o\", \"a\"]\n[69.846752, \"o\", \"t\"]\n[69.867018, \"o\", \"e\"]\n[69.9153, \"o\", \"d\"]\n[70.065545, \"o\", \" \"]\n[70.168883, \"o\", \"t\"]\n[70.258152, \"o\", \"h\"]\n[70.271501, \"o\", \"e\"]\n[70.28073, \"o\", \"m\"]\n[70.481093, \"o\", \".\"]\n[70.490785, \"o\", \"\\r\\n\"]\n[70.496448, \"o\", \"$ \\r\\n\"]\n[70.501664, \"o\", \"$ #\"]\n[70.529964, \"o\", \" \"]\n[70.714221, \"o\", \"B\"]\n[70.750476, \"o\", \"u\"]\n[70.866685, \"o\", \"t\"]\n[70.958924, \"o\", \" \"]\n[71.149225, \"o\", \"w\"]\n[71.166498, \"o\", \"h\"]\n[71.359756, \"o\", \"a\"]\n[71.407991, \"o\", \"t\"]\n[71.421261, \"o\", \" \"]\n[71.430476, \"o\", \"h\"]\n[71.448747, \"o\", \"a\"]\n[71.599001, \"o\", \"p\"]\n[71.750234, \"o\", \"p\"]\n[71.833426, \"o\", \"e\"]\n[71.882648, \"o\", \"n\"]\n[71.920917, \"o\", \"s\"]\n[71.945239, \"o\", \",\"]\n[72.145414, \"o\", \" \"]\n[72.174626, \"o\", \"w\"]\n[72.214867, \"o\", \"h\"]\n[72.239108, \"o\", \"e\"]\n[72.439393, \"o\", \"n\"]\n[72.461603, \"o\", \" \"]\n[72.473817, \"o\", \"w\"]\n[72.487089, \"o\", \"e\"]\n[72.687371, \"o\", \" \"]\n[72.86364, \"o\", \"m\"]\n[72.879841, \"o\", \"o\"]\n[72.999103, \"o\", \"v\"]\n[73.009202, \"o\", \"e\"]\n[73.075457, \"o\", \" \"]\n[73.127698, \"o\", \"a\"]\n[73.248951, \"o\", \" \"]\n[73.258159, \"o\", \"d\"]\n[73.272426, \"o\", \"i\"]\n[73.329649, \"o\", \"r\"]\n[73.354882, \"o\", \" \"]\n[73.364101, \"o\", \"a\"]\n[73.381275, \"o\", \"n\"]\n[73.402546, \"o\", \"d\"]\n[73.411758, \"o\", \" \"]\n[73.435011, \"o\", \"c\"]\n[73.44422, \"o\", \"r\"]\n[73.453394, \"o\", \"e\"]\n[73.610611, \"o\", \"a\"]\n[73.670846, \"o\", \"t\"]\n[73.711101, \"o\", \"e\"]\n[73.730366, \"o\", \" \"]\n[73.761533, \"o\", \"a\"]\n[73.909763, \"o\", \" \"]\n[73.94401, \"o\", \"n\"]\n[74.037243, \"o\", \"e\"]\n[74.109435, \"o\", \"w\"]\n[74.173634, \"o\", \" \"]\n[74.262874, \"o\", \"b\"]\n[74.335139, \"o\", \"a\"]\n[74.366424, \"o\", \"c\"]\n[74.447673, \"o\", \"k\"]\n[74.531912, \"o\", \"u\"]\n[74.555155, \"o\", \"p\"]\n[74.75541, \"o\", \"?\"]\n[74.765242, \"o\", \"\\r\\n\"]\n[74.77042, \"o\", \"$ m\"]\n[74.828702, \"o\", \"v\"]\n[75.028952, \"o\", \" \"]\n[75.038207, \"o\", \"W\"]\n[75.047413, \"o\", \"a\"]\n[75.059647, \"o\", \"l\"]\n[75.068871, \"o\", \"l\"]\n[75.080099, \"o\", \"p\"]\n[75.122399, \"o\", \"a\"]\n[75.131582, \"o\", \"p\"]\n[75.174824, \"o\", \"e\"]\n[75.201146, \"o\", \"r\"]\n[75.401312, \"o\", \"/\"]\n[75.439551, \"o\", \"b\"]\n[75.543797, \"o\", \"i\"]\n[75.558018, \"o\", \"g\"]\n[75.590267, \"o\", \"c\"]\n[75.610529, \"o\", \"o\"]\n[75.687784, \"o\", \"l\"]\n[75.888044, \"o\", \"l\"]\n[75.898236, \"o\", \"e\"]\n[75.938527, \"o\", \"c\"]\n[75.950758, \"o\", \"t\"]\n[76.027992, \"o\", \"i\"]\n[76.037221, \"o\", \"o\"]\n[76.097415, \"o\", \"n\"]\n[76.186636, \"o\", \" \"]\n[76.241869, \"o\", \"W\"]\n[76.251078, \"o\", \"a\"]\n[76.294356, \"o\", \"l\"]\n[76.361534, \"o\", \"l\"]\n[76.465786, \"o\", \"p\"]\n[76.478756, \"o\", \"a\"]\n[76.546004, \"o\", \"p\"]\n[76.606267, \"o\", \"e\"]\n[76.615487, \"o\", \"r\"]\n[76.745722, \"o\", \"/\"]\n[76.816982, \"o\", \"b\"]\n[76.826175, \"o\", \"i\"]\n[76.843447, \"o\", \"g\"]\n[76.896703, \"o\", \"c\"]\n[76.96194, \"o\", \"o\"]\n[76.994172, \"o\", \"l\"]\n[77.014431, \"o\", \"l\"]\n[77.023644, \"o\", \"e\"]\n[77.044914, \"o\", \"c\"]\n[77.067174, \"o\", \"t\"]\n[77.084444, \"o\", \"i\"]\n[77.142665, \"o\", \"o\"]\n[77.151852, \"o\", \"n\"]\n[77.161129, \"o\", \"_\"]\n[77.213291, \"o\", \"N\"]\n[77.244548, \"o\", \"E\"]\n[77.260819, \"o\", \"W\"]\n[77.318801, \"o\", \"\\r\\n\"]\n[77.329679, \"o\", \"$ \\r\\n\"]\n[77.33314, \"o\", \"$ b\"]\n[77.342305, \"o\", \"o\"]\n[77.54254, \"o\", \"r\"]\n[77.628787, \"o\", \"g\"]\n[77.676116, \"o\", \" \"]\n[77.743374, \"o\", \"c\"]\n[77.774609, \"o\", \"r\"]\n[77.801173, \"o\", \"e\"]\n[77.815449, \"o\", \"a\"]\n[77.825401, \"o\", \"t\"]\n[77.846987, \"o\", \"e\"]\n[78.047448, \"o\", \" \"]\n[78.106935, \"o\", \"-\"]\n[78.14254, \"o\", \"-\"]\n[78.153344, \"o\", \"s\"]\n[78.16262, \"o\", \"t\"]\n[78.172102, \"o\", \"a\"]\n[78.209331, \"o\", \"t\"]\n[78.241352, \"o\", \"s\"]\n[78.411208, \"o\", \" \"]\n[78.513499, \"o\", \"-\"]\n[78.59962, \"o\", \"-\"]\n[78.609363, \"o\", \"p\"]\n[78.6297, \"o\", \"r\"]\n[78.798934, \"o\", \"o\"]\n[78.808385, \"o\", \"g\"]\n[79.008927, \"o\", \"r\"]\n[79.020447, \"o\", \"e\"]\n[79.049225, \"o\", \"s\"]\n[79.059695, \"o\", \"s\"]\n[79.205329, \"o\", \" \"]\n[79.214453, \"o\", \"-\"]\n[79.227016, \"o\", \"-\"]\n[79.297294, \"o\", \"c\"]\n[79.49812, \"o\", \"o\"]\n[79.520796, \"o\", \"m\"]\n[79.536348, \"o\", \"p\"]\n[79.553311, \"o\", \"r\"]\n[79.592977, \"o\", \"e\"]\n[79.629942, \"o\", \"s\"]\n[79.656728, \"o\", \"s\"]\n[79.774107, \"o\", \"i\"]\n[79.808919, \"o\", \"o\"]\n[79.911941, \"o\", \"n\"]\n[79.933374, \"o\", \" \"]\n[79.951845, \"o\", \"l\"]\n[80.152409, \"o\", \"z\"]\n[80.313323, \"o\", \"4\"]\n[80.493778, \"o\", \" \"]\n[80.525364, \"o\", \"/\"]\n[80.601345, \"o\", \"m\"]\n[80.634233, \"o\", \"e\"]\n[80.643571, \"o\", \"d\"]\n[80.750189, \"o\", \"i\"]\n[80.83339, \"o\", \"a\"]\n[80.985813, \"o\", \"/\"]\n[81.068058, \"o\", \"b\"]\n[81.109491, \"o\", \"a\"]\n[81.309927, \"o\", \"c\"]\n[81.323449, \"o\", \"k\"]\n[81.392508, \"o\", \"u\"]\n[81.428703, \"o\", \"p\"]\n[81.449275, \"o\", \"/\"]\n[81.516755, \"o\", \"b\"]\n[81.526222, \"o\", \"o\"]\n[81.536684, \"o\", \"r\"]\n[81.546145, \"o\", \"g\"]\n[81.555588, \"o\", \"d\"]\n[81.582746, \"o\", \"e\"]\n[81.636297, \"o\", \"m\"]\n[81.681736, \"o\", \"o\"]\n[81.773332, \"o\", \":\"]\n[81.793656, \"o\", \":\"]\n[81.895127, \"o\", \"b\"]\n[81.926675, \"o\", \"a\"]\n[81.962742, \"o\", \"c\"]\n[81.973139, \"o\", \"k\"]\n[82.004992, \"o\", \"u\"]\n[82.021323, \"o\", \"p\"]\n[82.10136, \"o\", \"3\"]\n[82.217881, \"o\", \" \"]\n[82.229341, \"o\", \"W\"]\n[82.244659, \"o\", \"a\"]\n[82.281391, \"o\", \"l\"]\n[82.335922, \"o\", \"l\"]\n[82.345291, \"o\", \"p\"]\n[82.357335, \"o\", \"a\"]\n[82.389372, \"o\", \"p\"]\n[82.414494, \"o\", \"e\"]\n[82.497747, \"o\", \"r\"]\n[82.566107, \"o\", \"\\r\\n\"]\n[83.485219, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[85.045519, \"o\", \"\\r\"]\n[85.046073, \"o\", \"\\r\\n\"]\n[85.13408, \"o\", \"0 B O 0 B C 0 B D 0 N Wallpaper                                                 \\r\"]\n[85.151946, \"o\", \"Initializing cache transaction: Reading config                                  \\r\"]\n[85.152609, \"o\", \"Initializing cache transaction: Reading chunks                                  \\r\"]\n[85.153323, \"o\", \"Initializing cache transaction: Reading files                                   \\r\"]\n[85.153874, \"o\", \"                                                                                \\r\"]\n[85.3408, \"o\", \"41.33 MB O 41.19 MB C 0 B D 1 N Wallpaper/bigcollecti...enCL_528814521414_8K.jpg\\r\"]\n[85.554348, \"o\", \"85.57 MB O 85.31 MB C 0 B D 4 N Wallpaper/bigcollecti...x_OpenCL_18915424_8K.jpg\\r\"]\n[85.756273, \"o\", \"126.96 MB O 126.69 MB C 0 B D 10 N Wallpaper/bigcollec...penCL_4258952414_8K.jpg\\r\"]\n[85.966979, \"o\", \"170.79 MB O 170.51 MB C 0 B D 11 N Wallpaper/bigcollec...penCL_9648145412_8K.jpg\\r\"]\n[86.170995, \"o\", \"212.59 MB O 211.81 MB C 0 B D 16 N Wallpaper/bigcollec...enCL_51241841541_8K.jpg\\r\"]\n[86.372217, \"o\", \"255.05 MB O 254.10 MB C 0 B D 20 N Wallpaper/bigcollec...OpenCL_644289452_8K.jpg\\r\"]\n[86.585677, \"o\", \"298.20 MB O 296.90 MB C 0 B D 25 N Wallpaper/bigcollec...OpenCL_545185481_8K.jpg\\r\"]\n[86.794185, \"o\", \"342.20 MB O 340.79 MB C 0 B D 27 N Wallpaper/bigcollec...wamm_OpenCl_6184524.jpg\\r\"]\n[86.996936, \"o\", \"384.57 MB O 383.17 MB C 0 B D 29 N Wallpaper/bigcollec...ects._(14168975789).jpg\\r\"]\n[87.073155, \"o\", \"                                                                                \\r\"]\n[87.092608, \"o\", \"Saving files cache                                                              \\r\"]\n[87.094182, \"o\", \"Saving chunks cache                                                             \\r\"]\n[87.094972, \"o\", \"Saving cache config                                                             \\r\"]\n[87.096986, \"o\", \"                                                                                \\r\"]\n[87.099604, \"o\", \"------------------------------------------------------------------------------\\r\\r\\nRepository: /media/backup/borgdemo\\r\\r\\n\"]\n[87.099914, \"o\", \"Archive name: backup3\\r\\r\\n\"]\n[87.100242, \"o\", \"Archive fingerprint: 5ae78abd6558856f14289b7af3ea13f8550655c46624ad18d76e40da6ed1ee04\\r\\r\\n\"]\n[87.100603, \"o\", \"Time (start): Wed, 2022-07-06 21:29:03\\r\\r\\n\"]\n[87.100876, \"o\", \"Time (end):   Wed, 2022-07-06 21:29:05\\r\\r\\n\"]\n[87.101208, \"o\", \"Duration: 1.95 seconds\\r\\r\\n\"]\n[87.1015, \"o\", \"Number of files: 32\\r\\r\\n\"]\n[87.10179, \"o\", \"Utilization of max. archive size: 0%\\r\\r\\n\"]\n[87.102078, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[87.102524, \"o\", \"                       Original size      Compressed size    Deduplicated size\\r\\r\\n\"]\n[87.102832, \"o\", \"This archive:              401.15 MB            399.74 MB                550 B\\r\\r\\n\"]\n[87.103139, \"o\", \"All archives:                1.20 GB              1.20 GB            399.58 MB\\r\\r\\n\"]\n[87.103436, \"o\", \"\\r\\r\\n\"]\n[87.103734, \"o\", \"                       Unique chunks         Total chunks\\r\\r\\n\"]\n[87.104117, \"o\", \"Chunk index:                     213                  629\\r\\r\\n\"]\n[87.104448, \"o\", \"------------------------------------------------------------------------------\\r\\r\\n\"]\n[87.152849, \"o\", \"$ \\r\\n\"]\n[87.156745, \"o\", \"$ #\"]\n[87.209512, \"o\", \" \"]\n[87.293916, \"o\", \"S\"]\n[87.348284, \"o\", \"t\"]\n[87.366999, \"o\", \"i\"]\n[87.520505, \"o\", \"l\"]\n[87.53834, \"o\", \"l\"]\n[87.575493, \"o\", \" \"]\n[87.717844, \"o\", \"q\"]\n[87.821336, \"o\", \"u\"]\n[87.893612, \"o\", \"i\"]\n[87.933639, \"o\", \"t\"]\n[87.94296, \"o\", \"e\"]\n[87.976865, \"o\", \" \"]\n[88.014021, \"o\", \"f\"]\n[88.075291, \"o\", \"a\"]\n[88.097374, \"o\", \"s\"]\n[88.159006, \"o\", \"t\"]\n[88.203224, \"o\", \"…\"]\n[88.229661, \"o\", \"\\r\\n\"]\n[88.236034, \"o\", \"$ #\"]\n[88.37343, \"o\", \" \"]\n[88.40558, \"o\", \"B\"]\n[88.528518, \"o\", \"u\"]\n[88.569622, \"o\", \"t\"]\n[88.773509, \"o\", \" \"]\n[88.845482, \"o\", \"w\"]\n[88.876157, \"o\", \"h\"]\n[89.02846, \"o\", \"e\"]\n[89.037858, \"o\", \"n\"]\n[89.238437, \"o\", \" \"]\n[89.38946, \"o\", \"y\"]\n[89.441827, \"o\", \"o\"]\n[89.514261, \"o\", \"u\"]\n[89.610734, \"o\", \" \"]\n[89.677386, \"o\", \"l\"]\n[89.710803, \"o\", \"o\"]\n[89.767294, \"o\", \"o\"]\n[89.777491, \"o\", \"k\"]\n[89.833962, \"o\", \" \"]\n[89.873237, \"o\", \"a\"]\n[89.913382, \"o\", \"t\"]\n[90.045417, \"o\", \" \"]\n[90.06921, \"o\", \"t\"]\n[90.116559, \"o\", \"h\"]\n[90.131995, \"o\", \"e\"]\n[90.323428, \"o\", \" \"]\n[90.473531, \"o\", \"\\\"\"]\n[90.648246, \"o\", \"d\"]\n[90.661648, \"o\", \"e\"]\n[90.670834, \"o\", \"d\"]\n[90.723701, \"o\", \"u\"]\n[90.849449, \"o\", \"p\"]\n[90.892885, \"o\", \"l\"]\n[90.9136, \"o\", \"i\"]\n[90.945812, \"o\", \"c\"]\n[90.986109, \"o\", \"a\"]\n[91.08574, \"o\", \"t\"]\n[91.146406, \"o\", \"e\"]\n[91.16304, \"o\", \"d\"]\n[91.363904, \"o\", \" \"]\n[91.417291, \"o\", \"f\"]\n[91.433481, \"o\", \"i\"]\n[91.451001, \"o\", \"l\"]\n[91.481545, \"o\", \"e\"]\n[91.57759, \"o\", \" \"]\n[91.58698, \"o\", \"s\"]\n[91.706954, \"o\", \"i\"]\n[91.724347, \"o\", \"z\"]\n[91.8136, \"o\", \"e\"]\n[91.825139, \"o\", \"\\\"\"]\n[91.872525, \"o\", \" \"]\n[91.881863, \"o\", \"a\"]\n[91.973617, \"o\", \"g\"]\n[92.177436, \"o\", \"a\"]\n[92.251273, \"o\", \"i\"]\n[92.353509, \"o\", \"n\"]\n[92.537927, \"o\", \",\"]\n[92.571284, \"o\", \" \"]\n[92.663267, \"o\", \"y\"]\n[92.750934, \"o\", \"o\"]\n[92.797332, \"o\", \"u\"]\n[92.829397, \"o\", \" \"]\n[92.897956, \"o\", \"s\"]\n[92.908352, \"o\", \"e\"]\n[92.931057, \"o\", \"e\"]\n[92.955633, \"o\", \" \"]\n[92.992248, \"o\", \"t\"]\n[93.192829, \"o\", \"h\"]\n[93.320984, \"o\", \"a\"]\n[93.40135, \"o\", \"t\"]\n[93.60199, \"o\", \" \"]\n[93.645112, \"o\", \"b\"]\n[93.724391, \"o\", \"o\"]\n[93.862343, \"o\", \"r\"]\n[93.937373, \"o\", \"g\"]\n[93.990798, \"o\", \" \"]\n[94.150589, \"o\", \"a\"]\n[94.207057, \"o\", \"l\"]\n[94.241414, \"o\", \"s\"]\n[94.283425, \"o\", \"o\"]\n[94.436593, \"o\", \" \"]\n[94.446044, \"o\", \"r\"]\n[94.509642, \"o\", \"e\"]\n[94.535483, \"o\", \"c\"]\n[94.631229, \"o\", \"o\"]\n[94.665375, \"o\", \"g\"]\n[94.680755, \"o\", \"n\"]\n[94.713345, \"o\", \"i\"]\n[94.782818, \"o\", \"z\"]\n[94.792313, \"o\", \"e\"]\n[94.809509, \"o\", \"d\"]\n[94.853814, \"o\", \" \"]\n[94.863216, \"o\", \"t\"]\n[94.872586, \"o\", \"h\"]\n[94.89334, \"o\", \"a\"]\n[94.908753, \"o\", \"t\"]\n[94.941284, \"o\", \" \"]\n[94.987778, \"o\", \"o\"]\n[95.002399, \"o\", \"n\"]\n[95.147681, \"o\", \"l\"]\n[95.157126, \"o\", \"y\"]\n[95.173363, \"o\", \" \"]\n[95.19082, \"o\", \"t\"]\n[95.365301, \"o\", \"h\"]\n[95.454473, \"o\", \"e\"]\n[95.463921, \"o\", \" \"]\n[95.473274, \"o\", \"d\"]\n[95.551342, \"o\", \"i\"]\n[95.618296, \"o\", \"r\"]\n[95.718537, \"o\", \" \"]\n[95.741359, \"o\", \"a\"]\n[95.758986, \"o\", \"n\"]\n[95.793351, \"o\", \"d\"]\n[95.97488, \"o\", \" \"]\n[96.032312, \"o\", \"n\"]\n[96.05375, \"o\", \"o\"]\n[96.063143, \"o\", \"t\"]\n[96.26534, \"o\", \" \"]\n[96.349141, \"o\", \"t\"]\n[96.442503, \"o\", \"h\"]\n[96.52983, \"o\", \"e\"]\n[96.540245, \"o\", \" \"]\n[96.551714, \"o\", \"f\"]\n[96.616104, \"o\", \"i\"]\n[96.625488, \"o\", \"l\"]\n[96.642864, \"o\", \"e\"]\n[96.843732, \"o\", \"s\"]\n[96.861188, \"o\", \" \"]\n[96.977283, \"o\", \"c\"]\n[96.991812, \"o\", \"h\"]\n[97.095082, \"o\", \"a\"]\n[97.1045, \"o\", \"n\"]\n[97.113948, \"o\", \"g\"]\n[97.123416, \"o\", \"e\"]\n[97.209317, \"o\", \"d\"]\n[97.4101, \"o\", \" \"]\n[97.425341, \"o\", \"i\"]\n[97.589299, \"o\", \"n\"]\n[97.665522, \"o\", \" \"]\n[97.733363, \"o\", \"t\"]\n[97.755937, \"o\", \"h\"]\n[97.808313, \"o\", \"i\"]\n[97.817807, \"o\", \"s\"]\n[98.014252, \"o\", \" \"]\n[98.04937, \"o\", \"b\"]\n[98.081819, \"o\", \"a\"]\n[98.26958, \"o\", \"c\"]\n[98.317334, \"o\", \"k\"]\n[98.344896, \"o\", \"u\"]\n[98.385091, \"o\", \"p\"]\n[98.441567, \"o\", \".\"]\n[98.451772, \"o\", \"\\r\\n\"]\n[98.456939, \"o\", \"$ \\r\\n\"]\n[98.461066, \"o\", \"$ #\"]\n[98.529239, \"o\", \" \"]\n[98.570449, \"o\", \"N\"]\n[98.63069, \"o\", \"o\"]\n[98.696949, \"o\", \"w\"]\n[98.7642, \"o\", \" \"]\n[98.78642, \"o\", \"l\"]\n[98.855663, \"o\", \"e\"]\n[98.87792, \"o\", \"t\"]\n[98.906218, \"o\", \"s\"]\n[99.106438, \"o\", \" \"]\n[99.143675, \"o\", \"l\"]\n[99.152895, \"o\", \"o\"]\n[99.203156, \"o\", \"o\"]\n[99.240405, \"o\", \"k\"]\n[99.376664, \"o\", \" \"]\n[99.385854, \"o\", \"i\"]\n[99.400081, \"o\", \"n\"]\n[99.409194, \"o\", \"t\"]\n[99.495504, \"o\", \"o\"]\n[99.504768, \"o\", \" \"]\n[99.572024, \"o\", \"a\"]\n[99.581179, \"o\", \" \"]\n[99.617371, \"o\", \"r\"]\n[99.650628, \"o\", \"e\"]\n[99.705883, \"o\", \"p\"]\n[99.852135, \"o\", \"o\"]\n[99.861273, \"o\", \".\"]\n[99.964328, \"o\", \"\\r\\n\"]\n[99.969277, \"o\", \"$ b\"]\n[100.000532, \"o\", \"o\"]\n[100.009762, \"o\", \"r\"]\n[100.043017, \"o\", \"g\"]\n[100.052226, \"o\", \" \"]\n[100.0885, \"o\", \"l\"]\n[100.288738, \"o\", \"i\"]\n[100.305975, \"o\", \"s\"]\n[100.414237, \"o\", \"t\"]\n[100.549426, \"o\", \" \"]\n[100.576746, \"o\", \"/\"]\n[100.585934, \"o\", \"m\"]\n[100.63621, \"o\", \"e\"]\n[100.793379, \"o\", \"d\"]\n[100.802584, \"o\", \"i\"]\n[100.811831, \"o\", \"a\"]\n[100.840099, \"o\", \"/\"]\n[100.934358, \"o\", \"b\"]\n[100.945531, \"o\", \"a\"]\n[100.980793, \"o\", \"c\"]\n[101.046051, \"o\", \"k\"]\n[101.127323, \"o\", \"u\"]\n[101.146544, \"o\", \"p\"]\n[101.208799, \"o\", \"/\"]\n[101.316059, \"o\", \"b\"]\n[101.408305, \"o\", \"o\"]\n[101.559559, \"o\", \"r\"]\n[101.57882, \"o\", \"g\"]\n[101.619053, \"o\", \"d\"]\n[101.690318, \"o\", \"e\"]\n[101.851581, \"o\", \"m\"]\n[101.868834, \"o\", \"o\"]\n[101.895837, \"o\", \"\\r\\n\"]\n[102.82836, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[104.226926, \"o\", \"\\r\\r\\n\"]\n[104.317737, \"o\", \"backup1                              Wed, 2022-07-06 21:28:23 [4b2f146af02ce3a4df8018411ce46d97fe7eaad0512c969a4bbcd4f113900746]\\r\\r\\nbackup2                              Wed, 2022-07-06 21:28:40 [70d0bd96eb512ca96356308fb4aab162ae7d868c9fe491a36b1d8b00190e8e97]\\r\\r\\n\"]\n[104.318451, \"o\", \"backup3                              Wed, 2022-07-06 21:29:03 [5ae78abd6558856f14289b7af3ea13f8550655c46624ad18d76e40da6ed1ee04]\\r\\r\\n\"]\n[104.367169, \"o\", \"$ \"]\n[104.368499, \"o\", \"\\r\\n\"]\n[104.373216, \"o\", \"$ \"]\n[104.373787, \"o\", \"#\"]\n[104.385513, \"o\", \" \"]\n[104.40682, \"o\", \"Y\"]\n[104.441491, \"o\", \"o\"]\n[104.477718, \"o\", \"u\"]\n[104.678172, \"o\", \"'\"]\n[104.762495, \"o\", \"l\"]\n[104.794922, \"o\", \"l\"]\n[104.812639, \"o\", \" \"]\n[104.833147, \"o\", \"s\"]\n[104.901743, \"o\", \"e\"]\n[104.950219, \"o\", \"e\"]\n[105.122774, \"o\", \" \"]\n[105.177278, \"o\", \"a\"]\n[105.221438, \"o\", \" \"]\n[105.240947, \"o\", \"l\"]\n[105.26949, \"o\", \"i\"]\n[105.289501, \"o\", \"s\"]\n[105.301427, \"o\", \"t\"]\n[105.501757, \"o\", \" \"]\n[105.544171, \"o\", \"o\"]\n[105.555657, \"o\", \"f\"]\n[105.665411, \"o\", \" \"]\n[105.835304, \"o\", \"a\"]\n[105.84469, \"o\", \"l\"]\n[105.853955, \"o\", \"l\"]\n[105.878739, \"o\", \" \"]\n[105.93132, \"o\", \"b\"]\n[105.952058, \"o\", \"a\"]\n[105.981516, \"o\", \"c\"]\n[105.993763, \"o\", \"k\"]\n[106.035503, \"o\", \"u\"]\n[106.065513, \"o\", \"p\"]\n[106.109516, \"o\", \"s\"]\n[106.118728, \"o\", \".\"]\n[106.182447, \"o\", \"\\r\\n\"]\n[106.188985, \"o\", \"$ #\"]\n[106.289073, \"o\", \" \"]\n[106.29939, \"o\", \"Y\"]\n[106.308874, \"o\", \"o\"]\n[106.321566, \"o\", \"u\"]\n[106.521863, \"o\", \" \"]\n[106.596728, \"o\", \"c\"]\n[106.67751, \"o\", \"a\"]\n[106.700634, \"o\", \"n\"]\n[106.714282, \"o\", \" \"]\n[106.723841, \"o\", \"a\"]\n[106.807925, \"o\", \"l\"]\n[106.821467, \"o\", \"s\"]\n[107.025494, \"o\", \"o\"]\n[107.098377, \"o\", \" \"]\n[107.169619, \"o\", \"u\"]\n[107.180018, \"o\", \"s\"]\n[107.329587, \"o\", \"e\"]\n[107.397557, \"o\", \" \"]\n[107.419245, \"o\", \"t\"]\n[107.511283, \"o\", \"h\"]\n[107.572895, \"o\", \"e\"]\n[107.717994, \"o\", \" \"]\n[107.870106, \"o\", \"s\"]\n[107.993322, \"o\", \"a\"]\n[108.194277, \"o\", \"m\"]\n[108.229638, \"o\", \"e\"]\n[108.271397, \"o\", \" \"]\n[108.290782, \"o\", \"c\"]\n[108.333358, \"o\", \"o\"]\n[108.422361, \"o\", \"m\"]\n[108.46158, \"o\", \"m\"]\n[108.472043, \"o\", \"a\"]\n[108.481327, \"o\", \"n\"]\n[108.541571, \"o\", \"d\"]\n[108.56016, \"o\", \" \"]\n[108.625484, \"o\", \"t\"]\n[108.661281, \"o\", \"o\"]\n[108.861774, \"o\", \" \"]\n[108.871238, \"o\", \"l\"]\n[108.89738, \"o\", \"o\"]\n[108.993343, \"o\", \"o\"]\n[109.118797, \"o\", \"k\"]\n[109.201386, \"o\", \" \"]\n[109.29193, \"o\", \"i\"]\n[109.303382, \"o\", \"n\"]\n[109.321804, \"o\", \"t\"]\n[109.402727, \"o\", \"o\"]\n[109.419369, \"o\", \" \"]\n[109.46062, \"o\", \"a\"]\n[109.470188, \"o\", \"n\"]\n[109.670891, \"o\", \" \"]\n[109.687394, \"o\", \"a\"]\n[109.745313, \"o\", \"r\"]\n[109.807824, \"o\", \"c\"]\n[109.969317, \"o\", \"h\"]\n[110.023573, \"o\", \"i\"]\n[110.036116, \"o\", \"v\"]\n[110.045485, \"o\", \"e\"]\n[110.245943, \"o\", \".\"]\n[110.324697, \"o\", \" \"]\n[110.441361, \"o\", \"B\"]\n[110.459869, \"o\", \"u\"]\n[110.52936, \"o\", \"t\"]\n[110.729573, \"o\", \" \"]\n[110.738933, \"o\", \"w\"]\n[110.864128, \"o\", \"e\"]\n[110.969566, \"o\", \" \"]\n[110.984119, \"o\", \"b\"]\n[111.037292, \"o\", \"e\"]\n[111.108067, \"o\", \"t\"]\n[111.156532, \"o\", \"t\"]\n[111.172115, \"o\", \"e\"]\n[111.201326, \"o\", \"r\"]\n[111.405311, \"o\", \" \"]\n[111.506429, \"o\", \"f\"]\n[111.533961, \"o\", \"i\"]\n[111.548383, \"o\", \"l\"]\n[111.603669, \"o\", \"t\"]\n[111.645926, \"o\", \"e\"]\n[111.730354, \"o\", \"r\"]\n[111.817926, \"o\", \" \"]\n[111.838689, \"o\", \"t\"]\n[111.937146, \"o\", \"h\"]\n[111.953327, \"o\", \"e\"]\n[111.962645, \"o\", \" \"]\n[112.009343, \"o\", \"o\"]\n[112.13978, \"o\", \"u\"]\n[112.149267, \"o\", \"t\"]\n[112.205358, \"o\", \"p\"]\n[112.262791, \"o\", \"u\"]\n[112.309253, \"o\", \"t\"]\n[112.393456, \"o\", \" \"]\n[112.594288, \"o\", \"h\"]\n[112.617351, \"o\", \"e\"]\n[112.650093, \"o\", \"r\"]\n[112.721339, \"o\", \"e\"]\n[112.730808, \"o\", \":\"]\n[112.846207, \"o\", \"\\r\\n\"]\n[112.851859, \"o\", \"$ b\"]\n[112.911145, \"o\", \"o\"]\n[112.938366, \"o\", \"r\"]\n[112.963629, \"o\", \"g\"]\n[113.163858, \"o\", \" \"]\n[113.215117, \"o\", \"l\"]\n[113.235372, \"o\", \"i\"]\n[113.322596, \"o\", \"s\"]\n[113.331815, \"o\", \"t\"]\n[113.376114, \"o\", \" \"]\n[113.391322, \"o\", \"/\"]\n[113.416594, \"o\", \"m\"]\n[113.425798, \"o\", \"e\"]\n[113.519058, \"o\", \"d\"]\n[113.554283, \"o\", \"i\"]\n[113.636552, \"o\", \"a\"]\n[113.647766, \"o\", \"/\"]\n[113.667031, \"o\", \"b\"]\n[113.676302, \"o\", \"a\"]\n[113.757516, \"o\", \"c\"]\n[113.780751, \"o\", \"k\"]\n[113.869002, \"o\", \"u\"]\n[113.902268, \"o\", \"p\"]\n[114.040515, \"o\", \"/\"]\n[114.049714, \"o\", \"b\"]\n[114.063965, \"o\", \"o\"]\n[114.169232, \"o\", \"r\"]\n[114.195472, \"o\", \"g\"]\n[114.204679, \"o\", \"d\"]\n[114.334957, \"o\", \"e\"]\n[114.353261, \"o\", \"m\"]\n[114.362488, \"o\", \"o\"]\n[114.411742, \"o\", \":\"]\n[114.446012, \"o\", \":\"]\n[114.47327, \"o\", \"b\"]\n[114.494521, \"o\", \"a\"]\n[114.503728, \"o\", \"c\"]\n[114.575963, \"o\", \"k\"]\n[114.72126, \"o\", \"u\"]\n[114.903516, \"o\", \"p\"]\n[114.912735, \"o\", \"3\"]\n[114.936005, \"o\", \" \"]\n[114.959221, \"o\", \"|\"]\n[114.9684, \"o\", \" \"]\n[115.055677, \"o\", \"g\"]\n[115.107938, \"o\", \"r\"]\n[115.160181, \"o\", \"e\"]\n[115.220437, \"o\", \"p\"]\n[115.420688, \"o\", \" \"]\n[115.505923, \"o\", \"'\"]\n[115.540179, \"o\", \"d\"]\n[115.54932, \"o\", \"e\"]\n[115.576685, \"o\", \"e\"]\n[115.64793, \"o\", \"r\"]\n[115.847168, \"o\", \".\"]\n[115.889359, \"o\", \"j\"]\n[115.898548, \"o\", \"p\"]\n[115.965835, \"o\", \"g\"]\n[116.166111, \"o\", \"'\"]\n[116.175915, \"o\", \"\\r\\n\"]\n[117.125741, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[118.933873, \"o\", \"\\r\\r\\n\"]\n[119.024246, \"o\", \"-rw-r--r-- root   root     193033 Wed, 2022-07-06 21:27:37 Wallpaper/deer.jpg\\r\\r\\n\"]\n[119.077744, \"o\", \"$ \\r\\n\"]\n[119.081473, \"o\", \"$ #\"]\n[119.201982, \"o\", \" \"]\n[119.211507, \"o\", \"O\"]\n[119.253728, \"o\", \"h\"]\n[119.389612, \"o\", \",\"]\n[119.439978, \"o\", \" \"]\n[119.461686, \"o\", \"w\"]\n[119.52014, \"o\", \"e\"]\n[119.721157, \"o\", \" \"]\n[119.829452, \"o\", \"f\"]\n[119.853429, \"o\", \"o\"]\n[119.873001, \"o\", \"u\"]\n[119.891743, \"o\", \"n\"]\n[119.905595, \"o\", \"d\"]\n[120.10608, \"o\", \" \"]\n[120.148696, \"o\", \"o\"]\n[120.209153, \"o\", \"u\"]\n[120.333417, \"o\", \"r\"]\n[120.364325, \"o\", \" \"]\n[120.466866, \"o\", \"p\"]\n[120.581481, \"o\", \"i\"]\n[120.621287, \"o\", \"c\"]\n[120.640078, \"o\", \"t\"]\n[120.840575, \"o\", \"u\"]\n[120.857471, \"o\", \"r\"]\n[120.893922, \"o\", \"e\"]\n[121.020255, \"o\", \".\"]\n[121.036901, \"o\", \" \"]\n[121.071178, \"o\", \"N\"]\n[121.081806, \"o\", \"o\"]\n[121.099482, \"o\", \"w\"]\n[121.131093, \"o\", \" \"]\n[121.232917, \"o\", \"e\"]\n[121.242505, \"o\", \"x\"]\n[121.290029, \"o\", \"t\"]\n[121.337675, \"o\", \"r\"]\n[121.377245, \"o\", \"a\"]\n[121.534948, \"o\", \"c\"]\n[121.545462, \"o\", \"t\"]\n[121.60099, \"o\", \" \"]\n[121.649524, \"o\", \"i\"]\n[121.677501, \"o\", \"t\"]\n[121.749168, \"o\", \"…\"]\n[121.870956, \"o\", \"\\r\\n\"]\n[121.878453, \"o\", \"$ m\"]\n[121.905742, \"o\", \"v\"]\n[121.937554, \"o\", \" \"]\n[121.946689, \"o\", \"W\"]\n[121.967065, \"o\", \"a\"]\n[121.985499, \"o\", \"l\"]\n[121.994821, \"o\", \"l\"]\n[122.009501, \"o\", \"p\"]\n[122.057426, \"o\", \"a\"]\n[122.066873, \"o\", \"p\"]\n[122.267285, \"o\", \"e\"]\n[122.360793, \"o\", \"r\"]\n[122.453415, \"o\", \" \"]\n[122.513784, \"o\", \"W\"]\n[122.6265, \"o\", \"a\"]\n[122.730453, \"o\", \"l\"]\n[122.841305, \"o\", \"l\"]\n[123.009354, \"o\", \"p\"]\n[123.129353, \"o\", \"a\"]\n[123.169359, \"o\", \"p\"]\n[123.275878, \"o\", \"e\"]\n[123.313358, \"o\", \"r\"]\n[123.513655, \"o\", \".\"]\n[123.545339, \"o\", \"o\"]\n[123.559582, \"o\", \"r\"]\n[123.749344, \"o\", \"i\"]\n[123.801308, \"o\", \"g\"]\n[123.994717, \"o\", \"\\r\\n\"]\n[124.003853, \"o\", \"$ b\"]\n[124.013117, \"o\", \"o\"]\n[124.051773, \"o\", \"r\"]\n[124.098153, \"o\", \"g\"]\n[124.181336, \"o\", \" \"]\n[124.225346, \"o\", \"e\"]\n[124.291694, \"o\", \"x\"]\n[124.345105, \"o\", \"t\"]\n[124.442852, \"o\", \"r\"]\n[124.478188, \"o\", \"a\"]\n[124.531671, \"o\", \"c\"]\n[124.545143, \"o\", \"t\"]\n[124.634767, \"o\", \" \"]\n[124.689277, \"o\", \"/\"]\n[124.701314, \"o\", \"m\"]\n[124.870659, \"o\", \"e\"]\n[124.893388, \"o\", \"d\"]\n[124.961893, \"o\", \"i\"]\n[125.0534, \"o\", \"a\"]\n[125.062828, \"o\", \"/\"]\n[125.125355, \"o\", \"b\"]\n[125.193722, \"o\", \"a\"]\n[125.232321, \"o\", \"c\"]\n[125.277111, \"o\", \"k\"]\n[125.286592, \"o\", \"u\"]\n[125.317362, \"o\", \"p\"]\n[125.473543, \"o\", \"/\"]\n[125.493323, \"o\", \"b\"]\n[125.505356, \"o\", \"o\"]\n[125.531743, \"o\", \"r\"]\n[125.652233, \"o\", \"g\"]\n[125.677305, \"o\", \"d\"]\n[125.717712, \"o\", \"e\"]\n[125.747622, \"o\", \"m\"]\n[125.909364, \"o\", \"o\"]\n[126.110164, \"o\", \":\"]\n[126.185459, \"o\", \":\"]\n[126.217306, \"o\", \"b\"]\n[126.235863, \"o\", \"a\"]\n[126.261279, \"o\", \"c\"]\n[126.437637, \"o\", \"k\"]\n[126.610395, \"o\", \"u\"]\n[126.660867, \"o\", \"p\"]\n[126.681269, \"o\", \"3\"]\n[126.774683, \"o\", \" \"]\n[126.808707, \"o\", \"W\"]\n[126.826052, \"o\", \"a\"]\n[126.889457, \"o\", \"l\"]\n[126.902847, \"o\", \"l\"]\n[126.913333, \"o\", \"p\"]\n[126.977672, \"o\", \"a\"]\n[127.040095, \"o\", \"p\"]\n[127.093863, \"o\", \"e\"]\n[127.108476, \"o\", \"r\"]\n[127.117779, \"o\", \"/\"]\n[127.177324, \"o\", \"d\"]\n[127.208027, \"o\", \"e\"]\n[127.258753, \"o\", \"e\"]\n[127.328181, \"o\", \"r\"]\n[127.430698, \"o\", \".\"]\n[127.440219, \"o\", \"j\"]\n[127.493813, \"o\", \"p\"]\n[127.69734, \"o\", \"g\"]\n[127.745783, \"o\", \"\\r\\n\"]\n[128.676873, \"o\", \"Enter passphrase for key /media/backup/borgdemo: \"]\n[130.262495, \"o\", \"\\r\\r\\n\"]\n[130.40589, \"o\", \"$ \\r\\n\"]\n[130.408917, \"o\", \"$ \\r\\n\"]\n[130.411592, \"o\", \"$ #\"]\n[130.447042, \"o\", \" \"]\n[130.469463, \"o\", \"A\"]\n[130.486668, \"o\", \"n\"]\n[130.547045, \"o\", \"d\"]\n[130.610389, \"o\", \" \"]\n[130.619584, \"o\", \"c\"]\n[130.644904, \"o\", \"h\"]\n[130.684187, \"o\", \"e\"]\n[130.727571, \"o\", \"c\"]\n[130.781692, \"o\", \"k\"]\n[130.808069, \"o\", \" \"]\n[130.845396, \"o\", \"t\"]\n[131.045606, \"o\", \"h\"]\n[131.07192, \"o\", \"a\"]\n[131.104099, \"o\", \"t\"]\n[131.16738, \"o\", \" \"]\n[131.185618, \"o\", \"i\"]\n[131.305944, \"o\", \"t\"]\n[131.350278, \"o\", \"'\"]\n[131.362387, \"o\", \"s\"]\n[131.41572, \"o\", \" \"]\n[131.479032, \"o\", \"t\"]\n[131.525219, \"o\", \"h\"]\n[131.565355, \"o\", \"e\"]\n[131.739788, \"o\", \" \"]\n[131.765128, \"o\", \"s\"]\n[131.800433, \"o\", \"a\"]\n[131.844706, \"o\", \"m\"]\n[131.854797, \"o\", \"e\"]\n[131.949151, \"o\", \":\"]\n[132.150155, \"o\", \"\\r\\n\"]\n[132.155207, \"o\", \"$ d\"]\n[132.222684, \"o\", \"i\"]\n[132.231755, \"o\", \"f\"]\n[132.241114, \"o\", \"f\"]\n[132.264447, \"o\", \" \"]\n[132.408761, \"o\", \"-\"]\n[132.429115, \"o\", \"s\"]\n[132.568351, \"o\", \" \"]\n[132.604616, \"o\", \"W\"]\n[132.613685, \"o\", \"a\"]\n[132.683021, \"o\", \"l\"]\n[132.768332, \"o\", \"l\"]\n[132.777539, \"o\", \"p\"]\n[132.786743, \"o\", \"a\"]\n[132.804149, \"o\", \"p\"]\n[132.817466, \"o\", \"e\"]\n[132.848787, \"o\", \"r\"]\n[132.872136, \"o\", \"/\"]\n[132.895368, \"o\", \"d\"]\n[132.925566, \"o\", \"e\"]\n[132.949702, \"o\", \"e\"]\n[133.049156, \"o\", \"r\"]\n[133.05835, \"o\", \".\"]\n[133.203686, \"o\", \"j\"]\n[133.261883, \"o\", \"p\"]\n[133.293183, \"o\", \"g\"]\n[133.31844, \"o\", \" \"]\n[133.327651, \"o\", \"W\"]\n[133.355059, \"o\", \"a\"]\n[133.445333, \"o\", \"l\"]\n[133.529542, \"o\", \"l\"]\n[133.635825, \"o\", \"p\"]\n[133.657146, \"o\", \"a\"]\n[133.761233, \"o\", \"p\"]\n[133.846807, \"o\", \"e\"]\n[133.887209, \"o\", \"r\"]\n[134.059607, \"o\", \".\"]\n[134.068957, \"o\", \"o\"]\n[134.078295, \"o\", \"r\"]\n[134.087635, \"o\", \"i\"]\n[134.097002, \"o\", \"g\"]\n[134.188374, \"o\", \"/\"]\n[134.322782, \"o\", \"d\"]\n[134.464061, \"o\", \"e\"]\n[134.534314, \"o\", \"e\"]\n[134.734581, \"o\", \"r\"]\n[134.934851, \"o\", \".\"]\n[135.135128, \"o\", \"j\"]\n[135.320397, \"o\", \"p\"]\n[135.339681, \"o\", \"g\"]\n[135.399624, \"o\", \"\\r\\n\"]\n[135.408642, \"o\", \"Files Wallpaper/deer.jpg and Wallpaper.orig/deer.jpg are identical\"]\n[135.409223, \"o\", \"\\r\\r\\n\"]\n[135.409952, \"o\", \"$ \\r\\n\"]\n[135.413234, \"o\", \"$ #\"]\n[135.429446, \"o\", \" \"]\n[135.438723, \"o\", \"A\"]\n[135.491005, \"o\", \"n\"]\n[135.527292, \"o\", \"d\"]\n[135.563541, \"o\", \",\"]\n[135.594797, \"o\", \" \"]\n[135.732052, \"o\", \"o\"]\n[135.854273, \"o\", \"f\"]\n[135.887539, \"o\", \" \"]\n[135.972804, \"o\", \"c\"]\n[135.988043, \"o\", \"o\"]\n[136.120305, \"o\", \"u\"]\n[136.146563, \"o\", \"r\"]\n[136.190803, \"o\", \"s\"]\n[136.227062, \"o\", \"e\"]\n[136.310282, \"o\", \",\"]\n[136.338551, \"o\", \" \"]\n[136.388793, \"o\", \"w\"]\n[136.423051, \"o\", \"e\"]\n[136.432242, \"o\", \" \"]\n[136.473487, \"o\", \"c\"]\n[136.501693, \"o\", \"a\"]\n[136.544266, \"o\", \"n\"]\n[136.626521, \"o\", \" \"]\n[136.650782, \"o\", \"a\"]\n[136.689019, \"o\", \"l\"]\n[136.707277, \"o\", \"s\"]\n[136.740528, \"o\", \"o\"]\n[136.881757, \"o\", \" \"]\n[136.938024, \"o\", \"c\"]\n[136.968272, \"o\", \"r\"]\n[136.977439, \"o\", \"e\"]\n[137.048718, \"o\", \"a\"]\n[137.150961, \"o\", \"t\"]\n[137.191223, \"o\", \"e\"]\n[137.371461, \"o\", \" \"]\n[137.391709, \"o\", \"r\"]\n[137.406966, \"o\", \"e\"]\n[137.443207, \"o\", \"m\"]\n[137.467471, \"o\", \"o\"]\n[137.561694, \"o\", \"t\"]\n[137.600935, \"o\", \"e\"]\n[137.610129, \"o\", \" \"]\n[137.624379, \"o\", \"r\"]\n[137.63356, \"o\", \"e\"]\n[137.680854, \"o\", \"p\"]\n[137.766106, \"o\", \"o\"]\n[137.786347, \"o\", \"s\"]\n[137.813574, \"o\", \" \"]\n[137.832788, \"o\", \"v\"]\n[137.856081, \"o\", \"i\"]\n[137.89933, \"o\", \"a\"]\n[138.040577, \"o\", \" \"]\n[138.169867, \"o\", \"s\"]\n[138.290282, \"o\", \"s\"]\n[138.416402, \"o\", \"h\"]\n[138.442662, \"o\", \" \"]\n[138.454915, \"o\", \"w\"]\n[138.481224, \"o\", \"h\"]\n[138.552458, \"o\", \"e\"]\n[138.606708, \"o\", \"n\"]\n[138.805983, \"o\", \" \"]\n[138.886205, \"o\", \"b\"]\n[138.913396, \"o\", \"o\"]\n[138.948659, \"o\", \"r\"]\n[138.957905, \"o\", \"g\"]\n[138.967112, \"o\", \" \"]\n[139.060381, \"o\", \"i\"]\n[139.084639, \"o\", \"s\"]\n[139.162897, \"o\", \" \"]\n[139.172116, \"o\", \"s\"]\n[139.185255, \"o\", \"e\"]\n[139.194524, \"o\", \"t\"]\n[139.203728, \"o\", \"u\"]\n[139.218969, \"o\", \"p\"]\n[139.228203, \"o\", \" \"]\n[139.289406, \"o\", \"t\"]\n[139.298608, \"o\", \"h\"]\n[139.318866, \"o\", \"e\"]\n[139.383122, \"o\", \"r\"]\n[139.408376, \"o\", \"e\"]\n[139.473599, \"o\", \".\"]\n[139.490838, \"o\", \" \"]\n[139.623075, \"o\", \"T\"]\n[139.632287, \"o\", \"h\"]\n[139.82857, \"o\", \"i\"]\n[139.876831, \"o\", \"s\"]\n[139.896088, \"o\", \" \"]\n[139.907357, \"o\", \"c\"]\n[139.987619, \"o\", \"o\"]\n[140.128858, \"o\", \"m\"]\n[140.228101, \"o\", \"m\"]\n[140.256356, \"o\", \"a\"]\n[140.323596, \"o\", \"n\"]\n[140.332852, \"o\", \"d\"]\n[140.533173, \"o\", \" \"]\n[140.583433, \"o\", \"c\"]\n[140.677646, \"o\", \"r\"]\n[140.753916, \"o\", \"e\"]\n[140.834173, \"o\", \"a\"]\n[140.843438, \"o\", \"t\"]\n[140.883697, \"o\", \"e\"]\n[140.965972, \"o\", \"s\"]\n[140.995221, \"o\", \" \"]\n[141.022466, \"o\", \"a\"]\n[141.031666, \"o\", \" \"]\n[141.04089, \"o\", \"n\"]\n[141.050092, \"o\", \"e\"]\n[141.059315, \"o\", \"w\"]\n[141.073493, \"o\", \" \"]\n[141.122761, \"o\", \"r\"]\n[141.14304, \"o\", \"e\"]\n[141.167297, \"o\", \"m\"]\n[141.226536, \"o\", \"o\"]\n[141.235739, \"o\", \"t\"]\n[141.276996, \"o\", \"e\"]\n[141.389249, \"o\", \" \"]\n[141.41551, \"o\", \"r\"]\n[141.482767, \"o\", \"e\"]\n[141.534045, \"o\", \"p\"]\n[141.546224, \"o\", \"o\"]\n[141.606491, \"o\", \" \"]\n[141.641716, \"o\", \"i\"]\n[141.693986, \"o\", \"n\"]\n[141.894212, \"o\", \" \"]\n[141.913388, \"o\", \"a\"]\n[142.001611, \"o\", \" \"]\n[142.047848, \"o\", \"s\"]\n[142.057089, \"o\", \"u\"]\n[142.066324, \"o\", \"b\"]\n[142.075553, \"o\", \"d\"]\n[142.096819, \"o\", \"i\"]\n[142.156056, \"o\", \"r\"]\n[142.351298, \"o\", \"e\"]\n[142.37455, \"o\", \"c\"]\n[142.383749, \"o\", \"t\"]\n[142.470022, \"o\", \"o\"]\n[142.479243, \"o\", \"r\"]\n[142.488459, \"o\", \"y\"]\n[142.688723, \"o\", \" \"]\n[142.70696, \"o\", \"c\"]\n[142.734179, \"o\", \"a\"]\n[142.743376, \"o\", \"l\"]\n[142.802639, \"o\", \"l\"]\n[142.811856, \"o\", \"e\"]\n[142.917176, \"o\", \"d\"]\n[142.926375, \"o\", \" \"]\n[143.00463, \"o\", \"\\\"\"]\n[143.02188, \"o\", \"d\"]\n[143.096092, \"o\", \"e\"]\n[143.124341, \"o\", \"m\"]\n[143.140599, \"o\", \"o\"]\n[143.165884, \"o\", \"\\\"\"]\n[143.228134, \"o\", \":\"]\n[143.358243, \"o\", \"\\r\\n\"]\n[143.365151, \"o\", \"$ b\"]\n[143.448537, \"o\", \"o\"]\n[143.48368, \"o\", \"r\"]\n[143.525012, \"o\", \"g\"]\n[143.725553, \"o\", \" \"]\n[143.753366, \"o\", \"i\"]\n[143.821405, \"o\", \"n\"]\n[143.830651, \"o\", \"i\"]\n[143.941335, \"o\", \"t\"]\n[144.008199, \"o\", \" \"]\n[144.021391, \"o\", \"-\"]\n[144.03068, \"o\", \"-\"]\n[144.049269, \"o\", \"e\"]\n[144.077379, \"o\", \"n\"]\n[144.158636, \"o\", \"c\"]\n[144.168094, \"o\", \"r\"]\n[144.201268, \"o\", \"y\"]\n[144.213931, \"o\", \"p\"]\n[144.332512, \"o\", \"t\"]\n[144.34301, \"o\", \"i\"]\n[144.352454, \"o\", \"o\"]\n[144.429126, \"o\", \"n\"]\n[144.441555, \"o\", \"=\"]\n[144.498294, \"o\", \"r\"]\n[144.593347, \"o\", \"e\"]\n[144.666045, \"o\", \"p\"]\n[144.772517, \"o\", \"o\"]\n[144.781762, \"o\", \"k\"]\n[144.853563, \"o\", \"e\"]\n[144.899268, \"o\", \"y\"]\n[145.073332, \"o\", \" \"]\n[145.138026, \"o\", \"b\"]\n[145.1648, \"o\", \"o\"]\n[145.174212, \"o\", \"r\"]\n[145.185355, \"o\", \"g\"]\n[145.1947, \"o\", \"d\"]\n[145.231028, \"o\", \"e\"]\n[145.27231, \"o\", \"m\"]\n[145.281718, \"o\", \"o\"]\n[145.33316, \"o\", \"@\"]\n[145.345321, \"o\", \"r\"]\n[145.354569, \"o\", \"e\"]\n[145.363955, \"o\", \"m\"]\n[145.385359, \"o\", \"o\"]\n[145.417098, \"o\", \"t\"]\n[145.617375, \"o\", \"e\"]\n[145.626779, \"o\", \"s\"]\n[145.789323, \"o\", \"e\"]\n[145.974634, \"o\", \"r\"]\n[145.984032, \"o\", \"v\"]\n[146.109293, \"o\", \"e\"]\n[146.118745, \"o\", \"r\"]\n[146.220215, \"o\", \".\"]\n[146.229573, \"o\", \"e\"]\n[146.300845, \"o\", \"x\"]\n[146.315386, \"o\", \"a\"]\n[146.371957, \"o\", \"m\"]\n[146.423406, \"o\", \"p\"]\n[146.432879, \"o\", \"l\"]\n[146.461326, \"o\", \"e\"]\n[146.470777, \"o\", \":\"]\n[146.649372, \"o\", \".\"]\n[146.781057, \"o\", \"/\"]\n[146.79142, \"o\", \"d\"]\n[146.883059, \"o\", \"e\"]\n[147.083607, \"o\", \"m\"]\n[147.093004, \"o\", \"o\"]\n[147.137975, \"o\", \"\\r\\n\"]\n[149.114901, \"o\", \"Enter new passphrase: \"]\n[150.597186, \"o\", \"\\r\\r\\n\"]\n[150.598088, \"o\", \"Enter same passphrase again: \"]\n[151.665595, \"o\", \"\\r\\r\\n\"]\n[151.666627, \"o\", \"Do you want your passphrase to be displayed for verification? [yN]: \"]\n[151.667848, \"o\", \"\\r\\r\\n\"]\n[151.782469, \"o\", \"\\r\"]\n[151.782892, \"o\", \"\\r\\n\"]\n[151.783244, \"o\", \"By default repositories initialized with this version will produce security\\r\"]\n[151.783615, \"o\", \"\\r\\n\"]\n[151.783964, \"o\", \"errors if written to with an older version (up to and including Borg 1.0.8).\\r\"]\n[151.784342, \"o\", \"\\r\\n\"]\n[151.784708, \"o\", \"\\r\"]\n[151.785137, \"o\", \"\\r\\n\"]\n[151.785483, \"o\", \"If you want to use these older versions, you can disable the check by running:\\r\"]\n[151.785943, \"o\", \"\\r\\n\"]\n[151.786346, \"o\", \"borg upgrade --disable-tam ssh://borgdemo@remoteserver.example/./demo\\r\"]\n[151.787328, \"o\", \"\\r\\n\"]\n[151.787744, \"o\", \"\\r\"]\n[151.788193, \"o\", \"\\r\\n\"]\n[151.788604, \"o\", \"See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details about the security implications.\\r\"]\n[151.789148, \"o\", \"\\r\\n\"]\n[151.789649, \"o\", \"\\r\"]\n[151.79011, \"o\", \"\\r\\n\"]\n[151.790513, \"o\", \"IMPORTANT: you will need both KEY AND PASSPHRASE to access this repo!\\r\"]\n[151.790949, \"o\", \"\\r\\n\"]\n[151.791357, \"o\", \"If you used a repokey mode, the key is stored in the repo, but you should back it up separately.\\r\"]\n[151.791803, \"o\", \"\\r\\n\"]\n[151.792205, \"o\", \"Use \\\"borg key export\\\" to export the key, optionally in printable format.\\r\"]\n[151.792628, \"o\", \"\\r\\n\"]\n[151.793095, \"o\", \"Write down the passphrase. Store both at safe place(s).\\r\"]\n[151.793499, \"o\", \"\\r\\n\"]\n[151.793912, \"o\", \"\\r\"]\n[151.794148, \"o\", \"\\r\\n\"]\n[151.895133, \"o\", \"$ \\r\\n\"]\n[151.898504, \"o\", \"$ #\"]\n[151.907686, \"o\", \" \"]\n[151.970961, \"o\", \"E\"]\n[152.046187, \"o\", \"a\"]\n[152.072458, \"o\", \"s\"]\n[152.089663, \"o\", \"y\"]\n[152.169934, \"o\", \",\"]\n[152.22125, \"o\", \" \"]\n[152.241441, \"o\", \"i\"]\n[152.250723, \"o\", \"s\"]\n[152.259967, \"o\", \"n\"]\n[152.356266, \"o\", \"'\"]\n[152.365406, \"o\", \"t\"]\n[152.431677, \"o\", \" \"]\n[152.44087, \"o\", \"i\"]\n[152.53011, \"o\", \"t\"]\n[152.602336, \"o\", \"?\"]\n[152.629528, \"o\", \" \"]\n[152.680785, \"o\", \"T\"]\n[152.72004, \"o\", \"h\"]\n[152.762269, \"o\", \"a\"]\n[152.859548, \"o\", \"t\"]\n[152.943805, \"o\", \"'\"]\n[153.102059, \"o\", \"s\"]\n[153.202278, \"o\", \" \"]\n[153.216515, \"o\", \"a\"]\n[153.238773, \"o\", \"l\"]\n[153.301008, \"o\", \"l\"]\n[153.31021, \"o\", \" \"]\n[153.320433, \"o\", \"y\"]\n[153.451705, \"o\", \"o\"]\n[153.460955, \"o\", \"u\"]\n[153.473202, \"o\", \" \"]\n[153.499452, \"o\", \"n\"]\n[153.562694, \"o\", \"e\"]\n[153.599963, \"o\", \"e\"]\n[153.661289, \"o\", \"d\"]\n[153.692535, \"o\", \" \"]\n[153.701752, \"o\", \"t\"]\n[153.786055, \"o\", \"o\"]\n[153.795264, \"o\", \" \"]\n[153.804528, \"o\", \"k\"]\n[153.832785, \"o\", \"n\"]\n[153.842005, \"o\", \"o\"]\n[153.859276, \"o\", \"w\"]\n[153.896547, \"o\", \" \"]\n[153.945818, \"o\", \"f\"]\n[154.028037, \"o\", \"o\"]\n[154.063319, \"o\", \"r\"]\n[154.094576, \"o\", \" \"]\n[154.103766, \"o\", \"b\"]\n[154.112993, \"o\", \"a\"]\n[154.122204, \"o\", \"s\"]\n[154.182445, \"o\", \"i\"]\n[154.265671, \"o\", \"c\"]\n[154.407925, \"o\", \" \"]\n[154.47121, \"o\", \"u\"]\n[154.48045, \"o\", \"s\"]\n[154.51172, \"o\", \"a\"]\n[154.583955, \"o\", \"g\"]\n[154.59321, \"o\", \"e\"]\n[154.643476, \"o\", \".\"]\n[154.705542, \"o\", \"\\r\\n\"]\n[154.710326, \"o\", \"$ #\"]\n[154.794577, \"o\", \" \"]\n[154.822836, \"o\", \"I\"]\n[154.900102, \"o\", \"f\"]\n[154.934346, \"o\", \" \"]\n[154.965547, \"o\", \"y\"]\n[155.030799, \"o\", \"o\"]\n[155.039994, \"o\", \"u\"]\n[155.078232, \"o\", \" \"]\n[155.116495, \"o\", \"w\"]\n[155.13473, \"o\", \"a\"]\n[155.154984, \"o\", \"n\"]\n[155.243264, \"o\", \"t\"]\n[155.339518, \"o\", \" \"]\n[155.512774, \"o\", \"t\"]\n[155.563011, \"o\", \"o\"]\n[155.576324, \"o\", \" \"]\n[155.585503, \"o\", \"s\"]\n[155.623775, \"o\", \"e\"]\n[155.819025, \"o\", \"e\"]\n[155.832295, \"o\", \" \"]\n[155.841447, \"o\", \"m\"]\n[155.919707, \"o\", \"o\"]\n[155.972905, \"o\", \"r\"]\n[156.038151, \"o\", \"e\"]\n[156.168404, \"o\", \",\"]\n[156.281607, \"o\", \" \"]\n[156.296815, \"o\", \"h\"]\n[156.347095, \"o\", \"a\"]\n[156.356281, \"o\", \"v\"]\n[156.423555, \"o\", \"e\"]\n[156.62379, \"o\", \" \"]\n[156.72804, \"o\", \"a\"]\n[156.928302, \"o\", \" \"]\n[156.957503, \"o\", \"l\"]\n[157.093718, \"o\", \"o\"]\n[157.102981, \"o\", \"o\"]\n[157.25025, \"o\", \"k\"]\n[157.414462, \"o\", \" \"]\n[157.448727, \"o\", \"a\"]\n[157.482013, \"o\", \"t\"]\n[157.494193, \"o\", \" \"]\n[157.586441, \"o\", \"t\"]\n[157.604659, \"o\", \"h\"]\n[157.638907, \"o\", \"e\"]\n[157.725193, \"o\", \" \"]\n[157.789369, \"o\", \"s\"]\n[157.814605, \"o\", \"c\"]\n[157.835888, \"o\", \"r\"]\n[157.851084, \"o\", \"e\"]\n[157.874326, \"o\", \"e\"]\n[157.907588, \"o\", \"n\"]\n[157.922828, \"o\", \"c\"]\n[158.06203, \"o\", \"a\"]\n[158.183285, \"o\", \"s\"]\n[158.255522, \"o\", \"t\"]\n[158.312769, \"o\", \" \"]\n[158.356027, \"o\", \"s\"]\n[158.43626, \"o\", \"h\"]\n[158.445422, \"o\", \"o\"]\n[158.496674, \"o\", \"w\"]\n[158.534909, \"o\", \"i\"]\n[158.600166, \"o\", \"n\"]\n[158.616418, \"o\", \"g\"]\n[158.681622, \"o\", \" \"]\n[158.827882, \"o\", \"t\"]\n[158.840093, \"o\", \"h\"]\n[158.896394, \"o\", \"e\"]\n[159.096639, \"o\", \" \"]\n[159.105862, \"o\", \"\\\"\"]\n[159.238097, \"o\", \"a\"]\n[159.247287, \"o\", \"d\"]\n[159.36655, \"o\", \"v\"]\n[159.404799, \"o\", \"a\"]\n[159.436042, \"o\", \"n\"]\n[159.462286, \"o\", \"c\"]\n[159.471478, \"o\", \"e\"]\n[159.625717, \"o\", \"d\"]\n[159.79098, \"o\", \" \"]\n[159.823215, \"o\", \"u\"]\n[159.835412, \"o\", \"s\"]\n[159.99268, \"o\", \"a\"]\n[160.057935, \"o\", \"g\"]\n[160.072129, \"o\", \"e\"]\n[160.181283, \"o\", \"\\\"\"]\n[160.207533, \"o\", \".\"]\n[160.217424, \"o\", \"\\r\\n\"]\n[160.222722, \"o\", \"$ #\"]\n[160.422984, \"o\", \" \"]\n[160.50824, \"o\", \"I\"]\n[160.650484, \"o\", \"n\"]\n[160.749699, \"o\", \" \"]\n[160.758908, \"o\", \"a\"]\n[160.78818, \"o\", \"n\"]\n[160.845324, \"o\", \"y\"]\n[160.892602, \"o\", \" \"]\n[160.916871, \"o\", \"c\"]\n[161.034093, \"o\", \"a\"]\n[161.116346, \"o\", \"s\"]\n[161.125547, \"o\", \"e\"]\n[161.245789, \"o\", \",\"]\n[161.311995, \"o\", \" \"]\n[161.323216, \"o\", \"e\"]\n[161.346473, \"o\", \"n\"]\n[161.362715, \"o\", \"j\"]\n[161.390948, \"o\", \"o\"]\n[161.469285, \"o\", \"y\"]\n[161.487562, \"o\", \" \"]\n[161.511806, \"o\", \"u\"]\n[161.53403, \"o\", \"s\"]\n[161.586248, \"o\", \"i\"]\n[161.609441, \"o\", \"n\"]\n[161.628709, \"o\", \"g\"]\n[161.64495, \"o\", \" \"]\n[161.694222, \"o\", \"b\"]\n[161.765407, \"o\", \"o\"]\n[161.785601, \"o\", \"r\"]\n[161.836877, \"o\", \"g\"]\n[162.041912, \"o\", \"!\"]\n[162.062134, \"o\", \"\\r\\n\"]\n"
  },
  {
    "path": "docs/misc/asciinema/basic.tcl",
    "content": "# Configuration for send -h\n# Tries to emulate a human typing\n# Tweak this if typing is too fast or too slow\nset send_human {.05 .1 1 .01 .2}\n\nset script {\n# Here you'll see some basic commands to start working with borg.\n# Note: This teaser screencast was made with __BORG_VERSION__ – older or newer borg versions may behave differently.\n# But let's start.\n\n# First of all, you can always get help:\nborg help\n# These are a lot of commands, so better we start with a few:\n# Let's create a repo on an external drive…\nborg init --encryption=repokey /media/backup/borgdemo\n# This uses the repokey encryption. You may look at \"borg help init\" or the online doc at https://borgbackup.readthedocs.io/ for other modes.\n\n# So now, let's create our first (compressed) backup.\nborg create --stats --progress --compression lz4 /media/backup/borgdemo::backup1 Wallpaper\n\n# That's nice, so far.\n# So let's add a new file…\necho \"new nice file\" > Wallpaper/newfile.txt\n\nborg create --stats --progress --compression lz4 /media/backup/borgdemo::backup2 Wallpaper\n\n# Wow, this was a lot faster!\n# Notice the \"Deduplicated size\" for \"This archive\"!\n# Borg recognized that most files did not change and deduplicated them.\n\n# But what happens, when we move a dir and create a new backup?\nmv Wallpaper/bigcollection Wallpaper/bigcollection_NEW\n\nborg create --stats --progress --compression lz4 /media/backup/borgdemo::backup3 Wallpaper\n\n# Still quite fast…\n# But when you look at the \"deduplicated file size\" again, you see that borg also recognized that only the dir and not the files changed in this backup.\n\n# Now lets look into a repo.\nborg list /media/backup/borgdemo\n\n# You'll see a list of all backups.\n# You can also use the same command to look into an archive. But we better filter the output here:\nborg list /media/backup/borgdemo::backup3 | grep 'deer.jpg'\n\n# Oh, we found our picture. Now extract it…\nmv Wallpaper Wallpaper.orig\nborg extract /media/backup/borgdemo::backup3 Wallpaper/deer.jpg\n\n\n# And check that it's the same:\ndiff -s Wallpaper/deer.jpg Wallpaper.orig/deer.jpg\n\n# And, of course, we can also create remote repos via ssh when borg is setup there. This command creates a new remote repo in a subdirectory called \"demo\":\nborg init --encryption=repokey borgdemo@remoteserver.example:./demo\n\n# Easy, isn't it? That's all you need to know for basic usage.\n# If you want to see more, have a look at the screencast showing the \"advanced usage\".\n# In any case, enjoy using borg!\n}\n\nset script [string trim $script]\nset script [string map [list __BORG_VERSION__ [exec borg -V]] $script]\nset script [split $script \\n]\n\nforeach line $script {\n  send_user \"$ \"\n  send_user -h $line\\n\n  spawn -noecho /bin/sh -c $line\n  expect {\n    \"Enter new passphrase: \" {\n      send -h \"correct horse battery staple\\n\"\n      exp_continue\n    }\n    \"Enter same passphrase again: \" {\n      send -h \"correct horse battery staple\\n\"\n      exp_continue\n    }\n    \"Enter passphrase for key /media/backup/borgdemo: \" {\n      send -h \"correct horse battery staple\\n\"\n      exp_continue\n    }\n    -ex {Do you want your passphrase to be displayed for verification? [yN]: } {\n      send \\n\n      exp_continue\n    }\n    eof\n  }\n}\n"
  },
  {
    "path": "docs/misc/asciinema/install.json",
    "content": "{\"version\": 2, \"width\": 80, \"height\": 24, \"timestamp\": 1657142795, \"env\": {\"SHELL\": \"/bin/bash\", \"TERM\": \"vt100\"}}\n[0.014053, \"o\", \"$ \"]\n[0.014802, \"o\", \"#\"]\n[0.145809, \"o\", \" \"]\n[0.154894, \"o\", \"T\"]\n[0.185287, \"o\", \"h\"]\n[0.19475, \"o\", \"i\"]\n[0.211085, \"o\", \"s\"]\n[0.321388, \"o\", \" \"]\n[0.389817, \"o\", \"a\"]\n[0.433819, \"o\", \"s\"]\n[0.465846, \"o\", \"c\"]\n[0.477783, \"o\", \"i\"]\n[0.489867, \"o\", \"i\"]\n[0.502142, \"o\", \"n\"]\n[0.562389, \"o\", \"e\"]\n[0.584049, \"o\", \"m\"]\n[0.621852, \"o\", \"a\"]\n[0.657828, \"o\", \" \"]\n[0.667252, \"o\", \"w\"]\n[0.701711, \"o\", \"i\"]\n[0.721812, \"o\", \"l\"]\n[0.761768, \"o\", \"l\"]\n[0.842137, \"o\", \" \"]\n[0.977783, \"o\", \"s\"]\n[1.072032, \"o\", \"h\"]\n[1.117847, \"o\", \"o\"]\n[1.28305, \"o\", \"w\"]\n[1.347577, \"o\", \" \"]\n[1.420286, \"o\", \"y\"]\n[1.441849, \"o\", \"o\"]\n[1.465377, \"o\", \"u\"]\n[1.52183, \"o\", \" \"]\n[1.531208, \"o\", \"t\"]\n[1.548712, \"o\", \"h\"]\n[1.566005, \"o\", \"e\"]\n[1.609195, \"o\", \" \"]\n[1.621698, \"o\", \"i\"]\n[1.663977, \"o\", \"n\"]\n[1.741742, \"o\", \"s\"]\n[1.809851, \"o\", \"t\"]\n[1.929424, \"o\", \"a\"]\n[1.943799, \"o\", \"l\"]\n[2.039221, \"o\", \"l\"]\n[2.129831, \"o\", \"a\"]\n[2.144218, \"o\", \"t\"]\n[2.165843, \"o\", \"i\"]\n[2.17517, \"o\", \"o\"]\n[2.244748, \"o\", \"n\"]\n[2.294097, \"o\", \" \"]\n[2.304666, \"o\", \"o\"]\n[2.319139, \"o\", \"f\"]\n[2.436131, \"o\", \" \"]\n[2.469029, \"o\", \"b\"]\n[2.511209, \"o\", \"o\"]\n[2.558617, \"o\", \"r\"]\n[2.567952, \"o\", \"g\"]\n[2.768307, \"o\", \" \"]\n[2.813853, \"o\", \"a\"]\n[2.83777, \"o\", \"s\"]\n[2.901612, \"o\", \" \"]\n[2.957777, \"o\", \"a\"]\n[3.080693, \"o\", \" \"]\n[3.197825, \"o\", \"s\"]\n[3.240999, \"o\", \"t\"]\n[3.31471, \"o\", \"a\"]\n[3.486239, \"o\", \"n\"]\n[3.545799, \"o\", \"d\"]\n[3.746438, \"o\", \"a\"]\n[3.765807, \"o\", \"l\"]\n[3.799391, \"o\", \"o\"]\n[3.86407, \"o\", \"n\"]\n[3.877844, \"o\", \"e\"]\n[4.010577, \"o\", \" \"]\n[4.033887, \"o\", \"b\"]\n[4.119113, \"o\", \"i\"]\n[4.312337, \"o\", \"n\"]\n[4.341142, \"o\", \"a\"]\n[4.541664, \"o\", \"r\"]\n[4.576179, \"o\", \"y\"]\n[4.753067, \"o\", \".\"]\n[4.898458, \"o\", \" \"]\n[4.969834, \"o\", \"U\"]\n[4.986322, \"o\", \"s\"]\n[4.996775, \"o\", \"u\"]\n[5.016159, \"o\", \"a\"]\n[5.071603, \"o\", \"l\"]\n[5.088116, \"o\", \"l\"]\n[5.102688, \"o\", \"y\"]\n[5.113827, \"o\", \" \"]\n[5.165786, \"o\", \"y\"]\n[5.201829, \"o\", \"o\"]\n[5.233547, \"o\", \"u\"]\n[5.243953, \"o\", \" \"]\n[5.269861, \"o\", \"o\"]\n[5.279354, \"o\", \"n\"]\n[5.379507, \"o\", \"l\"]\n[5.388926, \"o\", \"y\"]\n[5.461163, \"o\", \" \"]\n[5.510869, \"o\", \"n\"]\n[5.539994, \"o\", \"e\"]\n[5.740252, \"o\", \"e\"]\n[5.749606, \"o\", \"d\"]\n[5.858895, \"o\", \" \"]\n[5.893866, \"o\", \"t\"]\n[5.907335, \"o\", \"h\"]\n[5.940743, \"o\", \"i\"]\n[5.955261, \"o\", \"s\"]\n[6.013788, \"o\", \" \"]\n[6.027155, \"o\", \"i\"]\n[6.097878, \"o\", \"f\"]\n[6.12984, \"o\", \" \"]\n[6.29044, \"o\", \"y\"]\n[6.32487, \"o\", \"o\"]\n[6.344524, \"o\", \"u\"]\n[6.361864, \"o\", \" \"]\n[6.406259, \"o\", \"w\"]\n[6.480319, \"o\", \"a\"]\n[6.491805, \"o\", \"n\"]\n[6.539051, \"o\", \"t\"]\n[6.621658, \"o\", \" \"]\n[6.666422, \"o\", \"t\"]\n[6.677777, \"o\", \"o\"]\n[6.687147, \"o\", \" \"]\n[6.745773, \"o\", \"h\"]\n[6.761323, \"o\", \"a\"]\n[6.857807, \"o\", \"v\"]\n[6.877241, \"o\", \"e\"]\n[6.928841, \"o\", \" \"]\n[6.953838, \"o\", \"a\"]\n[7.018219, \"o\", \"n\"]\n[7.204957, \"o\", \" \"]\n[7.214395, \"o\", \"u\"]\n[7.308934, \"o\", \"p\"]\n[7.341736, \"o\", \"-\"]\n[7.391894, \"o\", \"t\"]\n[7.488804, \"o\", \"o\"]\n[7.565371, \"o\", \"-\"]\n[7.673841, \"o\", \"d\"]\n[7.691235, \"o\", \"a\"]\n[7.730541, \"o\", \"t\"]\n[7.779885, \"o\", \"e\"]\n[7.82105, \"o\", \" \"]\n[7.857857, \"o\", \"v\"]\n[7.973835, \"o\", \"e\"]\n[8.005875, \"o\", \"r\"]\n[8.03317, \"o\", \"s\"]\n[8.04773, \"o\", \"i\"]\n[8.05921, \"o\", \"o\"]\n[8.108989, \"o\", \"n\"]\n[8.309151, \"o\", \" \"]\n[8.382718, \"o\", \"o\"]\n[8.395123, \"o\", \"f\"]\n[8.53363, \"o\", \" \"]\n[8.575906, \"o\", \"b\"]\n[8.633447, \"o\", \"o\"]\n[8.710623, \"o\", \"r\"]\n[8.738389, \"o\", \"g\"]\n[8.887713, \"o\", \" \"]\n[9.045955, \"o\", \"o\"]\n[9.085246, \"o\", \"r\"]\n[9.157826, \"o\", \" \"]\n[9.241871, \"o\", \"n\"]\n[9.2714, \"o\", \"o\"]\n[9.285859, \"o\", \" \"]\n[9.381743, \"o\", \"p\"]\n[9.391193, \"o\", \"a\"]\n[9.572886, \"o\", \"c\"]\n[9.582362, \"o\", \"k\"]\n[9.648051, \"o\", \"a\"]\n[9.657843, \"o\", \"g\"]\n[9.718129, \"o\", \"e\"]\n[9.732774, \"o\", \" \"]\n[9.757155, \"o\", \"i\"]\n[9.946487, \"o\", \"s\"]\n[10.107944, \"o\", \" \"]\n[10.195191, \"o\", \"a\"]\n[10.22102, \"o\", \"v\"]\n[10.230446, \"o\", \"a\"]\n[10.311846, \"o\", \"i\"]\n[10.393675, \"o\", \"l\"]\n[10.453805, \"o\", \"a\"]\n[10.475504, \"o\", \"b\"]\n[10.55474, \"o\", \"l\"]\n[10.585603, \"o\", \"e\"]\n[10.785858, \"o\", \" \"]\n[10.81877, \"o\", \"f\"]\n[10.828264, \"o\", \"o\"]\n[10.83782, \"o\", \"r\"]\n[10.869826, \"o\", \" \"]\n[11.040749, \"o\", \"y\"]\n[11.069832, \"o\", \"o\"]\n[11.106365, \"o\", \"u\"]\n[11.144892, \"o\", \"r\"]\n[11.345408, \"o\", \" \"]\n[11.396637, \"o\", \"d\"]\n[11.421235, \"o\", \"i\"]\n[11.577404, \"o\", \"s\"]\n[11.602081, \"o\", \"t\"]\n[11.621845, \"o\", \"r\"]\n[11.652436, \"o\", \"o\"]\n[11.709814, \"o\", \"/\"]\n[11.719056, \"o\", \"O\"]\n[11.763376, \"o\", \"S\"]\n[11.849898, \"o\", \".\"]\n[11.923473, \"o\", \"\\r\\n\"]\n[11.930008, \"o\", \"$ \\r\\n\"]\n[11.934924, \"o\", \"$ #\"]\n[12.07232, \"o\", \" \"]\n[12.084395, \"o\", \"F\"]\n[12.280637, \"o\", \"i\"]\n[12.347896, \"o\", \"r\"]\n[12.477136, \"o\", \"s\"]\n[12.486354, \"o\", \"t\"]\n[12.562609, \"o\", \",\"]\n[12.571853, \"o\", \" \"]\n[12.581063, \"o\", \"w\"]\n[12.656333, \"o\", \"e\"]\n[12.672544, \"o\", \" \"]\n[12.78082, \"o\", \"n\"]\n[12.789995, \"o\", \"e\"]\n[12.799208, \"o\", \"e\"]\n[12.81747, \"o\", \"d\"]\n[12.897764, \"o\", \" \"]\n[12.951046, \"o\", \"t\"]\n[12.961262, \"o\", \"o\"]\n[13.161539, \"o\", \" \"]\n[13.246799, \"o\", \"d\"]\n[13.367039, \"o\", \"o\"]\n[13.388292, \"o\", \"w\"]\n[13.397491, \"o\", \"n\"]\n[13.406729, \"o\", \"l\"]\n[13.44496, \"o\", \"o\"]\n[13.57222, \"o\", \"a\"]\n[13.60648, \"o\", \"d\"]\n[13.618687, \"o\", \" \"]\n[13.64895, \"o\", \"t\"]\n[13.758167, \"o\", \"h\"]\n[13.7754, \"o\", \"e\"]\n[13.824655, \"o\", \" \"]\n[13.929823, \"o\", \"v\"]\n[13.939035, \"o\", \"e\"]\n[14.053307, \"o\", \"r\"]\n[14.134551, \"o\", \"s\"]\n[14.143768, \"o\", \"i\"]\n[14.155002, \"o\", \"o\"]\n[14.304273, \"o\", \"n\"]\n[14.504522, \"o\", \",\"]\n[14.702776, \"o\", \" \"]\n[14.805964, \"o\", \"w\"]\n[14.84922, \"o\", \"e\"]\n[15.001462, \"o\", \"'\"]\n[15.073745, \"o\", \"d\"]\n[15.18095, \"o\", \" \"]\n[15.19016, \"o\", \"l\"]\n[15.200396, \"o\", \"i\"]\n[15.292691, \"o\", \"k\"]\n[15.301811, \"o\", \"e\"]\n[15.311038, \"o\", \" \"]\n[15.348299, \"o\", \"t\"]\n[15.358512, \"o\", \"o\"]\n[15.558793, \"o\", \" \"]\n[15.612999, \"o\", \"i\"]\n[15.677237, \"o\", \"n\"]\n[15.724497, \"o\", \"s\"]\n[15.733689, \"o\", \"t\"]\n[15.826957, \"o\", \"a\"]\n[15.836163, \"o\", \"l\"]\n[15.920434, \"o\", \"l\"]\n[16.120714, \"o\", \"…\"]\n[16.133834, \"o\", \"\\r\\n\"]\n[16.140577, \"o\", \"$ w\"]\n[16.205727, \"o\", \"g\"]\n[16.214938, \"o\", \"e\"]\n[16.230177, \"o\", \"t\"]\n[16.331425, \"o\", \" \"]\n[16.34063, \"o\", \"-\"]\n[16.540913, \"o\", \"q\"]\n[16.581185, \"o\", \" \"]\n[16.590402, \"o\", \"-\"]\n[16.689716, \"o\", \"-\"]\n[16.80895, \"o\", \"s\"]\n[16.858176, \"o\", \"h\"]\n[16.883419, \"o\", \"o\"]\n[16.924718, \"o\", \"w\"]\n[17.055923, \"o\", \"-\"]\n[17.100182, \"o\", \"p\"]\n[17.109385, \"o\", \"r\"]\n[17.189703, \"o\", \"o\"]\n[17.212934, \"o\", \"g\"]\n[17.24417, \"o\", \"r\"]\n[17.439442, \"o\", \"e\"]\n[17.456641, \"o\", \"s\"]\n[17.572948, \"o\", \"s\"]\n[17.636192, \"o\", \" \"]\n[17.692451, \"o\", \"h\"]\n[17.780737, \"o\", \"t\"]\n[17.789845, \"o\", \"t\"]\n[17.825103, \"o\", \"p\"]\n[17.876375, \"o\", \"s\"]\n[18.076633, \"o\", \":\"]\n[18.142884, \"o\", \"/\"]\n[18.152087, \"o\", \"/\"]\n[18.186329, \"o\", \"g\"]\n[18.235563, \"o\", \"i\"]\n[18.244785, \"o\", \"t\"]\n[18.253969, \"o\", \"h\"]\n[18.31823, \"o\", \"u\"]\n[18.327417, \"o\", \"b\"]\n[18.389743, \"o\", \".\"]\n[18.431976, \"o\", \"c\"]\n[18.50525, \"o\", \"o\"]\n[18.514446, \"o\", \"m\"]\n[18.536746, \"o\", \"/\"]\n[18.54586, \"o\", \"b\"]\n[18.578085, \"o\", \"o\"]\n[18.638338, \"o\", \"r\"]\n[18.708609, \"o\", \"g\"]\n[18.735857, \"o\", \"b\"]\n[18.770084, \"o\", \"a\"]\n[18.779288, \"o\", \"c\"]\n[18.834558, \"o\", \"k\"]\n[18.84879, \"o\", \"u\"]\n[18.859012, \"o\", \"p\"]\n[18.944287, \"o\", \"/\"]\n[18.978548, \"o\", \"b\"]\n[19.079789, \"o\", \"o\"]\n[19.105012, \"o\", \"r\"]\n[19.148268, \"o\", \"g\"]\n[19.164519, \"o\", \"/\"]\n[19.249746, \"o\", \"r\"]\n[19.271014, \"o\", \"e\"]\n[19.37926, \"o\", \"l\"]\n[19.438524, \"o\", \"e\"]\n[19.530793, \"o\", \"a\"]\n[19.546996, \"o\", \"s\"]\n[19.64026, \"o\", \"e\"]\n[19.70851, \"o\", \"s\"]\n[19.860819, \"o\", \"/\"]\n[19.931099, \"o\", \"d\"]\n[20.12236, \"o\", \"o\"]\n[20.131597, \"o\", \"w\"]\n[20.168902, \"o\", \"n\"]\n[20.178072, \"o\", \"l\"]\n[20.19734, \"o\", \"o\"]\n[20.255574, \"o\", \"a\"]\n[20.278822, \"o\", \"d\"]\n[20.441069, \"o\", \"/\"]\n[20.453262, \"o\", \"1\"]\n[20.613542, \"o\", \".\"]\n[20.631798, \"o\", \"2\"]\n[20.643995, \"o\", \".\"]\n[20.657161, \"o\", \"1\"]\n[20.696419, \"o\", \"/\"]\n[20.714662, \"o\", \"b\"]\n[20.797846, \"o\", \"o\"]\n[20.84203, \"o\", \"r\"]\n[20.988304, \"o\", \"g\"]\n[21.012612, \"o\", \"-\"]\n[21.021913, \"o\", \"l\"]\n[21.031187, \"o\", \"i\"]\n[21.111465, \"o\", \"n\"]\n[21.147724, \"o\", \"u\"]\n[21.217034, \"o\", \"x\"]\n[21.25131, \"o\", \"6\"]\n[21.260514, \"o\", \"4\"]\n[21.425711, \"o\", \"\\r\\n\"]\n[22.122156, \"o\", \"\\rborg-linux64          0%[                    ]       0  --.-KB/s               \"]\n[22.325471, \"o\", \"\\rborg-linux64          6%[>                   ]   1.33M  6.59MB/s               \"]\n[22.526514, \"o\", \"\\rborg-linux64         13%[=>                  ]   3.04M  7.52MB/s               \"]\n[22.728037, \"o\", \"\\rborg-linux64         22%[===>                ]   4.88M  8.06MB/s               \"]\n[22.928693, \"o\", \"\\rborg-linux64         30%[=====>              ]   6.69M  8.30MB/s               \"]\n[23.130437, \"o\", \"\\rborg-linux64         38%[======>             ]   8.35M  8.28MB/s               \"]\n[23.331937, \"o\", \"\\rborg-linux64         45%[========>           ]  10.01M  8.27MB/s               \"]\n[23.589691, \"o\", \"\\rborg-linux64         53%[=========>          ]  11.65M  7.95MB/s               \"]\n[23.789016, \"o\", \"\\rborg-linux64         64%[===========>        ]  14.08M  8.45MB/s               \"]\n[23.989182, \"o\", \"\\rborg-linux64         72%[=============>      ]  15.90M  8.52MB/s               \"]\n[24.189963, \"o\", \"\\rborg-linux64         81%[===============>    ]  17.74M  8.58MB/s               \"]\n[24.391128, \"o\", \"\\rborg-linux64         90%[=================>  ]  19.71M  8.69MB/s               \"]\n[24.591843, \"o\", \"\\rborg-linux64         99%[==================> ]  21.73M  8.80MB/s               \"]\n[24.605717, \"o\", \"\\rborg-linux64        100%[===================>]  21.87M  8.80MB/s    in 2.5s    \\r\\r\\n\"]\n[24.607438, \"o\", \"$ #\"]\n[24.711803, \"o\", \" \"]\n[24.756021, \"o\", \"a\"]\n[24.857266, \"o\", \"n\"]\n[24.95354, \"o\", \"d\"]\n[25.079747, \"o\", \" \"]\n[25.105032, \"o\", \"d\"]\n[25.173253, \"o\", \"o\"]\n[25.342493, \"o\", \" \"]\n[25.437792, \"o\", \"n\"]\n[25.545976, \"o\", \"o\"]\n[25.57012, \"o\", \"t\"]\n[25.750321, \"o\", \" \"]\n[25.780607, \"o\", \"f\"]\n[25.839866, \"o\", \"o\"]\n[25.989118, \"o\", \"r\"]\n[26.047359, \"o\", \"g\"]\n[26.125925, \"o\", \"e\"]\n[26.13532, \"o\", \"t\"]\n[26.205567, \"o\", \" \"]\n[26.229863, \"o\", \"t\"]\n[26.297164, \"o\", \"h\"]\n[26.330399, \"o\", \"e\"]\n[26.369653, \"o\", \" \"]\n[26.41591, \"o\", \"G\"]\n[26.42509, \"o\", \"P\"]\n[26.472358, \"o\", \"G\"]\n[26.485557, \"o\", \" \"]\n[26.525908, \"o\", \"s\"]\n[26.594062, \"o\", \"i\"]\n[26.730283, \"o\", \"g\"]\n[26.785586, \"o\", \"n\"]\n[26.809996, \"o\", \"a\"]\n[26.827218, \"o\", \"t\"]\n[26.865434, \"o\", \"u\"]\n[26.874623, \"o\", \"r\"]\n[26.949922, \"o\", \"e\"]\n[27.071147, \"o\", \"…\"]\n[27.108423, \"o\", \"!\"]\n[27.13146, \"o\", \"\\r\\n\"]\n[27.138689, \"o\", \"$ w\"]\n[27.164997, \"o\", \"g\"]\n[27.235295, \"o\", \"e\"]\n[27.24448, \"o\", \"t\"]\n[27.44489, \"o\", \" \"]\n[27.456982, \"o\", \"-\"]\n[27.466218, \"o\", \"q\"]\n[27.548499, \"o\", \" \"]\n[27.557754, \"o\", \"-\"]\n[27.676013, \"o\", \"-\"]\n[27.765225, \"o\", \"s\"]\n[27.774412, \"o\", \"h\"]\n[27.82672, \"o\", \"o\"]\n[27.835881, \"o\", \"w\"]\n[27.903196, \"o\", \"-\"]\n[27.912432, \"o\", \"p\"]\n[27.921642, \"o\", \"r\"]\n[27.930901, \"o\", \"o\"]\n[27.940177, \"o\", \"g\"]\n[28.000419, \"o\", \"r\"]\n[28.049667, \"o\", \"e\"]\n[28.058871, \"o\", \"s\"]\n[28.068098, \"o\", \"s\"]\n[28.077311, \"o\", \" \"]\n[28.086537, \"o\", \"h\"]\n[28.116886, \"o\", \"t\"]\n[28.125965, \"o\", \"t\"]\n[28.262193, \"o\", \"p\"]\n[28.271401, \"o\", \"s\"]\n[28.354675, \"o\", \":\"]\n[28.371903, \"o\", \"/\"]\n[28.4042, \"o\", \"/\"]\n[28.473444, \"o\", \"g\"]\n[28.482611, \"o\", \"i\"]\n[28.533931, \"o\", \"t\"]\n[28.581202, \"o\", \"h\"]\n[28.596485, \"o\", \"u\"]\n[28.633743, \"o\", \"b\"]\n[28.833992, \"o\", \".\"]\n[28.84319, \"o\", \"c\"]\n[28.854415, \"o\", \"o\"]\n[28.893704, \"o\", \"m\"]\n[28.917922, \"o\", \"/\"]\n[28.966099, \"o\", \"b\"]\n[29.016343, \"o\", \"o\"]\n[29.048615, \"o\", \"r\"]\n[29.099864, \"o\", \"g\"]\n[29.176115, \"o\", \"b\"]\n[29.198358, \"o\", \"a\"]\n[29.239604, \"o\", \"c\"]\n[29.253897, \"o\", \"k\"]\n[29.454082, \"o\", \"u\"]\n[29.507152, \"o\", \"p\"]\n[29.525347, \"o\", \"/\"]\n[29.599604, \"o\", \"b\"]\n[29.682859, \"o\", \"o\"]\n[29.731069, \"o\", \"r\"]\n[29.774349, \"o\", \"g\"]\n[29.846616, \"o\", \"/\"]\n[29.885768, \"o\", \"r\"]\n[30.007016, \"o\", \"e\"]\n[30.091268, \"o\", \"l\"]\n[30.170547, \"o\", \"e\"]\n[30.224826, \"o\", \"a\"]\n[30.327058, \"o\", \"s\"]\n[30.338255, \"o\", \"e\"]\n[30.347493, \"o\", \"s\"]\n[30.362751, \"o\", \"/\"]\n[30.428961, \"o\", \"d\"]\n[30.629286, \"o\", \"o\"]\n[30.640516, \"o\", \"w\"]\n[30.700784, \"o\", \"n\"]\n[30.740021, \"o\", \"l\"]\n[30.75024, \"o\", \"o\"]\n[30.769489, \"o\", \"a\"]\n[30.920759, \"o\", \"d\"]\n[30.978979, \"o\", \"/\"]\n[31.143241, \"o\", \"1\"]\n[31.213513, \"o\", \".\"]\n[31.294751, \"o\", \"2\"]\n[31.369956, \"o\", \".\"]\n[31.441199, \"o\", \"1\"]\n[31.522445, \"o\", \"/\"]\n[31.681726, \"o\", \"b\"]\n[31.7419, \"o\", \"o\"]\n[31.762117, \"o\", \"r\"]\n[31.800363, \"o\", \"g\"]\n[31.809617, \"o\", \"-\"]\n[31.93189, \"o\", \"l\"]\n[31.941089, \"o\", \"i\"]\n[31.991348, \"o\", \"n\"]\n[32.078615, \"o\", \"u\"]\n[32.181768, \"o\", \"x\"]\n[32.192943, \"o\", \"6\"]\n[32.207217, \"o\", \"4\"]\n[32.279486, \"o\", \".\"]\n[32.305741, \"o\", \"a\"]\n[32.324004, \"o\", \"s\"]\n[32.352294, \"o\", \"c\"]\n[32.553465, \"o\", \"\\r\\n\"]\n[33.113725, \"o\", \"\\rborg-linux64.asc      0%[                    ]       0  --.-KB/s               \"]\n[33.114471, \"o\", \"\\rborg-linux64.asc    100%[===================>]     862  --.-KB/s    in 0s      \"]\n[33.115443, \"o\", \"\\r\\r\\n\"]\n[33.116813, \"o\", \"$ \"]\n[33.116988, \"o\", \"\\r\\n\"]\n[33.121869, \"o\", \"$ #\"]\n[33.16912, \"o\", \" \"]\n[33.216381, \"o\", \"I\"]\n[33.238635, \"o\", \"n\"]\n[33.438905, \"o\", \" \"]\n[33.45917, \"o\", \"t\"]\n[33.540412, \"o\", \"h\"]\n[33.594643, \"o\", \"i\"]\n[33.684918, \"o\", \"s\"]\n[33.777142, \"o\", \" \"]\n[33.9464, \"o\", \"c\"]\n[33.968656, \"o\", \"a\"]\n[33.977801, \"o\", \"s\"]\n[34.107087, \"o\", \"e\"]\n[34.116346, \"o\", \",\"]\n[34.131568, \"o\", \" \"]\n[34.144888, \"o\", \"w\"]\n[34.15405, \"o\", \"e\"]\n[34.174323, \"o\", \" \"]\n[34.183524, \"o\", \"h\"]\n[34.223794, \"o\", \"a\"]\n[34.298019, \"o\", \"v\"]\n[34.359288, \"o\", \"e\"]\n[34.501551, \"o\", \" \"]\n[34.510743, \"o\", \"a\"]\n[34.523012, \"o\", \"l\"]\n[34.663265, \"o\", \"r\"]\n[34.68352, \"o\", \"e\"]\n[34.710755, \"o\", \"a\"]\n[34.759018, \"o\", \"d\"]\n[34.784347, \"o\", \"y\"]\n[34.957675, \"o\", \" \"]\n[34.966934, \"o\", \"i\"]\n[35.042172, \"o\", \"m\"]\n[35.052399, \"o\", \"p\"]\n[35.113748, \"o\", \"o\"]\n[35.123019, \"o\", \"r\"]\n[35.323308, \"o\", \"t\"]\n[35.346586, \"o\", \"e\"]\n[35.408868, \"o\", \"d\"]\n[35.453094, \"o\", \" \"]\n[35.46233, \"o\", \"t\"]\n[35.471566, \"o\", \"h\"]\n[35.488874, \"o\", \"e\"]\n[35.498087, \"o\", \" \"]\n[35.507337, \"o\", \"p\"]\n[35.548601, \"o\", \"u\"]\n[35.55776, \"o\", \"b\"]\n[35.566991, \"o\", \"l\"]\n[35.635262, \"o\", \"i\"]\n[35.644481, \"o\", \"c\"]\n[35.745742, \"o\", \" \"]\n[35.763015, \"o\", \"k\"]\n[35.845261, \"o\", \"e\"]\n[35.854518, \"o\", \"y\"]\n[35.893716, \"o\", \" \"]\n[35.973922, \"o\", \"o\"]\n[36.078141, \"o\", \"f\"]\n[36.13339, \"o\", \" \"]\n[36.145654, \"o\", \"a\"]\n[36.213852, \"o\", \" \"]\n[36.259106, \"o\", \"b\"]\n[36.268328, \"o\", \"o\"]\n[36.303574, \"o\", \"r\"]\n[36.326849, \"o\", \"g\"]\n[36.527126, \"o\", \" \"]\n[36.72738, \"o\", \"d\"]\n[36.741621, \"o\", \"e\"]\n[36.81091, \"o\", \"v\"]\n[36.833128, \"o\", \"e\"]\n[37.011383, \"o\", \"l\"]\n[37.020627, \"o\", \"o\"]\n[37.051895, \"o\", \"p\"]\n[37.139138, \"o\", \"e\"]\n[37.165375, \"o\", \"r\"]\n[37.196621, \"o\", \".\"]\n[37.387884, \"o\", \" \"]\n[37.455143, \"o\", \"S\"]\n[37.46434, \"o\", \"o\"]\n[37.638618, \"o\", \" \"]\n[37.671888, \"o\", \"w\"]\n[37.682073, \"o\", \"e\"]\n[37.744311, \"o\", \" \"]\n[37.783588, \"o\", \"o\"]\n[37.98384, \"o\", \"n\"]\n[38.009046, \"o\", \"l\"]\n[38.060303, \"o\", \"y\"]\n[38.216548, \"o\", \" \"]\n[38.231823, \"o\", \"n\"]\n[38.323064, \"o\", \"e\"]\n[38.398326, \"o\", \"e\"]\n[38.436586, \"o\", \"d\"]\n[38.493752, \"o\", \" \"]\n[38.581003, \"o\", \"t\"]\n[38.664281, \"o\", \"o\"]\n[38.751561, \"o\", \" \"]\n[38.79584, \"o\", \"v\"]\n[38.87505, \"o\", \"e\"]\n[38.92131, \"o\", \"r\"]\n[38.930503, \"o\", \"i\"]\n[39.0568, \"o\", \"f\"]\n[39.077954, \"o\", \"y\"]\n[39.103209, \"o\", \" \"]\n[39.112401, \"o\", \"i\"]\n[39.121714, \"o\", \"t\"]\n[39.173003, \"o\", \":\"]\n[39.184854, \"o\", \"\\r\\n\"]\n[39.190079, \"o\", \"$ g\"]\n[39.199322, \"o\", \"p\"]\n[39.215605, \"o\", \"g\"]\n[39.415869, \"o\", \" \"]\n[39.441075, \"o\", \"-\"]\n[39.472334, \"o\", \"-\"]\n[39.483535, \"o\", \"v\"]\n[39.546835, \"o\", \"e\"]\n[39.560054, \"o\", \"r\"]\n[39.583316, \"o\", \"i\"]\n[39.605629, \"o\", \"f\"]\n[39.683888, \"o\", \"y\"]\n[39.738098, \"o\", \" \"]\n[39.749313, \"o\", \"b\"]\n[39.791608, \"o\", \"o\"]\n[39.951853, \"o\", \"r\"]\n[40.003141, \"o\", \"g\"]\n[40.021369, \"o\", \"-\"]\n[40.06064, \"o\", \"l\"]\n[40.260916, \"o\", \"i\"]\n[40.298124, \"o\", \"n\"]\n[40.318369, \"o\", \"u\"]\n[40.363631, \"o\", \"x\"]\n[40.41488, \"o\", \"6\"]\n[40.424082, \"o\", \"4\"]\n[40.624368, \"o\", \".\"]\n[40.674613, \"o\", \"a\"]\n[40.684742, \"o\", \"s\"]\n[40.693927, \"o\", \"c\"]\n[40.860898, \"o\", \"\\r\\n\"]\n[40.870862, \"o\", \"gpg: assuming signed data in 'borg-linux64'\\r\\r\\n\"]\n[40.991536, \"o\", \"gpg: Signature made Sun Jun  5 21:37:49 2022 UTC\\r\\r\\ngpg:                using RSA key 2F81AFFBAB04E11FE8EE65D4243ACFA951F78E01\\r\\r\\ngpg:                issuer \\\"tw@waldmann-edv.de\\\"\\r\\r\\n\"]\n[40.993561, \"o\", \"gpg: Good signature from \\\"Thomas Waldmann <tw@waldmann-edv.de>\\\" [unknown]\\r\\r\\ngpg:                 aka \\\"Thomas Waldmann <thomas.j.waldmann@gmail.com>\\\" [unknown]\\r\\r\\ngpg:                 aka \\\"Thomas Waldmann <tw-public@gmx.de>\\\" [unknown]\\r\\r\\n\"]\n[40.993881, \"o\", \"gpg:                 aka \\\"Thomas Waldmann <twaldmann@thinkmo.de>\\\" [unknown]\\r\\r\\n\"]\n[40.995059, \"o\", \"gpg: WARNING: This key is not certified with a trusted signature!\\r\\r\\ngpg:          There is no indication that the signature belongs to the owner.\\r\\r\\nPrimary key fingerprint: 6D5B EF9A DD20 7580 5747  B70F 9F88 FB52 FAF7 B393\\r\\r\\n     Subkey fingerprint: 2F81 AFFB AB04 E11F E8EE  65D4 243A CFA9 51F7 8E01\\r\\r\\n\"]\n[40.995876, \"o\", \"$ #\"]\n[41.005075, \"o\", \" \"]\n[41.069956, \"o\", \"O\"]\n[41.098443, \"o\", \"k\"]\n[41.17776, \"o\", \"a\"]\n[41.292576, \"o\", \"y\"]\n[41.493851, \"o\", \",\"]\n[41.569465, \"o\", \" \"]\n[41.617829, \"o\", \"t\"]\n[41.738039, \"o\", \"h\"]\n[41.747542, \"o\", \"e\"]\n[41.94792, \"o\", \" \"]\n[41.979034, \"o\", \"b\"]\n[42.021223, \"o\", \"i\"]\n[42.1784, \"o\", \"n\"]\n[42.262162, \"o\", \"a\"]\n[42.281862, \"o\", \"r\"]\n[42.327452, \"o\", \"y\"]\n[42.465774, \"o\", \" \"]\n[42.559207, \"o\", \"i\"]\n[42.581862, \"o\", \"s\"]\n[42.614201, \"o\", \" \"]\n[42.624612, \"o\", \"v\"]\n[42.782706, \"o\", \"a\"]\n[42.841839, \"o\", \"l\"]\n[42.853148, \"o\", \"i\"]\n[42.909309, \"o\", \"d\"]\n[42.949832, \"o\", \"!\"]\n[42.960042, \"o\", \"\\r\\n\"]\n[42.966483, \"o\", \"$ \\r\\n\"]\n[42.970541, \"o\", \"$ #\"]\n[43.010934, \"o\", \" \"]\n[43.061848, \"o\", \"N\"]\n[43.113854, \"o\", \"o\"]\n[43.314271, \"o\", \"w\"]\n[43.367734, \"o\", \" \"]\n[43.38522, \"o\", \"i\"]\n[43.440638, \"o\", \"n\"]\n[43.453794, \"o\", \"s\"]\n[43.58115, \"o\", \"t\"]\n[43.602848, \"o\", \"a\"]\n[43.612287, \"o\", \"l\"]\n[43.66838, \"o\", \"l\"]\n[43.757865, \"o\", \" \"]\n[43.816951, \"o\", \"i\"]\n[43.867188, \"o\", \"t\"]\n[43.918628, \"o\", \":\"]\n[44.119535, \"o\", \"\\r\\n\"]\n[44.126514, \"o\", \"$ s\"]\n[44.137836, \"o\", \"u\"]\n[44.230129, \"o\", \"d\"]\n[44.252649, \"o\", \"o\"]\n[44.277864, \"o\", \" \"]\n[44.357858, \"o\", \"c\"]\n[44.37639, \"o\", \"p\"]\n[44.483759, \"o\", \" \"]\n[44.558149, \"o\", \"b\"]\n[44.629854, \"o\", \"o\"]\n[44.701844, \"o\", \"r\"]\n[44.763291, \"o\", \"g\"]\n[44.845639, \"o\", \"-\"]\n[44.900115, \"o\", \"l\"]\n[44.909474, \"o\", \"i\"]\n[45.058144, \"o\", \"n\"]\n[45.067669, \"o\", \"u\"]\n[45.077063, \"o\", \"x\"]\n[45.09981, \"o\", \"6\"]\n[45.122149, \"o\", \"4\"]\n[45.183708, \"o\", \" \"]\n[45.205868, \"o\", \"/\"]\n[45.222081, \"o\", \"u\"]\n[45.265325, \"o\", \"s\"]\n[45.297806, \"o\", \"r\"]\n[45.429847, \"o\", \"/\"]\n[45.439219, \"o\", \"l\"]\n[45.448719, \"o\", \"o\"]\n[45.473872, \"o\", \"c\"]\n[45.494395, \"o\", \"a\"]\n[45.609828, \"o\", \"l\"]\n[45.654127, \"o\", \"/\"]\n[45.688425, \"o\", \"b\"]\n[45.72362, \"o\", \"i\"]\n[45.787042, \"o\", \"n\"]\n[45.849829, \"o\", \"/\"]\n[45.859297, \"o\", \"b\"]\n[45.939606, \"o\", \"o\"]\n[45.949095, \"o\", \"r\"]\n[46.042557, \"o\", \"g\"]\n[46.12613, \"o\", \"\\r\\n\"]\n[46.172357, \"o\", \"$ s\"]\n[46.212876, \"o\", \"u\"]\n[46.325809, \"o\", \"d\"]\n[46.344195, \"o\", \"o\"]\n[46.431558, \"o\", \" \"]\n[46.522936, \"o\", \"c\"]\n[46.546729, \"o\", \"h\"]\n[46.570006, \"o\", \"o\"]\n[46.7523, \"o\", \"w\"]\n[46.819922, \"o\", \"n\"]\n[46.911633, \"o\", \" \"]\n[46.929206, \"o\", \"r\"]\n[47.008063, \"o\", \"o\"]\n[47.157866, \"o\", \"o\"]\n[47.225417, \"o\", \"t\"]\n[47.301865, \"o\", \":\"]\n[47.31111, \"o\", \"r\"]\n[47.362558, \"o\", \"o\"]\n[47.41601, \"o\", \"o\"]\n[47.446448, \"o\", \"t\"]\n[47.482538, \"o\", \" \"]\n[47.560369, \"o\", \"/\"]\n[47.623683, \"o\", \"u\"]\n[47.708182, \"o\", \"s\"]\n[47.743531, \"o\", \"r\"]\n[47.764228, \"o\", \"/\"]\n[47.872949, \"o\", \"l\"]\n[47.882292, \"o\", \"o\"]\n[47.891809, \"o\", \"c\"]\n[47.926001, \"o\", \"a\"]\n[47.952744, \"o\", \"l\"]\n[48.082119, \"o\", \"/\"]\n[48.168883, \"o\", \"b\"]\n[48.181753, \"o\", \"i\"]\n[48.191226, \"o\", \"n\"]\n[48.242791, \"o\", \"/\"]\n[48.298909, \"o\", \"b\"]\n[48.325598, \"o\", \"o\"]\n[48.364784, \"o\", \"r\"]\n[48.41717, \"o\", \"g\"]\n[48.618559, \"o\", \"\\r\\n\"]\n[48.636317, \"o\", \"$ #\"]\n[48.699577, \"o\", \" \"]\n[48.761734, \"o\", \"a\"]\n[48.860994, \"o\", \"n\"]\n[48.891258, \"o\", \"d\"]\n[48.90051, \"o\", \" \"]\n[49.089716, \"o\", \"m\"]\n[49.191956, \"o\", \"a\"]\n[49.220201, \"o\", \"k\"]\n[49.231415, \"o\", \"e\"]\n[49.250657, \"o\", \" \"]\n[49.262875, \"o\", \"i\"]\n[49.389135, \"o\", \"t\"]\n[49.58942, \"o\", \" \"]\n[49.623673, \"o\", \"e\"]\n[49.726909, \"o\", \"x\"]\n[49.778141, \"o\", \"e\"]\n[49.820374, \"o\", \"c\"]\n[49.883634, \"o\", \"u\"]\n[49.89287, \"o\", \"t\"]\n[49.96108, \"o\", \"a\"]\n[49.99032, \"o\", \"b\"]\n[50.164588, \"o\", \"l\"]\n[50.248871, \"o\", \"e\"]\n[50.315101, \"o\", \"…\"]\n[50.324917, \"o\", \"\\r\\n\"]\n[50.33024, \"o\", \"$ s\"]\n[50.444521, \"o\", \"u\"]\n[50.474784, \"o\", \"d\"]\n[50.600042, \"o\", \"o\"]\n[50.771296, \"o\", \" \"]\n[50.794535, \"o\", \"c\"]\n[50.845756, \"o\", \"h\"]\n[50.864021, \"o\", \"m\"]\n[50.873172, \"o\", \"o\"]\n[50.974454, \"o\", \"d\"]\n[50.983643, \"o\", \" \"]\n[51.052941, \"o\", \"7\"]\n[51.120224, \"o\", \"5\"]\n[51.15648, \"o\", \"5\"]\n[51.357071, \"o\", \" \"]\n[51.41431, \"o\", \"/\"]\n[51.481616, \"o\", \"u\"]\n[51.532876, \"o\", \"s\"]\n[51.562085, \"o\", \"r\"]\n[51.588324, \"o\", \"/\"]\n[51.604621, \"o\", \"l\"]\n[51.613907, \"o\", \"o\"]\n[51.656228, \"o\", \"c\"]\n[51.665393, \"o\", \"a\"]\n[51.67459, \"o\", \"l\"]\n[51.683892, \"o\", \"/\"]\n[51.696188, \"o\", \"b\"]\n[51.764495, \"o\", \"i\"]\n[51.820817, \"o\", \"n\"]\n[51.945194, \"o\", \"/\"]\n[52.055405, \"o\", \"b\"]\n[52.195699, \"o\", \"o\"]\n[52.218896, \"o\", \"r\"]\n[52.228068, \"o\", \"g\"]\n[52.424062, \"o\", \"\\r\\n\"]\n[52.440156, \"o\", \"$ \\r\\n\"]\n[52.442959, \"o\", \"$ #\"]\n[52.45844, \"o\", \" \"]\n[52.473717, \"o\", \"N\"]\n[52.508037, \"o\", \"o\"]\n[52.51717, \"o\", \"w\"]\n[52.56058, \"o\", \" \"]\n[52.574808, \"o\", \"c\"]\n[52.625984, \"o\", \"h\"]\n[52.728251, \"o\", \"e\"]\n[52.906625, \"o\", \"c\"]\n[52.926856, \"o\", \"k\"]\n[52.974077, \"o\", \" \"]\n[52.9983, \"o\", \"i\"]\n[53.037619, \"o\", \"t\"]\n[53.085974, \"o\", \":\"]\n[53.141237, \"o\", \" \"]\n[53.167627, \"o\", \"(\"]\n[53.231899, \"o\", \"p\"]\n[53.261185, \"o\", \"o\"]\n[53.287514, \"o\", \"s\"]\n[53.371779, \"o\", \"s\"]\n[53.427951, \"o\", \"i\"]\n[53.437239, \"o\", \"b\"]\n[53.470586, \"o\", \"l\"]\n[53.494825, \"o\", \"y\"]\n[53.665141, \"o\", \" \"]\n[53.693108, \"o\", \"n\"]\n[53.765334, \"o\", \"e\"]\n[53.902605, \"o\", \"e\"]\n[53.911802, \"o\", \"d\"]\n[53.977067, \"o\", \"s\"]\n[53.998324, \"o\", \" \"]\n[54.017644, \"o\", \"a\"]\n[54.026845, \"o\", \" \"]\n[54.058086, \"o\", \"t\"]\n[54.071336, \"o\", \"e\"]\n[54.080542, \"o\", \"r\"]\n[54.112803, \"o\", \"m\"]\n[54.218011, \"o\", \"i\"]\n[54.291263, \"o\", \"n\"]\n[54.321518, \"o\", \"a\"]\n[54.513747, \"o\", \"l\"]\n[54.529947, \"o\", \" \"]\n[54.621171, \"o\", \"r\"]\n[54.65843, \"o\", \"e\"]\n[54.675688, \"o\", \"s\"]\n[54.69193, \"o\", \"t\"]\n[54.752183, \"o\", \"a\"]\n[54.761376, \"o\", \"r\"]\n[54.826641, \"o\", \"t\"]\n[54.837793, \"o\", \")\"]\n[54.929883, \"o\", \"\\r\\n\"]\n[54.936116, \"o\", \"$ b\"]\n[54.945364, \"o\", \"o\"]\n[54.954567, \"o\", \"r\"]\n[54.995831, \"o\", \"g\"]\n[55.094044, \"o\", \" \"]\n[55.1113, \"o\", \"-\"]\n[55.120527, \"o\", \"V\"]\n[55.161722, \"o\", \"\\r\\n\"]\n[55.941702, \"o\", \"borg 1.2.1\"]\n[55.942305, \"o\", \"\\r\\r\\n\"]\n[55.993719, \"o\", \"$ \\r\\n\"]\n[55.997213, \"o\", \"$ #\"]\n[56.034666, \"o\", \" \"]\n[56.063952, \"o\", \"T\"]\n[56.110197, \"o\", \"h\"]\n[56.182455, \"o\", \"a\"]\n[56.23975, \"o\", \"t\"]\n[56.309047, \"o\", \"'\"]\n[56.32334, \"o\", \"s\"]\n[56.405649, \"o\", \" \"]\n[56.420997, \"o\", \"i\"]\n[56.452158, \"o\", \"t\"]\n[56.489524, \"o\", \"!\"]\n[56.499708, \"o\", \" \"]\n[56.517909, \"o\", \"C\"]\n[56.564198, \"o\", \"h\"]\n[56.597539, \"o\", \"e\"]\n[56.619709, \"o\", \"c\"]\n[56.63787, \"o\", \"k\"]\n[56.67012, \"o\", \" \"]\n[56.679362, \"o\", \"o\"]\n[56.689617, \"o\", \"u\"]\n[56.79894, \"o\", \"t\"]\n[56.808157, \"o\", \" \"]\n[56.820469, \"o\", \"t\"]\n[57.020769, \"o\", \"h\"]\n[57.067066, \"o\", \"e\"]\n[57.164398, \"o\", \" \"]\n[57.180648, \"o\", \"o\"]\n[57.211946, \"o\", \"t\"]\n[57.370177, \"o\", \"h\"]\n[57.396486, \"o\", \"e\"]\n[57.405783, \"o\", \"r\"]\n[57.435073, \"o\", \" \"]\n[57.484351, \"o\", \"s\"]\n[57.513656, \"o\", \"c\"]\n[57.591941, \"o\", \"r\"]\n[57.610164, \"o\", \"e\"]\n[57.754423, \"o\", \"e\"]\n[57.76372, \"o\", \"n\"]\n[57.815018, \"o\", \"c\"]\n[58.015317, \"o\", \"a\"]\n[58.050594, \"o\", \"s\"]\n[58.2029, \"o\", \"t\"]\n[58.212147, \"o\", \"s\"]\n[58.300444, \"o\", \" \"]\n[58.340736, \"o\", \"t\"]\n[58.394987, \"o\", \"o\"]\n[58.595282, \"o\", \" \"]\n[58.693538, \"o\", \"s\"]\n[58.755821, \"o\", \"e\"]\n[58.838122, \"o\", \"e\"]\n[58.847379, \"o\", \" \"]\n[58.892674, \"o\", \"h\"]\n[58.986978, \"o\", \"o\"]\n[58.996253, \"o\", \"w\"]\n[59.084591, \"o\", \" \"]\n[59.096866, \"o\", \"t\"]\n[59.124943, \"o\", \"o\"]\n[59.226381, \"o\", \" \"]\n[59.256843, \"o\", \"a\"]\n[59.26637, \"o\", \"c\"]\n[59.350895, \"o\", \"t\"]\n[59.388513, \"o\", \"u\"]\n[59.399086, \"o\", \"a\"]\n[59.42835, \"o\", \"l\"]\n[59.495588, \"o\", \"l\"]\n[59.695833, \"o\", \"y\"]\n[59.837075, \"o\", \" \"]\n[59.867315, \"o\", \"u\"]\n[59.945609, \"o\", \"s\"]\n[59.954833, \"o\", \"e\"]\n[60.01306, \"o\", \" \"]\n[60.030314, \"o\", \"b\"]\n[60.039559, \"o\", \"o\"]\n[60.086806, \"o\", \"r\"]\n[60.14006, \"o\", \"g\"]\n[60.169338, \"o\", \"b\"]\n[60.178534, \"o\", \"a\"]\n[60.201718, \"o\", \"c\"]\n[60.210988, \"o\", \"k\"]\n[60.28122, \"o\", \"u\"]\n[60.43448, \"o\", \"p\"]\n[60.447748, \"o\", \".\"]\n[60.469955, \"o\", \"\\r\\n\"]\n"
  },
  {
    "path": "docs/misc/asciinema/install.tcl",
    "content": "# Configuration for send -h\n# Tries to emulate a human typing\n# Tweak this if typing is too fast or too slow\nset send_human {.05 .1 1 .01 .2}\n\nset script [string trim {\n# This asciinema will show you the installation of borg as a standalone binary. Usually you only need this if you want to have an up-to-date version of borg or no package is available for your distro/OS.\n\n# First, we need to download the version, we'd like to install…\nwget -q --show-progress https://github.com/borgbackup/borg/releases/download/1.2.1/borg-linux64\n# and do not forget the GPG signature…!\nwget -q --show-progress https://github.com/borgbackup/borg/releases/download/1.2.1/borg-linux64.asc\n\n# In this case, we have already imported the public key of a borg developer. So we only need to verify it:\ngpg --verify borg-linux64.asc\n# Okay, the binary is valid!\n\n# Now install it:\nsudo cp borg-linux64 /usr/local/bin/borg\nsudo chown root:root /usr/local/bin/borg\n# and make it executable…\nsudo chmod 755 /usr/local/bin/borg\n\n# Now check it: (possibly needs a terminal restart)\nborg -V\n\n# That's it! Now check out the other screencasts to see how to use borgbackup.\n}]\n\n# wget may be slow\nset timeout -1\n\nforeach line [split $script \\n] {\n\tsend_user \"$ \"\n\tsend_user -h $line\\n\n\tspawn -noecho /bin/sh -c $line\n\texpect eof\n}\n"
  },
  {
    "path": "docs/misc/asciinema/sample-wallpapers.txt",
    "content": "https://upload.wikimedia.org/wikipedia/commons/2/22/Pseudo_kleinian_001_OpenCL_45154214_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/d/da/Mengerschwamm_Iteration_5_x_Mandelbulb_OpenCL_528814521414_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/e/eb/Blixos_logon_screen.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/9/90/Great_smokey_mountains_national_park_with_woman_sitting_under_tree_in_foreground.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/d/d2/Mengerschwamm_x_Generalized_Fold_Box_OpenCL_18915424_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/3/3d/Red_interesting_background.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/4/43/KIFS_OpenCL_54815_5K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/a/a1/ProjectStealth.png\nhttps://upload.wikimedia.org/wikipedia/commons/8/8d/KIFS_OpenCL_5434735835_5K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/d/db/Harvett_Fox_-_Wallpaper_%2816x9_ratio%2C_without_character_logo%2C_transparent_variant%29_%28vector_version%29.svg\nhttps://upload.wikimedia.org/wikipedia/commons/7/7f/Generalized_Fold_Box_OpenCL_4258952414_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/5/58/Mandelbox_Vary_Scale_4D_OpenCL_9648145412_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/6/62/Trapper_cabin.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/f/fd/Openarch.png\nhttps://upload.wikimedia.org/wikipedia/commons/a/a5/Mandelbox_-_Variable_8K_6595424.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Mengerschwamm_Iteration_6_x_Generalized_Fold_Box_OpenCL_14048152404910_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/c/cf/Sierp_Oktaeder_x_Menger_4D_OpenCL_51241841541_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/5/59/Airbus_Wing_01798_changed.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/8/8a/Holytrinfruitlandpark1b.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/5/5c/Abox_-_Mod_12_OpenCL_45184521485_5K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/d/d2/Menger_4D_x_Quaternion_OpenCL_644289452_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/3/3e/Gabrielsond.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/8/80/Mix_Pinski_4D_x_Mengerschwamm_OpenCL_461481542_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/8/84/Belinda_Vixen_-_Wallpaper_%28without_character_wordmark_and_hair_variant%29_%2816x9_ratio%29.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/0/00/Sierp_Oktaeder_Iteration_7_x_Menger_4D_OpenCL_2154188450481_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/2/24/Abox_4D_OpenCL_545185481_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/0/08/Sierpinski_4D_OpenCL_485274854_5K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/0/09/Vereinigung_Sierpinski_4D_und_Mengerschwamm_OpenCl_6184524.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/a/ae/Mengerschwamm_OpenCL_955141845_8K.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/6/64/Free_high-resolution_pictures_you_can_use_on_your_personal_and_commercial_projects._%2814168975789%29.jpg\n"
  },
  {
    "path": "docs/misc/benchmark-crud.txt",
    "content": "borg benchmark crud\n===================\n\nHere is an example of borg benchmark crud output.\n\nI ran it on my laptop: Core i5-4200U, 8 GB RAM, SATA SSD, Linux, ext4 filesystem.\n\"src\" as well as repo is local, on this SSD.\n\n$ BORG_PASSPHRASE=secret borg init --encryption repokey-blake2 repo\n$ BORG_PASSPHRASE=secret borg benchmark crud repo src\n\nC-Z-BIG         116.06 MB/s (10 * 100.00 MB all-zero files: 8.62s)\nR-Z-BIG         197.00 MB/s (10 * 100.00 MB all-zero files: 5.08s)\nU-Z-BIG         418.07 MB/s (10 * 100.00 MB all-zero files: 2.39s)\nD-Z-BIG         724.94 MB/s (10 * 100.00 MB all-zero files: 1.38s)\nC-R-BIG          42.21 MB/s (10 * 100.00 MB random files: 23.69s)\nR-R-BIG         134.45 MB/s (10 * 100.00 MB random files: 7.44s)\nU-R-BIG         316.83 MB/s (10 * 100.00 MB random files: 3.16s)\nD-R-BIG         251.10 MB/s (10 * 100.00 MB random files: 3.98s)\nC-Z-MEDIUM      118.53 MB/s (1000 * 1.00 MB all-zero files: 8.44s)\nR-Z-MEDIUM      218.49 MB/s (1000 * 1.00 MB all-zero files: 4.58s)\nU-Z-MEDIUM      591.59 MB/s (1000 * 1.00 MB all-zero files: 1.69s)\nD-Z-MEDIUM      730.04 MB/s (1000 * 1.00 MB all-zero files: 1.37s)\nC-R-MEDIUM       31.46 MB/s (1000 * 1.00 MB random files: 31.79s)\nR-R-MEDIUM      129.64 MB/s (1000 * 1.00 MB random files: 7.71s)\nU-R-MEDIUM      621.86 MB/s (1000 * 1.00 MB random files: 1.61s)\nD-R-MEDIUM      234.82 MB/s (1000 * 1.00 MB random files: 4.26s)\nC-Z-SMALL        19.81 MB/s (10000 * 10.00 kB all-zero files: 5.05s)\nR-Z-SMALL        97.69 MB/s (10000 * 10.00 kB all-zero files: 1.02s)\nU-Z-SMALL        36.35 MB/s (10000 * 10.00 kB all-zero files: 2.75s)\nD-Z-SMALL        57.04 MB/s (10000 * 10.00 kB all-zero files: 1.75s)\nC-R-SMALL         9.81 MB/s (10000 * 10.00 kB random files: 10.19s)\nR-R-SMALL        92.21 MB/s (10000 * 10.00 kB random files: 1.08s)\nU-R-SMALL        64.62 MB/s (10000 * 10.00 kB random files: 1.55s)\nD-R-SMALL        51.62 MB/s (10000 * 10.00 kB random files: 1.94s)\n\n\nA second run some time later gave:\n\nC-Z-BIG         115.22 MB/s (10 * 100.00 MB all-zero files: 8.68s)\nR-Z-BIG         196.06 MB/s (10 * 100.00 MB all-zero files: 5.10s)\nU-Z-BIG         439.50 MB/s (10 * 100.00 MB all-zero files: 2.28s)\nD-Z-BIG         671.11 MB/s (10 * 100.00 MB all-zero files: 1.49s)\nC-R-BIG          43.40 MB/s (10 * 100.00 MB random files: 23.04s)\nR-R-BIG         133.17 MB/s (10 * 100.00 MB random files: 7.51s)\nU-R-BIG         464.50 MB/s (10 * 100.00 MB random files: 2.15s)\nD-R-BIG         245.19 MB/s (10 * 100.00 MB random files: 4.08s)\nC-Z-MEDIUM      110.82 MB/s (1000 * 1.00 MB all-zero files: 9.02s)\nR-Z-MEDIUM      217.96 MB/s (1000 * 1.00 MB all-zero files: 4.59s)\nU-Z-MEDIUM      601.54 MB/s (1000 * 1.00 MB all-zero files: 1.66s)\nD-Z-MEDIUM      686.99 MB/s (1000 * 1.00 MB all-zero files: 1.46s)\nC-R-MEDIUM       39.91 MB/s (1000 * 1.00 MB random files: 25.06s)\nR-R-MEDIUM      128.91 MB/s (1000 * 1.00 MB random files: 7.76s)\nU-R-MEDIUM      599.00 MB/s (1000 * 1.00 MB random files: 1.67s)\nD-R-MEDIUM      230.69 MB/s (1000 * 1.00 MB random files: 4.33s)\nC-Z-SMALL        14.78 MB/s (10000 * 10.00 kB all-zero files: 6.76s)\nR-Z-SMALL        96.86 MB/s (10000 * 10.00 kB all-zero files: 1.03s)\nU-Z-SMALL        35.22 MB/s (10000 * 10.00 kB all-zero files: 2.84s)\nD-Z-SMALL        64.93 MB/s (10000 * 10.00 kB all-zero files: 1.54s)\nC-R-SMALL        11.08 MB/s (10000 * 10.00 kB random files: 9.02s)\nR-R-SMALL        92.34 MB/s (10000 * 10.00 kB random files: 1.08s)\nU-R-SMALL        64.49 MB/s (10000 * 10.00 kB random files: 1.55s)\nD-R-SMALL        46.96 MB/s (10000 * 10.00 kB random files: 2.13s)\n\n"
  },
  {
    "path": "docs/misc/create_chunker-params.txt",
    "content": "About borg create --chunker-params\n==================================\n\n--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE\n\nCHUNK_MIN_EXP and CHUNK_MAX_EXP provide the exponent N for the 2^N minimum and\nmaximum chunk sizes. Required: CHUNK_MIN_EXP < CHUNK_MAX_EXP.\n\nDefaults: 19 (2^19 == 512KiB) minimum, 23 (2^23 == 8MiB) maximum.\nCurrently, specifying a maximum greater than 23 is not supported.\n\nHASH_MASK_BITS is the number of least-significant bits of the rolling hash\nthat need to be zero to trigger a chunk cut.\nRecommended: CHUNK_MIN_EXP + X <= HASH_MASK_BITS <= CHUNK_MAX_EXP - X, X >= 2\n(this allows the rolling hash some freedom to make its cut at a place\ndetermined by the window's contents rather than the minimum/maximum chunk size).\n\nDefault: 21 (statistically, chunks will be about 2^21 == 2MiB in size).\n\nHASH_WINDOW_SIZE: the size of the window used for the rolling hash computation.\nMust be an odd number. Default: 4095B.\n\n\nTrying it out\n=============\n\nI backed up a VM directory to demonstrate how different chunker parameters\naffect repository size, index size/chunk count, compression, and deduplication.\n\nrepo-sm: ~64 KiB chunks (16 bits chunk mask), min chunk size 1 KiB (2^10B)\n         (these are Attic/Borg 0.23 internal defaults)\n\nrepo-lg: ~1MiB chunks (20 bits chunk mask), min chunk size 64 KiB (2^16B)\n\nrepo-xl: 8MiB chunks (2^23B max chunk size), min chunk size 64 KiB (2^16B).\n         The chunk mask bits were set to 31, so it (almost) never triggers.\n         This degrades the rolling hash based dedup to a fixed-offset dedup\n         as the cutting point is now (almost) always the end of the buffer\n         (at 2^23B == 8MiB).\n\nThe repo index size is an indicator for the RAM needs of Borg.\nIn this special case, the total RAM needs are about 2.1x the repo index size.\nYou see the index size of repo-sm is 16x larger than that of repo-lg, which corresponds\nto the ratio of the different target chunk sizes.\n\nNote: RAM needs were not a problem in this specific case (37GB data size).\n      But imagine you have 37TB of such data and much less than 42GB RAM,\n      then you should use the \"lg\" chunker parameters so you need only\n      2.6GB RAM. Or even larger chunks than shown for \"lg\" (see \"xl\").\n\nYou also see compression works better for larger chunks, as expected.\nDeduplication works worse for larger chunks, also as expected.\n\nsmall chunks\n============\n\n$ borg info /extra/repo-sm::1\n\nCommand line: /home/tw/w/borg-env/bin/borg create --chunker-params 10,23,16,4095 /extra/repo-sm::1 /home/tw/win\nNumber of files: 3\n\n                       Original size      Compressed size    Deduplicated size\nThis archive:               37.12 GB             14.81 GB             12.18 GB\nAll archives:               37.12 GB             14.81 GB             12.18 GB\n\n                       Unique chunks         Total chunks\nChunk index:                  378374               487316\n\n$ ls -l /extra/repo-sm/index*\n\n-rw-rw-r-- 1 tw tw 20971538 Jun 20 23:39 index.2308\n\n$ du -sk /extra/repo-sm\n11930840    /extra/repo-sm\n\nlarge chunks\n============\n\n$ borg info /extra/repo-lg::1\n\nCommand line: /home/tw/w/borg-env/bin/borg create --chunker-params 16,23,20,4095 /extra/repo-lg::1 /home/tw/win\nNumber of files: 3\n\n                       Original size      Compressed size    Deduplicated size\nThis archive:               37.10 GB             14.60 GB             13.38 GB\nAll archives:               37.10 GB             14.60 GB             13.38 GB\n\n                       Unique chunks         Total chunks\nChunk index:                   25889                29349\n\n$ ls -l /extra/repo-lg/index*\n\n-rw-rw-r-- 1 tw tw 1310738 Jun 20 23:10 index.2264\n\n$ du -sk /extra/repo-lg\n13073928    /extra/repo-lg\n\nxl chunks\n=========\n\n(borg-env)tw@tux:~/w/borg$ borg info /extra/repo-xl::1\nCommand line: /home/tw/w/borg-env/bin/borg create --chunker-params 16,23,31,4095 /extra/repo-xl::1 /home/tw/win\nNumber of files: 3\n\n                       Original size      Compressed size    Deduplicated size\nThis archive:               37.10 GB             14.59 GB             14.59 GB\nAll archives:               37.10 GB             14.59 GB             14.59 GB\n\n                       Unique chunks         Total chunks\nChunk index:                    4319                 4434\n\n$ ls -l /extra/repo-xl/index*\n-rw-rw-r-- 1 tw tw 327698 Jun 21 00:52 index.2011\n\n$ du -sk /extra/repo-xl/\n14253464    /extra/repo-xl/\n"
  },
  {
    "path": "docs/misc/internals-picture.txt",
    "content": "BorgBackup from 10,000 m\n========================\n\n+--------+ +--------+     +--------+\n|archive0| |archive1| ... |archiveN|\n+--------+ +--------+     +--+-----+\n    |          |             |\n    |          |             |\n    |      +---+             |\n    |      |                 |\n    |      |                 |\n    +------+-------+         |\n    |      |       |         |\n /chunk\\/chunk\\/chunk\\...   /maybe different chunks lists\\\n+-----------------------------------------------------------------+\n|item list                                                        |\n+-----------------------------------------------------------------+\n    |\n    +-------------------------------------+--------------+\n    |                                     |              |\n    |                                     |              |\n+-------------+                     +-------------+      |\n|item0        |                     |item1        |      |\n| - owner     |                     | - owner     |      |\n| - size      |                     | - size      |     ...\n| - ...       |                     | - ...       |\n| - chunks    |                     | - chunks    |\n+----+--------+                     +-----+-------+\n     |                                    |\n     | +-----+----------------------------+-----------------+\n     | |     |                                              |\n     +-o-----o------------+                                 |\n     | |     |            |                                 |\n  /chunk0\\/chunk1\\ ... /chunkN\\     /chunk0\\/chunk1\\ ... /chunkN'\\\n +-----------------------------+   +------------------------------+\n |file0                        |   |file0'                        |\n +-----------------------------+   +------------------------------+\n\n\nThanks to anarcat for drawing the picture!\n\n"
  },
  {
    "path": "docs/misc/logging.conf",
    "content": "[loggers]\nkeys=root\n\n[handlers]\nkeys=logfile\n\n[formatters]\nkeys=logfile\n\n[logger_root]\nlevel=NOTSET\nhandlers=logfile\n\n[handler_logfile]\nclass=FileHandler\nlevel=INFO\nformatter=logfile\nargs=('borg.log', 'w')\n\n[formatter_logfile]\nformat=%(asctime)s %(levelname)s %(message)s\ndatefmt=\nclass=logging.Formatter\n"
  },
  {
    "path": "docs/misc/prune-example.txt",
    "content": "borg prune visualized\n=====================\n\nAssume it is 2016-01-01. Today's backup has not yet been made. You have\ncreated at least one backup on each day in 2015 except on 2015-12-19 (no\nbackup was made on that day), and you started backing up with Borg on\n2015-01-01.\n\nThis is what borg prune --keep-daily 14 --keep-monthly 6 --keep-yearly 1\nwould keep.\n\nBackups kept by the --keep-daily rule are marked by a \"d\" to the right,\nbackups kept by the --keep-monthly rule are marked by a \"m\" to the right,\nand backups kept by the --keep-yearly rule are marked by a \"y\" to the\nright.\n\nCalendar view\n-------------\n\n                            2015\n      January               February               March\nMo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su\n          1y 2  3  4                     1                     1\n 5  6  7  8  9 10 11   2  3  4  5  6  7  8   2  3  4  5  6  7  8\n12 13 14 15 16 17 18   9 10 11 12 13 14 15   9 10 11 12 13 14 15\n19 20 21 22 23 24 25  16 17 18 19 20 21 22  16 17 18 19 20 21 22\n26 27 28 29 30 31     23 24 25 26 27 28     23 24 25 26 27 28 29\n                                            30 31\n\n       April                  May                   June\nMo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su\n       1  2  3  4  5               1  2  3   1  2  3  4  5  6  7\n 6  7  8  9 10 11 12   4  5  6  7  8  9 10   8  9 10 11 12 13 14\n13 14 15 16 17 18 19  11 12 13 14 15 16 17  15 16 17 18 19 20 21\n20 21 22 23 24 25 26  18 19 20 21 22 23 24  22 23 24 25 26 27 28\n27 28 29 30           25 26 27 28 29 30 31  29 30m\n\n\n        July                 August              September\nMo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su\n       1  2  3  4  5                  1  2      1  2  3  4  5  6\n 6  7  8  9 10 11 12   3  4  5  6  7  8  9   7  8  9 10 11 12 13\n13 14 15 16 17 18 19  10 11 12 13 14 15 16  14 15 16 17 18 19 20\n20 21 22 23 24 25 26  17 18 19 20 21 22 23  21 22 23 24 25 26 27\n27 28 29 30 31m       24 25 26 27 28 29 30  28 29 30m\n                      31m\n\n      October               November              December\nMo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su\n          1  2  3  4                     1      1  2  3  4  5  6\n 5  6  7  8  9 10 11   2  3  4  5  6  7  8   7  8  9 10 11 12 13\n12 13 14 15 16 17 18   9 10 11 12 13 14 15  14 15 16 17d18d19 20d\n19 20 21 22 23 24 25  16 17 18 19 20 21 22  21d22d23d24d25d26d27d\n26 27 28 29 30 31m    23 24 25 26 27 28 29  28d29d30d31d\n                      30m\n\nList view\n---------\n\n--keep-daily 14     --keep-monthly 6     --keep-yearly 1\n----------------------------------------------------------------\n 1. 2015-12-31       (2015-12-31 kept     (2015-12-31 kept\n 2. 2015-12-30        by daily rule)       by daily rule)\n 3. 2015-12-29       1. 2015-11-30        1. 2015-01-01 (oldest)\n 4. 2015-12-28       2. 2015-10-31\n 5. 2015-12-27       3. 2015-09-30\n 6. 2015-12-26       4. 2015-08-31\n 7. 2015-12-25       5. 2015-07-31\n 8. 2015-12-24       6. 2015-06-30\n 9. 2015-12-23\n10. 2015-12-22\n11. 2015-12-21\n12. 2015-12-20\n    (no backup made on 2015-12-19)\n13. 2015-12-18\n14. 2015-12-17\n\n\nNotes\n-----\n\n2015-12-31 is kept due to the --keep-daily 14 rule (because it is applied\nfirst), not due to the --keep-monthly or --keep-yearly rule.\n\nThe --keep-yearly 1 rule does not consider the December 31st backup because it\nhas already been kept due to the daily rule. There are no backups available\nfrom previous years, so the --keep-yearly target of 1 backup is not satisfied.\nBecause of this, the 2015-01-01 archive (the oldest archive available) is kept.\n\nThe --keep-monthly 6 rule keeps Nov, Oct, Sep, Aug, Jul and Jun. December is\nnot considered for this rule, because that backup was already kept because of\nthe daily rule.\n\n2015-12-17 is kept to satisfy the --keep-daily 14 rule, because no backup was\nmade on 2015-12-19. If a backup had been made on that day, it would not keep\nthe one from 2015-12-17.\n\nWe did not include weekly, hourly, minutely, or secondly rules to keep this\nexample simple. They all work in basically the same way.\n\nThe weekly rule is easy to understand roughly, but hard to understand in all\ndetails. If you are interested, read \"ISO 8601:2000 standard week-based year\".\n\nThe 13weekly and 3monthly rules are two different strategies for keeping one backup\nevery quarter of a year. There are `multiple ways` to define a quarter-year;\nborg prune recognizes two:\n\n* --keep-13weekly keeps one backup every 13 weeks using ISO 8601:2000's\n  definition of the week-based year. January 4th is always included in the\n  first week of a year, and January 1st to 3rd may be in week 52 or 53 of the\n  previous year. Week 53 is also in the fourth quarter of the year.\n* --keep-3monthly keeps one backup every 3 months. January 1st to\n  March 31, April 1st to June 30th, July 1st to September 30th, and October 1st\n  to December 31st form the quarters.\n\nIf the subtleties of the definition of a quarter-year don't matter to you, a\nshort summary of behavior is:\n\n* --keep-13weekly favors keeping backups at the beginning of Jan, Apr, Jul,\n  and Oct.\n* --keep-3monthly favors keeping backups at the end of Dec, Mar, Jun, and Sep.\n* Both strategies will have some overlap in which backups are kept.\n* The differences are negligible unless backups considered for deletion were\n  created weekly or more frequently.\n\n.. _multiple ways: https://en.wikipedia.org/wiki/Calendar_year#Quarter_year\n"
  },
  {
    "path": "docs/quickstart.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: bash\n.. _quickstart:\n\nQuick Start\n===========\n\nThis chapter will get you started with Borg and covers various use cases.\n\nA step-by-step example\n----------------------\n\n.. include:: quickstart_example.rst.inc\n\nArchives and repositories\n-------------------------\n\nA *Borg archive* is the result of a single backup (``borg create``). An archive\nstores a snapshot of the data of the files \"inside\" it. One can later extract or\nmount an archive to restore from a backup.\n\n*Repositories* are filesystem directories acting as self-contained stores of archives.\nRepositories can be accessed locally via path or remotely via SSH. Under the hood,\nrepositories contain data blocks and a manifest that tracks which blocks are in each\narchive. If some data hasn't changed between backups, Borg simply\nreferences an already-uploaded data chunk (deduplication).\n\n.. _about_free_space:\n\nImportant note about free space\n-------------------------------\n\nBefore you start creating backups, ensure that there is *always* plenty\nof free space on the destination filesystem that has your backup repository\n(and also on ~/.cache). A few GB should suffice for most hard-drive sized\nrepositories. See also :ref:`cache-memory-usage`.\n\nIf you do run out of disk space, it can be hard or impossible to free space,\nbecause Borg needs free space to operate - even to delete backup archives.\n\nYou can use some monitoring process or just include the free space information\nin your backup log files (you check them regularly anyway, right?).\n\nAlso helpful:\n\n- use `borg repo-space` to reserve some disk space that can be freed when the filesystem\n  does not have free space any more.\n- if you use LVM: use a LV + a filesystem that you can resize later and have\n  some unallocated PEs you can add to the LV.\n- consider using quotas (e.g. fs quota, quota settings of storage provider)\n- use `prune` and `compact` regularly\n\n\nImportant note about permissions\n--------------------------------\n\nTo avoid permission issues (in your borg repository or borg cache), **always\naccess the repository using the same user account**.\n\nIf you want to back up files of other users or the operating system, running\nborg as root likely will be required (otherwise you get `Permission denied`\nerrors).\nIf you only back up your own files, run it as your normal user (i.e. not root).\n\nFor a local repository always use the same user to invoke borg.\n\nFor a remote repository: always use e.g., ssh://borg@remote_host. You can use this\nfrom different local users; the remote user running borg and accessing the\nrepo will always be `borg`.\n\nIf you need to access a local repository from different users, you can use the\nsame method by using ssh to borg@localhost.\n\nImportant note about files changing during the backup process\n-------------------------------------------------------------\n\nBorg does not do anything about the internal consistency of the data\nit backs up.  It just reads and backs up each file in whatever state\nthat file is when Borg gets to it.  On an active system, this can lead\nto two kinds of inconsistency:\n\n- By the time Borg backs up a file, it might have changed since the backup process was initiated\n- A file could change while Borg is backing it up, making the file internally inconsistent\n\nIf you have a set of files and want to ensure that they are backed up\nin a specific or consistent state, you must take steps to prevent\nchanges to those files during the backup process.  There are a few\ncommon techniques to achieve this.\n\n- Avoid running any programs that might change the files.\n\n- Snapshot files, filesystems, container storage volumes, or logical volumes.\n  LVM or ZFS might be useful here.\n\n- Dump databases or stop the database servers.\n\n- Shut down virtual machines before backing up their disk image files.\n\n- Shut down containers before backing up their storage volumes.\n\nFor some systems, Borg might work well enough without these\nprecautions.  If you are simply backing up the files on a system that\nisn't very active (e.g. in a typical home directory), Borg usually\nworks well enough without further care for consistency.  Log files and\ncaches might not be in a perfect state, but this is rarely a problem.\n\nFor databases, virtual machines, and containers, there are specific\ntechniques for backing them up that do not simply use Borg to back up\nthe underlying filesystem.  For databases, check your database\ndocumentation for techniques that will save the database state between\ntransactions.  For virtual machines, consider running the backup on\nthe VM itself or mounting the filesystem while the VM is shut down.\nFor Docker containers, perhaps docker's \"save\" command can help.\n\nAutomating backups\n------------------\n\nThe following example script is meant to be run daily by the ``root`` user on\ndifferent local machines. It backs up a machine's important files (but not the\ncomplete operating system) to a repository ``~/backup/main``  on a remote server.\nSome files which aren't necessarily needed in this backup are excluded. See\n:ref:`borg_patterns` on how to add more exclude options.\n\nAfter the backup, this script also uses the :ref:`borg_prune` subcommand to keep\na certain number of old archives and deletes the others.\n\nFinally, it uses the :ref:`borg_compact` subcommand to remove deleted objects\nfrom the segment files in the repository to free disk space.\n\nBefore running, make sure that the repository is initialized as documented in\n:ref:`remote_repos` and that the script has the correct permissions to be executable\nby the root user, but not executable or readable by anyone else, i.e. root:root 0700.\n\nYou can use this script as a starting point and modify it where it's necessary to fit\nyour setup.\n\nDo not forget to test your created backups to make sure everything you need is\nbacked up and that the ``prune`` command keeps and deletes the correct backups.\n\n::\n\n    #!/bin/sh\n\n    # Setting this, so the repo does not need to be given on the commandline:\n    export BORG_REPO=ssh://username@example.com:2022/~/backup/main\n\n    # See the section \"Passphrase notes\" for more infos.\n    export BORG_PASSPHRASE='XYZl0ngandsecurepa_55_phrasea&&123'\n\n    # some helpers and error handling:\n    info() { printf \"\\n%s %s\\n\\n\" \"$( date )\" \"$*\" >&2; }\n    trap 'echo $( date ) Backup interrupted >&2; exit 2' INT TERM\n\n    info \"Starting backup\"\n\n    # Back up the most important directories into an archive named after\n    # the machine this script is currently running on:\n\n    borg create                         \\\n        --verbose                       \\\n        --filter AME                    \\\n        --list                          \\\n        --stats                         \\\n        --show-rc                       \\\n        --compression lz4               \\\n        --exclude-caches                \\\n        --exclude 'home/*/.cache/*'     \\\n        --exclude 'var/tmp/*'           \\\n                                        \\\n        '{hostname}'                    \\\n        /etc                            \\\n        /home                           \\\n        /root                           \\\n        /var\n\n    backup_exit=$?\n\n    info \"Pruning repository\"\n\n    # Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly\n    # archives of THIS machine. The '{hostname}' matching is very important to\n    # limit prune's operation to archives with exactly that name and not apply\n    # to archives with other names also:\n\n    borg prune               \\\n        '{hostname}'         \\\n        --list               \\\n        --show-rc            \\\n        --keep-daily    7    \\\n        --keep-weekly   4    \\\n        --keep-monthly  6\n\n    prune_exit=$?\n\n    # actually free repo disk space by compacting segments\n\n    info \"Compacting repository\"\n\n    borg compact -v\n\n    compact_exit=$?\n\n    # use highest exit code as global exit code\n    global_exit=$(( backup_exit > prune_exit ? backup_exit : prune_exit ))\n    global_exit=$(( compact_exit > global_exit ? compact_exit : global_exit ))\n\n    if [ ${global_exit} -eq 0 ]; then\n        info \"Backup, Prune, and Compact finished successfully\"\n    elif [ ${global_exit} -eq 1 ]; then\n        info \"Backup, Prune, and/or Compact finished with warnings\"\n    else\n        info \"Backup, Prune, and/or Compact finished with errors\"\n    fi\n\n    exit ${global_exit}\n\nPitfalls with shell variables and environment variables\n-------------------------------------------------------\n\nThis applies to all environment variables you want Borg to see, not just\n``BORG_PASSPHRASE``. TL;DR: always ``export`` your variable,\nand use single quotes if you're unsure of the details of your shell's expansion\nbehavior. E.g.::\n\n    export BORG_PASSPHRASE='complicated & long'\n\nThis is because ``export`` exposes variables to subprocesses, which Borg may be\none of. More on ``export`` can be found in the \"ENVIRONMENT\" section of the\nbash(1) man page.\n\nBeware of how ``sudo`` interacts with environment variables. For example, you\nmay be surprised that the following ``export`` has no effect on your command::\n\n   export BORG_PASSPHRASE='complicated & long'\n   sudo ./yourborgwrapper.sh  # still prompts for password\n\nFor more information, refer to the sudo(8) man page and ``env_keep`` in\nthe sudoers(5) man page.\n\n.. Tip::\n    To debug what your borg process sees, find its PID\n    (``ps aux|grep borg``) and then look into ``/proc/<PID>/environ``.\n\n.. passphrase_notes:\n\nPassphrase notes\n----------------\n\nIf you use encryption (or authentication), Borg will ask you interactively\nfor a passphrase to encrypt/decrypt the keyfile / repokey.\n\nA passphrase should be a single line of text. Any trailing linefeed will be\nstripped.\n\nDo not use empty passphrases, as these can be trivially guessed, which does not\nleave any encrypted data secure.\n\nAvoid passphrases containing non-ASCII characters.\nBorg can process any unicode text, but problems may arise at input due to text\nencoding or differing keyboard layouts, so best just avoid non-ASCII stuff.\n\nSee: https://xkcd.com/936/\n\nIf you want to automate, you can supply the passphrase\ndirectly or indirectly with the use of environment variables.\n\nSupply a passphrase directly::\n\n    # use this passphrase (use safe permissions on the script!):\n    export BORG_PASSPHRASE='my super secret passphrase'\n\nOr delegate to an external program to supply the passphrase::\n\n    # use the \"pass\" password manager to get the passphrase:\n    export BORG_PASSCOMMAND='pass show backup'\n\n    # use GPG to get the passphrase contained in a gpg-encrypted file:\n    export BORG_PASSCOMMAND='gpg --decrypt borg-passphrase.gpg'\n\nOr read the passphrase from an open file descriptor::\n\n    export BORG_PASSPHRASE_FD=42\n\nUsing hardware crypto devices (like Nitrokey, Yubikey and others) is not\ndirectly supported by borg, but you can use these indirectly.\nE.g. if your crypto device supports GPG and borg calls ``gpg`` via\n``BORG_PASSCOMMAND``, it should just work.\n\n.. backup_compression:\n\nBackup compression\n------------------\n\nThe default is lz4 (very fast, but low compression ratio), but other methods are\nsupported for different situations. Compression not only helps you save disk space,\nbut will especially speed up remote backups since less data needs to be transferred.\n\nzstd is a modern compression algorithm which can be parametrized to anything between\nN=1 for highest speed (and relatively low compression) to N=22 for highest compression\n(and lower speed)::\n\n    $ borg create --compression zstd,N arch ~\n\nIf you have a fast repo storage and you want minimum CPU usage you can disable\ncompression::\n\n    $ borg create --compression none arch ~\n\nYou can also use zlib and lzma instead of zstd, although zstd usually provides the\nthe best compression for a given resource consumption. Please see :ref:`borg_compression`\nfor all options.\n\nAn interesting alternative is ``auto``, which first checks with lz4 whether a chunk is\ncompressible (that check is very fast), and only if it is, compresses it with the\nspecified algorithm::\n\n    $ borg create --compression auto,zstd,7 arch ~\n\nYou'll need to experiment a bit to find the best compression for your use case.\nKeep an eye on CPU load and throughput.\n\n.. _encrypted_repos:\n\nRepository encryption\n---------------------\n\nYou can choose the repository encryption mode at repository creation time::\n\n    $ borg repo-create --encryption=MODE\n\nFor a list of available encryption MODEs and their descriptions, please refer\nto :ref:`borg_repo-create`.\n\nIf you use encryption, all data is encrypted on the client before being written\nto the repository.\nThis means that an attacker who manages to compromise the host containing an\nencrypted repository will not be able to access any of the data, even while the\nbackup is being made.\n\nKey material is stored in encrypted form and can be only decrypted by providing\nthe correct passphrase.\n\nFor automated backups the passphrase can be specified using the\n`BORG_PASSPHRASE` environment variable.\n\n.. note:: Be careful about how you set that environment, see\n          :ref:`this note about password environments <password_env>`\n          for more information.\n\n.. warning:: The repository data is totally inaccessible without the key\n    and the key passphrase.\n\n    In any case, make a backup of the borg key, see :ref:`borg_key_export` for\n    more details.\n\n\n.. _remote_repos:\n\nRemote repositories\n-------------------\n\nBorg can initialize and access repositories on remote hosts if the\nhost is accessible using SSH.  This is fastest and easiest when Borg\nis installed on the remote host, in which case the following syntax is used::\n\n  $ borg -r ssh://user@hostname:port/path/to/repo repo-create ...\n\nNote: Please see the usage chapter for a full documentation of repo URLs. Also\nsee :ref:`ssh_configuration` for recommended settings to avoid disconnects and hangs.\n\nRemote operations over SSH can be automated with SSH keys. You can restrict the\nuse of the SSH keypair by prepending a forced command to the SSH public key in\nthe remote server's `authorized_keys` file. This example will start Borg\nin server mode and limit it to a specific filesystem path::\n\n  command=\"borg serve --restrict-to-path /path/to/repo\",restrict ssh-rsa AAAAB3[...]\n\nIf it is not possible to install Borg on the remote host,\nit is still possible to use the remote host to store a repository by\nmounting the remote filesystem, for example, using sshfs::\n\n  $ sshfs user@hostname:/path/to /path/to\n  $ borg -r /path/to/repo repo-create ...\n  $ fusermount -u /path/to\n\nYou can also use other remote filesystems in a similar way. Just be careful,\nnot all filesystems out there are really stable and working good enough to\nbe acceptable for backup usage.\n\nOther kinds of repositories\n---------------------------\n\nDue to using the `borgstore` project, borg now also supports other kinds of\n(remote) repositories besides `file:` and `ssh:`:\n\n- sftp: the borg client will directly talk to an sftp server.\n  This does not require borg being installed on the sftp server.\n- rclone: the borg client will talk via rclone to cloud storage.\n- Others may come in the future, adding backends to `borgstore` is rather simple.\n\nRestoring a backup\n------------------\n\nPlease note that we describe only the most basic commands and options\nhere. Refer to the command reference to see more.\n\nTo restore, work **on the same machine as the same user**\nthat was used to create the backups of the wanted files. Doing so\navoids issues such as:\n\n- confusion relating to paths\n- mapping of user/group names to user/group IDs\n- permissions\n\nYou likely already have a working borg setup there, including perhaps:\n\n  - an environment variable for the key passphrase (for encrypted repos),\n  - a keyfile for the repo (not needed for repokey mode),\n  - a ssh key for the repo server (not needed for locally mounted repos),\n  - a valid borg cache for that repo (quicker than cache rebuild).\n\nThe **user** might be:\n\n- root (if full backups, backups including system stuff or multiple\n  users' files were made)\n- some specific user using sudo to execute borg as root\n- some specific user (if backups of that user's files were made)\n\nA borg **backup repository** can be either:\n\n- in a local directory (like e.g. a locally mounted USB disk)\n- on a remote backup server machine that is reachable via ssh (client/server)\n\nIf the repository is encrypted, you will also need the **key** and the **passphrase**\n(which is protecting the key).\n\nThe **key** can be located:\n\n- in the repository (**repokey** mode).\n\n  Easy, this will usually \"just work\".\n- in the home directory of the user who made the backup (**keyfile** mode).\n\n  This may cause a bit more effort:\n\n  - if you have just lost that home directory and you first need to restore the\n    borg key (e.g. from the separate backup you made of it or from another\n    user or machine accessing the same repository).\n  - if you first must find out the correct machine / user / home directory\n    (where the borg client was run to make the backups).\n\nThe **passphrase** for the key has been either:\n\n- entered interactively at backup time\n  (not practical if backup is automated / unattended).\n- acquired via some environment variable driven mechanism in the backup script\n  (look there for BORG_PASSPHRASE, BORG_PASSCOMMAND, etc. and just do it like\n  that).\n\nThere are **2 ways to restore** files from a borg backup repository:\n\n- **borg mount** - use this if:\n\n  - you don't know exactly which files you want to restore\n  - you don't know which archive contains the files (in the state) you want\n  - you need to look into files / directories before deciding what you want\n  - you need a relatively low volume of data restored\n  - you don't care for restoring stuff that FUSE mount does not implement yet\n    (like special fs flags, ACLs)\n  - you have a client with good resources (RAM, CPU, temporary disk space)\n  - you would rather use some filemanager to restore (copy) files than borg\n    extract shell commands\n\n- **borg extract** - use this if:\n\n  - you know precisely what you want (repo, archive, path)\n  - you need a high volume of files restored (best speed)\n  - you want a as-complete-as-it-gets reproduction of file metadata\n    (like special fs flags, ACLs)\n  - you have a client with low resources (RAM, CPU, temp. disk space)\n\n\nExample with **borg mount**:\n\n::\n\n    # open a new, separate terminal (this terminal will be blocked until umount)\n\n    # now we find out the archive ID of the archive we want to mount:\n    borg repo-list\n\n    # mount one archive giving its archive ID prefix:\n    borg mount -a aid:d34db33f /mnt/borg\n\n    # alternatively, mount all archives from a borg repo (slower):\n    borg mount /mnt/borg\n\n    # it may take a while until you will see stuff in /mnt/borg.\n\n    # now use another terminal or file browser and look into /mnt/borg.\n    # when finished, umount to unlock the repo and unblock the terminal:\n    borg umount /mnt/borg\n\n\nExample with **borg extract**:\n\n::\n\n    # borg extract always extracts into current directory and that directory\n    # should be empty (borg does not support transforming a non-empty dir to\n    # the state as present in your backup archive).\n    mkdir borg_restore\n    cd borg_restore\n\n    # now we find out the archive ID of the archive we want to extract:\n    borg repo-list\n\n    # find out how the paths stored in the the archive look like:\n    borg list aid:d34db33f\n\n    # we extract only some specific path (note: no leading / !):\n    borg extract aid:d34db33f path/to/extract\n\n    # alternatively, we could fully extract the archive:\n    borg extract aid:d34db33f\n\n    # now move the files to the correct place...\n\n\nDifference when using a **remote borg backup server**:\n\nIt is basically all the same as with the local repository, but you need to\nrefer to the repo using a ``ssh://`` URL.\n\nIn the given example, ``borg`` is the user name used to log into the machine\n``backup.example.org`` which runs ssh on port ``2222`` and has the borg repo\nin ``/path/to/repo``.\n\nInstead of giving a FQDN or a hostname, you can also give an IP address.\n\nAs usual, you either need a password to log in or the backup server might\nhave authentication set up via ssh ``authorized_keys`` (which is likely the\ncase if unattended, automated backups were done).\n\n::\n\n    borg -r ssh://borg@backup.example.org:2222/path/to/repo mount /mnt/borg\n    # or\n    borg -r ssh://borg@backup.example.org:2222/path/to/repo extract archive\n"
  },
  {
    "path": "docs/quickstart_example.rst.inc",
    "content": "1. Before a backup can be made, a repository has to be initialized::\n\n    $ borg -r /path/to/repo repo-create --encryption=repokey-aes-ocb\n\n2. Back up the ``~/src`` and ``~/Documents`` directories into an archive called\n   *docs*::\n\n    $ borg -r /path/to/repo create docs ~/src ~/Documents\n\n3. The next day, create a new archive using the same archive name::\n\n    $ borg -r /path/to/repo create --stats docs ~/src ~/Documents\n\n   This backup will be much quicker and much smaller, since only new,\n   never-before-seen data is stored. The ``--stats`` option causes Borg to\n   output statistics about the newly created archive such as the deduplicated\n   size (the amount of unique data not shared with other archives)::\n\n    Repository: /path/to/repo\n    Archive name: docs\n    Archive fingerprint: bcd1b53f9b4991b7afc2b339f851b7ffe3c6d030688936fe4552eccc1877718d\n    Time (start): Sat, 2022-06-25 20:21:43\n    Time (end):   Sat, 2022-06-25 20:21:43\n    Duration: 0.07 seconds\n    Utilization of maximum archive size: 0%\n    Number of files: 699\n    Original size: 31.14 MB\n    Deduplicated size: 502 B\n\n4. List all archives in the repository::\n\n    $ borg -r /path/to/repo repo-list\n    docs                                 Sat, 2022-06-25 20:21:14 [b80e24d2...b179f298]\n    docs                                 Sat, 2022-06-25 20:21:43 [bcd1b53f...1877718d]\n\n5. List the contents of the first archive::\n\n    $ borg -r /path/to/repo list aid:b80e24d2\n    drwxr-xr-x user   group          0 Mon, 2016-02-15 18:22:30 home/user/Documents\n    -rw-r--r-- user   group       7961 Mon, 2016-02-15 18:22:30 home/user/Documents/Important.doc\n    ...\n\n6. Restore the first archive by extracting the files relative to the current directory::\n\n    $ borg -r /path/to/repo extract aid:b80e24d2\n\n7. Delete the first archive (please note that this does **not** free repository disk space)::\n\n    $ borg -r /path/to/repo delete aid:b80e24d2\n\n   Be careful if you use an archive NAME (and not an archive ID), as it might match multiple archives.\n   Always use ``--dry-run`` and ``--list`` first!\n\n8. Recover disk space by compacting the segment files in the repository::\n\n    $ borg -r /path/to/repo compact -v\n\n.. Note::\n    Borg is quiet by default (it defaults to WARNING log level).\n    You can use options like ``--progress`` or ``--list`` to get specific\n    reports during command execution.  You can also add the ``-v`` (or\n    ``--verbose`` or ``--info``) option to adjust the log level to INFO to\n    get other informational messages.\n"
  },
  {
    "path": "docs/support.rst",
    "content": ".. _support:\n\nSupport\n=======\n\nSupport and Services\n--------------------\n\nPlease see https://www.borgbackup.org/ for free and paid support and service options.\n\n\n.. _security-contact:\n\nSecurity\n--------\n\nIn case you discover a security issue, please use this contact for reporting it\nprivately and please, if possible, use encrypted E-Mail:\n\nThomas Waldmann <tw@waldmann-edv.de>\n\nGPG Key Fingerprint: 6D5B EF9A DD20 7580 5747  B70F 9F88 FB52 FAF7 B393\n\nThe public key can be fetched from any GPG keyserver, but be careful: you must\nuse the **full fingerprint** to check that you got the correct key.\n\nVerifying signed releases\n-------------------------\n\n`Releases <https://github.com/borgbackup/borg/releases>`_ are signed with the\nsame GPG key and a .asc file is provided for each binary.\n\nTo verify a signature, the public key needs to be known to GPG. It can be\nimported into the local keystore from a keyserver with the fingerprint::\n\n      gpg --recv-keys \"6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393\"\n\nIf GPG successfully imported the key, the output should include (among other things)::\n\n      ...\n      gpg: Total number processed: 1\n      ...\n\nTo verify for example the signature of the borg-linux64 binary::\n\n      gpg --verify borg-linux64.asc\n\nGPG outputs if it finds a good signature. The output should look similar to this::\n\n      gpg: Signature made Sat 30 Dec 2017 01:07:36 PM CET using RSA key ID 51F78E01\n      gpg: Good signature from \"Thomas Waldmann <email>\"\n      gpg: aka \"Thomas Waldmann <email>\"\n      gpg: aka \"Thomas Waldmann <email>\"\n      gpg: aka \"Thomas Waldmann <email>\"\n      gpg: WARNING: This key is not certified with a trusted signature!\n      gpg: There is no indication that the signature belongs to the owner.\n      Primary key fingerprint: 6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393\n      Subkey fingerprint: 2F81 AFFB AB04 E11F E8EE 65D4 243A CFA9 51F7 8E01\n\nIf you want to make absolutely sure that you have the right key, you need to\nverify it via another channel and assign a trust-level to it.\n"
  },
  {
    "path": "docs/usage/analyze.rst",
    "content": ".. include:: analyze.rst.inc\n"
  },
  {
    "path": "docs/usage/analyze.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_analyze:\n\nborg analyze\n------------\n.. code-block:: none\n\n    borg [common options] analyze [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nAnalyze archives to find \"hot spots\".\n\n``borg analyze`` relies on the usual archive matching options to select the\narchives that should be considered for analysis (e.g. ``-a series_name``).\nThen it iterates over all matching archives, over all contained files, and\ncollects information about chunks stored in all directories it encounters.\n\nIt considers chunk IDs and their plaintext sizes (we do not have the compressed\nsize in the repository easily available) and adds up the sizes of added and removed\nchunks per direct parent directory, and outputs a list of \"directory: size\".\n\nYou can use that list to find directories with a lot of \"activity\" — maybe\nsome of these are temporary or cache directories you forgot to exclude.\n\nTo avoid including these unwanted directories in your backups, you can carefully\nexclude them in ``borg create`` (for future backups) or use ``borg recreate``\nto recreate existing archives without them."
  },
  {
    "path": "docs/usage/benchmark.rst",
    "content": ".. include:: benchmark_crud.rst.inc\n\n.. include:: benchmark_cpu.rst.inc\n"
  },
  {
    "path": "docs/usage/benchmark_cpu.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_benchmark_cpu:\n\nborg benchmark cpu\n------------------\n.. code-block:: none\n\n    borg [common options] benchmark cpu [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+------------+-----------------------+\n    | **options**                                                                                |\n    +-------------------------------------------------------+------------+-----------------------+\n    |                                                       | ``--json`` | format output as JSON |\n    +-------------------------------------------------------+------------+-----------------------+\n    | .. class:: borg-common-opt-ref                                                             |\n    |                                                                                            |\n    | :ref:`common_options`                                                                      |\n    +-------------------------------------------------------+------------+-----------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --json     format output as JSON\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command benchmarks miscellaneous CPU-bound Borg operations.\n\nIt creates input data in memory, runs the operation and then displays throughput.\nTo reduce outside influence on the timings, please make sure to run this with:\n\n- an otherwise as idle as possible machine\n- enough free memory so there will be no slow down due to paging activity"
  },
  {
    "path": "docs/usage/benchmark_crud.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_benchmark_crud:\n\nborg benchmark crud\n-------------------\n.. code-block:: none\n\n    borg [common options] benchmark crud [options] PATH\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n    | **positional arguments**                                                                                             |\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n    |                                                       | ``PATH``         | path where to create benchmark input data |\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n    | **options**                                                                                                          |\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n    |                                                       | ``--json-lines`` | Format output as JSON Lines.              |\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                       |\n    |                                                                                                                      |\n    | :ref:`common_options`                                                                                                |\n    +-------------------------------------------------------+------------------+-------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    PATH\n        path where to create benchmark input data\n\n\n    options\n        --json-lines     Format output as JSON Lines.\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command benchmarks borg CRUD (create, read, update, delete) operations.\n\nIt creates input data below the given PATH and backs up this data into the given REPO.\nThe REPO must already exist (it could be a fresh empty repo or an existing repo, the\ncommand will create / read / update / delete some archives named borg-benchmark-crud\\* there.\n\nMake sure you have free space there; you will need about 1 GB each (+ overhead).\n\nIf your repository is encrypted and borg needs a passphrase to unlock the key, use::\n\n    BORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH\n\nMeasurements are done with different input file sizes and counts.\nThe file contents are very artificial (either all zero or all random),\nthus the measurement results do not necessarily reflect performance with real data.\nAlso, due to the kind of content used, no compression is used in these benchmarks.\n\nC- == borg create (1st archive creation, no compression, do not use files cache)\n      C-Z- == all-zero files. full dedup, this is primarily measuring reader/chunker/hasher.\n      C-R- == random files. no dedup, measuring throughput through all processing stages.\n\nR- == borg extract (extract archive, dry-run, do everything, but do not write files to disk)\n      R-Z- == all zero files. Measuring heavily duplicated files.\n      R-R- == random files. No duplication here, measuring throughput through all processing\n      stages, except writing to disk.\n\nU- == borg create (2nd archive creation of unchanged input files, measure files cache speed)\n      The throughput value is kind of virtual here, it does not actually read the file.\n      U-Z- == needs to check the 2 all-zero chunks' existence in the repo.\n      U-R- == needs to check existence of a lot of different chunks in the repo.\n\nD- == borg delete archive (delete last remaining archive, measure deletion + compaction)\n      D-Z- == few chunks to delete / few segments to compact/remove.\n      D-R- == many chunks to delete / many segments to compact/remove.\n\nPlease note that there might be quite some variance in these measurements.\nTry multiple measurements and having a otherwise idle machine (and network, if you use it)."
  },
  {
    "path": "docs/usage/borgfs.rst",
    "content": ":orphan:\n\n.. include:: borgfs.rst.inc\n"
  },
  {
    "path": "docs/usage/borgfs.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_borgfs:\n\nborg borgfs\n-----------\n.. code-block:: none\n\n    borg [common options] borgfs [options] REPOSITORY_OR_ARCHIVE MOUNTPOINT [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                                     |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``REPOSITORY_OR_ARCHIVE``             | repository/archive to mount                                                                                                                            |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``MOUNTPOINT``                        | where to mount filesystem                                                                                                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``PATH``                              | paths to extract; patterns are supported                                                                                                               |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **optional arguments**                                                                                                                                                                                                                                                       |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-V``, ``--version``                 | show version number and exit                                                                                                                           |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-f``, ``--foreground``              | stay in foreground, do not daemonize                                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-o``                                | Extra mount options                                                                                                                                    |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                               |\n    |                                                                                                                                                                                                                                                                              |\n    | :ref:`common_options`                                                                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                                                  |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-P PREFIX``, ``--prefix PREFIX``    | only consider archive names starting with this prefix.                                                                                                 |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a GLOB``, ``--glob-archives GLOB`` | only consider archive names matching the glob. sh: rules apply, see \"borg help patterns\". ``--prefix`` and ``--glob-archives`` are mutually exclusive. |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                    | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp                                                       |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                         | consider first N archives after other filters were applied                                                                                             |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                          | consider last N archives after other filters were applied                                                                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Exclusion options**                                                                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN                                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--exclude-from EXCLUDEFILE``        | read exclude patterns from EXCLUDEFILE, one per line                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--pattern PATTERN``                 | experimental: include/exclude paths matching PATTERN                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--patterns-from PATTERNFILE``       | experimental: read include/exclude patterns from PATTERNFILE, one per line                                                                             |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--strip-components NUMBER``         | Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    REPOSITORY_OR_ARCHIVE\n        repository/archive to mount\n    MOUNTPOINT\n        where to mount filesystem\n    PATH\n        paths to extract; patterns are supported\n\n\n    optional arguments\n        -V, --version    show version number and exit\n        -f, --foreground    stay in foreground, do not daemonize\n        -o     Extra mount options\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -P PREFIX, --prefix PREFIX        only consider archive names starting with this prefix.\n        -a GLOB, --glob-archives GLOB     only consider archive names matching the glob. sh: rules apply, see \"borg help patterns\". ``--prefix`` and ``--glob-archives`` are mutually exclusive.\n        --sort-by KEYS                    Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp\n        --first N                         consider first N archives after other filters were applied\n        --last N                          consider last N archives after other filters were applied\n\n\n    Exclusion options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --strip-components NUMBER         Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command mounts an archive as a FUSE filesystem. This can be useful for\nbrowsing an archive or restoring individual files. Unless the ``--foreground``\noption is given the command will run in the background until the filesystem\nis ``umounted``.\n\nThe command ``borgfs`` provides a wrapper for ``borg mount``. This can also be\nused in fstab entries:\n``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0``\n\nTo allow a regular user to use fstab entries, add the ``user`` option:\n``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``\n\nFor mount options, see the fuse(8) manual page. Additional mount options\nsupported by borg:\n\n- versions: when used with a repository mount, this gives a merged, versioned\n  view of the files in the archives. EXPERIMENTAL, layout may change in future.\n- allow_damaged_files: by default damaged files (where missing chunks were\n  replaced with runs of zeros by borg check ``--repair``) are not readable and\n  return EIO (I/O error). Set this option to read such files.\n\nThe BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users\nto tweak the performance. It sets the number of cached data chunks; additional\nmemory usage can be up to ~8 MiB times this number. The default is the number\nof CPU cores.\n\nWhen the daemonized process receives a signal or crashes, it does not unmount.\nUnmounting in these cases could cause an active rsync or similar process\nto delete data unintentionally.\n\nWhen running in the foreground ^C/SIGINT unmounts cleanly, but other\nsignals or crashes do not."
  },
  {
    "path": "docs/usage/break-lock.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_break-lock:\n\nborg break-lock\n---------------\n.. code-block:: none\n\n    borg [common options] break-lock [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                        |\n    |                                                       |\n    | :ref:`common_options`                                 |\n    +-------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command breaks the repository and cache locks.\nUse with care and only when no Borg process (on any machine) is\ntrying to access the cache or the repository."
  },
  {
    "path": "docs/usage/check.rst",
    "content": ".. include:: check.rst.inc\n"
  },
  {
    "path": "docs/usage/check.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_check:\n\nborg check\n----------\n.. code-block:: none\n\n    borg [common options] check [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--repository-only``                        | only perform repository checks                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--archives-only``                          | only perform archive checks                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--verify-data``                            | perform cryptographic archive data integrity verification (conflicts with ``--repository-only``)                            |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--repair``                                 | attempt to repair any inconsistencies found                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--find-lost-archives``                     | attempt to find lost archives                                                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--max-duration SECONDS``                   | perform only a partial repository check for at most SECONDS seconds (default: unlimited)                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --repository-only    only perform repository checks\n        --archives-only    only perform archive checks\n        --verify-data     perform cryptographic archive data integrity verification (conflicts with ``--repository-only``)\n        --repair          attempt to repair any inconsistencies found\n        --find-lost-archives    attempt to find lost archives\n        --max-duration SECONDS    perform only a partial repository check for at most SECONDS seconds (default: unlimited)\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThe check command verifies the consistency of a repository and its archives.\nIt consists of two major steps:\n\n1. Checking the consistency of the repository itself. This includes checking\n   the file magic headers, and both the metadata and data of all objects in\n   the repository. The read data is checked by size and hash. Bit rot and other\n   types of accidental damage can be detected this way. Running the repository\n   check can be split into multiple partial checks using ``--max-duration``.\n   When checking an ssh:// remote repository, please note that the checks run on\n   the server and do not cause significant network traffic.\n\n2. Checking consistency and correctness of the archive metadata and optionally\n   archive data (requires ``--verify-data``). This includes ensuring that the\n   repository manifest exists, the archive metadata chunk is present, and that\n   all chunks referencing files (items) in the archive exist. This requires\n   reading archive and file metadata, but not data. To scan for archives whose\n   entries were lost from the archive directory, pass ``--find-lost-archives``.\n   It requires reading all data and is hence very time-consuming.\n   To additionally cryptographically verify the file (content) data integrity,\n   pass ``--verify-data``, which is even more time-consuming.\n\n   When checking archives of a remote repository, archive checks run on the client\n   machine because they require decrypting data and therefore the encryption key.\n\nBoth steps can also be run independently. Pass ``--repository-only`` to run the\nrepository checks only, or pass ``--archives-only`` to run the archive checks\nonly.\n\nThe ``--max-duration`` option can be used to split a long-running repository\ncheck into multiple partial checks. After the given number of seconds, the check\nis interrupted. The next partial check will continue where the previous one\nstopped, until the full repository has been checked. Assuming a complete check\nwould take 7 hours, then running a daily check with ``--max-duration=3600``\n(1 hour) would result in one full repository check per week. Doing a full\nrepository check aborts any previous partial check; the next partial check will\nrestart from the beginning. With partial repository checks you can run neither\narchive checks, nor enable repair mode. Consequently, if you want to use\n``--max-duration`` you must also pass ``--repository-only``, and must not pass\n``--archives-only``, nor ``--repair``.\n\n**Warning:** Please note that partial repository checks (i.e., running with\n``--max-duration``) can only perform non-cryptographic checksum checks on the\nrepository files. Enabling partial repository checks excludes archive checks\nfor the same reason. Therefore, partial checks may be useful only with very large\nrepositories where a full check would take too long.\n\nThe ``--verify-data`` option will perform a full integrity verification (as\nopposed to checking just the xxh64) of data, which means reading the\ndata from the repository, decrypting and decompressing it. It is a complete\ncryptographic verification and hence very time-consuming, but will detect any\naccidental and malicious corruption. Tamper-resistance is only guaranteed for\nencrypted repositories against attackers without access to the keys. You cannot\nuse ``--verify-data`` with ``--repository-only``.\n\nThe ``--find-lost-archives`` option will also scan the whole repository, but\ntells Borg to search for lost archive metadata. If Borg encounters any archive\nmetadata that does not match an archive directory entry (including\nsoft-deleted archives), it means that an entry was lost.\nUnless ``borg compact`` is called, these archives can be fully restored with\n``--repair``. Please note that ``--find-lost-archives`` must read a lot of\ndata from the repository and is thus very time-consuming. You cannot use\n``--find-lost-archives`` with ``--repository-only``.\n\nAbout repair mode\n+++++++++++++++++\n\nThe check command is a read-only task by default. If any corruption is found,\nBorg will report the issue and proceed with checking. To actually repair the\nissues found, pass ``--repair``.\n\n.. note::\n\n    ``--repair`` is a **POTENTIALLY DANGEROUS FEATURE** and might lead to data\n    loss! This does not just include data that was previously lost anyway, but\n    might include more data for kinds of corruption it is not capable of\n    dealing with. **BE VERY CAREFUL!**\n\nPursuant to the previous warning it is also highly recommended to test the\nreliability of the hardware running Borg with stress testing software. This\nespecially includes storage and memory testers. Unreliable hardware might lead\nto additional data loss.\n\nIt is highly recommended to create a backup of your repository before running\nin repair mode (i.e. running it with ``--repair``).\n\nRepair mode will attempt to fix any corruptions found. Fixing corruptions does\nnot mean recovering lost data: Borg cannot magically restore data lost due to\ne.g. a hardware failure. Repairing a repository means sacrificing some data\nfor the sake of the repository as a whole and the remaining data. Hence it is,\nby definition, a potentially lossy task.\n\nIn practice, repair mode hooks into both the repository and archive checks:\n\n1. When checking the repository's consistency, repair mode removes corrupted\n   objects from the repository after it did a 2nd try to read them correctly.\n\n2. When checking the consistency and correctness of archives, repair mode might\n   remove whole archives from the manifest if their archive metadata chunk is\n   corrupt or lost. Borg will also report files that reference missing chunks.\n\nIf ``--repair --find-lost-archives`` is given, previously lost entries will\nbe recreated in the archive directory. This is only possible before\n``borg compact`` would remove the archives' data completely."
  },
  {
    "path": "docs/usage/common-options.rst.inc",
    "content": "-h, --help               show this help message and exit\n--critical               work on log level CRITICAL\n--error                  work on log level ERROR\n--warning                work on log level WARNING (default)\n--info, -v, --verbose    work on log level INFO\n--debug                  enable debug output, work on log level DEBUG\n--debug-topic TOPIC      enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug.<TOPIC> if TOPIC is not fully qualified.\n-p, --progress           show progress information\n--iec                    format using IEC units (1KiB = 1024B)\n--log-json               Output one JSON object per log line instead of formatted text.\n--lock-wait SECONDS      wait at most SECONDS for acquiring a repository/cache lock (default: 10).\n--show-version           show/log the borg version\n--show-rc                show/log the return code (rc)\n--umask M                set umask to M (local only, default: 0077)\n--remote-path PATH       use PATH as borg executable on the remote (default: \"borg\")\n--upload-ratelimit RATE    set network upload rate limit in kiByte/s (default: 0=unlimited)\n--upload-buffer UPLOAD_BUFFER    set network upload buffer size in MiB. (default: 0=no buffer)\n--debug-profile FILE     Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with \".pyprof\".\n--rsh RSH                Use this command to connect to the 'borg serve' process (default: 'ssh')\n--socket PATH            Use UNIX DOMAIN (IPC) socket at PATH for client/server communication with socket: protocol.\n-r REPO, --repo REPO     repository to use\n"
  },
  {
    "path": "docs/usage/compact.rst",
    "content": ".. include:: compact.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Compact segments and free repository disk space\n    $ borg compact\n\n"
  },
  {
    "path": "docs/usage/compact.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_compact:\n\nborg compact\n------------\n.. code-block:: none\n\n    borg [common options] compact [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-----------------------+-----------------------------------------+\n    | **options**                                                                                                             |\n    +-------------------------------------------------------+-----------------------+-----------------------------------------+\n    |                                                       | ``-n``, ``--dry-run`` | do not change the repository            |\n    +-------------------------------------------------------+-----------------------+-----------------------------------------+\n    |                                                       | ``-s``, ``--stats``   | print statistics (might be much slower) |\n    +-------------------------------------------------------+-----------------------+-----------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                          |\n    |                                                                                                                         |\n    | :ref:`common_options`                                                                                                   |\n    +-------------------------------------------------------+-----------------------+-----------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        -n, --dry-run    do not change the repository\n        -s, --stats     print statistics (might be much slower)\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nFree repository space by deleting unused chunks.\n\n``borg compact`` analyzes all existing archives to determine which repository\nobjects are actually used (referenced). It then deletes all unused objects\nfrom the repository to free space.\n\nUnused objects may result from:\n\n- use of ``borg delete`` or ``borg prune``\n- interrupted backups (consider retrying the backup before running compact)\n- backups of source files that encountered an I/O error mid-transfer and were skipped\n- corruption of the repository (e.g., the archives directory lost entries; see notes below)\n\nYou usually do not want to run ``borg compact`` after every write operation, but\neither regularly (e.g., once a month, possibly together with ``borg check``) or\nwhen disk space needs to be freed.\n\n**Important:**\n\nAfter compacting, it is no longer possible to use ``borg undelete`` to recover\npreviously soft-deleted archives.\n\n``borg compact`` might also delete data from archives that were \"lost\" due to\narchives directory corruption. Such archives could potentially be restored with\n``borg check --find-lost-archives [--repair]``, which is slow. You therefore\nmight not want to do that unless there are signs of lost archives (e.g., when\nseeing fatal errors when creating backups or when archives are missing in\n``borg repo-list``).\n\nWhen using the ``--stats`` option, borg will internally list all repository\nobjects to determine their existence and stored size. It will build a fresh\nchunks index from that information and cache it in the repository. For some\ntypes of repositories, this might be very slow. It will tell you the sum of\nstored object sizes, before and after compaction.\n\nWithout ``--stats``, borg will rely on the cached chunks index to determine\nexisting object IDs (but there is no stored size information in the index,\nthus it cannot compute before/after compaction size statistics)."
  },
  {
    "path": "docs/usage/completion.rst",
    "content": ".. include:: completion.rst.inc\n\nExamples\n~~~~~~~~\n\nTo activate completion in your current shell session, evaluate the output\nof this command. To enable it persistently, add the corresponding line to\nyour shell's startup file.\n\n::\n\n    # Bash (in ~/.bashrc)\n    eval \"$(borg completion bash)\"\n\n    # Zsh (in ~/.zshrc)\n    eval \"$(borg completion zsh)\"\n\n"
  },
  {
    "path": "docs/usage/completion.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_completion:\n\nborg completion\n---------------\n.. code-block:: none\n\n    borg [common options] completion [options] SHELL\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-----------+--------------------------------------------------------+\n    | **positional arguments**                                                                                                   |\n    +-------------------------------------------------------+-----------+--------------------------------------------------------+\n    |                                                       | ``SHELL`` | shell to generate completion for (one of: %(choices)s) |\n    +-------------------------------------------------------+-----------+--------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                             |\n    |                                                                                                                            |\n    | :ref:`common_options`                                                                                                      |\n    +-------------------------------------------------------+-----------+--------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    SHELL\n        shell to generate completion for (one of: %(choices)s)\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command prints a shell completion script for the given shell.\n\nPlease note that for some dynamic completions (like archive IDs), the shell\ncompletion script will call borg to query the repository. This will work best\nif that call can be made without prompting for user input, so you may want to\nset BORG_REPO and BORG_PASSPHRASE environment variables."
  },
  {
    "path": "docs/usage/create.rst",
    "content": ".. include:: create.rst.inc\n\n.. note::\n\n   Archive series and performance: In Borg 2, archives that share the same NAME form an \"archive series\".\n   The files cache is maintained per series. For best performance on repeated backups, reuse the same\n   NAME every time you run ``borg create`` for the same dataset (e.g. always use ``my-documents``).\n   Frequently changing the NAME (for example by embedding date/time like ``my-documents-2025-11-10``)\n   prevents cache reuse and forces Borg to re-scan and re-chunk files, which can make incremental\n   backups vastly slower. Only vary the NAME if you intentionally want to start a new series.\n\n   If you must vary the archive name but still want cache reuse across names, see the advanced\n   knobs described in :ref:`upgradenotes2` (``BORG_FILES_CACHE_SUFFIX`` and ``BORG_FILES_CACHE_TTL``),\n   but the recommended approach is to keep a stable NAME per series.\n\nExamples\n~~~~~~~~\n::\n\n    # Backup ~/Documents into an archive named \"my-documents\"\n    $ borg create my-documents ~/Documents\n\n    # same, but list all files as we process them\n    $ borg create --list my-documents ~/Documents\n\n    # Backup /mnt/disk/docs, but strip path prefix using the slashdot hack\n    $ borg create --repo /path/to/repo docs /mnt/disk/./docs\n\n    # Backup ~/Documents and ~/src but exclude pyc files\n    $ borg create my-files                \\\n        ~/Documents                       \\\n        ~/src                             \\\n        --exclude '*.pyc'\n\n    # Backup home directories excluding image thumbnails (i.e. only\n    # /home/<one directory>/.thumbnails is excluded, not /home/*/*/.thumbnails etc.)\n    $ borg create my-files /home --exclude 'sh:home/*/.thumbnails'\n\n    # Back up the root filesystem into an archive named \"root-archive\"\n    # Use zlib compression (good, but slow) — default is LZ4 (fast, low compression ratio)\n    $ borg create -C zlib,6 --one-file-system root-archive /\n\n    # Backup into an archive name like FQDN-root\n    $ borg create '{fqdn}-root' /\n\n    # Back up a remote host locally (\"pull\" style) using SSHFS\n    $ mkdir sshfs-mount\n    $ sshfs root@example.com:/ sshfs-mount\n    $ cd sshfs-mount\n    $ borg create example.com-root .\n    $ cd ..\n    $ fusermount -u sshfs-mount\n\n    # Make a big effort in fine-grained deduplication (big chunk management\n    # overhead, needs a lot of RAM and disk space; see the formula in the internals docs):\n    $ borg create --chunker-params buzhash,10,23,16,4095 small /smallstuff\n\n    # Backup a raw device (must not be active/in use/mounted at that time)\n    $ borg create --read-special --chunker-params fixed,4194304 my-sdx /dev/sdX\n\n    # Backup a sparse disk image (must not be active/in use/mounted at that time)\n    $ borg create --sparse --chunker-params fixed,4194304 my-disk my-disk.raw\n\n    # No compression (none)\n    $ borg create --compression none arch ~\n\n    # Super fast, low compression (lz4, default)\n    $ borg create arch ~\n\n    # Less fast, higher compression (zlib, N = 0..9)\n    $ borg create --compression zlib,N arch ~\n\n    # Even slower, even higher compression (lzma, N = 0..9)\n    $ borg create --compression lzma,N arch ~\n\n    # Only compress compressible data with lzma,N (N = 0..9)\n    $ borg create --compression auto,lzma,N arch ~\n\n    # Use the short hostname and username as the archive name\n    $ borg create '{hostname}-{user}' ~\n\n    # Back up relative paths by moving into the correct directory first\n    $ cd /home/user/Documents\n    # The root directory of the archive will be \"projectA\"\n    $ borg create 'daily-projectA' projectA\n\n    # Use external command to determine files to archive\n    # Use --paths-from-stdin with find to back up only files less than 1 MB in size\n    $ find ~ -size -1000k | borg create --paths-from-stdin small-files-only\n    # Use --paths-from-command with find to back up files from only a given user\n    $ borg create --paths-from-command joes-files -- find /srv/samba/shared -user joe\n    # Use --paths-from-shell-command with find to back up a few files from only a given user -\n    # BE VERY CAREFUL AND ONLY USE TRUSTED INPUT FOR THE SHELL COMMAND!\n    $ borg create --paths-from-shell-command some-of-joes-files -- \"find /srv/samba/shared -user joe | head\"\n    # Use --paths-from-stdin with --paths-delimiter (for example, for filenames with newlines in them)\n    $ find ~ -size -1000k -print0 | borg create \\\n        --paths-from-stdin \\\n        --paths-delimiter \"\\0\" \\\n        smallfiles-handle-newline\n\n"
  },
  {
    "path": "docs/usage/create.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_create:\n\nborg create\n-----------\n.. code-block:: none\n\n    borg [common options] create [options] NAME [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``NAME``                                          | specify the archive name                                                                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``PATH``                                          | paths to archive                                                                                                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-n``, ``--dry-run``                             | do not create a backup archive                                                                                                                                                                    |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-s``, ``--stats``                               | print statistics for the created archive                                                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--list``                                        | output a verbose list of items (files, dirs, ...)                                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--filter STATUSCHARS``                          | only display items with the given status characters (see description)                                                                                                                             |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--json``                                        | output stats as JSON. Implies ``--stats``.                                                                                                                                                        |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--stdin-name NAME``                             | use NAME in archive for stdin data (default: 'stdin')                                                                                                                                             |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--stdin-user USER``                             | set user USER in archive for stdin data (default: do not store user/uid)                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--stdin-group GROUP``                           | set group GROUP in archive for stdin data (default: do not store group/gid)                                                                                                                       |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--stdin-mode M``                                | set mode to M in archive for stdin data (default: 0660)                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--content-from-command``                        | interpret PATH as a command and store its stdout. See also the section 'Reading from stdin' below.                                                                                                |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--paths-from-stdin``                            | read DELIM-separated list of paths to back up from stdin. All control is external: it will back up all files given - no more, no less.                                                            |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--paths-from-command``                          | interpret PATH as command and treat its output as ``--paths-from-stdin``                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--paths-from-shell-command``                    | interpret PATH as shell command and treat its output as ``--paths-from-stdin``                                                                                                                    |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--paths-delimiter DELIM``                       | set path delimiter for ``--paths-from-stdin`` and ``--paths-from-command`` (default: ``\\n``)                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                                |\n    |                                                                                                                                                                                                                                                                                                               |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-e PATTERN``, ``--exclude PATTERN``             | exclude paths matching PATTERN                                                                                                                                                                    |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-from EXCLUDEFILE``                    | read exclude patterns from EXCLUDEFILE, one per line                                                                                                                                              |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--pattern PATTERN``                             | include/exclude paths matching PATTERN                                                                                                                                                            |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--patterns-from PATTERNFILE``                   | read include/exclude patterns from PATTERNFILE, one per line                                                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-caches``                              | exclude directories that contain a CACHEDIR.TAG file (https://www.bford.info/cachedir/spec.html)                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-if-present NAME``                     | exclude directories that are tagged by containing a filesystem object with the given NAME                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--keep-exclude-tags``                           | if tag objects are specified with ``--exclude-if-present``, do not omit the tag objects themselves from the backup archive                                                                        |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Filesystem options**                                                                                                                                                                                                                                                                                        |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-x``, ``--one-file-system``                     | stay in the same file system and do not store mount points of other file systems - this might behave different from your expectations, see the description below.                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--numeric-ids``                                 | only store numeric user and group identifiers                                                                                                                                                     |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--atime``                                       | do store atime into archive                                                                                                                                                                       |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noctime``                                     | do not store ctime into archive                                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--nobirthtime``                                 | do not store birthtime (creation date) into archive                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noflags``                                     | do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noacls``                                      | do not read and store ACLs into archive                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noxattrs``                                    | do not read and store xattrs into archive                                                                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--sparse``                                      | detect sparse holes in input (supported only by fixed chunker)                                                                                                                                    |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--files-cache MODE``                            | operate files cache in MODE. default: ctime,size,inode                                                                                                                                            |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--files-changed MODE``                          | specify how to detect if a file has changed during backup (ctime, mtime, disabled). default: ctime (on Windows: mtime, because ctime is file creation time there).                                |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--read-special``                                | open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files.                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive options**                                                                                                                                                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--comment COMMENT``                             | add a comment text to the archive                                                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--timestamp TIMESTAMP``                         | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--chunker-params PARAMS``                       | specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095                                                             |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the \"borg help compression\" command for details.                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--hostname HOSTNAME``                           | explicitly set hostname for the archive                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--username USERNAME``                           | explicitly set username for the archive                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--tags TAG``                                    | add tags to archive (comma-separated or multiple arguments)                                                                                                                                       |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n    PATH\n        paths to archive\n\n\n    options\n        -n, --dry-run    do not create a backup archive\n        -s, --stats     print statistics for the created archive\n        --list          output a verbose list of items (files, dirs, ...)\n        --filter STATUSCHARS    only display items with the given status characters (see description)\n        --json          output stats as JSON. Implies ``--stats``.\n        --stdin-name NAME    use NAME in archive for stdin data (default: 'stdin')\n        --stdin-user USER    set user USER in archive for stdin data (default: do not store user/uid)\n        --stdin-group GROUP    set group GROUP in archive for stdin data (default: do not store group/gid)\n        --stdin-mode M    set mode to M in archive for stdin data (default: 0660)\n        --content-from-command    interpret PATH as a command and store its stdout. See also the section 'Reading from stdin' below.\n        --paths-from-stdin    read DELIM-separated list of paths to back up from stdin. All control is external: it will back up all files given - no more, no less.\n        --paths-from-command    interpret PATH as command and treat its output as ``--paths-from-stdin``\n        --paths-from-shell-command    interpret PATH as shell command and treat its output as ``--paths-from-stdin``\n        --paths-delimiter DELIM    set path delimiter for ``--paths-from-stdin`` and ``--paths-from-command`` (default: ``\\n``) \n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --exclude-caches                  exclude directories that contain a CACHEDIR.TAG file (https://www.bford.info/cachedir/spec.html)\n        --exclude-if-present NAME         exclude directories that are tagged by containing a filesystem object with the given NAME\n        --keep-exclude-tags               if tag objects are specified with ``--exclude-if-present``, do not omit the tag objects themselves from the backup archive\n\n\n    Filesystem options\n        -x, --one-file-system     stay in the same file system and do not store mount points of other file systems - this might behave different from your expectations, see the description below.\n        --numeric-ids             only store numeric user and group identifiers\n        --atime                   do store atime into archive\n        --noctime                 do not store ctime into archive\n        --nobirthtime             do not store birthtime (creation date) into archive\n        --noflags                 do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive\n        --noacls                  do not read and store ACLs into archive\n        --noxattrs                do not read and store xattrs into archive\n        --sparse                  detect sparse holes in input (supported only by fixed chunker)\n        --files-cache MODE        operate files cache in MODE. default: ctime,size,inode\n        --files-changed MODE      specify how to detect if a file has changed during backup (ctime, mtime, disabled). default: ctime (on Windows: mtime, because ctime is file creation time there).\n        --read-special            open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files.\n\n\n    Archive options\n        --comment COMMENT                             add a comment text to the archive\n        --timestamp TIMESTAMP                         manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n        --chunker-params PARAMS                       specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095\n        -C COMPRESSION, --compression COMPRESSION     select compression algorithm, see the output of the \"borg help compression\" command for details.\n        --hostname HOSTNAME                           explicitly set hostname for the archive\n        --username USERNAME                           explicitly set username for the archive\n        --tags TAG                                    add tags to archive (comma-separated or multiple arguments)\n\n\nDescription\n~~~~~~~~~~~\n\nThis command creates a backup archive containing all files found while recursively\ntraversing all specified paths. Paths are added to the archive as they are given,\nwhich means that if relative paths are desired, the command must be run from the correct\ndirectory.\n\nThe slashdot hack in paths (recursion roots) is triggered by using ``/./``:\n``/this/gets/stripped/./this/gets/archived`` means to process that fs object, but\nstrip the prefix on the left side of ``./`` from the archived items (in this case,\n``this/gets/archived`` will be the path in the archived item).\n\nWhen specifying '-' as a path, borg will read data from standard input and create a\nfile named 'stdin' in the created archive from that data. In some cases, it is more\nappropriate to use --content-from-command. See the section *Reading from stdin*\nbelow for details.\n\nThe archive will consume almost no disk space for files or parts of files that\nhave already been stored in other archives.\n\nThe ``--tags`` option can be used to add a list of tags to the new archive.\n\nThe archive name does not need to be unique; you can and should use the same\nname for a series of archives. The unique archive identifier is its ID (hash),\nand you can abbreviate the ID as long as it is unique.\n\nIn the archive name, you may use the following placeholders:\n{now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.\n\nBackup speed is increased by not reprocessing files that are already part of\nexisting archives and were not modified. The detection of unmodified files is\ndone by comparing multiple file metadata values with previous values kept in\nthe files cache.\n\nThis comparison can operate in different modes as given by ``--files-cache``:\n\n- ctime,size,inode (default)\n- mtime,size,inode (default behaviour of borg versions older than 1.1.0rc4)\n- ctime,size (ignore the inode number)\n- mtime,size (ignore the inode number)\n- rechunk,ctime (all files are considered modified - rechunk, cache ctime)\n- rechunk,mtime (all files are considered modified - rechunk, cache mtime)\n- disabled (disable the files cache, all files considered modified - rechunk)\n\ninode number: better safety, but often unstable on network filesystems\n\nNormally, detecting file modifications will take inode information into\nconsideration to improve the reliability of file change detection.\nThis is problematic for files located on sshfs and similar network file\nsystems which do not provide stable inode numbers, such files will always\nbe considered modified. You can use modes without `inode` in this case to\nimprove performance, but reliability of change detection might be reduced.\n\nctime vs. mtime: safety vs. speed\n\n- ctime is a rather safe way to detect changes to a file (metadata and contents)\n  as it cannot be set from userspace. But a metadata-only change will already\n  update the ctime, so there might be some unnecessary chunking/hashing even\n  without content changes. Some filesystems do not support ctime (change time).\n  E.g. doing a chown or chmod to a file will change its ctime.\n- mtime usually works and only updates if file contents were changed. But mtime\n  can be arbitrarily set from userspace, e.g., to set mtime back to the same value\n  it had before a content change happened. This can be used maliciously as well as\n  well-meant, but in both cases mtime-based cache modes can be problematic.\n\nThe ``--files-changed`` option controls how Borg detects if a file has changed during backup:\n - ctime (default on POSIX): Use ctime to detect changes. This is the safest option.\n   Not supported on Windows (ctime is file creation time there).\n - mtime (default on Windows): Use mtime to detect changes.\n - disabled: Disable the \"file has changed while we backed it up\" detection completely.\n   This is not recommended unless you know what you're doing, as it could lead to\n   inconsistent backups if files change during the backup process.\n\nThe mount points of filesystems or filesystem snapshots should be the same for every\ncreation of a new archive to ensure fast operation. This is because the file cache that\nis used to determine changed files quickly uses absolute filenames.\nIf this is not possible, consider creating a bind mount to a stable location.\n\nThe ``--progress`` option shows (from left to right) Original and (uncompressed)\ndeduplicated size (O and U respectively), then the Number of files (N) processed so far,\nfollowed by the currently processed path.\n\nWhen using ``--stats``, you will get some statistics about how much data was\nadded - the \"This Archive\" deduplicated size there is most interesting as that is\nhow much your repository will grow. Please note that the \"All archives\" stats refer to\nthe state after creation. Also, the ``--stats`` and ``--dry-run`` options are mutually\nexclusive because the data is not actually compressed and deduplicated during a dry run.\n\nFor more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\nFor more help on placeholders, see the :ref:`borg_placeholders` command output.\n\n.. man NOTES\n\nThe ``--exclude`` patterns are not like tar. In tar ``--exclude`` .bundler/gems will\nexclude foo/.bundler/gems. In borg it will not, you need to use ``--exclude``\n'\\*/.bundler/gems' to get the same effect.\n\nIn addition to using ``--exclude`` patterns, it is possible to use\n``--exclude-if-present`` to specify the name of a filesystem object (e.g. a file\nor folder name) which, when contained within another folder, will prevent the\ncontaining folder from being backed up.  By default, the containing folder and\nall of its contents will be omitted from the backup.  If, however, you wish to\nonly include the objects specified by ``--exclude-if-present`` in your backup,\nand not include any other contents of the containing folder, this can be enabled\nthrough using the ``--keep-exclude-tags`` option.\n\nThe ``-x`` or ``--one-file-system`` option excludes directories, that are mountpoints (and everything in them).\nIt detects mountpoints by comparing the device number from the output of ``stat()`` of the directory and its\nparent directory. Specifically, it excludes directories for which ``stat()`` reports a device number different\nfrom the device number of their parent.\nIn general: be aware that there are directories with device number different from their parent, which the kernel\ndoes not consider a mountpoint and also the other way around.\nLinux examples for this are bind mounts (possibly same device number, but always a mountpoint) and ALL\nsubvolumes of a btrfs (different device number from parent but not necessarily a mountpoint).\nmacOS examples are the apfs mounts of a typical macOS installation.\nTherefore, when using ``--one-file-system``, you should double-check that the backup works as intended.\n\n.. _list_item_flags:\n\nItem flags\n++++++++++\n\n``--list`` outputs a list of all files, directories and other\nfile system items it considered (no matter whether they had content changes\nor not). For each item, it prefixes a single-letter flag that indicates type\nand/or status of the item.\n\nIf you are interested only in a subset of that output, you can give e.g.\n``--filter=AME`` and it will only show regular files with A, M or E status (see\nbelow).\n\nA uppercase character represents the status of a regular file relative to the\n\"files\" cache (not relative to the repo -- this is an issue if the files cache\nis not used). Metadata is stored in any case and for 'A' and 'M' also new data\nchunks are stored. For 'U' all data chunks refer to already existing chunks.\n\n- 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ)\n- 'M' = regular file, modified\n- 'U' = regular file, unchanged\n- 'C' = regular file, it changed while we backed it up\n- 'E' = regular file, an error happened while accessing/reading *this* file\n\nA lowercase character means a file type other than a regular file,\nborg usually just stores their metadata:\n\n- 'd' = directory\n- 'b' = block device\n- 'c' = char device\n- 'h' = regular file, hard link (to already seen inodes)\n- 's' = symlink\n- 'f' = fifo\n\nOther flags used include:\n\n- '+' = included, item would be backed up (if not in dry-run mode)\n- '-' = excluded, item would not be / was not backed up\n- 'i' = backup data was read from standard input (stdin)\n- '?' = missing status code (if you see this, please file a bug report!)\n\nReading backup data from stdin\n++++++++++++++++++++++++++++++\n\nThere are two methods to read from stdin. Either specify ``-`` as path and\npipe directly to borg::\n\n    backup-vm --id myvm --stdout | borg create --repo REPO ARCHIVE -\n\nOr use ``--content-from-command`` to have Borg manage the execution of the\ncommand and piping. If you do so, the first PATH argument is interpreted\nas command to execute and any further arguments are treated as arguments\nto the command::\n\n    borg create --content-from-command --repo REPO ARCHIVE -- backup-vm --id myvm --stdout\n\n``--`` is used to ensure ``--id`` and ``--stdout`` are **not** considered\narguments to ``borg`` but rather ``backup-vm``.\n\nThe difference between the two approaches is that piping to borg creates an\narchive even if the command piping to borg exits with a failure. In this case,\n**one can end up with truncated output being backed up**. Using\n``--content-from-command``, in contrast, borg is guaranteed to fail without\ncreating an archive should the command fail. The command is considered failed\nwhen it returned a non-zero exit code.\n\nReading from stdin yields just a stream of data without file metadata\nassociated with it, and the files cache is not needed at all. So it is\nsafe to disable it via ``--files-cache disabled`` and speed up backup\ncreation a bit.\n\nBy default, the content read from stdin is stored in a file called 'stdin'.\nUse ``--stdin-name`` to change the name.\n\nFeeding all file paths from externally\n++++++++++++++++++++++++++++++++++++++\n\nUsually, you give a starting path (recursion root) to borg and then borg\nautomatically recurses, finds and backs up all fs objects contained in\nthere (optionally considering include/exclude rules).\n\nIf you need more control and you want to give every single fs object path\nto borg (maybe implementing your own recursion or your own rules), you can use\n``--paths-from-stdin``, ``--paths-from-command`` or ``--paths-from-shell-command``\n(with the latter two, borg will fail to create an archive should the command fail).\n\nBorg supports paths with the slashdot hack to strip path prefixes here also.\nSo, be careful not to unintentionally trigger that."
  },
  {
    "path": "docs/usage/debug.rst",
    "content": "Debugging Facilities\n--------------------\n\nThere is a ``borg debug`` command that has some subcommands which are all\n**not intended for normal use** and **potentially very dangerous** if used incorrectly.\n\nFor example, ``borg debug put-obj`` and ``borg debug delete-obj`` will only do\nwhat their name suggests: put objects into the repository / delete objects from the repository.\n\nPlease note:\n\n- they will not update the chunks cache (chunks index) about the object\n- they will not update the manifest (so no automatic chunks index resync is triggered)\n- they will not check whether the object is in use (e.g. before delete-obj)\n- they will not update any metadata which may point to the object\n\nThey exist to improve debugging capabilities without direct system access, e.g.\nin case you ever run into some severe malfunction. Use them only if you know\nwhat you are doing or if a trusted Borg developer tells you what to do.\n\nBorg has a ``--debug-topic TOPIC`` option to enable specific debugging messages. Topics\nare generally not documented.\n\nA ``--debug-profile FILE`` option exists which writes a profile of the main program's\nexecution to a file. The format of these files is not directly compatible with the\nPython profiling tools, since these use the \"marshal\" format, which is not intended\nto be secure (quoting the Python docs: \"Never unmarshal data received from an untrusted\nor unauthenticated source.\").\n\nThe ``borg debug profile-convert`` command can be used to take a Borg profile and convert\nit to a profile file that is compatible with the Python tools.\n\nAdditionally, if the filename specified for ``--debug-profile`` ends with \".pyprof\", a\nPython-compatible profile is generated. This is only intended for local use by developers.\n"
  },
  {
    "path": "docs/usage/delete.rst",
    "content": ".. include:: delete.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Delete all backup archives named \"kenny-files\":\n    $ borg delete -a kenny-files\n    # Actually free disk space:\n    $ borg compact\n\n    # Delete a specific backup archive using its unique archive ID prefix\n    $ borg delete aid:d34db33f\n\n    # Delete all archives whose names begin with the machine's hostname followed by \"-\"\n    $ borg delete -a 'sh:{hostname}-*'\n\n    # Delete all archives whose names contain \"-2012-\"\n    $ borg delete -a 'sh:*-2012-*'\n\n    # See what would be deleted if delete was run without --dry-run\n    $ borg delete --list --dry-run -a 'sh:*-May-*'\n\n"
  },
  {
    "path": "docs/usage/delete.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_delete:\n\nborg delete\n-----------\n.. code-block:: none\n\n    borg [common options] delete [options] [NAME]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``NAME``                                     | specify the archive name                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-n``, ``--dry-run``                        | do not change the repository                                                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list``                                   | output a verbose list of archives                                                                                           |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n\n\n    options\n        -n, --dry-run     do not change the repository\n        --list            output a verbose list of archives\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command soft-deletes archives from the repository.\n\nImportant:\n\n- The delete command will only mark archives for deletion (\"soft-deletion\"),\n  repository disk space is **not** freed until you run ``borg compact``.\n- You can use ``borg undelete`` to undelete archives, but only until\n  you run ``borg compact``.\n\nWhen in doubt, use ``--dry-run --list`` to see what would be deleted.\n\nYou can delete multiple archives by specifying a match pattern using\nthe ``--match-archives PATTERN`` option (for more information on these\npatterns, see :ref:`borg_patterns`)."
  },
  {
    "path": "docs/usage/diff.rst",
    "content": ".. include:: diff.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg diff archive1 archive2\n        +17 B      -5 B [-rw-r--r-- -> -rwxr-xr-x] file1\n       +135 B    -252 B file2\n    added           0 B file4\n    removed         0 B file3\n\n    $ borg diff archive1 archive2\n    {\"path\": \"file1\", \"changes\": [{\"type\": \"modified\", \"added\": 17, \"removed\": 5}, {\"type\": \"mode\", \"old_mode\": \"-rw-r--r--\", \"new_mode\": \"-rwxr-xr-x\"}]}\n    {\"path\": \"file2\", \"changes\": [{\"type\": \"modified\", \"added\": 135, \"removed\": 252}]}\n    {\"path\": \"file4\", \"changes\": [{\"type\": \"added\", \"size\": 0}]}\n    {\"path\": \"file3\", \"changes\": [{\"type\": \"removed\", \"size\": 0}]}\n\n\n    # Use --sort-by with a comma-separated list; sorts apply stably from last to first.\n    # Here: primary by net size change descending, tie-breaker by path ascending\n    $ borg diff --sort-by=\">size_diff,path\" archive1 archive2\n        +17 B      -5 B [-rw-r--r-- -> -rwxr-xr-x] file1\n    removed         0 B file3\n    added           0 B file4\n       +135 B    -252 B file2\n"
  },
  {
    "path": "docs/usage/diff.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_diff:\n\nborg diff\n---------\n.. code-block:: none\n\n    borg [common options] diff [options] ARCHIVE1 ARCHIVE2 [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``ARCHIVE1``                          | ARCHIVE1 name                                                                    |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``ARCHIVE2``                          | ARCHIVE2 name                                                                    |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``PATH``                              | paths of items inside the archives to compare; patterns are supported.           |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--numeric-ids``                     | only consider numeric user and group identifiers                                 |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--same-chunker-params``             | override the check of chunker parameters                                         |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--format FORMAT``                   | specify format for differences between archives (default: \"{change} {path}{NL}\") |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--json-lines``                      | Format output as JSON Lines.                                                     |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--sort-by``                         | Sort output by comma-separated fields (e.g., '>size_added,path').                |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--content-only``                    | Only compare differences in content (exclude metadata differences)               |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                   |\n    |                                                                                                                                                                                  |\n    | :ref:`common_options`                                                                                                                                                            |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN                                                   |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-from EXCLUDEFILE``        | read exclude patterns from EXCLUDEFILE, one per line                             |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--pattern PATTERN``                 | include/exclude paths matching PATTERN                                           |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n    |                                                       | ``--patterns-from PATTERNFILE``       | read include/exclude patterns from PATTERNFILE, one per line                     |\n    +-------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    ARCHIVE1\n        ARCHIVE1 name\n    ARCHIVE2\n        ARCHIVE2 name\n    PATH\n        paths of items inside the archives to compare; patterns are supported.\n\n\n    options\n        --numeric-ids    only consider numeric user and group identifiers\n        --same-chunker-params    override the check of chunker parameters\n        --format FORMAT    specify format for differences between archives (default: \"{change} {path}{NL}\")\n        --json-lines    Format output as JSON Lines.\n        --sort-by     Sort output by comma-separated fields (e.g., '>size_added,path').\n        --content-only    Only compare differences in content (exclude metadata differences)\n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n\n\nDescription\n~~~~~~~~~~~\n\nThis command finds differences (file contents, metadata) between ARCHIVE1 and ARCHIVE2.\n\nFor more help on include/exclude patterns, see the output of the :ref:`borg_patterns` command.\n\n.. man NOTES\n\nThe FORMAT specifier syntax\n+++++++++++++++++++++++++++\n\nThe ``--format`` option uses Python's `format string syntax\n<https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\nExamples:\n::\n\n    $ borg diff --format '{content:30} {path}{NL}' ArchiveFoo ArchiveBar\n    modified:  +4.1 kB  -1.0 kB    file-diff\n    ...\n\n    # {VAR:<NUMBER} - pad to NUMBER columns left-aligned.\n    # {VAR:>NUMBER} - pad to NUMBER columns right-aligned.\n    $ borg diff --format '{content:>30} {path}{NL}' ArchiveFoo ArchiveBar\n       modified:  +4.1 kB  -1.0 kB file-diff\n    ...\n\nThe following keys are always available:\n- NEWLINE: OS dependent line separator\n- NL: alias of NEWLINE\n- NUL: NUL character for creating print0 / xargs -0 like output\n- SPACE: space character\n- TAB: tab character\n- CR: carriage return character\n- LF: line feed character\n\n\nKeys available only when showing differences between archives:\n\n- path: archived file path\n- change: all available changes\n\n- content: file content change\n- mode: file mode change\n- type: file type change\n- owner: file owner (user/group) change\n- group: file group change\n- user: file user change\n\n- link: file link change\n- directory: file directory change\n- blkdev: file block device change\n- chrdev: file character device change\n- fifo: file fifo change\n\n- mtime: file modification time change\n- ctime: file change time change\n- isomtime: file modification time change (ISO 8601)\n- isoctime: file creation time change (ISO 8601)\n\n\nWhat is compared\n+++++++++++++++++\nFor each matching item in both archives, Borg reports:\n\n- Content changes: total added/removed bytes within files. If chunker parameters are comparable,\n  Borg compares chunk IDs quickly; otherwise, it compares the content.\n- Metadata changes: user, group, mode, and other metadata shown inline, like\n  \"[old_mode -> new_mode]\" for mode changes. Use ``--content-only`` to suppress metadata changes.\n- Added/removed items: printed as \"added SIZE path\" or \"removed SIZE path\".\n\nOutput formats\n++++++++++++++\nThe default (text) output shows one line per changed path, e.g.::\n\n    +135 B    -252 B [ -rw-r--r-- -> -rwxr-xr-x ] path/to/file\n\nJSON Lines output (``--json-lines``) prints one JSON object per changed path, e.g.::\n\n    {\"path\": \"PATH\", \"changes\": [\n        {\"type\": \"modified\", \"added\": BYTES, \"removed\": BYTES},\n        {\"type\": \"mode\", \"old_mode\": \"-rw-r--r--\", \"new_mode\": \"-rwxr-xr-x\"},\n        {\"type\": \"added\", \"size\": SIZE},\n        {\"type\": \"removed\", \"size\": SIZE}\n    ]}\n\nSorting\n++++++++\nUse ``--sort-by FIELDS`` where FIELDS is a comma-separated list of fields.\nSorts are applied stably from last to first in the given list. Prepend \">\" for\ndescending, \"<\" (or no prefix) for ascending, for example ``--sort-by=\">size_added,path\"``.\nSupported fields include:\n\n- path: the item path\n- size_added: total bytes added for the item content\n- size_removed: total bytes removed for the item content\n- size_diff: size_added - size_removed (net content change)\n- size: size of the item as stored in ARCHIVE2 (0 for removed items)\n- user, group, uid, gid, ctime, mtime: taken from the item state in ARCHIVE2 when present\n- ctime_diff, mtime_diff: timestamp difference (ARCHIVE2 - ARCHIVE1)\n\nPerformance considerations\n++++++++++++++++++++++++++\ndiff automatically detects whether the archives were created with the same chunker\nparameters. If so, only chunk IDs are compared, which is very fast.\n"
  },
  {
    "path": "docs/usage/export-tar.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_export-tar:\n\nborg export-tar\n---------------\n.. code-block:: none\n\n    borg [common options] export-tar [options] NAME FILE [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``NAME``                              | specify the archive name                                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``FILE``                              | output tar file. \"-\" to write to stdout instead.                                                          |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``PATH``                              | paths to extract; patterns are supported                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--tar-filter``                      | filter program to pipe data through                                                                       |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--list``                            | output verbose list of items (files, dirs, ...)                                                           |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--tar-format FMT``                  | select tar format: BORG, PAX or GNU                                                                       |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                            |\n    |                                                                                                                                                                                                           |\n    | :ref:`common_options`                                                                                                                                                                                     |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN                                                                            |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-from EXCLUDEFILE``        | read exclude patterns from EXCLUDEFILE, one per line                                                      |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--pattern PATTERN``                 | include/exclude paths matching PATTERN                                                                    |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--patterns-from PATTERNFILE``       | read include/exclude patterns from PATTERNFILE, one per line                                              |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--strip-components NUMBER``         | Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped. |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n    FILE\n        output tar file. \"-\" to write to stdout instead.\n    PATH\n        paths to extract; patterns are supported\n\n\n    options\n        --tar-filter         filter program to pipe data through\n        --list               output verbose list of items (files, dirs, ...)\n        --tar-format FMT     select tar format: BORG, PAX or GNU\n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --strip-components NUMBER         Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command creates a tarball from an archive.\n\nWhen giving '-' as the output FILE, Borg will write a tar stream to standard output.\n\nBy default (``--tar-filter=auto``) Borg will detect whether the FILE should be compressed\nbased on its file extension and pipe the tarball through an appropriate filter\nbefore writing it to FILE:\n\n- .tar.gz or .tgz: gzip\n- .tar.bz2 or .tbz: bzip2\n- .tar.xz or .txz: xz\n- .tar.zstd or .tar.zst: zstd\n- .tar.lz4: lz4\n\nAlternatively, a ``--tar-filter`` program may be explicitly specified. It should\nread the uncompressed tar stream from stdin and write a compressed/filtered\ntar stream to stdout.\n\nDepending on the ``--tar-format`` option, these formats are created:\n\n+--------------+---------------------------+----------------------------+\n| --tar-format | Specification             | Metadata                   |\n+--------------+---------------------------+----------------------------+\n| BORG         | BORG specific, like PAX   | all as supported by borg   |\n+--------------+---------------------------+----------------------------+\n| PAX          | POSIX.1-2001 (pax) format | GNU + atime/ctime/mtime ns |\n|              |                           | + xattrs                   |\n+--------------+---------------------------+----------------------------+\n| GNU          | GNU tar format            | mtime s, no atime/ctime,   |\n|              |                           | no ACLs/xattrs/bsdflags    |\n+--------------+---------------------------+----------------------------+\n\nA ``--sparse`` option (as found in borg extract) is not supported.\n\nBy default the entire archive is extracted but a subset of files and directories\ncan be selected by passing a list of ``PATHs`` as arguments.\nThe file selection can further be restricted by using the ``--exclude`` option.\n\nFor more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\n``--progress`` can be slower than no progress display, since it makes one additional\npass over the archive metadata."
  },
  {
    "path": "docs/usage/extract.rst",
    "content": ".. include:: extract.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Extract entire archive\n    $ borg extract my-files\n\n    # Extract entire archive and list files while processing\n    $ borg extract --list my-files\n\n    # Verify whether an archive could be successfully extracted, but do not write files to disk\n    $ borg extract --dry-run my-files\n\n    # Extract the \"src\" directory\n    $ borg extract my-files home/USERNAME/src\n\n    # Extract the \"src\" directory but exclude object files\n    $ borg extract my-files home/USERNAME/src --exclude '*.o'\n\n    # Extract only the C files\n    $ borg extract my-files 'sh:home/USERNAME/src/*.c'\n\n    # Restore a raw device (must not be active/in use/mounted at that time)\n    $ borg extract --stdout my-sdx | dd of=/dev/sdx bs=10M\n\n"
  },
  {
    "path": "docs/usage/extract.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_extract:\n\nborg extract\n------------\n.. code-block:: none\n\n    borg [common options] extract [options] NAME [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``NAME``                              | specify the archive name                                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``PATH``                              | paths to extract; patterns are supported                                                                  |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--list``                            | output a verbose list of items (files, dirs, ...)                                                         |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-n``, ``--dry-run``                 | do not actually change any files                                                                          |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--numeric-ids``                     | only use numeric user and group identifiers                                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noflags``                         | do not extract/set flags (e.g. NODUMP, IMMUTABLE)                                                         |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noacls``                          | do not extract/set ACLs                                                                                   |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--noxattrs``                        | do not extract/set xattrs                                                                                 |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--stdout``                          | write all extracted data to stdout                                                                        |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--sparse``                          | create holes in the output sparse file from all-zero chunks                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--continue``                        | continue a previously interrupted extraction of the same archive                                          |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                            |\n    |                                                                                                                                                                                                           |\n    | :ref:`common_options`                                                                                                                                                                                     |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN                                                                            |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-from EXCLUDEFILE``        | read exclude patterns from EXCLUDEFILE, one per line                                                      |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--pattern PATTERN``                 | include/exclude paths matching PATTERN                                                                    |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--patterns-from PATTERNFILE``       | read include/exclude patterns from PATTERNFILE, one per line                                              |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--strip-components NUMBER``         | Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped. |\n    +-------------------------------------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n    PATH\n        paths to extract; patterns are supported\n\n\n    options\n        --list            output a verbose list of items (files, dirs, ...)\n        -n, --dry-run     do not actually change any files\n        --numeric-ids     only use numeric user and group identifiers\n        --noflags         do not extract/set flags (e.g. NODUMP, IMMUTABLE)\n        --noacls          do not extract/set ACLs\n        --noxattrs        do not extract/set xattrs\n        --stdout          write all extracted data to stdout\n        --sparse          create holes in the output sparse file from all-zero chunks\n        --continue        continue a previously interrupted extraction of the same archive\n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --strip-components NUMBER         Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command extracts the contents of an archive.\n\nBy default, the entire archive is extracted, but a subset of files and directories\ncan be selected by passing a list of ``PATH`` arguments. The default interpretation\nfor the paths to extract is `pp:` which is a literal path-prefix match. If you want\nto use e.g. a wildcard, you must select a different pattern style such as `sh:` or\n`fm:`. See :ref:`borg_patterns` for more information.\n\nThe file selection can be further restricted by using the ``--exclude`` option.\nFor more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\nBy using ``--dry-run``, you can do all extraction steps except actually writing the\noutput data: reading metadata and data chunks from the repository, checking the hash/HMAC,\ndecrypting, and decompressing.\n\n``--progress`` can be slower than no progress display, since it makes one additional\npass over the archive metadata.\n\n.. note::\n\n    Currently, extract always writes into the current working directory (\".\"),\n    so make sure you ``cd`` to the right place before calling ``borg extract``.\n\n    When parent directories are not extracted (because of using file/directory selection\n    or any other reason), Borg cannot restore parent directories' metadata, e.g., owner,\n    group, permissions, etc."
  },
  {
    "path": "docs/usage/general/archive-specification.rst.inc",
    "content": ".. _archive_specification:\n\nSpecifying an archive\n~~~~~~~~~~~~~~~~~~~~~\n\nHow to refer to an archive depends on whether you use archive series or not.\n\n**By ID:** if you use archive series, many or all archives will have the same name, thus\nyou need to refer to a single archive by its archive ID (see ``borg repo-list``\noutput)::\n\n    borg info aid:f7dea078\n\nThe ``aid:`` prefix does a prefix match on the archive ID (the hex representation\nof the archive fingerprint). You only need to give enough hex digits to uniquely\nidentify the archive. This is useful when archive names are ambiguous or when\nyou want to refer to an archive by its immutable ID.\n\n**By name:** if you don't use archive series, but do it old-style by giving every archive\na unique name, you can refer to an archive by its name::\n\n    borg info my-backup-202512312359\n\nFor more details on archive matching patterns (including shell-style globs,\nregular expressions, and matching by user/host/tags), see :ref:`borg_match-archives`.\n"
  },
  {
    "path": "docs/usage/general/config.rst.inc",
    "content": "Configuration Precedence\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n From lowest to highest:\n\n    1. Defaults defined in the source code.\n    2. Default config file (``$BORG_CONFIG_DIR/default.yaml``).\n    3. ``--config`` file(s) (in the order given).\n    4. Full config environment variable: (``BORG_CONFIG``).\n    5. Environment variables (e.g. ``BORG_LOG_LEVEL``).\n    6. Command-line arguments in order left to right (might include config files).\n\nConfiguration files\n~~~~~~~~~~~~~~~~~~~\n\nBorg supports reading options from YAML configuration files.  This is\nimplemented via `jsonargparse <https://jsonargparse.readthedocs.io/>`_\nand works for all options that can also be set on the command line.\n\nDefault configuration file\n    ``$BORG_CONFIG_DIR/default.yaml`` is loaded automatically on every Borg\n    invocation if it exists. You do not need to pass ``--config`` explicitly\n    for this file.\n\n``--config PATH``\n    Load additional options from the YAML file at *PATH*.\n    Options in this file take precedence over the default config file but are\n    overridden by explicit command-line arguments. This option can be used\n    multiple times, with later files overriding earlier ones.\n\n``--print_config``\n    Print the current effective configuration (all options in YAML format) to\n    stdout and exit. This reflects the merged result of the default config\n    file, any ``--config`` file, environment variables, and command-line\n    arguments given before ``--print_config``. The output can be used as a\n    starting point for a config file.\n\nFile format\n    Config files are YAML documents. Top-level keys are option names\n    (without leading ``--`` and with ``-`` replaced by ``_``).\n    Nested keys correspond to subcommands.\n\n    Example ``default.yaml``::\n\n        # apply to all borg commands:\n        log_level: info\n        show_rc: true\n\n        # options specific to \"borg create\":\n        create:\n            compression: zstd,3\n            stats: true\n\n    The top-level keys set options that are common to all commands (equivalent\n    to placing them before the subcommand on the command line).  Keys nested\n    under a subcommand name (e.g. ``create:``) are only applied when that\n    subcommand is invoked.\n\n.. note::\n    ``--print_config`` shows the merged effective configuration and is a\n    convenient way to check what values Borg will actually use, and to\n    generate contents for your borg config file(s)::\n\n        borg --repo /backup/main create --compression zstd,3 --print_config\n"
  },
  {
    "path": "docs/usage/general/date-time.rst.inc",
    "content": "Date and Time\n~~~~~~~~~~~~~\n\nWe format date and time in accordance with ISO 8601, that is: YYYY-MM-DD and\nHH:MM:SS (24-hour clock).\n\nFor more information, see: https://xkcd.com/1179/\n\nUnless otherwise noted, we display local date and time.\nInternally, we store and process date and time as UTC.\n\n\n.. rubric:: TIMESPAN\n\nSome options accept a TIMESPAN parameter, which can be given as a number of\nyears (e.g. ``2y``), months (e.g. ``12m``), weeks (e.g. ``2w``),\ndays (e.g. ``7d``), hours (e.g. ``8H``), minutes (e.g. ``30M``),\nor seconds (e.g. ``150S``).\n"
  },
  {
    "path": "docs/usage/general/environment.rst.inc",
    "content": "Environment Variables\n~~~~~~~~~~~~~~~~~~~~~\n\nBorg uses some environment variables for automation:\n\nGeneral:\n    BORG_REPO\n        When set, use the value to give the default repository location.\n        Use this so you do not need to type ``--repo /path/to/my/repo`` all the time.\n    BORG_OTHER_REPO\n        Similar to BORG_REPO, but gives the default for ``--other-repo``.\n    BORG_PASSPHRASE (and BORG_OTHER_PASSPHRASE)\n        When set, use the value to answer the passphrase question for encrypted repositories.\n        It is used when a passphrase is needed to access an encrypted repo as well as when a new\n        passphrase should be initially set when initializing an encrypted repo.\n        See also BORG_NEW_PASSPHRASE.\n    BORG_PASSCOMMAND (and BORG_OTHER_PASSCOMMAND)\n        When set, use the standard output of the command (trailing newlines are stripped) to answer the\n        passphrase question for encrypted repositories.\n        It is used when a passphrase is needed to access an encrypted repo as well as when a new\n        passphrase should be initially set when initializing an encrypted repo. Note that the command\n        is executed without a shell. So variables, like ``$HOME`` will work, but ``~`` won't.\n        If BORG_PASSPHRASE is also set, it takes precedence.\n        See also BORG_NEW_PASSPHRASE.\n    BORG_PASSPHRASE_FD (and BORG_OTHER_PASSPHRASE_FD)\n        When set, specifies a file descriptor to read a passphrase\n        from. Programs starting borg may choose to open an anonymous pipe\n        and use it to pass a passphrase. This is safer than passing via\n        BORG_PASSPHRASE, because on some systems (e.g. Linux) environment\n        can be examined by other processes.\n        If BORG_PASSPHRASE or BORG_PASSCOMMAND are also set, they take precedence.\n    BORG_NEW_PASSPHRASE\n        When set, use the value to answer the passphrase question when a **new** passphrase is asked for.\n        This variable is checked first. If it is not set, BORG_PASSPHRASE and BORG_PASSCOMMAND will also\n        be checked.\n        Main use case for this is to fully automate ``borg change-passphrase``.\n    BORG_DISPLAY_PASSPHRASE\n        When set, use the value to answer the \"display the passphrase for verification\" question when defining a new passphrase for encrypted repositories.\n    BORG_DEBUG_PASSPHRASE\n        When set to YES, display debugging information that includes passphrases used and passphrase related env vars set.\n    BORG_EXIT_CODES\n        When set to \"modern\", the borg process will return more specific exit codes (rc).\n        When set to \"legacy\", the borg process will return rc 2 for all errors, 1 for all warnings, 0 for success.\n        Default is \"modern\".\n    BORG_HOST_ID\n        Borg usually computes a host id from the FQDN plus the results of ``uuid.getnode()`` (which usually returns\n        a unique id based on the MAC address of the network interface. Except if that MAC happens to be all-zero - in\n        that case it returns a random value, which is not what we want (because it kills automatic stale lock removal).\n        So, if you have an all-zero MAC address or other reasons to better control the host id externally, just set this\n        environment variable to a unique value. If all your FQDNs are unique, you can just use the FQDN. If not,\n        use FQDN@uniqueid.\n    BORG_LOCK_WAIT\n        You can set the default value for the ``--lock-wait`` option with this, so\n        you do not need to give it as a command line option.\n    BORG_LOGGING_CONF\n        When set, use the given filename as INI_-style logging configuration.\n        A basic example conf can be found at ``docs/misc/logging.conf``.\n    BORG_RSH\n        When set, use this command instead of ``ssh``. This can be used to specify ssh options, such as\n        a custom identity file ``ssh -i /path/to/private/key``. See ``man ssh`` for other options. Using\n        the ``--rsh CMD`` command line option overrides the environment variable.\n    BORG_REMOTE_PATH\n        When set, use the given path as borg executable on the remote (defaults to \"borg\" if unset).\n        Using the ``--remote-path PATH`` command line option overrides the environment variable.\n    BORG_REPO_PERMISSIONS\n        Set repository permissions, see also: :ref:`borg_serve`\n    BORG_FILES_CACHE_SUFFIX\n        When set to a value at least one character long, instructs borg to use a specifically named\n        (based on the suffix) alternative files cache. This can be used to avoid loading and saving\n        cache entries for backup sources other than the current sources.\n    BORG_FILES_CACHE_TTL\n        When set to a numeric value, this determines the maximum \"time to live\" for the files cache\n        entries (default: 2). The files cache is used to determine quickly whether a file is unchanged.\n    BORG_USE_CHUNKS_ARCHIVE\n        When set to no (default: yes), the ``chunks.archive.d`` folder will not be used. This reduces\n        disk space usage but slows down cache resyncs.\n    BORG_SHOW_SYSINFO\n        When set to no (default: yes), system information (like OS, Python version, ...) in\n        exceptions is not shown.\n        Please only use for good reasons as it makes issues harder to analyze.\n    BORG_MSGPACK_VERSION_CHECK\n        Controls whether Borg checks the ``msgpack`` version.\n        The default is ``yes`` (strict check). Set to ``no`` to disable the version check and\n        allow any installed ``msgpack`` version. Use this at your own risk; malfunctioning or\n        incompatible ``msgpack`` versions may cause subtle bugs or repository data corruption.\n    BORG_FUSE_IMPL\n        Choose the low-level FUSE implementation borg shall use for ``borg mount``.\n        This is a comma-separated list of implementation names, they are tried in the\n        given order, e.g.:\n\n        - ``mfusepy,pyfuse3,llfuse``: default, first try to load mfusepy, then pyfuse3, then llfuse.\n        - ``llfuse,pyfuse3``: first try to load llfuse, then try to load pyfuse3.\n        - ``mfusepy``: only try to load mfusepy\n        - ``pyfuse3``: only try to load pyfuse3\n        - ``llfuse``: only try to load llfuse\n        - ``none``: do not try to load an implementation\n    BORG_SELFTEST\n        This can be used to influence borg's built-in self-tests. The default is to execute the tests\n        at the beginning of each borg command invocation.\n\n        BORG_SELFTEST=disabled can be used to switch off the tests and rather save some time.\n        Disabling is not recommended for normal borg users, but large scale borg storage providers can\n        use this to optimize production servers after at least doing a one-time test borg (with\n        self-tests not disabled) when installing or upgrading machines/OS/Borg.\n    BORG_WORKAROUNDS\n        A list of comma-separated strings that trigger workarounds in borg,\n        e.g. to work around bugs in other software.\n\n        Currently known strings are:\n\n        basesyncfile\n            Use the more simple BaseSyncFile code to avoid issues with sync_file_range.\n            You might need this to run borg on WSL (Windows Subsystem for Linux) or\n            in systemd.nspawn containers on some architectures (e.g. ARM).\n            Using this does not affect data safety, but might result in a more bursty\n            write-to-disk behavior (not continuously streaming to disk).\n\n        retry_erofs\n            Retry opening a file without O_NOATIME if opening a file with O_NOATIME\n            caused EROFS. You will need this to make archives from volume shadow copies\n            in WSL1 (Windows Subsystem for Linux 1).\n\n        authenticated_no_key\n            Work around a lost passphrase or key for an ``authenticated`` mode repository\n            (these are only authenticated, but not encrypted).\n            If the key is missing in the repository config, add ``key = anything`` there.\n\n            This workaround is **only** for emergencies and **only** to extract data\n            from an affected repository (read-only access)::\n\n                BORG_WORKAROUNDS=authenticated_no_key borg extract --repo repo archive\n\n            After you have extracted all data you need, you MUST delete the repository::\n\n                BORG_WORKAROUNDS=authenticated_no_key borg delete repo\n\n            Now you can init a fresh repo. Make sure you do not use the workaround any more.\n\nOutput formatting:\n    BORG_LIST_FORMAT\n        Giving the default value for ``borg list --format=X``.\n    BORG_REPO_LIST_FORMAT\n        Giving the default value for ``borg repo-list --format=X``.\n    BORG_PRUNE_FORMAT\n        Giving the default value for ``borg prune --format=X``.\n\nSome automatic \"answerers\" (if set, they automatically answer confirmation questions):\n    BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no (or =yes)\n        For \"Warning: Attempting to access a previously unknown unencrypted repository\"\n    BORG_RELOCATED_REPO_ACCESS_IS_OK=no (or =yes)\n        For \"Warning: The repository at location ... was previously located at ...\"\n    BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO (or =YES)\n        For \"This is a potentially dangerous function...\" (check --repair)\n    BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES)\n        For \"You requested to DELETE the repository completely *including* all archives it contains:\"\n\n    Note: answers are case sensitive. setting an invalid answer value might either give the default\n    answer or ask you interactively, depending on whether retries are allowed (they by default are\n    allowed). So please test your scripts interactively before making them a non-interactive script.\n\n.. _XDG env var: https://specifications.freedesktop.org/basedir-spec/0.6/ar01s03.html\n\nDirectories and files:\n\n    .. note::\n\n        Borg 2 uses the `platformdirs <https://pypi.org/project/platformdirs/>`_ library to determine\n        default directory locations. This means that default paths are **platform-specific**:\n\n        - **Linux**: Uses XDG Base Directory Specification paths (e.g., ``~/.config/borg``,\n          ``~/.cache/borg``, ``~/.local/share/borg``). `XDG env var`_ variables are honoured.\n        - **macOS**: Uses native macOS directories (e.g., ``~/Library/Application Support/borg``,\n          ``~/Library/Caches/borg``). `XDG env var`_ variables are **not** honoured.\n        - **Windows**: Uses Windows AppData directories (e.g., ``C:\\Users\\<user>\\AppData\\Roaming\\borg``,\n          ``C:\\Users\\<user>\\AppData\\Local\\borg``). `XDG env var`_ variables are **not** honoured.\n\n        On all platforms, you can override each directory individually using the specific environment\n        variables described below. You can also set ``BORG_BASE_DIR`` to force borg to use\n        ``BORG_BASE_DIR/.config/borg``, ``BORG_BASE_DIR/.cache/borg``, etc., regardless of the platform.\n\n    Default directory locations by platform (when no ``BORG_*`` environment variables are set):\n\n    .. list-table::\n       :header-rows: 1\n       :widths: 15 25 30 30\n\n       * - Directory\n         - Linux\n         - macOS\n         - Windows\n       * - Config\n         - ``~/.config/borg``\n         - ``~/Library/Application Support/borg``\n         - ``%APPDATA%\\borg``\n       * - Cache\n         - ``~/.cache/borg``\n         - ``~/Library/Caches/borg``\n         - ``%LOCALAPPDATA%\\borg\\Cache``\n       * - Data\n         - ``~/.local/share/borg``\n         - ``~/Library/Application Support/borg``\n         - ``%LOCALAPPDATA%\\borg``\n       * - Runtime\n         - ``/run/user/<uid>/borg``\n         - ``~/Library/Caches/TemporaryItems/borg``\n         - ``%TEMP%\\borg``\n       * - Keys\n         - ``<config_dir>/keys``\n         - ``<config_dir>/keys``\n         - ``<config_dir>\\keys``\n       * - Security\n         - ``<data_dir>/security``\n         - ``<data_dir>/security``\n         - ``<data_dir>\\security``\n\n    BORG_BASE_DIR\n        Defaults to ``$HOME`` or ``~$USER`` or ``~`` (in that order).\n        If you want to move all borg-specific folders to a custom path at once, all you need to do is\n        to modify ``BORG_BASE_DIR``: the other paths for cache, config etc. will adapt accordingly\n        (assuming you didn't set them to a different custom value).\n    BORG_CACHE_DIR\n        Defaults to the platform-specific cache directory (see table above).\n        If ``BORG_BASE_DIR`` is set, defaults to ``$BORG_BASE_DIR/.cache/borg``.\n        On Linux, `XDG env var`_ ``XDG_CACHE_HOME`` is also honoured if ``BORG_BASE_DIR`` is not set.\n        This directory contains the local cache and might need a lot\n        of space for dealing with big repositories. Make sure you're aware of the associated\n        security aspects of the cache location: :ref:`cache_security`\n    BORG_CONFIG_DIR\n        Defaults to the platform-specific config directory (see table above).\n        If ``BORG_BASE_DIR`` is set, defaults to ``$BORG_BASE_DIR/.config/borg``.\n        On Linux, `XDG env var`_ ``XDG_CONFIG_HOME`` is also honoured if ``BORG_BASE_DIR`` is not set.\n        This directory contains all borg configuration directories, see the FAQ\n        for a security advisory about the data in this directory: :ref:`home_config_borg`\n    BORG_DATA_DIR\n        Defaults to the platform-specific data directory (see table above).\n        If ``BORG_BASE_DIR`` is set, defaults to ``$BORG_BASE_DIR/.local/share/borg``.\n        On Linux, `XDG env var`_ ``XDG_DATA_HOME`` is also honoured if ``BORG_BASE_DIR`` is not set.\n        This directory contains all borg data directories, see the FAQ\n        for a security advisory about the data in this directory: :ref:`home_data_borg`\n    BORG_RUNTIME_DIR\n        Defaults to the platform-specific runtime directory (see table above).\n        If ``BORG_BASE_DIR`` is set, defaults to ``$BORG_BASE_DIR/.cache/borg``.\n        On Linux, `XDG env var`_ ``XDG_RUNTIME_DIR`` is also honoured if ``BORG_BASE_DIR`` is not set.\n        This directory contains borg runtime files, like e.g. the socket file.\n    BORG_SECURITY_DIR\n        Defaults to ``$BORG_DATA_DIR/security``.\n        This directory contains security relevant data.\n    BORG_KEYS_DIR\n        Defaults to ``$BORG_CONFIG_DIR/keys``.\n        This directory contains keys for encrypted repositories.\n    BORG_KEY_FILE\n        When set, use the given path as repository key file. Please note that this is only\n        for rather special applications that externally fully manage the key files:\n\n        - this setting only applies to the keyfile modes (not to the repokey modes).\n        - using a full, absolute path to the key file is recommended.\n        - all directories in the given path must exist.\n        - this setting forces borg to use the key file at the given location.\n        - the key file must either exist (for most commands) or will be created (``borg repo-create``).\n        - you need to give a different path for different repositories.\n        - you need to point to the correct key file matching the repository the command will operate on.\n    TMPDIR\n        This is where temporary files are stored (might need a lot of temporary space for some\n        operations), see tempfile_ for details.\n\nBuilding:\n    BORG_OPENSSL_NAME\n        Defines the subdirectory name for OpenSSL (setup.py).\n    BORG_OPENSSL_PREFIX\n        Adds given OpenSSL header file directory to the default locations (setup.py).\n    BORG_LIBACL_PREFIX\n        Adds given prefix directory to the default locations. If an 'include/acl/libacl.h' is found\n        Borg will be linked against the system libacl instead of a bundled implementation. (setup.py)\n    BORG_LIBLZ4_PREFIX\n        Adds given prefix directory to the default locations. If a 'include/lz4.h' is found Borg\n        will be linked against the system liblz4 instead of a bundled implementation. (setup.py)\n\nPlease note:\n\n- Be very careful when using the \"yes\" sayers, the warnings with prompt exist for your / your data's security/safety.\n- Also be very careful when putting your passphrase into a script, make sure it has appropriate file permissions (e.g.\n  mode 600, root:root).\n\n.. _INI: https://docs.python.org/3/library/logging.config.html#configuration-file-format\n\n.. _tempfile: https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir\n\n\nAutomatically generated Environment Variables (jsonargparse)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBorg uses jsonargparse_ with ``default_env=True``, which means that every\ncommand-line option can also be set via an environment variable.\n\nThe environment variable name is derived from the program name (``borg``),\nthe subcommand (if any), and the option name, all converted to uppercase\nwith dashes replaced by underscores.\n\nFor **top-level options** (not specific to a subcommand), the pattern is::\n\n    BORG_<OPTION>\n\nFor example, ``--lock-wait`` can be set via ``BORG_LOCK_WAIT``.\n\nFor **subcommand options**, the subcommand and option are separated by a\ndouble underscore::\n\n    BORG_<SUBCOMMAND>__<OPTION>\n\nFor example, ``borg create --comment`` can be set via ``BORG_CREATE__COMMENT``.\n\n.. _jsonargparse: https://jsonargparse.readthedocs.io/\n"
  },
  {
    "path": "docs/usage/general/file-metadata.rst.inc",
    "content": "Support for file metadata\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBesides regular file and directory structures, Borg can preserve\n\n* symlinks (stored as a symlink; the symlink is not followed)\n* special files:\n\n  * character and block device files (restored via mknod(2))\n  * FIFOs (\"named pipes\")\n  * special file *contents* can be backed up in ``--read-special`` mode.\n    By default, the metadata to create them with mknod(2), mkfifo(2), etc. is stored.\n* hard-linked regular files, devices, symlinks, FIFOs (considering all items in the same archive)\n* timestamps with nanosecond precision: mtime, atime, ctime\n* other timestamps: birthtime (on platforms supporting it)\n* permissions:\n\n  * IDs of owning user and owning group\n  * names of owning user and owning group (if the IDs can be resolved)\n  * Unix Mode/Permissions (u/g/o permissions, suid, sgid, sticky)\n\nOn some platforms additional features are supported:\n\n.. Yes/No's are grouped by reason/mechanism/reference.\n\n+-------------------------+----------+-----------+------------+\n| Platform                | ACLs     | xattr     | Flags      |\n|                         | [#acls]_ | [#xattr]_ | [#flags]_  |\n+=========================+==========+===========+============+\n| Linux                   | Yes      | Yes       | Yes [1]_   |\n+-------------------------+----------+-----------+------------+\n| macOS                   | Yes      | Yes       | Yes (all)  |\n+-------------------------+----------+-----------+------------+\n| FreeBSD                 | Yes      | Yes       | Yes (all)  |\n+-------------------------+----------+-----------+------------+\n| OpenBSD                 | n/a      | n/a       | Yes (all)  |\n+-------------------------+----------+-----------+------------+\n| NetBSD                  | n/a      | No [2]_   | Yes (all)  |\n+-------------------------+----------+-----------+------------+\n| Solaris and derivatives | No [3]_  | No [3]_   | n/a        |\n+-------------------------+----------+-----------+------------+\n| Windows (cygwin)        | No [4]_  | No        | No         |\n+-------------------------+----------+-----------+------------+\n\nOther Unix-like operating systems may work as well, but have not been tested yet.\n\nNote that most platform-dependent features also depend on the filesystem.\nFor example, ntfs-3g on Linux is not able to convey NTFS ACLs.\n\n.. [1] Only \"nodump\", \"immutable\", \"compressed\" and \"append\" are supported.\n    Feature request :issue:`618` for more flags.\n.. [2] Feature request :issue:`1332`\n.. [3] Feature request :issue:`1337`\n.. [4] Cygwin tries to map NTFS ACLs to permissions with varying degrees of success.\n\n.. [#acls] The native access control list mechanism of the OS. This normally limits access to\n    non-native ACLs. For example, NTFS ACLs are not completely accessible on Linux with ntfs-3g.\n.. [#xattr] Extended attributes; key-value pairs attached to a file, mainly used by the OS.\n    This includes resource forks on macOS.\n.. [#flags] Also known as *BSD flags*. The Linux set of flags [1]_ is portable across platforms.\n    The BSDs define additional flags.\n"
  },
  {
    "path": "docs/usage/general/file-systems.rst.inc",
    "content": "File systems\n~~~~~~~~~~~~\n\nWe recommend using a reliable, scalable journaling filesystem for the\nrepository, e.g., zfs, btrfs, ext4, apfs.\n\nBorg now uses the ``borgstore`` package to implement the key/value store it\nuses for the repository.\n\nIt currently uses the ``file:`` store (posixfs backend) either with a local\ndirectory or via SSH and a remote ``borg serve`` agent using borgstore on the\nremote side.\n\nThis means that it will store each chunk into a separate filesystem file\n(for more details, see the ``borgstore`` project).\n\nThis has some pros and cons (compared to legacy Borg 1.x segment files):\n\nPros:\n\n- Simplicity and better maintainability of the Borg code.\n- Sometimes faster, less I/O, better scalability: e.g., borg compact can just\n  remove unused chunks by deleting a single file and does not need to read\n  and rewrite segment files to free space.\n- In the future, easier to adapt to other kinds of storage:\n  borgstore's backends are quite simple to implement.\n  ``sftp:`` and ``rclone:`` backends already exist, others might be easy to add.\n- Parallel repository access with less locking is easier to implement.\n\nCons:\n\n- The repository filesystem will have to deal with a large number of files (there\n  are provisions in borgstore against having too many files in a single directory\n  by using a nested directory structure).\n- Greater filesystem space overhead (depends on the allocation block size — modern\n  filesystems like zfs are rather clever here, using a variable block size).\n- Sometimes slower, due to less sequential and more random access operations.\n"
  },
  {
    "path": "docs/usage/general/logging.rst.inc",
    "content": "Logging\n~~~~~~~\n\nBorg writes all log output to stderr by default. However, output on stderr does\nnot necessarily indicate an error. Check the log levels of the messages and the\nreturn code of borg to determine error, warning, or success conditions.\n\nIf you want to capture the log output to a file, just redirect it:\n\n::\n\n    borg create --repo repo archive myfiles 2>> logfile\n\n\nCustom logging configurations can be implemented via BORG_LOGGING_CONF.\n\nThe log level of the built-in logging configuration defaults to WARNING.\nThis is because we want Borg to be mostly silent and only output\nwarnings, errors, and critical messages unless output has been requested\nby supplying an option that implies output (e.g., ``--list`` or ``--progress``).\n\nLog levels: DEBUG < INFO < WARNING < ERROR < CRITICAL\n\nUse ``--debug`` to set the DEBUG log level —\nthis prints debug, info, warning, error, and critical messages.\n\nUse ``--info`` (or ``-v`` or ``--verbose``) to set the INFO log level —\nthis prints info, warning, error, and critical messages.\n\nUse ``--warning`` (default) to set the WARNING log level —\nthis prints warning, error, and critical messages.\n\nUse ``--error`` to set the ERROR log level —\nthis prints error and critical messages.\n\nUse ``--critical`` to set the CRITICAL log level —\nthis prints only critical messages.\n\nWhile you can set miscellaneous log levels, do not expect every command to\nproduce different output at different log levels — it's merely a possibility.\n\n.. warning:: Options ``--critical`` and ``--error`` are provided for completeness,\n             their usage is not recommended as you might miss important information.\n"
  },
  {
    "path": "docs/usage/general/positional-arguments.rst.inc",
    "content": "Positional Arguments and Options: Order matters\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBorg only supports taking options (``-s`` and ``--progress`` in the example)\neither to the left or to the right of all positional arguments (``repo::archive`` and ``path``\nin the example), but not in between them:\n\n::\n\n    borg create -s --progress archive path  # good and preferred\n    borg create archive path -s --progress  # also works\n    borg create -s archive path --progress  # works, but ugly\n    borg create archive -s --progress path  # BAD\n\nThis is due to a problem in the argparse module: https://bugs.python.org/issue15112\n"
  },
  {
    "path": "docs/usage/general/repository-locations.rst.inc",
    "content": "Repository Locations / Archive Names\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMany commands need to know the repository location; specify it via ``-r``/``--repo``\nor use the ``BORG_REPO`` environment variable.\n\nCommands that need one or two archive names usually take them as positional arguments.\n\nCommands that work with an arbitrary number of archives usually accept ``-a ARCH_GLOB``.\n\nArchive names must not contain the ``/`` (slash) character. For simplicity,\nalso avoid spaces or other characters that have special meaning to the\nshell or in a filesystem (``borg mount`` uses the archive name as a directory\nname).\n"
  },
  {
    "path": "docs/usage/general/repository-urls.rst.inc",
    "content": "Repository URLs\n~~~~~~~~~~~~~~~\n\n**Local filesystem** (or locally mounted network filesystem):\n\n``/path/to/repo`` — filesystem path to the repository directory (absolute path)\n\n``path/to/repo`` — filesystem path to the repository directory (relative path)\n\nAlso, paths like ``~/path/to/repo`` or ``~other/path/to/repo`` work (this is\nexpanded by your shell).\n\nNote: You may also prepend ``file://`` to a filesystem path to use URL style.\n\n**Remote repositories** accessed via SSH user@host:\n\n``ssh://user@host:port//abs/path/to/repo`` — absolute path\n\n``ssh://user@host:port/rel/path/to/repo`` — path relative to the current directory\n\n**Remote repositories** accessed via SFTP:\n\n``sftp://user@host:port//abs/path/to/repo`` — absolute path\n\n``sftp://user@host:port/rel/path/to/repo`` — path relative to the current directory\n\nFor SSH and SFTP URLs, the ``user@`` and ``:port`` parts are optional.\n\n**Remote repositories** accessed via rclone:\n\n``rclone:remote:path`` — see the rclone docs for more details about ``remote:path``.\n\n**Remote repositories** accessed via S3:\n\n``(s3|b2):[(profile|(access_key_id:access_key_secret))@][scheme://hostname[:port]]/bucket/path`` — see the boto3 docs for more details about credentials.\n\nIf you are connecting to AWS S3, ``[schema://hostname[:port]]`` is optional, but ``bucket`` and ``path`` are always required.\n`scheme` is usually `https` here, hostname and optional port refer to your S3/B2 server, if that is not Amazon's.\n\nNote: There is a known issue with some S3-compatible services, e.g., Backblaze B2. If you encounter problems, try using ``b2:`` instead of ``s3:`` in the URL.\n\n\nIf you frequently need the same repository URL, it is a good idea to set the\n``BORG_REPO`` environment variable to set a default repository URL:\n\n::\n\n    export BORG_REPO='ssh://user@host:port/rel/path/to/repo'\n\nThen simply omit the ``--repo`` option when you want\nto use the default — it will be read from BORG_REPO.\n"
  },
  {
    "path": "docs/usage/general/resources.rst.inc",
    "content": "Resource Usage\n~~~~~~~~~~~~~~\n\nBorg might use significant resources depending on the size of the data set it is dealing with.\n\nIf you use Borg in a client/server way (with an SSH repository),\nthe resource usage occurs partly on the client and partly on the\nserver.\n\nIf you use Borg as a single process (with a filesystem repository),\nall resource usage occurs in that one process, so add up client and\nserver to get the approximate resource usage.\n\nCPU client:\n    - **borg create:** chunking, hashing, compression, encryption (high CPU usage)\n    - **chunks cache sync:** quite heavy on CPU, doing lots of hash table operations\n    - **borg extract:** decryption, decompression (medium to high CPU usage)\n    - **borg check:** similar to extract, but depends on options given\n    - **borg prune/borg delete archive:** low to medium CPU usage\n    - **borg delete repo:** done on the server\n\n    It will not use more than 100% of one CPU core as the code is currently single-threaded.\n    Especially higher zlib and lzma compression levels use significant amounts\n    of CPU cycles. Crypto might be cheap on the CPU (if hardware-accelerated) or\n    expensive (if not).\n\nCPU server:\n    It usually does not need much CPU; it just deals with the key/value store\n    (repository) and uses the repository index for that.\n\n    borg check: the repository check computes the checksums of all chunks\n    (medium CPU usage)\n    borg delete repo: low CPU usage\n\nCPU (only for client/server operation):\n    When using Borg in a client/server way with an ssh-type repository, the SSH\n    processes used for the transport layer will need some CPU on the client and\n    on the server due to the crypto they are doing — especially if you are pumping\n    large amounts of data.\n\nMemory (RAM) client:\n    The chunks index and the files index are read into memory for performance\n    reasons. Might need large amounts of memory (see below).\n    Compression, especially lzma compression with high levels, might need substantial\n    amounts of memory.\n\nMemory (RAM) server:\n    The server process will load the repository index into memory. Might need\n    considerable amounts of memory, but less than on the client (see below).\n\nChunks index (client only):\n    Proportional to the number of data chunks in your repo. Lots of chunks\n    in your repo imply a big chunks index.\n    It is possible to tweak the chunker parameters (see create options).\n\nFiles index (client only):\n    Proportional to the number of files in your last backups. Can be switched\n    off (see create options), but the next backup might be much slower if you do.\n    The speed benefit of using the files cache is proportional to file size.\n\nRepository index (server only):\n    Proportional to the number of data chunks in your repo. Lots of chunks\n    in your repo imply a big repository index.\n    It is possible to tweak the chunker parameters (see create options) to\n    influence the number of chunks created.\n\nTemporary files (client):\n    Reading data and metadata from a FUSE-mounted repository will consume up to\n    the size of all deduplicated, small chunks in the repository. Big chunks\n    will not be locally cached.\n\nTemporary files (server):\n    A non-trivial amount of data will be stored in the remote temporary directory\n    for each client that connects to it. For some remotes, this can fill the\n    default temporary directory in /tmp. This can be mitigated by ensuring the\n    $TMPDIR, $TEMP, or $TMP environment variable is properly set for the sshd\n    process.\n    For some OSes, this can be done by setting the correct value in the\n    .bashrc (or equivalent login config file for other shells); however, in\n    other cases it may be necessary to first enable ``PermitUserEnvironment yes``\n    in your ``sshd_config`` file, then add ``environment=\"TMPDIR=/my/big/tmpdir\"``\n    at the start of the public key to be used in the ``authorized_keys`` file.\n\nCache files (client only):\n    Contains the chunks index and files index (plus a collection of single-\n    archive chunk indexes), which might need huge amounts of disk space\n    depending on archive count and size — see the FAQ for how to reduce this.\n\nNetwork (only for client/server operation):\n    If your repository is remote, all deduplicated (and optionally compressed/\n    encrypted) data has to go over the connection (``ssh://`` repository URL).\n    If you use a locally mounted network filesystem, some additional copy\n    operations used for transaction support also go over the connection. If\n    you back up multiple sources to one target repository, additional traffic\n    happens for cache resynchronization.\n"
  },
  {
    "path": "docs/usage/general/return-codes.rst.inc",
    "content": "Return codes\n~~~~~~~~~~~~\n\nBorg can exit with the following return codes (rc):\n\n=========== =======\nReturn code Meaning\n=========== =======\n0           success (logged as INFO)\n1           generic warning (operation reached its normal end, but there were warnings —\n            you should check the log; logged as WARNING)\n2           generic error (like a fatal error or a local/remote exception; the operation\n            did not reach its normal end; logged as ERROR)\n3..99       specific error (enabled by BORG_EXIT_CODES=modern)\n100..127    specific warning (enabled by BORG_EXIT_CODES=modern)\n128+N       killed by signal N (e.g. 137 == kill -9)\n=========== =======\n\nIf you use ``--show-rc``, the return code is also logged at the indicated\nlevel as the last log entry.\n\nThe modern exit codes (return codes, \"rc\") are documented here: see :ref:`msgid`.\n"
  },
  {
    "path": "docs/usage/general/units.rst.inc",
    "content": "Units\n~~~~~\n\nTo display quantities, Borg takes care of respecting the\nusual conventions of scale. Disk sizes are displayed in `decimal\n<https://en.wikipedia.org/wiki/Decimal>`_, using powers of ten (so\n``kB`` means 1000 bytes). For memory usage, `binary prefixes\n<https://en.wikipedia.org/wiki/Binary_prefix>`_ are used, and are\nindicated using the `IEC binary prefixes\n<https://en.wikipedia.org/wiki/IEC_80000-13#Prefixes_for_binary_multiples>`_,\nusing powers of two (so ``KiB`` means 1024 bytes).\n"
  },
  {
    "path": "docs/usage/general.rst",
    "content": "General\n-------\n\nBorg consists of a number of commands. Each command accepts\na number of arguments and options and interprets various environment variables.\nThe following sections will describe each command in detail.\n\nCommands, options, parameters, paths, and similar elements are shown in ``fixed-width``.\nOption values are `underlined. Borg has a few options that accept a fixed set\nof values (e.g., ``--encryption`` of :ref:`borg_repo-create`).\n\n.. container:: experimental\n\n   Experimental features are marked with red stripes on the sides, like this paragraph.\n\n   Experimental features are not stable, which means that they may be changed in incompatible\n   ways or even removed entirely without prior notice in following releases.\n\n.. include:: usage_general.rst.inc\n\nIf you are interested in more details (such as formulas), see\n:ref:`internals`. For details on the available JSON output, refer to\n:ref:`json_output`.\n\n.. _common_options:\n\nCommon options\n~~~~~~~~~~~~~~\n\nAll Borg commands share these options:\n\n.. include:: common-options.rst.inc\n\nOption ``--help`` when used as a command works as expected on subcommands (e.g., ``borg help compact``).\nBut it does not work when the help command is used on sub-sub-commands (e.g., ``borg help key export``).\nThe workaround for this is to use the help command as a flag (e.g., ``borg key export --help``).\n\nExamples\n~~~~~~~~\n::\n\n    # Create an archive and log: borg version, files list, return code\n    $ borg -r /path/to/repo create --show-version --list --show-rc my-files files\n\n"
  },
  {
    "path": "docs/usage/help.rst",
    "content": "Miscellaneous Help\n------------------\n\n.. include:: help.rst.inc\n"
  },
  {
    "path": "docs/usage/help.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_patterns:\n\nborg help patterns\n~~~~~~~~~~~~~~~~~~\n\n\nWhen specifying one or more file paths in a Borg command that supports\npatterns for the respective option or argument, you can apply the\npatterns described here to include only desired files and/or exclude\nunwanted ones. Patterns can be used\n\n- for ``--exclude`` option,\n- in the file given with ``--exclude-from`` option,\n- for ``--pattern`` option,\n- in the file given with ``--patterns-from`` option and\n- for ``PATH`` arguments that explicitly support them.\n\nThe path/filenames used as input for the pattern matching start with the\ncurrently active recursion root. You usually give the recursion root(s)\nwhen invoking borg and these can be either relative or absolute paths.\n\nBe careful, your patterns must match the archived paths:\n\n- Archived paths never start with a leading slash ('/'), nor with '.', nor with '..'.\n\n  - When you back up absolute paths like ``/home/user``, the archived\n    paths start with ``home/user``.\n  - When you back up relative paths like ``./src``, the archived paths\n    start with ``src``.\n  - When you back up relative paths like ``../../src``, the archived paths\n    start with ``src``.\n  - On native Windows, archived absolute paths look like ``C/Windows/System32``.\n\nBorg supports different pattern styles. To define a non-default\nstyle for a specific pattern, prefix it with two characters followed\nby a colon ':' (i.e. ``fm:path/*``, ``sh:path/**``).\n\nNote: Windows users must only use forward slashes in patterns, not backslashes.\n\nThe default pattern style for ``--exclude`` differs from ``--pattern``, see below.\n\n`Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector ``fm:``\n    This is the default style for ``--exclude`` and ``--exclude-from``.\n    These patterns use a variant of shell pattern syntax, with '\\*' matching\n    any number of characters, '?' matching any single character, '[...]'\n    matching any single character specified, including ranges, and '[!...]'\n    matching any character not specified. For the purpose of these patterns,\n    the path separator (forward slash '/') is not treated specially.\n    Wrap meta-characters in brackets for a literal\n    match (i.e. ``[?]`` to match the literal character '?'). For a path\n    to match a pattern, the full path must match, or it must match\n    from the start of the full path to just before a path separator. Except\n    for the root path, paths will never end in the path separator when\n    matching is attempted.  Thus, if a given pattern ends in a path\n    separator, a '\\*' is appended before matching is attempted. A leading\n    path separator is always removed.\n\nShell-style patterns, selector ``sh:``\n    This is the default style for ``--pattern`` and ``--patterns-from``.\n    Like fnmatch patterns these are similar to shell patterns. The difference\n    is that the pattern may include ``**/`` for matching zero or more directory\n    levels, ``*`` for matching zero or more arbitrary characters with the\n    exception of any path separator, ``{}`` containing comma-separated\n    alternative patterns. A leading path separator is always removed.\n\n`Regular expressions <https://docs.python.org/3/library/re.html>`_, selector ``re:``\n    Unlike shell patterns, regular expressions are not required to match the full\n    path and any substring match is sufficient. It is strongly recommended to\n    anchor patterns to the start ('^'), to the end ('$') or both.\n\nPath prefix, selector ``pp:``\n    This pattern style is useful to match whole subdirectories. The pattern\n    ``pp:root/somedir`` matches ``root/somedir`` and everything therein.\n    A leading path separator is always removed.\n\nPath full-match, selector ``pf:``\n    This pattern style is (only) useful to match full paths.\n    This is kind of a pseudo pattern as it cannot have any variable or\n    unspecified parts - the full path must be given. ``pf:root/file.ext``\n    matches ``root/file.ext`` only. A leading path separator is always\n    removed.\n\n    Implementation note: this is implemented via very time-efficient O(1)\n    hashtable lookups (this means you can have huge amounts of such patterns\n    without impacting performance much).\n    Due to that, this kind of pattern does not respect any context or order.\n    If you use such a pattern to include a file, it will always be included\n    (if the directory recursion encounters it).\n    Other include/exclude patterns that would normally match will be ignored.\n    Same logic applies for exclude.\n\n.. note::\n\n    ``re:``, ``sh:`` and ``fm:`` patterns are all implemented on top of\n    the Python SRE engine. It is very easy to formulate patterns for each\n    of these types which requires an inordinate amount of time to match\n    paths. If untrusted users are able to supply patterns, ensure they\n    cannot supply ``re:`` patterns. Further, ensure that ``sh:`` and\n    ``fm:`` patterns only contain a handful of wildcards at most.\n\n.. note::\n\n    **Windows path handling**: All paths in Borg archives use forward slashes (``/``)\n    as path separators, regardless of the platform. When creating archives on Windows,\n    backslashes from filesystem paths are automatically converted to forward slashes.\n\n.. note::\n\n    **Windows reserved characters**: On Windows, when extracting archives created on\n    POSIX systems, paths may contain characters that are reserved from being used in\n    file or directory names (like: ``< > : \" \\ | ? *``).\n    These are replaced by characters in the unicode private use area (``U+F0xx``) like\n    the CIFS mapchars feature also does it. It won't be pretty, but at least it works.\n\nExclusions can be passed via the command line option ``--exclude``. When used\nfrom within a shell, the patterns should be quoted to protect them from\nexpansion.\n\nPatterns matching special characters, e.g. whitespace, within a shell may\nrequire adjustments, such as putting quotation marks around the arguments.\nExample:\nUsing bash, the following command line option would match and exclude \"item name\":\n``--pattern='-path/item name'``\nNote that when patterns are used within a pattern file directly read by borg,\ne.g. when using ``--exclude-from`` or ``--patterns-from``, there is no shell\ninvolved and thus no quotation marks are required.\n\nThe ``--exclude-from`` option permits loading exclusion patterns from a text\nfile with one pattern per line. Lines empty or starting with the hash sign\n'#' after removing whitespace on both ends are ignored. The optional style\nselector prefix is also supported for patterns loaded from a file. Due to\nwhitespace removal, paths with whitespace at the beginning or end can only be\nexcluded using regular expressions.\n\nTo test your exclusion patterns without performing an actual backup you can\nrun ``borg create --list --dry-run ...``.\n\nExamples::\n\n    # Exclude a directory anywhere in the tree named ``steamapps/common``\n    # (and everything below it), regardless of where it appears:\n    $ borg create -e 'sh:**/steamapps/common/**' archive /\n\n    # Exclude the contents of ``/home/user/.cache``:\n    $ borg create -e 'sh:home/user/.cache/**' archive /home/user\n    $ borg create -e home/user/.cache/ archive /home/user\n\n    # The file '/home/user/.cache/important' is *not* backed up:\n    $ borg create -e home/user/.cache/ archive / /home/user/.cache/important\n\n    # Exclude '/home/user/file.o' but not '/home/user/file.odt':\n    $ borg create -e '*.o' archive /\n\n    # Exclude '/home/user/junk' and '/home/user/subdir/junk' but\n    # not '/home/user/importantjunk' or '/etc/junk':\n    $ borg create -e 'home/*/junk' archive /\n\n    # The contents of directories in '/home' are not backed up when their name\n    # ends in '.tmp'\n    $ borg create --exclude 're:^home/[^/]+\\.tmp/' archive /\n\n    # Load exclusions from file\n    $ cat >exclude.txt <<EOF\n    # Comment line\n    home/*/junk\n    *.tmp\n    fm:aa:something/*\n    re:^home/[^/]+\\.tmp/\n    sh:home/*/.thumbnails\n    # Example with spaces, no need to escape as it is processed by borg\n    some file with spaces.txt\n    EOF\n    $ borg create --exclude-from exclude.txt archive /\n\nA more general and easier to use way to define filename matching patterns\nexists with the ``--pattern`` and ``--patterns-from`` options. Using\nthese, you may specify the backup roots, default pattern styles and\npatterns for inclusion and exclusion.\n\nRoot path prefix ``R``\n    A recursion root path starts with the prefix ``R``, followed by a path\n    (a plain path, not a file pattern). Use this prefix to have the root\n    paths in the patterns file rather than as command line arguments.\n\nPattern style prefix ``P`` (only useful within patterns files)\n    To change the default pattern style, use the ``P`` prefix, followed by\n    the pattern style abbreviation (``fm``, ``pf``, ``pp``, ``re``, ``sh``).\n    All patterns following this line in the same patterns file will use this\n    style until another style is specified or the end of the file is reached.\n    When the current patterns file is finished, the default pattern style will\n    reset.\n\nExclude pattern prefix ``-``\n    Use the prefix ``-``, followed by a pattern, to define an exclusion.\n    This has the same effect as the ``--exclude`` option.\n\nExclude no-recurse pattern prefix ``!``\n    Use the prefix ``!``, followed by a pattern, to define an exclusion\n    that does not recurse into subdirectories. This saves time, but\n    prevents include patterns to match any files in subdirectories.\n\nInclude pattern prefix ``+``\n    Use the prefix ``+``, followed by a pattern, to define inclusions.\n    This is useful to include paths that are covered in an exclude\n    pattern and would otherwise not be backed up.\n\nThe first matching pattern is used, so if an include pattern matches\nbefore an exclude pattern, the file is backed up. Note that a no-recurse\nexclude stops examination of subdirectories so that potential includes\nwill not match - use normal excludes for such use cases.\n\nExample::\n\n    # Define the recursion root\n    R /\n    # Exclude all iso files in any directory\n    - **/*.iso\n    # Explicitly include all inside etc and root\n    + etc/**\n    + root/**\n    # Exclude a specific directory under each user's home directories\n    - home/*/.cache\n    # Explicitly include everything in /home\n    + home/**\n    # Explicitly exclude some directories without recursing into them\n    ! re:^(dev|proc|run|sys|tmp)\n    # Exclude all other files and directories\n    # that are not specifically included earlier.\n    - **\n\n**Tip: You can easily test your patterns with --dry-run and  --list**::\n\n    $ borg create --dry-run --list --patterns-from patterns.txt archive\n\nThis will list the considered files one per line, prefixed with a\ncharacter that indicates the action (e.g. 'x' for excluding, see\n**Item flags** in `borg create` usage docs).\n\n.. note::\n\n    It is possible that a subdirectory or file is matched while its parent\n    directories are not. In that case, parent directories are not backed\n    up and thus their user, group, permission, etc. cannot be restored.\n\nPatterns (``--pattern``) and excludes (``--exclude``) from the command line are\nconsidered first (in the order of appearance). Then patterns from ``--patterns-from``\nare added. Exclusion patterns from ``--exclude-from`` files are appended last.\n\nExamples::\n\n    # back up pics, but not the ones from 2018, except the good ones:\n    # note: using = is essential to avoid cmdline argument parsing issues.\n    borg create --pattern=+pics/2018/good --pattern=-pics/2018 archive pics\n\n    # back up only JPG/JPEG files (case insensitive) in all home directories:\n    borg create --pattern '+ re:\\.jpe?g(?i)$' archive /home\n\n    # back up homes, but exclude big downloads (like .ISO files) or hidden files:\n    borg create --exclude 're:\\.iso(?i)$' --exclude 'sh:home/**/.*' archive /home\n\n    # use a file with patterns (recursion root '/' via command line):\n    borg create --patterns-from patterns.lst archive /\n\nThe patterns.lst file could look like that::\n\n    # \"sh:\" pattern style is the default\n    # exclude caches\n    - home/*/.cache\n    # include susans home\n    + home/susan\n    # also back up this exact file\n    + pf:home/bobby/specialfile.txt\n    # don't back up the other home directories\n    - home/*\n    # don't even look in /dev, /proc, /run, /sys, /tmp (note: would exclude files like /device, too)\n    ! re:^(dev|proc|run|sys|tmp)\n\nYou can specify recursion roots either on the command line or in a patternfile::\n\n    # these two commands do the same thing\n    borg create --exclude home/bobby/junk archive /home/bobby /home/susan\n    borg create --patterns-from patternfile.lst archive\n\npatternfile.lst::\n\n    # note that excludes use fm: by default and patternfiles use sh: by default.\n    # therefore, we need to specify fm: to have the same exact behavior.\n    P fm\n    R /home/bobby\n    R /home/susan\n    - home/bobby/junk\n\nThis allows you to share the same patterns between multiple repositories\nwithout needing to specify them on the command line.\n\n.. _borg_match-archives:\n\nborg help match-archives\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n\nThe ``--match-archives`` option matches a given pattern against the list of all archives\nin the repository. It can be given multiple times.\n\nThe patterns can have a prefix of:\n\n- name: pattern match on the archive name (default)\n- aid: prefix match on the archive id (only one result allowed)\n- user: exact match on the username who created the archive\n- host: exact match on the hostname where the archive was created\n- tags: match on the archive tags\n\nIn case of a name pattern match,\nit uses pattern styles similar to the ones described by ``borg help patterns``:\n\nIdentical match pattern, selector ``id:`` (default)\n    Simple string match, must fully match exactly as given.\n\nShell-style patterns, selector ``sh:``\n    Match like on the shell, wildcards like `*` and `?` work.\n\n`Regular expressions <https://docs.python.org/3/library/re.html>`_, selector ``re:``\n    Full regular expression support.\n    This is very powerful, but can also get rather complicated.\n\nExamples::\n\n    # name match, id: style\n    borg delete --match-archives 'id:archive-with-crap'\n    borg delete -a 'id:archive-with-crap'  # same, using short option\n    borg delete -a 'archive-with-crap'  # same, because 'id:' is the default\n\n    # name match, sh: style\n    borg delete -a 'sh:home-kenny-*'\n\n    # name match, re: style\n    borg delete -a 're:pc[123]-home-(user1|user2)-2022-09-.*'\n\n    # archive id prefix match:\n    borg delete -a 'aid:d34db33f'\n\n    # host or user match\n    borg delete -a 'user:kenny'\n    borg delete -a 'host:kenny-pc'\n\n    # tags match\n    borg delete -a 'tags:TAG1' -a 'tags:TAG2'\n\n.. _borg_placeholders:\n\nborg help placeholders\n~~~~~~~~~~~~~~~~~~~~~~\n\n\nRepository URLs, ``--name``, ``-a`` / ``--match-archives``, ``--comment``\nand ``--remote-path`` values support these placeholders:\n\n{hostname}\n    The (short) hostname of the machine.\n\n{fqdn}\n    The full name of the machine.\n\n{reverse-fqdn}\n    The full name of the machine in reverse domain name notation.\n\n{now}\n    The current local date and time, by default in ISO-8601 format.\n    You can also supply your own `format string <https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior>`_, e.g. {now:%Y-%m-%d_%H:%M:%S}\n\n{utcnow}\n    The current UTC date and time, by default in ISO-8601 format.\n    You can also supply your own `format string <https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior>`_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S}\n\n{user}\n    The user name (or UID, if no name is available) of the user running borg.\n\n{pid}\n    The current process ID.\n\n{borgversion}\n    The version of borg, e.g.: 1.0.8rc1\n\n{borgmajor}\n    The version of borg, only the major version, e.g.: 1\n\n{borgminor}\n    The version of borg, only major and minor version, e.g.: 1.0\n\n{borgpatch}\n    The version of borg, only major, minor and patch version, e.g.: 1.0.8\n\nIf literal curly braces need to be used, double them for escaping::\n\n    borg create --repo /path/to/repo {{literal_text}}\n\nExamples::\n\n    borg create --repo /path/to/repo {hostname}-{user}-{utcnow} ...\n    borg create --repo /path/to/repo {hostname}-{now:%Y-%m-%d_%H:%M:%S%z} ...\n    borg prune -a 'sh:{hostname}-*' ...\n\n.. note::\n    systemd uses a difficult, non-standard syntax for command lines in unit files (refer to\n    the `systemd.unit(5)` manual page).\n\n    When invoking borg from unit files, pay particular attention to escaping,\n    especially when using the now/utcnow placeholders, since systemd performs its own\n    %-based variable replacement even in quoted text. To avoid interference from systemd,\n    double all percent signs (``{hostname}-{now:%Y-%m-%d_%H:%M:%S}``\n    becomes ``{hostname}-{now:%%Y-%%m-%%d_%%H:%%M:%%S}``).\n\n.. _borg_compression:\n\nborg help compression\n~~~~~~~~~~~~~~~~~~~~~\n\n\nIt is no problem to mix different compression methods in one repository,\ndeduplication is done on the source data chunks (not on the compressed\nor encrypted data).\n\nIf some specific chunk was once compressed and stored into the repository, creating\nanother backup that also uses this chunk will not change the stored chunk.\nSo if you use different compression specs for the backups, whichever stores a\nchunk first determines its compression. See also ``borg recreate``.\n\nCompression is lz4 by default. If you want something else, you have to specify what you want.\n\nValid compression specifiers are:\n\nnone\n    Do not compress.\n\nlz4\n    Use lz4 compression. Very high speed, very low compression. (default)\n\nzstd[,L]\n    Use zstd (\"zstandard\") compression, a modern wide-range algorithm.\n    If you do not explicitly give the compression level L (ranging from 1\n    to 22), it will use level 3.\n\nzlib[,L]\n    Use zlib (\"gz\") compression. Medium speed, medium compression.\n    If you do not explicitly give the compression level L (ranging from 0\n    to 9), it will use level 6.\n    Giving level 0 (means \"no compression\", but still has zlib protocol\n    overhead) is usually pointless, you better use \"none\" compression.\n\nlzma[,L]\n    Use lzma (\"xz\") compression. Low speed, high compression.\n    If you do not explicitly give the compression level L (ranging from 0\n    to 9), it will use level 6.\n    Giving levels above 6 is pointless and counterproductive because it does\n    not compress better due to the buffer size used by borg - but it wastes\n    lots of CPU cycles and RAM.\n\nauto,C[,L]\n    Use a built-in heuristic to decide per chunk whether to compress or not.\n    The heuristic tries with lz4 whether the data is compressible.\n    For incompressible data, it will not use compression (uses \"none\").\n    For compressible data, it uses the given C[,L] compression - with C[,L]\n    being any valid compression specifier. This can be helpful for media files\n    which often cannot be compressed much more.\n\nobfuscate,SPEC,C[,L]\n    Use compressed-size obfuscation to make fingerprinting attacks based on\n    the observable stored chunk size more difficult. Note:\n\n    - You must combine this with encryption, or it won't make any sense.\n    - Your repo size will be bigger, of course.\n    - A chunk is limited by the constant ``MAX_DATA_SIZE`` (cur. ~20MiB).\n\n    The SPEC value determines how the size obfuscation works:\n\n    *Relative random reciprocal size variation* (multiplicative)\n\n    Size will increase by a factor, relative to the compressed data size.\n    Smaller factors are used often, larger factors rarely.\n\n    Available factors::\n\n      1:     0.01 ..        100\n      2:     0.1  ..      1,000\n      3:     1    ..     10,000\n      4:    10    ..    100,000\n      5:   100    ..  1,000,000\n      6: 1,000    .. 10,000,000\n\n    Example probabilities for SPEC ``1``::\n\n      90   %  0.01 ..   0.1\n       9   %  0.1  ..   1\n       0.9 %  1    ..  10\n       0.09% 10    .. 100\n\n    *Randomly sized padding up to the given size* (additive)\n\n    ::\n\n      110: 1kiB (2 ^ (SPEC - 100))\n      ...\n      120: 1MiB\n      ...\n      123: 8MiB (max.)\n\n    *Padmé padding* (deterministic)\n\n    ::\n\n      250: pads to sums of powers of 2, max 12% overhead\n\n    Uses the Padmé algorithm to deterministically pad the compressed size to a sum of\n    powers of 2, limiting overhead to 12%. See https://lbarman.ch/blog/padme/ for details.\n\nExamples::\n\n    borg create --compression lz4 --repo REPO ARCHIVE data\n    borg create --compression zstd --repo REPO ARCHIVE data\n    borg create --compression zstd,10 --repo REPO ARCHIVE data\n    borg create --compression zlib --repo REPO ARCHIVE data\n    borg create --compression zlib,1 --repo REPO ARCHIVE data\n    borg create --compression auto,lzma,6 --repo REPO ARCHIVE data\n    borg create --compression auto,lzma ...\n    borg create --compression obfuscate,110,none ...\n    borg create --compression obfuscate,3,auto,zstd,10 ...\n    borg create --compression obfuscate,2,zstd,6 ...\n    borg create --compression obfuscate,250,zstd,3 ...\n\n"
  },
  {
    "path": "docs/usage/import-tar.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_import-tar:\n\nborg import-tar\n---------------\n.. code-block:: none\n\n    borg [common options] import-tar [options] NAME TARFILE\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                                                                      |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``NAME``                                          | specify the archive name                                                                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``TARFILE``                                       | input tar file. \"-\" to read from stdin instead.                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--tar-filter``                                  | filter program to pipe data through                                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-s``, ``--stats``                               | print statistics for the created archive                                                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--list``                                        | output verbose list of items (files, dirs, ...)                                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--filter STATUSCHARS``                          | only display items with the given status characters                                                                                                                                               |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--json``                                        | output stats as JSON (implies --stats)                                                                                                                                                            |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--ignore-zeros``                                | ignore zero-filled blocks in the input tarball                                                                                                                                                    |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                                |\n    |                                                                                                                                                                                                                                                                                                               |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive options**                                                                                                                                                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--comment COMMENT``                             | add a comment text to the archive                                                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--timestamp TIMESTAMP``                         | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--chunker-params PARAMS``                       | specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095                                                             |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the \"borg help compression\" command for details.                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n    TARFILE\n        input tar file. \"-\" to read from stdin instead.\n\n\n    options\n        --tar-filter    filter program to pipe data through\n        -s, --stats     print statistics for the created archive\n        --list          output verbose list of items (files, dirs, ...)\n        --filter STATUSCHARS    only display items with the given status characters\n        --json          output stats as JSON (implies --stats)\n        --ignore-zeros    ignore zero-filled blocks in the input tarball\n\n\n    :ref:`common_options`\n        |\n\n    Archive options\n        --comment COMMENT                             add a comment text to the archive\n        --timestamp TIMESTAMP                         manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n        --chunker-params PARAMS                       specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095\n        -C COMPRESSION, --compression COMPRESSION     select compression algorithm, see the output of the \"borg help compression\" command for details.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command creates a backup archive from a tarball.\n\nWhen giving '-' as path, Borg will read a tar stream from standard input.\n\nBy default (--tar-filter=auto) Borg will detect whether the file is compressed\nbased on its file extension and pipe the file through an appropriate filter:\n\n- .tar.gz or .tgz: gzip -d\n- .tar.bz2 or .tbz: bzip2 -d\n- .tar.xz or .txz: xz -d\n- .tar.zstd or .tar.zst: zstd -d\n- .tar.lz4: lz4 -d\n\nAlternatively, a --tar-filter program may be explicitly specified. It should\nread compressed data from stdin and output an uncompressed tar stream on\nstdout.\n\nMost documentation of borg create applies. Note that this command does not\nsupport excluding files.\n\nA ``--sparse`` option (as found in borg create) is not supported.\n\nAbout tar formats and metadata conservation or loss, please see ``borg export-tar``.\n\nimport-tar reads these tar formats:\n\n- BORG: borg specific (PAX-based)\n- PAX: POSIX.1-2001\n- GNU: GNU tar\n- POSIX.1-1988 (ustar)\n- UNIX V7 tar\n- SunOS tar with extended attributes\n\nTo import multiple tarballs into a single archive, they can be simply\nconcatenated (e.g. using \"cat\") into a single file, and imported with an\n``--ignore-zeros`` option to skip through the stop markers between them."
  },
  {
    "path": "docs/usage/info.rst",
    "content": ".. include:: info.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg info aid:f7dea078\n    Archive name: source-backup\n    Archive fingerprint: f7dea0788dfc026cc2be1c0f5b94beb4e4084eb3402fc40c38d8719b1bf2d943\n    Comment:\n    Hostname: mba2020\n    Username: tw\n    Time (start): Sat, 2022-06-25 20:51:40\n    Time (end): Sat, 2022-06-25 20:51:40\n    Duration: 0.03 seconds\n    Command line: /usr/bin/borg -r path/to/repo create source-backup src\n    Utilization of maximum supported archive size: 0%\n    Number of files: 244\n    Original size: 13.80 MB\n    Deduplicated size: 531 B\n\n"
  },
  {
    "path": "docs/usage/info.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_info:\n\nborg info\n---------\n.. code-block:: none\n\n    borg [common options] info [options] [NAME]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``NAME``                                     | specify the archive name                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--json``                                   | format output as JSON                                                                                                       |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n\n\n    options\n        --json     format output as JSON\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command displays detailed information about the specified archive.\n\nPlease note that the deduplicated sizes of the individual archives do not add\nup to the deduplicated size of the repository (\"all archives\"), because the two\nmean different things:\n\nThis archive / deduplicated size = amount of data stored ONLY for this archive\n= unique chunks of this archive.\nAll archives / deduplicated size = amount of data stored in the repository\n= all chunks in the repository."
  },
  {
    "path": "docs/usage/key.rst",
    "content": ".. include:: key_change-location.rst.inc\n\n.. _borg-change-passphrase:\n\n.. include:: key_change-passphrase.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Create a key file protected repository\n    $ borg repo-create --encryption=keyfile-aes-ocb -v\n    Initializing repository at \"/path/to/repo\"\n    Enter new passphrase:\n    Enter same passphrase again:\n    Remember your passphrase. Your data will be inaccessible without it.\n    Key in \"/root/.config/borg/keys/mnt_backup\" created.\n    Keep this key safe. Your data will be inaccessible without it.\n    Synchronizing chunks cache...\n    Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0.\n    Done.\n\n    # Change key file passphrase\n    $ borg key change-passphrase -v\n    Enter passphrase for key /root/.config/borg/keys/mnt_backup:\n    Enter new passphrase:\n    Enter same passphrase again:\n    Remember your passphrase. Your data will be inaccessible without it.\n    Key updated\n\n.. note::\n\n    The key file paths shown above are the defaults for Linux (``~/.config/borg/keys/``).\n    On macOS, key files are stored in ``~/Library/Application Support/borg/keys/``.\n    On Windows, they are stored in ``C:\\Users\\<user>\\AppData\\Roaming\\borg\\keys\\``.\n    See :ref:`env_vars` for details.\n\n::\n\n    # Import a previously-exported key into the specified\n    # key file (creating or overwriting the output key)\n    # (keyfile repositories only)\n    $ BORG_KEY_FILE=/path/to/output-key borg key import /path/to/exported\n\nFully automated using environment variables:\n\n::\n\n    $ BORG_NEW_PASSPHRASE=old borg repo-create --encryption=repokey-aes-ocb\n    # now \"old\" is the current passphrase.\n    $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase\n    # now \"new\" is the current passphrase.\n\n\n.. include:: key_export.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    borg key export > encrypted-key-backup\n    borg key export --paper > encrypted-key-backup.txt\n    borg key export --qr-html > encrypted-key-backup.html\n    # Or pass the output file as an argument instead of redirecting stdout:\n    borg key export encrypted-key-backup\n    borg key export --paper encrypted-key-backup.txt\n    borg key export --qr-html encrypted-key-backup.html\n\n.. include:: key_import.rst.inc\n"
  },
  {
    "path": "docs/usage/key_change-location.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_key_change-location:\n\nborg key change-location\n------------------------\n.. code-block:: none\n\n    borg [common options] key change-location [options] KEY_LOCATION\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n    | **positional arguments**                                                                                                                  |\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n    |                                                       | ``KEY_LOCATION`` | select key location                                            |\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n    | **options**                                                                                                                               |\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n    |                                                       | ``--keep``       | keep the key also at the current location (default: remove it) |\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                            |\n    |                                                                                                                                           |\n    | :ref:`common_options`                                                                                                                     |\n    +-------------------------------------------------------+------------------+----------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    KEY_LOCATION\n        select key location\n\n\n    options\n        --keep     keep the key also at the current location (default: remove it)\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nChange the location of a Borg key. The key can be stored at different locations:\n\n- keyfile: locally, usually in the home directory\n- repokey: inside the repository (in the repository config)\n\nPlease note:\n\nThis command does NOT change the crypto algorithms, just the key location,\nthus you must ONLY give the key location (keyfile or repokey)."
  },
  {
    "path": "docs/usage/key_change-passphrase.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_key_change-passphrase:\n\nborg key change-passphrase\n--------------------------\n.. code-block:: none\n\n    borg [common options] key change-passphrase [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                        |\n    |                                                       |\n    | :ref:`common_options`                                 |\n    +-------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThe key files used for repository encryption are optionally passphrase\nprotected. This command can be used to change this passphrase.\n\nPlease note that this command only changes the passphrase, but not any\nsecret protected by it (like e.g. encryption/MAC keys or chunker seed).\nThus, changing the passphrase after passphrase and borg key got compromised\ndoes not protect future (nor past) backups to the same repository."
  },
  {
    "path": "docs/usage/key_export.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_key_export:\n\nborg key export\n---------------\n.. code-block:: none\n\n    borg [common options] key export [options] [PATH]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                       |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    |                                                       | ``PATH``      | where to store the backup                                              |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    | **options**                                                                                                                                    |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    |                                                       | ``--paper``   | Create an export suitable for printing and later type-in               |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    |                                                       | ``--qr-html`` | Create an HTML file suitable for printing and later type-in or QR scan |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                 |\n    |                                                                                                                                                |\n    | :ref:`common_options`                                                                                                                          |\n    +-------------------------------------------------------+---------------+------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    PATH\n        where to store the backup\n\n\n    options\n        --paper       Create an export suitable for printing and later type-in\n        --qr-html     Create an HTML file suitable for printing and later type-in or QR scan\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command backs up the borg key.\n\nIf repository encryption is used, the repository is inaccessible\nwithout the borg key (and the passphrase that protects the borg key).\nIf a repository is not encrypted, but authenticated, the borg key is\nstill needed to access the repository normally.\n\nFor repositories using **keyfile** encryption the key is kept locally\non the system that is capable of doing backups. To guard against loss\nor corruption of this key, the key needs to be backed up independently\nof the main data backup.\n\nFor repositories using **repokey** encryption or **authenticated** mode\nthe key is kept in the repository. A backup is thus not strictly needed,\nbut guards against the repository becoming inaccessible if the key is\ncorrupted or lost.\n\nNote that the backup produced does not include the passphrase itself\n(i.e. the exported key stays encrypted). In order to regain access to a\nrepository, one needs both the exported key and the original passphrase.\nKeep the exported key and the passphrase at safe places.\n\nThere are three backup formats. The normal backup format is suitable for\ndigital storage as a file. The ``--paper`` backup format is optimized\nfor printing and typing in while importing, with per line checks to\nreduce problems with manual input. The ``--qr-html`` creates a printable\nHTML template with a QR code and a copy of the ``--paper``-formatted key."
  },
  {
    "path": "docs/usage/key_import.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_key_import:\n\nborg key import\n---------------\n.. code-block:: none\n\n    borg [common options] key import [options] [PATH]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n    | **positional arguments**                                                                                                       |\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n    |                                                       | ``PATH``    | path to the backup ('-' to read from stdin)              |\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n    | **options**                                                                                                                    |\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n    |                                                       | ``--paper`` | interactively import from a backup done with ``--paper`` |\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                 |\n    |                                                                                                                                |\n    | :ref:`common_options`                                                                                                          |\n    +-------------------------------------------------------+-------------+----------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    PATH\n        path to the backup ('-' to read from stdin)\n\n\n    options\n        --paper     interactively import from a backup done with ``--paper``\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command restores a key previously backed up with the export command.\n\nIf the ``--paper`` option is given, the import will be an interactive\nprocess in which each line is checked for plausibility before\nproceeding to the next line. For this format PATH must not be given.\n\nFor repositories using keyfile encryption, the key file which ``borg key\nimport`` writes to depends on several factors. If the ``BORG_KEY_FILE``\nenvironment variable is set and non-empty, ``borg key import`` creates\nor overwrites that file named by ``$BORG_KEY_FILE``. Otherwise, ``borg\nkey import`` searches in the ``$BORG_KEYS_DIR`` directory for a key file\nassociated with the repository. If a key file is found in\n``$BORG_KEYS_DIR``, ``borg key import`` overwrites it; otherwise, ``borg\nkey import`` creates a new key file in ``$BORG_KEYS_DIR``."
  },
  {
    "path": "docs/usage/list.rst",
    "content": ".. include:: list.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg list root-2016-02-15\n    drwxr-xr-x root   root          0 Mon, 2016-02-15 17:44:27 .\n    drwxrwxr-x root   root          0 Mon, 2016-02-15 19:04:49 bin\n    -rwxr-xr-x root   root    1029624 Thu, 2014-11-13 00:08:51 bin/bash\n    lrwxrwxrwx root   root          0 Fri, 2015-03-27 20:24:26 bin/bzcmp -> bzdiff\n    -rwxr-xr-x root   root       2140 Fri, 2015-03-27 20:24:22 bin/bzdiff\n    ...\n\n    $ borg list root-2016-02-15 --pattern \"- bin/ba*\"\n    drwxr-xr-x root   root          0 Mon, 2016-02-15 17:44:27 .\n    drwxrwxr-x root   root          0 Mon, 2016-02-15 19:04:49 bin\n    lrwxrwxrwx root   root          0 Fri, 2015-03-27 20:24:26 bin/bzcmp -> bzdiff\n    -rwxr-xr-x root   root       2140 Fri, 2015-03-27 20:24:22 bin/bzdiff\n    ...\n\n    $ borg list archiveA --format=\"{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}\"\n    drwxrwxr-x user   user          0 Sun, 2015-02-01 11:00:00 .\n    drwxrwxr-x user   user          0 Sun, 2015-02-01 11:00:00 code\n    drwxrwxr-x user   user          0 Sun, 2015-02-01 11:00:00 code/myproject\n    -rw-rw-r-- user   user    1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext\n    -rw-rw-r-- user   user    1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.text\n    ...\n\n    $ borg list archiveA --pattern '+ re:\\.ext$' --pattern '- re:^.*$'\n    -rw-rw-r-- user   user    1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext\n    ...\n\n    $ borg list archiveA --pattern '+ re:.ext$' --pattern '- re:^.*$'\n    -rw-rw-r-- user   user    1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext\n    -rw-rw-r-- user   user    1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.text\n    ...\n\n"
  },
  {
    "path": "docs/usage/list.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_list:\n\nborg list\n---------\n.. code-block:: none\n\n    borg [common options] list [options] NAME [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                                              |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``NAME``                              | specify the archive name                                                                                                                                                              |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``PATH``                              | paths to list; patterns are supported                                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--short``                           | only print file/directory names, nothing else                                                                                                                                         |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--format FORMAT``                   | specify format for file listing (default: \"{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\")                                                                             |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--json-lines``                      | Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--depth N``                         | only list files up to the specified directory depth                                                                                                                                   |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                        |\n    |                                                                                                                                                                                                                                                                                       |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                 |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                                                                                                           |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN                                                                                                                                                        |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--exclude-from EXCLUDEFILE``        | read exclude patterns from EXCLUDEFILE, one per line                                                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--pattern PATTERN``                 | include/exclude paths matching PATTERN                                                                                                                                                |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--patterns-from PATTERNFILE``       | read include/exclude patterns from PATTERNFILE, one per line                                                                                                                          |\n    +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n    PATH\n        paths to list; patterns are supported\n\n\n    options\n        --short     only print file/directory names, nothing else\n        --format FORMAT    specify format for file listing (default: \"{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\")\n        --json-lines    Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text.\n        --depth N    only list files up to the specified directory depth\n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n\n\nDescription\n~~~~~~~~~~~\n\nThis command lists the contents of an archive.\n\nFor more help on include/exclude patterns, see the output of :ref:`borg_patterns`.\n\n.. man NOTES\n\nThe FORMAT specifier syntax\n+++++++++++++++++++++++++++\n\nThe ``--format`` option uses Python's `format string syntax\n<https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\nExamples:\n::\n\n    $ borg list --format '{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}' ArchiveFoo\n    -rw-rw-r-- user   user       1024 Thu, 2021-12-09 10:22:17 file-foo\n    ...\n\n    # {VAR:<NUMBER} - pad to NUMBER columns left-aligned.\n    # {VAR:>NUMBER} - pad to NUMBER columns right-aligned.\n    $ borg list --format '{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}' ArchiveFoo\n    -rw-rw-r--   user   user 1024     Thu, 2021-12-09 10:22:17 file-foo\n    ...\n\nThe following keys are always available:\n- NEWLINE: OS dependent line separator\n- NL: alias of NEWLINE\n- NUL: NUL character for creating print0 / xargs -0 like output\n- SPACE: space character\n- TAB: tab character\n- CR: carriage return character\n- LF: line feed character\n\n\nKeys available only when listing files in an archive:\n\n- type: file type (file, dir, symlink, ...)\n- mode: file mode (as in stat)\n- uid: user id of file owner\n- gid: group id of file owner\n- user: user name of file owner\n- group: group name of file owner\n- path: file path\n- target: link target for symlinks\n- hlid: hard link identity (same if hardlinking same fs object)\n- inode: inode number\n- flags: file flags\n\n- size: file size\n- num_chunks: number of chunks in this file\n\n- mtime: file modification time\n- ctime: file change time\n- atime: file access time\n- isomtime: file modification time (ISO 8601 format)\n- isoctime: file change time (ISO 8601 format)\n- isoatime: file access time (ISO 8601 format)\n\n- fingerprint: Fingerprint of the file content (may have false negatives), format: H(conditions)-H(chunk_ids)\n- blake2b\n- blake2s\n- md5\n- sha1\n- sha224\n- sha256\n- sha384\n- sha3_224\n- sha3_256\n- sha3_384\n- sha3_512\n- sha512\n- xxh64: XXH64 checksum of this file (note: this is NOT a cryptographic hash!)\n\n- archiveid: internal ID of the archive\n- archivename: name of the archive\n- extra: prepends {target} with \" -> \" for soft links and \" link to \" for hard links\n"
  },
  {
    "path": "docs/usage/lock.rst",
    "content": ".. include:: with-lock.rst.inc\n\n.. include:: break-lock.rst.inc\n"
  },
  {
    "path": "docs/usage/mount.rst",
    "content": ".. include:: mount.rst.inc\n\n.. include:: umount.rst.inc\n\nExamples\n~~~~~~~~\n\n::\n\n    # Mounting the repository shows all archives.\n    # Archives are loaded lazily, expect some delay when navigating to an archive\n    # for the first time.\n    $ borg mount /tmp/mymountpoint\n    $ ls /tmp/mymountpoint\n    root-2016-02-14 root-2016-02-15\n    $ borg umount /tmp/mymountpoint\n\n    # The \"versions view\" merges all archives in the repository\n    # and provides a versioned view on files.\n    $ borg mount -o versions /tmp/mymountpoint\n    $ ls -l /tmp/mymountpoint/home/user/doc.txt/\n    total 24\n    -rw-rw-r-- 1 user group 12357 Aug 26 21:19 doc.cda00bc9.txt\n    -rw-rw-r-- 1 user group 12204 Aug 26 21:04 doc.fa760f28.txt\n    $ borg umount /tmp/mymountpoint\n\n    # Archive filters are supported.\n    # These are especially handy for the \"versions view\",\n    # which does not support lazy processing of archives.\n    $ borg mount -o versions --match-archives 'sh:*-my-home' --last 10 /tmp/mymountpoint\n\n    # Exclusion options are supported.\n    # These can speed up mounting and lower memory needs significantly.\n    $ borg mount /path/to/repo /tmp/mymountpoint only/that/path\n    $ borg mount --exclude '...' /tmp/mymountpoint\n\n\nborgfs\n++++++\n\n::\n\n    $ echo '/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0' >> /etc/fstab\n    $ mount /tmp/myrepo\n    $ ls /tmp/myrepo\n    root-2016-02-01 root-2016-02-15\n\n.. Note::\n\n    ``borgfs`` will be automatically provided if you used a distribution\n    package or ``pip`` to install Borg. Users of the standalone binary will have\n    to manually create a symlink (see :ref:`pyinstaller-binary`).\n"
  },
  {
    "path": "docs/usage/mount.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_mount:\n\nborg mount\n----------\n.. code-block:: none\n\n    borg [common options] mount [options] MOUNTPOINT [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``MOUNTPOINT``                               | where to mount the filesystem                                                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``PATH``                                     | paths to extract; patterns are supported                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-f``, ``--foreground``                     | stay in foreground, do not daemonize                                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-o``                                       | extra mount options                                                                                                         |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--numeric-ids``                            | use numeric user and group identifiers from archives                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-e PATTERN``, ``--exclude PATTERN``        | exclude paths matching PATTERN                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--exclude-from EXCLUDEFILE``               | read exclude patterns from EXCLUDEFILE, one per line                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--pattern PATTERN``                        | include/exclude paths matching PATTERN                                                                                      |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--patterns-from PATTERNFILE``              | read include/exclude patterns from PATTERNFILE, one per line                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--strip-components NUMBER``                | Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.                   |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    MOUNTPOINT\n        where to mount the filesystem\n    PATH\n        paths to extract; patterns are supported\n\n\n    options\n        -f, --foreground    stay in foreground, do not daemonize\n        -o     extra mount options\n        --numeric-ids    use numeric user and group identifiers from archives\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --strip-components NUMBER         Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command mounts a repository or an archive as a FUSE filesystem.\nThis can be useful for browsing or restoring individual files.\n\nWhen restoring, take into account that the current FUSE implementation does\nnot support special fs flags and ACLs.\n\nWhen mounting a repository, the top directories will be named like the\narchives and the directory structure below these will be loaded on-demand from\nthe repository when entering these directories, so expect some delay.\n\nCare should be taken, as Borg backs up symlinks as-is. When an archive \nor repository is mounted, it is possible to “jump” outside the mount point \nby following a symlink. If this happens, files or directories (or versions of them)\nthat are not part of the archive or repository may appear to be within the mount point.\n\nUnless the ``--foreground`` option is given, the command will run in the\nbackground until the filesystem is ``unmounted``.\n\nPerformance tips:\n\n- When doing a \"whole repository\" mount:\n  do not enter archive directories if not needed; this avoids on-demand loading.\n- Only mount a specific archive, not the whole repository.\n- Only mount specific paths in a specific archive, not the complete archive.\n\nThe command ``borgfs`` provides a wrapper for ``borg mount``. This can also be\nused in fstab entries:\n``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0``\n\nTo allow a regular user to use fstab entries, add the ``user`` option:\n``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``\n\nFor FUSE configuration and mount options, see the mount.fuse(8) manual page.\n\nBorg's default behavior is to use the archived user and group names of each\nfile and map them to the system's respective user and group IDs.\nAlternatively, using ``numeric-ids`` will instead use the archived user and\ngroup IDs without any mapping.\n\nThe ``uid`` and ``gid`` mount options (implemented by Borg) can be used to\noverride the user and group IDs of all files (i.e., ``borg mount -o\nuid=1000,gid=1000``).\n\nThe man page references ``user_id`` and ``group_id`` mount options\n(implemented by FUSE) which specify the user and group ID of the mount owner\n(also known as the user who does the mounting). It is set automatically by libfuse (or\nthe filesystem if libfuse is not used). However, you should not specify these\nmanually. Unlike the ``uid`` and ``gid`` mount options, which affect all files,\n``user_id`` and ``group_id`` affect the user and group ID of the mounted\n(base) directory.\n\nAdditional mount options supported by Borg:\n\n- ``versions``: when used with a repository mount, this gives a merged, versioned\n  view of the files in the archives. EXPERIMENTAL; layout may change in the future.\n- ``allow_damaged_files``: by default, damaged files (where chunks are missing)\n  will return EIO (I/O error) when trying to read the related parts of the file.\n  Set this option to replace the missing parts with all-zero bytes.\n- ``ignore_permissions``: for security reasons the ``default_permissions`` mount\n  option is internally enforced by Borg. ``ignore_permissions`` can be given to\n  not enforce ``default_permissions``.\n\nThe BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is intended for advanced users\nto tweak performance. It sets the number of cached data chunks; additional\nmemory usage can be up to ~8 MiB times this number. The default is the number\nof CPU cores.\n\nWhen the daemonized process receives a signal or crashes, it does not unmount.\nUnmounting in these cases could cause an active rsync or similar process\nto delete data unintentionally.\n\nWhen running in the foreground, ^C/SIGINT cleanly unmounts the filesystem,\nbut other signals or crashes do not.\n\nDebugging:\n\n``borg mount`` usually daemonizes and the daemon process sends stdout/stderr\nto /dev/null. Thus, you need to either use ``-f / --foreground`` to make it stay\nin the foreground and not daemonize, or use ``BORG_LOGGING_CONF`` to reconfigure\nthe logger to output to a file."
  },
  {
    "path": "docs/usage/notes.rst",
    "content": "Additional Notes\n----------------\n\nHere are miscellaneous notes about topics that may not be covered in enough detail in the usage section.\n\n.. _chunker-params:\n\n``--chunker-params``\n~~~~~~~~~~~~~~~~~~~~\n\nThe chunker parameters influence how input files are cut into pieces (chunks)\nwhich are then considered for deduplication. They also have a big impact on\nresource usage (RAM and disk space) as the amount of resources needed is\n(also) determined by the total number of chunks in the repository (see\n:ref:`cache-memory-usage` for details).\n\n``--chunker-params=buzhash,10,23,16,4095`` results in a fine-grained deduplication|\nand creates a large number of chunks and thus uses a lot of resources to manage\nthem. This is good for relatively small data volumes and if the machine has a\ngood amount of free RAM and disk space.\n\n``--chunker-params=buzhash,19,23,21,4095`` (default) results in a coarse-grained\ndeduplication and creates a much smaller number of chunks and thus uses less\nresources. This is good for relatively big data volumes and if the machine has\na relatively low amount of free RAM and disk space.\n\n``--chunker-params=fixed,4194304`` results in fixed 4 MiB-sized block\ndeduplication and is more efficient than the previous example when used with\nfor block devices (like disks, partitions, LVM LVs) or raw disk image files.\n\n``--chunker-params=fixed,4096,512`` results in fixed 4 KiB-sized blocks,\nbut the first header block will only be 512B long. This might be useful to\ndedup files with 1 header + N fixed size data blocks. Be careful not to\nproduce too many chunks (for example, using a small block size for huge\nfiles).\n\nIf you have already created some archives in a repository and then change\nchunker parameters, this of course impacts deduplication as the chunks will be\ncut differently.\n\nIn the worst case (all files are big and were touched in between backups), this\nwill store all content into the repository again.\n\nUsually, it is not that bad though:\n\n- usually most files are not touched, so it will just re-use the old chunks\n  it already has in the repo\n- files smaller than the (both old and new) minimum chunk size result in only\n  one chunk anyway, so the resulting chunks are the same and deduplication will apply\n\nIf you switch chunker parameters to save resources for an existing repository that\nalready has some backup archives, you will see an increasing effect over time,\nwhen more and more files have been touched and stored again using the bigger\nchunk size **and** all references to the smaller, older chunks have been removed\n(by deleting / pruning archives).\n\nIf you want to see an immediate, significant effect on resource usage, you should start\na new repository when changing chunker parameters.\n\nFor more details, see :ref:`chunker_details`.\n\n\n``--noatime / --noctime``\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can use these ``borg create`` options to not store the respective timestamp\ninto the archive, in case you do not really need it.\n\nBesides saving a little space by omitting the timestamp, it might also\naffect metadata stream deduplication: if only this timestamp changes between\nbackups and is stored into the metadata stream, the metadata stream chunks\nwill not deduplicate just because of that.\n\n``--nobsdflags / --noflags``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can use this to avoid querying and storing (or extracting and setting) flags — in case\nyou don't need them or if they are broken for your filesystem.\n\nOn Linux, dealing with the flags needs some additional syscalls. Especially when\ndealing with lots of small files, this causes a noticeable overhead, so you can\nuse this option also for speeding up operations.\n\n``--umask``\n~~~~~~~~~~~\n\nborg uses a safe default umask of 077 (that means the files borg creates have\nonly permissions for owner, but no permissions for group and others) - so there\nshould rarely be a need to change the default behaviour.\n\nThis option only affects the process to which it is given. Thus, when you run\nborg in client/server mode and you want to change the behaviour on the server\nside, you need to use ``borg serve --umask=XXX ...`` as a ssh forced command\nin ``authorized_keys``. The ``--umask`` value given on the client side is\n**not** transferred to the server side.\n\nAlso, if you choose to use the ``--umask`` option, always be consistent and use\nthe same umask value so you do not create a mixup of permissions in a borg\nrepository or with other files borg creates.\n\n``--read-special``\n~~~~~~~~~~~~~~~~~~\n\nThe ``--read-special`` option is special - you do not want to use it for normal\nfull-filesystem backups, but rather after carefully picking some targets for it.\n\nThe option ``--read-special`` triggers special treatment for block and char\ndevice files as well as FIFOs. Instead of storing them as such a device (or\nFIFO), they will get opened, their content will be read and in the backup\narchive they will show up like a regular file.\n\nSymlinks will also get special treatment if (and only if) they point to such\na special file: instead of storing them as a symlink, the target special file\nwill get processed as described above.\n\nOne intended use case of this is backing up the contents of one or multiple\nblock devices, like e.g. LVM snapshots or inactive LVs or disk partitions.\n\nYou need to be careful about what you include when using ``--read-special``,\ne.g. if you include ``/dev/zero``, your backup will never terminate.\n\nRestoring such files' content is currently only supported one at a time via\n``--stdout`` option (and you have to redirect stdout to where ever it shall go,\nmaybe directly into an existing device file of your choice or indirectly via\n``dd``).\n\nTo some extent, mounting a backup archive with the backups of special files\nvia ``borg mount`` and then loop-mounting the image files from inside the mount\npoint will work. If you plan to access a lot of data in there, it likely will\nscale and perform better if you do not work via the FUSE mount.\n\nExample\n+++++++\n\nImagine you have made some snapshots of logical volumes (LVs) you want to back up.\n\n.. note::\n\n    For some scenarios, this is a good method to get \"crash-like\" consistency\n    (I call it crash-like because it is the same as you would get if you just\n    hit the reset button or your machine would abruptly and completely crash).\n    This is better than no consistency at all and a good method for some use\n    cases, but likely not good enough if you have databases running.\n\nThen you create a backup archive of all these snapshots. The backup process will\nsee a \"frozen\" state of the logical volumes, while the processes working in the\noriginal volumes continue changing the data stored there.\n\nYou also add the output of ``lvdisplay`` to your backup, so you can see the LV\nsizes in case you ever need to recreate and restore them.\n\nAfter the backup has completed, you remove the snapshots again.\n\n::\n\n    $ # create snapshots here\n    $ lvdisplay > lvdisplay.txt\n    $ borg create --read-special arch lvdisplay.txt /dev/vg0/*-snapshot\n    $ # remove snapshots here\n\nNow, let's see how to restore some LVs from such a backup.\n\n::\n\n    $ borg extract arch lvdisplay.txt\n    $ # create empty LVs with correct sizes here (look into lvdisplay.txt).\n    $ # we assume that you created an empty root and home LV and overwrite it now:\n    $ borg extract --stdout arch dev/vg0/root-snapshot > /dev/vg0/root\n    $ borg extract --stdout arch dev/vg0/home-snapshot > /dev/vg0/home\n\n\n.. _separate_compaction:\n\nSeparate compaction\n~~~~~~~~~~~~~~~~~~~\n\nBorg does not auto-compact the segment files in the repository at commit time\n(at the end of each repository-writing command) any more (since borg 1.2.0).\n\nThis has some notable consequences:\n\n- repository space is not freed immediately when deleting / pruning archives\n- commands finish quicker\n- repository is more robust and might be easier to recover after damages (as\n  it contains data in a more sequential manner, historic manifests, multiple\n  commits - until you run ``borg compact``)\n- user can choose when to run compaction (it should be done regularly, but not\n  necessarily after each single borg command)\n- user can choose from where to invoke ``borg compact`` to do the compaction\n  (from client or from server, it does not need a key)\n- less repo sync data traffic in case you create a copy of your repository by\n  using a sync tool (like rsync, rclone, ...)\n\nYou can manually run compaction by invoking the ``borg compact`` command.\n\nSSH batch mode\n~~~~~~~~~~~~~~\n\nWhen running Borg using an automated script, ``ssh`` might still ask for a password,\neven if there is an SSH key for the target server. Use this to make scripts more robust::\n\n    export BORG_RSH='ssh -oBatchMode=yes'\n\n"
  },
  {
    "path": "docs/usage/prune.rst",
    "content": ".. include:: prune.rst.inc\n\nExamples\n~~~~~~~~\n\nBe careful: prune is a potentially dangerous command that removes backup\narchives.\n\nBy default, prune applies to **all archives in the repository** unless you\nrestrict its operation to a subset of the archives.\n\nThe recommended way to name archives (with ``borg create``) is to use the\nidentical archive name within a series of archives. Then you can simply give\nthat name to prune as well, so it operates only on that series of archives.\n\nAlternatively, you can use ``-a``/``--match-archives`` to match archive names\nand select a subset of them.\nWhen using ``-a``, be careful to choose a good pattern — for example, do not use a\nprefix \"foo\" if you do not also want to match \"foobar\".\n\nIt is strongly recommended to always run ``prune -v --list --dry-run ...``\nfirst, so you will see what it would do without it actually doing anything.\n\nDo not forget to run ``borg compact -v`` after prune to actually free disk space.\n\n::\n\n    # Keep 7 end of day and 4 additional end of week archives.\n    # Do a dry-run without actually deleting anything.\n    $ borg prune -v --list --dry-run --keep-daily=7 --keep-weekly=4\n\n    # Similar to the above, but only apply to the archive series named '{hostname}':\n    $ borg prune -v --list --keep-daily=7 --keep-weekly=4 '{hostname}'\n\n    # Similar to the above, but apply to archive names starting with the hostname\n    # of the machine followed by a '-' character:\n    $ borg prune -v --list --keep-daily=7 --keep-weekly=4 -a 'sh:{hostname}-*'\n\n    # Keep 7 end of day, 4 additional end of week archives,\n    # and an end of month archive for every month:\n    $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --keep-monthly=-1\n\n    # Keep all backups in the last 10 days, 4 additional end of week archives,\n    # and an end of month archive for every month:\n    $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1\n\nThere is also a visualized prune example in ``docs/misc/prune-example.txt``:\n\n.. highlight:: none\n.. include:: ../misc/prune-example.txt\n    :literal:\n"
  },
  {
    "path": "docs/usage/prune.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_prune:\n\nborg prune\n----------\n.. code-block:: none\n\n    borg [common options] prune [options] [NAME]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``NAME``                                     | specify the archive name                                                                           |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                     |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-n``, ``--dry-run``                        | do not change the repository                                                                       |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list``                                   | output a verbose list of archives it keeps/prunes                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--short``                                  | use a less wide archive part format                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list-pruned``                            | output verbose list of archives it prunes                                                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list-kept``                              | output verbose list of archives it keeps                                                           |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--format FORMAT``                          | specify format for the archive part (default: \"{archive:<36} {time} [{id}]\")                       |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-within INTERVAL``                   | keep all archives within this time interval                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-last``, ``--keep-secondly``         | number of secondly archives to keep                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-minutely``                          | number of minutely archives to keep                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-H``, ``--keep-hourly``                    | number of hourly archives to keep                                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-d``, ``--keep-daily``                     | number of daily archives to keep                                                                   |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-w``, ``--keep-weekly``                    | number of weekly archives to keep                                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-m``, ``--keep-monthly``                   | number of monthly archives to keep                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-13weekly``                          | number of quarterly archives to keep (13 week strategy)                                            |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-3monthly``                          | number of quarterly archives to keep (3 month strategy)                                            |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-y``, ``--keep-yearly``                    | number of yearly archives to keep                                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                  |\n    |                                                                                                                                                                                                                                 |\n    | :ref:`common_options`                                                                                                                                                                                                           |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                     |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                      |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m. |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m. |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n\n\n    options\n        -n, --dry-run         do not change the repository\n        --list                output a verbose list of archives it keeps/prunes\n        --short               use a less wide archive part format\n        --list-pruned         output verbose list of archives it prunes\n        --list-kept           output verbose list of archives it keeps\n        --format FORMAT       specify format for the archive part (default: \"{archive:<36} {time} [{id}]\")\n        --keep-within INTERVAL    keep all archives within this time interval\n        --keep-last, --keep-secondly    number of secondly archives to keep\n        --keep-minutely       number of minutely archives to keep\n        -H, --keep-hourly     number of hourly archives to keep\n        -d, --keep-daily      number of daily archives to keep\n        -w, --keep-weekly     number of weekly archives to keep\n        -m, --keep-monthly    number of monthly archives to keep\n        --keep-13weekly       number of quarterly archives to keep (13 week strategy)\n        --keep-3monthly       number of quarterly archives to keep (3 month strategy)\n        -y, --keep-yearly     number of yearly archives to keep\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThe prune command prunes a repository by soft-deleting all archives not\nmatching any of the specified retention options.\n\nImportant:\n\n- The prune command will only mark archives for deletion (\"soft-deletion\"),\n  repository disk space is **not** freed until you run ``borg compact``.\n- You can use ``borg undelete`` to undelete archives, but only until\n  you run ``borg compact``.\n\nThis command is normally used by automated backup scripts wanting to keep a\ncertain number of historic backups. This retention policy is commonly referred to as\n`GFS <https://en.wikipedia.org/wiki/Backup_rotation_scheme#Grandfather-father-son>`_\n(Grandfather-father-son) backup rotation scheme.\n\nThe recommended way to use prune is to give the archive series name to it via the\nNAME argument (assuming you have the same name for all archives in a series).\nAlternatively, you can also use --match-archives (-a), then only archives that\nmatch the pattern are considered for deletion and only those archives count\ntowards the totals specified by the rules.\nOtherwise, *all* archives in the repository are candidates for deletion!\nThere is no automatic distinction between archives representing different\ncontents. These need to be distinguished by specifying matching globs.\n\nIf you have multiple series of archives with different data sets (e.g.\nfrom different machines) in one shared repository, use one prune call per\nseries.\n\nThe ``--keep-within`` option takes an argument of the form \"<int><char>\",\nwhere char is \"y\", \"m\", \"w\", \"d\", \"H\", \"M\", or \"S\".  For example,\n``--keep-within 2d`` means to keep all archives that were created within\nthe past 2 days.  \"1m\" is taken to mean \"31d\". The archives kept with\nthis option do not count towards the totals specified by any other options.\n\nA good procedure is to thin out more and more the older your backups get.\nAs an example, ``--keep-daily 7`` means to keep the latest backup on each day,\nup to 7 most recent days with backups (days without backups do not count).\nThe rules are applied from secondly to yearly, and backups selected by previous\nrules do not count towards those of later rules. The time that each backup\nstarts is used for pruning purposes. Dates and times are interpreted in the local\ntimezone of the system where borg prune runs, and weeks go from Monday to Sunday.\nSpecifying a negative number of archives to keep means that there is no limit.\n\nBorg will retain the oldest archive if any of the secondly, minutely, hourly,\ndaily, weekly, monthly, quarterly, or yearly rules was not otherwise able to\nmeet its retention target. This enables the first chronological archive to\ncontinue aging until it is replaced by a newer archive that meets the retention\ncriteria.\n\nThe ``--keep-13weekly`` and ``--keep-3monthly`` rules are two different\nstrategies for keeping archives every quarter year.\n\nThe ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will\nkeep the last N archives under the assumption that you do not create more than one\nbackup archive in the same second).\n\nYou can influence how the ``--list`` output is formatted by using the ``--short``\noption (less wide output) or by giving a custom format using ``--format`` (see\nthe ``borg repo-list`` description for more details about the format string)."
  },
  {
    "path": "docs/usage/recreate.rst",
    "content": ".. include:: recreate.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Create a backup with fast, low compression\n    $ borg create archive /some/files --compression lz4\n    # Then recompress it — this might take longer, but the backup has already completed,\n    # so there are no inconsistencies from a long-running backup job.\n    $ borg recreate -a archive --recompress --compression zlib,9\n\n    # Remove unwanted files from all archives in a repository.\n    # Note the relative path for the --exclude option — archives only contain relative paths.\n    $ borg recreate --exclude home/icke/Pictures/drunk_photos\n\n    # Change the archive comment\n    $ borg create --comment \"This is a comment\" archivename ~\n    $ borg info -a archivename\n    Name: archivename\n    Fingerprint: ...\n    Comment: This is a comment\n    ...\n    $ borg recreate --comment \"This is a better comment\" -a archivename\n    $ borg info -a archivename\n    Name: archivename\n    Fingerprint: ...\n    Comment: This is a better comment\n    ...\n\n"
  },
  {
    "path": "docs/usage/recreate.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_recreate:\n\nborg recreate\n-------------\n.. code-block:: none\n\n    borg [common options] recreate [options] [PATH...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                                                                                            |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``PATH``                                          | paths to recreate; patterns are supported                                                                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list``                                        | output verbose list of items (files, dirs, ...)                                                                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--filter STATUSCHARS``                          | only display items with the given status characters (listed in borg create --help)                                                                                                                |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-n``, ``--dry-run``                             | do not change anything                                                                                                                                                                            |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-s``, ``--stats``                               | print statistics at end                                                                                                                                                                           |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                                                      |\n    |                                                                                                                                                                                                                                                                                                                                     |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                                               |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Include/Exclude options**                                                                                                                                                                                                                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-e PATTERN``, ``--exclude PATTERN``             | exclude paths matching PATTERN                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--exclude-from EXCLUDEFILE``                    | read exclude patterns from EXCLUDEFILE, one per line                                                                                                                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--pattern PATTERN``                             | include/exclude paths matching PATTERN                                                                                                                                                            |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--patterns-from PATTERNFILE``                   | read include/exclude patterns from PATTERNFILE, one per line                                                                                                                                      |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--exclude-caches``                              | exclude directories that contain a CACHEDIR.TAG file (https://www.bford.info/cachedir/spec.html)                                                                                                  |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--exclude-if-present NAME``                     | exclude directories that are tagged by containing a filesystem object with the given NAME                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--keep-exclude-tags``                           | if tag objects are specified with ``--exclude-if-present``, do not omit the tag objects themselves from the backup archive                                                                        |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                                                                                                         |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN``      | only consider archives matching all patterns. See \"borg help match-archives\".                                                                                                                     |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                                | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp                                                                       |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                     | consider the first N archives after other filters are applied                                                                                                                                     |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                      | consider the last N archives after other filters are applied                                                                                                                                      |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                             | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                                                                                                |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                             | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                                                                                                |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                              | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                              | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                                   |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--target TARGET``                               | create a new archive with the name ARCHIVE, do not replace existing archive                                                                                                                       |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--comment COMMENT``                             | add a comment text to the archive                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--timestamp TIMESTAMP``                         | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the \"borg help compression\" command for details.                                                                                                  |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--chunker-params PARAMS``                       | rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk                   |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    PATH\n        paths to recreate; patterns are supported\n\n\n    options\n        --list          output verbose list of items (files, dirs, ...)\n        --filter STATUSCHARS    only display items with the given status characters (listed in borg create --help)\n        -n, --dry-run    do not change anything\n        -s, --stats     print statistics at end\n\n\n    :ref:`common_options`\n        |\n\n    Include/Exclude options\n        -e PATTERN, --exclude PATTERN     exclude paths matching PATTERN\n        --exclude-from EXCLUDEFILE        read exclude patterns from EXCLUDEFILE, one per line\n        --pattern PATTERN                 include/exclude paths matching PATTERN\n        --patterns-from PATTERNFILE       read include/exclude patterns from PATTERNFILE, one per line\n        --exclude-caches                  exclude directories that contain a CACHEDIR.TAG file (https://www.bford.info/cachedir/spec.html)\n        --exclude-if-present NAME         exclude directories that are tagged by containing a filesystem object with the given NAME\n        --keep-exclude-tags               if tag objects are specified with ``--exclude-if-present``, do not omit the tag objects themselves from the backup archive\n\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n        --target TARGET                          create a new archive with the name ARCHIVE, do not replace existing archive\n        --comment COMMENT                        add a comment text to the archive\n        --timestamp TIMESTAMP                    manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\n        -C COMPRESSION, --compression COMPRESSION    select compression algorithm, see the output of the \"borg help compression\" command for details.\n        --chunker-params PARAMS                  rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk\n\n\nDescription\n~~~~~~~~~~~\n\nRecreate the contents of existing archives.\n\nRecreate is a potentially dangerous function and might lead to data loss\n(if used wrongly). BE VERY CAREFUL!\n\nImportant: Repository disk space is **not** freed until you run ``borg compact``.\n\n``--exclude``, ``--exclude-from``, ``--exclude-if-present``, ``--keep-exclude-tags``\nand PATH have the exact same semantics as in \"borg create\", but they only check\nfiles in the archives and not in the local filesystem. If paths are specified,\nthe resulting archives will contain only files from those paths.\n\nNote that all paths in an archive are relative, therefore absolute patterns/paths\nwill *not* match (``--exclude``, ``--exclude-from``, PATHs).\n\n``--chunker-params`` will re-chunk all files in the archive, this can be\nused to have upgraded Borg 0.xx archives deduplicate with Borg 1.x archives.\n\n**USE WITH CAUTION.**\nDepending on the paths and patterns given, recreate can be used to\ndelete files from archives permanently.\nWhen in doubt, use ``--dry-run --verbose --list`` to see how patterns/paths are\ninterpreted. See :ref:`list_item_flags` in ``borg create`` for details.\n\nThe archive being recreated is only removed after the operation completes. The\narchive that is built during the operation exists at the same time at\n\"<ARCHIVE>.recreate\". The new archive will have a different archive ID.\n\nWith ``--target`` the original archive is not replaced, instead a new archive is created.\n\nWhen rechunking, space usage can be substantial - expect\nat least the entire deduplicated size of the archives using the previous\nchunker params.\n\nIf your most recent borg check found missing chunks, please first run another\nbackup for the same data, before doing any rechunking. If you are lucky, that\nwill recreate the missing chunks. Optionally, do another borg check to see\nif the chunks are still missing."
  },
  {
    "path": "docs/usage/rename.rst",
    "content": ".. include:: rename.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg create archivename ~\n    $ borg repo-list\n    archivename                          Mon, 2016-02-15 19:50:19\n\n    $ borg rename archivename newname\n    $ borg repo-list\n    newname                              Mon, 2016-02-15 19:50:19\n\n"
  },
  {
    "path": "docs/usage/rename.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_rename:\n\nborg rename\n-----------\n.. code-block:: none\n\n    borg [common options] rename [options] OLDNAME NEWNAME\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-------------+----------------------------------+\n    | **positional arguments**                                                                               |\n    +-------------------------------------------------------+-------------+----------------------------------+\n    |                                                       | ``OLDNAME`` | specify the current archive name |\n    +-------------------------------------------------------+-------------+----------------------------------+\n    |                                                       | ``NEWNAME`` | specify the new archive name     |\n    +-------------------------------------------------------+-------------+----------------------------------+\n    | .. class:: borg-common-opt-ref                                                                         |\n    |                                                                                                        |\n    | :ref:`common_options`                                                                                  |\n    +-------------------------------------------------------+-------------+----------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    OLDNAME\n        specify the current archive name\n    NEWNAME\n        specify the new archive name\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command renames an archive in the repository.\n\nThis results in a different archive ID."
  },
  {
    "path": "docs/usage/repo-compress.rst",
    "content": ".. include:: repo-compress.rst.inc\n\nExamples\n~~~~~~~~\n\n::\n\n    # Recompress repository contents\n    $ borg repo-compress --progress --compression=zstd,3\n\n    # Recompress and obfuscate repository contents\n    $ borg repo-compress --progress --compression=obfuscate,1,zstd,3\n"
  },
  {
    "path": "docs/usage/repo-compress.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-compress:\n\nborg repo-compress\n------------------\n.. code-block:: none\n\n    borg [common options] repo-compress [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                  |\n    +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+\n    |                                                       | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the \"borg help compression\" command for details. |\n    +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+\n    |                                                       | ``-s``, ``--stats``                               | print statistics                                                                                 |\n    +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                               |\n    |                                                                                                                                                                                                              |\n    | :ref:`common_options`                                                                                                                                                                                        |\n    +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        -C COMPRESSION, --compression COMPRESSION    select compression algorithm, see the output of the \"borg help compression\" command for details.\n        -s, --stats     print statistics\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nRepository (re-)compression (and/or re-obfuscation).\n\nReads all chunks in the repository and recompresses them if they are not already\nusing the compression type/level and obfuscation level given via ``--compression``.\n\nIf the outcome of the chunk processing indicates a change in compression\ntype/level or obfuscation level, the processed chunk is written to the repository.\nPlease note that the outcome might not always be the desired compression\ntype/level - if no compression gives a shorter output, that might be chosen.\n\nPlease note that this command can not work in low (or zero) free disk space\nconditions.\n\nIf the ``borg repo-compress`` process receives a SIGINT signal (Ctrl-C), the repo\nwill be committed and compacted and borg will terminate cleanly afterwards.\n\nBoth ``--progress`` and ``--stats`` are recommended when ``borg repo-compress``\nis used interactively.\n\nYou do **not** need to run ``borg compact`` after ``borg repo-compress``."
  },
  {
    "path": "docs/usage/repo-create.rst",
    "content": ".. _borg_repo_create:\n\n.. include:: repo-create.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Local repository\n    $ export BORG_REPO=/path/to/repo\n    # Recommended repokey AEAD cryptographic modes\n    $ borg repo-create --encryption=repokey-aes-ocb\n    $ borg repo-create --encryption=repokey-chacha20-poly1305\n    $ borg repo-create --encryption=repokey-blake2-aes-ocb\n    $ borg repo-create --encryption=repokey-blake2-chacha20-poly1305\n    # No encryption (not recommended)\n    $ borg repo-create --encryption=authenticated\n    $ borg repo-create --encryption=authenticated-blake2\n    $ borg repo-create --encryption=none\n\n    # Remote repository (accesses a remote Borg via SSH)\n    $ export BORG_REPO=ssh://user@hostname/~/backup\n    # repokey: stores the encrypted key in <REPO_DIR>/config\n    $ borg repo-create --encryption=repokey-aes-ocb\n    # keyfile: stores the encrypted key in the config dir's keys/ subdir\n    # (e.g. ~/.config/borg/keys/ on Linux, ~/Library/Application Support/borg/keys/ on macOS)\n    $ borg repo-create --encryption=keyfile-aes-ocb\n\n"
  },
  {
    "path": "docs/usage/repo-create.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-create:\n\nborg repo-create\n----------------\n.. code-block:: none\n\n    borg [common options] repo-create [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                         |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--other-repo SRC_REPOSITORY``    | reuse the key material from the other repository                                                                       |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--from-borg1``                   | other repository is Borg 1.x                                                                                           |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-e MODE``, ``--encryption MODE`` | select encryption key mode **(required)**                                                                              |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--copy-crypt-key``               | copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key). |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                      |\n    |                                                                                                                                                                                                                     |\n    | :ref:`common_options`                                                                                                                                                                                               |\n    +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --other-repo SRC_REPOSITORY    reuse the key material from the other repository\n        --from-borg1                   other repository is Borg 1.x\n        -e MODE, --encryption MODE     select encryption key mode **(required)**\n        --copy-crypt-key               copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key).\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command creates a new, empty repository. A repository is a ``borgstore`` store\ncontaining the deduplicated data from zero or more archives.\n\nRepository creation can be quite slow for some kinds of stores (e.g. for ``sftp:``) -\nthis is due to borgstore pre-creating all directories needed, making usage of the\nstore faster.\n\nEncryption mode TL;DR\n+++++++++++++++++++++\n\nThe encryption mode can only be configured when creating a new repository - you can\nneither configure it on a per-archive basis nor change the mode of an existing repository.\nThis example will likely NOT give optimum performance on your machine (performance\ntips will come below):\n\n::\n\n    borg repo-create --encryption repokey-aes-ocb\n\nBorg will:\n\n1. Ask you to come up with a passphrase.\n2. Create a borg key (which contains some random secrets. See :ref:`key_files`).\n3. Derive a \"key encryption key\" from your passphrase\n4. Encrypt and sign the key with the key encryption key\n5. Store the encrypted borg key inside the repository directory (in the repo config).\n   This is why it is essential to use a secure passphrase.\n6. Encrypt and sign your backups to prevent anyone from reading or forging them unless they\n   have the key and know the passphrase. Make sure to keep a backup of\n   your key **outside** the repository - do not lock yourself out by\n   \"leaving your keys inside your car\" (see :ref:`borg_key_export`).\n   The encryption is done locally - if you use a remote repository, the remote machine\n   never sees your passphrase, your unencrypted key or your unencrypted files.\n   Chunking and ID generation are also based on your key to improve\n   your privacy.\n7. Use the key when extracting files to decrypt them and to verify that the contents of\n   the backups have not been accidentally or maliciously altered.\n\nPicking a passphrase\n++++++++++++++++++++\n\nMake sure you use a good passphrase. Not too short, not too simple. The real\nencryption / decryption key is encrypted with / locked by your passphrase.\nIf an attacker gets your key, they cannot unlock and use it without knowing the\npassphrase.\n\nBe careful with special or non-ASCII characters in your passphrase:\n\n- Borg processes the passphrase as Unicode (and encodes it as UTF-8),\n  so it does not have problems dealing with even the strangest characters.\n- BUT: that does not necessarily apply to your OS/VM/keyboard configuration.\n\nSo better use a long passphrase made from simple ASCII characters than one that\nincludes non-ASCII stuff or characters that are hard or impossible to enter on\na different keyboard layout.\n\nYou can change your passphrase for existing repositories at any time; it will not affect\nthe encryption/decryption key or other secrets.\n\nChoosing an encryption mode\n+++++++++++++++++++++++++++\n\nDepending on your hardware, hashing and crypto performance may vary widely.\nThe easiest way to find out what is fastest is to run ``borg benchmark cpu``.\n\n`repokey` modes: if you want ease-of-use and \"passphrase\" security is good enough -\nthe key will be stored in the repository (in ``repo_dir/config``).\n\n`keyfile` modes: if you want \"passphrase and having-the-key\" security -\nthe key will be stored in your home directory (in ``~/.config/borg/keys``).\n\nThe following table is roughly sorted in order of preference, the better ones are\nin the upper part of the table, in the lower part is the old and/or unsafe(r) stuff:\n\n.. nanorst: inline-fill\n\n+-----------------------------------+--------------+----------------+--------------------+\n| Mode (K = keyfile or repokey)     | ID-Hash      | Encryption     | Authentication     |\n+-----------------------------------+--------------+----------------+--------------------+\n| K-blake2-chacha20-poly1305        | BLAKE2b      | CHACHA20       | POLY1305           |\n+-----------------------------------+--------------+----------------+--------------------+\n| K-chacha20-poly1305               | HMAC-SHA-256 | CHACHA20       | POLY1305           |\n+-----------------------------------+--------------+----------------+--------------------+\n| K-blake2-aes-ocb                  | BLAKE2b      | AES256-OCB     | AES256-OCB         |\n+-----------------------------------+--------------+----------------+--------------------+\n| K-aes-ocb                         | HMAC-SHA-256 | AES256-OCB     | AES256-OCB         |\n+-----------------------------------+--------------+----------------+--------------------+\n| authenticated-blake2              | BLAKE2b      | none           | BLAKE2b            |\n+-----------------------------------+--------------+----------------+--------------------+\n| authenticated                     | HMAC-SHA-256 | none           | HMAC-SHA256        |\n+-----------------------------------+--------------+----------------+--------------------+\n| none                              | SHA-256      | none           | none               |\n+-----------------------------------+--------------+----------------+--------------------+\n\n.. nanorst: inline-replace\n\n`none` mode uses no encryption and no authentication. You are advised NOT to use this mode\nas it would expose you to all sorts of issues (DoS, confidentiality, tampering, ...) in\ncase of malicious activity in the repository.\n\nIf you do **not** want to encrypt the contents of your backups, but still want to detect\nmalicious tampering, use an `authenticated` mode. It is like `repokey` minus encryption.\nTo normally work with ``authenticated`` repositories, you will need the passphrase, but\nthere is an emergency workaround; see ``BORG_WORKAROUNDS=authenticated_no_key`` docs.\n\nCreating a related repository\n+++++++++++++++++++++++++++++\n\nYou can use ``borg repo-create --other-repo ORIG_REPO ...`` to create a related repository\nthat uses the same secret key material as the given other/original repository.\n\nBy default, only the ID key and chunker secret will be the same (these are important\nfor deduplication) and the AE crypto keys will be newly generated random keys.\n\nOptionally, if you use ``--copy-crypt-key`` you can also keep the same crypt_key\n(used for authenticated encryption). This might be desired, for example, if you want to have fewer\nkeys to manage.\n\nCreating related repositories is useful, for example, if you want to use ``borg transfer`` later.\n\nCreating a related repository for data migration from Borg 1.2 or 1.4\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nYou can use ``borg repo-create --other-repo ORIG_REPO --from-borg1 ...`` to create a related\nrepository that uses the same secret key material as the given other/original repository.\n\nThen use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives."
  },
  {
    "path": "docs/usage/repo-delete.rst",
    "content": ".. include:: repo-delete.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # delete the whole repository and the related local cache:\n    $ borg repo-delete\n    You requested to DELETE the repository completely *including* all archives it contains:\n    repo                                 Mon, 2016-02-15 19:26:54\n    root-2016-02-15                      Mon, 2016-02-15 19:36:29\n    newname                              Mon, 2016-02-15 19:50:19\n    Type 'YES' if you understand this and want to continue: YES\n\n"
  },
  {
    "path": "docs/usage/repo-delete.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-delete:\n\nborg repo-delete\n----------------\n.. code-block:: none\n\n    borg [common options] repo-delete [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                             |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    |                                                       | ``-n``, ``--dry-run``    | do not change the repository                                                                         |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--list``               | output a verbose list of archives                                                                    |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--force``              | force deletion of corrupted archives; use ``--force --force`` if a single ``--force`` does not work. |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--cache-only``         | delete only the local cache for the given repository                                                 |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--keep-security-info`` | keep the local security info when deleting a repository                                              |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                          |\n    |                                                                                                                                                                                         |\n    | :ref:`common_options`                                                                                                                                                                   |\n    +-------------------------------------------------------+--------------------------+------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        -n, --dry-run     do not change the repository\n        --list            output a verbose list of archives\n        --force           force deletion of corrupted archives; use ``--force --force`` if a single ``--force`` does not work.\n        --cache-only      delete only the local cache for the given repository\n        --keep-security-info    keep the local security info when deleting a repository\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command deletes a complete repository.\n\nWhen you delete a complete repository, the security info and local cache for it\n(if any) are also deleted. Alternatively, you can delete just the local cache\nwith the ``--cache-only`` option, or keep the security info with the\n``--keep-security-info`` option.\n\nAlways first use ``--dry-run --list`` to see what would be deleted."
  },
  {
    "path": "docs/usage/repo-info.rst",
    "content": ".. include:: repo-info.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg repo-info\n    Repository ID: 0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\n    Location: /Users/tw/w/borg/path/to/repo\n    Encrypted: Yes (repokey AES-OCB)\n    Cache: /Users/tw/.cache/borg/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\n    Security dir: /Users/tw/.config/borg/security/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9\n    Original size: 152.14 MB\n    Deduplicated size: 30.38 MB\n    Unique chunks: 654\n    Total chunks: 3302\n\n"
  },
  {
    "path": "docs/usage/repo-info.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-info:\n\nborg repo-info\n--------------\n.. code-block:: none\n\n    borg [common options] repo-info [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+------------+-----------------------+\n    | **options**                                                                                |\n    +-------------------------------------------------------+------------+-----------------------+\n    |                                                       | ``--json`` | format output as JSON |\n    +-------------------------------------------------------+------------+-----------------------+\n    | .. class:: borg-common-opt-ref                                                             |\n    |                                                                                            |\n    | :ref:`common_options`                                                                      |\n    +-------------------------------------------------------+------------+-----------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --json     format output as JSON\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command displays detailed information about the repository."
  },
  {
    "path": "docs/usage/repo-list.rst",
    "content": ".. include:: repo-list.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    $ borg repo-list\n    151b1a57  Mon, 2024-09-23 22:57:11 +0200  docs             tw          MacBook-Pro  this is a comment\n    3387a079  Thu, 2024-09-26 09:07:07 +0200  scripts          tw          MacBook-Pro\n    ca774425  Thu, 2024-09-26 10:05:23 +0200  scripts          tw          MacBook-Pro\n    ba56c4a5  Thu, 2024-09-26 10:12:45 +0200  src              tw          MacBook-Pro\n    7567b79a  Thu, 2024-09-26 10:15:07 +0200  scripts          tw          MacBook-Pro\n    21ab3600  Thu, 2024-09-26 10:15:17 +0200  docs             tw          MacBook-Pro\n    ...\n\n"
  },
  {
    "path": "docs/usage/repo-list.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-list:\n\nborg repo-list\n--------------\n.. code-block:: none\n\n    borg [common options] repo-list [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--short``                                  | only print the archive IDs, nothing else                                                                                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--format FORMAT``                          | specify format for archive listing (default: \"{archive:<36} {time} [{id}]{NL}\")                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--json``                                   | Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                               |\n    |                                                                                                                                                                                                                                                                                                              |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                                                                                  |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                                                                                   |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp                                                     |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                                                                                   |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--deleted``                                | consider only soft-deleted archives.                                                                                                                                            |\n    +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --short     only print the archive IDs, nothing else\n        --format FORMAT    specify format for archive listing (default: \"{archive:<36} {time} [{id}]{NL}\")\n        --json      Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text.\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n        --deleted                                consider only soft-deleted archives.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command lists the archives contained in a repository.\n\n.. man NOTES\n\nThe FORMAT specifier syntax\n+++++++++++++++++++++++++++\n\nThe ``--format`` option uses Python's `format string syntax\n<https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\nExamples:\n::\n\n    $ borg repo-list --format '{archive}{NL}'\n    ArchiveFoo\n    ArchiveBar\n    ...\n\n    # {VAR:NUMBER} - pad to NUMBER columns.\n    # Strings are left-aligned, numbers are right-aligned.\n    # Note: time columns except ``isomtime``, ``isoctime`` and ``isoatime`` cannot be padded.\n    $ borg repo-list --format '{archive:36} {time} [{id}]{NL}' /path/to/repo\n    ArchiveFoo                           Thu, 2021-12-09 10:22:28 [0b8e9...3b274]\n    ...\n\nThe following keys are always available:\n- NEWLINE: OS dependent line separator\n- NL: alias of NEWLINE\n- NUL: NUL character for creating print0 / xargs -0 like output\n- SPACE: space character\n- TAB: tab character\n- CR: carriage return character\n- LF: line feed character\n\n\nKeys available only when listing archives in a repository:\n\n- archive: archive name\n- name: alias of \"archive\"\n- comment: archive comment\n- id: internal ID of the archive\n- tags: archive tags\n\n- time: nominal time of the archive\n- start: start time of the archive operation\n- end: end time of the archive operation\n- command_line: command line which was used to create the archive\n\n- hostname: hostname of host on which this archive was created\n- username: username of user who created this archive\n\n- size: size of this archive (data plus metadata, not considering compression and deduplication)\n- nfiles: count of files in this archive\n"
  },
  {
    "path": "docs/usage/repo-space.rst",
    "content": ".. include:: repo-space.rst.inc\n"
  },
  {
    "path": "docs/usage/repo-space.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_repo-space:\n\nborg repo-space\n---------------\n.. code-block:: none\n\n    borg [common options] repo-space [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+---------------------+----------------------------------------------------------------------+\n    | **options**                                                                                                                                        |\n    +-------------------------------------------------------+---------------------+----------------------------------------------------------------------+\n    |                                                       | ``--reserve SPACE`` | Amount of space to reserve (e.g. 100M, 1G). Default: 0.              |\n    +-------------------------------------------------------+---------------------+----------------------------------------------------------------------+\n    |                                                       | ``--free``          | Free all reserved space. Do not forget to reserve space again later. |\n    +-------------------------------------------------------+---------------------+----------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                     |\n    |                                                                                                                                                    |\n    | :ref:`common_options`                                                                                                                              |\n    +-------------------------------------------------------+---------------------+----------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --reserve SPACE     Amount of space to reserve (e.g. 100M, 1G). Default: 0.\n        --free              Free all reserved space. Do not forget to reserve space again later.\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command manages reserved space in a repository.\n\nBorg cannot work in disk-full conditions (it cannot lock a repository and thus cannot\nrun prune/delete or compact operations to free disk space).\n\nTo avoid running into such dead-end situations, you can put some objects into a\nrepository that take up disk space. If you ever run into a disk-full situation, you\ncan free that space, and then Borg will be able to run normally so you can free more\ndisk space by using ``borg prune``/``borg delete``/``borg compact``. After that, do\nnot forget to reserve space again, in case you run into that situation again later.\n\nExamples::\n\n    # Create a new repository:\n    $ borg repo-create ...\n    # Reserve approx. 1 GiB of space for emergencies:\n    $ borg repo-space --reserve 1G\n\n    # Check the amount of reserved space in the repository:\n    $ borg repo-space\n\n    # EMERGENCY! Free all reserved space to get things back to normal:\n    $ borg repo-space --free\n    $ borg prune ...\n    $ borg delete ...\n    $ borg compact -v  # only this actually frees space of deleted archives\n    $ borg repo-space --reserve 1G  # reserve space again for next time\n\nReserved space is always rounded up to full reservation blocks of 64 MiB."
  },
  {
    "path": "docs/usage/serve.rst",
    "content": ".. include:: serve.rst.inc\n\nExamples\n~~~~~~~~\n\n``borg serve`` has special support for ssh forced commands (see ``authorized_keys``\nexample below): if the environment variable SSH_ORIGINAL_COMMAND is set it will\nignore some options given on the command line and use the values from the\nvariable instead. This only applies to a carefully controlled allowlist of safe\noptions. This list currently contains:\n\n- Options that control the log level and debug topics printed\n  such as ``--verbose``, ``--info``, ``--debug``, ``--debug-topic``, etc.\n- ``--lock-wait`` to allow the client to control how long to wait before\n  giving up and aborting the operation when another process is holding a lock.\n\nEnvironment variables (such as BORG_XXX) contained in the original\ncommand sent by the client are *not* interpreted, but ignored. If BORG_XXX environment\nvariables should be set on the ``borg serve`` side, then these must be set in system-specific\nlocations like ``/etc/environment`` or in the forced command itself (example below).\n\n::\n\n    # Allow an SSH keypair to run only borg, and only have access to /path/to/repo.\n    # Use key options to disable unneeded and potentially dangerous SSH functionality.\n    # This will help to secure an automated remote backup system.\n    $ cat ~/.ssh/authorized_keys\n    command=\"borg serve --restrict-to-path /path/to/repo\",restrict ssh-rsa AAAAB3[...]\n\n    # Specify repository permissions for an SSH keypair.\n    $ cat ~/.ssh/authorized_keys\n    command=\"borg serve --permissions=read-only\",restrict ssh-rsa AAAAB3[...]\n\n    # Set a BORG_XXX environment variable on the \"borg serve\" side\n    $ cat ~/.ssh/authorized_keys\n    command=\"BORG_XXX=value borg serve [...]\",restrict ssh-rsa [...]\n\n.. note::\n    The examples above use the ``restrict`` directive and assume a POSIX\n    compliant shell set as the user's login shell.\n    This automatically blocks potentially dangerous SSH features, even when\n    they are added in a future update. Thus, this option should be preferred.\n\n    If you are using OpenSSH server < 7.2, however, you must explicitly\n    specify the SSH features to restrict and cannot simply use the ``restrict`` option, as it\n    was introduced in v7.2. We recommend using\n    ``no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc``\n    in this case.\n\nDetails about sshd usage: `sshd(8) <https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8>`_\n\n.. _ssh_configuration:\n\nSSH Configuration\n~~~~~~~~~~~~~~~~~\n\n``borg serve``'s pipes (``stdin``/``stdout``/``stderr``) are connected to the ``sshd`` process on the server side. In the event that the SSH connection between ``borg serve`` and the client is disconnected or stuck abnormally (for example, due to a network outage), it can take a long time for ``sshd`` to notice the client is disconnected. In the meantime, ``sshd`` continues running, and as a result so does the ``borg serve`` process holding the lock on the repository. This can cause subsequent ``borg`` operations on the remote repository to fail with the error: ``Failed to create/acquire the lock``.\n\nTo avoid this, it is recommended to perform the following additional SSH configuration:\n\nEither in the client-side ``~/.ssh/config`` file or in the client's ``/etc/ssh/ssh_config`` file:\n::\n\n    Host backupserver\n            ServerAliveInterval 10\n            ServerAliveCountMax 30\n\nReplacing ``backupserver`` with the hostname, FQDN or IP address of the borg server.\n\nThis will cause the client to send a keepalive to the server every 10 seconds. If 30 consecutive keepalives are sent without a response (a time of 300 seconds), the SSH client process will be terminated, causing the borg process to terminate gracefully.\n\nIn the server-side ``sshd`` configuration file (typically ``/etc/ssh/sshd_config``):\n::\n\n    ClientAliveInterval 10\n    ClientAliveCountMax 30\n\nThis will cause the server to send a keepalive to the client every 10 seconds. If 30 consecutive keepalives are sent without a response (a time of 300 seconds), the server's sshd process will be terminated, causing the ``borg serve`` process to terminate gracefully and release the lock on the repository.\n\nIf you then run borg commands with ``--lock-wait 600``, this gives sufficient time for the borg serve processes to terminate after the SSH connection is torn down after the 300 second wait for the keepalives to fail.\n\nYou may, of course, modify the timeout values demonstrated above to values that suit your environment and use case.\n\nWhen the client is untrusted, it is a good idea to set the backup\nuser's shell to a simple implementation (``/bin/sh`` is only an example and may or may\nnot be such a simple implementation)::\n\n  chsh -s /bin/sh BORGUSER\n\nBecause the configured shell is used by `OpenSSH <https://www.openssh.com/>`_\nto execute the command configured through the ``authorized_keys`` file\nusing ``\"$SHELL\" -c \"$COMMAND\"``,\nsetting a minimal shell implementation reduces the attack surface\ncompared to when a feature-rich and complex shell implementation is\nused.\n"
  },
  {
    "path": "docs/usage/serve.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_serve:\n\nborg serve\n----------\n.. code-block:: none\n\n    borg [common options] serve [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--restrict-to-path PATH``       | Restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all subdirectories is granted implicitly; PATH does not need to point directly to a repository.                                                                                                                                                                                                                                 |\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--restrict-to-repository PATH`` | Restrict repository access. Only the repository located at PATH (no subdirectories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike ``--restrict-to-path``, subdirectories are not accessible; PATH must point directly to a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. |\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                       | ``--permissions``                 | Set repository permission mode. Overrides BORG_REPO_PERMISSIONS environment variable.                                                                                                                                                                                                                                                                                                                                                                            |\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |\n    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n    +-------------------------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        --restrict-to-path PATH           Restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all subdirectories is granted implicitly; PATH does not need to point directly to a repository.\n        --restrict-to-repository PATH     Restrict repository access. Only the repository located at PATH (no subdirectories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike ``--restrict-to-path``, subdirectories are not accessible; PATH must point directly to a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there.\n        --permissions                     Set repository permission mode. Overrides BORG_REPO_PERMISSIONS environment variable.\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command starts a repository server process.\n\n`borg serve` currently supports:\n\n- Being automatically started via SSH when the borg client uses an ssh://...\n  remote repository. In this mode, `borg serve` will run until that SSH connection\n  is terminated.\n\n- Being started by some other means (not by the borg client) as a long-running socket\n  server to be used for borg clients using a socket://... repository (see the `--socket`\n  option if you do not want to use the default path for the socket and PID file).\n\nPlease note that `borg serve` does not support providing a specific repository via the\n`--repo` option or the `BORG_REPO` environment variable. It is always the borg client that\nspecifies the repository to use when communicating with `borg serve`.\n\nThe --permissions option enforces repository permissions:\n\n- `all`: All permissions are granted. (Default; the permissions system is not used.)\n- `no-delete`: Allow reading and writing; disallow deleting and overwriting data.\n  New archives can be created; existing archives cannot be deleted. New chunks can\n  be added; existing chunks cannot be deleted or overwritten.\n- `write-only`: Allow writing; disallow reading data.\n  New archives can be created; existing archives cannot be read.\n  New chunks can be added; existing chunks cannot be read, deleted, or overwritten.\n- `read-only`: Allow reading; disallow writing or deleting data.\n  Existing archives can be read, but no archives can be created or deleted."
  },
  {
    "path": "docs/usage/tag.rst",
    "content": ".. include:: tag.rst.inc\n"
  },
  {
    "path": "docs/usage/tag.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_tag:\n\nborg tag\n--------\n.. code-block:: none\n\n    borg [common options] tag [options] [NAME]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``NAME``                                     | specify the archive name                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--set TAG``                                | set tags                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--add TAG``                                | add tags                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--remove TAG``                             | remove tags                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n\n\n    options\n        --set TAG     set tags\n        --add TAG     add tags\n        --remove TAG    remove tags\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nManage archive tags.\n\nBorg archives can have a set of tags which can be used for matching archives.\n\nYou can set the tags to a specific set of tags or you can add or remove\ntags from the current set of tags.\n\nUser-defined tags must not start with `@` because such tags are considered\nspecial and users are only allowed to use known special tags:\n\n``@PROT``: protects archives against archive deletion or pruning.\n\nPre-existing special tags cannot be removed via ``--set``. You can still use\n``--set``, but you must also give pre-existing special tags (so they won't be\nremoved)."
  },
  {
    "path": "docs/usage/tar.rst",
    "content": ".. include:: export-tar.rst.inc\n\n.. include:: import-tar.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # Export as an uncompressed tar archive\n    $ borg export-tar Monday Monday.tar\n\n    # Import an uncompressed tar archive\n    $ borg import-tar Monday Monday.tar\n\n    # Exclude some file types and compress using gzip\n    $ borg export-tar Monday Monday.tar.gz --exclude '*.so'\n\n    # Use a higher compression level with gzip\n    $ borg export-tar --tar-filter=\"gzip -9\" Monday Monday.tar.gz\n\n    # Copy an archive from repoA to repoB\n    $ borg -r repoA export-tar --tar-format=BORG archive - | borg -r repoB import-tar archive -\n\n    # Export a tar, but instead of storing it on disk, upload it to a remote site using curl\n    $ borg export-tar Monday - | curl --data-binary @- https://somewhere/to/POST\n\n    # Remote extraction via 'tarpipe'\n    $ borg export-tar Monday - | ssh somewhere \"cd extracted; tar x\"\n\nArchives transfer script\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nOutputs a script that copies all archives from repo1 to repo2:\n\n::\n\n    for N I T in `borg list --format='{archive} {id} {time:%Y-%m-%dT%H:%M:%S}{NL}'`\n    do\n      echo \"borg -r repo1 export-tar --tar-format=BORG aid:$I - | borg -r repo2 import-tar --timestamp=$T $N -\"\n    done\n\nKept:\n\n- archive name, archive timestamp\n- archive contents (all items with metadata and data)\n\nLost:\n\n- some archive metadata (like the original command line, execution time, etc.)\n\nPlease note:\n\n- all data goes over that pipe, again and again for every archive\n- the pipe is dumb, there is no data or transfer time reduction there due to deduplication\n- maybe add compression\n- pipe over ssh for remote transfer\n- no special sparse file support\n"
  },
  {
    "path": "docs/usage/transfer.rst",
    "content": ".. include:: transfer.rst.inc\n\nExamples\n~~~~~~~~\n::\n\n    # 0. Have Borg 2.0 installed on the client AND server; have a b12 repository copy for testing.\n\n    # 1. Create a new \"related\" repository:\n    # Here, the existing Borg 1.2 repository used repokey-blake2 (and AES-CTR mode),\n    # thus we use repokey-blake2-aes-ocb for the new Borg 2.0 repository.\n    # Staying with the same chunk ID algorithm (BLAKE2) and with the same\n    # key material (via --other-repo <oldrepo>) will make deduplication work\n    # between old archives (copied with borg transfer) and future ones.\n    # The AEAD cipher does not matter (everything must be re-encrypted and\n    # re-authenticated anyway); you could also choose repokey-blake2-chacha20-poly1305.\n    # In case your old Borg repository did not use BLAKE2, just remove the \"-blake2\".\n    $ borg --repo       ssh://borg2@borgbackup/./tests/b20 repo-create \\\n           --other-repo ssh://borg2@borgbackup/./tests/b12 -e repokey-blake2-aes-ocb\n\n    # 2. Check what and how much it would transfer:\n    $ borg --repo       ssh://borg2@borgbackup/./tests/b20 transfer --upgrader=From12To20 \\\n           --other-repo ssh://borg2@borgbackup/./tests/b12 --dry-run\n\n    # 3. Transfer (copy) archives from the old repository into the new repository (takes time and space!):\n    $ borg --repo       ssh://borg2@borgbackup/./tests/b20 transfer --upgrader=From12To20 \\\n           --other-repo ssh://borg2@borgbackup/./tests/b12\n\n    # 4. Check whether we have everything (same as step 2):\n    $ borg --repo       ssh://borg2@borgbackup/./tests/b20 transfer --upgrader=From12To20 \\\n           --other-repo ssh://borg2@borgbackup/./tests/b12 --dry-run\n\nKeyfile considerations when upgrading from borg 1.x\n++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nIf you are using a ``keyfile`` encryption mode (not ``repokey``), borg 2\nmay not automatically find your borg 1.x key file, because the default\nkey file directory has changed on some platforms due to the switch to\nthe `platformdirs <https://pypi.org/project/platformdirs/>`_ library.\n\nOn **Linux**, there is typically no change -- both borg 1.x and borg 2\nuse ``~/.config/borg/keys/``.\n\nOn **macOS**, borg 1.x stored key files in ``~/.config/borg/keys/``,\nbut borg 2 defaults to ``~/Library/Application Support/borg/keys/``.\n\nOn **Windows**, borg 1.x used XDG-style paths (e.g. ``~/.config/borg/keys/``),\nwhile borg 2 defaults to ``C:\\Users\\<user>\\AppData\\Roaming\\borg\\keys\\``.\n\nIf borg 2 cannot find your key file, you have several options:\n\n1. **Copy the key file** from the old location to the new one.\n2. **Set BORG_KEYS_DIR** to point to the old key file directory::\n\n       export BORG_KEYS_DIR=~/.config/borg/keys\n\n3. **Set BORG_KEY_FILE** to point directly to the specific key file::\n\n       export BORG_KEY_FILE=~/.config/borg/keys/your_key_file\n\n4. **Set BORG_BASE_DIR** to force borg 2 to use the same base directory\n   as borg 1.x::\n\n       export BORG_BASE_DIR=$HOME\n\n   This makes borg 2 use ``$HOME/.config/borg``, ``$HOME/.cache/borg``,\n   etc., matching borg 1.x behaviour on all platforms.\n\nSee :ref:`env_vars` for more details on directory environment variables.\n\n"
  },
  {
    "path": "docs/usage/transfer.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_transfer:\n\nborg transfer\n-------------\n.. code-block:: none\n\n    borg [common options] transfer [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-n``, ``--dry-run``                             | do not change repository, just check                                                                                                                                                                                                                                                                                      |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--other-repo SRC_REPOSITORY``                   | transfer archives from the other repository                                                                                                                                                                                                                                                                               |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--from-borg1``                                  | other repository is borg 1.x                                                                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--upgrader UPGRADER``                           | use the upgrader to convert transferred data (default: no conversion)                                                                                                                                                                                                                                                     |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the \"borg help compression\" command for details.                                                                                                                                                                                                                          |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--recompress MODE``                             | recompress data chunks according to `MODE` and ``--compression``. Possible modes are `always`: recompress unconditionally; and `never`: do not recompress (faster: re-uses compressed data chunks w/o change).If no MODE is given, `always` will be used. Not passing --recompress is equivalent to \"--recompress never\". |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--chunker-params PARAMS``                       | rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk                                                                                                                                           |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                                                                                                                                                                                                                              |\n    |                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n    | :ref:`common_options`                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN``      | only consider archives matching all patterns. See \"borg help match-archives\".                                                                                                                                                                                                                                             |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                                | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp                                                                                                                                                                                               |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                     | consider the first N archives after other filters are applied                                                                                                                                                                                                                                                             |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                      | consider the last N archives after other filters are applied                                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                             | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                             | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                                                                                                                                                                                                                        |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                              | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                                                                                                                                                           |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                              | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                                                                                                                                                                                                                           |\n    +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    options\n        -n, --dry-run     do not change repository, just check\n        --other-repo SRC_REPOSITORY    transfer archives from the other repository\n        --from-borg1      other repository is borg 1.x\n        --upgrader UPGRADER    use the upgrader to convert transferred data (default: no conversion)\n        -C COMPRESSION, --compression COMPRESSION    select compression algorithm, see the output of the \"borg help compression\" command for details.\n        --recompress MODE    recompress data chunks according to `MODE` and ``--compression``. Possible modes are `always`: recompress unconditionally; and `never`: do not recompress (faster: re-uses compressed data chunks w/o change).If no MODE is given, `always` will be used. Not passing --recompress is equivalent to \"--recompress never\".\n        --chunker-params PARAMS    rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command transfers archives from one repository to another repository.\nOptionally, it can also upgrade the transferred data.\nOptionally, it can also recompress the transferred data.\nOptionally, it can also re-chunk the transferred data using different chunker parameters.\n\nIt is easiest (and fastest) to give ``--compression=COMPRESSION --recompress=never`` using\nthe same COMPRESSION mode as in the SRC_REPO - borg will use that COMPRESSION for metadata (in\nany case) and keep data compressed \"as is\" (saves time as no data compression is needed).\n\nIf you want to globally change compression while transferring archives to the DST_REPO,\ngive ``--compress=WANTED_COMPRESSION --recompress=always``.\n\nThe default is to transfer all archives.\n\nYou could use the misc. archive filter options to limit which archives it will\ntransfer, e.g. using the ``-a`` option. This is recommended for big\nrepositories with multiple data sets to keep the runtime per invocation lower.\n\nGeneral purpose archive transfer\n++++++++++++++++++++++++++++++++\n\nTransfer borg2 archives into a related other borg2 repository::\n\n    # create a related DST_REPO (reusing key material from SRC_REPO), so that\n    # chunking and chunk id generation will work in the same way as before.\n    borg --repo=DST_REPO repo-create --encryption=DST_ENC --other-repo=SRC_REPO\n\n    # transfer archives from SRC_REPO to DST_REPO\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run  # check what it would do\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO            # do it!\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run  # check! anything left?\n\nData migration / upgrade from borg 1.x\n++++++++++++++++++++++++++++++++++++++\n\nTo migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar\nto the above, but you need the ``--from-borg1`` option::\n\n    borg --repo=DST_REPO repocreate --encryption=DST_ENC --other-repo=SRC_REPO --from-borg1\n\n    # to continue using lz4 compression as you did in SRC_REPO:\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\\n         --compress=lz4 --recompress=never\n\n    # alternatively, to recompress everything to zstd,3:\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\\n         --compress=zstd,3 --recompress=always\n\n    # to re-chunk using different chunker parameters:\n    borg --repo=DST_REPO transfer --other-repo=SRC_REPO \\\n         --chunker-params=buzhash,19,23,21,4095\n"
  },
  {
    "path": "docs/usage/umount.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_umount:\n\nborg umount\n-----------\n.. code-block:: none\n\n    borg [common options] umount [options] MOUNTPOINT\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+----------------+-----------------------------------------+\n    | **positional arguments**                                                                                         |\n    +-------------------------------------------------------+----------------+-----------------------------------------+\n    |                                                       | ``MOUNTPOINT`` | mountpoint of the filesystem to unmount |\n    +-------------------------------------------------------+----------------+-----------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                   |\n    |                                                                                                                  |\n    | :ref:`common_options`                                                                                            |\n    +-------------------------------------------------------+----------------+-----------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    MOUNTPOINT\n        mountpoint of the filesystem to unmount\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command unmounts a FUSE filesystem that was mounted with ``borg mount``.\n\nThis is a convenience wrapper that just calls the platform-specific shell\ncommand - usually this is either umount or fusermount -u."
  },
  {
    "path": "docs/usage/undelete.rst",
    "content": ".. include:: undelete.rst.inc\n"
  },
  {
    "path": "docs/usage/undelete.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_undelete:\n\nborg undelete\n-------------\n.. code-block:: none\n\n    borg [common options] undelete [options] [NAME]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **positional arguments**                                                                                                                                                                                                                                 |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``NAME``                                     | specify the archive name                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **options**                                                                                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-n``, ``--dry-run``                        | do not change the repository                                                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--list``                                   | output a verbose list of archives                                                                                           |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |\n    |                                                                                                                                                                                                                                                          |\n    | :ref:`common_options`                                                                                                                                                                                                                                    |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. See \"borg help match-archives\".                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--first N``                                | consider the first N archives after other filters are applied                                                               |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--last N``                                 | consider the last N archives after other filters are applied                                                                |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.                          |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.                                                             |\n    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    NAME\n        specify the archive name\n\n\n    options\n        -n, --dry-run     do not change the repository\n        --list            output a verbose list of archives\n\n\n    :ref:`common_options`\n        |\n\n    Archive filters\n        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. See \"borg help match-archives\".\n        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp\n        --first N                                consider the first N archives after other filters are applied\n        --last N                                 consider the last N archives after other filters are applied\n        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\n        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\n        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\n        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\n\n\nDescription\n~~~~~~~~~~~\n\nThis command undeletes archives in the repository.\n\nImportant: Undeleting archives is only possible before compacting.\nOnce ``borg compact`` has run, all disk space occupied only by the\nsoft-deleted archives will be freed, and undeleting is no longer\npossible.\n\nWhen in doubt, use ``--dry-run --list`` to see what would be\nundeleted.\n\nYou can undelete multiple archives by specifying a match pattern using\nthe ``--match-archives PATTERN`` option (for more information on these\npatterns, see :ref:`borg_patterns`)."
  },
  {
    "path": "docs/usage/usage_general.rst.inc",
    "content": ".. include:: general/positional-arguments.rst.inc\n\n.. include:: general/repository-urls.rst.inc\n\n.. include:: general/repository-locations.rst.inc\n\n.. include:: general/archive-specification.rst.inc\n\n.. include:: general/logging.rst.inc\n\n.. include:: general/return-codes.rst.inc\n\n.. _config:\n\n.. include:: general/config.rst.inc\n\n.. _env_vars:\n\n.. include:: general/environment.rst.inc\n\n.. _file-systems:\n\n.. include:: general/file-systems.rst.inc\n\n.. include:: general/units.rst.inc\n\n.. include:: general/date-time.rst.inc\n\n.. include:: general/resources.rst.inc\n\n.. _platforms:\n\n.. include:: general/file-metadata.rst.inc\n"
  },
  {
    "path": "docs/usage/version.rst",
    "content": ".. include:: version.rst.inc\n"
  },
  {
    "path": "docs/usage/version.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_version:\n\nborg version\n------------\n.. code-block:: none\n\n    borg [common options] version [options]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+\n    | .. class:: borg-common-opt-ref                        |\n    |                                                       |\n    | :ref:`common_options`                                 |\n    +-------------------------------------------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command displays the Borg client and server versions.\n\nIf a local repository is given, the client code directly accesses the repository,\nso the client version is also shown as the server version.\n\nIf a remote repository is given (e.g., ssh:), the remote Borg is queried, and\nits version is displayed as the server version.\n\nExamples::\n\n    # local repository (client uses 1.4.0 alpha version)\n    $ borg version /mnt/backup\n    1.4.0a / 1.4.0a\n\n    # remote repository (client uses 1.4.0 alpha, server uses 1.2.7 release)\n    $ borg version ssh://borg@borgbackup:repo\n    1.4.0a / 1.2.7\n\nDue to the version tuple format used in Borg client/server negotiation, only\na simplified version is displayed (as provided by borg.version.format_version).\n\nYou can also use ``borg --version`` to display a potentially more precise client version."
  },
  {
    "path": "docs/usage/with-lock.rst.inc",
    "content": ".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n.. _borg_with-lock:\n\nborg with-lock\n--------------\n.. code-block:: none\n\n    borg [common options] with-lock [options] COMMAND [ARGS...]\n\n.. only:: html\n\n    .. class:: borg-options-table\n\n    +-------------------------------------------------------+-------------+-------------------+\n    | **positional arguments**                                                                |\n    +-------------------------------------------------------+-------------+-------------------+\n    |                                                       | ``COMMAND`` | command to run    |\n    +-------------------------------------------------------+-------------+-------------------+\n    |                                                       | ``ARGS``    | command arguments |\n    +-------------------------------------------------------+-------------+-------------------+\n    | .. class:: borg-common-opt-ref                                                          |\n    |                                                                                         |\n    | :ref:`common_options`                                                                   |\n    +-------------------------------------------------------+-------------+-------------------+\n\n    .. raw:: html\n\n        <script type='text/javascript'>\n        $(document).ready(function () {\n            $('.borg-options-table colgroup').remove();\n        })\n        </script>\n\n.. only:: latex\n\n    COMMAND\n        command to run\n    ARGS\n        command arguments\n\n\n    :ref:`common_options`\n        |\n\nDescription\n~~~~~~~~~~~\n\nThis command runs a user-specified command while locking the repository. For example:\n\n::\n\n    $ BORG_REPO=/mnt/borgrepo borg with-lock rsync -av /mnt/borgrepo /somewhere/else/borgrepo\n\nIt first tries to acquire the lock (make sure that no other operation is\nrunning in the repository), then executes the given command as a subprocess and waits\nfor its termination, releases the lock, and returns the user command's return\ncode as Borg's return code.\n\n.. note::\n\n    If you copy a repository with the lock held, the lock will be present in\n    the copy. Before using Borg on the copy from a different host,\n    you need to run ``borg break-lock`` on the copied repository, because\n    Borg is cautious and does not automatically remove stale locks made by a different host."
  },
  {
    "path": "docs/usage.rst",
    "content": ".. include:: global.rst.inc\n.. highlight:: none\n.. _detailed_usage:\n\nUsage\n=====\n\n.. raw:: html\n   Redirecting...\n\n   <script type=\"text/javascript\">\n   // Fixes old links that were just anchor fragments\n   var hash = window.location.hash.substring(1);\n\n   // usage.html is empty; it contains no content. It purely serves to implement a \"correct\" toctree\n   // due to reST/Sphinx limitations. Refer to https://github.com/sphinx-doc/sphinx/pull/3622\n\n   // Redirect to general docs\n   if(hash == \"\") {\n       var replaced = window.location.pathname.replace(\"usage.html\", \"usage/general.html\");\n       if (replaced != window.location.pathname) {\n           window.location.pathname = replaced;\n       }\n   }\n   // Fixup anchored links from when usage.html contained all the commands\n   else if(hash.startsWith(\"borg-key\") || hash == \"borg-change-passphrase\") {\n      window.location.hash = \"\";\n      window.location.pathname = window.location.pathname.replace(\"usage.html\", \"usage/key.html\");\n   }\n   else if(hash.startsWith(\"borg-\")) {\n      window.location.hash = \"\";\n      window.location.pathname = window.location.pathname.replace(\"usage.html\", \"usage/\") + hash.substr(5) + \".html\";\n   }\n   </script>\n\n.. toctree::\n   usage/general\n\n   usage/repo-create\n   usage/repo-space\n   usage/repo-list\n   usage/repo-info\n   usage/repo-compress\n   usage/repo-delete\n   usage/serve\n   usage/version\n   usage/compact\n   usage/lock\n   usage/key\n\n   usage/create\n   usage/extract\n   usage/check\n   usage/list\n   usage/tag\n   usage/rename\n   usage/diff\n   usage/delete\n   usage/prune\n   usage/undelete\n   usage/info\n   usage/analyze\n   usage/mount\n   usage/recreate\n   usage/tar\n\n   usage/transfer\n   usage/benchmark\n\n   usage/help\n   usage/completion\n   usage/debug\n   usage/notes\n"
  },
  {
    "path": "docs/usage_general.rst.inc",
    "content": ".. include:: usage/general/positional-arguments.rst.inc\n\n.. include:: usage/general/repository-urls.rst.inc\n\n.. include:: usage/general/repository-locations.rst.inc\n\n.. include:: usage/general/logging.rst.inc\n\n.. include:: usage/general/return-codes.rst.inc\n\n.. include:: usage/general/environment.rst.inc\n\n.. include:: usage/general/file-systems.rst.inc\n\n.. include:: usage/general/units.rst.inc\n\n.. include:: usage/general/date-time.rst.inc\n\n.. include:: usage/general/resources.rst.inc\n\n.. include:: usage/general/file-metadata.rst.inc\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"borgbackup\"\ndynamic = [\"version\", \"readme\"]\nauthors = [{name=\"The Borg Collective (see AUTHORS file)\"}]\nmaintainers = [\n    {name=\"Thomas Waldmann\", email=\"tw@waldmann-edv.de\"},\n]\ndescription = \"Deduplicated, encrypted, authenticated, and compressed backups\"\nrequires-python = \">=3.10\"\nkeywords = [\"backup\", \"borgbackup\"]\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Environment :: Console\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: BSD :: FreeBSD\",\n    \"Operating System :: POSIX :: BSD :: OpenBSD\",\n    \"Operating System :: POSIX :: BSD :: NetBSD\",\n    \"Operating System :: MacOS :: MacOS X\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Security :: Cryptography\",\n    \"Topic :: System :: Archiving :: Backup\",\n]\nlicense = \"BSD-3-Clause\"\nlicense-files = [\"LICENSE\", \"AUTHORS\"]\ndependencies = [\n  \"borghash ~= 0.1.0\",\n  \"borgstore ~= 0.4.0\",\n  \"msgpack >=1.0.3, <=1.1.2\",\n  \"packaging\",\n  \"platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'\",  # for macOS: breaking changes in 3.0.0.\n  \"platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'\",  # for others: 2.6+ works consistently.\n  \"argon2-cffi\",\n  \"shtab>=1.8.0\",\n  \"backports-zstd; python_version < '3.14'\", # for python < 3.14.\n  \"xxhash>=2.0.0\",\n  \"jsonargparse>=4.47.0\",\n  \"PyYAML>=6.0.2\",  # we need to register our types with yaml, jsonargparse uses yaml for config files\n]\n\n[project.optional-dependencies]\nllfuse = [\"llfuse >= 1.3.8\"]  # fuse 2, low-level\npyfuse3 = [\"pyfuse3 >= 3.1.1\"]  # fuse 3, low-level, async\nmfusepy = [\"mfusepy >= 3.1.0, <4.0.0\"]  # fuse 2+3, high-level\n# a pypi release of borgbackup can't contain a dependency on github!\n# mfusepym = [\"mfusepy @ git+https://github.com/mxmlnkn/mfusepy.git@master\"]\nnofuse = []\ns3 = [\"borgstore[s3] ~= 0.4.0\"]\nsftp = [\"borgstore[sftp] ~= 0.4.0\"]\nrclone = [\"borgstore[rclone] ~= 0.4.0\"]\nrest = [\"borgstore[rest] ~= 0.4.0\"]\ncockpit = [\"textual>=6.8.0\"]  # might also work with older versions, untested\n\n[project.urls]\n\"Homepage\" = \"https://borgbackup.org/\"\n\"Bug Tracker\" = \"https://github.com/borgbackup/borg/issues\"\n\"Documentation\" = \"https://borgbackup.readthedocs.io/\"\n\"Repository\" = \"https://github.com/borgbackup/borg\"\n\"Changelog\" = \"https://github.com/borgbackup/borg/blob/master/docs/changes.rst\"\n\n[project.scripts]\nborg = \"borg.archiver:main\"\nborgfs = \"borg.archiver:main\"\n\n[tool.setuptools]\n# See also the MANIFEST.in file.\n# We want to install all the files in the package directories...\ninclude-package-data = true\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n\n[tool.setuptools.exclude-package-data]\n# ...except the source files which have been compiled (C extensions):\n\"*\" = [\"*.c\", \"*.h\", \"*.pyx\"]\n\n[build-system]\nrequires = [\"setuptools>=78.1.1\", \"wheel\", \"pkgconfig\", \"Cython>=3.0.3\", \"setuptools_scm[toml]>=6.2\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools_scm]\n# Make sure we have the same versioning scheme with all setuptools_scm versions, to avoid different autogenerated files.\n# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1015052\n# https://github.com/borgbackup/borg/issues/6875\nwrite_to = \"src/borg/_version.py\"\nwrite_to_template = \"__version__ = version = {version!r}\\n\"\n\n[tool.black]\nline-length = 120\nskip-magic-trailing-comma = true\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n# Exclude a variety of commonly ignored directories.\nexclude = [\n    \".cache\",\n    \".eggs\",\n    \".git\",\n    \".git-rewrite\",\n    \".idea\",\n    \".mypy_cache\",\n    \".ruff_cache\",\n    \".tox\",\n    \"build\",\n    \"dist\",\n]\n\n[tool.ruff.lint]\n# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.\nselect = [\"E\", \"F\"]\n\n# for reference ...\n#   E402 module level import not at top\n#   E501 line too long\n#   F401 import unused\n#   F405 undefined or defined from star imports\n#   F811 redef of unused var\n\n# borg code style guidelines:\nignore = [\"F405\", \"E402\"]\n\n# Allow autofix for all enabled rules (when `--fix` is provided).\nfixable = [\"ALL\"]\nunfixable = []\n\n# Allow unused variables when underscore-prefixed.\ndummy-variable-rgx = \"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$\"\n\n# Code style violation exceptions:\n# please note that the values are adjusted so that they do not cause failures\n# with existing code. if you want to change them, you should first fix all\n# ruff failures that appear with your change.\n[tool.ruff.lint.per-file-ignores]\n\"scripts/make.py\" = [\"E501\"]\n\"src/borg/archive.py\" = [\"E501\"]\n\"src/borg/archiver/help_cmd.py\" = [\"E501\"]\n\"src/borg/cache.py\" = [\"E501\"]\n\"src/borg/helpers/__init__.py\" = [\"F401\"]\n\"src/borg/platform/__init__.py\" = [\"F401\"]\n\"src/borg/testsuite/archiver/disk_full_test.py\" = [\"F811\"]\n\"src/borg/testsuite/archiver/return_codes_test.py\" = [\"F811\"]\n\"src/borg/testsuite/benchmark_test.py\" = [\"F811\"]\n\"src/borg/testsuite/platform/platform_test.py\" = [\"F811\"]\n\n[tool.pytest.ini_options]\nmarkers = []\n\n[tool.mypy]\npython_version = \"3.10\"\nstrict_optional = false\nlocal_partial_types = true\nshow_error_codes = true\nfiles = \"src/borg/**/*.py\"\n\n[[tool.mypy.overrides]]\nmodule = [\n    \"msgpack.*\",\n    \"llfuse\",\n    \"pyfuse3\",\n    \"trio\",\n    \"borg.crypto.low_level\",\n    \"borg.platform.*\",\n]\nignore_missing_imports = true\n\n[tool.tox]\nrequires = [\"tox>=4.19\", \"pkgconfig\", \"cython\", \"wheel\", \"setuptools_scm\"]\n# Important: when adding/removing Python versions here,\n#            also update the section \"Test environments with different FUSE implementations\" accordingly.\nenv_list = [\"py{310,311,312,313,314}-{none,llfuse,pyfuse3,mfusepy}\", \"docs\", \"ruff\", \"mypy\", \"bandit\"]\n\n[tool.tox.env_run_base]\npackage = \"editable-legacy\"  # without this it does not find setup_docs when running under fakeroot\ndeps = [\"-rrequirements.d/development.lock.txt\"]\ncommands = [[\"python\", \"-m\", \"pytest\", \"-v\", \"-n\", \"{env:XDISTN:auto}\", \"-rs\", \"--cov=borg\", \"--cov-config=pyproject.toml\", \"--cov-report=xml\", \"--junitxml=test-results.xml\", \"--benchmark-skip\", \"--pyargs\", \"{posargs:borg.testsuite}\"]]\npass_env = [\"*\"]  # fakeroot -u needs some env vars\n\n[tool.tox.env_pkg_base]\npass_env = [\"*\"]  # needed by tox4, so env vars are visible for building borg\n\n# Test environments with different FUSE implementations\n[tool.tox.env.py310-none]\n\n[tool.tox.env.py310-llfuse]\nset_env = {BORG_FUSE_IMPL = \"llfuse\"}\nextras = [\"llfuse\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py310-pyfuse3]\nset_env = {BORG_FUSE_IMPL = \"pyfuse3\"}\nextras = [\"pyfuse3\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py310-mfusepy]\nset_env = {BORG_FUSE_IMPL = \"mfusepy\"}\nextras = [\"mfusepy\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py311-none]\n\n[tool.tox.env.py311-llfuse]\nset_env = {BORG_FUSE_IMPL = \"llfuse\"}\nextras = [\"llfuse\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py311-pyfuse3]\nset_env = {BORG_FUSE_IMPL = \"pyfuse3\"}\nextras = [\"pyfuse3\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py311-mfusepy]\nset_env = {BORG_FUSE_IMPL = \"mfusepy\"}\nextras = [\"mfusepy\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py312-none]\n\n[tool.tox.env.py312-llfuse]\nset_env = {BORG_FUSE_IMPL = \"llfuse\"}\nextras = [\"llfuse\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py312-pyfuse3]\nset_env = {BORG_FUSE_IMPL = \"pyfuse3\"}\nextras = [\"pyfuse3\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py312-mfusepy]\nset_env = {BORG_FUSE_IMPL = \"mfusepy\"}\nextras = [\"mfusepy\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py313-none]\n\n[tool.tox.env.py313-llfuse]\nset_env = {BORG_FUSE_IMPL = \"llfuse\"}\nextras = [\"llfuse\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py313-pyfuse3]\nset_env = {BORG_FUSE_IMPL = \"pyfuse3\"}\nextras = [\"pyfuse3\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py313-mfusepy]\nset_env = {BORG_FUSE_IMPL = \"mfusepy\"}\nextras = [\"mfusepy\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py314-none]\n\n[tool.tox.env.py314-llfuse]\nset_env = {BORG_FUSE_IMPL = \"llfuse\"}\nextras = [\"llfuse\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py314-pyfuse3]\nset_env = {BORG_FUSE_IMPL = \"pyfuse3\"}\nextras = [\"pyfuse3\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.py314-mfusepy]\nset_env = {BORG_FUSE_IMPL = \"mfusepy\"}\nextras = [\"mfusepy\", \"sftp\", \"s3\", \"rest\", \"rclone\"]\n\n[tool.tox.env.ruff]\nskip_install = true\ndeps = [\"ruff\"]\ncommands = [[\"ruff\", \"check\", \".\"]]\n\n[tool.tox.env.mypy]\ndeps = [\"pytest\", \"mypy\", \"pkgconfig\", \"types-PyYAML\"]\ncommands = [[\"mypy\", \"--ignore-missing-imports\"]]\n\n[tool.tox.env.docs]\nchange_dir = \"docs\"\ndeps = [\"sphinx\", \"sphinxcontrib-jquery\", \"guzzle_sphinx_theme\"]\ncommands = [[\"sphinx-build\", \"-n\", \"-v\", \"-W\", \"--keep-going\", \"-b\", \"html\", \"-d\", \"{envtmpdir}/doctrees\", \".\", \"{envtmpdir}/html\"]]\n\n[tool.bandit]\nexclude_dirs = [\".cache\", \".eggs\", \".git\", \".git-rewrite\", \".idea\", \".mypy_cache\", \".ruff_cache\", \".tox\", \"build\", \"dist\", \"src/borg/testsuite\"]\nskips = [\n    \"B101\",  # skip assert warnings, we do not allow running borg with assertions disabled.\n    \"B404\",  # do not warn about just import subprocess\n]\n\n[tool.tox.env.bandit]\nskip_install = true\ndeps = [\"bandit[toml]\"]\ncommands = [[\"bandit\", \"-r\", \"src/borg\", \"-c\", \"pyproject.toml\"]]\n\n[tool.coverage.run]\nbranch = true\ndisable_warnings = [\"module-not-measured\", \"no-ctracer\"]\npatch = [\"subprocess\", \"_exit\"]\nsource = [\"src/borg\"]\nomit = [\n    \"*/borg/__init__.py\",\n    \"*/borg/__main__.py\",\n    \"*/borg/_version.py\",\n    \"*/borg/testsuite/*\",\n]\n\n[tool.coverage.report]\nexclude_lines = [\n    \"pragma: no cover\",\n    \"def __repr__\",\n    \"raise AssertionError\",\n    \"raise NotImplementedError\",\n    \"if 0:\",\n    \"if __name__ == .__main__.:\",\n]\nignore_errors = true\n\n[tool.coverage.xml]\noutput = \"coverage.xml\"\n"
  },
  {
    "path": "requirements.d/codestyle.txt",
    "content": "black >=24.0, <25\n"
  },
  {
    "path": "requirements.d/development.lock.txt",
    "content": "chardet==5.2.0\nsetuptools==80.10.2\nsetuptools-scm==9.2.2\npip==26.0.1\nwheel==0.46.3\nvirtualenv==20.36.1\nbuild==1.4.0\npkgconfig==1.5.5\ntox==4.35.0\npytest==9.0.2\npytest-xdist==3.8.0\ncoverage[toml]==7.13.4\npytest-cov==7.0.0\npytest-benchmark==5.2.3\nCython==3.2.4\npre-commit==4.5.1\ntypes-PyYAML==6.0.12.20250915\n"
  },
  {
    "path": "requirements.d/development.txt",
    "content": "chardet < 6\nsetuptools >=78.1.1\nsetuptools_scm\npip !=24.2\nwheel\nvirtualenv\nbuild\npkgconfig\ntox\npytest\npytest-xdist\ncoverage[toml]\npytest-cov\npytest-benchmark\nCython\npre-commit\nbandit[toml]\ntypes-PyYAML\n"
  },
  {
    "path": "requirements.d/docs.txt",
    "content": "sphinx\nsphinxcontrib-jquery\nguzzle_sphinx_theme\n"
  },
  {
    "path": "requirements.d/pyinstaller.txt",
    "content": "pyinstaller==6.19.0\n"
  },
  {
    "path": "scripts/Dockerfile.linux-run",
    "content": "ARG BASE_IMAGE=python:3.13\nFROM ${BASE_IMAGE}\n\n# Install system dependencies\n# These match the dependencies installed in CI for Linux (Debian/Ubuntu)\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    build-essential \\\n    pkg-config \\\n    libssl-dev \\\n    libacl1-dev \\\n    liblz4-dev \\\n    libfuse3-dev \\\n    fuse3 \\\n    python3-dev \\\n    git \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install tox\nRUN pip install tox\n\n# Set working directory\nWORKDIR /app\n\n# Check that we have the expected python version\nRUN python3 --version\n\n# Default command (can be overridden)\nCMD [\"tox\"]\n"
  },
  {
    "path": "scripts/borg.exe.spec",
    "content": "# -*- mode: python -*-\n# This PyInstaller spec file is used to build Borg binaries on POSIX platforms and Windows.\n\nimport os, sys\n\nis_win32 = sys.platform.startswith('win32')\n\n# Note: SPEC contains the spec file argument given to pyinstaller\nhere = os.path.dirname(os.path.abspath(SPEC))\nbasepath = os.path.abspath(os.path.join(here, '..'))\n\nif is_win32:\n    hiddenimports = ['borghash']\nelse:\n    hiddenimports = ['borg.platform.posix', 'borghash', 'rich._unicode_data.unicode17-0-0']\n\nblock_cipher = None\n\na = Analysis([os.path.join(basepath, 'src', 'borg', '__main__.py'), ],\n             pathex=[basepath, ],\n             binaries=[],\n             datas=[\n                (os.path.join(basepath, 'src', 'borg', 'paperkey.html'), 'borg'),\n                (os.path.join(basepath, 'src', 'borg', 'cockpit', 'cockpit.tcss'), os.path.join('borg', 'cockpit')),\n             ],\n             hiddenimports=hiddenimports,\n             hookspath=[],\n             runtime_hooks=[],\n             excludes=[\n                # '_ssl', 'ssl',  # do not exclude these, needed for pyfuse3/trio\n                'pkg_resources',  # avoid pkg_resources related warnings\n             ],\n             win_no_prefer_redirects=False,\n             win_private_assemblies=False,\n             cipher=block_cipher)\n\nif sys.platform == 'darwin':\n    # do not bundle the osxfuse libraries, so we do not get a version\n    # mismatch to the installed kernel driver of osxfuse.\n    a.binaries = [b for b in a.binaries if 'libosxfuse' not in b[0]]\n\npyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)\n\nexe = EXE(pyz,\n          a.scripts,\n          a.binaries,\n          a.zipfiles,\n          a.datas,\n          name='borg.exe',\n          debug=False,\n          strip=False,\n          upx=True,\n          console=True,\n          icon='NONE')\n\n# Build a directory-based binary in addition to a packed\n# single-file binary. This allows you to look at all included\n# files easily (e.g., without having to strace or halt the built binary\n# and introspect /tmp). It also avoids unpacking all libraries when\n# running the app, which is better for application signing on various OSes.\nslim_exe = EXE(pyz,\n            a.scripts,\n            exclude_binaries=True,\n            name='borg.exe',\n            debug=False,\n            strip=False,\n            upx=False,\n            console=True)\n\ncoll = COLLECT(slim_exe,\n                a.binaries,\n                a.zipfiles,\n                a.datas,\n                strip=False,\n                upx=False,\n                name='borg-dir')\n"
  },
  {
    "path": "scripts/errorlist.py",
    "content": "#!/usr/bin/env python3\n# This script automatically generates the error list for the documentation by\n# looking at the \"Error\" class and its subclasses.\n\nfrom textwrap import indent\n\nimport borg.archiver  # noqa: F401 - need import to get Error subclasses.\nfrom borg.constants import *  # NOQA\nfrom borg.helpers import Error, BackupError, BorgWarning\n\n\ndef subclasses(cls):\n    direct_subclasses = cls.__subclasses__()\n    return set(direct_subclasses) | {s for c in direct_subclasses for s in subclasses(c)}\n\n\n# 0, 1, 2 are used for success, generic warning, generic error\n# 3..99 are available for specific errors\n# 100..127 are available for specific warnings\n# 128+ are reserved for signals\nfree_error_rcs = set(range(EXIT_ERROR_BASE, EXIT_WARNING_BASE))  # 3 .. 99\nfree_warning_rcs = set(range(EXIT_WARNING_BASE, EXIT_SIGNAL_BASE))  # 100 .. 127\n\n# these classes map to rc 2\ngeneric_error_rc_classes = set()\ngeneric_warning_rc_classes = set()\n\nerror_classes = {Error} | subclasses(Error)\n\nfor cls in sorted(error_classes, key=lambda cls: (cls.__module__, cls.__qualname__)):\n    traceback = \"yes\" if cls.traceback else \"no\"\n    rc = cls.exit_mcode\n    print(\"   \", cls.__qualname__, \"rc:\", rc, \"traceback:\", traceback)\n    print(indent(cls.__doc__, \" \" * 8))\n    if rc in free_error_rcs:\n        free_error_rcs.remove(rc)\n    elif rc == 2:\n        generic_error_rc_classes.add(cls.__qualname__)\n    else:  # rc != 2\n        # if we did not intentionally map this to the generic error rc, this might be an issue:\n        print(f\"ERROR: {rc} is not a free/available RC, but either duplicate or invalid\")\n\nprint()\nprint(\"free error RCs:\", sorted(free_error_rcs))\nprint(\"generic errors:\", sorted(generic_error_rc_classes))\n\nwarning_classes = {BorgWarning} | subclasses(BorgWarning) | {BackupError} | subclasses(BackupError)\n\nfor cls in sorted(warning_classes, key=lambda cls: (cls.__module__, cls.__qualname__)):\n    rc = cls.exit_mcode\n    print(\"   \", cls.__qualname__, \"rc:\", rc)\n    print(indent(cls.__doc__, \" \" * 8))\n    if rc in free_warning_rcs:\n        free_warning_rcs.remove(rc)\n    elif rc == 1:\n        generic_warning_rc_classes.add(cls.__qualname__)\n    else:  # rc != 1\n        # if we did not intentionally map this to the generic warning rc, this might be an issue:\n        print(f\"ERROR: {rc} is not a free/available RC, but either duplicate or invalid\")\n\nprint(\"\\n\")\nprint(\"free warning RCs:\", sorted(free_warning_rcs))\nprint(\"generic warnings:\", sorted(generic_warning_rc_classes))\n"
  },
  {
    "path": "scripts/fetch-binaries",
    "content": "#!/bin/bash\n\nmkdir -p dist/\n\ncheck_and_copy () {\n    echo \"--- EXE $2 -----------------------------------------------\"\n    vagrant ssh $1 -c \"/vagrant/borg/borg.exe -V\"\n    vagrant scp $1:/vagrant/borg/borg.exe   dist/$2\n    echo \"--- DIR $2 -----------------------------------------------\"\n    vagrant ssh $1 -c \"/vagrant/borg/borg-dir/borg.exe -V\"\n    vagrant scp $1:/vagrant/borg/borg.tgz   dist/$2.tgz\n    echo \"\"\n}\n\ncheck_and_copy bullseye  borg-linux-glibc231-x86_64\ncheck_and_copy bookworm  borg-linux-glibc236-x86_64\ncheck_and_copy trixie    borg-linux-glibc241-x86_64\n\ncheck_and_copy freebsd14 borg-freebsd14-x86_64\n"
  },
  {
    "path": "scripts/glibc_check.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nCheck whether all given binaries work with the specified glibc version.\n\nUsage: glibc_check.py 2.11 BIN [BIN ...]\n\nExit code 0 means \"yes\"; exit code 1 means \"no\".\n\"\"\"\n\nimport re\nimport subprocess\nimport sys\n\nverbose = True\nglibc_re = re.compile(r\"GLIBC_([0-9]\\.[0-9]+)\")\n\n\ndef parse_version(v):\n    major, minor = v.split(\".\")\n    return int(major), int(minor)\n\n\ndef format_version(version):\n    return \"%d.%d\" % version\n\n\ndef main():\n    given = parse_version(sys.argv[1])\n    filenames = sys.argv[2:]\n\n    overall_versions = set()\n    for filename in filenames:\n        try:\n            output = subprocess.check_output([\"objdump\", \"-T\", filename], stderr=subprocess.STDOUT)\n            output = output.decode()\n            versions = {parse_version(match.group(1)) for match in glibc_re.finditer(output)}\n            requires_glibc = max(versions)\n            overall_versions.add(requires_glibc)\n            if verbose:\n                print(f\"{filename} {format_version(requires_glibc)}\")\n        except subprocess.CalledProcessError:\n            if verbose:\n                print(\"%s failed.\" % filename)\n\n    wanted = max(overall_versions)\n    ok = given >= wanted\n\n    if verbose:\n        if ok:\n            print(\"The binaries work with the given glibc %s.\" % format_version(given))\n        else:\n            print(\n                \"The binaries do not work with the given glibc %s. \"\n                \"Minimum required is %s.\" % (format_version(given), format_version(wanted))\n            )\n    return ok\n\n\nif __name__ == \"__main__\":\n    ok = main()\n    sys.exit(0 if ok else 1)\n"
  },
  {
    "path": "scripts/linux-run",
    "content": "#!/bin/bash\n# run commands in a linux container, e.g. for testing - for more info, see docs/development.rst.\n\nset -euo pipefail\n# set -x # Uncomment for debugging\n\n# Default configuration\nBASE_IMAGE=\"python:3.13\"\nIMAGE_NAME=\"borg-test-env\"\nCONTAINER_NAME=\"borg-test-runner\"\n\nusage() {\n    echo \"Usage: $0 [options] [command [args...]]\"\n    echo \"\"\n    echo \"Options:\"\n    echo \"  --image IMAGE    Base Docker image to use (default: $BASE_IMAGE)\"\n    echo \"  --rebuild        Force rebuild of the container image\"\n    echo \"  --help           Show this help message\"\n    echo \"\"\n    echo \"Examples:\"\n    echo \"  $0                                       # Open interactive shell (default)\"\n    echo \"  $0 tox -e py313-pyfuse3                  # Run tox in the container\"\n    echo \"  $0 --image python:3.11 tox -e py311-none # Run with specific image\"\n    echo \"  $0 ls -la                                # Run arbitrary command\"\n    exit 1\n}\n\n# Parse specific arguments\nREBUILD=false\n\n# We use an array to store the command and its arguments.\nCOMMAND=()\n\nwhile [[ $# -gt 0 ]]; do\n    case $1 in\n        --image)\n            BASE_IMAGE=\"$2\"\n            shift 2\n            ;;\n        --rebuild)\n            REBUILD=true\n            shift\n            ;;\n        --help)\n            usage\n            ;;\n        --)\n            # Stop option parsing and treat the rest as command\n            shift\n            COMMAND+=(\"$@\")\n            break\n            ;;\n        *)\n            # Stop option parsing and treat this and the rest as command\n            COMMAND+=(\"$@\")\n            break\n            ;;\n    esac\ndone\n\n# Ensure Podman is available\nif ! command -v podman &> /dev/null; then\n    echo \"Error: podman is not installed. Please run 'brew install podman' and initialize it.\"\n    exit 1\nfi\n\n# Build the image if needed or requested.\n# We tag the image with the base image name to allow multiple versions.\nTAG_SUFFIX=$(echo \"$BASE_IMAGE\" | tr ':' '-')\nFULL_IMAGE_NAME=\"${IMAGE_NAME}:${TAG_SUFFIX}\"\n\nif [[ \"$REBUILD\" == \"true\" ]] || ! podman image exists \"$FULL_IMAGE_NAME\"; then\n    echo \"Building test image based on $BASE_IMAGE...\"\n    podman build \\\n        --build-arg BASE_IMAGE=\"$BASE_IMAGE\" \\\n        -t \"$FULL_IMAGE_NAME\" \\\n        -f scripts/Dockerfile.linux-run .\nfi\n\necho \"Running in container...\"\necho \"Base Image: $BASE_IMAGE\"\nif [[ ${#COMMAND[@]} -gt 0 ]]; then\n    echo \"Command: ${COMMAND[*]}\"\nelse\n    echo \"Command: /bin/bash (default)\"\nfi\n\n# Run the container\n# --userns=keep-id: Maps the container user to the host user (Crucial for macOS volume mounts)\n# --security-opt label=disable: Disables SELinux separation if active (harmless on macOS)\n# --device /dev/fuse: Required for FUSE filesystem support (borg mount tests)\n# --volume $(pwd):/app: Mounts current directory to /app\n# --mount type=volume,dst=/tmp: Using a volume for /tmp to prevent issues with tmp files from different users\n\nCMD=(podman run --rm -it \\\n    --name \"$CONTAINER_NAME\" \\\n    --userns=keep-id \\\n    --cap-add SYS_ADMIN \\\n    --security-opt label=disable \\\n    --device /dev/fuse \\\n    --volume \"$(pwd):/app\" \\\n    --mount type=volume,dst=/tmp \\\n    --workdir /app \\\n    \"$FULL_IMAGE_NAME\")\n\nif [[ ${#COMMAND[@]} -gt 0 ]]; then\n    \"${CMD[@]}\" \"${COMMAND[@]}\"\nelse\n    \"${CMD[@]}\" /bin/bash\nfi\n"
  },
  {
    "path": "scripts/make-testdata/test_transfer_upgrade.sh",
    "content": "# This script uses Borg 1.2 to generate test data for \"borg transfer --upgrader=From12To20\".\nBORG=./borg-1.2.2\n# On macOS, GNU tar is available as gtar.\nTAR=gtar\nSRC=/tmp/borgtest\nARCHIVE=`pwd`/src/borg/testsuite/archiver/repo12.tar.gz\n\nexport BORG_REPO=/tmp/repo12\nMETA=$BORG_REPO/test_meta\nexport BORG_PASSPHRASE=\"waytooeasyonlyfortests\"\nexport BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=YES\n\n$BORG init -e repokey 2> /dev/null\nmkdir $META\n\n# archive1\nmkdir $SRC\n\npushd $SRC >/dev/null\n\nmkdir directory\n\necho \"content\" > directory/no_hardlink\n\necho \"hardlink content\" > hardlink1\nln hardlink1 hardlink2\n\necho \"symlinked content\" > target\nln -s target symlink\n\nln -s doesnotexist broken_symlink\n\nmkfifo fifo\n\ntouch without_xattrs\ntouch with_xattrs\nxattr -w key1 value with_xattrs\nxattr -w key2 \"\"    with_xattrs\n\ntouch without_flags\ntouch with_flags\nchflags nodump with_flags\n\npopd >/dev/null\n\n$BORG create ::archive1 $SRC\n$BORG list ::archive1 --json-lines > $META/archive1_list.json\nrm -rf $SRC\n\n# archive2\nmkdir $SRC\n\npushd $SRC >/dev/null\n\nsudo mkdir root_stuff\nsudo mknod root_stuff/bdev_12_34 b 12 34\nsudo mknod root_stuff/cdev_34_56 c 34 56\nsudo touch root_stuff/strange_uid_gid  # No user name or group name exists for this UID/GID!\nsudo chown 54321:54321 root_stuff/strange_uid_gid\n\npopd >/dev/null\n\n$BORG create ::archive2 $SRC\n$BORG list ::archive2 --json-lines > $META/archive2_list.json\nsudo rm -rf $SRC/root_stuff\nrm -rf $SRC\n\n\n$BORG --version > $META/borg_version.txt\n$BORG list :: --json > $META/repo_list.json\n\npushd $BORG_REPO >/dev/null\n$TAR czf $ARCHIVE .\npopd >/dev/null\n\n$BORG delete :: 2> /dev/null\n"
  },
  {
    "path": "scripts/make.py",
    "content": "# Support code for building docs (build_usage, build_man)\n\nimport glob\nimport os\nimport io\nimport re\nimport sys\nimport textwrap\nfrom collections import OrderedDict\nfrom datetime import datetime, timezone\nimport time\nimport argparse  # do not change to jsonargparse, shall not require 3rd party pkgs\n\n\ndef format_metavar(option):\n    if option.nargs in (\"*\", \"...\"):\n        return \"[%s...]\" % option.metavar\n    elif option.nargs == \"?\":\n        return \"[%s]\" % option.metavar\n    elif option.nargs is None:\n        return option.metavar\n    else:\n        raise ValueError(f\"Can't format metavar {option.metavar}, unknown nargs {option.nargs}!\")\n\n\nclass BuildUsage:\n    \"\"\"generate usage docs for each command\"\"\"\n\n    def run(self):\n        print(\"generating usage docs\")\n        import borg\n\n        borg.doc_mode = \"build_man\"\n        os.makedirs(\"docs/usage\", exist_ok=True)\n        # allows us to build docs without the C modules fully loaded during help generation\n        from borg.archiver import Archiver\n\n        parser = Archiver(prog=\"borg\").build_parser()\n        # borgfs has a separate man page to satisfy debian's \"every program from a package\n        # must have a man page\" requirement, but it doesn't need a separate HTML docs page\n        # borgfs_parser = Archiver(prog='borgfs').build_parser()\n\n        self.generate_level(\"\", parser, Archiver)\n        return 0\n\n    def generate_level(self, prefix, parser, Archiver, extra_choices=None):\n        is_subcommand = False\n        choices = {}\n        for action in parser._actions:\n            if action.choices is not None and \"SubCommands\" in str(action.__class__):\n                is_subcommand = True\n                for cmd, parser in action.choices.items():\n                    choices[prefix + cmd] = parser\n        if extra_choices is not None:\n            choices |= extra_choices\n        if prefix and not choices:\n            return\n        print(\"found commands: %s\" % list(choices.keys()))\n\n        for command, parser in sorted(choices.items()):\n            if command.startswith(\"debug\"):\n                print(\"skipping\", command)\n                continue\n            print(\"generating help for %s\" % command)\n\n            if self.generate_level(command + \" \", parser, Archiver):\n                continue\n\n            with open(\"docs/usage/%s.rst.inc\" % command.replace(\" \", \"_\"), \"w\") as doc:\n                doc.write(\".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\\n\\n\")\n                if command == \"help\":\n                    for topic in Archiver.helptext:\n                        params = {\"topic\": topic, \"underline\": \"~\" * len(\"borg help \" + topic)}\n                        doc.write(\".. _borg_{topic}:\\n\\n\".format(**params))\n                        doc.write(\"borg help {topic}\\n{underline}\\n\\n\".format(**params))\n                        doc.write(Archiver.helptext[topic])\n                else:\n                    params = {\n                        \"command\": command,\n                        \"command_\": command.replace(\" \", \"_\"),\n                        \"underline\": \"-\" * len(\"borg \" + command),\n                    }\n                    doc.write(\".. _borg_{command_}:\\n\\n\".format(**params))\n                    doc.write(\n                        \"borg {command}\\n{underline}\\n.. code-block:: none\\n\\n    borg [common options] {command}\".format(\n                            **params\n                        )\n                    )\n                    self.write_usage(parser, doc)\n                    epilog = parser.epilog\n                    parser.epilog = None\n                    self.write_options(parser, doc)\n                    doc.write(\"\\n\\nDescription\\n~~~~~~~~~~~\\n\")\n                    doc.write(epilog)\n\n        if \"create\" in choices:\n            common_options = [group for group in choices[\"create\"]._action_groups if group.title == \"Common options\"][0]\n            with open(\"docs/usage/common-options.rst.inc\", \"w\") as doc:\n                self.write_options_group(common_options, doc, False, base_indent=0)\n\n        return is_subcommand\n\n    def write_usage(self, parser, fp):\n        actions = [o for o in parser._actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n        if any(len(o.option_strings) for o in actions):\n            fp.write(\" [options]\")\n        for option in actions:\n            if option.option_strings:\n                continue\n            fp.write(\" \" + format_metavar(option))\n        fp.write(\"\\n\\n\")\n\n    def write_options(self, parser, fp):\n        def is_positional_group(actions):\n            return any(not o.option_strings for o in actions)\n\n        # HTML output:\n        # A table using some column-spans\n\n        rows = []\n        for group in parser._action_groups:\n            if group.title == \"Common options\":\n                # (no of columns used, columns, ...)\n                rows.append((1, \".. class:: borg-common-opt-ref\\n\\n:ref:`common_options`\"))\n            else:\n                actions = [o for o in group._group_actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n                if not actions:\n                    continue\n                group_header = \"**%s**\" % group.title\n                if group.description:\n                    group_header += \" — \" + group.description\n                rows.append((1, group_header))\n                if is_positional_group(actions):\n                    for option in actions:\n                        rows.append((3, \"\", \"``%s``\" % option.metavar, option.help or \"\"))\n                else:\n                    for option in actions:\n                        if option.metavar:\n                            option_fmt = \"``%s \" + option.metavar + \"``\"\n                        else:\n                            option_fmt = \"``%s``\"\n                        option_str = \", \".join(option_fmt % s for s in option.option_strings)\n                        option_desc = textwrap.dedent((option.help or \"\") % option.__dict__)\n                        rows.append((3, \"\", option_str, option_desc))\n\n        fp.write(\".. only:: html\\n\\n\")\n        table = io.StringIO()\n        table.write(\".. class:: borg-options-table\\n\\n\")\n        self.rows_to_table(rows, table.write)\n        fp.write(textwrap.indent(table.getvalue(), \" \" * 4))\n\n        # LaTeX output:\n        # Regular rST option lists (irregular column widths)\n        latex_options = io.StringIO()\n        for group in parser._action_groups:\n            if group.title == \"Common options\":\n                latex_options.write(\"\\n\\n:ref:`common_options`\\n\")\n                latex_options.write(\"    |\")\n            else:\n                self.write_options_group(group, latex_options)\n        fp.write(\"\\n.. only:: latex\\n\\n\")\n        fp.write(textwrap.indent(latex_options.getvalue(), \" \" * 4))\n\n    def rows_to_table(self, rows, write):\n        def write_row_separator():\n            write(\"+\")\n            for column_width in column_widths:\n                write(\"-\" * (column_width + 1))\n                write(\"+\")\n            write(\"\\n\")\n\n        # Find column count and width\n        column_count = max(columns for columns, *_ in rows)\n        column_widths = [0] * column_count\n        for columns, *cells in rows:\n            for i in range(columns):\n                # \"+ 1\" because we want a space between the cell contents and the delimiting \"|\" in the output\n                column_widths[i] = max(column_widths[i], len(cells[i]) + 1)\n\n        for columns, *original_cells in rows:\n            write_row_separator()\n            # If a cell contains newlines, then the row must be split up in individual rows\n            # where each cell contains no newline.\n            rowspanning_cells = []\n            original_cells = list(original_cells)\n            while any(\"\\n\" in cell for cell in original_cells):\n                cell_bloc = []\n                for i, cell in enumerate(original_cells):\n                    pre, _, original_cells[i] = cell.partition(\"\\n\")\n                    cell_bloc.append(pre)\n                rowspanning_cells.append(cell_bloc)\n            rowspanning_cells.append(original_cells)\n            for cells in rowspanning_cells:\n                for i, column_width in enumerate(column_widths):\n                    if i < columns:\n                        write(\"| \")\n                        write(cells[i].ljust(column_width))\n                    else:\n                        write(\"  \")\n                        write(\"\".ljust(column_width))\n                write(\"|\\n\")\n\n        write_row_separator()\n        # This bit of JavaScript kills the <colgroup> that is invariably inserted by docutils,\n        # but does absolutely no good here. It sets bogus column widths which cannot be overridden\n        # with CSS alone.\n        # Since this is HTML-only output, it would be possible to just generate a <table> directly,\n        # but then we'd lose rST formatting.\n        write(\n            textwrap.dedent(\n                \"\"\"\n        .. raw:: html\n\n            <script type='text/javascript'>\n            $(document).ready(function () {\n                $('.borg-options-table colgroup').remove();\n            })\n            </script>\n        \"\"\"\n            )\n        )\n\n    def write_options_group(self, group, fp, with_title=True, base_indent=4):\n        def is_positional_group(actions):\n            return any(not o.option_strings for o in actions)\n\n        indent = \" \" * base_indent\n        actions = [o for o in group._group_actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n\n        if is_positional_group(actions):\n            for option in actions:\n                fp.write(option.metavar + \"\\n\")\n                fp.write(textwrap.indent(option.help or \"\", \" \" * base_indent) + \"\\n\")\n            return\n\n        if not actions:\n            return\n\n        if with_title:\n            fp.write(\"\\n\\n\")\n            fp.write(group.title + \"\\n\")\n\n        opts = OrderedDict()\n\n        for option in actions:\n            if option.metavar:\n                option_fmt = \"%s \" + option.metavar\n            else:\n                option_fmt = \"%s\"\n            option_str = \", \".join(option_fmt % s for s in option.option_strings)\n            option_desc = textwrap.dedent((option.help or \"\") % option.__dict__)\n            opts[option_str] = textwrap.indent(option_desc, \" \" * 4)\n\n        padding = len(max(opts)) + 1\n\n        for option, desc in opts.items():\n            fp.write(indent + option.ljust(padding) + desc + \"\\n\")\n\n\nclass BuildMan:\n    \"\"\"build man pages\"\"\"\n\n    see_also = {\n        \"create\": (\"delete\", \"prune\", \"check\", \"patterns\", \"placeholders\", \"compression\", \"repo-create\"),\n        \"recreate\": (\"patterns\", \"placeholders\", \"compression\"),\n        \"list\": (\"info\", \"diff\", \"prune\", \"patterns\", \"repo-list\"),\n        \"info\": (\"list\", \"diff\", \"repo-info\"),\n        \"repo-create\": (\n            \"repo-delete\",\n            \"repo-list\",\n            \"check\",\n            \"benchmark-cpu\",\n            \"key-import\",\n            \"key-export\",\n            \"key-change-passphrase\",\n        ),\n        \"key-import\": (\"key-export\",),\n        \"key-export\": (\"key-import\",),\n        \"mount\": (\"umount\", \"extract\"),  # Would be cooler if these two were on the same page\n        \"umount\": (\"mount\",),\n        \"extract\": (\"mount\",),\n        \"delete\": (\"compact\", \"repo-delete\"),\n        \"prune\": (\"compact\",),\n    }\n\n    rst_prelude = textwrap.dedent(\n        \"\"\"\n    .. role:: ref(title)\n\n    .. |project_name| replace:: Borg\n\n    \"\"\"\n    )\n\n    usage_group = {\n        \"break-lock\": \"lock\",\n        \"with-lock\": \"lock\",\n        \"key_change-passphrase\": \"key\",\n        \"key_change-location\": \"key\",\n        \"key_export\": \"key\",\n        \"key_import\": \"key\",\n        \"export-tar\": \"tar\",\n        \"import-tar\": \"tar\",\n        \"benchmark_crud\": \"benchmark\",\n        \"benchmark_cpu\": \"benchmark\",\n        \"umount\": \"mount\",\n    }\n\n    def run(self):\n        print(\"building man pages (in docs/man)\", file=sys.stderr)\n        import borg\n\n        borg.doc_mode = \"build_man\"\n        os.makedirs(\"docs/man\", exist_ok=True)\n        # allows us to build docs without the C modules fully loaded during help generation\n        from borg.archiver import Archiver\n\n        parser = Archiver(prog=\"borg\").build_parser()\n        borgfs_parser = Archiver(prog=\"borgfs\").build_parser()\n\n        self.generate_level(\"\", parser, Archiver, {\"borgfs\": borgfs_parser})\n        self.build_topic_pages(Archiver)\n        self.build_intro_page()\n        return 0\n\n    def generate_level(self, prefix, parser, Archiver, extra_choices=None):\n        is_subcommand = False\n        choices = {}\n        for action in parser._actions:\n            if action.choices is not None and \"SubCommands\" in str(action.__class__):\n                is_subcommand = True\n                for cmd, parser in action.choices.items():\n                    choices[prefix + cmd] = parser\n        if extra_choices is not None:\n            choices |= extra_choices\n        if prefix and not choices:\n            return\n\n        for command, parser in sorted(choices.items()):\n            if command.startswith(\"debug\") or command == \"help\":\n                continue\n\n            if command == \"borgfs\":\n                man_title = command\n            else:\n                man_title = \"borg-\" + command.replace(\" \", \"-\")\n            print(\"building man page\", man_title + \"(1)\", file=sys.stderr)\n\n            is_intermediary = self.generate_level(command + \" \", parser, Archiver)\n\n            doc, write = self.new_doc()\n            self.write_man_header(write, man_title, parser.description)\n\n            self.write_heading(write, \"SYNOPSIS\")\n            if is_intermediary:\n                subparsers = [action for action in parser._actions if \"SubCommands\" in str(action.__class__)][0]\n                for subcommand in subparsers.choices:\n                    write(\"| borg\", \"[common options]\", command, subcommand, \"...\")\n                    self.see_also.setdefault(command, []).append(f\"{command}-{subcommand}\")\n            else:\n                if command == \"borgfs\":\n                    write(command, end=\"\")\n                else:\n                    write(\"borg\", \"[common options]\", command, end=\"\")\n                self.write_usage(write, parser)\n            write(\"\\n\")\n\n            description, _, notes = parser.epilog.partition(\"\\n.. man NOTES\")\n\n            if description:\n                self.write_heading(write, \"DESCRIPTION\")\n                write(description)\n\n            if not is_intermediary:\n                self.write_heading(write, \"OPTIONS\")\n                write(\"See `borg-common(1)` for common options of Borg commands.\")\n                write()\n                self.write_options(write, parser)\n\n                self.write_examples(write, command)\n\n            if notes:\n                self.write_heading(write, \"NOTES\")\n                write(notes)\n\n            self.write_see_also(write, man_title)\n\n            self.gen_man_page(man_title, doc.getvalue())\n\n        # Generate the borg-common(1) man page with the common options.\n        if \"create\" in choices:\n            doc, write = self.new_doc()\n            man_title = \"borg-common\"\n            self.write_man_header(write, man_title, \"Common options of Borg commands\")\n\n            common_options = [group for group in choices[\"create\"]._action_groups if group.title == \"Common options\"][0]\n\n            self.write_heading(write, \"SYNOPSIS\")\n            self.write_options_group(write, common_options)\n            self.write_see_also(write, man_title)\n            self.gen_man_page(man_title, doc.getvalue())\n\n        return is_subcommand\n\n    def build_topic_pages(self, Archiver):\n        for topic, text in Archiver.helptext.items():\n            doc, write = self.new_doc()\n            man_title = \"borg-\" + topic\n            print(\"building man page\", man_title + \"(1)\", file=sys.stderr)\n\n            self.write_man_header(write, man_title, \"Details regarding \" + topic)\n            self.write_heading(write, \"DESCRIPTION\")\n            write(text)\n            self.gen_man_page(man_title, doc.getvalue())\n\n    def build_intro_page(self):\n        doc, write = self.new_doc()\n        man_title = \"borg\"\n        print(\"building man page borg(1)\", file=sys.stderr)\n\n        with open(\"docs/man_intro.rst\") as fd:\n            man_intro = fd.read()\n\n        self.write_man_header(write, man_title, \"deduplicating and encrypting backup tool\")\n        self.gen_man_page(man_title, doc.getvalue() + man_intro)\n\n    def new_doc(self):\n        doc = io.StringIO(self.rst_prelude)\n        doc.read()\n        write = self.printer(doc)\n        return doc, write\n\n    def printer(self, fd):\n        def write(*args, **kwargs):\n            print(*args, file=fd, **kwargs)\n\n        return write\n\n    def write_heading(self, write, header, char=\"-\", double_sided=False):\n        write()\n        if double_sided:\n            write(char * len(header))\n        write(header)\n        write(char * len(header))\n        write()\n\n    def write_man_header(self, write, title, description):\n        self.write_heading(write, title, \"=\", double_sided=True)\n        self.write_heading(write, description, double_sided=True)\n        # man page metadata\n        write(\":Author: The Borg Collective\")\n        source_date_epoch = int(os.environ.get(\"SOURCE_DATE_EPOCH\", time.time()))\n        write(\":Date:\", datetime.fromtimestamp(source_date_epoch, timezone.utc).date().isoformat())\n        write(\":Manual section: 1\")\n        write(\":Manual group: borg backup tool\")\n        write()\n\n    def write_examples(self, write, command):\n        command = command.replace(\" \", \"_\")\n        with open(\"docs/usage/%s.rst\" % self.usage_group.get(command, command)) as fd:\n            usage = fd.read()\n            usage_include = \".. include:: %s.rst.inc\" % command\n            begin = usage.find(usage_include)\n            end = usage.find(\".. include\", begin + 1)\n            # If a command has a dedicated anchor, it will occur before the command's include.\n            if 0 < usage.find(\".. _\", begin + 1) < end:\n                end = usage.find(\".. _\", begin + 1)\n            examples = usage[begin:end]\n            examples = examples.replace(usage_include, \"\")\n            examples = examples.replace(\"Examples\\n~~~~~~~~\", \"\")\n            examples = examples.replace(\"Miscellaneous Help\\n------------------\", \"\")\n            examples = examples.replace(\"``docs/misc/prune-example.txt``:\", \"``docs/misc/prune-example.txt``.\")\n            examples = examples.replace(\".. highlight:: none\\n\", \"\")  # we don't support highlight\n            examples = re.sub(\"^(~+)$\", lambda matches: \"+\" * len(matches.group(0)), examples, flags=re.MULTILINE)\n            examples = examples.strip()\n        if examples:\n            self.write_heading(write, \"EXAMPLES\", \"-\")\n            write(examples)\n\n    def write_see_also(self, write, man_title):\n        see_also = self.see_also.get(man_title.replace(\"borg-\", \"\"), ())\n        see_also = [\"`borg-%s(1)`\" % s for s in see_also]\n        see_also.insert(0, \"`borg-common(1)`\")\n        self.write_heading(write, \"SEE ALSO\")\n        write(\", \".join(see_also))\n\n    def gen_man_page(self, name, rst):\n        from docutils.writers import manpage\n        from docutils.core import publish_string\n        from docutils.nodes import inline\n        from docutils.parsers.rst import roles\n        from borg.archiver._common import rst_plain_text_references\n\n        def issue(name, rawtext, text, lineno, inliner, options={}, content=[]):\n            return [inline(rawtext, \"#\" + text)], []\n\n        def ref_role(name, rawtext, text, lineno, inliner, options={}, content=[]):\n            replacement = rst_plain_text_references.get(text, text)\n            return [inline(rawtext, replacement)], []\n\n        roles.register_local_role(\"issue\", issue)\n        roles.register_local_role(\"ref\", ref_role)\n        # We give the source_path so that docutils can find relative includes\n        # as-if the document where located in the docs/ directory.\n        man_page = publish_string(source=rst, source_path=\"docs/%s.rst\" % name, writer=manpage.Writer())\n        with open(\"docs/man/%s.1\" % name, \"wb\") as fd:\n            fd.write(man_page)\n\n    def write_usage(self, write, parser):\n        actions = [o for o in parser._actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n        if any(len(o.option_strings) for o in actions):\n            write(\" [options] \", end=\"\")\n        for option in actions:\n            if option.option_strings:\n                continue\n            write(format_metavar(option), end=\" \")\n\n    def write_options(self, write, parser):\n        for group in parser._action_groups:\n            actions = [o for o in group._group_actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n            if group.title == \"Common options\" or not actions:\n                continue\n            title = \"arguments\" if group.title == \"positional arguments\" else group.title\n            self.write_heading(write, title, \"+\")\n            self.write_options_group(write, group)\n\n    def write_options_group(self, write, group):\n        def is_positional_group(actions):\n            return any(not o.option_strings for o in actions)\n\n        actions = [o for o in group._group_actions if getattr(o, \"help\", None) != argparse.SUPPRESS]\n\n        if is_positional_group(actions):\n            for option in actions:\n                write(option.metavar)\n                write(textwrap.indent(option.help or \"\", \" \" * 4))\n            return\n\n        opts = OrderedDict()\n\n        for option in actions:\n            if option.metavar:\n                option_fmt = \"%s \" + option.metavar\n            else:\n                option_fmt = \"%s\"\n            option_str = \", \".join(option_fmt % s for s in option.option_strings)\n            option_desc = textwrap.dedent((option.help or \"\") % option.__dict__)\n            opts[option_str] = textwrap.indent(option_desc, \" \" * 4)\n\n        padding = len(max(opts)) + 1\n\n        for option, desc in opts.items():\n            write(option.ljust(padding), desc)\n\n\ncython_sources = \"\"\"\nsrc/borg/compress.pyx\nsrc/borg/crypto/low_level.pyx\nsrc/borg/chunkers/buzhash.pyx\nsrc/borg/chunkers/buzhash64.pyx\nsrc/borg/chunkers/reader.pyx\nsrc/borg/hashindex.pyx\nsrc/borg/item.pyx\nsrc/borg/checksums.pyx\nsrc/borg/platform/posix.pyx\nsrc/borg/platform/linux.pyx\nsrc/borg/platform/syncfilerange.pyx\nsrc/borg/platform/darwin.pyx\nsrc/borg/platform/freebsd.pyx\nsrc/borg/platform/windows.pyx\n\"\"\".strip().splitlines()\n\n\ndef rm(file):\n    try:\n        os.unlink(file)\n    except FileNotFoundError:\n        return False\n    else:\n        return True\n\n\nclass Clean:\n    def run(self):\n        for source in cython_sources:\n            genc = source.replace(\".pyx\", \".c\")\n            rm(genc)\n            compiled_glob = source.replace(\".pyx\", \".cpython*\")\n            for compiled in sorted(glob.glob(compiled_glob)):\n                rm(compiled)\n        return 0\n\n\ndef usage():\n    print(\n        textwrap.dedent(\n            \"\"\"\n        Usage:\n            python scripts/make.py clean        # clean workdir (remove generated files)\n            python scripts/make.py build_usage  # build usage documentation\n            python scripts/make.py build_man    # build man pages\n    \"\"\"\n        )\n    )\n\n\ndef main(argv):\n    if len(argv) < 2 or len(argv) == 2 and argv[1] in (\"-h\", \"--help\"):\n        usage()\n        return 0\n    command = argv[1]\n    if command == \"clean\":\n        return Clean().run()\n    if command == \"build_usage\":\n        return BuildUsage().run()\n    if command == \"build_man\":\n        return BuildMan().run()\n    usage()\n    return 1\n\n\nif __name__ == \"__main__\":\n    rc = main(sys.argv)\n    sys.exit(rc)\n"
  },
  {
    "path": "scripts/msys2-install-deps",
    "content": "#!/bin/bash\n\npacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,lz4,xxhash,openssl,rclone,python-msgpack,python-argon2_cffi,python-platformdirs,python,cython,python-setuptools,python-wheel,python-build,python-pkgconfig,python-packaging,python-pip,python-paramiko}\n\nif [ \"$1\" = \"development\" ]; then\n\tpacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-python-{pytest,pytest-benchmark,pytest-cov,pytest-xdist}\nfi\n"
  },
  {
    "path": "scripts/sdist-sign",
    "content": "#!/bin/bash\n\nR=$1\n\nif [ \"$R\" = \"\" ]; then\n    echo \"Usage: sdist-sign 1.2.3\"\n    exit\nfi\n\nif [ \"$QUBES_GPG_DOMAIN\" = \"\" ]; then\n    GPG=gpg\nelse\n    GPG=qubes-gpg-client-wrapper\nfi\n\npython -m build\n\nD=dist/borgbackup-$R.tar.gz\n\n$GPG --detach-sign --local-user \"Thomas Waldmann\" --armor --output \"$D.asc\" \"$D\"\n"
  },
  {
    "path": "scripts/shell_completions/fish/borg.fish",
    "content": "# Completions for borg\n# https://www.borgbackup.org/\n# Note:\n# Listing archives works on password-protected repositories only if $BORG_PASSPHRASE is set.\n# Install:\n# Copy this file to /usr/share/fish/vendor_completions.d/\n\n# Commands\n\ncomplete -c borg -f -n __fish_is_first_token -a 'analyze' -d 'Analyze archives to find \"hot spots\"'\ncomplete -c borg -f -n __fish_is_first_token -a 'create' -d 'Create a new archive'\ncomplete -c borg -f -n __fish_is_first_token -a 'extract' -d 'Extract archive contents'\ncomplete -c borg -f -n __fish_is_first_token -a 'check' -d 'Check repository consistency'\ncomplete -c borg -f -n __fish_is_first_token -a 'rename' -d 'Rename an existing archive'\ncomplete -c borg -f -n __fish_is_first_token -a 'list' -d 'List archive or repository contents'\ncomplete -c borg -f -n __fish_is_first_token -a 'diff' -d 'Find differences between archives'\ncomplete -c borg -f -n __fish_is_first_token -a 'delete' -d 'Delete an archive'\ncomplete -c borg -f -n __fish_is_first_token -a 'prune' -d 'Prune repository archives'\ncomplete -c borg -f -n __fish_is_first_token -a 'compact' -d 'Free repository space'\ncomplete -c borg -f -n __fish_is_first_token -a 'info' -d 'Show archive details'\ncomplete -c borg -f -n __fish_is_first_token -a 'mount' -d 'Mount archive or a repository'\ncomplete -c borg -f -n __fish_is_first_token -a 'umount' -d 'Unmount the mounted archive'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-compress' -d 'Repository (re-)compression'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-create' -d 'Create a new, empty repository'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-delete' -d 'Delete a repository'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-info' -d 'Show repository information'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-list' -d 'List repository contents'\ncomplete -c borg -f -n __fish_is_first_token -a 'repo-space' -d 'Manage reserved space in a repository'\ncomplete -c borg -f -n __fish_is_first_token -a 'tag' -d 'Tag archives'\ncomplete -c borg -f -n __fish_is_first_token -a 'transfer' -d 'Transfer of archives from another repository'\ncomplete -c borg -f -n __fish_is_first_token -a 'undelete' -d 'Undelete archives'\ncomplete -c borg -f -n __fish_is_first_token -a 'version' -d 'Display Borg client version / Borg server version'\n\nfunction __fish_borg_seen_key\n    if __fish_seen_subcommand_from key\n        and not __fish_seen_subcommand_from import export change-passphrase\n        return 0\n    end\n    return 1\nend\ncomplete -c borg -f -n __fish_is_first_token -a 'key' -d 'Manage a repository key'\ncomplete -c borg -f -n __fish_borg_seen_key  -a 'import' -d 'Import a repository key'\ncomplete -c borg -f -n __fish_borg_seen_key  -a 'export' -d 'Export a repository key'\ncomplete -c borg -f -n __fish_borg_seen_key  -a 'change-passphrase' -d 'Change key file passphrase'\n\ncomplete -c borg -f -n __fish_is_first_token -a 'serve' -d 'Start in server mode'\ncomplete -c borg -f -n __fish_is_first_token -a 'recreate' -d 'Recreate contents of existing archives'\ncomplete -c borg -f -n __fish_is_first_token -a 'export-tar' -d 'Create tarball from an archive'\ncomplete -c borg -f -n __fish_is_first_token -a 'with-lock' -d 'Run a command while the repository lock is held'\ncomplete -c borg -f -n __fish_is_first_token -a 'break-lock' -d 'Break the repository lock'\n\nfunction __fish_borg_seen_benchmark\n    if __fish_seen_subcommand_from benchmark\n        and not __fish_seen_subcommand_from crud\n        return 0\n    end\n    return 1\nend\ncomplete -c borg -f -n __fish_is_first_token -a 'benchmark' -d 'Benchmark borg operations'\ncomplete -c borg -f -n __fish_borg_seen_benchmark -a 'crud' -d 'Benchmark borg CRUD operations'\n\nfunction __fish_borg_seen_help\n    if __fish_seen_subcommand_from help\n        and not __fish_seen_subcommand_from patterns placeholders compression\n        return 0\n    end\n    return 1\nend\ncomplete -c borg -f -n __fish_is_first_token -a 'help' -d 'Miscellaneous Help'\ncomplete -c borg -f -n __fish_borg_seen_help -a 'patterns' -d 'Help for patterns'\ncomplete -c borg -f -n __fish_borg_seen_help -a 'placeholders' -d 'Help for placeholders'\ncomplete -c borg -f -n __fish_borg_seen_help -a 'compression' -d 'Help for compression'\n\n# Common options\ncomplete -c borg -f -s h -l 'help'                  -d 'Show help information'\ncomplete -c borg -f      -l 'version'               -d 'Show version information'\ncomplete -c borg -f      -l 'critical'              -d 'Log level CRITICAL'\ncomplete -c borg -f      -l 'error'                 -d 'Log level ERROR'\ncomplete -c borg -f      -l 'warning'               -d 'Log level WARNING (default)'\ncomplete -c borg -f      -l 'info'                  -d 'Log level INFO'\ncomplete -c borg -f -s v -l 'verbose'               -d 'Log level INFO'\ncomplete -c borg -f      -l 'debug'                 -d 'Log level DEBUG'\ncomplete -c borg -f      -l 'debug-topic'           -d 'Enable TOPIC debugging'\ncomplete -c borg -f -s p -l 'progress'              -d 'Show progress information'\ncomplete -c borg -f      -l 'iec'                   -d 'Format using IEC units (1KiB = 1024B)'\ncomplete -c borg -f      -l 'log-json'              -d 'Output one JSON object per log line'\ncomplete -c borg -f      -l 'lock-wait'             -d 'Wait for lock max N seconds [1]'\ncomplete -c borg -f      -l 'show-version'          -d 'Log version information'\ncomplete -c borg -f      -l 'show-rc'               -d 'Log the return code'\ncomplete -c borg -f      -l 'umask'                 -d 'Set umask to M [0077]'\ncomplete -c borg         -l 'remote-path'           -d 'Use PATH as remote borg executable'\ncomplete -c borg -f      -l 'upload-ratelimit'      -d 'Set network upload rate limit in KiB/s'\ncomplete -c borg -f      -l 'upload-buffer'         -d 'Set network upload buffer size in MiB'\ncomplete -c borg         -l 'debug-profile'         -d 'Write execution profile into FILE'\ncomplete -c borg         -l 'rsh'                   -d 'Use COMMAND instead of ssh'\ncomplete -c borg         -l 'socket'                -d 'Use UNIX domain socket at PATH'\n\n# borg analyze options\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only archive names matching PATTERN'        -n \"__fish_seen_subcommand_from analyze\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from analyze\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from analyze\"\n\n# borg repo-compress options\n# Define compression methods once at the top\nset -l compression_methods \"none auto lz4 zstd,1 zstd,2 zstd,3 zstd,4 zstd,5 zstd,6 zstd,7 zstd,8 zstd,9 zstd,10 zstd,11 zstd,12 zstd,13 zstd,14 zstd,15 zstd,16 zstd,17 zstd,18 zstd,19 zstd,20 zstd,21 zstd,22 zlib,1 zlib,2 zlib,3 zlib,4 zlib,5 zlib,6 zlib,7 zlib,8 zlib,9 lzma,0 lzma,1 lzma,2 lzma,3 lzma,4 lzma,5 lzma,6 lzma,7 lzma,8 lzma,9\"\ncomplete -c borg -f -s C -l 'compression'           -d 'Select compression ALGORITHM,LEVEL [lz4]' -a \"$compression_methods\" -n \"__fish_seen_subcommand_from repo-compress\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print statistics'                            -n \"__fish_seen_subcommand_from repo-compress\"\n\n# borg create options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not create a backup archive'                 -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print verbose statistics'                       -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                    -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'filter'                -d 'Only items with given STATUSCHARS'              -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'json'                  -d 'Print verbose stats as json'                    -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'stdin-name'            -d 'Use NAME in archive for stdin data'             -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'content-from-command'  -d 'Interpret PATH as command and store its stdout' -n \"__fish_seen_subcommand_from create\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'                 -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'         -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'         -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'         -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'exclude-caches'        -d 'Exclude directories tagged as cache'            -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg         -l 'exclude-if-present'    -d 'Exclude directories that contain FILENAME'      -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'keep-exclude-tags'     -d 'Keep tag files of excluded directories'         -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'exclude-nodump'        -d 'Exclude files flagged NODUMP'                   -n \"__fish_seen_subcommand_from create\"\n# Filesystem options\ncomplete -c borg -f -s x -l 'one-file-system'       -d 'Stay in the same file system'                   -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'numeric-ids'           -d 'Only store numeric user:group identifiers'      -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'noatime'               -d 'Do not store atime'                             -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'noctime'               -d 'Do not store ctime'                             -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'nobirthtime'           -d 'Do not store creation date'                     -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'nobsdflags'            -d 'Do not store bsdflags'                          -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'noacls'                -d 'Do not read and store ACLs into archive'        -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'noxattrs'              -d 'Do not read and store xattrs into archive'      -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'noflags'               -d 'Do not store flags'                             -n \"__fish_seen_subcommand_from create\"\nset -l files_cache_mode \"ctime,size,inode mtime,size,inode ctime,size mtime,size rechunk,ctime rechunk,mtime size disabled\"\ncomplete -c borg -f      -l 'files-cache'           -d 'Operate files cache in MODE' -a \"$files_cache_mode\" -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'read-special'          -d 'Open device files like regular files'           -n \"__fish_seen_subcommand_from create\"\n# Archive options\ncomplete -c borg -f      -l 'comment'               -d 'Add COMMENT to the archive'                     -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'timestamp'             -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)'        -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg         -l 'timestamp'             -d 'Set creation time by reference FILE'            -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f      -l 'chunker-params'        -d 'Chunker PARAMETERS [19,23,21,4095]'             -n \"__fish_seen_subcommand_from create\"\ncomplete -c borg -f -s C -l 'compression'           -d 'Select compression ALGORITHM,LEVEL [lz4]' -a \"$compression_methods\" -n \"__fish_seen_subcommand_from create\"\n\n# borg extract options\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not actually extract any files'          -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'numeric-ids'           -d 'Only obey numeric user:group identifiers'   -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'nobsdflags'            -d 'Do not extract/set bsdflags'                -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'noflags'               -d 'Do not extract/set flags'                   -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'noacls'                -d 'Do not extract/set ACLs'                    -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'noxattrs'              -d 'Do not extract/set xattrs'                  -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'stdout'                -d 'Write all extracted data to stdout'         -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'sparse'                -d 'Create holes in output sparse file'         -n \"__fish_seen_subcommand_from extract\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg         -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from extract\"\ncomplete -c borg -f      -l 'strip-components'      -d 'Remove NUMBER of leading path elements'     -n \"__fish_seen_subcommand_from extract\"\n\n# borg check options\ncomplete -c borg -f      -l 'repository-only'       -d 'Only perform repository checks'             -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'archives-only'         -d 'Only perform archives checks'               -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'verify-data'           -d 'Cryptographic integrity verification'       -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'repair'                -d 'Attempt to repair found inconsistencies'    -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'max-duration'          -d 'Partial repo check for max. SECONDS'        -n \"__fish_seen_subcommand_from check\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from check\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from check\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from check\"\n\n# borg rename\n# no specific options\n\n# borg list options\ncomplete -c borg -f      -l 'short'                 -d 'Only print file/directory names'            -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'format'                -d 'Specify FORMAT for file listing'            -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'json'                  -d 'List contents in json format'               -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'json-lines'            -d 'List contents in json lines format'         -n \"__fish_seen_subcommand_from list\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from list\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from list\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from list\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from list\"\n\n# borg diff options\ncomplete -c borg -f      -l 'numeric-ids'           -d 'Only consider numeric user:group'           -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg -f      -l 'same-chunker-params'   -d 'Override check of chunker parameters'       -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg -f      -l 'sort'                  -d 'Sort the output lines by file path'         -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg -f      -l 'json-lines'            -d 'Format output as JSON Lines'                -n \"__fish_seen_subcommand_from diff\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from diff\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from diff\"\n\n# borg delete options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not change the repository'               -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'list'                  -d 'Output verbose list of archives'              -n \"__fish_seen_subcommand_from delete\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from delete\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from delete\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from delete\"\n\n# borg prune options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not change the repository'               -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'force'                 -d 'Force pruning of corrupted archives'        -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print verbose statistics'                   -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-within'           -d 'Keep archives within time INTERVAL'         -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-last'             -d 'NUMBER of secondly archives to keep'        -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-secondly'         -d 'NUMBER of secondly archives to keep'        -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-minutely'         -d 'NUMBER of minutely archives to keep'        -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s H -l 'keep-hourly'           -d 'NUMBER of hourly archives to keep'          -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s d -l 'keep-daily'            -d 'NUMBER of daily archives to keep'           -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s w -l 'keep-weekly'           -d 'NUMBER of weekly archives to keep'          -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s m -l 'keep-monthly'          -d 'NUMBER of monthly archives to keep'         -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-13weekly'         -d 'NUMBER of quarterly archives to keep (13 week strategy)' -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f      -l 'keep-3monthly'         -d 'NUMBER of quarterly archives to keep (3 month strategy)' -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s y -l 'keep-yearly'           -d 'NUMBER of yearly archives to keep'          -n \"__fish_seen_subcommand_from prune\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from prune\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from prune\"\n\n# borg compact options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do nothing'                                  -n \"__fish_seen_subcommand_from compact\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print statistics (might be much slower)'     -n \"__fish_seen_subcommand_from compact\"\n\n# borg info options\ncomplete -c borg -f      -l 'json'                  -d 'Format output in json format'               -n \"__fish_seen_subcommand_from info\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from info\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from info\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from info\"\n\n# borg repo-list options\ncomplete -c borg -f      -l 'short'                 -d 'Only print the archive IDs, nothing else'   -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'format'                -d 'Specify format for archive listing'         -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'json'                  -d 'Format output as JSON'                      -n \"__fish_seen_subcommand_from repo-list\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from repo-list\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from repo-list\"\ncomplete -c borg -f      -l 'deleted'               -d 'Consider only soft-deleted archives'        -n \"__fish_seen_subcommand_from repo-list\"\n\n# borg mount options\ncomplete -c borg -f -s f -l 'foreground'            -d 'Stay in foreground, do not daemonize'       -n \"__fish_seen_subcommand_from mount\"\n# FIXME This list is probably not full, but I tried to pick only those that are relevant to borg mount -o:\nset -l fuse_options \"ac_attr_timeout= allow_damaged_files allow_other allow_root attr_timeout= auto auto_cache auto_unmount default_permissions entry_timeout= gid= group_id= kernel_cache max_read= negative_timeout= noauto noforget remember= remount rootmode= uid= umask= user user_id= versions\"\ncomplete -c borg -f -s o                            -d 'Fuse mount OPTION' -a \"$fuse_options\"       -n \"__fish_seen_subcommand_from mount\"\n# Archive filters\ncomplete -c borg -f -s P -l 'prefix'                -d 'Only archive names starting with PREFIX'    -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from mount\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from mount\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from mount\"\ncomplete -c borg -f      -l 'strip-components'      -d 'Remove NUMBER of leading path elements'     -n \"__fish_seen_subcommand_from mount\"\n\n# borg umount\n# no specific options\n\n# borg tag options\ncomplete -c borg -f      -l 'set'                   -d 'Set tags (can be given multiple times)'      -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'add'                   -d 'Add tags (can be given multiple times)'      -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'remove'                -d 'Remove tags (can be given multiple times)'   -n \"__fish_seen_subcommand_from tag\"\n# Archive filters\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from tag\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from tag\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from tag\"\n\n# borg key change-passphrase\n# no specific options\n\n# borg key export\ncomplete -c borg -f      -l 'paper'                 -d 'Create an export for printing'              -n \"__fish_seen_subcommand_from export\"\ncomplete -c borg -f      -l 'qr-html'               -d 'Create an html file for printing and qr'    -n \"__fish_seen_subcommand_from export\"\n\n# borg transfer options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not change repository, just check'       -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'other-repo'            -d 'Transfer archives from the other repository' -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'from-borg1'            -d 'Other repository is borg 1.x'               -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'upgrader'              -d 'Use the upgrader to convert transferred data' -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f -s C -l 'compression'           -d 'Select compression algorithm' -a \"$compression_methods\" -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'recompress'            -d 'Recompress chunks CONDITION' -a \"$recompress_when\" -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'chunker-params'        -d 'Chunker PARAMETERS [19,23,21,4095]'         -n \"__fish_seen_subcommand_from transfer\"\n# Archive filters\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from transfer\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from transfer\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from transfer\"\n\n# borg key import\ncomplete -c borg -f      -l 'paper'                 -d 'Import from a backup done with --paper'     -n \"__fish_seen_subcommand_from import\"\n\n# borg undelete options\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not change repository'                   -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'list'                  -d 'Output verbose list of archives'            -n \"__fish_seen_subcommand_from undelete\"\n# Archive filters\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from undelete\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from undelete\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from undelete\"\n\n\n# borg recreate\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'filter'                -d 'Only items with given STATUSCHARS'          -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f -s n -l 'dry-run'               -d 'Do not change the repository'               -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print verbose statistics'                   -n \"__fish_seen_subcommand_from recreate\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'exclude-caches'        -d 'Exclude directories tagged as cache'        -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'exclude-if-present'    -d 'Exclude directories that contain FILENAME'  -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'keep-exclude-tags'     -d 'Keep tag files of excluded directories'     -n \"__fish_seen_subcommand_from recreate\"\n# Archive filters\ncomplete -c borg -f -s a -l 'match-archives'        -d 'Only consider archives matching all patterns' -n \"__fish_seen_subcommand_from recreate\"\nset -l sort_keys \"timestamp archive name id tags host user\"\ncomplete -c borg -f      -l 'sort-by'               -d 'Sorting KEYS [timestamp]' -a \"$sort_keys\"   -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'first'                 -d 'Only first N archives'                      -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'last'                  -d 'Only last N archives'                       -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'oldest'                -d 'Consider archives within TIMESPAN from oldest' -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'newest'                -d 'Consider archives within TIMESPAN from newest' -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'older'                 -d 'Consider archives older than TIMESPAN'      -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'newer'                 -d 'Consider archives newer than TIMESPAN'      -n \"__fish_seen_subcommand_from recreate\"\n# Archive options\ncomplete -c borg -f      -l 'target'                -d \"Create a new ARCHIVE\"                       -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'comment'               -d 'Add COMMENT to the archive'                 -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'timestamp'             -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)'    -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'timestamp'             -d 'Set creation time using reference FILE'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f -s C -l 'compression'           -d 'Select compression ALGORITHM,LEVEL [lz4]' -a \"$compression_methods\" -n \"__fish_seen_subcommand_from recreate\"\nset -l recompress_when \"if-different always never\"\ncomplete -c borg -f      -l 'recompress'            -d 'Recompress chunks CONDITION' -a \"$recompress_when\" -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'chunker-params'        -d 'Chunker PARAMETERS [19,23,21,4095]'         -n \"__fish_seen_subcommand_from recreate\"\n\n# borg export-tar options\ncomplete -c borg         -l 'tar-filter'            -d 'Filter program to pipe data through'        -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg -f      -l 'tar-format'            -d 'Select tar format: BORG, PAX or GNU'        -n \"__fish_seen_subcommand_from export-tar\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from export-tar\"\ncomplete -c borg -f      -l 'strip-components'      -d 'Remove NUMBER of leading path elements'     -n \"__fish_seen_subcommand_from export-tar\"\n\n# borg import-tar options\ncomplete -c borg         -l 'tar-filter'            -d 'Filter program to pipe data through'        -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f -s s -l 'stats'                 -d 'Print statistics for the created archive'   -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'list'                  -d 'Print verbose list of items'                -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'filter'                -d 'Only display items with given STATUSCHARS'  -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'json'                  -d 'Output stats as JSON'                       -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'ignore-zeros'          -d 'Ignore zero-filled blocks in the input'     -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'comment'               -d 'Add COMMENT to the archive'                 -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'timestamp'             -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)'    -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f      -l 'chunker-params'        -d 'Chunker PARAMETERS [19,23,21,4095]'         -n \"__fish_seen_subcommand_from import-tar\"\ncomplete -c borg -f -s C -l 'compression'           -d 'Select compression ALGORITHM,LEVEL [lz4]' -a \"$compression_methods\" -n \"__fish_seen_subcommand_from import-tar\"\n# Exclusion options\ncomplete -c borg    -s e -l 'exclude'               -d 'Exclude paths matching PATTERN'             -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'exclude-from'          -d 'Read exclude patterns from EXCLUDEFILE'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'pattern'               -d 'Include/exclude paths matching PATTERN'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg         -l 'patterns-from'         -d 'Include/exclude paths from PATTERNFILE'     -n \"__fish_seen_subcommand_from recreate\"\ncomplete -c borg -f      -l 'strip-components'      -d 'Remove NUMBER of leading path elements'     -n \"__fish_seen_subcommand_from recreate\"\n\n# borg serve\ncomplete -c borg         -l 'restrict-to-path'      -d 'Restrict repository access to PATH'         -n \"__fish_seen_subcommand_from serve\"\ncomplete -c borg         -l 'restrict-to-repository' -d 'Restrict repository access at PATH'        -n \"__fish_seen_subcommand_from serve\"\n\n\n# borg with-lock\n# no specific options\n\n# borg break-lock\n# no specific options\n\n# borg benchmark\n# no specific options\n\n# borg help\n# no specific options\n\n\nfunction __fish_borg_archives\n    # additionally to aid:XXXXXXXX we show archive (series) name and timestamp\n    borg repo-list --format=\"aid:{id:.8}{TAB}{archive} {start}{NEWLINE}\" 2>/dev/null\nend\n\nfunction __fish_borg_archive_arg --description 'Test if current command is a specific borg command with token count' --argument command token_count\n    # Check if we're in the context of a specific borg command\n    set -l tokens (commandline --tokenize)\n    set -l cmdline (commandline --current-process)\n\n    # Make sure we're in a borg command context\n    if not test $tokens[1] = \"borg\"\n        return 1\n    end\n\n    # Make sure we're in the specific command context\n    if not test $tokens[2] = \"$command\"\n        return 1\n    end\n\n    # Check if we're at the right token position\n    if not test (count $tokens) \"-eq\" \"$token_count\"\n        return 1\n    end\n\n    # Additional check to ensure we're not in the middle of typing an option\n    if string match --quiet --regex -- \"^-\" (commandline --current-token)\n        return 1\n    end\n\n    return 0\nend\n\n# The following completions use the -F flag to force disable filename completion\n# for various borg commands, ensuring only archive names are suggested.\n# We also use the -e flag to explicitly erase all default completions before adding our custom ones.\n\n# Global rules to disable filename completions for specific borg commands\n# This ensures that no filename completions are shown for these commands\ncomplete -c borg -e -n '__fish_seen_subcommand_from diff delete list info extract mount export-tar rename tag undelete recreate transfer check analyze'\n\n# First, explicitly erase all default completions for each command\n# This is the most specific rule and should take precedence\ncomplete -c borg -e -n '__fish_borg_archive_arg \"diff\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"diff\" 3'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"delete\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"list\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"info\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"extract\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"mount\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"export-tar\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"rename\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"tag\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"undelete\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"recreate\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"transfer\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"check\" 2'\ncomplete -c borg -e -n '__fish_borg_archive_arg \"analyze\" 2'\n\n# Also add specific rules to disable filename completions at the exact position\n# This ensures that no filename completions are shown at the position where we expect archive names\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"diff\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"diff\" 3'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"delete\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"list\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"info\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"extract\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"mount\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"export-tar\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"rename\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"tag\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"undelete\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"recreate\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"transfer\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"check\" 2'\ncomplete -c borg --no-files -n '__fish_borg_archive_arg \"analyze\" 2'\n\n# Then add our custom completions with high priority and no-files\n# This ensures no filename completions are shown and our custom completions take precedence\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"diff\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"diff\" 3' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"delete\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"list\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"info\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"extract\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"mount\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"export-tar\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"rename\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"tag\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"undelete\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"recreate\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"transfer\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"check\" 2' -a '(__fish_borg_archives)' --no-files\ncomplete -c borg -p 100 -n '__fish_borg_archive_arg \"analyze\" 2' -a '(__fish_borg_archives)' --no-files\n"
  },
  {
    "path": "scripts/sign-binaries",
    "content": "#!/bin/bash\n\nD=$1\n\nif [ \"$D\" = \"\" ]; then\n    echo \"Usage: sign-binaries 201912312359\"\n    exit\nfi\n\nif [ \"$QUBES_GPG_DOMAIN\" = \"\" ]; then\n    GPG=gpg\nelse\n    GPG=qubes-gpg-client-wrapper\nfi\n\nfor file in dist/borg-*; do\n    $GPG --local-user \"Thomas Waldmann\" --armor --detach-sign --output \"$file.asc\" \"$file\"\ndone\n\ntouch -t \"$D\" dist/*\n"
  },
  {
    "path": "scripts/upload-pypi",
    "content": "#!/bin/bash\n\nR=$1\n\nif [ \"$R\" = \"\" ]; then\n    echo \"Usage: upload-pypi 1.2.3 [test]\"\n    exit\nfi\n\nif [ \"$2\" = \"test\" ]; then\n    export TWINE_REPOSITORY=testborgbackup\nelse\n    export TWINE_REPOSITORY=borgbackup\nfi\n\nD=dist/borgbackup-$R.tar.gz\n\ntwine upload $D\n"
  },
  {
    "path": "setup.py",
    "content": "# borgbackup - main setup code (extension building here, rest see pyproject.toml)\n\nimport os\nimport re\nimport sys\nfrom collections import defaultdict\n\ntry:\n    import multiprocessing\nexcept ImportError:\n    multiprocessing = None\n\nfrom setuptools.command.build_ext import build_ext\nfrom setuptools import setup, Extension\nfrom setuptools.command.sdist import sdist\n\ntry:\n    from Cython.Build import cythonize\n\n    cythonize_import_error_msg = None\nexcept ImportError as exc:\n    # either there is no Cython installed or there is some issue with it.\n    cythonize = None\n    cythonize_import_error_msg = \"ImportError: \" + str(exc)\n    if \"failed to map segment from shared object\" in cythonize_import_error_msg:\n        cythonize_import_error_msg += \" Check if the borg build uses a +exec filesystem.\"\n\nsys.path += [os.path.dirname(__file__)]\n\nis_win32 = sys.platform.startswith(\"win32\")\nis_openbsd = sys.platform.startswith(\"openbsd\")\n\n# Number of threads to use for cythonize, not used on Windows\ncpu_threads = multiprocessing.cpu_count() if multiprocessing and multiprocessing.get_start_method() != \"spawn\" else None\n\n# How the build process finds the system libs:\n#\n# 1. if BORG_{LIBXXX,OPENSSL}_PREFIX is set, it will use headers and libs from there.\n# 2. if not and pkg-config can locate the lib, the lib located by\n#    pkg-config will be used. We use the pkg-config tool via the pkgconfig\n#    python package, which must be installed before invoking setup.py.\n#    if pkgconfig is not installed, this step is skipped.\n# 3. otherwise raise a fatal error.\n\n# Are we building on ReadTheDocs?\non_rtd = os.environ.get(\"READTHEDOCS\")\n\n# Extra cflags for all extensions, usually just warnings we want to enable explicitly\ncflags = [\"-Wall\", \"-Wextra\", \"-Wpointer-arith\", \"-Wno-unreachable-code-fallthrough\"]\n\ncompress_source = \"src/borg/compress.pyx\"\ncrypto_ll_source = \"src/borg/crypto/low_level.pyx\"\nbuzhash_source = \"src/borg/chunkers/buzhash.pyx\"\nbuzhash64_source = \"src/borg/chunkers/buzhash64.pyx\"\nreader_source = \"src/borg/chunkers/reader.pyx\"\nhashindex_source = \"src/borg/hashindex.pyx\"\nitem_source = \"src/borg/item.pyx\"\nchecksums_source = \"src/borg/checksums.pyx\"\nplatform_posix_source = \"src/borg/platform/posix.pyx\"\nplatform_linux_source = \"src/borg/platform/linux.pyx\"\nplatform_syncfilerange_source = \"src/borg/platform/syncfilerange.pyx\"\nplatform_darwin_source = \"src/borg/platform/darwin.pyx\"\nplatform_freebsd_source = \"src/borg/platform/freebsd.pyx\"\nplatform_netbsd_source = \"src/borg/platform/netbsd.pyx\"\nplatform_windows_source = \"src/borg/platform/windows.pyx\"\n\ncython_sources = [\n    compress_source,\n    crypto_ll_source,\n    buzhash_source,\n    buzhash64_source,\n    reader_source,\n    hashindex_source,\n    item_source,\n    checksums_source,\n    platform_posix_source,\n    platform_linux_source,\n    platform_syncfilerange_source,\n    platform_freebsd_source,\n    platform_netbsd_source,\n    platform_darwin_source,\n    platform_windows_source,\n]\n\nif cythonize:\n    Sdist = sdist\nelse:\n\n    class Sdist(sdist):\n        def __init__(self, *args, **kwargs):\n            raise Exception(\"Cython is required to run sdist\")\n\n    cython_c_files = [fn.replace(\".pyx\", \".c\") for fn in cython_sources]\n    if not on_rtd and not all(os.path.exists(path) for path in cython_c_files):\n        raise ImportError(\n            \"The Git version of Borg needs a working Cython. \"\n            + \"Install or fix Cython or use a released Borg version. \"\n            + \"Importing cythonize failed with: \"\n            + cythonize_import_error_msg\n        )\n\n\ncmdclass = {\"build_ext\": build_ext, \"sdist\": Sdist}\n\n\next_modules = []\nif not on_rtd:\n\n    def members_appended(*ds):\n        result = defaultdict(list)\n        for d in ds:\n            for k, v in d.items():\n                assert isinstance(v, list)\n                result[k].extend(v)\n        return result\n\n    try:\n        import pkgconfig as pc\n    except ImportError:\n        print(\"Warning: cannot import pkgconfig Python package.\")\n        pc = None\n\n    def lib_ext_kwargs(pc, prefix_env_var, lib_name, lib_pkg_name, pc_version, lib_subdir=\"lib\"):\n        system_prefix = os.environ.get(prefix_env_var)\n        if system_prefix:\n            print(f\"Detected and preferring {lib_pkg_name} [via {prefix_env_var}]\")\n            return dict(\n                include_dirs=[os.path.join(system_prefix, \"include\")],\n                library_dirs=[os.path.join(system_prefix, lib_subdir)],\n                libraries=[lib_name],\n            )\n\n        if pc and pc.installed(lib_pkg_name, pc_version):\n            print(f\"Detected and preferring {lib_pkg_name} [via pkg-config]\")\n            return pc.parse(lib_pkg_name)\n        raise Exception(\n            f\"Could not find {lib_name} lib/headers, please set {prefix_env_var} \"\n            f\"or ensure {lib_pkg_name}.pc is in PKG_CONFIG_PATH.\"\n        )\n\n    if is_win32:\n        crypto_ext_lib = lib_ext_kwargs(pc, \"BORG_OPENSSL_PREFIX\", \"libcrypto\", \"libcrypto\", \">=1.1.1\", lib_subdir=\"\")\n    elif is_openbsd:\n        # Use OpenSSL (not LibreSSL) because we need AES-OCB via the EVP API. Link\n        # it statically to avoid conflicting with shared libcrypto from the base\n        # OS pulled in via dependencies.\n        openssl_prefix = os.environ.get(\"BORG_OPENSSL_PREFIX\", \"/usr/local\")\n        openssl_name = os.environ.get(\"BORG_OPENSSL_NAME\", \"eopenssl35\")\n        crypto_ext_lib = dict(\n            include_dirs=[os.path.join(openssl_prefix, \"include\", openssl_name)],\n            extra_objects=[os.path.join(openssl_prefix, \"lib\", openssl_name, \"libcrypto.a\")],\n        )\n    else:\n        crypto_ext_lib = lib_ext_kwargs(pc, \"BORG_OPENSSL_PREFIX\", \"crypto\", \"libcrypto\", \">=1.1.1\")\n\n    crypto_ext_kwargs = members_appended(\n        dict(sources=[crypto_ll_source]), crypto_ext_lib, dict(extra_compile_args=cflags)\n    )\n\n    compress_ext_kwargs = members_appended(\n        dict(sources=[compress_source]),\n        lib_ext_kwargs(pc, \"BORG_LIBLZ4_PREFIX\", \"lz4\", \"liblz4\", \">= 1.7.0\"),\n        dict(extra_compile_args=cflags),\n    )\n\n    checksums_ext_kwargs = members_appended(dict(sources=[checksums_source]), dict(extra_compile_args=cflags))\n\n    if sys.platform == \"linux\":\n        linux_ext_kwargs = members_appended(\n            dict(sources=[platform_linux_source]),\n            lib_ext_kwargs(pc, \"BORG_LIBACL_PREFIX\", \"acl\", \"libacl\", \">= 2.2.47\"),\n            dict(extra_compile_args=cflags),\n        )\n    else:\n        linux_ext_kwargs = members_appended(\n            dict(sources=[platform_linux_source], libraries=[\"acl\"], extra_compile_args=cflags)\n        )\n\n    ext_modules += [\n        Extension(\"borg.crypto.low_level\", **crypto_ext_kwargs),\n        Extension(\"borg.compress\", **compress_ext_kwargs),\n        Extension(\"borg.hashindex\", [hashindex_source], extra_compile_args=cflags),\n        Extension(\"borg.item\", [item_source], extra_compile_args=cflags),\n        Extension(\"borg.chunkers.buzhash\", [buzhash_source], extra_compile_args=cflags),\n        Extension(\"borg.chunkers.buzhash64\", [buzhash64_source], extra_compile_args=cflags),\n        Extension(\"borg.chunkers.reader\", [reader_source], extra_compile_args=cflags),\n        Extension(\"borg.checksums\", **checksums_ext_kwargs),\n    ]\n\n    posix_ext = Extension(\"borg.platform.posix\", [platform_posix_source], extra_compile_args=cflags)\n    linux_ext = Extension(\"borg.platform.linux\", **linux_ext_kwargs)\n\n    syncfilerange_ext = Extension(\n        \"borg.platform.syncfilerange\", [platform_syncfilerange_source], extra_compile_args=cflags\n    )\n    freebsd_ext = Extension(\"borg.platform.freebsd\", [platform_freebsd_source], extra_compile_args=cflags)\n    netbsd_ext = Extension(\"borg.platform.netbsd\", [platform_netbsd_source], extra_compile_args=cflags)\n    darwin_ext = Extension(\"borg.platform.darwin\", [platform_darwin_source], extra_compile_args=cflags)\n    windows_ext = Extension(\"borg.platform.windows\", [platform_windows_source], extra_compile_args=cflags)\n\n    if not is_win32:\n        ext_modules.append(posix_ext)\n    else:\n        ext_modules.append(windows_ext)\n    if sys.platform == \"linux\":\n        ext_modules.append(linux_ext)\n        ext_modules.append(syncfilerange_ext)\n    elif sys.platform.startswith(\"freebsd\"):\n        ext_modules.append(freebsd_ext)\n    elif sys.platform.startswith(\"netbsd\"):\n        ext_modules.append(netbsd_ext)\n    elif sys.platform == \"darwin\":\n        ext_modules.append(darwin_ext)\n\n    # sometimes there's no need to cythonize\n    # this breaks chained commands like 'clean sdist'\n    cythonizing = (\n        len(sys.argv) > 1\n        and sys.argv[1] not in ((\"clean\", \"egg_info\", \"--help-commands\", \"--version\"))\n        and \"--help\" not in sys.argv[1:]\n    )\n\n    if cythonize and cythonizing:\n        # 3str is the default in Cython3 and we do not support older Cython releases.\n        # we only set this to avoid the related FutureWarning from Cython3.\n        cython_opts = dict(compiler_directives={\"language_level\": \"3str\"})\n        if not is_win32:\n            # Compile .pyx extensions to .c in parallel; does not work on Windows\n            cython_opts[\"nthreads\"] = cpu_threads\n\n        # generate C code from Cython for ALL supported platforms, so we have them in the sdist.\n        # the sdist does not require Cython at install time, so we need all as C.\n        cythonize(\n            [posix_ext, linux_ext, syncfilerange_ext, freebsd_ext, netbsd_ext, darwin_ext, windows_ext], **cython_opts\n        )\n        # generate C code from Cython for THIS platform (and for all platform-independent Cython parts).\n        ext_modules = cythonize(ext_modules, **cython_opts)\n\n\ndef long_desc_from_readme():\n    with open(\"README.rst\") as fd:\n        long_description = fd.read()\n        # remove header, but have one \\n before first headline\n        start = long_description.find(\"What is BorgBackup?\")\n        assert start >= 0\n        long_description = \"\\n\" + long_description[start:]\n        # remove badges\n        long_description = re.compile(r\"^\\.\\. start-badges.*^\\.\\. end-badges\", re.M | re.S).sub(\"\", long_description)\n        # remove unknown directives\n        long_description = re.compile(r\"^\\.\\. highlight:: \\w+$\", re.M).sub(\"\", long_description)\n        return long_description\n\n\nsetup(cmdclass=cmdclass, ext_modules=ext_modules, long_description=long_desc_from_readme())\n"
  },
  {
    "path": "src/borg/__init__.py",
    "content": "from packaging.version import parse as parse_version\n\nfrom ._version import version as __version__\n\n\n__version_tuple__ = parse_version(__version__).release\n\n# assert that all semver components are integers\n# this is mainly to show errors when people repackage poorly\n# and setuptools_scm determines a 0.1.dev... version\nassert all(isinstance(v, int) for v in __version_tuple__), (\n    \"\"\"\\\nBroken BorgBackup version metadata: %r\n\nVersion metadata is obtained dynamically during installation via setuptools_scm;\nplease ensure your Git repository has the correct tags, or provide the version\nusing SETUPTOOLS_SCM_PRETEND_VERSION in your build script.\n\"\"\"\n    % __version__\n)\n"
  },
  {
    "path": "src/borg/__main__.py",
    "content": "import sys\nimport os\n\n# On Windows, loading the bundled libcrypto DLL fails if the folder\n# containing the DLL is not in the search path. The DLL is shipped\n# with Python in the \"DLLs\" folder, so we add this folder\n# to the PATH. The folder is always present in sys.path; get it from there.\nif sys.platform.startswith(\"win32\"):\n    # Keep it an iterable to support multiple folders that contain \"DLLs\".\n    dll_path = (p for p in sys.path if \"DLLs\" in os.path.normpath(p).split(os.path.sep))\n    os.environ[\"PATH\"] = os.pathsep.join(dll_path) + os.pathsep + os.environ[\"PATH\"]\n\n\n# Note: absolute import from \"borg\"; PyInstaller binaries do not work without this.\nfrom borg.archiver import main\n\nmain()\n"
  },
  {
    "path": "src/borg/_item.c",
    "content": "#include \"Python.h\"\n\n/*\n * This is not quite as dark magic as it looks. We just convert the address of (pointer to)\n * a PyObject into a bytes object in _wrap_object, and convert these bytes back to the\n * pointer to the original object.\n *\n * This mainly looks a bit confusing due to our mental special-casing of \"char*\" from other\n * pointers.\n *\n * The big upside to this is that this neither does *any* serialization (beyond creating tiny\n * bytes objects as \"stand-ins\"), nor has to copy the entire object that's passed around.\n */\n\nstatic PyObject *\n_object_to_optr(PyObject *obj)\n{\n    /*\n     * Create a temporary reference to the object being passed around so it does not vanish.\n     * Note that we never decref this one in _unwrap_object, since we just transfer that reference\n     * there, i.e. there is an elided \"Py_INCREF(x); Py_DECREF(x)\".\n     * Since the reference is transferred, calls to _wrap_object and _unwrap_object must be symmetric.\n     */\n    Py_INCREF(obj);\n    return PyBytes_FromStringAndSize((const char*) &obj, sizeof(void*));\n}\n\nstatic PyObject *\n_optr_to_object(PyObject *bytes)\n{\n    if(!PyBytes_Check(bytes)) {\n        PyErr_SetString(PyExc_TypeError, \"Cannot unwrap non-bytes object\");\n        return NULL;\n    }\n    if(PyBytes_Size(bytes) != sizeof(void*)) {\n        PyErr_SetString(PyExc_TypeError, \"Invalid length of bytes object\");\n        return NULL;\n    }\n    PyObject *object = * (PyObject **) PyBytes_AsString(bytes);\n    return object;\n}\n"
  },
  {
    "path": "src/borg/archive.py",
    "content": "import base64\nimport errno\nimport json\nimport os\nimport posixpath\nimport stat\nimport sys\nimport time\nfrom collections import OrderedDict, defaultdict\nfrom contextlib import contextmanager\nfrom datetime import timedelta\nfrom functools import partial\nfrom getpass import getuser\nfrom io import BytesIO\nfrom itertools import groupby, zip_longest\nfrom collections.abc import Iterator\nfrom shutil import get_terminal_size\n\nfrom .platformflags import is_win32\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfrom . import xattr\nfrom .chunkers import get_chunker, Chunk\nfrom .cache import ChunkListEntry, build_chunkindex_from_repo, delete_chunkindex_cache\nfrom .crypto.key import key_factory, UnsupportedPayloadError\nfrom .constants import *  # NOQA\nfrom .crypto.low_level import IntegrityError as IntegrityErrorBase\nfrom .helpers import BackupError, BackupRaceConditionError, BackupItemExcluded\nfrom .helpers import BackupOSError, BackupPermissionError, BackupFileNotFoundError, BackupIOError\nfrom .hashindex import ChunkIndex, ChunkIndexEntry\nfrom .helpers import HardLinkManager\nfrom .helpers import ChunkIteratorFileWrapper, open_item\nfrom .helpers import Error, IntegrityError, set_ec\nfrom .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns\nfrom .helpers import parse_timestamp, archive_ts_now, CompressionSpec\nfrom .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize\nfrom .helpers import safe_encode, make_path_safe, remove_surrogates, text_to_json, join_cmd, remove_dotdot_prefixes\nfrom .helpers import StableDict\nfrom .helpers import bin_to_hex\nfrom .helpers import safe_ns\nfrom .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi\nfrom .helpers import os_open, flags_normal, flags_dir\nfrom .helpers import os_stat\nfrom .helpers import msgpack\nfrom .helpers.lrucache import LRUCache\nfrom .manifest import Manifest\nfrom .patterns import PathPrefixPattern, FnmatchPattern, IECommand\nfrom .item import Item, ArchiveItem, ItemDiff\nfrom . import platform\nfrom .platform import acl_get, acl_set, set_flags, get_flags, swidth\nfrom .remote import RemoteRepository, cache_if_remote\nfrom .repository import Repository, NoManifestError\nfrom .repoobj import RepoObj\n\nhas_link = hasattr(os, \"link\")\n\n\nclass Statistics:\n    def __init__(self, output_json=False, iec=False):\n        self.output_json = output_json\n        self.iec = iec\n        self.osize = self.usize = self.nfiles = 0\n        self.last_progress = 0  # timestamp when last progress was shown\n        self.files_stats = defaultdict(int)\n        self.chunking_time = 0.0\n        self.hashing_time = 0.0\n        self.rx_bytes = 0\n        self.tx_bytes = 0\n\n    def update(self, size, unique):\n        self.osize += size\n        if unique:\n            self.usize += size\n\n    def __add__(self, other):\n        if not isinstance(other, Statistics):\n            raise TypeError(\"can only add Statistics objects\")\n        stats = Statistics(self.output_json, self.iec)\n        stats.osize = self.osize + other.osize\n        stats.usize = self.usize + other.usize\n        stats.nfiles = self.nfiles + other.nfiles\n        stats.chunking_time = self.chunking_time + other.chunking_time\n        stats.hashing_time = self.hashing_time + other.hashing_time\n        st1, st2 = self.files_stats, other.files_stats\n        stats.files_stats = defaultdict(int, {key: (st1[key] + st2[key]) for key in st1.keys() | st2.keys()})\n\n        return stats\n\n    def __str__(self):\n        hashing_time = format_timedelta(timedelta(seconds=self.hashing_time))\n        chunking_time = format_timedelta(timedelta(seconds=self.chunking_time))\n        return \"\"\"\\\nNumber of files: {stats.nfiles}\nOriginal size: {stats.osize_fmt}\nDeduplicated size: {stats.usize_fmt}\nTime spent in hashing: {hashing_time}\nTime spent in chunking: {chunking_time}\nAdded files: {added_files}\nUnchanged files: {unchanged_files}\nModified files: {modified_files}\nError files: {error_files}\nFiles changed while reading: {files_changed_while_reading}\nBytes read from remote: {stats.rx_bytes}\nBytes sent to remote: {stats.tx_bytes}\n\"\"\".format(\n            stats=self,\n            hashing_time=hashing_time,\n            chunking_time=chunking_time,\n            added_files=self.files_stats[\"A\"],\n            unchanged_files=self.files_stats[\"U\"],\n            modified_files=self.files_stats[\"M\"],\n            error_files=self.files_stats[\"E\"],\n            files_changed_while_reading=self.files_stats[\"C\"],\n        )\n\n    def __repr__(self):\n        return \"<{cls} object at {hash:#x} ({self.osize}, {self.usize})>\".format(\n            cls=type(self).__name__, hash=id(self), self=self\n        )\n\n    def as_dict(self):\n        return {\n            \"original_size\": FileSize(self.osize, iec=self.iec),\n            \"nfiles\": self.nfiles,\n            \"hashing_time\": self.hashing_time,\n            \"chunking_time\": self.chunking_time,\n            \"files_stats\": self.files_stats,\n        }\n\n    def as_raw_dict(self):\n        return {\"size\": self.osize, \"nfiles\": self.nfiles}\n\n    @classmethod\n    def from_raw_dict(cls, **kw):\n        self = cls()\n        self.osize = kw[\"size\"]\n        self.nfiles = kw[\"nfiles\"]\n        return self\n\n    @property\n    def osize_fmt(self):\n        return format_file_size(self.osize, iec=self.iec)\n\n    @property\n    def usize_fmt(self):\n        return format_file_size(self.usize, iec=self.iec)\n\n    def show_progress(self, item=None, final=False, stream=None, dt=None):\n        now = time.monotonic()\n        if dt is None or now - self.last_progress > dt:\n            stream = stream or sys.stderr\n            self.last_progress = now\n            if self.output_json:\n                if not final:\n                    data = self.as_dict()\n                    if item:\n                        data |= text_to_json(\"path\", item.path)\n                else:\n                    data = {}\n                data |= {\"time\": time.time(), \"type\": \"archive_progress\", \"finished\": final}\n                msg = json.dumps(data)\n                end = \"\\n\"\n            elif not stream.isatty():\n                # Non-TTY output: use normal linefeeds and do not truncate the path.\n                if not final:\n                    msg = \"{0.osize_fmt} O {0.usize_fmt} U {0.nfiles} N \".format(self)\n                    msg += remove_surrogates(item.path) if item else \"\"\n                else:\n                    msg = \"\"\n                end = \"\\n\"\n            else:\n                columns, lines = get_terminal_size()\n                if not final:\n                    msg = \"{0.osize_fmt} O {0.usize_fmt} U {0.nfiles} N \".format(self)\n                    path = remove_surrogates(item.path) if item else \"\"\n                    space = columns - swidth(msg)\n                    if space < 12:\n                        msg = \"\"\n                        space = columns - swidth(msg)\n                    if space >= 8:\n                        msg += ellipsis_truncate(path, space)\n                else:\n                    msg = \" \" * columns\n                end = \"\\r\"\n            print(msg, end=end, file=stream, flush=True)\n\n\ndef is_special(mode):\n    # file types that get special treatment in --read-special mode\n    return stat.S_ISBLK(mode) or stat.S_ISCHR(mode) or stat.S_ISFIFO(mode)\n\n\nclass BackupIO:\n    op = \"\"\n\n    def __call__(self, op=\"\"):\n        self.op = op\n        return self\n\n    def __enter__(self):\n        pass\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if exc_type and issubclass(exc_type, OSError):\n            E_MAP = {\n                errno.EPERM: BackupPermissionError,\n                errno.EISDIR: BackupPermissionError,\n                errno.EACCES: BackupPermissionError,\n                errno.EBUSY: BackupPermissionError,\n                errno.ENOENT: BackupFileNotFoundError,\n                errno.EIO: BackupIOError,\n            }\n            e_cls = E_MAP.get(exc_val.errno, BackupOSError)\n            raise e_cls(self.op, exc_val) from exc_val\n\n\nbackup_io = BackupIO()\n\n\ndef backup_io_iter(iterator):\n    backup_io.op = \"read\"\n    while True:\n        with backup_io:\n            try:\n                item = next(iterator)\n            except StopIteration:\n                return\n        yield item\n\n\ndef stat_update_check(st_old, st_curr):\n    \"\"\"\n    this checks for some race conditions between the first filename-based stat()\n    we did before dispatching to the (hopefully correct) file type backup handler\n    and the (hopefully) fd-based fstat() we did in the handler.\n\n    if there is a problematic difference (e.g. file type changed), we rather\n    skip the file than being tricked into a security problem.\n\n    such races should only happen if:\n    - we are backing up a live filesystem (no snapshot, not inactive)\n    - if files change due to normal fs activity at an unfortunate time\n    - if somebody is doing an attack against us\n    \"\"\"\n    # assuming that a file type change implicates a different inode change AND that inode numbers\n    # are not duplicate in a short timeframe, this check is redundant and solved by the ino check:\n    if stat.S_IFMT(st_old.st_mode) != stat.S_IFMT(st_curr.st_mode):\n        # in this case, we dispatched to wrong handler - abort\n        raise BackupRaceConditionError(\"file type changed (race condition), skipping file\")\n    if st_old.st_ino != st_curr.st_ino:\n        # in this case, the hard-links-related code in create_helper has the wrong inode - abort!\n        raise BackupRaceConditionError(\"file inode changed (race condition), skipping file\")\n    # looks ok, we are still dealing with the same thing - return current stat:\n    return st_curr\n\n\n@contextmanager\ndef OsOpen(*, flags, path=None, parent_fd=None, name=None, noatime=False, op=\"open\"):\n    with backup_io(op):\n        fd = os_open(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=noatime)\n    try:\n        yield fd\n    finally:\n        # On windows fd is None for directories.\n        if fd is not None:\n            os.close(fd)\n\n\nclass DownloadPipeline:\n    def __init__(self, repository, repo_objs):\n        self.repository = repository\n        self.repo_objs = repo_objs\n        self.hlids_preloaded = None\n\n    def unpack_many(self, ids, *, filter=None):\n        \"\"\"\n        Return iterator of items.\n\n        *ids* is a chunk ID list of an item content data stream.\n        *filter* is an optional callable to decide whether an item will be yielded, default: yield all items.\n        \"\"\"\n        self.hlids_preloaded = set()\n        unpacker = msgpack.Unpacker(use_list=False)\n        for data in self.fetch_many(ids, ro_type=ROBJ_ARCHIVE_STREAM, replacement_chunk=False):\n            if data is None:\n                continue  # archive stream chunk missing\n            unpacker.feed(data)\n            for _item in unpacker:\n                item = Item(internal_dict=_item)\n                if filter is None or filter(item):\n                    if \"chunks\" in item:\n                        item.chunks = [ChunkListEntry(*e) for e in item.chunks]\n                    if \"chunks_healthy\" in item:  # legacy\n                        item.chunks_healthy = [ChunkListEntry(*e) for e in item.chunks_healthy]\n                    yield item\n\n    def preload_item_chunks(self, item, optimize_hardlinks=False):\n        \"\"\"\n        Preloads the content data chunks of an item (if any).\n        optimize_hardlinks can be set to True if item chunks only need to be preloaded for\n        1st hard link, but not for any further hard link to same inode / with same hlid.\n        Returns True if chunks were preloaded.\n\n        Warning: if data chunks are preloaded then all data chunks have to be retrieved,\n        otherwise preloaded chunks will accumulate in RemoteRepository and create a memory leak.\n        \"\"\"\n        preload_chunks = False\n        if \"chunks\" in item:\n            if optimize_hardlinks:\n                hlid = item.get(\"hlid\", None)\n                if hlid is None:\n                    preload_chunks = True\n                elif hlid in self.hlids_preloaded:\n                    preload_chunks = False\n                else:\n                    # not having the hard link's chunks already preloaded for other hard link to same inode\n                    preload_chunks = True\n                    self.hlids_preloaded.add(hlid)\n            else:\n                preload_chunks = True\n            if preload_chunks:\n                self.repository.preload([c.id for c in item.chunks])\n        return preload_chunks\n\n    def fetch_many(self, chunks, is_preloaded=False, ro_type=None, replacement_chunk=True):\n        assert ro_type is not None\n        ids = []\n        sizes = []\n        if all(isinstance(chunk, ChunkListEntry) for chunk in chunks):\n            for chunk in chunks:\n                ids.append(chunk.id)\n                sizes.append(chunk.size)\n        elif all(isinstance(chunk, bytes) for chunk in chunks):\n            ids = list(chunks)\n            sizes = [None] * len(ids)\n        else:\n            raise TypeError(f\"unsupported or mixed element types: {chunks}\")\n        for id, size, cdata in zip(\n            ids, sizes, self.repository.get_many(ids, is_preloaded=is_preloaded, raise_missing=False)\n        ):\n            if cdata is None:\n                if replacement_chunk and size is not None:\n                    logger.error(f\"repository object {bin_to_hex(id)} missing, returning {size} zero bytes.\")\n                    data = zeros[:size]  # return an all-zero replacement chunk of correct size\n                else:\n                    logger.error(f\"repository object {bin_to_hex(id)} missing, returning None.\")\n                    data = None\n            else:\n                _, data = self.repo_objs.parse(id, cdata, ro_type=ro_type)\n            assert size is None or len(data) == size\n            yield data\n\n\nclass ChunkBuffer:\n    BUFFER_SIZE = 8 * 1024 * 1024\n\n    def __init__(self, key, chunker_params=ITEMS_CHUNKER_PARAMS):\n        self.buffer = BytesIO()\n        self.packer = msgpack.Packer()\n        self.chunks = []\n        self.key = key\n        self.chunker = get_chunker(*chunker_params, key=self.key, sparse=False)\n        self.saved_chunks_len = None\n\n    def add(self, item):\n        self.buffer.write(self.packer.pack(item.as_dict()))\n        if self.is_full():\n            self.flush()\n\n    def write_chunk(self, chunk):\n        raise NotImplementedError\n\n    def flush(self, flush=False):\n        if self.buffer.tell() == 0:\n            return\n        self.buffer.seek(0)\n        # The chunker returns a memoryview to its internal buffer,\n        # thus a copy is needed before resuming the chunker iterator.\n        # the metadata stream may produce all-zero chunks, so deal\n        # with CH_ALLOC (and CH_HOLE, for completeness) here.\n        chunks = []\n        for chunk in self.chunker.chunkify(self.buffer):\n            alloc = chunk.meta[\"allocation\"]\n            if alloc == CH_DATA:\n                data = bytes(chunk.data)\n            elif alloc in (CH_ALLOC, CH_HOLE):\n                data = zeros[: chunk.meta[\"size\"]]\n            else:\n                raise ValueError(\"chunk allocation has unsupported value of %r\" % alloc)\n            chunks.append(data)\n        self.buffer.seek(0)\n        self.buffer.truncate(0)\n        # Leave the last partial chunk in the buffer unless flush is True\n        end = None if flush or len(chunks) == 1 else -1\n        for chunk in chunks[:end]:\n            self.chunks.append(self.write_chunk(chunk))\n        if end == -1:\n            self.buffer.write(chunks[-1])\n\n    def is_full(self):\n        return self.buffer.tell() > self.BUFFER_SIZE\n\n\nclass CacheChunkBuffer(ChunkBuffer):\n    def __init__(self, cache, key, stats, chunker_params=ITEMS_CHUNKER_PARAMS):\n        super().__init__(key, chunker_params)\n        self.cache = cache\n        self.stats = stats\n\n    def write_chunk(self, chunk):\n        id_, _ = self.cache.add_chunk(\n            self.key.id_hash(chunk), {}, chunk, stats=self.stats, wait=False, ro_type=ROBJ_ARCHIVE_STREAM\n        )\n        logger.debug(f\"writing item metadata stream chunk {bin_to_hex(id_)}\")\n        self.cache.repository.async_response(wait=False)\n        return id_\n\n\ndef get_item_uid_gid(item, *, numeric, uid_forced=None, gid_forced=None, uid_default=0, gid_default=0):\n    if uid_forced is not None:\n        uid = uid_forced\n    else:\n        uid = None if numeric else user2uid(item.get(\"user\"))\n        uid = item.get(\"uid\") if uid is None else uid\n        if uid is None or uid < 0:\n            uid = uid_default\n    if gid_forced is not None:\n        gid = gid_forced\n    else:\n        gid = None if numeric else group2gid(item.get(\"group\"))\n        gid = item.get(\"gid\") if gid is None else gid\n        if gid is None or gid < 0:\n            gid = gid_default\n    return uid, gid\n\n\ndef archive_get_items(metadata, *, repo_objs, repository):\n    if \"item_ptrs\" in metadata:  # looks like a v2+ archive\n        assert \"items\" not in metadata\n        items = []\n        for id, cdata in zip(metadata.item_ptrs, repository.get_many(metadata.item_ptrs)):\n            _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_ARCHIVE_CHUNKIDS)\n            ids = msgpack.unpackb(data)\n            items.extend(ids)\n        return items\n\n    if \"items\" in metadata:  # legacy, v1 archive\n        assert \"item_ptrs\" not in metadata\n        return metadata.items\n\n\ndef archive_put_items(chunk_ids, *, repo_objs, cache=None, stats=None, add_reference=None):\n    \"\"\"gets a (potentially large) list of archive metadata stream chunk ids and writes them to repo objects\"\"\"\n    item_ptrs = []\n    for i in range(0, len(chunk_ids), IDS_PER_CHUNK):\n        data = msgpack.packb(chunk_ids[i : i + IDS_PER_CHUNK])\n        id = repo_objs.id_hash(data)\n        logger.debug(f\"writing item_ptrs chunk {bin_to_hex(id)}\")\n        if cache is not None and stats is not None:\n            cache.add_chunk(id, {}, data, stats=stats, ro_type=ROBJ_ARCHIVE_CHUNKIDS)\n        elif add_reference is not None:\n            cdata = repo_objs.format(id, {}, data, ro_type=ROBJ_ARCHIVE_CHUNKIDS)\n            add_reference(id, len(data), cdata)\n        else:\n            raise NotImplementedError\n        item_ptrs.append(id)\n    return item_ptrs\n\n\nclass Archive:\n    class AlreadyExists(Error):\n        \"\"\"Archive {} already exists\"\"\"\n\n        exit_mcode = 30\n\n    class DoesNotExist(Error):\n        \"\"\"Archive {} does not exist\"\"\"\n\n        exit_mcode = 31\n\n    class IncompatibleFilesystemEncodingError(Error):\n        \"\"\"Failed to encode filename \"{}\" into file system encoding \"{}\". Consider configuring the LANG environment variable.\"\"\"\n\n        exit_mcode = 32\n\n    def __init__(\n        self,\n        manifest,\n        name,\n        *,\n        cache=None,\n        create=False,\n        numeric_ids=False,\n        noatime=False,\n        noctime=False,\n        noflags=False,\n        noacls=False,\n        noxattrs=False,\n        progress=False,\n        chunker_params=CHUNKER_PARAMS,\n        timestamp=None,\n        start=None,\n        end=None,\n        log_json=False,\n        iec=False,\n        deleted=False,\n        hostname=None,\n        username=None,\n    ):\n        name_is_id = isinstance(name, bytes)\n        if not name_is_id:\n            assert len(name) <= 255\n        self.cwd = os.getcwd()\n        assert isinstance(manifest, Manifest)\n        self.manifest = manifest\n        self.key = manifest.repo_objs.key\n        self.repo_objs = manifest.repo_objs\n        self.repository = manifest.repository\n        self.cache = cache\n        self.stats = Statistics(output_json=log_json, iec=iec)\n        self.iec = iec\n        self.show_progress = progress\n        self.name = name  # overwritten later with name from archive metadata\n        self.name_in_manifest = name  # can differ from .name later (if borg check fixed duplicate archive names)\n        self.comment = None\n        self.tags = None\n        self.hostname = hostname if hostname is not None else platform.hostname\n        self.username = username if username is not None else getuser()\n        self.numeric_ids = numeric_ids\n        self.noatime = noatime\n        self.noctime = noctime\n        self.noflags = noflags\n        self.noacls = noacls\n        self.noxattrs = noxattrs\n        self.chunker_params = chunker_params\n        self.start = start if start is not None else archive_ts_now()\n        self.end = end if end is not None else self.start\n        self.timestamp = timestamp if timestamp is not None else self.start\n        self.pipeline = DownloadPipeline(self.repository, self.repo_objs)\n        self.create = create\n        if self.create:\n            self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats)\n            self.tags = set()\n        else:\n            if name_is_id:\n                # we also go over the manifest here to avoid soft-deleted archives,\n                # except if we explicitly request one via deleted=True.\n                info = self.manifest.archives.get_by_id(name, deleted=deleted)\n            else:\n                info = self.manifest.archives.get(name)\n            if info is None:\n                raise self.DoesNotExist(name)\n            self.load(info.id)\n\n    def _load_meta(self, id):\n        cdata = self.repository.get(id)\n        _, data = self.repo_objs.parse(id, cdata, ro_type=ROBJ_ARCHIVE_META)\n        archive = self.key.unpack_archive(data)\n        metadata = ArchiveItem(internal_dict=archive)\n        if metadata.version not in (1, 2):  # legacy: still need to read v1 archives\n            raise Exception(\"Unknown archive metadata version\")\n        # note: metadata.items must not get written to disk!\n        metadata.items = archive_get_items(metadata, repo_objs=self.repo_objs, repository=self.repository)\n        return metadata\n\n    def load(self, id):\n        self.id = id\n        self.metadata = self._load_meta(self.id)\n        self.name = self.metadata.name\n        self.comment = self.metadata.get(\"comment\", \"\")\n        self.tags = set(self.metadata.get(\"tags\", []))\n\n    @property\n    def ts(self):\n        \"\"\"Nominal archive timestamp in UTC.\"\"\"\n        ts = self.metadata.time\n        return parse_timestamp(ts)\n\n    @property\n    def ts_start(self):\n        \"\"\"Timestamp of archive operation start in UTC.\"\"\"\n        # fall back to \"time\" in case \"start\" is not found\n        ts = self.metadata.get(\"start\") or self.metadata.time\n        return parse_timestamp(ts)\n\n    @property\n    def ts_end(self):\n        \"\"\"Timestamp of archive operation end in UTC.\"\"\"\n        # fall back to \"time\" in case \"end\" or \"time_end\" are not found\n        ts = self.metadata.get(\"end\") or self.metadata.get(\"time_end\") or self.metadata.time\n        return parse_timestamp(ts)\n\n    @property\n    def fpr(self):\n        return bin_to_hex(self.id)\n\n    @property\n    def duration(self):\n        return format_timedelta(self.end - self.start)\n\n    @property\n    def duration_from_meta(self):\n        return format_timedelta(self.ts_end - self.ts)\n\n    def info(self):\n        if self.create:\n            stats = self.stats\n            ts = self.timestamp\n            start = self.start\n            end = self.end\n        else:\n            stats = self.calc_stats(self.cache)\n            ts = self.ts\n            start = self.ts_start\n            end = self.ts_end\n        info = {\n            \"name\": self.name,\n            \"id\": self.fpr,\n            \"time\": OutputTimestamp(ts),\n            \"start\": OutputTimestamp(start),\n            \"end\": OutputTimestamp(end),\n            \"duration\": (end - start).total_seconds(),\n            \"stats\": stats.as_dict(),\n        }\n        if self.create:\n            info[\"command_line\"] = join_cmd(sys.argv)\n        else:\n            info |= {\n                \"command_line\": self.metadata.command_line,\n                \"cwd\": self.metadata.get(\"cwd\", \"\"),\n                \"hostname\": self.metadata.hostname,\n                \"username\": self.metadata.username,\n                \"comment\": self.metadata.get(\"comment\", \"\"),\n                \"tags\": sorted(self.tags),\n                \"chunker_params\": self.metadata.get(\"chunker_params\", \"\"),\n            }\n        return info\n\n    def __str__(self):\n        return \"\"\"\\\nRepository: {location}\nArchive name: {0.name}\nArchive fingerprint: {0.fpr}\nTime (nominal): {time}\nTime (start):   {start}\nTime (end):     {end}\nDuration: {0.duration}\n\"\"\".format(\n            self,\n            time=OutputTimestamp(self.timestamp),\n            start=OutputTimestamp(self.start),\n            end=OutputTimestamp(self.end),\n            location=self.repository._location.canonical_path(),\n        )\n\n    def __repr__(self):\n        return \"Archive(%r)\" % self.name\n\n    def item_filter(self, item, filter=None):\n        return filter(item) if filter else True\n\n    def iter_items(self, filter=None):\n        yield from self.pipeline.unpack_many(self.metadata.items, filter=lambda item: self.item_filter(item, filter))\n\n    def preload_item_chunks(self, item, optimize_hardlinks=False):\n        \"\"\"\n        Preloads item content data chunks from the repository.\n\n        Warning: if data chunks are preloaded then all data chunks have to be retrieved,\n        otherwise preloaded chunks will accumulate in RemoteRepository and create a memory leak.\n        \"\"\"\n        return self.pipeline.preload_item_chunks(item, optimize_hardlinks=optimize_hardlinks)\n\n    def add_item(self, item, show_progress=True, stats=None):\n        if show_progress and self.show_progress:\n            if stats is None:\n                stats = self.stats\n            stats.show_progress(item=item, dt=0.2)\n        self.items_buffer.add(item)\n\n    def save(self, name=None, comment=None, timestamp=None, stats=None, additional_metadata=None):\n        name = name or self.name\n        self.items_buffer.flush(flush=True)  # this adds the size of metadata stream chunks to stats.osize\n        item_ptrs = archive_put_items(\n            self.items_buffer.chunks, repo_objs=self.repo_objs, cache=self.cache, stats=self.stats\n        )  # this adds the sizes of the item ptrs chunks to stats.osize\n        start = self.start\n        end = self.end = archive_ts_now()\n        nominal = start if timestamp is None else timestamp\n        self.timestamp = nominal\n        metadata = {\n            \"version\": 2,\n            \"name\": name,\n            \"comment\": comment or \"\",\n            \"tags\": list(sorted(self.tags)),\n            \"item_ptrs\": item_ptrs,  # see #1473\n            \"command_line\": join_cmd(sys.argv),\n            \"cwd\": self.cwd,\n            \"hostname\": self.hostname,\n            \"username\": self.username,\n            \"time\": nominal.isoformat(timespec=\"microseconds\"),\n            \"start\": start.isoformat(timespec=\"microseconds\"),\n            \"end\": end.isoformat(timespec=\"microseconds\"),\n            \"chunker_params\": self.chunker_params,\n        }\n        # we always want to create archives with the addtl. metadata (nfiles, etc.),\n        # because borg info relies on them. so, either use the given stats (from args)\n        # or fall back to self.stats if it was not given.\n        stats = stats or self.stats\n        metadata |= {\"size\": stats.osize, \"nfiles\": stats.nfiles}\n        metadata |= additional_metadata or {}\n        if metadata.get(\"cwd\") is None:\n            del metadata[\"cwd\"]\n        metadata = ArchiveItem(metadata)\n        data = self.key.pack_metadata(metadata.as_dict())\n        self.id = self.repo_objs.id_hash(data)\n        try:\n            self.cache.add_chunk(self.id, {}, data, stats=self.stats, ro_type=ROBJ_ARCHIVE_META)\n        except IntegrityError as err:\n            err_msg = str(err)\n            # hack to avoid changing the RPC protocol by introducing new (more specific) exception class\n            if \"More than allowed put data\" in err_msg:\n                raise Error(\"%s - archive too big (issue #1473)!\" % err_msg)\n            else:\n                raise\n        while self.repository.async_response(wait=True) is not None:\n            pass\n        self.manifest.archives.create(name, self.id, metadata.time)\n        self.manifest.write()\n        return metadata\n\n    def calc_stats(self, cache, want_unique=True):\n        stats = Statistics(iec=self.iec)\n        stats.usize = 0  # this is expensive to compute\n        stats.nfiles = self.metadata.nfiles\n        stats.osize = self.metadata.size\n        return stats\n\n    @contextmanager\n    def extract_helper(self, item, path, hlm, *, dry_run=False):\n        hardlink_set = False\n        # Hard link?\n        if \"hlid\" in item:\n            link_target = hlm.retrieve(id=item.hlid)\n            if link_target is not None and has_link:\n                if not dry_run:\n                    # another hard link to same inode (same hlid) was extracted previously, just link to it\n                    with backup_io(\"link\"):\n                        os.link(link_target, path, follow_symlinks=False)\n                hardlink_set = True\n        yield hardlink_set\n        if not hardlink_set:\n            if \"hlid\" in item and has_link:\n                # Update entry with extracted item path, so that following hard links don't extract twice.\n                # We have hardlinking support, so we will hard link not extract.\n                hlm.remember(id=item.hlid, info=path)\n            else:\n                # Broken platform with no hardlinking support.\n                # In this case, we *want* to extract twice, because there is no other way.\n                pass\n\n    def extract_item(\n        self,\n        item,\n        *,\n        restore_attrs=True,\n        dry_run=False,\n        stdout=False,\n        sparse=False,\n        hlm=None,\n        pi=None,\n        continue_extraction=False,\n    ):\n        \"\"\"\n        Extract archive item.\n\n        :param item: the item to extract\n        :param restore_attrs: restore file attributes\n        :param dry_run: do not write any data\n        :param stdout: write extracted data to stdout\n        :param sparse: write sparse files (chunk-granularity, independent of the original being sparse)\n        :param hlm: maps hlid to link_target for extracting subtrees with hard links correctly\n        :param pi: ProgressIndicatorPercent (or similar) for file extraction progress (in bytes)\n        :param continue_extraction: continue a previously interrupted extraction of the same archive\n        \"\"\"\n\n        def same_item(item, st):\n            \"\"\"Is the archived item the same as the filesystem item at the same path with stat st?\"\"\"\n            is_file = stat.S_ISREG(st.st_mode)\n            is_dir = stat.S_ISDIR(st.st_mode)\n            if not (is_file or is_dir):\n                # we only \"optimize\" for regular files and directories.\n                # other file types are less frequent and have no content extraction we could \"optimize away\".\n                return False\n            if item.mode != st.st_mode:\n                # we want to extract a different type of file than what is present in the filesystem.\n                return False\n            if is_file and item.size != st.st_size:\n                # the size check catches incomplete previous regular file extraction\n                return False\n            if item.get(\"mtime\") != st.st_mtime_ns:\n                # note: mtime is \"extracted\" late, after xattrs and ACLs, but before flags.\n                return False\n            # this is good enough for the intended use case:\n            # continuing an extraction of same archive that initially started in an empty directory.\n            # there is a very small risk that \"bsdflags\" of one file are wrong:\n            # if a previous extraction was interrupted between setting the mtime and setting non-default flags.\n            return True\n\n        if dry_run or stdout:\n            with self.extract_helper(item, \"\", hlm, dry_run=dry_run or stdout) as hardlink_set:\n                if not hardlink_set:\n                    # it does not really set hard links due to dry_run, but we need to behave same\n                    # as non-dry_run concerning fetching preloaded chunks from the pipeline or\n                    # it would get stuck.\n                    if \"chunks\" in item:\n                        item_chunks_size = 0\n                        for data in self.pipeline.fetch_many(item.chunks, is_preloaded=True, ro_type=ROBJ_FILE_STREAM):\n                            if pi:\n                                pi.show(increase=len(data), info=[remove_surrogates(item.path)])\n                            if stdout:\n                                sys.stdout.buffer.write(data)\n                            item_chunks_size += len(data)\n                        if stdout:\n                            sys.stdout.buffer.flush()\n                        if \"size\" in item:\n                            item_size = item.size\n                            if item_size != item_chunks_size:\n                                raise BackupError(\n                                    \"Size inconsistency detected: size {}, chunks size {}\".format(\n                                        item_size, item_chunks_size\n                                    )\n                                )\n            return\n\n        dest = self.cwd\n        path = os.path.join(dest, item.path)\n        # Attempt to remove existing files, ignore errors on failure\n        try:\n            st = os.stat(path, follow_symlinks=False)\n            if continue_extraction and same_item(item, st):\n                return  # done! we already have fully extracted this file in a previous run.\n            if not stat.S_ISDIR(st.st_mode):\n                os.unlink(path)\n            elif stat.S_ISDIR(item.mode):\n                # if we have an existing directory and we want to extract a directory,\n                # we just use the existing one and do not remove it.\n                # This fixes the issue that the existing directory might be a BTRFS subvolume.\n                # If we removed it, we would lose the subvolume, see #4233.\n                pass\n            else:\n                os.rmdir(path)  # only works for empty directories\n        except UnicodeEncodeError:\n            raise self.IncompatibleFilesystemEncodingError(path, sys.getfilesystemencoding()) from None\n        except OSError:\n            pass\n\n        def make_parent(path):\n            parent_dir = os.path.dirname(path)\n            if not os.path.exists(parent_dir):\n                os.makedirs(parent_dir)\n\n        mode = item.mode\n        if stat.S_ISREG(mode):\n            with backup_io(\"makedirs\"):\n                make_parent(path)\n            with self.extract_helper(item, path, hlm) as hardlink_set:\n                if hardlink_set:\n                    return\n                with backup_io(\"open\"):\n                    fd = open(path, \"wb\")\n                with fd:\n                    trailing_hole = False\n                    for data in self.pipeline.fetch_many(item.chunks, is_preloaded=True, ro_type=ROBJ_FILE_STREAM):\n                        if pi:\n                            pi.show(increase=len(data), info=[remove_surrogates(item.path)])\n                        with backup_io(\"write\"):\n                            if sparse and zeros.startswith(data):\n                                # all-zero chunk: create a hole in a sparse file\n                                fd.seek(len(data), 1)\n                                trailing_hole = True\n                            else:\n                                fd.write(data)\n                                trailing_hole = False\n                    with backup_io(\"truncate_and_attrs\"):\n                        pos = item_chunks_size = fd.tell()\n                        if is_win32 and trailing_hole and pos > 0:\n                            # Windows: truncate() does not zero-fill properly (no VDL update).\n                            # Writing a single zero at the end forces NTFS to zero-fill the hole\n                            # and update valid data length.\n                            fd.seek(pos - 1)\n                            fd.write(b\"\\0\")\n                        fd.truncate(pos)\n                        fd.flush()\n                        self.restore_attrs(path, item, fd=fd.fileno())\n                if \"size\" in item:\n                    item_size = item.size\n                    if item_size != item_chunks_size:\n                        raise BackupError(\n                            f\"Size inconsistency detected: size {item_size}, chunks size {item_chunks_size}\"\n                        )\n            return\n        with backup_io:\n            # No repository access beyond this point.\n            if stat.S_ISDIR(mode):\n                make_parent(path)\n                if not os.path.exists(path):\n                    os.mkdir(path)\n                if restore_attrs:\n                    # note: if we did not create the directory freshly, existing attributes\n                    # might get mixed up with the archived attributes. this is acceptable,\n                    # considering we usually extract into an empty base directory.\n                    # when continuing an extraction, the existing attributes and the archived\n                    # attributes should be identical anyway.\n                    # Also, we want to avoid #4223 (losing btrfs subvolumes).\n                    self.restore_attrs(path, item)\n            elif stat.S_ISLNK(mode):\n                make_parent(path)\n                with self.extract_helper(item, path, hlm) as hardlink_set:\n                    if hardlink_set:\n                        # unusual, but possible: this is a hardlinked symlink.\n                        return\n                    target = item.target\n                    try:\n                        os.symlink(target, path)\n                    except UnicodeEncodeError:\n                        raise self.IncompatibleFilesystemEncodingError(target, sys.getfilesystemencoding()) from None\n                    self.restore_attrs(path, item, symlink=True)\n            elif stat.S_ISFIFO(mode):\n                make_parent(path)\n                with self.extract_helper(item, path, hlm) as hardlink_set:\n                    if hardlink_set:\n                        return\n                    os.mkfifo(path)\n                    self.restore_attrs(path, item)\n            elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode):\n                make_parent(path)\n                with self.extract_helper(item, path, hlm) as hardlink_set:\n                    if hardlink_set:\n                        return\n                    os.mknod(path, item.mode, item.rdev)\n                    self.restore_attrs(path, item)\n            else:\n                raise Exception(\"Unknown archive item type %r\" % item.mode)\n\n    def restore_attrs(self, path, item, symlink=False, fd=None):\n        \"\"\"\n        Restore filesystem attributes on *path* (*fd*) from *item*.\n\n        Does not access the repository.\n        \"\"\"\n        backup_io.op = \"attrs\"\n        # This code is a bit of a mess due to OS specific differences.\n        if not is_win32:\n            # by using uid_default = -1 and gid_default = -1, they will not be restored if\n            # the archived item has no information about them.\n            uid, gid = get_item_uid_gid(item, numeric=self.numeric_ids, uid_default=-1, gid_default=-1)\n            # if uid and/or gid is -1, chown will keep it as is and not change it.\n            try:\n                if fd:\n                    os.fchown(fd, uid, gid)\n                else:\n                    os.chown(path, uid, gid, follow_symlinks=False)\n            except OSError:\n                pass\n            if fd:\n                os.fchmod(fd, item.mode)\n            else:\n                # To check whether a particular function in the os module accepts False for its\n                # follow_symlinks parameter, the in operator on supports_follow_symlinks should be\n                # used. However, os.chmod is special as some platforms without a working lchmod() do\n                # have fchmodat(), which has a flag that makes it behave like lchmod(). fchmodat()\n                # is ignored when deciding whether or not os.chmod should be set in\n                # os.supports_follow_symlinks. Work around this by using try/except.\n                try:\n                    os.chmod(path, item.mode, follow_symlinks=False)\n                except NotImplementedError:\n                    if not symlink:\n                        os.chmod(path, item.mode)\n            if not self.noacls:\n                try:\n                    acl_set(path, item, self.numeric_ids, fd=fd)\n                except OSError as e:\n                    if e.errno not in (errno.ENOTSUP,):\n                        raise\n            if not self.noxattrs and \"xattrs\" in item:\n                # chown removes Linux capabilities, so set the extended attributes at the end, after chown,\n                # since they include the Linux capabilities in the \"security.capability\" attribute.\n                warning = xattr.set_all(fd or path, item.xattrs, follow_symlinks=False)\n                if warning:\n                    set_ec(EXIT_WARNING)\n            # set timestamps rather late\n            mtime = item.mtime\n            atime = item.atime if \"atime\" in item else mtime\n            if \"birthtime\" in item:\n                birthtime = item.birthtime\n                try:\n                    # This should work on FreeBSD, NetBSD, and Darwin and be harmless on other platforms.\n                    # See utimes(2) on either of the BSDs for details.\n                    if fd:\n                        os.utime(fd, None, ns=(atime, birthtime))\n                    else:\n                        os.utime(path, None, ns=(atime, birthtime), follow_symlinks=False)\n                except OSError:\n                    # some systems don't support calling utime on a symlink\n                    pass\n            try:\n                if fd:\n                    os.utime(fd, None, ns=(atime, mtime))\n                else:\n                    os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)\n            except OSError:\n                # some systems don't support calling utime on a symlink\n                pass\n            # bsdflags include the immutable flag and need to be set last:\n            if not self.noflags and \"bsdflags\" in item:\n                try:\n                    set_flags(path, item.bsdflags, fd=fd)\n                except OSError:\n                    pass\n        else:  # win32\n            # set timestamps rather late\n            mtime = item.mtime\n            atime = item.atime if \"atime\" in item else mtime\n            try:\n                # note: no fd support on win32\n                os.utime(path, None, ns=(atime, mtime))\n            except OSError:\n                # some systems don't support calling utime on a symlink\n                pass\n\n    def set_meta(self, key, value):\n        metadata = self._load_meta(self.id)\n        setattr(metadata, key, value)\n        if \"items\" in metadata:\n            del metadata.items\n        data = self.key.pack_metadata(metadata.as_dict())\n        new_id = self.key.id_hash(data)\n        self.cache.add_chunk(new_id, {}, data, stats=self.stats, ro_type=ROBJ_ARCHIVE_META)\n        self.manifest.archives.create(self.name, new_id, metadata.time, overwrite=True)\n        self.id = new_id\n\n    def rename(self, name):\n        old_id = self.id\n        self.name = name\n        self.set_meta(\"name\", name)\n        self.manifest.archives.delete_by_id(old_id)\n\n    def delete(self):\n        # quick and dirty: we just nuke the archive from the archives list - that will\n        # potentially orphan all chunks previously referenced by the archive, except the ones also\n        # referenced by other archives. In the end, \"borg compact\" will clean up and free space.\n        self.manifest.archives.delete_by_id(self.id)\n\n    @staticmethod\n    def compare_archives_iter(\n        archive1: \"Archive\", archive2: \"Archive\", matcher=None, can_compare_chunk_ids=False\n    ) -> Iterator[ItemDiff]:\n        \"\"\"\n        Yields an ItemDiff instance describing changes/indicating equality.\n\n        :param matcher: PatternMatcher class to restrict results to only matching paths.\n        :param can_compare_chunk_ids: Whether --chunker-params are the same for both archives.\n        \"\"\"\n\n        def compare_items(path: str, item1: Item, item2: Item):\n            return ItemDiff(\n                path,\n                item1,\n                item2,\n                archive1.pipeline.fetch_many(item1.get(\"chunks\", []), ro_type=ROBJ_FILE_STREAM),\n                archive2.pipeline.fetch_many(item2.get(\"chunks\", []), ro_type=ROBJ_FILE_STREAM),\n                can_compare_chunk_ids=can_compare_chunk_ids,\n            )\n\n        orphans_archive1: OrderedDict[str, Item] = OrderedDict()\n        orphans_archive2: OrderedDict[str, Item] = OrderedDict()\n\n        assert matcher is not None, \"matcher must be set\"\n\n        for item1, item2 in zip_longest(\n            archive1.iter_items(lambda item: matcher.match(item.path)),\n            archive2.iter_items(lambda item: matcher.match(item.path)),\n        ):\n            if item1 and item2 and item1.path == item2.path:\n                yield compare_items(item1.path, item1, item2)\n                continue\n            if item1:\n                matching_orphan = orphans_archive2.pop(item1.path, None)\n                if matching_orphan:\n                    yield compare_items(item1.path, item1, matching_orphan)\n                else:\n                    orphans_archive1[item1.path] = item1\n            if item2:\n                matching_orphan = orphans_archive1.pop(item2.path, None)\n                if matching_orphan:\n                    yield compare_items(matching_orphan.path, matching_orphan, item2)\n                else:\n                    orphans_archive2[item2.path] = item2\n        # At this point orphans_* contain items that had no matching partner in the other archive\n        for added in orphans_archive2.values():\n            path = added.path\n            deleted_item = Item.create_deleted(path)\n            yield compare_items(path, deleted_item, added)\n        for deleted in orphans_archive1.values():\n            path = deleted.path\n            deleted_item = Item.create_deleted(path)\n            yield compare_items(path, deleted, deleted_item)\n\n\nclass MetadataCollector:\n    def __init__(self, *, noatime, noctime, nobirthtime, numeric_ids, noflags, noacls, noxattrs):\n        self.noatime = noatime\n        self.noctime = noctime\n        self.numeric_ids = numeric_ids\n        self.noflags = noflags\n        self.noacls = noacls\n        self.noxattrs = noxattrs\n        self.nobirthtime = nobirthtime\n\n    def stat_simple_attrs(self, st, path, fd=None):\n        attrs = {}\n        attrs[\"mode\"] = st.st_mode\n        # borg can work with archives only having mtime (very old borg archives do not have\n        # atime/ctime). it can be useful to omit atime/ctime, if they change without the\n        # file content changing - e.g. to get better metadata deduplication.\n        attrs[\"mtime\"] = safe_ns(st.st_mtime_ns)\n        if not self.noatime:\n            attrs[\"atime\"] = safe_ns(st.st_atime_ns)\n        if not self.noctime:\n            attrs[\"ctime\"] = safe_ns(st.st_ctime_ns)\n        if not self.nobirthtime:\n            birthtime_ns = get_birthtime_ns(st, path, fd=fd)\n            if birthtime_ns is not None:\n                attrs[\"birthtime\"] = safe_ns(birthtime_ns)\n        attrs[\"uid\"] = st.st_uid\n        attrs[\"gid\"] = st.st_gid\n        if not self.numeric_ids:\n            user = uid2user(st.st_uid)\n            if user is not None:\n                attrs[\"user\"] = user\n            group = gid2group(st.st_gid)\n            if group is not None:\n                attrs[\"group\"] = group\n        if st.st_ino > 0:\n            attrs[\"inode\"] = st.st_ino\n        return attrs\n\n    def stat_ext_attrs(self, st, path, fd=None):\n        attrs = {}\n        if not self.noflags:\n            with backup_io(\"extended stat (flags)\"):\n                flags = get_flags(path, st, fd=fd)\n            attrs[\"bsdflags\"] = flags\n        if not self.noxattrs:\n            with backup_io(\"extended stat (xattrs)\"):\n                xattrs = xattr.get_all(fd or path, follow_symlinks=False)\n            attrs[\"xattrs\"] = StableDict(xattrs)\n        if not self.noacls:\n            with backup_io(\"extended stat (ACLs)\"):\n                try:\n                    acl_get(path, attrs, st, self.numeric_ids, fd=fd)\n                except OSError as e:\n                    if e.errno not in (errno.ENOTSUP,):\n                        raise\n        return attrs\n\n    def stat_attrs(self, st, path, fd=None):\n        attrs = self.stat_simple_attrs(st, path, fd=fd)\n        attrs |= self.stat_ext_attrs(st, path, fd=fd)\n        return attrs\n\n\n# remember a few recently used all-zero chunk hashes in this mapping.\n# (hash_func, chunk_length) -> chunk_hash\n# we play safe and have the hash_func in the mapping key, in case we\n# have different hash_funcs within the same borg run.\nzero_chunk_ids = LRUCache(10)  # type: ignore[var-annotated]\n\n\ndef cached_hash(chunk, id_hash):\n    allocation = chunk.meta[\"allocation\"]\n    if allocation == CH_DATA:\n        data = chunk.data\n        chunk_id = id_hash(data)\n    elif allocation in (CH_HOLE, CH_ALLOC):\n        size = chunk.meta[\"size\"]\n        assert size <= len(zeros)\n        data = memoryview(zeros)[:size]\n        try:\n            chunk_id = zero_chunk_ids[(id_hash, size)]\n        except KeyError:\n            chunk_id = id_hash(data)\n            zero_chunk_ids[(id_hash, size)] = chunk_id\n    else:\n        raise ValueError(\"unexpected allocation type\")\n    return chunk_id, data\n\n\nclass ChunksProcessor:\n    # Processes an iterator of chunks for an Item\n\n    def __init__(self, *, key, cache, add_item, rechunkify):\n        self.key = key\n        self.cache = cache\n        self.add_item = add_item\n        self.rechunkify = rechunkify\n\n    def process_file_chunks(self, item, cache, stats, show_progress, chunk_iter, chunk_processor=None):\n        if not chunk_processor:\n\n            def chunk_processor(chunk):\n                started_hashing = time.monotonic()\n                chunk_id, data = cached_hash(chunk, self.key.id_hash)\n                stats.hashing_time += time.monotonic() - started_hashing\n                chunk_entry = cache.add_chunk(chunk_id, {}, data, stats=stats, wait=False, ro_type=ROBJ_FILE_STREAM)\n                self.cache.repository.async_response(wait=False)\n                return chunk_entry\n\n        item.chunks = []\n        for chunk in chunk_iter:\n            chunk_entry = chunk_processor(chunk)\n            item.chunks.append(chunk_entry)\n            if show_progress:\n                stats.show_progress(item=item, dt=0.2)\n\n\ndef maybe_exclude_by_attr(item):\n    if xattrs := item.get(\"xattrs\"):\n        apple_excluded = xattrs.get(b\"com.apple.metadata:com_apple_backup_excludeItem\")\n        linux_excluded = xattrs.get(b\"user.xdg.robots.backup\")\n        if apple_excluded is not None or linux_excluded == b\"true\":\n            raise BackupItemExcluded\n\n    if flags := item.get(\"bsdflags\"):\n        if flags & stat.UF_NODUMP:\n            raise BackupItemExcluded\n\n\nclass FilesystemObjectProcessors:\n    # When ported to threading, then this doesn't need chunker, cache, key any more.\n    # process_file becomes a callback passed to __init__.\n\n    def __init__(\n        self,\n        *,\n        metadata_collector,\n        cache,\n        key,\n        add_item,\n        process_file_chunks,\n        chunker_params,\n        show_progress,\n        sparse,\n        log_json,\n        iec,\n        file_status_printer=None,\n        files_changed=\"mtime\" if is_win32 else \"ctime\",\n    ):\n        self.metadata_collector = metadata_collector\n        self.cache = cache\n        self.key = key\n        self.add_item = add_item\n        self.process_file_chunks = process_file_chunks\n        self.show_progress = show_progress\n        self.print_file_status = file_status_printer or (lambda *args: None)\n        self.files_changed = files_changed\n\n        self.hlm = HardLinkManager(id_type=tuple, info_type=(list, type(None)))  # (dev, ino) -> chunks or None\n        self.stats = Statistics(output_json=log_json, iec=iec)  # threading: done by cache (including progress)\n        self.cwd = os.getcwd()\n        self.chunker = get_chunker(*chunker_params, key=key, sparse=sparse)\n\n    @contextmanager\n    def create_helper(self, path, st, status=None, hardlinkable=True, strip_prefix=None):\n        if strip_prefix is not None:\n            assert not path.endswith(\"/\")\n            if strip_prefix.startswith(path + \"/\"):\n                # still on a directory level that shall be stripped - do not create an item for this!\n                yield None, \"x\", False, None\n                return\n            # adjust path, remove stripped directory levels\n            path = path.removeprefix(strip_prefix)\n\n        sanitized_path = remove_dotdot_prefixes(path)\n        item = Item(path=sanitized_path)\n        hardlinked = hardlinkable and st.st_nlink > 1\n        hl_chunks = None\n        update_map = False\n        if hardlinked:\n            status = \"h\"  # hard link\n            nothing = object()\n            chunks = self.hlm.retrieve(id=(st.st_ino, st.st_dev), default=nothing)\n            if chunks is nothing:\n                update_map = True\n            elif chunks is not None:\n                hl_chunks = chunks\n            item.hlid = self.hlm.hardlink_id_from_inode(ino=st.st_ino, dev=st.st_dev)\n        yield item, status, hardlinked, hl_chunks\n        maybe_exclude_by_attr(item)\n        self.add_item(item, stats=self.stats)\n        if update_map:\n            # remember the hlid of this fs object and if the item has chunks,\n            # also remember them, so we do not have to re-chunk a hard link.\n            chunks = item.chunks if \"chunks\" in item else None\n            self.hlm.remember(id=(st.st_ino, st.st_dev), info=chunks)\n\n    def process_dir_with_fd(self, *, path, fd, st, strip_prefix):\n        with self.create_helper(path, st, \"d\", hardlinkable=False, strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):\n            if item is not None:\n                item.update(self.metadata_collector.stat_attrs(st, path, fd=fd))\n            return status\n\n    def process_dir(self, *, path, parent_fd, name, st, strip_prefix):\n        with self.create_helper(path, st, \"d\", hardlinkable=False, strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):\n            if item is None:\n                return status\n            with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags_dir, noatime=True, op=\"dir_open\") as fd:\n                # fd is None for directories on windows, in that case a race condition check is not possible.\n                if fd is not None:\n                    with backup_io(\"fstat\"):\n                        st = stat_update_check(st, os.fstat(fd))\n                item.update(self.metadata_collector.stat_attrs(st, path, fd=fd))\n                return status\n\n    def process_fifo(self, *, path, parent_fd, name, st, strip_prefix):\n        with self.create_helper(path, st, \"f\", strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):  # fifo\n            if item is None:\n                return status\n            with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags_normal, noatime=True) as fd:\n                with backup_io(\"fstat\"):\n                    st = stat_update_check(st, os.fstat(fd))\n                item.update(self.metadata_collector.stat_attrs(st, path, fd=fd))\n                return status\n\n    def process_dev(self, *, path, parent_fd, name, st, dev_type, strip_prefix):\n        with self.create_helper(path, st, dev_type, strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):  # char/block device\n            # looks like we can not work fd-based here without causing issues when trying to open/close the device\n            if item is None:\n                return status\n            with backup_io(\"stat\"):\n                st = stat_update_check(st, os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False))\n            item.rdev = st.st_rdev\n            item.update(self.metadata_collector.stat_attrs(st, path))\n            return status\n\n    def process_symlink(self, *, path, parent_fd, name, st, strip_prefix):\n        with self.create_helper(path, st, \"s\", hardlinkable=True, strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):\n            if item is None:\n                return status\n            fname = name if name is not None and parent_fd is not None else path\n            with backup_io(\"readlink\"):\n                target = os.readlink(fname, dir_fd=parent_fd)\n            item.target = target\n            item.update(self.metadata_collector.stat_attrs(st, path))  # can't use FD here?\n            return status\n\n    def process_pipe(self, *, path, cache, fd, mode, user=None, group=None):\n        status = \"i\"  # stdin (or other pipe)\n        self.print_file_status(status, path)\n        status = None  # we already printed the status\n        if user is not None:\n            uid = user2uid(user)\n            if uid is None:\n                raise Error(\"no such user: %s\" % user)\n        else:\n            uid = None\n        if group is not None:\n            gid = group2gid(group)\n            if gid is None:\n                raise Error(\"no such group: %s\" % group)\n        else:\n            gid = None\n        t = int(time.time()) * 1000000000\n        item = Item(path=path, mode=mode & 0o107777 | 0o100000, mtime=t, atime=t, ctime=t)  # forcing regular file mode\n        if user is not None:\n            item.user = user\n        if group is not None:\n            item.group = group\n        if uid is not None:\n            item.uid = uid\n        if gid is not None:\n            item.gid = gid\n        self.process_file_chunks(item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd)))\n        item.get_size(memorize=True)\n        self.stats.nfiles += 1\n        self.add_item(item, stats=self.stats)\n        return status\n\n    def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, last_try=False, strip_prefix):\n        with self.create_helper(path, st, None, strip_prefix=strip_prefix) as (\n            item,\n            status,\n            hardlinked,\n            hl_chunks,\n        ):  # no status yet\n            if item is None:\n                return status\n            with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=True) as fd:\n                with backup_io(\"fstat\"):\n                    st = stat_update_check(st, os.fstat(fd))\n                item.update(self.metadata_collector.stat_simple_attrs(st, path, fd=fd))\n                item.update(self.metadata_collector.stat_ext_attrs(st, path, fd=fd))\n                maybe_exclude_by_attr(item)  # check early, before processing all the file content\n                is_special_file = is_special(st.st_mode)\n                if is_special_file:\n                    # we process a special file like a regular file. reflect that in mode,\n                    # so it can be extracted / accessed in FUSE mount like a regular file.\n                    # this needs to be done early, so that part files also get the patched mode.\n                    item.mode = stat.S_IFREG | stat.S_IMODE(item.mode)\n                # we begin processing chunks now.\n                if hl_chunks is not None:  # create_helper gave us chunks from a previous hard link\n                    item.chunks = []\n                    for chunk_id, chunk_size in hl_chunks:\n                        # process one-by-one, so we will know in item.chunks how far we got\n                        chunk_entry = cache.reuse_chunk(chunk_id, chunk_size, self.stats)\n                        item.chunks.append(chunk_entry)\n                else:  # normal case, no \"2nd+\" hard link\n                    if not is_special_file:\n                        hashed_path = safe_encode(item.path)  # path as in archive item!\n                        started_hashing = time.monotonic()\n                        path_hash = self.key.id_hash(hashed_path)\n                        self.stats.hashing_time += time.monotonic() - started_hashing\n                        known, chunks = cache.file_known_and_unchanged(hashed_path, path_hash, st)\n                    else:\n                        # in --read-special mode, we may be called for special files.\n                        # there should be no information in the cache about special files processed in\n                        # read-special mode, but we better play safe as this was wrong in the past:\n                        hashed_path = path_hash = None\n                        known, chunks = False, None\n                    if chunks is not None:\n                        # Make sure all ids are available\n                        for chunk in chunks:\n                            if not cache.seen_chunk(chunk.id):\n                                # cache said it is unmodified, but we lost a chunk: process file like modified\n                                status = \"M\"\n                                break\n                        else:\n                            item.chunks = []\n                            for chunk in chunks:\n                                # process one-by-one, so we will know in item.chunks how far we got\n                                cache.reuse_chunk(chunk.id, chunk.size, self.stats)\n                                item.chunks.append(chunk)\n                            status = \"U\"  # regular file, unchanged\n                    else:\n                        status = \"M\" if known else \"A\"  # regular file, modified or added\n                    self.print_file_status(status, path)\n                    # Only chunkify the file if needed\n                    changed_while_backup = False\n                    if \"chunks\" not in item:\n                        start_reading = time.time_ns()\n                        with backup_io(\"read\"):\n                            self.process_file_chunks(\n                                item,\n                                cache,\n                                self.stats,\n                                self.show_progress,\n                                backup_io_iter(self.chunker.chunkify(None, fd)),\n                            )\n                            self.stats.chunking_time = self.chunker.chunking_time\n                        end_reading = time.time_ns()\n                        with backup_io(\"fstat2\"):\n                            st2 = os.fstat(fd)\n                        if self.files_changed == \"disabled\" or is_special_file:\n                            # special files:\n                            # - fifos change naturally, because they are fed from the other side. no problem.\n                            # - blk/chr devices don't change ctime anyway.\n                            pass\n                        elif self.files_changed == \"ctime\":\n                            if st.st_ctime_ns != st2.st_ctime_ns:\n                                # ctime was changed, this is either a metadata or a data change.\n                                changed_while_backup = True\n                            elif start_reading - TIME_DIFFERS1_NS < st2.st_ctime_ns < end_reading + TIME_DIFFERS1_NS:\n                                # this is to treat a very special race condition, see #3536.\n                                # - file was changed right before st.ctime was determined.\n                                # - then, shortly afterwards, but already while we read the file, the\n                                #   file was changed again, but st2.ctime is the same due to ctime granularity.\n                                # when comparing file ctime to local clock, widen interval by TIME_DIFFERS1_NS.\n                                changed_while_backup = True\n                        elif self.files_changed == \"mtime\":\n                            if st.st_mtime_ns != st2.st_mtime_ns:\n                                # mtime was changed, this is either a data change.\n                                changed_while_backup = True\n                            elif start_reading - TIME_DIFFERS1_NS < st2.st_mtime_ns < end_reading + TIME_DIFFERS1_NS:\n                                # this is to treat a very special race condition, see #3536.\n                                # - file was changed right before st.mtime was determined.\n                                # - then, shortly afterwards, but already while we read the file, the\n                                #   file was changed again, but st2.mtime is the same due to mtime granularity.\n                                # when comparing file mtime to local clock, widen interval by TIME_DIFFERS1_NS.\n                                changed_while_backup = True\n                        if changed_while_backup:\n                            # regular file changed while we backed it up, might be inconsistent/corrupt!\n                            if last_try:\n                                status = \"C\"  # crap! retries did not help.\n                            else:\n                                raise BackupError(\"file changed while we read it!\")\n                        if not is_special_file and not changed_while_backup:\n                            # we must not memorize special files, because the contents of e.g. a\n                            # block or char device will change without its mtime/size/inode changing.\n                            # also, we must not memorize a potentially inconsistent/corrupt file that\n                            # changed while we backed it up.\n                            cache.memorize_file(hashed_path, path_hash, st, item.chunks)\n                    self.stats.files_stats[status] += 1  # must be done late\n                    if not changed_while_backup:\n                        status = None  # we already called print_file_status\n                self.stats.nfiles += 1\n                item.get_size(memorize=True)\n                return status\n\n\nclass TarfileObjectProcessors:\n    def __init__(\n        self,\n        *,\n        cache,\n        key,\n        add_item,\n        process_file_chunks,\n        chunker_params,\n        show_progress,\n        log_json,\n        iec,\n        file_status_printer=None,\n    ):\n        self.cache = cache\n        self.key = key\n        self.add_item = add_item\n        self.process_file_chunks = process_file_chunks\n        self.show_progress = show_progress\n        self.print_file_status = file_status_printer or (lambda *args: None)\n\n        self.stats = Statistics(output_json=log_json, iec=iec)  # threading: done by cache (including progress)\n        self.chunker = get_chunker(*chunker_params, key=key, sparse=False)\n        self.hlm = HardLinkManager(id_type=str, info_type=list)  # normalized/safe path -> chunks\n\n    @contextmanager\n    def create_helper(self, tarinfo, status=None, type=None):\n        ph = tarinfo.pax_headers\n        if ph and \"BORG.item.version\" in ph:\n            assert ph[\"BORG.item.version\"] == \"1\"\n            meta_bin = base64.b64decode(ph[\"BORG.item.meta\"])\n            meta_dict = msgpack.unpackb(meta_bin, object_hook=StableDict)\n            item = Item(internal_dict=meta_dict)\n        else:\n\n            def s_to_ns(s):\n                return safe_ns(int(float(s) * 1e9))\n\n            # if the tar has names starting with \"./\", normalize them like borg create also does.\n            # ./dir/file must become dir/file in the borg archive.\n            normalized_path = posixpath.normpath(tarinfo.name)\n            item = Item(\n                path=make_path_safe(normalized_path),\n                mode=tarinfo.mode | type,\n                uid=tarinfo.uid,\n                gid=tarinfo.gid,\n                mtime=s_to_ns(tarinfo.mtime),\n            )\n            if tarinfo.uname:\n                item.user = tarinfo.uname\n            if tarinfo.gname:\n                item.group = tarinfo.gname\n            if ph:\n                # note: for mtime this is a bit redundant as it is already done by tarfile module,\n                #       but we just do it in our way to be consistent for sure.\n                for name in \"atime\", \"ctime\", \"mtime\":\n                    if name in ph:\n                        ns = s_to_ns(ph[name])\n                        setattr(item, name, ns)\n                xattrs = StableDict()\n                for key, value in ph.items():\n                    if key.startswith(SCHILY_XATTR):\n                        key = key.removeprefix(SCHILY_XATTR)\n                        # the tarfile code gives us str keys and str values,\n                        # but we need bytes keys and bytes values.\n                        bkey = key.encode(\"utf-8\", errors=\"surrogateescape\")\n                        bvalue = value.encode(\"utf-8\", errors=\"surrogateescape\")\n                        xattrs[bkey] = bvalue\n                    elif key == SCHILY_ACL_ACCESS:\n                        # Process POSIX access ACL\n                        item.acl_access = value.encode(\"utf-8\", errors=\"surrogateescape\")\n                    elif key == SCHILY_ACL_DEFAULT:\n                        # Process POSIX default ACL\n                        item.acl_default = value.encode(\"utf-8\", errors=\"surrogateescape\")\n                if xattrs:\n                    item.xattrs = xattrs\n        yield item, status\n        # if we get here, \"with\"-block worked ok without error/exception, the item was processed ok...\n        self.add_item(item, stats=self.stats)\n\n    def process_dir(self, *, tarinfo, status, type):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            return status\n\n    def process_fifo(self, *, tarinfo, status, type):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            return status\n\n    def process_dev(self, *, tarinfo, status, type):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            item.rdev = os.makedev(tarinfo.devmajor, tarinfo.devminor)\n            return status\n\n    def process_symlink(self, *, tarinfo, status, type):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            item.target = tarinfo.linkname\n            return status\n\n    def process_hardlink(self, *, tarinfo, status, type):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            # create a not hardlinked borg item, reusing the chunks, see HardLinkManager.__doc__\n            normalized_path = posixpath.normpath(tarinfo.linkname)\n            safe_path = make_path_safe(normalized_path)\n            chunks = self.hlm.retrieve(safe_path)\n            if chunks is not None:\n                item.chunks = chunks\n            item.get_size(memorize=True, from_chunks=True)\n            self.stats.nfiles += 1\n            return status\n\n    def process_file(self, *, tarinfo, status, type, tar):\n        with self.create_helper(tarinfo, status, type) as (item, status):\n            self.print_file_status(status, item.path)\n            status = None  # we already printed the status\n            fd = tar.extractfile(tarinfo)\n            self.process_file_chunks(\n                item, self.cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd))\n            )\n            item.get_size(memorize=True, from_chunks=True)\n            self.stats.nfiles += 1\n            # we need to remember ALL files, see HardLinkManager.__doc__\n            self.hlm.remember(id=item.path, info=item.chunks)\n            return status\n\n\ndef valid_msgpacked_dict(d, keys_serialized):\n    \"\"\"check if the data <d> looks like a msgpacked dict\"\"\"\n    d_len = len(d)\n    if d_len == 0:\n        return False\n    if d[0] & 0xF0 == 0x80:  # object is a fixmap (up to 15 elements)\n        offs = 1\n    elif d[0] == 0xDE:  # object is a map16 (up to 2^16-1 elements)\n        offs = 3\n    else:\n        # object is not a map (dict)\n        # note: we must not have dicts with > 2^16-1 elements\n        return False\n    if d_len <= offs:\n        return False\n    # is the first dict key a bytestring?\n    if d[offs] & 0xE0 == 0xA0:  # key is a small bytestring (up to 31 chars)\n        pass\n    elif d[offs] in (0xD9, 0xDA, 0xDB):  # key is a str8, str16 or str32\n        pass\n    else:\n        # key is not a bytestring\n        return False\n    # is the bytestring any of the expected key names?\n    key_serialized = d[offs:]\n    return any(key_serialized.startswith(pattern) for pattern in keys_serialized)\n\n\nclass RobustUnpacker:\n    \"\"\"A restartable/robust version of the streaming msgpack unpacker\"\"\"\n\n    def __init__(self, validator, item_keys):\n        super().__init__()\n        self.item_keys = [msgpack.packb(name) for name in item_keys]\n        self.validator = validator\n        self._buffered_data = []\n        self._resync = False\n        self._unpacker = msgpack.Unpacker(object_hook=StableDict)\n\n    def resync(self):\n        self._buffered_data = []\n        self._resync = True\n\n    def feed(self, data):\n        if self._resync:\n            self._buffered_data.append(data)\n        else:\n            self._unpacker.feed(data)\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        if self._resync:\n            data = b\"\".join(self._buffered_data)\n            while self._resync:\n                if not data:\n                    raise StopIteration\n                # Abort early if the data does not look like a serialized item dict\n                if not valid_msgpacked_dict(data, self.item_keys):\n                    data = data[1:]\n                    continue\n                self._unpacker = msgpack.Unpacker(object_hook=StableDict)\n                self._unpacker.feed(data)\n                try:\n                    item = next(self._unpacker)\n                except (msgpack.UnpackException, StopIteration):\n                    # as long as we are resyncing, we also ignore StopIteration\n                    pass\n                else:\n                    if self.validator(item):\n                        self._resync = False\n                        return item\n                data = data[1:]\n        else:\n            return next(self._unpacker)\n\n\nclass ArchiveChecker:\n    def __init__(self):\n        self.error_found = False\n        self.key = None\n\n    def check(\n        self,\n        repository,\n        *,\n        verify_data=False,\n        repair=False,\n        find_lost_archives=False,\n        match=None,\n        sort_by=\"\",\n        first=0,\n        last=0,\n        older=None,\n        newer=None,\n        oldest=None,\n        newest=None,\n    ):\n        \"\"\"Perform a set of checks on 'repository'\n\n        :param repair: enable repair mode, write updated or corrected data into repository\n        :param find_lost_archives: create archive directory entries that are missing\n        :param first/last/sort_by: only check this number of first/last archives ordered by sort_by\n        :param match: only check archives matching this pattern\n        :param older/newer: only check archives older/newer than timedelta from now\n        :param oldest/newest: only check archives older/newer than timedelta from oldest/newest archive timestamp\n        :param verify_data: integrity verification of data referenced by archives\n        \"\"\"\n        if not isinstance(repository, (Repository, RemoteRepository)):\n            logger.error(\"Checking legacy repositories is not supported.\")\n            return False\n        logger.info(\"Starting archive consistency check...\")\n        self.check_all = not any((first, last, match, older, newer, oldest, newest))\n        self.repair = repair\n        self.repository = repository\n        # Repository.check already did a full repository-level check and has built and cached a fresh chunkindex -\n        # we can use that here, so we don't disable the caches (also no need to cache immediately, again):\n        self.chunks = build_chunkindex_from_repo(self.repository, disable_caches=False, cache_immediately=False)\n        if self.key is None:\n            self.key = self.make_key(repository)\n        self.repo_objs = RepoObj(self.key)\n        if verify_data:\n            self.verify_data()\n        rebuild_manifest = False\n        try:\n            repository.get_manifest()\n        except NoManifestError:\n            logger.error(\"Repository manifest is missing.\")\n            self.error_found = True\n            rebuild_manifest = True\n        else:\n            try:\n                self.manifest = Manifest.load(repository, (Manifest.Operation.CHECK,), key=self.key)\n            except IntegrityErrorBase as exc:\n                logger.error(\"Repository manifest is corrupted: %s\", exc)\n                self.error_found = True\n                rebuild_manifest = True\n        if rebuild_manifest:\n            self.manifest = self.rebuild_manifest()\n        if find_lost_archives:\n            self.rebuild_archives_directory()\n        self.rebuild_archives(\n            match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest\n        )\n        self.finish()\n        if self.error_found:\n            logger.error(\"Archive consistency check complete, problems found.\")\n        else:\n            logger.info(\"Archive consistency check complete, no problems found.\")\n        return self.repair or not self.error_found\n\n    def make_key(self, repository, manifest_only=False):\n        attempt = 0\n\n        #  try the manifest first!\n        try:\n            cdata = repository.get_manifest()\n        except NoManifestError:\n            pass\n        else:\n            try:\n                return key_factory(repository, cdata)\n            except UnsupportedPayloadError:\n                # we get here, if the cdata we got has a corrupted key type byte\n                pass  # ignore it, just continue trying\n\n        if not manifest_only:\n            for chunkid, _ in self.chunks.iteritems():\n                attempt += 1\n                if attempt > 999:\n                    # we did a lot of attempts, but could not create the key via key_factory, give up.\n                    break\n                cdata = repository.get(chunkid)\n                try:\n                    return key_factory(repository, cdata)\n                except UnsupportedPayloadError:\n                    # we get here, if the cdata we got has a corrupted key type byte\n                    pass  # ignore it, just try the next chunk\n\n        if attempt == 0:\n            if manifest_only:\n                msg = \"make_key: failed to create the key (tried only the manifest)\"\n            else:\n                msg = \"make_key: repository has no chunks at all!\"\n        else:\n            msg = \"make_key: failed to create the key (tried %d chunks)\" % attempt\n        raise IntegrityError(msg)\n\n    def verify_data(self):\n        logger.info(\"Starting cryptographic data integrity verification...\")\n        chunks_count = len(self.chunks)\n        errors = 0\n        defect_chunks = []\n        pi = ProgressIndicatorPercent(\n            total=chunks_count, msg=\"Verifying data %6.2f%%\", step=0.01, msgid=\"check.verify_data\"\n        )\n        for chunk_id, _ in self.chunks.iteritems():\n            pi.show()\n            try:\n                encrypted_data = self.repository.get(chunk_id)\n            except (Repository.ObjectNotFound, IntegrityErrorBase) as err:\n                self.error_found = True\n                errors += 1\n                logger.error(\"chunk %s: %s\", bin_to_hex(chunk_id), err)\n                if isinstance(err, IntegrityErrorBase):\n                    defect_chunks.append(chunk_id)\n            else:\n                try:\n                    # we must decompress, so it'll call assert_id() in there:\n                    self.repo_objs.parse(chunk_id, encrypted_data, decompress=True, ro_type=ROBJ_DONTCARE)\n                except IntegrityErrorBase as integrity_error:\n                    self.error_found = True\n                    errors += 1\n                    logger.error(\"chunk %s, integrity error: %s\", bin_to_hex(chunk_id), integrity_error)\n                    defect_chunks.append(chunk_id)\n        pi.finish()\n        if defect_chunks:\n            if self.repair:\n                # if we kill the defect chunk here, subsequent actions within this \"borg check\"\n                # run will find missing chunks.\n                logger.warning(\n                    \"Found defect chunks and will delete them now. \"\n                    \"Reading files referencing these chunks will result in an I/O error.\"\n                )\n                for defect_chunk in defect_chunks:\n                    # remote repo (ssh): retry might help for strange network / NIC / RAM errors\n                    # as the chunk will be retransmitted from remote server.\n                    # local repo (fs): as chunks.iteritems loop usually pumps a lot of data through,\n                    # a defect chunk is likely not in the fs cache any more and really gets re-read\n                    # from the underlying media.\n                    try:\n                        encrypted_data = self.repository.get(defect_chunk)\n                        # we must decompress, so it'll call assert_id() in there:\n                        self.repo_objs.parse(defect_chunk, encrypted_data, decompress=True, ro_type=ROBJ_DONTCARE)\n                    except IntegrityErrorBase:\n                        # failed twice -> get rid of this chunk\n                        del self.chunks[defect_chunk]\n                        self.repository.delete(defect_chunk)\n                        logger.debug(\"chunk %s deleted.\", bin_to_hex(defect_chunk))\n                    else:\n                        logger.warning(\"chunk %s not deleted, did not consistently fail.\", bin_to_hex(defect_chunk))\n            else:\n                logger.warning(\"Found defect chunks. With --repair, they would get deleted.\")\n                for defect_chunk in defect_chunks:\n                    logger.debug(\"chunk %s is defect.\", bin_to_hex(defect_chunk))\n        log = logger.error if errors else logger.info\n        log(\n            \"Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.\",\n            chunks_count,\n            errors,\n        )\n\n    def rebuild_manifest(self):\n        \"\"\"Rebuild the manifest object.\"\"\"\n\n        logger.info(\"Rebuilding missing/corrupted manifest.\")\n        # as we have lost the manifest, we do not know any more what valid item keys we had.\n        # collecting any key we encounter in a damaged repo seems unwise, thus we just use\n        # the hardcoded list from the source code. thus, it is not recommended to rebuild a\n        # lost manifest on a older borg version than the most recent one that was ever used\n        # within this repository (assuming that newer borg versions support more item keys).\n        return Manifest(self.key, self.repository)\n\n    def rebuild_archives_directory(self):\n        \"\"\"Rebuild the archives directory, undeleting archives.\n\n        Iterates through all objects in the repository looking for archive metadata blocks.\n        When finding some that do not have a corresponding archives directory entry (either\n        a normal entry for an \"existing\" archive, or a soft-deleted entry for a \"deleted\"\n        archive), it will create that entry (making the archives directory consistent with\n        the repository).\n        \"\"\"\n\n        def valid_archive(obj):\n            if not isinstance(obj, dict):\n                return False\n            return REQUIRED_ARCHIVE_KEYS.issubset(obj)\n\n        logger.info(\"Rebuilding missing archives directory entries, this might take some time...\")\n        pi = ProgressIndicatorPercent(\n            total=len(self.chunks),\n            msg=\"Rebuilding missing archives directory entries %6.2f%%\",\n            step=0.01,\n            msgid=\"check.rebuild_archives_directory\",\n        )\n        for chunk_id, _ in self.chunks.iteritems():\n            pi.show()\n            cdata = self.repository.get(chunk_id, read_data=False)  # only get metadata\n            try:\n                meta = self.repo_objs.parse_meta(chunk_id, cdata, ro_type=ROBJ_DONTCARE)\n            except IntegrityErrorBase as exc:\n                logger.error(\"Skipping corrupted chunk: %s\", exc)\n                self.error_found = True\n                continue\n            if meta[\"type\"] != ROBJ_ARCHIVE_META:\n                continue\n            # now we know it is an archive metadata chunk, load the full object from the repo:\n            cdata = self.repository.get(chunk_id)\n            try:\n                meta, data = self.repo_objs.parse(chunk_id, cdata, ro_type=ROBJ_DONTCARE)\n            except IntegrityErrorBase as exc:\n                logger.error(\"Skipping corrupted chunk: %s\", exc)\n                self.error_found = True\n                continue\n            if meta[\"type\"] != ROBJ_ARCHIVE_META:\n                continue  # should never happen\n            try:\n                archive = msgpack.unpackb(data)\n            # Ignore exceptions that might be raised when feeding msgpack with invalid data\n            except msgpack.UnpackException:\n                continue\n            if valid_archive(archive):\n                archive = self.key.unpack_archive(data)\n                archive = ArchiveItem(internal_dict=archive)\n                name = archive.name\n                archive_id, archive_id_hex = chunk_id, bin_to_hex(chunk_id)\n                if self.manifest.archives.exists_id(archive_id, deleted=False):\n                    logger.debug(f\"We already have an archives directory entry for {name} {archive_id_hex}.\")\n                elif self.manifest.archives.exists_id(archive_id, deleted=True):\n                    logger.debug(\n                        f\"We already have a soft-deleted archives directory entry for {name} {archive_id_hex}.\"\n                    )\n                else:\n                    self.error_found = True\n                    if self.repair:\n                        logger.warning(f\"Creating archives directory entry for {name} {archive_id_hex}.\")\n                        self.manifest.archives.create(name, archive_id, archive.time)\n                    else:\n                        logger.warning(f\"Would create archives directory entry for {name} {archive_id_hex}.\")\n\n        pi.finish()\n        logger.info(\"Rebuilding missing archives directory entries completed.\")\n\n    def rebuild_archives(\n        self, first=0, last=0, sort_by=\"\", match=None, older=None, newer=None, oldest=None, newest=None\n    ):\n        \"\"\"Analyze and rebuild archives, expecting some damage and trying to make stuff consistent again.\"\"\"\n\n        def add_callback(chunk):\n            id_ = self.key.id_hash(chunk)\n            cdata = self.repo_objs.format(id_, {}, chunk, ro_type=ROBJ_ARCHIVE_STREAM)\n            add_reference(id_, len(chunk), cdata)\n            return id_\n\n        def add_reference(id_, size, cdata):\n            # either we already have this chunk in repo and chunks index or we add it now\n            if id_ not in self.chunks:\n                assert cdata is not None\n                self.chunks[id_] = ChunkIndexEntry(flags=ChunkIndex.F_USED, size=size)\n                if self.repair:\n                    self.repository.put(id_, cdata)\n\n        def verify_file_chunks(archive_name, item):\n            \"\"\"Verifies that all file chunks are present. Missing file chunks will be logged.\"\"\"\n            offset = 0\n            for chunk in item.chunks:\n                chunk_id, size = chunk\n                if chunk_id not in self.chunks:\n                    logger.error(\n                        \"{}: {}: Missing file chunk detected (Byte {}-{}, Chunk {}).\".format(\n                            archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)\n                        )\n                    )\n                    self.error_found = True\n                offset += size\n            if \"size\" in item:\n                item_size = item.size\n                item_chunks_size = item.get_size(from_chunks=True)\n                if item_size != item_chunks_size:\n                    # just warn, but keep the inconsistency, so that borg extract can warn about it.\n                    logger.warning(\n                        \"{}: {}: size inconsistency detected: size {}, chunks size {}\".format(\n                            archive_name, item.path, item_size, item_chunks_size\n                        )\n                    )\n\n        def robust_iterator(archive):\n            \"\"\"Iterates through all archive items\n\n            Missing item chunks will be skipped and the msgpack stream will be restarted\n            \"\"\"\n            item_keys = self.manifest.item_keys\n            required_item_keys = REQUIRED_ITEM_KEYS\n            unpacker = RobustUnpacker(\n                lambda item: isinstance(item, StableDict) and \"path\" in item, self.manifest.item_keys\n            )\n            _state = 0\n\n            def missing_chunk_detector(chunk_id):\n                nonlocal _state\n                if _state % 2 != int(chunk_id not in self.chunks):\n                    _state += 1\n                return _state\n\n            def report(msg, chunk_id, chunk_no):\n                cid = bin_to_hex(chunk_id)\n                msg += \" [chunk: %06d_%s]\" % (chunk_no, cid)  # see \"debug dump-archive-items\"\n                self.error_found = True\n                logger.error(msg)\n\n            def list_keys_safe(keys):\n                return \", \".join(k.decode(errors=\"replace\") if isinstance(k, bytes) else str(k) for k in keys)\n\n            def valid_item(obj):\n                if not isinstance(obj, StableDict):\n                    return False, \"not a dictionary\"\n                keys = set(obj)\n                if not required_item_keys.issubset(keys):\n                    return False, \"missing required keys: \" + list_keys_safe(required_item_keys - keys)\n                if not keys.issubset(item_keys):\n                    return False, \"invalid keys: \" + list_keys_safe(keys - item_keys)\n                return True, \"\"\n\n            i = 0\n            archive_items = archive_get_items(archive, repo_objs=self.repo_objs, repository=repository)\n            for state, items in groupby(archive_items, missing_chunk_detector):\n                items = list(items)\n                if state % 2:\n                    for chunk_id in items:\n                        report(\"item metadata chunk missing\", chunk_id, i)\n                        i += 1\n                    continue\n                if state > 0:\n                    unpacker.resync()\n                for chunk_id, cdata in zip(items, repository.get_many(items)):\n                    try:\n                        _, data = self.repo_objs.parse(chunk_id, cdata, ro_type=ROBJ_ARCHIVE_STREAM)\n                        unpacker.feed(data)\n                        for item in unpacker:\n                            valid, reason = valid_item(item)\n                            if valid:\n                                yield Item(internal_dict=item)\n                            else:\n                                report(\n                                    \"Did not get expected metadata dict when unpacking item metadata (%s)\" % reason,\n                                    chunk_id,\n                                    i,\n                                )\n                    except IntegrityError as integrity_error:\n                        # repo_objs.parse() detected integrity issues.\n                        # maybe the repo gave us a valid cdata, but not for the chunk_id we wanted.\n                        # or the authentication of cdata failed, meaning the encrypted data was corrupted.\n                        report(str(integrity_error), chunk_id, i)\n                    except msgpack.UnpackException:\n                        report(\"Unpacker crashed while unpacking item metadata, trying to resync...\", chunk_id, i)\n                        unpacker.resync()\n                    except Exception:\n                        report(\"Exception while decrypting or unpacking item metadata\", chunk_id, i)\n                        raise\n                    i += 1\n\n        sort_by = sort_by.split(\",\")\n        if any((first, last, match, older, newer, newest, oldest)):\n            archive_infos = self.manifest.archives.list(\n                sort_by=sort_by,\n                match=match,\n                first=first,\n                last=last,\n                oldest=oldest,\n                newest=newest,\n                older=older,\n                newer=newer,\n            )\n            if match and not archive_infos:\n                logger.warning(\"--match-archives %s does not match any archives\", match)\n            if first and len(archive_infos) < first:\n                logger.warning(\"--first %d archives: only found %d archives\", first, len(archive_infos))\n            if last and len(archive_infos) < last:\n                logger.warning(\"--last %d archives: only found %d archives\", last, len(archive_infos))\n        else:\n            archive_infos = self.manifest.archives.list(sort_by=sort_by)\n        num_archives = len(archive_infos)\n\n        pi = ProgressIndicatorPercent(\n            total=num_archives, msg=\"Checking archives %3.1f%%\", step=0.1, msgid=\"check.rebuild_archives\"\n        )\n        with cache_if_remote(self.repository) as repository:\n            for i, info in enumerate(archive_infos):\n                pi.show(i)\n                archive_id, archive_id_hex = info.id, bin_to_hex(info.id)\n                logger.info(\n                    f\"Analyzing archive {info.name} {info.ts.astimezone()} {archive_id_hex} ({i + 1}/{num_archives})\"\n                )\n                if archive_id not in self.chunks:\n                    logger.error(f\"Archive metadata block {archive_id_hex} is missing!\")\n                    self.error_found = True\n                    if self.repair:\n                        logger.error(f\"Deleting broken archive {info.name} {archive_id_hex}.\")\n                        self.manifest.archives.delete_by_id(archive_id)\n                    else:\n                        logger.error(f\"Would delete broken archive {info.name} {archive_id_hex}.\")\n                    continue\n                cdata = self.repository.get(archive_id)\n                try:\n                    _, data = self.repo_objs.parse(archive_id, cdata, ro_type=ROBJ_ARCHIVE_META)\n                except IntegrityError as integrity_error:\n                    logger.error(f\"Archive metadata block {archive_id_hex} is corrupted: {integrity_error}\")\n                    self.error_found = True\n                    if self.repair:\n                        logger.error(f\"Deleting broken archive {info.name} {archive_id_hex}.\")\n                        self.manifest.archives.delete_by_id(archive_id)\n                    else:\n                        logger.error(f\"Would delete broken archive {info.name} {archive_id_hex}.\")\n                    continue\n                archive = self.key.unpack_archive(data)\n                archive = ArchiveItem(internal_dict=archive)\n                if archive.version != 2:\n                    raise Exception(\"Unknown archive metadata version\")\n                items_buffer = ChunkBuffer(self.key)\n                items_buffer.write_chunk = add_callback\n                for item in robust_iterator(archive):\n                    if \"chunks\" in item:\n                        verify_file_chunks(info.name, item)\n                    items_buffer.add(item)\n                items_buffer.flush(flush=True)\n                if self.repair:\n                    archive.item_ptrs = archive_put_items(\n                        items_buffer.chunks, repo_objs=self.repo_objs, add_reference=add_reference\n                    )\n                    data = self.key.pack_metadata(archive.as_dict())\n                    new_archive_id = self.key.id_hash(data)\n                    logger.debug(f\"archive id old: {bin_to_hex(archive_id)}\")\n                    logger.debug(f\"archive id new: {bin_to_hex(new_archive_id)}\")\n                    cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META)\n                    add_reference(new_archive_id, len(data), cdata)\n                    self.manifest.archives.create(info.name, new_archive_id, info.ts)\n                    if archive_id != new_archive_id:\n                        self.manifest.archives.delete_by_id(archive_id)\n            pi.finish()\n\n    def finish(self):\n        if self.repair:\n            # we may have deleted chunks, remove the chunks index cache!\n            logger.info(\"Deleting chunks cache in repository - next repository access will cause a rebuild.\")\n            delete_chunkindex_cache(self.repository)\n            logger.info(\"Writing Manifest.\")\n            self.manifest.write()\n\n\nclass ArchiveRecreater:\n    class Interrupted(Exception):\n        def __init__(self, metadata=None):\n            self.metadata = metadata or {}\n\n    @staticmethod\n    def is_temporary_archive(archive_name):\n        return archive_name.endswith(\".recreate\")\n\n    def __init__(\n        self,\n        manifest,\n        cache,\n        matcher,\n        exclude_caches=False,\n        exclude_if_present=None,\n        keep_exclude_tags=False,\n        chunker_params=None,\n        compression=None,\n        dry_run=False,\n        stats=False,\n        progress=False,\n        file_status_printer=None,\n        timestamp=None,\n    ):\n        self.manifest = manifest\n        self.repository = manifest.repository\n        self.key = manifest.key\n        self.repo_objs = manifest.repo_objs\n        self.cache = cache\n\n        self.matcher = matcher\n        self.exclude_caches = exclude_caches\n        self.exclude_if_present = exclude_if_present or []\n        self.keep_exclude_tags = keep_exclude_tags\n\n        self.rechunkify = chunker_params is not None\n        if self.rechunkify:\n            logger.debug(\"Rechunking archives to %s\", chunker_params)\n        self.chunker_params = chunker_params or CHUNKER_PARAMS\n        self.compression = compression or CompressionSpec(\"none\")\n        self.seen_chunks = set()\n\n        self.timestamp = timestamp\n        self.dry_run = dry_run\n        self.stats = stats\n        self.progress = progress\n        self.print_file_status = file_status_printer or (lambda *args: None)\n\n    def recreate(self, archive_id, target_name, delete_original, comment=None):\n        archive = self.open_archive(archive_id)\n        target = self.create_target(archive, target_name)\n        if self.exclude_if_present or self.exclude_caches:\n            self.matcher_add_tagged_dirs(archive)\n        if self.matcher.empty() and not target.recreate_rechunkify and comment is None:\n            # nothing to do\n            return False\n        self.process_items(archive, target)\n        self.save(archive, target, comment, delete_original=delete_original)\n        return True\n\n    def process_items(self, archive, target):\n        matcher = self.matcher\n\n        for item in archive.iter_items():\n            if not matcher.match(item.path):\n                self.print_file_status(\"-\", item.path)  # excluded (either by \"-\" or by \"!\")\n                continue\n            if self.dry_run:\n                self.print_file_status(\"+\", item.path)  # included\n            else:\n                self.process_item(archive, target, item)\n        if self.progress:\n            target.stats.show_progress(final=True)\n\n    def process_item(self, archive, target, item):\n        status = file_status(item.mode)\n        if \"chunks\" in item:\n            self.print_file_status(status, item.path)\n            status = None\n            self.process_chunks(archive, target, item)\n            target.stats.nfiles += 1\n        target.add_item(item, stats=target.stats)\n        self.print_file_status(status, item.path)\n\n    def process_chunks(self, archive, target, item):\n        if not target.recreate_rechunkify:\n            for chunk_id, size in item.chunks:\n                self.cache.reuse_chunk(chunk_id, size, target.stats)\n            return item.chunks\n        chunk_iterator = self.iter_chunks(archive, target, list(item.chunks))\n        chunk_processor = partial(self.chunk_processor, target)\n        target.process_file_chunks(item, self.cache, target.stats, self.progress, chunk_iterator, chunk_processor)\n\n    def chunk_processor(self, target, chunk):\n        chunk_id, data = cached_hash(chunk, self.key.id_hash)\n        size = len(data)\n        if chunk_id in self.seen_chunks:\n            return self.cache.reuse_chunk(chunk_id, size, target.stats)\n        chunk_entry = self.cache.add_chunk(chunk_id, {}, data, stats=target.stats, wait=False, ro_type=ROBJ_FILE_STREAM)\n        self.cache.repository.async_response(wait=False)\n        self.seen_chunks.add(chunk_entry.id)\n        return chunk_entry\n\n    def iter_chunks(self, archive, target, chunks):\n        chunk_iterator = archive.pipeline.fetch_many(chunks, ro_type=ROBJ_FILE_STREAM)\n        if target.recreate_rechunkify:\n            # The target.chunker will read the file contents through ChunkIteratorFileWrapper chunk-by-chunk\n            # (does not load the entire file into memory)\n            file = ChunkIteratorFileWrapper(chunk_iterator)\n            yield from target.chunker.chunkify(file)\n        else:\n            for chunk in chunk_iterator:\n                yield Chunk(chunk, size=len(chunk), allocation=CH_DATA)\n\n    def save(self, archive, target, comment=None, delete_original=True):\n        if self.dry_run:\n            return\n        if comment is None:\n            comment = archive.metadata.get(\"comment\", \"\")\n        additional_metadata = {\n            \"command_line\": archive.metadata.command_line,\n            # but also remember recreate metadata:\n            \"recreate_command_line\": join_cmd(sys.argv),\n        }\n        if self.timestamp is None:\n            # if no timestamp is specified, keep the original timestamp\n            additional_metadata[\"time\"] = archive.metadata.time\n        target.save(comment=comment, timestamp=self.timestamp, additional_metadata=additional_metadata)\n        if delete_original:\n            archive.delete()\n        if self.stats:\n            log_multi(str(target), str(target.stats))\n\n    def matcher_add_tagged_dirs(self, archive):\n        \"\"\"Add excludes to the matcher created by exclude_cache and exclude_if_present.\"\"\"\n\n        def exclude(dir, tag_item):\n            if self.keep_exclude_tags:\n                tag_files.append(PathPrefixPattern(tag_item.path, recurse_dir=False))\n                tagged_dirs.append(FnmatchPattern(dir + \"/\", recurse_dir=False))\n            else:\n                tagged_dirs.append(PathPrefixPattern(dir, recurse_dir=False))\n\n        matcher = self.matcher\n        tag_files = []\n        tagged_dirs = []\n\n        for item in archive.iter_items(\n            filter=lambda item: os.path.basename(item.path) == CACHE_TAG_NAME or matcher.match(item.path)\n        ):\n            dir, tag_file = os.path.split(item.path)\n            if tag_file in self.exclude_if_present:\n                exclude(dir, item)\n            elif self.exclude_caches and tag_file == CACHE_TAG_NAME and stat.S_ISREG(item.mode):\n                file = open_item(archive, item)\n                if file.read(len(CACHE_TAG_CONTENTS)) == CACHE_TAG_CONTENTS:\n                    exclude(dir, item)\n        matcher.add(tag_files, IECommand.Include)\n        matcher.add(tagged_dirs, IECommand.ExcludeNoRecurse)\n\n    def create_target(self, archive, target_name):\n        \"\"\"Create target archive.\"\"\"\n        target = self.create_target_archive(target_name)\n        # If the archives use the same chunker params, then don't rechunkify\n        source_chunker_params = tuple(archive.metadata.get(\"chunker_params\", []))\n        if len(source_chunker_params) == 4 and isinstance(source_chunker_params[0], int):\n            # this is a borg < 1.2 chunker_params tuple, no chunker algo specified, but we only had buzhash:\n            source_chunker_params = (CH_BUZHASH,) + source_chunker_params\n        target.recreate_rechunkify = self.rechunkify and source_chunker_params != target.chunker_params\n        if target.recreate_rechunkify:\n            logger.debug(\n                \"Rechunking archive from %s to %s\", source_chunker_params or \"(unknown)\", target.chunker_params\n            )\n        target.process_file_chunks = ChunksProcessor(\n            cache=self.cache, key=self.key, add_item=target.add_item, rechunkify=target.recreate_rechunkify\n        ).process_file_chunks\n        target.chunker = get_chunker(*target.chunker_params, key=self.key, sparse=False)\n        return target\n\n    def create_target_archive(self, name):\n        target = Archive(\n            self.manifest,\n            name,\n            create=True,\n            progress=self.progress,\n            chunker_params=self.chunker_params,\n            cache=self.cache,\n        )\n        return target\n\n    def open_archive(self, archive_id, **kwargs):\n        return Archive(self.manifest, archive_id, cache=self.cache, **kwargs)\n"
  },
  {
    "path": "src/borg/archiver/__init__.py",
    "content": "# borg cli interface / toplevel archiver code\n\nimport sys\nimport traceback\n\n# quickfix to disallow running borg with assertions switched off\ntry:\n    assert False\nexcept AssertionError:\n    pass  # OK\nelse:\n    print(\n        \"Borg requires working assertions. Please run Python without -O and/or unset PYTHONOPTIMIZE.\", file=sys.stderr\n    )\n    sys.exit(2)  # == EXIT_ERROR\n\ntry:\n    import faulthandler\n    import functools\n    import inspect\n    import itertools\n    import json\n    import logging\n    import os\n    import shlex\n    import signal\n    from datetime import datetime, timezone\n\n    from ..logger import create_logger, setup_logging\n\n    logger = create_logger()\n\n    from ._common import Highlander\n    from .. import __version__\n    from ..constants import *  # NOQA\n    from ..helpers import EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE\n    from ..helpers import Error, CommandError, get_ec, modern_ec\n    from ..helpers import add_warning, BorgWarning, BackupWarning\n    from ..helpers import format_file_size\n    from ..helpers import remove_surrogates, text_to_json\n    from ..helpers import DatetimeWrapper, replace_placeholders\n    from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, SUPPRESS\n    from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo\n    from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm\n    from ..helpers import ErrorIgnoringTextIOWrapper\n    from ..helpers import msgpack\n    from ..helpers import sig_int\n    from ..helpers import get_config_dir\n    from ..platformflags import is_msystem\n    from ..remote import RemoteRepository\n    from ..selftest import selftest\nexcept BaseException:\n    # an unhandled exception in the try-block would cause the borg cli command to exit with rc 1 due to python's\n    # default behavior, see issue #4424.\n    # as borg defines rc 1 as WARNING, this would be a mismatch, because a crash should be an ERROR (rc 2).\n    traceback.print_exc()\n    sys.exit(2)  # == EXIT_ERROR\n\nassert EXIT_ERROR == 2, \"EXIT_ERROR is not 2, as expected - fix assert AND exception handler right above this line.\"\n\n\nSTATS_HEADER = \"                       Original size    Deduplicated size\"\n\nPURE_PYTHON_MSGPACK_WARNING = \"Using a pure-python msgpack! This will result in lower performance.\"\n\n\nfrom .analyze_cmd import AnalyzeMixIn\nfrom .benchmark_cmd import BenchmarkMixIn\nfrom .check_cmd import CheckMixIn\nfrom .compact_cmd import CompactMixIn\nfrom .completion_cmd import CompletionMixIn\nfrom .create_cmd import CreateMixIn\nfrom .debug_cmd import DebugMixIn\nfrom .delete_cmd import DeleteMixIn\nfrom .diff_cmd import DiffMixIn\nfrom .extract_cmd import ExtractMixIn\nfrom .help_cmd import HelpMixIn\nfrom .info_cmd import InfoMixIn\nfrom .key_cmds import KeysMixIn\nfrom .list_cmd import ListMixIn\nfrom .lock_cmds import LocksMixIn\nfrom .mount_cmds import MountMixIn\nfrom .prune_cmd import PruneMixIn\nfrom .repo_compress_cmd import RepoCompressMixIn\nfrom .recreate_cmd import RecreateMixIn\nfrom .rename_cmd import RenameMixIn\nfrom .repo_create_cmd import RepoCreateMixIn\nfrom .repo_info_cmd import RepoInfoMixIn\nfrom .repo_delete_cmd import RepoDeleteMixIn\nfrom .repo_list_cmd import RepoListMixIn\nfrom .repo_space_cmd import RepoSpaceMixIn\nfrom .serve_cmd import ServeMixIn\nfrom .tag_cmd import TagMixIn\nfrom .tar_cmds import TarMixIn\nfrom .transfer_cmd import TransferMixIn\nfrom .undelete_cmd import UnDeleteMixIn\nfrom .version_cmd import VersionMixIn\n\n\nclass Archiver(\n    AnalyzeMixIn,\n    BenchmarkMixIn,\n    CheckMixIn,\n    CompactMixIn,\n    CompletionMixIn,\n    CreateMixIn,\n    DebugMixIn,\n    DeleteMixIn,\n    DiffMixIn,\n    ExtractMixIn,\n    HelpMixIn,\n    InfoMixIn,\n    KeysMixIn,\n    ListMixIn,\n    LocksMixIn,\n    MountMixIn,\n    PruneMixIn,\n    RecreateMixIn,\n    RenameMixIn,\n    RepoCompressMixIn,\n    RepoCreateMixIn,\n    RepoDeleteMixIn,\n    RepoInfoMixIn,\n    RepoListMixIn,\n    RepoSpaceMixIn,\n    ServeMixIn,\n    TagMixIn,\n    TarMixIn,\n    TransferMixIn,\n    UnDeleteMixIn,\n    VersionMixIn,\n):\n    def __init__(self, lock_wait=None, prog=None):\n        self.lock_wait = lock_wait\n        self.prog = prog\n        self.start_backup = None\n\n    def print_warning(self, msg, *args, **kw):\n        warning_code = kw.get(\"wc\", EXIT_WARNING)  # note: wc=None can be used to not influence exit code\n        warning_type = kw.get(\"wt\", \"percent\")\n        assert warning_type in (\"percent\", \"curly\")\n        warning_msgid = kw.get(\"msgid\")\n        if warning_code is not None:\n            add_warning(msg, *args, wc=warning_code, wt=warning_type)\n        if warning_type == \"percent\":\n            output = args and msg % args or msg\n        else:  # == \"curly\"\n            output = args and msg.format(*args) or msg\n        logger.warning(output, msgid=warning_msgid) if warning_msgid else logger.warning(output)\n\n    def print_warning_instance(self, warning):\n        assert isinstance(warning, BorgWarning)\n        # if it is a BackupWarning, use the wrapped BackupError exception instance:\n        cls = type(warning.args[1]) if isinstance(warning, BackupWarning) else type(warning)\n        msg, msgid, args, wc = cls.__doc__, cls.__qualname__, warning.args, warning.exit_code\n        self.print_warning(msg, *args, wc=wc, wt=\"curly\", msgid=msgid)\n\n    def print_file_status(self, status, path):\n        # if we get called with status == None, the final file status was already printed\n        if self.output_list and status is not None and (self.output_filter is None or status in self.output_filter):\n            if self.log_json:\n                json_data = {\"type\": \"file_status\", \"status\": status}\n                json_data |= text_to_json(\"path\", path)\n                print(json.dumps(json_data), file=sys.stderr)\n            else:\n                logging.getLogger(\"borg.output.list\").info(\"%1s %s\", status, remove_surrogates(path))\n\n    def preprocess_args(self, args):\n        deprecations = [\n            # ('--old', '--new' or None, 'Warning: \"--old\" has been deprecated. Use \"--new\" instead.'),\n        ]\n        for i, arg in enumerate(args[:]):\n            for old_name, new_name, warning in deprecations:\n                # either --old_name or --old_name=...\n                if arg == old_name or (arg.startswith(old_name) and arg[len(old_name)] == \"=\"):\n                    if new_name is not None:\n                        args[i] = arg.replace(old_name, new_name)\n                    print(warning, file=sys.stderr)\n        return args\n\n    class CommonOptions:\n        \"\"\"\n        Support class to allow specifying common options at multiple levels of the command hierarchy.\n\n        Common options (e.g. --log-level, --repo) can be placed anywhere in the command line:\n\n            borg --info create ...          # before the subcommand\n            borg create --info ...          # after the subcommand\n            borg --info debug info --debug  # at both levels of a two-level command\n\n        Each parser level registers the same options with the same dest names.\n        Defaults are only provided on the top-level parser; all sub-parsers use SUPPRESS so\n        that unset options don't appear in the namespace at all.\n\n        flatten_namespace() handles precedence: it walks sub-namespaces depth-first, so the\n        most-specific (innermost) value wins.  For append-action options (e.g. --debug-topic)\n        it merges lists from all levels.\n        \"\"\"\n\n        def __init__(self, define_common_options):\n            \"\"\"\n            *define_common_options* should be a callable taking one argument, which\n            will be an argparse.Parser.add_argument-like function.\n\n            *define_common_options* will be called multiple times, and should call\n            the passed function to define common options exactly the same way each time.\n            \"\"\"\n            self.define_common_options = define_common_options\n            # This is the sentinel object that replaces all default values in parsers\n            # below the top-level parser.\n            self.default_sentinel = object()\n\n        def add_common_group(self, parser, provide_defaults=False):\n            \"\"\"\n            Add common options to *parser*.\n\n            *provide_defaults* must be True exactly once in a parser hierarchy (the top-level\n            parser) and False on all sub-parsers.  Sub-parsers get SUPPRESS as the default so\n            that an unspecified option produces no attribute, leaving the top-level default intact\n            after flatten_namespace() merges the namespaces.\n            \"\"\"\n\n            def add_argument(*args, **kwargs):\n                if \"dest\" in kwargs:\n                    kwargs.setdefault(\"action\", \"store\")\n                    assert kwargs[\"action\"] in (\n                        Highlander,\n                        \"help\",\n                        \"store_const\",\n                        \"store_true\",\n                        \"store_false\",\n                        \"store\",\n                        \"append\",\n                    )\n                    is_append = kwargs[\"action\"] == \"append\"\n                    if not provide_defaults:\n                        # Interpolate help now, in case %(default)d (or similar) is mentioned,\n                        # to avoid producing incorrect help output.\n                        kwargs[\"help\"] = kwargs[\"help\"] % kwargs\n                        if not is_append:\n                            kwargs[\"default\"] = SUPPRESS\n\n                common_group.add_argument(*args, **kwargs)\n\n            common_group = parser.add_argument_group(\"Common options\")\n            self.define_common_options(add_argument)\n\n    def build_parser(self):\n        from ._common import define_common_options\n\n        parser = ArgumentParser(\n            prog=self.prog,\n            description=\"Borg - Deduplicated Backups\",\n            default_config_files=[os.path.join(get_config_dir(), \"default.yaml\")],\n            default_env=True,\n            env_prefix=\"BORG\",\n        )\n        parser.add_argument(\"--config\", action=\"config\")\n        # paths and patterns must have an empty list as default everywhere\n        parser.common_options = self.CommonOptions(define_common_options)\n        parser.add_argument(\n            \"-V\", \"--version\", action=\"version\", version=\"%(prog)s \" + __version__, help=\"show version number and exit\"\n        )\n        parser.add_argument(\"--cockpit\", dest=\"cockpit\", action=\"store_true\", help=\"Start the Borg TUI\")\n        parser.common_options.add_common_group(parser, provide_defaults=True)\n\n        common_parser = ArgumentParser(prog=self.prog)\n        parser.common_options.add_common_group(common_parser)\n\n        mid_common_parser = ArgumentParser(prog=self.prog)\n        parser.common_options.add_common_group(mid_common_parser)\n\n        if parser.prog == \"borgfs\":\n            return self.build_parser_borgfs(parser)\n\n        subparsers = parser.add_subcommands(required=False, title=\"required arguments\", metavar=\"<command>\")\n\n        self.build_parser_analyze(subparsers, common_parser, mid_common_parser)\n        self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)\n        self.build_parser_check(subparsers, common_parser, mid_common_parser)\n        self.build_parser_compact(subparsers, common_parser, mid_common_parser)\n        self.build_parser_completion(subparsers, common_parser, mid_common_parser)\n        self.build_parser_create(subparsers, common_parser, mid_common_parser)\n        self.build_parser_debug(subparsers, common_parser, mid_common_parser)\n        self.build_parser_delete(subparsers, common_parser, mid_common_parser)\n        self.build_parser_diff(subparsers, common_parser, mid_common_parser)\n        self.build_parser_extract(subparsers, common_parser, mid_common_parser)\n        self.build_parser_help(subparsers, common_parser, mid_common_parser, parser)\n        self.build_parser_info(subparsers, common_parser, mid_common_parser)\n        self.build_parser_keys(subparsers, common_parser, mid_common_parser)\n        self.build_parser_list(subparsers, common_parser, mid_common_parser)\n        self.build_parser_locks(subparsers, common_parser, mid_common_parser)\n        self.build_parser_mount_umount(subparsers, common_parser, mid_common_parser)\n        self.build_parser_prune(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_compress(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_create(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_delete(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_info(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_list(subparsers, common_parser, mid_common_parser)\n        self.build_parser_recreate(subparsers, common_parser, mid_common_parser)\n        self.build_parser_rename(subparsers, common_parser, mid_common_parser)\n        self.build_parser_repo_space(subparsers, common_parser, mid_common_parser)\n        self.build_parser_serve(subparsers, common_parser, mid_common_parser)\n        self.build_parser_tag(subparsers, common_parser, mid_common_parser)\n        self.build_parser_tar(subparsers, common_parser, mid_common_parser)\n        self.build_parser_transfer(subparsers, common_parser, mid_common_parser)\n        self.build_parser_undelete(subparsers, common_parser, mid_common_parser)\n        self.build_parser_version(subparsers, common_parser, mid_common_parser)\n        return parser\n\n    def get_args(self, argv, cmd):\n        \"\"\"Usually just returns argv, except when dealing with an SSH forced command for borg serve.\"\"\"\n        result = self.parse_args(argv[1:])\n        if cmd is not None and result.func == self.do_serve:\n            # borg serve case:\n            # - \"result\" is how borg got invoked (e.g. via forced command from authorized_keys),\n            # - \"client_result\" (from \"cmd\") refers to the command the client wanted to execute,\n            #   which might be different in the case of a forced command or same otherwise.\n            client_argv = shlex.split(cmd)\n            # Drop environment variables (do *not* interpret them) before trying to parse\n            # the borg command line.\n            client_argv = list(itertools.dropwhile(lambda arg: \"=\" in arg, client_argv))\n            client_result = self.parse_args(client_argv[1:])\n            if client_result.func == result.func:\n                # make sure we only process like normal if the client is executing\n                # the same command as specified in the forced command, otherwise\n                # just skip this block and return the forced command (== result).\n                # client is allowed to specify the allowlisted options,\n                # everything else comes from the forced \"borg serve\" command (or the defaults).\n                # stuff from denylist must never be used from the client.\n                denylist = {\"restrict_to_paths\", \"restrict_to_repositories\", \"umask\", \"permissions\"}\n                allowlist = {\"debug_topics\", \"lock_wait\", \"log_level\"}\n                not_present = object()\n                for attr_name in allowlist:\n                    assert attr_name not in denylist, \"allowlist has denylisted attribute name %s\" % attr_name\n                    value = getattr(client_result, attr_name, not_present)\n                    if value is not not_present:\n                        # note: it is not possible to specify a allowlisted option via a forced command,\n                        # it always gets overridden by the value specified (or defaulted to) by the client command.\n                        setattr(result, attr_name, value)\n\n        return result\n\n    def parse_args(self, args=None):\n        if args:\n            args = self.preprocess_args(args)\n        parser = self.build_parser()\n        args = parser.parse_args(args or [\"-h\"])\n        args = flatten_namespace(args)\n\n        # Ensure list defaults previously handled by set_defaults are present\n        for list_attr in (\"paths\", \"patterns\", \"pattern_roots\"):\n            if getattr(args, list_attr, None) is None:\n                setattr(args, list_attr, [])\n\n        func = self.get_func(args, parser)\n        if func == self.do_create and args.paths and args.paths_from_stdin:\n            parser.error(\"Must not pass PATH with --paths-from-stdin.\")\n        if args.progress and getattr(args, \"output_list\", False) and not args.log_json:\n            parser.error(\"Options --progress and --list do not play nicely together.\")\n        if func == self.do_create and not args.paths:\n            if args.content_from_command or args.paths_from_command:\n                parser.error(\"No command given.\")\n            elif not args.paths_from_stdin and not args.pattern_roots:\n                parser.error(\"Need at least one PATH argument.\")\n        # we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing,\n        # e.g. due to options like --timestamp that override the current time.\n        # thus we have to initialize replace_placeholders here and process all args that need placeholder replacement.\n        if getattr(args, \"timestamp\", None):\n            replace_placeholders.override(\"now\", DatetimeWrapper(args.timestamp))\n            replace_placeholders.override(\"utcnow\", DatetimeWrapper(args.timestamp.astimezone(timezone.utc)))\n            args.location = args.location.with_timestamp(args.timestamp)\n        for name in \"name\", \"other_name\", \"newname\", \"comment\":\n            value = getattr(args, name, None)\n            if value is not None:\n                setattr(args, name, replace_placeholders(value))\n        for name in (\"match_archives\",):  # lists\n            value = getattr(args, name, None)\n            if value:\n                setattr(args, name, [replace_placeholders(elem) for elem in value])\n\n        args.func = func\n\n        return args\n\n    def get_func(self, args, parser):\n        if not getattr(args, \"subcommand\", None):\n            return functools.partial(self.do_maincommand_help, parser)\n\n        method_name = \"do_\" + args.subcommand.replace(\" \", \"_\").replace(\"-\", \"_\")\n        func = getattr(self, method_name, None)\n        if func is not None:\n            if method_name == \"do_help\":\n                return functools.partial(func, parser)\n            return func\n\n        # fallback to general help for e.g., \"borg key\"\n        return functools.partial(self.do_maincommand_help, parser)\n\n    def prerun_checks(self, logger, is_serve):\n        if (\n            not is_serve\n            and is_msystem\n            and (\"MSYS2_ARG_CONV_EXCL\" not in os.environ or \"MSYS2_ENV_CONV_EXCL\" not in os.environ)\n        ):\n            logger.warning(\n                \"MSYS2 path translation is active. This can cause POSIX paths to be mangled into \"\n                \"Windows paths in archives. Consider setting MSYS2_ARG_CONV_EXCL='*' and \"\n                \"MSYS2_ENV_CONV_EXCL='*'. See https://www.msys2.org/docs/filesystem-paths/ for details.\"\n            )\n        selftest(logger)\n\n    def _setup_implied_logging(self, args):\n        \"\"\"Turn on INFO-level logging for arguments that imply they will produce output.\"\"\"\n        # map of option name to name of logger for that option\n        option_logger = {\n            \"show_version\": \"borg.output.show-version\",\n            \"show_rc\": \"borg.output.show-rc\",\n            \"stats\": \"borg.output.stats\",\n            \"progress\": \"borg.output.progress\",\n        }\n        for option, logger_name in option_logger.items():\n            option_set = args.get(option, False)\n            logging.getLogger(logger_name).setLevel(\"INFO\" if option_set else \"WARN\")\n\n        # special-case --list / --list-kept / --list-pruned as they all work on same logger\n        options = [args.get(name, False) for name in (\"output_list\", \"list_kept\", \"list_pruned\")]\n        logging.getLogger(\"borg.output.list\").setLevel(\"INFO\" if any(options) else \"WARN\")\n\n    def _setup_topic_debugging(self, args):\n        \"\"\"Turn on DEBUG level logging for specified --debug-topics.\"\"\"\n        for topic in args.debug_topics:\n            if \".\" not in topic:\n                topic = \"borg.debug.\" + topic\n            logger.debug(\"Enabling debug topic %s\", topic)\n            logging.getLogger(topic).setLevel(\"DEBUG\")\n\n    def run(self, args):\n        os.umask(args.umask)  # early, before opening files\n        self.lock_wait = args.lock_wait\n        func = args.func\n        # do not use loggers before this!\n        is_serve = func == self.do_serve\n        self.log_json = args.log_json and not is_serve\n        func_name = getattr(func, \"__name__\", \"none\")\n        setup_logging(level=args.log_level, is_serve=is_serve, log_json=self.log_json, func=func_name)\n        args.progress |= is_serve\n        self._setup_implied_logging(vars(args))\n        self._setup_topic_debugging(args)\n        if getattr(args, \"stats\", False) and getattr(args, \"dry_run\", False):\n            # the data needed for --stats is not computed when using --dry-run, so we can't do it.\n            # for ease of scripting, we just ignore --stats when given with --dry-run.\n            logger.warning(\"Ignoring --stats. It is not supported when using --dry-run.\")\n            args.stats = False\n        if args.show_version:\n            logging.getLogger(\"borg.output.show-version\").info(\"borgbackup version %s\" % __version__)\n        self.prerun_checks(logger, is_serve)\n        if not is_supported_msgpack():\n            logger.error(\"You do not have a supported version of the msgpack python package installed. Terminating.\")\n            logger.error(\"This should never happen as specific, supported versions are required by our pyproject.toml.\")\n            logger.error(\"Do not contact borgbackup support about this.\")\n            raise Error(\"unsupported msgpack version\")\n        if is_slow_msgpack():\n            logger.warning(PURE_PYTHON_MSGPACK_WARNING)\n        if args.debug_profile:\n            # Import only when needed - avoids a further increase in startup time\n            import cProfile\n            import marshal\n\n            logger.debug(\"Writing execution profile to %s\", args.debug_profile)\n            # Open the file early, before running the main program, to avoid\n            # a very late crash in case the specified path is invalid.\n            with open(args.debug_profile, \"wb\") as fd:\n                profiler = cProfile.Profile()\n                variables = dict(locals())\n                profiler.enable()\n                try:\n                    return get_ec(func(args))\n                finally:\n                    profiler.disable()\n                    profiler.snapshot_stats()\n                    if args.debug_profile.endswith(\".pyprof\"):\n                        marshal.dump(profiler.stats, fd)\n                    else:\n                        # We use msgpack here instead of the marshal module used by cProfile itself,\n                        # because the latter is insecure. Since these files may be shared over the\n                        # internet we don't want a format that is impossible to interpret outside\n                        # an insecure implementation.\n                        # See scripts/msgpack2marshal.py for a small script that turns a msgpack file\n                        # into a marshal file that can be read by e.g. pyprof2calltree.\n                        # For local use it's unnecessary hassle, though, that's why .pyprof makes\n                        # it compatible (see above).\n                        msgpack.pack(profiler.stats, fd, use_bin_type=True)\n        else:\n            rc = func(args)\n            assert rc is None\n            return get_ec(rc)\n\n\ndef sig_info_handler(sig_no, stack):  # pragma: no cover\n    \"\"\"Search the stack for information about the currently processed file and print it.\"\"\"\n    with signal_handler(sig_no, signal.SIG_IGN):\n        for frame in inspect.getouterframes(stack):\n            func, loc = frame[3], frame[0].f_locals\n            if func in (\"process_file\", \"_rec_walk\"):  # create op\n                path = loc[\"path\"]\n                try:\n                    pos = loc[\"fd\"].tell()\n                    total = loc[\"st\"].st_size\n                except Exception:\n                    pos, total = 0, 0\n                logger.info(f\"{path} {format_file_size(pos)}/{format_file_size(total)}\")\n                break\n            if func in (\"extract_item\",):  # extract op\n                path = loc[\"item\"].path\n                try:\n                    pos = loc[\"fd\"].tell()\n                except Exception:\n                    pos = 0\n                logger.info(f\"{path} {format_file_size(pos)}/???\")\n                break\n\n\ndef sig_trace_handler(sig_no, stack):  # pragma: no cover\n    print(\"\\nReceived SIGUSR2 at %s, dumping trace...\" % datetime.now().replace(microsecond=0), file=sys.stderr)\n    faulthandler.dump_traceback()\n\n\ndef format_tb(exc):\n    qualname = type(exc).__qualname__\n    remote = isinstance(exc, RemoteRepository.RPCError)\n    if remote:\n        prefix = \"Borg server: \"\n        trace_back = \"\\n\".join(prefix + line for line in exc.exception_full.splitlines())\n        sys_info = \"\\n\".join(prefix + line for line in exc.sysinfo.splitlines())\n    else:\n        trace_back = traceback.format_exc()\n        sys_info = sysinfo()\n    result = f\"\"\"\nError:\n\n{qualname}: {exc}\n\nIf reporting bugs, please include the following:\n\n{trace_back}\n{sys_info}\n\"\"\"\n    return result\n\n\ndef main():  # pragma: no cover\n    # Make sure stdout and stderr have errors='replace' to avoid unicode\n    # issues when print()-ing unicode file names\n    sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, \"replace\", line_buffering=True)\n    sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, \"replace\", line_buffering=True)\n\n    # If we receive SIGINT (ctrl-c), SIGTERM (kill) or SIGHUP (kill -HUP),\n    # catch them and raise a proper exception that can be handled for an\n    # orderly exit.\n    # SIGHUP is important especially for systemd systems, where logind\n    # sends it when a session exits, in addition to any traditional use.\n    # Output some info if we receive SIGUSR1 or SIGINFO (ctrl-t).\n\n    # Register fault handler for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL.\n    faulthandler.enable()\n    with (\n        signal_handler(\"SIGINT\", raising_signal_handler(KeyboardInterrupt)),\n        signal_handler(\"SIGHUP\", raising_signal_handler(SigHup)),\n        signal_handler(\"SIGTERM\", raising_signal_handler(SigTerm)),\n        signal_handler(\"SIGUSR1\", sig_info_handler),\n        signal_handler(\"SIGUSR2\", sig_trace_handler),\n        signal_handler(\"SIGINFO\", sig_info_handler),\n    ):\n        archiver = Archiver()\n        msg = msgid = tb = None\n        tb_log_level = logging.ERROR\n        try:\n            args = archiver.get_args(sys.argv, os.environ.get(\"SSH_ORIGINAL_COMMAND\"))\n        except Error as e:\n            # we might not have logging setup yet, so get out quickly\n            msg = e.get_message()\n            print(msg, file=sys.stderr)\n            if e.traceback:\n                tb = format_tb(e)\n                print(tb, file=sys.stderr)\n            sys.exit(e.exit_code)\n        except ArgumentTypeError as e:\n            # we might not have logging setup yet, so get out quickly\n            print(str(e), file=sys.stderr)\n            sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR)\n        except Exception:\n            msg = \"Local Exception\"\n            tb = f\"{traceback.format_exc()}\\n{sysinfo()}\"\n            # we might not have logging setup yet, so get out quickly\n            print(msg, file=sys.stderr)\n            print(tb, file=sys.stderr)\n            sys.exit(EXIT_ERROR)\n\n        if args.cockpit:\n            # Cockpit TUI operation\n            try:\n                from ..cockpit.app import BorgCockpitApp\n            except ImportError as err:\n                print(f\"ImportError: {err}\", file=sys.stderr)\n                print(\"The Borg Cockpit feature has some additional requirements.\", file=sys.stderr)\n                print(\"Please install them using: pip install 'borgbackup[cockpit]'\", file=sys.stderr)\n                sys.exit(EXIT_ERROR)\n\n            app = BorgCockpitApp()\n            app.borg_args = [arg for arg in sys.argv[1:] if arg != \"--cockpit\"]\n            app.run()\n            sys.exit(EXIT_SUCCESS)  # borg subprocess RC was already shown on the TUI\n\n        # normal borg CLI operation\n        try:\n            with sig_int:\n                exit_code = archiver.run(args)\n        except Error as e:\n            msg = e.get_message()\n            msgid = type(e).__qualname__\n            tb_log_level = logging.ERROR if e.traceback else logging.DEBUG\n            tb = format_tb(e)\n            exit_code = e.exit_code\n        except RemoteRepository.RPCError as e:\n            important = e.traceback\n            msg = e.exception_full if important else e.get_message()\n            msgid = e.exception_class\n            tb_log_level = logging.ERROR if important else logging.DEBUG\n            tb = format_tb(e)\n            exit_code = EXIT_ERROR\n        except Exception as e:\n            msg = \"Local Exception\"\n            msgid = \"Exception\"\n            tb_log_level = logging.ERROR\n            tb = format_tb(e)\n            exit_code = EXIT_ERROR\n        except KeyboardInterrupt as e:\n            msg = \"Keyboard interrupt\"\n            tb_log_level = logging.DEBUG\n            tb = format_tb(e)\n            exit_code = EXIT_SIGNAL_BASE + 2\n        except SigTerm as e:\n            msg = \"Received SIGTERM\"\n            msgid = \"Signal.SIGTERM\"\n            tb_log_level = logging.DEBUG\n            tb = format_tb(e)\n            exit_code = EXIT_SIGNAL_BASE + 15\n        except SigHup as e:\n            msg = \"Received SIGHUP.\"\n            msgid = \"Signal.SIGHUP\"\n            tb_log_level = logging.DEBUG\n            tb = format_tb(e)\n            exit_code = EXIT_SIGNAL_BASE + 1\n        if msg:\n            logger.error(msg, msgid=msgid)\n        if tb:\n            logger.log(tb_log_level, tb)\n        if args.show_rc:\n            from ..helpers import do_show_rc\n\n            do_show_rc(exit_code)\n        sys.exit(exit_code)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/borg/archiver/_common.py",
    "content": "import functools\nimport os\nimport textwrap\n\nimport borg\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..cache import Cache, assert_secure\nfrom ..helpers import Error\nfrom ..helpers import SortBySpec, location_validator, Location, relative_time_marker_validator\nfrom ..helpers import Highlander, octal_int\nfrom ..helpers.argparsing import SUPPRESS, PositiveInt\nfrom ..helpers.nanorst import rst_to_terminal\nfrom ..manifest import Manifest, AI_HUMAN_SORT_KEYS\nfrom ..patterns import PatternMatcher\nfrom ..legacyremote import LegacyRemoteRepository\nfrom ..remote import RemoteRepository\nfrom ..legacyrepository import LegacyRepository\nfrom ..repository import Repository\nfrom ..repoobj import RepoObj, RepoObj1\nfrom ..patterns import (\n    ArgparsePatternAction,\n    ArgparseExcludeFileAction,\n    ArgparsePatternFileAction,\n    parse_exclude_pattern,\n)\n\n\nfrom ..logger import create_logger\n\nlogger = create_logger(__name__)\n\n\ndef get_repository(location, *, create, exclusive, lock_wait, lock, args, v1_or_v2):\n    if location.proto in (\"ssh\", \"socket\"):\n        RemoteRepoCls = LegacyRemoteRepository if v1_or_v2 else RemoteRepository\n        repository = RemoteRepoCls(\n            location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock, args=args\n        )\n\n    elif (\n        location.proto in (\"sftp\", \"file\", \"http\", \"https\", \"rclone\", \"s3\", \"b2\") and not v1_or_v2\n    ):  # stuff directly supported by borgstore\n        repository = Repository(location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock)\n\n    else:\n        RepoCls = LegacyRepository if v1_or_v2 else Repository\n        repository = RepoCls(location.path, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock)\n    return repository\n\n\ndef compat_check(*, create, manifest, key, cache, compatibility, decorator_name):\n    if not create and (manifest or key or cache):\n        if compatibility is None:\n            raise AssertionError(f\"{decorator_name} decorator used without compatibility argument\")\n        if type(compatibility) is not tuple:\n            raise AssertionError(f\"{decorator_name} decorator compatibility argument must be of type tuple\")\n    else:\n        if compatibility is not None:\n            raise AssertionError(\n                f\"{decorator_name} called with compatibility argument, \" f\"but would not check {compatibility!r}\"\n            )\n        if create:\n            compatibility = Manifest.NO_OPERATION_CHECK\n    return compatibility\n\n\ndef with_repository(\n    create=False, lock=True, exclusive=False, manifest=True, cache=False, secure=True, compatibility=None\n):\n    \"\"\"\n    Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …)\n\n    If a parameter (where allowed) is a str the attribute named of args is used instead.\n    :param create: create repository\n    :param lock: lock repository\n    :param exclusive: (bool) lock repository exclusively (for writing)\n    :param manifest: load manifest and repo_objs (key), pass them as keyword arguments\n    :param cache: open cache, pass it as keyword argument (implies manifest)\n    :param secure: do assert_secure after loading manifest\n    :param compatibility: mandatory if not create and (manifest or cache), specifies mandatory\n           feature categories to check\n    \"\"\"\n    # Note: with_repository decorator does not have a \"key\" argument (yet?)\n    compatibility = compat_check(\n        create=create,\n        manifest=manifest,\n        key=manifest,\n        cache=cache,\n        compatibility=compatibility,\n        decorator_name=\"with_repository\",\n    )\n\n    # We may need to modify `lock` inside `wrapper`. Therefore we cannot use the\n    # `nonlocal` statement to access `lock` as modifications would also\n    # affect the scope outside of `wrapper`. Subsequent calls would\n    # only see the overwritten value of `lock`, not the original one.\n    # The solution is to define a place holder variable `_lock` to\n    # propagate the value into `wrapper`.\n    _lock = lock\n\n    def decorator(method):\n        @functools.wraps(method)\n        def wrapper(self, args, **kwargs):\n            location = getattr(args, \"location\")\n            if not location.valid:  # location always must be given\n                raise Error(\"missing repository, please use --repo or BORG_REPO env var!\")\n            assert isinstance(exclusive, bool)\n            lock = getattr(args, \"lock\", _lock)\n\n            repository = get_repository(\n                location,\n                create=create,\n                exclusive=exclusive,\n                lock_wait=self.lock_wait,\n                lock=lock,\n                args=args,\n                v1_or_v2=False,\n            )\n\n            with repository:\n                if repository.version not in (3,):\n                    raise Error(\n                        f\"This borg version only accepts version 3 repos for -r/--repo, \"\n                        f\"but not version {repository.version}. \"\n                        f\"You can use 'borg transfer' to copy archives from old to new repos.\"\n                    )\n                if manifest or cache:\n                    manifest_ = Manifest.load(repository, compatibility, other=False)\n                    kwargs[\"manifest\"] = manifest_\n                    if \"compression\" in args:\n                        manifest_.repo_objs.compressor = args.compression.compressor\n                    if secure:\n                        assert_secure(repository, manifest_)\n                if cache:\n                    with Cache(\n                        repository,\n                        manifest_,\n                        progress=getattr(args, \"progress\", False),\n                        cache_mode=getattr(args, \"files_cache_mode\", FILES_CACHE_MODE_DISABLED),\n                        start_backup=getattr(self, \"start_backup\", None),\n                        iec=getattr(args, \"iec\", False),\n                    ) as cache_:\n                        return method(self, args, repository=repository, cache=cache_, **kwargs)\n                else:\n                    return method(self, args, repository=repository, **kwargs)\n\n        return wrapper\n\n    return decorator\n\n\ndef with_other_repository(manifest=False, cache=False, compatibility=None):\n    \"\"\"\n    this is a simplified version of \"with_repository\", just for the \"other location\".\n\n    the repository at the \"other location\" is intended to get used as a **source** (== read operations).\n    \"\"\"\n\n    compatibility = compat_check(\n        create=False,\n        manifest=manifest,\n        key=manifest,\n        cache=cache,\n        compatibility=compatibility,\n        decorator_name=\"with_other_repository\",\n    )\n\n    def decorator(method):\n        @functools.wraps(method)\n        def wrapper(self, args, **kwargs):\n            location = getattr(args, \"other_location\")\n            if not location.valid:  # nothing to do\n                return method(self, args, **kwargs)\n\n            v1_or_v2 = getattr(args, \"v1_or_v2\", False)\n\n            repository = get_repository(\n                location,\n                create=False,\n                exclusive=True,\n                lock_wait=self.lock_wait,\n                lock=True,\n                args=args,\n                v1_or_v2=v1_or_v2,\n            )\n\n            with repository:\n                acceptable_versions = (1, 2) if v1_or_v2 else (3,)\n                if repository.version not in acceptable_versions:\n                    raise Error(\n                        f\"This borg version only accepts version {' or '.join(acceptable_versions)} \"\n                        f\"repos for --other-repo.\"\n                    )\n                kwargs[\"other_repository\"] = repository\n                if manifest or cache:\n                    manifest_ = Manifest.load(\n                        repository, compatibility, other=True, ro_cls=RepoObj if repository.version > 1 else RepoObj1\n                    )\n                    assert_secure(repository, manifest_)\n                    if manifest:\n                        kwargs[\"other_manifest\"] = manifest_\n                if cache:\n                    with Cache(\n                        repository,\n                        manifest_,\n                        progress=False,\n                        cache_mode=getattr(args, \"files_cache_mode\", FILES_CACHE_MODE_DISABLED),\n                        iec=getattr(args, \"iec\", False),\n                    ) as cache_:\n                        kwargs[\"other_cache\"] = cache_\n                        return method(self, args, **kwargs)\n                else:\n                    return method(self, args, **kwargs)\n\n        return wrapper\n\n    return decorator\n\n\ndef with_archive(method):\n    @functools.wraps(method)\n    def wrapper(self, args, repository, manifest, **kwargs):\n        archive_name = getattr(args, \"name\", None)\n        assert archive_name is not None\n        archive_info = manifest.archives.get_one([archive_name])\n        archive = Archive(\n            manifest,\n            archive_info.id,\n            numeric_ids=getattr(args, \"numeric_ids\", False),\n            noflags=getattr(args, \"noflags\", False),\n            noacls=getattr(args, \"noacls\", False),\n            noxattrs=getattr(args, \"noxattrs\", False),\n            cache=kwargs.get(\"cache\"),\n            log_json=args.log_json,\n            iec=args.iec,\n        )\n        return method(self, args, repository=repository, manifest=manifest, archive=archive, **kwargs)\n\n    return wrapper\n\n\n# You can use :ref:`xyz` in the following usage pages. However, for plain-text view,\n# e.g. through \"borg ... --help\", define a substitution for the reference here.\n# It will replace the entire :ref:`foo` verbatim.\nrst_plain_text_references = {\n    \"a_status_oddity\": '\"I am seeing ‘A’ (added) status for an unchanged file!?\"',\n    \"separate_compaction\": '\"Separate compaction\"',\n    \"list_item_flags\": '\"Item flags\"',\n    \"borg_patterns\": '\"borg help patterns\"',\n    \"borg_placeholders\": '\"borg help placeholders\"',\n    \"key_files\": \"Internals -> Data structures and file formats -> Key files\",\n    \"borg_key_export\": \"borg key export --help\",\n}\n\n\ndef process_epilog(epilog):\n    epilog = textwrap.dedent(epilog).splitlines()\n    try:\n        mode = borg.doc_mode\n    except AttributeError:\n        mode = \"command-line\"\n    if mode in (\"command-line\", \"build_usage\"):\n        epilog = [line for line in epilog if not line.startswith(\".. man\")]\n    epilog = \"\\n\".join(epilog)\n    if mode == \"command-line\":\n        epilog = rst_to_terminal(epilog, rst_plain_text_references)\n    return epilog\n\n\ndef define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False):\n    add_option(\"--pattern-roots-internal\", dest=\"pattern_roots\", action=\"append\", default=[], help=SUPPRESS)\n    add_option(\"--patterns-internal\", dest=\"patterns\", action=\"append\", default=[], help=SUPPRESS)\n    add_option(\n        \"-e\",\n        \"--exclude\",\n        metavar=\"PATTERN\",\n        dest=\"patterns\",\n        type=parse_exclude_pattern,\n        action=\"append\",\n        default=[],\n        help=\"exclude paths matching PATTERN\",\n    )\n    add_option(\n        \"--exclude-from\",\n        metavar=\"EXCLUDEFILE\",\n        action=ArgparseExcludeFileAction,\n        help=\"read exclude patterns from EXCLUDEFILE, one per line\",\n    )\n    add_option(\n        \"--pattern\", metavar=\"PATTERN\", action=ArgparsePatternAction, help=\"include/exclude paths matching PATTERN\"\n    )\n    add_option(\n        \"--patterns-from\",\n        metavar=\"PATTERNFILE\",\n        action=ArgparsePatternFileAction,\n        help=\"read include/exclude patterns from PATTERNFILE, one per line\",\n    )\n\n    if tag_files:\n        add_option(\n            \"--exclude-caches\",\n            dest=\"exclude_caches\",\n            action=\"store_true\",\n            help=\"exclude directories that contain a CACHEDIR.TAG file \" \"(https://www.bford.info/cachedir/spec.html)\",\n        )\n        add_option(\n            \"--exclude-if-present\",\n            metavar=\"NAME\",\n            dest=\"exclude_if_present\",\n            action=\"append\",\n            type=str,\n            help=\"exclude directories that are tagged by containing a filesystem object with the given NAME\",\n        )\n        add_option(\n            \"--keep-exclude-tags\",\n            dest=\"keep_exclude_tags\",\n            action=\"store_true\",\n            help=\"if tag objects are specified with ``--exclude-if-present``, \"\n            \"do not omit the tag objects themselves from the backup archive\",\n        )\n\n    if strip_components:\n        add_option(\n            \"--strip-components\",\n            metavar=\"NUMBER\",\n            dest=\"strip_components\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"Remove the specified number of leading path elements. \"\n            \"Paths with fewer elements will be silently skipped.\",\n        )\n\n\ndef define_exclusion_group(subparser, **kwargs):\n    exclude_group = subparser.add_argument_group(\"Include/Exclude options\")\n    define_exclude_and_patterns(exclude_group.add_argument, **kwargs)\n    return exclude_group\n\n\ndef define_archive_filters_group(\n    subparser, *, sort_by=True, first_last=True, oldest_newest=True, older_newer=True, deleted=False\n):\n    filters_group = subparser.add_argument_group(\n        \"Archive filters\", \"Archive filters can be applied to repository targets.\"\n    )\n    group = filters_group.add_mutually_exclusive_group()\n    group.add_argument(\n        \"-a\",\n        \"--match-archives\",\n        metavar=\"PATTERN\",\n        dest=\"match_archives\",\n        action=\"append\",\n        help='only consider archives matching all patterns. See \"borg help match-archives\".',\n    )\n\n    if sort_by:\n        sort_by_default = \"timestamp\"\n        filters_group.add_argument(\n            \"--sort-by\",\n            metavar=\"KEYS\",\n            dest=\"sort_by\",\n            type=SortBySpec,\n            default=sort_by_default,\n            action=Highlander,\n            help=\"Comma-separated list of sorting keys; valid keys are: {}; default is: {}\".format(\n                \", \".join(AI_HUMAN_SORT_KEYS), sort_by_default\n            ),\n        )\n\n    if first_last:\n        group = filters_group.add_mutually_exclusive_group()\n        group.add_argument(\n            \"--first\",\n            metavar=\"N\",\n            dest=\"first\",\n            type=PositiveInt,\n            action=Highlander,\n            help=\"consider the first N archives after other filters are applied\",\n        )\n        group.add_argument(\n            \"--last\",\n            metavar=\"N\",\n            dest=\"last\",\n            type=PositiveInt,\n            action=Highlander,\n            help=\"consider the last N archives after other filters are applied\",\n        )\n\n    if oldest_newest:\n        group = filters_group.add_mutually_exclusive_group()\n        group.add_argument(\n            \"--oldest\",\n            metavar=\"TIMESPAN\",\n            dest=\"oldest\",\n            type=relative_time_marker_validator,\n            action=Highlander,\n            help=\"consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g., 7d or 12m.\",\n        )\n        group.add_argument(\n            \"--newest\",\n            metavar=\"TIMESPAN\",\n            dest=\"newest\",\n            type=relative_time_marker_validator,\n            action=Highlander,\n            help=\"consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g., 7d or 12m.\",\n        )\n\n    if older_newer:\n        group = filters_group.add_mutually_exclusive_group()\n        group.add_argument(\n            \"--older\",\n            metavar=\"TIMESPAN\",\n            dest=\"older\",\n            type=relative_time_marker_validator,\n            action=Highlander,\n            help=\"consider archives older than (now - TIMESPAN), e.g., 7d or 12m.\",\n        )\n        group.add_argument(\n            \"--newer\",\n            metavar=\"TIMESPAN\",\n            dest=\"newer\",\n            type=relative_time_marker_validator,\n            action=Highlander,\n            help=\"consider archives newer than (now - TIMESPAN), e.g., 7d or 12m.\",\n        )\n\n    if deleted:\n        filters_group.add_argument(\n            \"--deleted\", dest=\"deleted\", action=\"store_true\", help=\"consider only soft-deleted archives.\"\n        )\n\n    return filters_group\n\n\ndef define_common_options(add_common_option):\n    add_common_option(\"-h\", \"--help\", action=\"help\", help=\"show this help message and exit\")\n    add_common_option(\n        \"--critical\",\n        dest=\"log_level\",\n        action=\"store_const\",\n        const=\"critical\",\n        default=\"warning\",\n        help=\"work on log level CRITICAL\",\n    )\n    add_common_option(\n        \"--error\",\n        dest=\"log_level\",\n        action=\"store_const\",\n        const=\"error\",\n        default=\"warning\",\n        help=\"work on log level ERROR\",\n    )\n    add_common_option(\n        \"--warning\",\n        dest=\"log_level\",\n        action=\"store_const\",\n        const=\"warning\",\n        default=\"warning\",\n        help=\"work on log level WARNING (default)\",\n    )\n    add_common_option(\n        \"--info\",\n        \"-v\",\n        \"--verbose\",\n        dest=\"log_level\",\n        action=\"store_const\",\n        const=\"info\",\n        default=\"warning\",\n        help=\"work on log level INFO\",\n    )\n    add_common_option(\n        \"--debug\",\n        dest=\"log_level\",\n        action=\"store_const\",\n        const=\"debug\",\n        default=\"warning\",\n        help=\"enable debug output, work on log level DEBUG\",\n    )\n    add_common_option(\n        \"--debug-topic\",\n        metavar=\"TOPIC\",\n        dest=\"debug_topics\",\n        action=\"append\",\n        default=[],\n        help=\"enable TOPIC debugging (can be specified multiple times). \"\n        \"The logger path is borg.debug.<TOPIC> if TOPIC is not fully qualified.\",\n    )\n    add_common_option(\"-p\", \"--progress\", dest=\"progress\", action=\"store_true\", help=\"show progress information\")\n    add_common_option(\"--iec\", dest=\"iec\", action=\"store_true\", help=\"format using IEC units (1KiB = 1024B)\")\n    add_common_option(\n        \"--log-json\",\n        dest=\"log_json\",\n        action=\"store_true\",\n        help=\"Output one JSON object per log line instead of formatted text.\",\n    )\n    add_common_option(\n        \"--lock-wait\",\n        metavar=\"SECONDS\",\n        dest=\"lock_wait\",\n        type=int,\n        default=int(os.environ.get(\"BORG_LOCK_WAIT\", 10)),\n        action=Highlander,\n        help=\"wait at most SECONDS for acquiring a repository/cache lock (default: %(default)d).\",\n    )\n    add_common_option(\"--show-version\", dest=\"show_version\", action=\"store_true\", help=\"show/log the borg version\")\n    add_common_option(\"--show-rc\", dest=\"show_rc\", action=\"store_true\", help=\"show/log the return code (rc)\")\n    add_common_option(\n        \"--umask\",\n        metavar=\"M\",\n        dest=\"umask\",\n        type=octal_int,\n        default=UMASK_DEFAULT,\n        action=Highlander,\n        help=\"set umask to M (local only, default: %(default)04o)\",\n    )\n    add_common_option(\n        \"--remote-path\",\n        metavar=\"PATH\",\n        dest=\"remote_path\",\n        action=Highlander,\n        help='use PATH as borg executable on the remote (default: \"borg\")',\n    )\n    add_common_option(\n        \"--upload-ratelimit\",\n        metavar=\"RATE\",\n        dest=\"upload_ratelimit\",\n        type=int,\n        action=Highlander,\n        help=\"set network upload rate limit in kiByte/s (default: 0=unlimited)\",\n    )\n    add_common_option(\n        \"--upload-buffer\",\n        metavar=\"UPLOAD_BUFFER\",\n        dest=\"upload_buffer\",\n        type=int,\n        action=Highlander,\n        help=\"set network upload buffer size in MiB. (default: 0=no buffer)\",\n    )\n    add_common_option(\n        \"--debug-profile\",\n        metavar=\"FILE\",\n        dest=\"debug_profile\",\n        default=None,\n        action=Highlander,\n        help=\"Write execution profile in Borg format into FILE. For local use a Python-\"\n        'compatible file can be generated by suffixing FILE with \".pyprof\".',\n    )\n    add_common_option(\n        \"--rsh\",\n        metavar=\"RSH\",\n        dest=\"rsh\",\n        action=Highlander,\n        help=\"Use this command to connect to the 'borg serve' process (default: 'ssh')\",\n    )\n    add_common_option(\n        \"--socket\",\n        metavar=\"PATH\",\n        dest=\"use_socket\",\n        default=False,\n        const=True,\n        nargs=\"?\",\n        action=Highlander,\n        help=\"Use UNIX DOMAIN (IPC) socket at PATH for client/server communication with socket: protocol.\",\n    )\n    add_common_option(\n        \"-r\",\n        \"--repo\",\n        metavar=\"REPO\",\n        dest=\"location\",\n        type=location_validator(other=False),\n        default=Location(other=False),\n        action=Highlander,\n        help=\"repository to use\",\n    )\n\n\ndef build_matcher(inclexcl_patterns, include_paths, pattern_roots=()):\n    matcher = PatternMatcher()\n    matcher.add_inclexcl(inclexcl_patterns)\n    paths = list(pattern_roots) + list(include_paths)\n    matcher.add_includepaths(paths)\n    return matcher\n\n\ndef build_filter(matcher, strip_components):\n    if strip_components:\n\n        def item_filter(item):\n            matched = matcher.match(item.path) and len(item.path.split(os.sep)) > strip_components\n            return matched\n\n    else:\n\n        def item_filter(item):\n            matched = matcher.match(item.path)\n            return matched\n\n    return item_filter\n"
  },
  {
    "path": "src/borg/archiver/analyze_cmd.py",
    "content": "from collections import defaultdict\nimport os\n\nfrom ._common import with_repository, define_archive_filters_group\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import bin_to_hex, Error\nfrom ..helpers import ProgressIndicatorPercent\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\nfrom ..remote import RemoteRepository\nfrom ..repository import Repository\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass ArchiveAnalyzer:\n    def __init__(self, args, repository, manifest):\n        self.args = args\n        self.repository = repository\n        assert isinstance(repository, (Repository, RemoteRepository))\n        self.manifest = manifest\n        self.difference_by_path = defaultdict(int)  # directory path -> count of chunks changed\n\n    def analyze(self):\n        logger.info(\"Starting archives analysis...\")\n        self.analyze_archives()\n        self.report()\n        logger.info(\"Finished archives analysis.\")\n\n    def analyze_archives(self) -> None:\n        \"\"\"Analyze all archives matching the given selection criteria.\"\"\"\n        archive_infos = self.manifest.archives.list_considering(self.args)\n        num_archives = len(archive_infos)\n        if num_archives < 2:\n            raise Error(\"Need at least 2 archives to analyze.\")\n\n        pi = ProgressIndicatorPercent(\n            total=num_archives, msg=\"Analyzing archives %3.1f%%\", step=0.1, msgid=\"analyze.analyze_archives\"\n        )\n        i = 0\n        info = archive_infos[i]\n        pi.show(i)\n        logger.info(\n            f\"Analyzing archive {info.name} {info.ts.astimezone()} {bin_to_hex(info.id)} ({i + 1}/{num_archives})\"\n        )\n        base = self.analyze_archive(info.id)\n        for i, info in enumerate(archive_infos[1:]):\n            pi.show(i + 1)\n            logger.info(\n                f\"Analyzing archive {info.name} {info.ts.astimezone()} {bin_to_hex(info.id)} ({i + 2}/{num_archives})\"\n            )\n            new = self.analyze_archive(info.id)\n            self.analyze_change(base, new)\n            base = new\n        pi.finish()\n\n    def analyze_archive(self, id):\n        \"\"\"compute the set of chunks for each directory in this archive\"\"\"\n        archive = Archive(self.manifest, id)\n        chunks_by_path = defaultdict(dict)  # collect all chunk IDs generated from files in this directory path\n        for item in archive.iter_items():\n            if \"chunks\" in item:\n                item_chunks = dict(item.chunks)  # chunk id -> plaintext size\n                directory_path = os.path.dirname(item.path)\n                chunks_by_path[directory_path] |= item_chunks\n        return chunks_by_path\n\n    def analyze_change(self, base, new):\n        \"\"\"for each directory path, sum up the changed (removed or added) chunks' sizes between base and new.\"\"\"\n\n        def analyze_path_change(path):\n            base_chunks = base[path]\n            new_chunks = new[path]\n            # add up added chunks' sizes\n            for id in new_chunks.keys() - base_chunks.keys():\n                self.difference_by_path[directory_path] += new_chunks[id]\n            # add up removed chunks' sizes\n            for id in base_chunks.keys() - new_chunks.keys():\n                self.difference_by_path[directory_path] += base_chunks[id]\n\n        for directory_path in base:\n            analyze_path_change(directory_path)\n        for directory_path in new:\n            if directory_path not in base:\n                analyze_path_change(directory_path)\n\n    def report(self):\n        print()\n        print(\"chunks added or removed by directory path\")\n        print(\"=========================================\")\n        for directory_path in sorted(self.difference_by_path, key=lambda p: self.difference_by_path[p], reverse=True):\n            difference = self.difference_by_path[directory_path]\n            print(f\"{directory_path}: {difference}\")\n\n\nclass AnalyzeMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    def do_analyze(self, args, repository, manifest):\n        \"\"\"Analyzes archives.\"\"\"\n        ArchiveAnalyzer(args, repository, manifest).analyze()\n\n    def build_parser_analyze(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        analyze_epilog = process_epilog(\n            \"\"\"\n            Analyze archives to find \"hot spots\".\n\n            ``borg analyze`` relies on the usual archive matching options to select the\n            archives that should be considered for analysis (e.g. ``-a series_name``).\n            Then it iterates over all matching archives, over all contained files, and\n            collects information about chunks stored in all directories it encounters.\n\n            It considers chunk IDs and their plaintext sizes (we do not have the compressed\n            size in the repository easily available) and adds up the sizes of added and removed\n            chunks per direct parent directory, and outputs a list of \"directory: size\".\n\n            You can use that list to find directories with a lot of \"activity\" — maybe\n            some of these are temporary or cache directories you forgot to exclude.\n\n            To avoid including these unwanted directories in your backups, you can carefully\n            exclude them in ``borg create`` (for future backups) or use ``borg recreate``\n            to recreate existing archives without them.\n            \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_analyze.__doc__, epilog=analyze_epilog)\n        subparsers.add_subcommand(\"analyze\", subparser, help=\"analyze archives\")\n        define_archive_filters_group(subparser)\n"
  },
  {
    "path": "src/borg/archiver/benchmark_cmd.py",
    "content": "from contextlib import contextmanager\nimport json\nimport logging\nimport os\nimport tempfile\nimport time\n\nfrom ..constants import *  # NOQA\nfrom ..crypto.key import FlexiKey\nfrom ..helpers import format_file_size, CompressionSpec\nfrom ..helpers import json_print\nfrom ..helpers import msgpack\nfrom ..helpers import get_reset_ec\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..item import Item\nfrom ..platform import SyncFile\n\n\nclass BenchmarkMixIn:\n    def do_benchmark_crud(self, args):\n        \"\"\"Benchmark Create, Read, Update, Delete for archives.\"\"\"\n\n        def parse_args(args, cmd):\n            # we need to inherit some essential options from the \"borg benchmark crud\" invocation\n            if args.rsh is not None:\n                cmd[1:1] = [\"--rsh\", args.rsh]\n            if args.remote_path is not None:\n                cmd[1:1] = [\"--remote-path\", args.remote_path]\n            return self.parse_args(cmd)\n\n        def measurement_run(repo, path):\n            # Suppress \"Done. Run borg compact...\" warnings from internal do_delete() calls —\n            # they clutter benchmark output and are irrelevant here (repo is temporary).\n            archiver_logger = logging.getLogger(\"borg.archiver\")\n            original_level = archiver_logger.level\n            archiver_logger.setLevel(logging.ERROR)\n            try:\n                compression = \"--compression=none\"\n                # measure create perf (without files cache to always have it chunking)\n                t_start = time.monotonic()\n                rc = get_reset_ec(\n                    self.do_create(\n                        parse_args(\n                            args,\n                            [\n                                f\"--repo={repo}\",\n                                \"create\",\n                                compression,\n                                \"--files-cache=disabled\",\n                                \"borg-benchmark-crud1\",\n                                path,\n                            ],\n                        )\n                    )\n                )\n                t_end = time.monotonic()\n                dt_create = t_end - t_start\n                assert rc == 0\n                # now build files cache\n                rc1 = get_reset_ec(\n                    self.do_create(\n                        parse_args(args, [f\"--repo={repo}\", \"create\", compression, \"borg-benchmark-crud2\", path])\n                    )\n                )\n                rc2 = get_reset_ec(\n                    self.do_delete(parse_args(args, [f\"--repo={repo}\", \"delete\", \"-a\", \"borg-benchmark-crud2\"]))\n                )\n                assert rc1 == rc2 == 0\n                # measure a no-change update (archive1 is still present)\n                t_start = time.monotonic()\n                rc1 = get_reset_ec(\n                    self.do_create(\n                        parse_args(args, [f\"--repo={repo}\", \"create\", compression, \"borg-benchmark-crud3\", path])\n                    )\n                )\n                t_end = time.monotonic()\n                dt_update = t_end - t_start\n                rc2 = get_reset_ec(\n                    self.do_delete(parse_args(args, [f\"--repo={repo}\", \"delete\", \"-a\", \"borg-benchmark-crud3\"]))\n                )\n                assert rc1 == rc2 == 0\n                # measure extraction (dry-run: without writing result to disk)\n                t_start = time.monotonic()\n                rc = get_reset_ec(\n                    self.do_extract(\n                        parse_args(args, [f\"--repo={repo}\", \"extract\", \"borg-benchmark-crud1\", \"--dry-run\"])\n                    )\n                )\n                t_end = time.monotonic()\n                dt_extract = t_end - t_start\n                assert rc == 0\n                # measure archive deletion (of LAST present archive with the data)\n                t_start = time.monotonic()\n                rc = get_reset_ec(\n                    self.do_delete(parse_args(args, [f\"--repo={repo}\", \"delete\", \"-a\", \"borg-benchmark-crud1\"]))\n                )\n                t_end = time.monotonic()\n                dt_delete = t_end - t_start\n                assert rc == 0\n                return dt_create, dt_update, dt_extract, dt_delete\n            finally:\n                archiver_logger.setLevel(original_level)\n\n        @contextmanager\n        def test_files(path, count, size, random):\n            with tempfile.TemporaryDirectory(prefix=\"borg-test-data-\", dir=path) as path:\n                z_buff = None if random else memoryview(zeros)[:size] if size <= len(zeros) else b\"\\0\" * size\n                for i in range(count):\n                    fname = os.path.join(path, \"file_%d\" % i)\n                    data = z_buff if not random else os.urandom(size)\n                    with SyncFile(fname, binary=True) as fd:  # used for posix_fadvise's sake\n                        fd.write(data)\n                yield path\n\n        if \"_BORG_BENCHMARK_CRUD_TEST\" in os.environ:\n            tests = [(\"Z-TEST\", 1, 1, False), (\"R-TEST\", 1, 1, True)]\n        else:\n            tests = [\n                (\"Z-BIG\", 10, 100000000, False),\n                (\"R-BIG\", 10, 100000000, True),\n                (\"Z-MEDIUM\", 1000, 1000000, False),\n                (\"R-MEDIUM\", 1000, 1000000, True),\n                (\"Z-SMALL\", 10000, 10000, False),\n                (\"R-SMALL\", 10000, 10000, True),\n            ]\n\n        for msg, count, size, random in tests:\n            with test_files(args.path, count, size, random) as path:\n                dt_create, dt_update, dt_extract, dt_delete = measurement_run(args.location.canonical_path(), path)\n            total_size = count * size\n            if args.json_lines:\n                for cmd_letter, cmd_name, dt in [\n                    (\"C\", \"create1\", dt_create),\n                    (\"R\", \"extract\", dt_extract),\n                    (\"U\", \"create2\", dt_update),\n                    (\"D\", \"delete\", dt_delete),\n                ]:\n                    print(\n                        json.dumps(\n                            {\n                                \"id\": f\"{cmd_letter}-{msg}\",\n                                \"command\": cmd_name,\n                                \"sample\": msg,\n                                \"sample_count\": count,\n                                \"sample_size\": size,\n                                \"sample_random\": random,\n                                \"time\": dt,\n                                \"io\": int(total_size / dt),\n                            },\n                            sort_keys=True,\n                        )\n                    )\n            else:\n                total_size_MB = total_size / 1e06\n                file_size_formatted = format_file_size(size)\n                content = \"random\" if random else \"all-zero\"\n                fmt = \"%s-%-10s %9.2f MB/s (%d * %s %s files: %.2fs)\"\n                print(fmt % (\"C\", msg, total_size_MB / dt_create, count, file_size_formatted, content, dt_create))\n                print(fmt % (\"R\", msg, total_size_MB / dt_extract, count, file_size_formatted, content, dt_extract))\n                print(fmt % (\"U\", msg, total_size_MB / dt_update, count, file_size_formatted, content, dt_update))\n                print(fmt % (\"D\", msg, total_size_MB / dt_delete, count, file_size_formatted, content, dt_delete))\n\n    def do_benchmark_cpu(self, args):\n        \"\"\"Benchmark CPU-bound operations.\"\"\"\n        from timeit import timeit\n\n        result = {} if args.json else None\n\n        is_test = \"_BORG_BENCHMARK_CPU_TEST\" in os.environ\n        # Use minimal iterations and data size in test mode to keep CI fast.\n        number_default = 1 if is_test else 100\n        number_compression = 1 if is_test else 10\n        number_kdf = 1 if is_test else 5\n        data_size = 100 * 1000 if is_test else 10 * 1000 * 1000\n\n        random_10M = os.urandom(data_size)\n        key_256 = os.urandom(32)\n        key_128 = os.urandom(16)\n        key_96 = os.urandom(12)\n\n        import io\n        from ..chunkers import get_chunker  # noqa\n\n        if not args.json:\n            print(\"Chunkers =======================================================\")\n        else:\n            result[\"chunkers\"] = []\n        size = 1000000000\n\n        def chunkit(ch):\n            with io.BytesIO(random_10M) as data_file:\n                for _ in ch.chunkify(fd=data_file):\n                    pass\n\n        for spec, setup, func, vars in [\n            (\n                \"buzhash,19,23,21,4095\",\n                \"ch = get_chunker('buzhash', 19, 23, 21, 4095, sparse=False)\",\n                \"chunkit(ch)\",\n                locals(),\n            ),\n            # note: the buzhash64 chunker creation is rather slow, so we must keep it in setup\n            (\n                \"buzhash64,19,23,21,4095\",\n                \"ch = get_chunker('buzhash64', 19, 23, 21, 4095, sparse=False)\",\n                \"chunkit(ch)\",\n                locals(),\n            ),\n            (\"fixed,1048576\", \"ch = get_chunker('fixed', 1048576, sparse=False)\", \"chunkit(ch)\", locals()),\n        ]:\n            dt = timeit(func, setup, number=number_default, globals=vars)\n            if args.json:\n                algo, _, algo_params = spec.partition(\",\")\n                result[\"chunkers\"].append({\"algo\": algo, \"algo_params\": algo_params, \"size\": size, \"time\": dt})\n            else:\n                print(f\"{spec:<24} {format_file_size(size):<10} {dt:.3f}s\")\n\n        from xxhash import xxh64\n        from ..checksums import crc32\n\n        if not args.json:\n            print(\"Non-cryptographic checksums / hashes ===========================\")\n        else:\n            result[\"checksums\"] = []\n        size = 1000000000\n        tests = [(\"xxh64\", lambda: xxh64(random_10M).digest()), (\"crc32 (zlib)\", lambda: crc32(random_10M))]\n        for spec, func in tests:\n            dt = timeit(func, number=number_default)\n            if args.json:\n                result[\"checksums\"].append({\"algo\": spec, \"size\": size, \"time\": dt})\n            else:\n                print(f\"{spec:<24} {format_file_size(size):<10} {dt:.3f}s\")\n\n        from ..crypto.low_level import hmac_sha256, blake2b_256\n\n        if not args.json:\n            print(\"Cryptographic hashes / MACs ====================================\")\n        else:\n            result[\"hashes\"] = []\n        size = 1000000000\n        for spec, func in [\n            (\"hmac-sha256\", lambda: hmac_sha256(key_256, random_10M)),\n            (\"blake2b-256\", lambda: blake2b_256(key_256, random_10M)),\n        ]:\n            dt = timeit(func, number=number_default)\n            if args.json:\n                result[\"hashes\"].append({\"algo\": spec, \"size\": size, \"time\": dt})\n            else:\n                print(f\"{spec:<24} {format_file_size(size):<10} {dt:.3f}s\")\n\n        from ..crypto.low_level import AES256_CTR_BLAKE2b, AES256_CTR_HMAC_SHA256\n        from ..crypto.low_level import AES256_OCB, CHACHA20_POLY1305\n\n        if not args.json:\n            print(\"Encryption =====================================================\")\n        else:\n            result[\"encryption\"] = []\n        size = 1000000000\n\n        tests = [\n            (\n                \"aes-256-ctr-hmac-sha256\",\n                lambda: AES256_CTR_HMAC_SHA256(key_256, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(\n                    random_10M, header=b\"X\"\n                ),\n            ),\n            (\n                \"aes-256-ctr-blake2b\",\n                lambda: AES256_CTR_BLAKE2b(key_256 * 4, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(\n                    random_10M, header=b\"X\"\n                ),\n            ),\n            (\n                \"aes-256-ocb\",\n                lambda: AES256_OCB(key_256, iv=key_96, header_len=1, aad_offset=1).encrypt(random_10M, header=b\"X\"),\n            ),\n            (\n                \"chacha20-poly1305\",\n                lambda: CHACHA20_POLY1305(key_256, iv=key_96, header_len=1, aad_offset=1).encrypt(\n                    random_10M, header=b\"X\"\n                ),\n            ),\n        ]\n        for spec, func in tests:\n            dt = timeit(func, number=number_default)\n            if args.json:\n                result[\"encryption\"].append({\"algo\": spec, \"size\": size, \"time\": dt})\n            else:\n                print(f\"{spec:<24} {format_file_size(size):<10} {dt:.3f}s\")\n\n        if not args.json:\n            print(\"KDFs (slow is GOOD, use argon2!) ===============================\")\n        else:\n            result[\"kdf\"] = []\n        for spec, func in [\n            (\"pbkdf2\", lambda: FlexiKey.pbkdf2(\"mypassphrase\", b\"salt\" * 8, PBKDF2_ITERATIONS, 32)),\n            (\"argon2\", lambda: FlexiKey.argon2(\"mypassphrase\", 64, b\"S\" * ARGON2_SALT_BYTES, **ARGON2_ARGS)),\n        ]:\n            dt = timeit(func, number=number_kdf)\n            if args.json:\n                result[\"kdf\"].append({\"algo\": spec, \"count\": number_kdf, \"time\": dt})\n            else:\n                print(f\"{spec:<24} {number_kdf:<10} {dt:.3f}s\")\n\n        if not args.json:\n            print(\"Compression ====================================================\")\n        else:\n            result[\"compression\"] = []\n        for spec in [\n            \"lz4\",\n            \"zstd,1\",\n            \"zstd,3\",\n            \"zstd,5\",\n            \"zstd,10\",\n            \"zstd,16\",\n            \"zstd,22\",\n            \"zlib,0\",\n            \"zlib,6\",\n            \"zlib,9\",\n            \"lzma,0\",\n            \"lzma,6\",\n            \"lzma,9\",\n        ]:\n            compressor = CompressionSpec(spec).compressor\n            size = 100000000\n            dt = timeit(lambda: compressor.compress({}, random_10M), number=number_compression)\n            if args.json:\n                algo, _, algo_params = spec.partition(\",\")\n                result[\"compression\"].append({\"algo\": algo, \"algo_params\": algo_params, \"size\": size, \"time\": dt})\n            else:\n                print(f\"{spec:<12} {format_file_size(size):<10} {dt:.3f}s\")\n\n        if not args.json:\n            print(\"msgpack ========================================================\")\n        else:\n            result[\"msgpack\"] = []\n        item = Item(path=\"foo/bar/baz\", mode=660, mtime=1234567)\n        items = [item.as_dict()] * 1000\n        size = \"100k Items\"\n        spec = \"msgpack\"\n        dt = timeit(lambda: msgpack.packb(items), number=number_default)\n        if args.json:\n            result[\"msgpack\"].append({\"algo\": spec, \"count\": 100000, \"time\": dt})\n        else:\n            print(f\"{spec:<12} {size:<10} {dt:.3f}s\")\n\n        if args.json:\n            json_print(result)\n\n    def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        benchmark_epilog = process_epilog(\"These commands do various benchmarks.\")\n\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=\"benchmark command\", epilog=benchmark_epilog\n        )\n        subparsers.add_subcommand(\"benchmark\", subparser, help=\"benchmark command\")\n\n        benchmark_parsers = subparser.add_subcommands(required=False, title=\"required arguments\", metavar=\"<command>\")\n\n        bench_crud_epilog = process_epilog(\n            \"\"\"\n        This command benchmarks borg CRUD (create, read, update, delete) operations.\n\n        It creates input data below the given PATH and backs up this data into the given REPO.\n        The REPO must already exist (it could be a fresh empty repo or an existing repo, the\n        command will create / read / update / delete some archives named borg-benchmark-crud\\\\* there.\n\n        Make sure you have free space there; you will need about 1 GB each (+ overhead).\n\n        If your repository is encrypted and borg needs a passphrase to unlock the key, use::\n\n            BORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH\n\n        Measurements are done with different input file sizes and counts.\n        The file contents are very artificial (either all zero or all random),\n        thus the measurement results do not necessarily reflect performance with real data.\n        Also, due to the kind of content used, no compression is used in these benchmarks.\n\n        C- == borg create (1st archive creation, no compression, do not use files cache)\n              C-Z- == all-zero files. full dedup, this is primarily measuring reader/chunker/hasher.\n              C-R- == random files. no dedup, measuring throughput through all processing stages.\n\n        R- == borg extract (extract archive, dry-run, do everything, but do not write files to disk)\n              R-Z- == all zero files. Measuring heavily duplicated files.\n              R-R- == random files. No duplication here, measuring throughput through all processing\n              stages, except writing to disk.\n\n        U- == borg create (2nd archive creation of unchanged input files, measure files cache speed)\n              The throughput value is kind of virtual here, it does not actually read the file.\n              U-Z- == needs to check the 2 all-zero chunks' existence in the repo.\n              U-R- == needs to check existence of a lot of different chunks in the repo.\n\n        D- == borg delete archive (delete last remaining archive, measure deletion + compaction)\n              D-Z- == few chunks to delete / few segments to compact/remove.\n              D-R- == many chunks to delete / many segments to compact/remove.\n\n        Please note that there might be quite some variance in these measurements.\n        Try multiple measurements and having a otherwise idle machine (and network, if you use it).\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_benchmark_crud.__doc__, epilog=bench_crud_epilog\n        )\n        benchmark_parsers.add_subcommand(\n            \"crud\", subparser, help=\"benchmarks Borg CRUD (create, extract, update, delete).\"\n        )\n\n        subparser.add_argument(\"path\", metavar=\"PATH\", help=\"path where to create benchmark input data\")\n        subparser.add_argument(\"--json-lines\", action=\"store_true\", help=\"Format output as JSON Lines.\")\n\n        bench_cpu_epilog = process_epilog(\n            \"\"\"\n        This command benchmarks miscellaneous CPU-bound Borg operations.\n\n        It creates input data in memory, runs the operation and then displays throughput.\n        To reduce outside influence on the timings, please make sure to run this with:\n\n        - an otherwise as idle as possible machine\n        - enough free memory so there will be no slow down due to paging activity\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_benchmark_cpu.__doc__, epilog=bench_cpu_epilog\n        )\n        benchmark_parsers.add_subcommand(\"cpu\", subparser, help=\"benchmarks Borg CPU-bound operations.\")\n        subparser.add_argument(\"--json\", action=\"store_true\", help=\"format output as JSON\")\n"
  },
  {
    "path": "src/borg/archiver/check_cmd.py",
    "content": "from ._common import with_repository, Highlander\nfrom ..archive import ArchiveChecker\nfrom ..constants import *  # NOQA\nfrom ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError\nfrom ..helpers import yes\nfrom ..helpers.argparsing import ArgumentParser\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass CheckMixIn:\n    @with_repository(exclusive=True, manifest=False)\n    def do_check(self, args, repository):\n        \"\"\"Checks repository consistency.\"\"\"\n        if args.repair:\n            msg = (\n                \"This is a potentially dangerous function.\\n\"\n                \"check --repair might lead to data loss (for kinds of corruption it is not\\n\"\n                \"capable of dealing with). BE VERY CAREFUL!\\n\"\n                \"\\n\"\n                \"Type 'YES' if you understand this and want to continue: \"\n            )\n            if not yes(\n                msg,\n                false_msg=\"Aborting.\",\n                invalid_msg=\"Invalid answer, aborting.\",\n                truish=(\"YES\",),\n                retry=False,\n                env_var_override=\"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\",\n            ):\n                raise CancelledByUser()\n        if args.repo_only and any((args.verify_data, args.first, args.last, args.match_archives)):\n            raise CommandError(\n                \"--repository-only contradicts --first, --last, -a / --match-archives and --verify-data arguments.\"\n            )\n        if args.repo_only and args.find_lost_archives:\n            raise CommandError(\"--repository-only contradicts the --find-lost-archives option.\")\n        if args.repair and args.max_duration:\n            raise CommandError(\"--repair does not allow --max-duration argument.\")\n        if args.max_duration and not args.repo_only:\n            # when doing a partial repo check, we can only check xxh64 hashes in repository files.\n            # archives check requires that a full repo check was done before and has built/cached a ChunkIndex.\n            # also, there is no max_duration support in the archives check code anyway.\n            raise CommandError(\"--repository-only is required for --max-duration support.\")\n        if not args.repo_only:\n            # if we need the key later for the archives check, ask NOW for the passphrase! #1931\n            archive_checker = ArchiveChecker()\n            try:\n                archive_checker.key = archive_checker.make_key(repository, manifest_only=True)\n            except IntegrityError:\n                pass  # will try to make key later again\n        if not args.archives_only:\n            if not repository.check(repair=args.repair, max_duration=args.max_duration):\n                set_ec(EXIT_WARNING)\n        if not args.repo_only and not archive_checker.check(\n            repository,\n            verify_data=args.verify_data,\n            repair=args.repair,\n            find_lost_archives=args.find_lost_archives,\n            match=args.match_archives,\n            sort_by=args.sort_by or \"timestamp\",\n            first=args.first,\n            last=args.last,\n            older=args.older,\n            newer=args.newer,\n            oldest=args.oldest,\n            newest=args.newest,\n        ):\n            set_ec(EXIT_WARNING)\n            return\n\n    def build_parser_check(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_archive_filters_group\n\n        check_epilog = process_epilog(\n            \"\"\"\n        The check command verifies the consistency of a repository and its archives.\n        It consists of two major steps:\n\n        1. Checking the consistency of the repository itself. This includes checking\n           the file magic headers, and both the metadata and data of all objects in\n           the repository. The read data is checked by size and hash. Bit rot and other\n           types of accidental damage can be detected this way. Running the repository\n           check can be split into multiple partial checks using ``--max-duration``.\n           When checking an ssh:// remote repository, please note that the checks run on\n           the server and do not cause significant network traffic.\n\n        2. Checking consistency and correctness of the archive metadata and optionally\n           archive data (requires ``--verify-data``). This includes ensuring that the\n           repository manifest exists, the archive metadata chunk is present, and that\n           all chunks referencing files (items) in the archive exist. This requires\n           reading archive and file metadata, but not data. To scan for archives whose\n           entries were lost from the archive directory, pass ``--find-lost-archives``.\n           It requires reading all data and is hence very time-consuming.\n           To additionally cryptographically verify the file (content) data integrity,\n           pass ``--verify-data``, which is even more time-consuming.\n\n           When checking archives of a remote repository, archive checks run on the client\n           machine because they require decrypting data and therefore the encryption key.\n\n        Both steps can also be run independently. Pass ``--repository-only`` to run the\n        repository checks only, or pass ``--archives-only`` to run the archive checks\n        only.\n\n        The ``--max-duration`` option can be used to split a long-running repository\n        check into multiple partial checks. After the given number of seconds, the check\n        is interrupted. The next partial check will continue where the previous one\n        stopped, until the full repository has been checked. Assuming a complete check\n        would take 7 hours, then running a daily check with ``--max-duration=3600``\n        (1 hour) would result in one full repository check per week. Doing a full\n        repository check aborts any previous partial check; the next partial check will\n        restart from the beginning. With partial repository checks you can run neither\n        archive checks, nor enable repair mode. Consequently, if you want to use\n        ``--max-duration`` you must also pass ``--repository-only``, and must not pass\n        ``--archives-only``, nor ``--repair``.\n\n        **Warning:** Please note that partial repository checks (i.e., running with\n        ``--max-duration``) can only perform non-cryptographic checksum checks on the\n        repository files. Enabling partial repository checks excludes archive checks\n        for the same reason. Therefore, partial checks may be useful only with very large\n        repositories where a full check would take too long.\n\n        The ``--verify-data`` option will perform a full integrity verification (as\n        opposed to checking just the xxh64) of data, which means reading the\n        data from the repository, decrypting and decompressing it. It is a complete\n        cryptographic verification and hence very time-consuming, but will detect any\n        accidental and malicious corruption. Tamper-resistance is only guaranteed for\n        encrypted repositories against attackers without access to the keys. You cannot\n        use ``--verify-data`` with ``--repository-only``.\n\n        The ``--find-lost-archives`` option will also scan the whole repository, but\n        tells Borg to search for lost archive metadata. If Borg encounters any archive\n        metadata that does not match an archive directory entry (including\n        soft-deleted archives), it means that an entry was lost.\n        Unless ``borg compact`` is called, these archives can be fully restored with\n        ``--repair``. Please note that ``--find-lost-archives`` must read a lot of\n        data from the repository and is thus very time-consuming. You cannot use\n        ``--find-lost-archives`` with ``--repository-only``.\n\n        About repair mode\n        +++++++++++++++++\n\n        The check command is a read-only task by default. If any corruption is found,\n        Borg will report the issue and proceed with checking. To actually repair the\n        issues found, pass ``--repair``.\n\n        .. note::\n\n            ``--repair`` is a **POTENTIALLY DANGEROUS FEATURE** and might lead to data\n            loss! This does not just include data that was previously lost anyway, but\n            might include more data for kinds of corruption it is not capable of\n            dealing with. **BE VERY CAREFUL!**\n\n        Pursuant to the previous warning it is also highly recommended to test the\n        reliability of the hardware running Borg with stress testing software. This\n        especially includes storage and memory testers. Unreliable hardware might lead\n        to additional data loss.\n\n        It is highly recommended to create a backup of your repository before running\n        in repair mode (i.e. running it with ``--repair``).\n\n        Repair mode will attempt to fix any corruptions found. Fixing corruptions does\n        not mean recovering lost data: Borg cannot magically restore data lost due to\n        e.g. a hardware failure. Repairing a repository means sacrificing some data\n        for the sake of the repository as a whole and the remaining data. Hence it is,\n        by definition, a potentially lossy task.\n\n        In practice, repair mode hooks into both the repository and archive checks:\n\n        1. When checking the repository's consistency, repair mode removes corrupted\n           objects from the repository after it did a 2nd try to read them correctly.\n\n        2. When checking the consistency and correctness of archives, repair mode might\n           remove whole archives from the manifest if their archive metadata chunk is\n           corrupt or lost. Borg will also report files that reference missing chunks.\n\n        If ``--repair --find-lost-archives`` is given, previously lost entries will\n        be recreated in the archive directory. This is only possible before\n        ``borg compact`` would remove the archives' data completely.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_check.__doc__, epilog=check_epilog)\n        subparsers.add_subcommand(\"check\", subparser, help=\"verify the repository\")\n        subparser.add_argument(\n            \"--repository-only\", dest=\"repo_only\", action=\"store_true\", help=\"only perform repository checks\"\n        )\n        subparser.add_argument(\n            \"--archives-only\", dest=\"archives_only\", action=\"store_true\", help=\"only perform archive checks\"\n        )\n        subparser.add_argument(\n            \"--verify-data\",\n            dest=\"verify_data\",\n            action=\"store_true\",\n            help=\"perform cryptographic archive data integrity verification (conflicts with ``--repository-only``)\",\n        )\n        subparser.add_argument(\n            \"--repair\", dest=\"repair\", action=\"store_true\", help=\"attempt to repair any inconsistencies found\"\n        )\n        subparser.add_argument(\n            \"--find-lost-archives\", dest=\"find_lost_archives\", action=\"store_true\", help=\"attempt to find lost archives\"\n        )\n        subparser.add_argument(\n            \"--max-duration\",\n            metavar=\"SECONDS\",\n            dest=\"max_duration\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"perform only a partial repository check for at most SECONDS seconds (default: unlimited)\",\n        )\n        define_archive_filters_group(subparser)\n"
  },
  {
    "path": "src/borg/archiver/compact_cmd.py",
    "content": "from pathlib import Path\n\nfrom ._common import with_repository\nfrom ..archive import Archive\nfrom ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo\nfrom ..cache import files_cache_name, discover_files_cache_names\nfrom ..helpers import get_cache_dir\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..constants import *  # NOQA\nfrom ..hashindex import ChunkIndex, ChunkIndexEntry\nfrom ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex\nfrom ..helpers import ProgressIndicatorPercent\nfrom ..manifest import Manifest\nfrom ..remote import RemoteRepository\nfrom ..repository import Repository, repo_lister\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass ArchiveGarbageCollector:\n    def __init__(self, repository, manifest, *, stats, iec):\n        self.repository = repository\n        assert isinstance(repository, (Repository, RemoteRepository))\n        self.manifest = manifest\n        self.chunks = None  # a ChunkIndex, here used for: id -> (is_used, stored_size)\n        self.total_files = None  # overall number of source files written to all archives in this repo\n        self.total_size = None  # overall size of source file content data written to all archives\n        self.archives_count = None  # number of archives\n        self.stats = stats  # compute repo space usage before/after - lists all repo objects, can be slow.\n        self.iec = iec  # formats statistics using IEC units (1KiB = 1024B)\n\n    @property\n    def repository_size(self):\n        if self.chunks is None or not self.stats:\n            return None\n        return sum(entry.size for id, entry in self.chunks.iteritems())  # sum of stored sizes\n\n    def garbage_collect(self):\n        \"\"\"Removes unused chunks from a repository.\"\"\"\n        logger.info(\"Starting compaction / garbage collection...\")\n        self.chunks = self.get_repository_chunks()\n        logger.info(\"Computing object IDs used by archives...\")\n        (self.missing_chunks, self.total_files, self.total_size, self.archives_count) = self.analyze_archives()\n        self.report_and_delete()\n        self.save_chunk_index()\n        self.cleanup_files_cache()\n        logger.info(\"Finished compaction / garbage collection...\")\n\n    def get_repository_chunks(self) -> ChunkIndex:\n        \"\"\"return a chunks index\"\"\"\n        if self.stats:  # slow method: build a fresh chunks index, with stored chunk sizes.\n            logger.info(\"Getting object IDs present in the repository...\")\n            chunks = ChunkIndex()\n            for id, stored_size in repo_lister(self.repository, limit=LIST_SCAN_LIMIT):\n                # we add this id to the chunks index (as unused chunk), because\n                # we do not know yet whether it is actually referenced from some archives.\n                # we \"abuse\" the size field here. usually there is the plaintext size,\n                # but we use it for the size of the stored object here.\n                chunks[id] = ChunkIndexEntry(flags=ChunkIndex.F_NONE, size=stored_size)\n        else:  # faster: rely on existing chunks index (with flags F_NONE and size 0).\n            logger.info(\"Getting object IDs from cached chunks index...\")\n            chunks = build_chunkindex_from_repo(self.repository, cache_immediately=True)\n        return chunks\n\n    def save_chunk_index(self):\n        # as we may have deleted some chunks, we must write a full updated chunkindex to the repo\n        # and also remove all older cached chunk indexes.\n        # write_chunkindex_to_repo now removes all flags and size infos.\n        # we need this, as we put the wrong size in there to support --stats computations.\n        write_chunkindex_to_repo_cache(\n            self.repository, self.chunks, incremental=False, clear=True, force_write=True, delete_other=True\n        )\n        self.chunks = None  # nothing there (cleared!)\n\n    def cleanup_files_cache(self):\n        \"\"\"\n        Clean up files cache files for archive series names that no longer exist in the repository.\n\n        Note: this only works perfectly if the files cache filename suffixes are automatically generated\n        and the user does not manually control them via more than one BORG_FILES_CACHE_SUFFIX env var value.\n        \"\"\"\n        logger.info(\"Cleaning up files cache...\")\n\n        cache_dir = Path(get_cache_dir()) / self.repository.id_str\n        if not cache_dir.exists():\n            logger.debug(\"Cache directory does not exist, skipping files cache cleanup\")\n            return\n\n        # Get all existing archive series names\n        existing_series = set(self.manifest.archives.names())\n        logger.debug(f\"Found {len(existing_series)} existing archive series.\")\n\n        # Get the set of all existing files cache file names.\n        try:\n            files_cache_names = set(discover_files_cache_names(cache_dir))\n            logger.debug(f\"Found {len(files_cache_names)} files cache files.\")\n        except (FileNotFoundError, PermissionError) as e:\n            logger.warning(f\"Could not access cache directory: {e}\")\n            return\n\n        used_files_cache_names = {files_cache_name(series_name) for series_name in existing_series}\n        unused_files_cache_names = files_cache_names - used_files_cache_names\n\n        for cache_filename in unused_files_cache_names:\n            cache_path = cache_dir / cache_filename\n            try:\n                cache_path.unlink()\n            except (FileNotFoundError, PermissionError) as e:\n                logger.warning(f\"Could not access cache file: {e}\")\n        logger.info(f\"Removed {len(unused_files_cache_names)} unused files cache files.\")\n\n    def analyze_archives(self) -> tuple[set, int, int, int]:\n        \"\"\"Iterate over all items in all archives, create the dicts id -> size of all used chunks.\"\"\"\n\n        def use_it(id):\n            entry = self.chunks.get(id)\n            if entry is not None:\n                # the chunk is in the repo, mark it used.\n                self.chunks[id] = entry._replace(flags=entry.flags | ChunkIndex.F_USED)\n            else:\n                # with --stats: we do NOT have this chunk in the repository!\n                # without --stats: we do not have this chunk or the chunks index is incomplete.\n                missing_chunks.add(id)\n\n        missing_chunks: set[bytes] = set()\n        archive_infos = self.manifest.archives.list(sort_by=[\"ts\"])\n        num_archives = len(archive_infos)\n        pi = ProgressIndicatorPercent(\n            total=num_archives, msg=\"Computing used chunks %3.1f%%\", step=0.1, msgid=\"compact.analyze_archives\"\n        )\n        total_size, total_files = 0, 0\n        for i, info in enumerate(archive_infos):\n            pi.show(i)\n            logger.info(\n                f\"Analyzing archive {info.name} {info.ts.astimezone()} {bin_to_hex(info.id)} ({i + 1}/{num_archives})\"\n            )\n            archive = Archive(self.manifest, info.id, iec=self.iec)\n            # archive metadata size unknown, but usually small/irrelevant:\n            use_it(archive.id)\n            for id in archive.metadata.item_ptrs:\n                use_it(id)\n            for id in archive.metadata.items:\n                use_it(id)\n            # archive items content data:\n            for item in archive.iter_items():\n                total_files += 1  # every fs object counts, not just regular files\n                if \"chunks\" in item:\n                    for id, size in item.chunks:\n                        total_size += size  # original, uncompressed file content size\n                        use_it(id)\n        pi.finish()\n        return missing_chunks, total_files, total_size, num_archives\n\n    def report_and_delete(self):\n        if self.missing_chunks:\n            logger.error(f\"Repository has {len(self.missing_chunks)} missing objects!\")\n            for id in sorted(self.missing_chunks):\n                logger.debug(f\"Missing object {bin_to_hex(id)}\")\n            set_ec(EXIT_ERROR)\n\n        logger.info(\"Cleaning archives directory from soft-deleted archives...\")\n        archive_infos = self.manifest.archives.list(sort_by=[\"ts\"], deleted=True)\n        for archive_info in archive_infos:\n            name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)\n            try:\n                self.manifest.archives.nuke_by_id(id)\n            except self.repository.ObjectNotFound:\n                logger.warning(f\"Soft-deleted archive {name} {hex_id} not found.\")\n\n        repo_size_before = self.repository_size\n        logger.info(\"Determining unused objects...\")\n        unused = set()\n        for id, entry in self.chunks.iteritems():\n            if not (entry.flags & ChunkIndex.F_USED):\n                unused.add(id)\n        logger.info(f\"Deleting {len(unused)} unused objects...\")\n        pi = ProgressIndicatorPercent(\n            total=len(unused), msg=\"Deleting unused objects %3.1f%%\", step=0.1, msgid=\"compact.report_and_delete\"\n        )\n        for i, id in enumerate(unused):\n            pi.show(i)\n            self.repository.delete(id)\n            del self.chunks[id]\n        pi.finish()\n        repo_size_after = self.repository_size\n\n        count = len(self.chunks)\n        logger.info(f\"Overall statistics, considering all {self.archives_count} archives in this repository:\")\n        logger.info(\n            f\"Source data size was {format_file_size(self.total_size, precision=0, iec=self.iec)} \"\n            f\"in {self.total_files} files.\"\n        )\n        if self.stats:\n            logger.info(\n                f\"Repository size is {format_file_size(repo_size_after, precision=0, iec=self.iec)} \"\n                f\"in {count} objects.\"\n            )\n            logger.info(\n                f\"Compaction saved \"\n                f\"{format_file_size(repo_size_before - repo_size_after, precision=0, iec=self.iec)}.\"\n            )\n        else:\n            logger.info(f\"Repository has data stored in {count} objects.\")\n\n\nclass CompactMixIn:\n    @with_repository(exclusive=True, compatibility=(Manifest.Operation.DELETE,))\n    def do_compact(self, args, repository, manifest):\n        \"\"\"Collects garbage in the repository.\"\"\"\n        if not args.dry_run:  # support --dry-run to simplify scripting\n            ArchiveGarbageCollector(repository, manifest, stats=args.stats, iec=args.iec).garbage_collect()\n\n    def build_parser_compact(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        compact_epilog = process_epilog(\n            \"\"\"\n            Free repository space by deleting unused chunks.\n\n            ``borg compact`` analyzes all existing archives to determine which repository\n            objects are actually used (referenced). It then deletes all unused objects\n            from the repository to free space.\n\n            Unused objects may result from:\n\n            - use of ``borg delete`` or ``borg prune``\n            - interrupted backups (consider retrying the backup before running compact)\n            - backups of source files that encountered an I/O error mid-transfer and were skipped\n            - corruption of the repository (e.g., the archives directory lost entries; see notes below)\n\n            You usually do not want to run ``borg compact`` after every write operation, but\n            either regularly (e.g., once a month, possibly together with ``borg check``) or\n            when disk space needs to be freed.\n\n            **Important:**\n\n            After compacting, it is no longer possible to use ``borg undelete`` to recover\n            previously soft-deleted archives.\n\n            ``borg compact`` might also delete data from archives that were \"lost\" due to\n            archives directory corruption. Such archives could potentially be restored with\n            ``borg check --find-lost-archives [--repair]``, which is slow. You therefore\n            might not want to do that unless there are signs of lost archives (e.g., when\n            seeing fatal errors when creating backups or when archives are missing in\n            ``borg repo-list``).\n\n            When using the ``--stats`` option, borg will internally list all repository\n            objects to determine their existence and stored size. It will build a fresh\n            chunks index from that information and cache it in the repository. For some\n            types of repositories, this might be very slow. It will tell you the sum of\n            stored object sizes, before and after compaction.\n\n            Without ``--stats``, borg will rely on the cached chunks index to determine\n            existing object IDs (but there is no stored size information in the index,\n            thus it cannot compute before/after compaction size statistics).\n            \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_compact.__doc__, epilog=compact_epilog)\n        subparsers.add_subcommand(\"compact\", subparser, help=\"compact the repository\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change the repository\"\n        )\n        subparser.add_argument(\n            \"-s\", \"--stats\", dest=\"stats\", action=\"store_true\", help=\"print statistics (might be much slower)\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/completion_cmd.py",
    "content": "\"\"\"\nShell completion support for Borg commands.\n\nThis module implements the `borg completion` command, which generates shell completion\nscripts for bash and zsh. It uses the shtab library for basic completion generation\nand extends it with custom dynamic completions for Borg-specific argument types.\n\nDynamic Completions\n-------------------\n\nThe following argument types have intelligent, context-aware completion:\n\n1. Archive names/IDs (archivename_validator):\n   - Completes archive names by default (e.g., \"my-backup-2024\")\n   - Completes archive IDs when prefixed with \"aid:\" (e.g., \"aid:12345678\")\n   - In zsh, shows archive metadata (name, timestamp, user@host) as descriptions\n   - Respects --repo/-r flags to query the correct repository\n\n2. Sort keys (SortBySpec):\n   - Completes comma-separated sort keys (timestamp, archive, name, id, tags, host, user)\n   - Prevents duplicate keys in the same option\n\n3. Files cache mode (FilesCacheMode):\n   - Completes comma-separated cache mode tokens (ctime, mtime, size, inode, rechunk, disabled)\n   - Enforces mutual exclusivity (e.g., ctime vs mtime, disabled vs others)\n\n4. Compression algorithms (CompressionSpec):\n   - Suggests compression specs with examples (lz4, zstd,3, auto,zstd,10, etc.)\n\n5. Chunker parameters (ChunkerParams):\n   - Suggests chunker param examples (default, fixed,4194304, buzhash,19,23,21,4095, etc.)\n\n6. Paths (PathSpec):\n   - Completes directories using standard shell directory completion\n\n7. Help topics:\n   - Completes help command topics and subcommand names\n\n8. Tags (tag_validator):\n   - Completes existing tags from the repository\n\n9. Relative time markers (relative_time_marker_validator):\n   - Suggests common time intervals (60S, 60M, 24H, 7d, 4w, 12m, 1000y)\n\n10. Timestamps (timestamp):\n   - Completes file paths when starting with / or .\n   - Otherwise suggests current timestamp in ISO format\n\n11. File sizes (parse_file_size):\n   - Suggests common file size values (500M, 1G, 10G, 100G, 1T, etc.)\n\"\"\"\n\nimport shtab\n\nfrom ._common import process_epilog\nfrom ..constants import *  # NOQA\nfrom ..helpers import (\n    archivename_validator,\n    SortBySpec,\n    FilesCacheMode,\n    PathSpec,\n    ChunkerParams,\n    CompressionSpec,\n    tag_validator,\n    relative_time_marker_validator,\n    parse_file_size,\n)\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..helpers.argparsing import ActionSubCommands\nfrom ..helpers.time import timestamp\nfrom ..helpers.parseformat import partial_format\nfrom ..manifest import AI_HUMAN_SORT_KEYS\n\n# Global bash preamble that is prepended to the generated completion script.\n# It aggregates only what we need:\n# - wordbreak fixes for ':' and '=' so tokens like 'aid:' and '--repo=/path' stay intact\n# - a minimal dynamic completion helper for aid: archive IDs\nBASH_PREAMBLE_TMPL = r\"\"\"\n# keep ':' and '=' intact so tokens like 'aid:' and '--repo=/path' stay whole\nif [[ ${COMP_WORDBREAKS-} == *:* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//:}; fi\nif [[ ${COMP_WORDBREAKS-} == *=* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//=}; fi\n\n_borg_complete_archive() {\n  local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\n  # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V\n  local repo_arg=()\n  local i w\n  for (( i=0; i<${#COMP_WORDS[@]}; i++ )); do\n    w=\"${COMP_WORDS[i]}\"\n    if [[ \"$w\" == --repo=* ]]; then repo_arg=( --repo \"${w#--repo=}\" ); break\n    elif [[ \"$w\" == -r=* ]]; then repo_arg=( -r \"${w#-r=}\" ); break\n    elif [[ \"$w\" == -r* && \"$w\" != \"-r\" ]]; then repo_arg=( -r \"${w#-r}\" ); break\n    elif [[ \"$w\" == \"--repo\" || \"$w\" == \"-r\" ]]; then\n      if (( i+1 < ${#COMP_WORDS[@]} )); then repo_arg=( \"$w\" \"${COMP_WORDS[i+1]}\" ); fi\n      break\n    fi\n  done\n\n  # Check if completing aid: prefix\n  if [[ \"$cur\" == aid:* ]]; then\n    local prefix=\"${cur#aid:}\"\n    [[ -n \"$prefix\" && ! \"$prefix\" =~ ^[0-9a-fA-F]*$ ]] && return 0\n\n    # ask borg for raw IDs; avoid prompts and suppress stderr\n    local out\n    if [[ -n \"${repo_arg[*]}\" ]]; then\n      out=$( borg repo-list \"${repo_arg[@]}\" --format '{id}{NL}' 2>/dev/null </dev/null )\n    else\n      out=$( borg repo-list --format '{id}{NL}' 2>/dev/null </dev/null )\n    fi\n    [[ -z \"$out\" ]] && return 0\n\n    # filter by (case-insensitive) hex prefix and emit candidates\n    local IFS=$'\\n' id prelower idlower\n    prelower=\"$(printf '%s' \"$prefix\" | tr '[:upper:]' '[:lower:]')\"\n    while IFS= read -r id; do\n      [[ -z \"$id\" ]] && continue\n      idlower=\"$(printf '%s' \"$id\" | tr '[:upper:]' '[:lower:]')\"\n      # Print only the first 8 hex digits of the ID for completion suggestions.\n      [[ \"$idlower\" == \"$prelower\"* ]] && printf 'aid:%s\\n' \"${id:0:8}\"\n    done <<< \"$out\"\n  else\n    # Complete archive names\n    local out\n    if [[ -n \"${repo_arg[*]}\" ]]; then\n      out=$( borg repo-list \"${repo_arg[@]}\" --format '{archive}{NL}' 2>/dev/null </dev/null )\n    else\n      out=$( borg repo-list --format '{archive}{NL}' 2>/dev/null </dev/null )\n    fi\n    [[ -z \"$out\" ]] && return 0\n\n    # filter by prefix and emit candidates\n    local IFS=$'\\n' name\n    while IFS= read -r name; do\n      [[ -z \"$name\" ]] && continue\n      [[ -z \"$cur\" || \"$name\" == \"$cur\"* ]] && printf '%s\\n' \"$name\"\n    done <<< \"$out\"\n  fi\n  return 0\n}\n\n# Complete compression spec options\n_borg_complete_compression_spec() {\n  local choices=\"{COMP_SPEC_CHOICES}\"\n  local IFS=$' \\t\\n'\n  compgen -W \"${choices}\" -- \"$1\"\n}\n\n# Complete chunker params options\n_borg_complete_chunker_params() {\n  local choices=\"{CHUNKER_PARAMS_CHOICES}\"\n  local IFS=$' \\t\\n'\n  compgen -W \"${choices}\" -- \"$1\"\n}\n\n# Complete tags from repository\n_borg_complete_tags() {\n  local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\n  # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V\n  local repo_arg=()\n  local i w\n  for (( i=0; i<${#COMP_WORDS[@]}; i++ )); do\n    w=\"${COMP_WORDS[i]}\"\n    if [[ \"$w\" == --repo=* ]]; then repo_arg=( --repo \"${w#--repo=}\" ); break\n    elif [[ \"$w\" == -r=* ]]; then repo_arg=( -r \"${w#-r=}\" ); break\n    elif [[ \"$w\" == -r* && \"$w\" != \"-r\" ]]; then repo_arg=( -r \"${w#-r}\" ); break\n    elif [[ \"$w\" == \"--repo\" || \"$w\" == \"-r\" ]]; then\n      if (( i+1 < ${#COMP_WORDS[@]} )); then repo_arg=( \"$w\" \"${COMP_WORDS[i+1]}\" ); fi\n      break\n    fi\n  done\n\n  # ask borg for tags; avoid prompts and suppress stderr\n  local out\n  if [[ -n \"${repo_arg[*]}\" ]]; then\n    out=$( borg repo-list \"${repo_arg[@]}\" --format '{tags}{NL}' 2>/dev/null </dev/null )\n  else\n    out=$( borg repo-list --format '{tags}{NL}' 2>/dev/null </dev/null )\n  fi\n  [[ -z \"$out\" ]] && return 0\n\n  # extract unique tags and filter by prefix\n  local IFS=$'\\n' line tag\n  local -A seen\n  while IFS= read -r line; do\n    [[ -z \"$line\" ]] && continue\n    # tags are comma-separated, split and deduplicate\n    IFS=',' read -ra tags <<< \"$line\"\n    for tag in \"${tags[@]}\"; do\n      tag=\"${tag# }\"\n      tag=\"${tag% }\"\n      [[ -z \"$tag\" ]] && continue\n      [[ -n \"${seen[$tag]}\" ]] && continue\n      seen[$tag]=1\n      [[ -z \"$cur\" || \"$tag\" == \"$cur\"* ]] && printf '%s\\n' \"$tag\"\n    done\n  done <<< \"$out\"\n  return 0\n}\n\n# Complete relative time markers\n_borg_complete_relative_time() {\n  local choices=\"{RELATIVE_TIME_CHOICES}\"\n  local IFS=$' \\t\\n'\n  compgen -W \"${choices}\" -- \"$1\"\n}\n\n# Complete timestamp (file path or ISO timestamp)\n_borg_complete_timestamp() {\n  local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\n  # If starts with / or ., complete as file path\n  if [[ \"$cur\" == /* || \"$cur\" == ./* || \"$cur\" == ../* || \"$cur\" == . || \"$cur\" == .. ]]; then\n    compgen -f -- \"$cur\"\n  else\n    # Suggest current timestamp in ISO format\n    date +\"%Y-%m-%dT%H:%M:%S%z\" | sed 's/\\([0-9]\\{2\\}\\)$/:\\1/'\n  fi\n}\n\n# Complete file size values\n_borg_complete_file_size() {\n  local choices=\"{FILE_SIZE_CHOICES}\"\n  local IFS=$' \\t\\n'\n  compgen -W \"${choices}\" -- \"$1\"\n}\n\n# Complete comma-separated sort keys for any option with type=SortBySpec.\n# Keys are validated against Borg's AI_HUMAN_SORT_KEYS.\n_borg_complete_sortby() {\n  local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\n  # Extract value part for --opt=value forms; otherwise the value is the word itself\n  local val prefix_eq\n  if [[ \"$cur\" == *=* ]]; then\n    prefix_eq=\"${cur%%=*}=\"\n    val=\"${cur#*=}\"\n  else\n    prefix_eq=\"\"\n    val=\"$cur\"\n  fi\n\n  # Split into head (selected keys + trailing comma if any) and fragment (last token being typed)\n  local head frag\n  if [[ \"$val\" == *,* ]]; then\n    head=\"${val%,*},\"\n    frag=\"${val##*,}\"\n  else\n    head=\"\"\n    frag=\"$val\"\n  fi\n\n  # Build a comma-delimited list for cheap membership testing\n  local headlist\n  if [[ -n \"$head\" ]]; then\n    headlist=\",${head%,},\"\n  else\n    headlist=\",\"  # nothing selected yet\n  fi\n\n  # Valid keys (embedded at generation time)\n  local keys=({SORT_KEYS})\n\n  local k\n  for k in \"${keys[@]}\"; do\n    # skip already-selected keys\n    [[ \"$headlist\" == *\",${k},\"* ]] && continue\n    # match prefix of last fragment\n    [[ -n \"$frag\" && \"$k\" != \"$frag\"* ]] && continue\n    printf '%s\\n' \"${prefix_eq}${head}${k}\"\n  done\n}\n\n# Complete comma-separated files cache mode tokens for options with type=FilesCacheMode.\n_borg_complete_filescachemode() {\n  local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\n  # Extract value part for --opt=value forms; otherwise the value is the word itself\n  local val prefix_eq\n  if [[ \"$cur\" == *=* ]]; then\n    prefix_eq=\"${cur%%=*}=\"\n    val=\"${cur#*=}\"\n  else\n    prefix_eq=\"\"\n    val=\"$cur\"\n  fi\n\n  # Split into head (selected keys + trailing comma if any) and fragment (last token being typed)\n  local head frag\n  if [[ \"$val\" == *,* ]]; then\n    head=\"${val%,*},\"\n    frag=\"${val##*,}\"\n  else\n    head=\"\"\n    frag=\"$val\"\n  fi\n\n  # Build a comma-delimited list for cheap membership testing\n  local headlist\n  if [[ -n \"$head\" ]]; then\n    headlist=\",${head%,},\"\n  else\n    headlist=\",\"  # nothing selected yet\n  fi\n\n  # Valid tokens (embedded at generation time)\n  local keys=({FCM_KEYS})\n\n  # If 'disabled' is already selected, there is nothing else to suggest.\n  if [[ \"$headlist\" == *\",disabled,\"* ]]; then\n    return 0\n  fi\n\n  local k\n  for k in \"${keys[@]}\"; do\n    # skip duplicates\n    [[ \"$headlist\" == *\",${k},\"* ]] && continue\n    # do not suggest 'disabled' if any other token is already selected\n    if [[ -n \"$head\" && \"$k\" == \"disabled\" ]]; then\n      continue\n    fi\n    # ctime/mtime are mutually exclusive: don't suggest the other if one is present\n    if [[ \"$k\" == \"ctime\" && \"$headlist\" == *\",mtime,\"* ]]; then\n      continue\n    fi\n    if [[ \"$k\" == \"mtime\" && \"$headlist\" == *\",ctime,\"* ]]; then\n      continue\n    fi\n    # match prefix of last fragment\n    [[ -n \"$frag\" && \"$k\" != \"$frag\"* ]] && continue\n    printf '%s\\n' \"${prefix_eq}${head}${k}\"\n  done\n}\n\n_borg_help_topics() {\n    local choices=\"{HELP_CHOICES}\"\n    local IFS=$' \\t\\n'\n    compgen -W \"${choices}\" -- \"$1\"\n}\n\"\"\"\n\n# Global zsh preamble providing dynamic completion for aid:<hex> archive IDs.\n#\n# Notes:\n# - We use zsh's $words/$CURRENT arrays to inspect the command line.\n# - Candidates are returned via `compadd`.\n# - We try to detect repo context from --repo=V, --repo V, -r=V, -rV, -r V.\nZSH_PREAMBLE_TMPL = r\"\"\"\n_borg_complete_archive() {\n  local cur\n  cur=\"${words[$CURRENT]}\"\n\n  # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V\n  local -a repo_arg=()\n  local i w\n  for i in {1..$#words}; do\n    w=\"$words[$i]\"\n    if [[ \"$w\" == --repo=* ]]; then repo_arg=( --repo \"${w#--repo=}\" ); break\n    elif [[ \"$w\" == -r=* ]]; then repo_arg=( -r \"${w#-r=}\" ); break\n    elif [[ \"$w\" == -r* && \"$w\" != \"-r\" ]]; then repo_arg=( -r \"${w#-r}\" ); break\n    elif [[ \"$w\" == \"--repo\" || \"$w\" == \"-r\" ]]; then\n      if (( i+1 <= $#words )); then repo_arg=( \"$w\" \"${words[$((i+1))]}\" ); fi\n      break\n    fi\n  done\n\n  # Check if completing aid: prefix\n  if [[ \"$cur\" == aid:* ]]; then\n    local prefix=\"${cur#aid:}\"\n    # allow only hex digits as prefix; empty prefix also allowed (list all)\n    [[ -n \"$prefix\" && ! \"$prefix\" == [0-9a-fA-F]# ]] && return 0\n\n    # ask borg for IDs with metadata; avoid prompts and suppress stderr\n    # Use tab as delimiter to avoid issues with spaces in archive names\n    local out\n    if (( ${#repo_arg[@]} > 0 )); then\n      out=$( borg repo-list \"${repo_arg[@]}\" --format '{id}{TAB}{archive}{TAB}{time}{TAB}{username}@{hostname}{NL}' \\\n             2>/dev/null </dev/null )\n    else\n      out=$( borg repo-list --format '{id}{TAB}{archive}{TAB}{time}{TAB}{username}@{hostname}{NL}' \\\n             2>/dev/null </dev/null )\n    fi\n    [[ -z \"$out\" ]] && return 0\n\n    # filter by (case-insensitive) hex prefix and build candidates with descriptions\n    local prelower id idlower line\n    prelower=\"${prefix:l}\"\n    local -a candidates=()\n    local -a descriptions=()\n    while IFS=$'\\t' read -r id archive time userhost; do\n      [[ -z \"$id\" ]] && continue\n      idlower=\"${id:l}\"\n      if [[ \"$idlower\" == \"$prelower\"* ]]; then\n        candidates+=( \"aid:${id[1,8]}\" )\n        # Description: show full ID, archive name, time, user@host\n        descriptions+=( \"${id[1,8]}: ${archive} (${time} ${userhost})\" )\n      fi\n    done <<< \"$out\"\n    # -Q: do not escape special chars, -d: provide descriptions, -l: one per line\n    compadd -Q -l -d descriptions -- $candidates\n  else\n    # Complete archive names\n    local out\n    if (( ${#repo_arg[@]} > 0 )); then\n      out=$( borg repo-list \"${repo_arg[@]}\" --format '{archive}{NL}' 2>/dev/null </dev/null )\n    else\n      out=$( borg repo-list --format '{archive}{NL}' 2>/dev/null </dev/null )\n    fi\n    [[ -z \"$out\" ]] && return 0\n\n    # filter by prefix and emit candidates\n    local -a candidates=()\n    local name\n    for name in ${(f)out}; do\n      [[ -z \"$name\" ]] && continue\n      if [[ -z \"$cur\" || \"$name\" == \"$cur\"* ]]; then\n        candidates+=( \"$name\" )\n      fi\n    done\n    compadd -Q -- $candidates\n  fi\n  return 0\n}\n\n# Complete compression spec options\n_borg_complete_compression_spec() {\n  local choices=({COMP_SPEC_CHOICES})\n  # use compadd -V to preserve order (do not sort)\n  compadd -V 'compression algorithms' -Q -a choices\n}\n\n# Complete chunker params options\n_borg_complete_chunker_params() {\n  local choices=({CHUNKER_PARAMS_CHOICES})\n  # use compadd -V to preserve order (do not sort)\n  compadd -V 'chunker params' -Q -a choices\n}\n\n# Complete tags from repository\n_borg_complete_tags() {\n  local cur\n  cur=\"${words[$CURRENT]}\"\n\n  # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V\n  local -a repo_arg=()\n  local i w\n  for i in {1..$#words}; do\n    w=\"$words[$i]\"\n    if [[ \"$w\" == --repo=* ]]; then repo_arg=( --repo \"${w#--repo=}\" ); break\n    elif [[ \"$w\" == -r=* ]]; then repo_arg=( -r \"${w#-r=}\" ); break\n    elif [[ \"$w\" == -r* && \"$w\" != \"-r\" ]]; then repo_arg=( -r \"${w#-r}\" ); break\n    elif [[ \"$w\" == \"--repo\" || \"$w\" == \"-r\" ]]; then\n      if (( i+1 <= $#words )); then repo_arg=( \"$w\" \"${words[$((i+1))]}\" ); fi\n      break\n    fi\n  done\n\n  # ask borg for tags; avoid prompts and suppress stderr\n  local out\n  if (( ${#repo_arg[@]} > 0 )); then\n    out=$( borg repo-list \"${repo_arg[@]}\" --format '{tags}{NL}' 2>/dev/null </dev/null )\n  else\n    out=$( borg repo-list --format '{tags}{NL}' 2>/dev/null </dev/null )\n  fi\n  [[ -z \"$out\" ]] && return 0\n\n  # extract unique tags and filter by prefix\n  local line tag\n  local -A seen\n  local -a candidates=()\n  for line in ${(f)out}; do\n    [[ -z \"$line\" ]] && continue\n    # tags are comma-separated, split and deduplicate\n    for tag in ${(s:,:)line}; do\n      tag=\"${tag## }\"\n      tag=\"${tag%% }\"\n      [[ -z \"$tag\" ]] && continue\n      [[ -n \"${seen[$tag]}\" ]] && continue\n      seen[$tag]=1\n      if [[ -z \"$cur\" || \"$tag\" == \"$cur\"* ]]; then\n        candidates+=( \"$tag\" )\n      fi\n    done\n  done\n  compadd -Q -- $candidates\n  return 0\n}\n\n# Complete relative time markers\n_borg_complete_relative_time() {\n  local choices=({RELATIVE_TIME_CHOICES})\n  # use compadd -V to preserve order (do not sort)\n  compadd -V 'relative time' -Q -a choices\n}\n\n# Complete timestamp (file path or ISO timestamp)\n_borg_complete_timestamp() {\n  local cur\n  cur=\"${words[$CURRENT]}\"\n\n  # If starts with / or ., complete as file path\n  if [[ \"$cur\" == /* || \"$cur\" == ./* || \"$cur\" == ../* || \"$cur\" == . || \"$cur\" == .. ]]; then\n    _files\n  else\n    # Suggest current timestamp in ISO format\n    local timestamp\n    timestamp=$(date +\"%Y-%m-%dT%H:%M:%S%z\" | sed 's/\\([0-9]\\{2\\}\\)$/:\\1/')\n    compadd -Q -- \"$timestamp\"\n  fi\n}\n\n# Complete file size values\n_borg_complete_file_size() {\n  local choices=({FILE_SIZE_CHOICES})\n  # use compadd -V to preserve order (do not sort)\n  compadd -V 'file size' -Q -a choices\n}\n\n# Complete comma-separated sort keys for any option with type=SortBySpec.\n_borg_complete_sortby() {\n  local cur\n  cur=\"${words[$CURRENT]}\"\n\n  local val prefix_eq\n  if [[ \"$cur\" == *\"=\"* ]]; then\n    prefix_eq=\"${cur%%\\=*}=\"\n    val=\"${cur#*=}\"\n  else\n    prefix_eq=\"\"\n    val=\"$cur\"\n  fi\n\n  local head frag\n  if [[ \"$val\" == *\",\"* ]]; then\n    head=\"${val%,*},\"\n    frag=\"${val##*,}\"\n  else\n    head=\"\"\n    frag=\"$val\"\n  fi\n\n  local headlist\n  if [[ -n \"$head\" ]]; then\n    headlist=\",${head%,},\"\n  else\n    headlist=\",\"  # nothing selected yet\n  fi\n\n  # Valid keys (embedded at generation time)\n  local -a keys=({SORT_KEYS})\n\n  local -a candidates=()\n  local k\n  for k in ${keys[@]}; do\n    [[ \"$headlist\" == *\",${k},\"* ]] && continue\n    [[ -n \"$frag\" && \"$k\" != \"$frag\"* ]] && continue\n    candidates+=( \"${prefix_eq}${head}${k}\" )\n  done\n  compadd -Q -- $candidates\n  return 0\n}\n\n# Complete comma-separated files cache mode tokens for options with type=FilesCacheMode.\n_borg_complete_filescachemode() {\n  local cur\n  cur=\"${words[$CURRENT]}\"\n\n  local val prefix_eq\n  if [[ \"$cur\" == *\"=\"* ]]; then\n    prefix_eq=\"${cur%%\\=*}=\"\n    val=\"${cur#*=}\"\n  else\n    prefix_eq=\"\"\n    val=\"$cur\"\n  fi\n\n  local head frag\n  if [[ \"$val\" == *\",\"* ]]; then\n    head=\"${val%,*},\"\n    frag=\"${val##*,}\"\n  else\n    head=\"\"\n    frag=\"$val\"\n  fi\n\n  local headlist\n  if [[ -n \"$head\" ]]; then\n    headlist=\",${head%,},\"\n  else\n    headlist=\",\"  # nothing selected yet\n  fi\n\n  # Valid tokens (embedded at generation time)\n  local -a keys=({FCM_KEYS})\n\n  # If 'disabled' is already selected, there is nothing else to suggest.\n  if [[ \"$headlist\" == *\",disabled,\"* ]]; then\n    return 0\n  fi\n\n  local -a candidates=()\n  local k\n  for k in ${keys[@]}; do\n    [[ \"$headlist\" == *\",${k},\"* ]] && continue\n    if [[ -n \"$head\" && \"$k\" == \"disabled\" ]]; then\n      continue\n    fi\n    if [[ \"$k\" == \"ctime\" && \"$headlist\" == *\",mtime,\"* ]]; then\n      continue\n    fi\n    if [[ \"$k\" == \"mtime\" && \"$headlist\" == *\",ctime,\"* ]]; then\n      continue\n    fi\n    [[ -n \"$frag\" && \"$k\" != \"$frag\"* ]] && continue\n    candidates+=( \"${prefix_eq}${head}${k}\" )\n  done\n  compadd -Q -- $candidates\n  return 0\n}\n\n_borg_help_topics() {\n    local choices=({HELP_CHOICES})\n    _describe 'help topics' choices\n}\n\"\"\"\n\n\ndef _attach_completion(parser: ArgumentParser, type_class, completion_dict: dict):\n    \"\"\"Tag all arguments with type `type_class` with completion choices from `completion_dict`.\"\"\"\n\n    for action in parser._actions:\n        if isinstance(action, ActionSubCommands):\n            for sub in action.choices.values():\n                _attach_completion(sub, type_class, completion_dict)\n            continue\n\n        if action.type is type_class:\n            action.complete = completion_dict  # type: ignore[attr-defined]\n\n\ndef _attach_help_completion(parser: ArgumentParser, completion_dict: dict):\n    \"\"\"Tag the 'topic' argument of the 'help' command with static completion choices.\"\"\"\n    for action in parser._actions:\n        if isinstance(action, ActionSubCommands):\n            for sub in action.choices.values():\n                _attach_help_completion(sub, completion_dict)\n            continue\n\n        if action.dest == \"topic\":\n            action.complete = completion_dict  # type: ignore[attr-defined]\n\n\nclass CompletionMixIn:\n    def do_completion(self, args):\n        \"\"\"Output shell completion script for the given shell.\"\"\"\n        # Automagically generates completions for subcommands and options. Also\n        # adds dynamic completion for archive IDs with the aid: prefix for all ARCHIVE\n        # arguments (identified by archivename_validator). It reuses `borg repo-list`\n        # to enumerate archives and does not introduce any new commands or caching.\n        parser = self.build_parser()\n        _attach_completion(\n            parser, archivename_validator, {\"bash\": \"_borg_complete_archive\", \"zsh\": \"_borg_complete_archive\"}\n        )\n        _attach_completion(parser, SortBySpec, {\"bash\": \"_borg_complete_sortby\", \"zsh\": \"_borg_complete_sortby\"})\n        _attach_completion(\n            parser, FilesCacheMode, {\"bash\": \"_borg_complete_filescachemode\", \"zsh\": \"_borg_complete_filescachemode\"}\n        )\n        _attach_completion(\n            parser,\n            CompressionSpec,\n            {\"bash\": \"_borg_complete_compression_spec\", \"zsh\": \"_borg_complete_compression_spec\"},\n        )\n        _attach_completion(parser, PathSpec, shtab.DIRECTORY)\n        _attach_completion(\n            parser, ChunkerParams, {\"bash\": \"_borg_complete_chunker_params\", \"zsh\": \"_borg_complete_chunker_params\"}\n        )\n        _attach_completion(parser, tag_validator, {\"bash\": \"_borg_complete_tags\", \"zsh\": \"_borg_complete_tags\"})\n        _attach_completion(\n            parser,\n            relative_time_marker_validator,\n            {\"bash\": \"_borg_complete_relative_time\", \"zsh\": \"_borg_complete_relative_time\"},\n        )\n        _attach_completion(parser, timestamp, {\"bash\": \"_borg_complete_timestamp\", \"zsh\": \"_borg_complete_timestamp\"})\n        _attach_completion(\n            parser, parse_file_size, {\"bash\": \"_borg_complete_file_size\", \"zsh\": \"_borg_complete_file_size\"}\n        )\n\n        # Collect all commands and help topics for \"borg help\" completion\n        help_choices = list(self.helptext.keys())\n        for action in parser._actions:\n            if isinstance(action, ActionSubCommands):\n                help_choices.extend(action.choices.keys())\n\n        help_completion_fn = \"_borg_help_topics\"\n        _attach_help_completion(parser, {\"bash\": help_completion_fn, \"zsh\": help_completion_fn})\n\n        # Build preambles using partial_format to avoid escaping braces etc.\n        sort_keys = \" \".join(AI_HUMAN_SORT_KEYS)\n        fcm_keys = \" \".join([\"ctime\", \"mtime\", \"size\", \"inode\", \"rechunk\", \"disabled\"])  # keep in sync with parser\n\n        # Help completion templates\n        help_choices = \" \".join(sorted(help_choices))\n\n        # Compression spec choices (static list)\n        comp_spec_choices = [\"lz4\", \"zstd,3\", \"auto,zstd,10\", \"zlib,6\", \"lzma,6\", \"obfuscate,250,lz4\", \"none\"]\n        comp_spec_choices_str = \" \".join(comp_spec_choices)\n\n        # Chunker params choices (static list)\n        chunker_params_choices = [\"default\", \"fixed,4194304\", \"buzhash,19,23,21,4095\", \"buzhash64,19,23,21,4095\"]\n        chunker_params_choices_str = \" \".join(chunker_params_choices)\n\n        # Relative time marker choices (static list)\n        relative_time_choices = [\"60S\", \"60M\", \"24H\", \"7d\", \"4w\", \"12m\", \"1000y\"]\n        relative_time_choices_str = \" \".join(relative_time_choices)\n\n        # File size choices (static list)\n        file_size_choices = [\"500M\", \"1G\", \"10G\", \"100G\", \"1T\"]\n        file_size_choices_str = \" \".join(file_size_choices)\n\n        mapping = {\n            \"SORT_KEYS\": sort_keys,\n            \"FCM_KEYS\": fcm_keys,\n            \"COMP_SPEC_CHOICES\": comp_spec_choices_str,\n            \"CHUNKER_PARAMS_CHOICES\": chunker_params_choices_str,\n            \"RELATIVE_TIME_CHOICES\": relative_time_choices_str,\n            \"FILE_SIZE_CHOICES\": file_size_choices_str,\n            \"HELP_CHOICES\": help_choices,\n        }\n        bash_preamble = partial_format(BASH_PREAMBLE_TMPL, mapping)\n        zsh_preamble = partial_format(ZSH_PREAMBLE_TMPL, mapping)\n\n        if args.shell == \"bash\":\n            preambles = [bash_preamble]\n        elif args.shell == \"zsh\":\n            preambles = [zsh_preamble]\n        else:\n            preambles = []\n        script = parser.get_completion_script(f\"shtab-{args.shell}\", preambles=preambles)\n        print(script)\n\n    def build_parser_completion(self, subparsers, common_parser, mid_common_parser):\n        shells = tuple(shtab.SUPPORTED_SHELLS)\n\n        completion_epilog = process_epilog(\n            \"\"\"\n        This command prints a shell completion script for the given shell.\n\n        Please note that for some dynamic completions (like archive IDs), the shell\n        completion script will call borg to query the repository. This will work best\n        if that call can be made without prompting for user input, so you may want to\n        set BORG_REPO and BORG_PASSPHRASE environment variables.\n        \"\"\"\n        )\n\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_completion.__doc__, epilog=completion_epilog\n        )\n        subparsers.add_subcommand(\"completion\", subparser, help=\"output shell completion script\")\n        subparser.add_argument(\n            \"shell\", metavar=\"SHELL\", choices=shells, help=\"shell to generate completion for (one of: %(choices)s)\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/create_cmd.py",
    "content": "import errno\nimport sys\nimport logging\nimport os\nimport posixpath\nimport stat\nimport subprocess\nimport time\nfrom io import TextIOWrapper\n\nfrom ._common import with_repository, Highlander\nfrom .. import helpers\nfrom ..archive import Archive, is_special\nfrom ..archive import BackupError, BackupOSError, BackupItemExcluded, backup_io, OsOpen, stat_update_check\nfrom ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProcessor\nfrom ..cache import Cache\nfrom ..constants import *  # NOQA\nfrom ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec, CompressionSpec\nfrom ..helpers import archivename_validator, FilesCacheMode, octal_int\nfrom ..helpers import eval_escapes\nfrom ..helpers import timestamp, archive_ts_now\nfrom ..helpers import get_cache_dir, os_stat, get_strip_prefix, slashify\nfrom ..helpers import dir_is_tagged\nfrom ..helpers import log_multi\nfrom ..helpers import basic_json_data, json_print\nfrom ..helpers import flags_dir, flags_special_follow, flags_special\nfrom ..helpers import prepare_subprocess_env\nfrom ..helpers import sig_int, ignore_sigint\nfrom ..helpers import iter_separated\nfrom ..helpers import MakePathSafeAction\nfrom ..helpers import Error, CommandError, BackupWarning, FileChangedWarning\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\nfrom ..patterns import PatternMatcher\nfrom ..platform import is_win32\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass CreateMixIn:\n    @with_repository(compatibility=(Manifest.Operation.WRITE,))\n    def do_create(self, args, repository, manifest):\n        \"\"\"Creates a new archive.\"\"\"\n        key = manifest.key\n        matcher = PatternMatcher(fallback=True)\n        matcher.add_inclexcl(args.patterns)\n\n        def create_inner(archive, cache, fso):\n            # Add cache dir to inode_skip list\n            skip_inodes = set()\n            try:\n                st = os.stat(get_cache_dir())\n                skip_inodes.add((st.st_ino, st.st_dev))\n            except OSError:\n                pass\n            # Add local repository dir to inode_skip list\n            if not args.location.host:\n                try:\n                    st = os.stat(args.location.path)\n                    skip_inodes.add((st.st_ino, st.st_dev))\n                except OSError:\n                    pass\n            logger.debug(\"Processing files ...\")\n            if args.content_from_command:\n                path = args.stdin_name\n                mode = args.stdin_mode\n                user = args.stdin_user\n                group = args.stdin_group\n                if not dry_run:\n                    try:\n                        try:\n                            env = prepare_subprocess_env(system=True)\n                            proc = subprocess.Popen(  # nosec B603\n                                args.paths,\n                                stdout=subprocess.PIPE,\n                                env=env,\n                                preexec_fn=None if is_win32 else ignore_sigint,\n                            )\n                        except (FileNotFoundError, PermissionError) as e:\n                            raise CommandError(f\"Failed to execute command: {e}\")\n                        status = fso.process_pipe(\n                            path=path, cache=cache, fd=proc.stdout, mode=mode, user=user, group=group\n                        )\n                        rc = proc.wait()\n                        if rc != 0:\n                            raise CommandError(f\"Command {args.paths[0]!r} exited with status {rc}\")\n                    except BackupError as e:\n                        raise Error(f\"{path!r}: {e}\")\n                else:\n                    status = \"+\"  # included\n                self.print_file_status(status, path)\n            elif args.paths_from_command or args.paths_from_shell_command or args.paths_from_stdin:\n                paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else \"\\n\"\n                if args.paths_from_command or args.paths_from_shell_command:\n                    try:\n                        env = prepare_subprocess_env(system=True)\n                        if args.paths_from_shell_command:\n                            # Use shell=True to support pipes, redirection, etc.\n                            shell = True\n                            cmd = \" \".join(args.paths)\n                        else:\n                            shell = False\n                            cmd = args.paths\n                        proc = subprocess.Popen(\n                            cmd,\n                            stdout=subprocess.PIPE,\n                            env=env,\n                            shell=shell,  # nosec B602\n                            preexec_fn=None if is_win32 else ignore_sigint,\n                        )\n                    except (FileNotFoundError, PermissionError) as e:\n                        raise CommandError(f\"Failed to execute command: {e}\")\n                    pipe_bin = proc.stdout\n                else:  # args.paths_from_stdin == True\n                    pipe_bin = sys.stdin.buffer\n                pipe = TextIOWrapper(pipe_bin, errors=\"surrogateescape\")\n                for path in iter_separated(pipe, paths_sep):\n                    path = slashify(path)\n                    strip_prefix = get_strip_prefix(path)\n                    path = posixpath.normpath(path)\n                    try:\n                        with backup_io(\"stat\"):\n                            st = os_stat(path=path, parent_fd=None, name=None, follow_symlinks=False)\n                        status = self._process_any(\n                            path=path,\n                            parent_fd=None,\n                            name=None,\n                            st=st,\n                            fso=fso,\n                            cache=cache,\n                            read_special=args.read_special,\n                            dry_run=dry_run,\n                            strip_prefix=strip_prefix,\n                        )\n                    except BackupError as e:\n                        self.print_warning_instance(BackupWarning(path, e))\n                        status = \"E\"\n                    if status == \"C\":\n                        self.print_warning_instance(FileChangedWarning(path))\n                    self.print_file_status(status, path)\n                    if not dry_run and status is not None:\n                        fso.stats.files_stats[status] += 1\n                if args.paths_from_command or args.paths_from_shell_command:\n                    rc = proc.wait()\n                    if rc != 0:\n                        raise CommandError(f\"Command {args.paths[0]!r} exited with status {rc}\")\n            else:\n                paths = list(args.pattern_roots) + list(args.paths)\n                for path in paths:\n                    if path == \"\":  # issue #5637\n                        self.print_warning(\"An empty string was given as PATH, ignoring.\")\n                        continue\n                    if path == \"-\":  # stdin\n                        path = args.stdin_name\n                        mode = args.stdin_mode\n                        user = args.stdin_user\n                        group = args.stdin_group\n                        if not dry_run:\n                            try:\n                                status = fso.process_pipe(\n                                    path=path, cache=cache, fd=sys.stdin.buffer, mode=mode, user=user, group=group\n                                )\n                            except BackupError as e:\n                                self.print_warning_instance(BackupWarning(path, e))\n                                status = \"E\"\n                        else:\n                            status = \"+\"  # included\n                        self.print_file_status(status, path)\n                        if not dry_run and status is not None:\n                            fso.stats.files_stats[status] += 1\n                        continue\n\n                    strip_prefix = get_strip_prefix(path)\n                    path = posixpath.normpath(path)\n                    try:\n                        with backup_io(\"stat\"):\n                            st = os_stat(path=path, parent_fd=None, name=None, follow_symlinks=False)\n                        restrict_dev = st.st_dev if args.one_file_system else None\n                        self._rec_walk(\n                            path=path,\n                            parent_fd=None,\n                            name=None,\n                            fso=fso,\n                            cache=cache,\n                            matcher=matcher,\n                            exclude_caches=args.exclude_caches,\n                            exclude_if_present=args.exclude_if_present,\n                            keep_exclude_tags=args.keep_exclude_tags,\n                            skip_inodes=skip_inodes,\n                            restrict_dev=restrict_dev,\n                            read_special=args.read_special,\n                            dry_run=dry_run,\n                            strip_prefix=strip_prefix,\n                        )\n                        # if we get back here, we've finished recursing into <path>,\n                        # we do not ever want to get back in there (even if path is given twice as recursion root)\n                        skip_inodes.add((st.st_ino, st.st_dev))\n                    except BackupError as e:\n                        # this comes from os.stat, self._rec_walk has own exception handler\n                        self.print_warning_instance(BackupWarning(path, e))\n                        continue\n            if not dry_run:\n                if args.progress:\n                    archive.stats.show_progress(final=True)\n                archive.stats += fso.stats\n                archive.stats.rx_bytes = getattr(repository, \"rx_bytes\", 0)\n                archive.stats.tx_bytes = getattr(repository, \"tx_bytes\", 0)\n                if sig_int:\n                    # do not save the archive if the user ctrl-c-ed.\n                    raise Error(\"Got Ctrl-C / SIGINT.\")\n                else:\n                    archive.tags = set(args.tags or [])\n                    archive.save(comment=args.comment, timestamp=args.timestamp)\n                    args.stats |= args.json\n                    if args.stats:\n                        if args.json:\n                            json_print(basic_json_data(manifest, cache=cache, extra={\"archive\": archive}))\n                        else:\n                            log_multi(str(archive), str(archive.stats), logger=logging.getLogger(\"borg.output.stats\"))\n\n        self.output_filter = args.output_filter\n        self.output_list = args.output_list\n        self.noflags = args.noflags\n        self.noacls = args.noacls\n        self.noxattrs = args.noxattrs\n        dry_run = args.dry_run\n        self.start_backup = time.time_ns()\n        t0 = archive_ts_now()\n        logger.info('Creating archive at \"%s\"' % args.location.processed)\n        if not dry_run:\n            with Cache(\n                repository,\n                manifest,\n                progress=args.progress,\n                cache_mode=args.files_cache_mode,\n                iec=args.iec,\n                archive_name=args.name,\n            ) as cache:\n                archive = Archive(\n                    manifest,\n                    args.name,\n                    cache=cache,\n                    create=True,\n                    numeric_ids=args.numeric_ids,\n                    noatime=not args.atime,\n                    noctime=args.noctime,\n                    progress=args.progress,\n                    chunker_params=args.chunker_params,\n                    start=t0,\n                    log_json=args.log_json,\n                    iec=args.iec,\n                    hostname=args.hostname,\n                    username=args.username,\n                )\n                metadata_collector = MetadataCollector(\n                    noatime=not args.atime,\n                    noctime=args.noctime,\n                    noflags=args.noflags,\n                    noacls=args.noacls,\n                    noxattrs=args.noxattrs,\n                    numeric_ids=args.numeric_ids,\n                    nobirthtime=args.nobirthtime,\n                )\n                cp = ChunksProcessor(cache=cache, key=key, add_item=archive.add_item, rechunkify=False)\n                if is_win32 and args.files_changed == \"ctime\":\n                    self.print_warning(\n                        \"--files-changed=ctime is not supported on Windows \"\n                        \"(ctime is file creation time, not change time). Using mtime instead.\",\n                        wc=None,\n                    )\n                    args.files_changed = \"mtime\"\n                fso = FilesystemObjectProcessors(\n                    metadata_collector=metadata_collector,\n                    cache=cache,\n                    key=key,\n                    process_file_chunks=cp.process_file_chunks,\n                    add_item=archive.add_item,\n                    chunker_params=args.chunker_params,\n                    show_progress=args.progress,\n                    sparse=args.sparse,\n                    log_json=args.log_json,\n                    iec=args.iec,\n                    file_status_printer=self.print_file_status,\n                    files_changed=args.files_changed,\n                )\n                create_inner(archive, cache, fso)\n        else:\n            create_inner(None, None, None)\n\n    def _process_any(self, *, path, parent_fd, name, st, fso, cache, read_special, dry_run, strip_prefix):\n        \"\"\"\n        Call the right method on the given FilesystemObjectProcessor.\n        \"\"\"\n\n        if dry_run:\n            return \"+\"  # included\n        MAX_RETRIES = 10  # count includes the initial try (initial try == \"retry 0\")\n        for retry in range(MAX_RETRIES):\n            last_try = retry == MAX_RETRIES - 1\n            try:\n                if stat.S_ISREG(st.st_mode):\n                    return fso.process_file(\n                        path=path,\n                        parent_fd=parent_fd,\n                        name=name,\n                        st=st,\n                        cache=cache,\n                        last_try=last_try,\n                        strip_prefix=strip_prefix,\n                    )\n                elif stat.S_ISDIR(st.st_mode):\n                    return fso.process_dir(path=path, parent_fd=parent_fd, name=name, st=st, strip_prefix=strip_prefix)\n                elif stat.S_ISLNK(st.st_mode):\n                    if not read_special:\n                        return fso.process_symlink(\n                            path=path, parent_fd=parent_fd, name=name, st=st, strip_prefix=strip_prefix\n                        )\n                    else:\n                        try:\n                            st_target = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=True)\n                        except OSError:\n                            special = False\n                        else:\n                            special = is_special(st_target.st_mode)\n                        if special:\n                            return fso.process_file(\n                                path=path,\n                                parent_fd=parent_fd,\n                                name=name,\n                                st=st_target,\n                                cache=cache,\n                                flags=flags_special_follow,\n                                last_try=last_try,\n                                strip_prefix=strip_prefix,\n                            )\n                        else:\n                            return fso.process_symlink(\n                                path=path, parent_fd=parent_fd, name=name, st=st, strip_prefix=strip_prefix\n                            )\n                elif stat.S_ISFIFO(st.st_mode):\n                    if not read_special:\n                        return fso.process_fifo(\n                            path=path, parent_fd=parent_fd, name=name, st=st, strip_prefix=strip_prefix\n                        )\n                    else:\n                        return fso.process_file(\n                            path=path,\n                            parent_fd=parent_fd,\n                            name=name,\n                            st=st,\n                            cache=cache,\n                            flags=flags_special,\n                            last_try=last_try,\n                            strip_prefix=strip_prefix,\n                        )\n                elif stat.S_ISCHR(st.st_mode):\n                    if not read_special:\n                        return fso.process_dev(\n                            path=path, parent_fd=parent_fd, name=name, st=st, dev_type=\"c\", strip_prefix=strip_prefix\n                        )\n                    else:\n                        return fso.process_file(\n                            path=path,\n                            parent_fd=parent_fd,\n                            name=name,\n                            st=st,\n                            cache=cache,\n                            flags=flags_special,\n                            last_try=last_try,\n                            strip_prefix=strip_prefix,\n                        )\n                elif stat.S_ISBLK(st.st_mode):\n                    if not read_special:\n                        return fso.process_dev(\n                            path=path, parent_fd=parent_fd, name=name, st=st, dev_type=\"b\", strip_prefix=strip_prefix\n                        )\n                    else:\n                        return fso.process_file(\n                            path=path,\n                            parent_fd=parent_fd,\n                            name=name,\n                            st=st,\n                            cache=cache,\n                            flags=flags_special,\n                            last_try=last_try,\n                            strip_prefix=strip_prefix,\n                        )\n                elif stat.S_ISSOCK(st.st_mode):\n                    # Ignore unix sockets\n                    return\n                elif stat.S_ISDOOR(st.st_mode):\n                    # Ignore Solaris doors\n                    return\n                elif stat.S_ISPORT(st.st_mode):\n                    # Ignore Solaris event ports\n                    return\n                else:\n                    self.print_warning(\"Unknown file type: %s\", path)\n                    return\n            except BackupItemExcluded:\n                return \"-\"\n            except BackupError as err:\n                if isinstance(err, BackupOSError):\n                    if err.errno in (errno.EPERM, errno.EACCES):\n                        # Do not try again, such errors can not be fixed by retrying.\n                        raise\n                # sleep a bit, so temporary problems might go away...\n                sleep_s = 1000.0 / 1e6 * 10 ** (retry / 2)  # retry 0: 1ms, retry 6: 1s, ...\n                time.sleep(sleep_s)\n                if retry < MAX_RETRIES - 1:\n                    logger.warning(\n                        f\"{path}: {err}, slept {sleep_s:.3f}s, next: retry: {retry + 1} of {MAX_RETRIES - 1}...\"\n                    )\n                else:\n                    # giving up with retries, error will be dealt with (logged) by upper error handler\n                    raise\n                # we better do a fresh stat on the file, just to make sure to get the current file\n                # mode right (which could have changed due to a race condition and is important for\n                # dispatching) and also to get current inode number of that file.\n                with backup_io(\"stat\"):\n                    st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)\n\n    def _rec_walk(\n        self,\n        *,\n        path,\n        parent_fd,\n        name,\n        fso,\n        cache,\n        matcher,\n        exclude_caches,\n        exclude_if_present,\n        keep_exclude_tags,\n        skip_inodes,\n        restrict_dev,\n        read_special,\n        dry_run,\n        strip_prefix,\n    ):\n        \"\"\"\n        Process *path* (or, preferably, parent_fd/name) recursively according to the various parameters.\n\n        This should only raise on critical errors. Per-item errors must be handled within this method.\n        \"\"\"\n        if sig_int and sig_int.action_done():\n            # the user says \"get out of here!\" and we have already completed the desired action.\n            return\n\n        status = None\n        try:\n            recurse_excluded_dir = False\n            if matcher.match(path):\n                with backup_io(\"stat\"):\n                    st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)\n            else:\n                self.print_file_status(\"-\", path)  # excluded\n                # get out here as quickly as possible:\n                # we only need to continue if we shall recurse into an excluded directory.\n                # if we shall not recurse, then do not even touch (stat()) the item, it\n                # could trigger an error, e.g. if access is forbidden, see #3209.\n                if not matcher.recurse_dir:\n                    return\n                recurse_excluded_dir = True\n                with backup_io(\"stat\"):\n                    st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)\n                if not stat.S_ISDIR(st.st_mode):\n                    return\n\n            if (st.st_ino, st.st_dev) in skip_inodes:\n                return\n            # if restrict_dev is given, we do not want to recurse into a new filesystem,\n            # but we WILL save the mountpoint directory (or more precise: the root\n            # directory of the mounted filesystem that shadows the mountpoint dir).\n            recurse = restrict_dev is None or st.st_dev == restrict_dev\n\n            if not stat.S_ISDIR(st.st_mode):\n                # directories cannot go in this branch because they can be excluded based on tag\n                # files they might contain\n                status = self._process_any(\n                    path=path,\n                    parent_fd=parent_fd,\n                    name=name,\n                    st=st,\n                    fso=fso,\n                    cache=cache,\n                    read_special=read_special,\n                    dry_run=dry_run,\n                    strip_prefix=strip_prefix,\n                )\n            else:\n                with OsOpen(\n                    path=path, parent_fd=parent_fd, name=name, flags=flags_dir, noatime=True, op=\"dir_open\"\n                ) as child_fd:\n                    # child_fd is None for directories on windows, in that case a race condition check is not possible.\n                    if child_fd is not None:\n                        with backup_io(\"fstat\"):\n                            st = stat_update_check(st, os.fstat(child_fd))\n                    if recurse:\n                        tag_names = dir_is_tagged(path, exclude_caches, exclude_if_present, dir_fd=child_fd)\n                        if tag_names:\n                            # if we are already recursing in an excluded dir, we do not need to do anything else than\n                            # returning (we do not need to archive or recurse into tagged directories), see #3991:\n                            if not recurse_excluded_dir:\n                                if keep_exclude_tags:\n                                    if not dry_run:\n                                        fso.process_dir_with_fd(\n                                            path=path, fd=child_fd, st=st, strip_prefix=strip_prefix\n                                        )\n                                    for tag_name in tag_names:\n                                        tag_path = posixpath.join(path, tag_name)\n                                        self._rec_walk(\n                                            path=tag_path,\n                                            parent_fd=child_fd,\n                                            name=tag_name,\n                                            fso=fso,\n                                            cache=cache,\n                                            matcher=matcher,\n                                            exclude_caches=exclude_caches,\n                                            exclude_if_present=exclude_if_present,\n                                            keep_exclude_tags=keep_exclude_tags,\n                                            skip_inodes=skip_inodes,\n                                            restrict_dev=restrict_dev,\n                                            read_special=read_special,\n                                            dry_run=dry_run,\n                                            strip_prefix=strip_prefix,\n                                        )\n                                self.print_file_status(\"-\", path)  # excluded\n                            return\n                    if not recurse_excluded_dir:\n                        if not dry_run:\n                            try:\n                                status = fso.process_dir_with_fd(\n                                    path=path, fd=child_fd, st=st, strip_prefix=strip_prefix\n                                )\n                            except BackupItemExcluded:\n                                status = \"-\"  # excluded (dir)\n                                recurse = False\n                        else:\n                            status = \"+\"  # included (dir)\n                    if recurse:\n                        with backup_io(\"scandir\"):\n                            entries = helpers.scandir_inorder(path=path, fd=child_fd)\n                        for dirent in entries:\n                            normpath = posixpath.normpath(posixpath.join(path, dirent.name))\n                            self._rec_walk(\n                                path=normpath,\n                                parent_fd=child_fd,\n                                name=dirent.name,\n                                fso=fso,\n                                cache=cache,\n                                matcher=matcher,\n                                exclude_caches=exclude_caches,\n                                exclude_if_present=exclude_if_present,\n                                keep_exclude_tags=keep_exclude_tags,\n                                skip_inodes=skip_inodes,\n                                restrict_dev=restrict_dev,\n                                read_special=read_special,\n                                dry_run=dry_run,\n                                strip_prefix=strip_prefix,\n                            )\n\n        except BackupError as e:\n            self.print_warning_instance(BackupWarning(path, e))\n            status = \"E\"\n        if status == \"C\":\n            self.print_warning_instance(FileChangedWarning(path))\n        if not recurse_excluded_dir:\n            self.print_file_status(status, path)\n            if not dry_run and status is not None:\n                fso.stats.files_stats[status] += 1\n\n    def build_parser_create(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_exclusion_group\n\n        create_epilog = process_epilog(\n            \"\"\"\n        This command creates a backup archive containing all files found while recursively\n        traversing all specified paths. Paths are added to the archive as they are given,\n        which means that if relative paths are desired, the command must be run from the correct\n        directory.\n\n        The slashdot hack in paths (recursion roots) is triggered by using ``/./``:\n        ``/this/gets/stripped/./this/gets/archived`` means to process that fs object, but\n        strip the prefix on the left side of ``./`` from the archived items (in this case,\n        ``this/gets/archived`` will be the path in the archived item).\n\n        When specifying '-' as a path, borg will read data from standard input and create a\n        file named 'stdin' in the created archive from that data. In some cases, it is more\n        appropriate to use --content-from-command. See the section *Reading from stdin*\n        below for details.\n\n        The archive will consume almost no disk space for files or parts of files that\n        have already been stored in other archives.\n\n        The ``--tags`` option can be used to add a list of tags to the new archive.\n\n        The archive name does not need to be unique; you can and should use the same\n        name for a series of archives. The unique archive identifier is its ID (hash),\n        and you can abbreviate the ID as long as it is unique.\n\n        In the archive name, you may use the following placeholders:\n        {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.\n\n        Backup speed is increased by not reprocessing files that are already part of\n        existing archives and were not modified. The detection of unmodified files is\n        done by comparing multiple file metadata values with previous values kept in\n        the files cache.\n\n        This comparison can operate in different modes as given by ``--files-cache``:\n\n        - ctime,size,inode (default)\n        - mtime,size,inode (default behaviour of borg versions older than 1.1.0rc4)\n        - ctime,size (ignore the inode number)\n        - mtime,size (ignore the inode number)\n        - rechunk,ctime (all files are considered modified - rechunk, cache ctime)\n        - rechunk,mtime (all files are considered modified - rechunk, cache mtime)\n        - disabled (disable the files cache, all files considered modified - rechunk)\n\n        inode number: better safety, but often unstable on network filesystems\n\n        Normally, detecting file modifications will take inode information into\n        consideration to improve the reliability of file change detection.\n        This is problematic for files located on sshfs and similar network file\n        systems which do not provide stable inode numbers, such files will always\n        be considered modified. You can use modes without `inode` in this case to\n        improve performance, but reliability of change detection might be reduced.\n\n        ctime vs. mtime: safety vs. speed\n\n        - ctime is a rather safe way to detect changes to a file (metadata and contents)\n          as it cannot be set from userspace. But a metadata-only change will already\n          update the ctime, so there might be some unnecessary chunking/hashing even\n          without content changes. Some filesystems do not support ctime (change time).\n          E.g. doing a chown or chmod to a file will change its ctime.\n        - mtime usually works and only updates if file contents were changed. But mtime\n          can be arbitrarily set from userspace, e.g., to set mtime back to the same value\n          it had before a content change happened. This can be used maliciously as well as\n          well-meant, but in both cases mtime-based cache modes can be problematic.\n\n        The ``--files-changed`` option controls how Borg detects if a file has changed during backup:\n         - ctime (default on POSIX): Use ctime to detect changes. This is the safest option.\n           Not supported on Windows (ctime is file creation time there).\n         - mtime (default on Windows): Use mtime to detect changes.\n         - disabled: Disable the \"file has changed while we backed it up\" detection completely.\n           This is not recommended unless you know what you're doing, as it could lead to\n           inconsistent backups if files change during the backup process.\n\n        The mount points of filesystems or filesystem snapshots should be the same for every\n        creation of a new archive to ensure fast operation. This is because the file cache that\n        is used to determine changed files quickly uses absolute filenames.\n        If this is not possible, consider creating a bind mount to a stable location.\n\n        The ``--progress`` option shows (from left to right) Original and (uncompressed)\n        deduplicated size (O and U respectively), then the Number of files (N) processed so far,\n        followed by the currently processed path.\n\n        When using ``--stats``, you will get some statistics about how much data was\n        added - the \"This Archive\" deduplicated size there is most interesting as that is\n        how much your repository will grow. Please note that the \"All archives\" stats refer to\n        the state after creation. Also, the ``--stats`` and ``--dry-run`` options are mutually\n        exclusive because the data is not actually compressed and deduplicated during a dry run.\n\n        For more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\n        For more help on placeholders, see the :ref:`borg_placeholders` command output.\n\n        .. man NOTES\n\n        The ``--exclude`` patterns are not like tar. In tar ``--exclude`` .bundler/gems will\n        exclude foo/.bundler/gems. In borg it will not, you need to use ``--exclude``\n        '\\\\*/.bundler/gems' to get the same effect.\n\n        In addition to using ``--exclude`` patterns, it is possible to use\n        ``--exclude-if-present`` to specify the name of a filesystem object (e.g. a file\n        or folder name) which, when contained within another folder, will prevent the\n        containing folder from being backed up.  By default, the containing folder and\n        all of its contents will be omitted from the backup.  If, however, you wish to\n        only include the objects specified by ``--exclude-if-present`` in your backup,\n        and not include any other contents of the containing folder, this can be enabled\n        through using the ``--keep-exclude-tags`` option.\n\n        The ``-x`` or ``--one-file-system`` option excludes directories, that are mountpoints (and everything in them).\n        It detects mountpoints by comparing the device number from the output of ``stat()`` of the directory and its\n        parent directory. Specifically, it excludes directories for which ``stat()`` reports a device number different\n        from the device number of their parent.\n        In general: be aware that there are directories with device number different from their parent, which the kernel\n        does not consider a mountpoint and also the other way around.\n        Linux examples for this are bind mounts (possibly same device number, but always a mountpoint) and ALL\n        subvolumes of a btrfs (different device number from parent but not necessarily a mountpoint).\n        macOS examples are the apfs mounts of a typical macOS installation.\n        Therefore, when using ``--one-file-system``, you should double-check that the backup works as intended.\n\n        .. _list_item_flags:\n\n        Item flags\n        ++++++++++\n\n        ``--list`` outputs a list of all files, directories and other\n        file system items it considered (no matter whether they had content changes\n        or not). For each item, it prefixes a single-letter flag that indicates type\n        and/or status of the item.\n\n        If you are interested only in a subset of that output, you can give e.g.\n        ``--filter=AME`` and it will only show regular files with A, M or E status (see\n        below).\n\n        A uppercase character represents the status of a regular file relative to the\n        \"files\" cache (not relative to the repo -- this is an issue if the files cache\n        is not used). Metadata is stored in any case and for 'A' and 'M' also new data\n        chunks are stored. For 'U' all data chunks refer to already existing chunks.\n\n        - 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ)\n        - 'M' = regular file, modified\n        - 'U' = regular file, unchanged\n        - 'C' = regular file, it changed while we backed it up\n        - 'E' = regular file, an error happened while accessing/reading *this* file\n\n        A lowercase character means a file type other than a regular file,\n        borg usually just stores their metadata:\n\n        - 'd' = directory\n        - 'b' = block device\n        - 'c' = char device\n        - 'h' = regular file, hard link (to already seen inodes)\n        - 's' = symlink\n        - 'f' = fifo\n\n        Other flags used include:\n\n        - '+' = included, item would be backed up (if not in dry-run mode)\n        - '-' = excluded, item would not be / was not backed up\n        - 'i' = backup data was read from standard input (stdin)\n        - '?' = missing status code (if you see this, please file a bug report!)\n\n        Reading backup data from stdin\n        ++++++++++++++++++++++++++++++\n\n        There are two methods to read from stdin. Either specify ``-`` as path and\n        pipe directly to borg::\n\n            backup-vm --id myvm --stdout | borg create --repo REPO ARCHIVE -\n\n        Or use ``--content-from-command`` to have Borg manage the execution of the\n        command and piping. If you do so, the first PATH argument is interpreted\n        as command to execute and any further arguments are treated as arguments\n        to the command::\n\n            borg create --content-from-command --repo REPO ARCHIVE -- backup-vm --id myvm --stdout\n\n        ``--`` is used to ensure ``--id`` and ``--stdout`` are **not** considered\n        arguments to ``borg`` but rather ``backup-vm``.\n\n        The difference between the two approaches is that piping to borg creates an\n        archive even if the command piping to borg exits with a failure. In this case,\n        **one can end up with truncated output being backed up**. Using\n        ``--content-from-command``, in contrast, borg is guaranteed to fail without\n        creating an archive should the command fail. The command is considered failed\n        when it returned a non-zero exit code.\n\n        Reading from stdin yields just a stream of data without file metadata\n        associated with it, and the files cache is not needed at all. So it is\n        safe to disable it via ``--files-cache disabled`` and speed up backup\n        creation a bit.\n\n        By default, the content read from stdin is stored in a file called 'stdin'.\n        Use ``--stdin-name`` to change the name.\n\n        Feeding all file paths from externally\n        ++++++++++++++++++++++++++++++++++++++\n\n        Usually, you give a starting path (recursion root) to borg and then borg\n        automatically recurses, finds and backs up all fs objects contained in\n        there (optionally considering include/exclude rules).\n\n        If you need more control and you want to give every single fs object path\n        to borg (maybe implementing your own recursion or your own rules), you can use\n        ``--paths-from-stdin``, ``--paths-from-command`` or ``--paths-from-shell-command``\n        (with the latter two, borg will fail to create an archive should the command fail).\n\n        Borg supports paths with the slashdot hack to strip path prefixes here also.\n        So, be careful not to unintentionally trigger that.\n        \"\"\"\n        )\n\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_create.__doc__, epilog=create_epilog)\n        subparsers.add_subcommand(\"create\", subparser, help=\"create a backup\")\n\n        # note: --dry-run and --stats are mutually exclusive, but we do not want to abort when\n        #  parsing, but rather proceed with the dry-run, but without stats (see run() method).\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not create a backup archive\"\n        )\n        subparser.add_argument(\n            \"-s\", \"--stats\", dest=\"stats\", action=\"store_true\", help=\"print statistics for the created archive\"\n        )\n\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of items (files, dirs, ...)\"\n        )\n        subparser.add_argument(\n            \"--filter\",\n            metavar=\"STATUSCHARS\",\n            dest=\"output_filter\",\n            action=Highlander,\n            help=\"only display items with the given status characters (see description)\",\n        )\n        subparser.add_argument(\"--json\", action=\"store_true\", help=\"output stats as JSON. Implies ``--stats``.\")\n        subparser.add_argument(\n            \"--stdin-name\",\n            metavar=\"NAME\",\n            dest=\"stdin_name\",\n            default=\"stdin\",\n            action=MakePathSafeAction,\n            help=\"use NAME in archive for stdin data (default: %(default)r)\",\n        )\n        subparser.add_argument(\n            \"--stdin-user\",\n            metavar=\"USER\",\n            dest=\"stdin_user\",\n            default=None,\n            action=Highlander,\n            help=\"set user USER in archive for stdin data (default: do not store user/uid)\",\n        )\n        subparser.add_argument(\n            \"--stdin-group\",\n            metavar=\"GROUP\",\n            dest=\"stdin_group\",\n            default=None,\n            action=Highlander,\n            help=\"set group GROUP in archive for stdin data (default: do not store group/gid)\",\n        )\n        subparser.add_argument(\n            \"--stdin-mode\",\n            metavar=\"M\",\n            dest=\"stdin_mode\",\n            type=octal_int,\n            default=STDIN_MODE_DEFAULT,\n            action=Highlander,\n            help=\"set mode to M in archive for stdin data (default: %(default)04o)\",\n        )\n        subparser.add_argument(\n            \"--content-from-command\",\n            action=\"store_true\",\n            help=\"interpret PATH as a command and store its stdout. See also the section 'Reading from stdin' below.\",\n        )\n        subparser.add_argument(\n            \"--paths-from-stdin\",\n            action=\"store_true\",\n            help=\"read DELIM-separated list of paths to back up from stdin. All control is external: it will back\"\n            \" up all files given - no more, no less.\",\n        )\n        subparser.add_argument(\n            \"--paths-from-command\",\n            action=\"store_true\",\n            help=\"interpret PATH as command and treat its output as ``--paths-from-stdin``\",\n        )\n        subparser.add_argument(\n            \"--paths-from-shell-command\",\n            action=\"store_true\",\n            help=\"interpret PATH as shell command and treat its output as ``--paths-from-stdin``\",\n        )\n        subparser.add_argument(\n            \"--paths-delimiter\",\n            action=Highlander,\n            metavar=\"DELIM\",\n            help=\"set path delimiter for ``--paths-from-stdin`` and ``--paths-from-command`` (default: ``\\\\n``) \",\n        )\n\n        define_exclusion_group(subparser, tag_files=True)\n\n        fs_group = subparser.add_argument_group(\"Filesystem options\")\n        fs_group.add_argument(\n            \"-x\",\n            \"--one-file-system\",\n            dest=\"one_file_system\",\n            action=\"store_true\",\n            help=\"stay in the same file system and do not store mount points of other file systems - \"\n            \"this might behave different from your expectations, see the description below.\",\n        )\n        fs_group.add_argument(\n            \"--numeric-ids\",\n            dest=\"numeric_ids\",\n            action=\"store_true\",\n            help=\"only store numeric user and group identifiers\",\n        )\n        fs_group.add_argument(\"--atime\", dest=\"atime\", action=\"store_true\", help=\"do store atime into archive\")\n        fs_group.add_argument(\"--noctime\", dest=\"noctime\", action=\"store_true\", help=\"do not store ctime into archive\")\n        fs_group.add_argument(\n            \"--nobirthtime\",\n            dest=\"nobirthtime\",\n            action=\"store_true\",\n            help=\"do not store birthtime (creation date) into archive\",\n        )\n        fs_group.add_argument(\n            \"--noflags\",\n            dest=\"noflags\",\n            action=\"store_true\",\n            help=\"do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive\",\n        )\n        fs_group.add_argument(\n            \"--noacls\", dest=\"noacls\", action=\"store_true\", help=\"do not read and store ACLs into archive\"\n        )\n        fs_group.add_argument(\n            \"--noxattrs\", dest=\"noxattrs\", action=\"store_true\", help=\"do not read and store xattrs into archive\"\n        )\n        fs_group.add_argument(\n            \"--sparse\",\n            dest=\"sparse\",\n            action=\"store_true\",\n            help=\"detect sparse holes in input (supported only by fixed chunker)\",\n        )\n        fs_group.add_argument(\n            \"--files-cache\",\n            metavar=\"MODE\",\n            dest=\"files_cache_mode\",\n            action=Highlander,\n            type=FilesCacheMode,\n            default=FILES_CACHE_MODE_UI_DEFAULT,\n            help=\"operate files cache in MODE. default: %s\" % FILES_CACHE_MODE_UI_DEFAULT,\n        )\n        fs_group.add_argument(\n            \"--files-changed\",\n            metavar=\"MODE\",\n            dest=\"files_changed\",\n            action=Highlander,\n            choices=[\"ctime\", \"mtime\", \"disabled\"],\n            default=\"mtime\" if is_win32 else \"ctime\",\n            help=\"specify how to detect if a file has changed during backup (ctime, mtime, disabled). \"\n            \"default: ctime (on Windows: mtime, because ctime is file creation time there).\",\n        )\n        fs_group.add_argument(\n            \"--read-special\",\n            dest=\"read_special\",\n            action=\"store_true\",\n            help=\"open and read block and char device files as well as FIFOs as if they were \"\n            \"regular files. Also follows symlinks pointing to these kinds of files.\",\n        )\n\n        archive_group = subparser.add_argument_group(\"Archive options\")\n        archive_group.add_argument(\n            \"--comment\",\n            metavar=\"COMMENT\",\n            dest=\"comment\",\n            type=comment_validator,\n            default=\"\",\n            action=Highlander,\n            help=\"add a comment text to the archive\",\n        )\n        archive_group.add_argument(\n            \"--timestamp\",\n            metavar=\"TIMESTAMP\",\n            dest=\"timestamp\",\n            type=timestamp,\n            default=None,\n            action=Highlander,\n            help=\"manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, \"\n            \"(+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\",\n        )\n        archive_group.add_argument(\n            \"--chunker-params\",\n            metavar=\"PARAMS\",\n            dest=\"chunker_params\",\n            type=ChunkerParams,\n            default=CHUNKER_PARAMS,\n            action=Highlander,\n            help=\"specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, \"\n            \"HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %s,%d,%d,%d,%d\" % CHUNKER_PARAMS,\n        )\n        archive_group.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n        archive_group.add_argument(\n            \"--hostname\",\n            metavar=\"HOSTNAME\",\n            dest=\"hostname\",\n            type=str,\n            default=None,\n            action=Highlander,\n            help=\"explicitly set hostname for the archive\",\n        )\n        archive_group.add_argument(\n            \"--username\",\n            metavar=\"USERNAME\",\n            dest=\"username\",\n            type=str,\n            default=None,\n            action=Highlander,\n            help=\"explicitly set username for the archive\",\n        )\n        archive_group.add_argument(\n            \"--tags\",\n            metavar=\"TAG\",\n            dest=\"tags\",\n            type=helpers.tag_validator,\n            nargs=\"+\",\n            help=\"add tags to archive (comma-separated or multiple arguments)\",\n        )\n\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=FilesystemPathSpec, action=\"extend\", help=\"paths to archive\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/debug_cmd.py",
    "content": "import json\nimport textwrap\n\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import msgpack\nfrom ..helpers import sysinfo\nfrom ..helpers import bin_to_hex, hex_to_bin, prepare_dump_dict\nfrom ..helpers import dash_open\nfrom ..helpers import StableDict\nfrom ..helpers import archivename_validator, CompressionSpec\nfrom ..helpers import CommandError, RTError\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\nfrom ..platform import get_process_id\nfrom ..repository import Repository, LIST_SCAN_LIMIT, repo_lister\nfrom ..repoobj import RepoObj\n\nfrom ._common import with_repository, Highlander\nfrom ._common import process_epilog\n\n\nclass DebugMixIn:\n    def do_debug_info(self, args):\n        \"\"\"Displays system information for debugging and bug reports.\"\"\"\n        print(sysinfo())\n        print(\"Process ID:\", get_process_id())\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_dump_archive_items(self, args, repository, manifest):\n        \"\"\"Dumps (decrypted, decompressed) archive item metadata (not data).\"\"\"\n        repo_objs = manifest.repo_objs\n        archive_info = manifest.archives.get_one([args.name])\n        archive = Archive(manifest, archive_info.id)\n        for i, item_id in enumerate(archive.metadata.items):\n            _, data = repo_objs.parse(item_id, repository.get(item_id), ro_type=ROBJ_ARCHIVE_STREAM)\n            filename = \"%06d_%s.items\" % (i, bin_to_hex(item_id))\n            print(\"Dumping\", filename)\n            with open(filename, \"wb\") as fd:\n                fd.write(data)\n        print(\"Done.\")\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_dump_archive(self, args, repository, manifest):\n        \"\"\"Dumps decoded archive metadata (not data).\"\"\"\n        archive_info = manifest.archives.get_one([args.name])\n        repo_objs = manifest.repo_objs\n        try:\n            archive_meta_orig = manifest.archives.get_by_id(archive_info.id, raw=True)\n        except KeyError:\n            raise Archive.DoesNotExist(args.name)\n\n        indent = 4\n\n        def do_indent(d):\n            return textwrap.indent(json.dumps(d, indent=indent), prefix=\" \" * indent)\n\n        def output(fd):\n            # this outputs megabytes of data for a modest sized archive, so some manual streaming json output\n            fd.write(\"{\\n\")\n            fd.write('    \"_name\": ' + json.dumps(args.name) + \",\\n\")\n            fd.write('    \"_manifest_entry\":\\n')\n            fd.write(do_indent(prepare_dump_dict(archive_meta_orig)))\n            fd.write(\",\\n\")\n\n            archive_id = archive_meta_orig[\"id\"]\n            _, data = repo_objs.parse(archive_id, repository.get(archive_id), ro_type=ROBJ_ARCHIVE_META)\n            archive_org_dict = msgpack.unpackb(data, object_hook=StableDict)\n\n            fd.write('    \"_meta\":\\n')\n            fd.write(do_indent(prepare_dump_dict(archive_org_dict)))\n            fd.write(\",\\n\")\n            fd.write('    \"_items\": [\\n')\n\n            unpacker = msgpack.Unpacker(use_list=False, object_hook=StableDict)\n            first = True\n            items = []\n            for chunk_id in archive_org_dict[\"item_ptrs\"]:\n                _, data = repo_objs.parse(chunk_id, repository.get(chunk_id), ro_type=ROBJ_ARCHIVE_CHUNKIDS)\n                items.extend(msgpack.unpackb(data))\n            for item_id in items:\n                _, data = repo_objs.parse(item_id, repository.get(item_id), ro_type=ROBJ_ARCHIVE_STREAM)\n                unpacker.feed(data)\n                for item in unpacker:\n                    item = prepare_dump_dict(item)\n                    if first:\n                        first = False\n                    else:\n                        fd.write(\",\\n\")\n                    fd.write(do_indent(item))\n\n            fd.write(\"\\n\")\n            fd.write(\"    ]\\n}\\n\")\n\n        with dash_open(args.path, \"w\") as fd:\n            output(fd)\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_dump_manifest(self, args, repository, manifest):\n        \"\"\"Dumps decoded repository manifest.\"\"\"\n        repo_objs = manifest.repo_objs\n        cdata = repository.get_manifest()\n        _, data = repo_objs.parse(manifest.MANIFEST_ID, cdata, ro_type=ROBJ_MANIFEST)\n\n        meta = prepare_dump_dict(msgpack.unpackb(data, object_hook=StableDict))\n\n        with dash_open(args.path, \"w\") as fd:\n            json.dump(meta, fd, indent=4)\n\n    @with_repository(manifest=False)\n    def do_debug_dump_repo_objs(self, args, repository):\n        \"\"\"Dumps (decrypted, decompressed) repository objects.\"\"\"\n        from ..crypto.key import key_factory\n\n        def decrypt_dump(id, cdata):\n            if cdata is not None:\n                _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE)\n            else:\n                _, data = {}, b\"\"\n            filename = f\"{bin_to_hex(id)}.obj\"\n            print(\"Dumping\", filename)\n            with open(filename, \"wb\") as fd:\n                fd.write(data)\n\n        # set up the key without depending on a manifest obj\n        result = repository.list(limit=1, marker=None)\n        id, _ = result[0]\n        cdata = repository.get(id)\n        key = key_factory(repository, cdata)\n        repo_objs = RepoObj(key)\n        for id, stored_size in repo_lister(repository, limit=LIST_SCAN_LIMIT):\n            cdata = repository.get(id)\n            decrypt_dump(id, cdata)\n        print(\"Done.\")\n\n    @with_repository(manifest=False)\n    def do_debug_search_repo_objs(self, args, repository):\n        \"\"\"Searches for byte sequences in repository objects; the repository index MUST be current/correct.\"\"\"\n        context = 32\n\n        def print_finding(info, wanted, data, offset):\n            before = data[offset - context : offset]\n            after = data[offset + len(wanted) : offset + len(wanted) + context]\n            print(\n                \"{}: {} {} {} == {!r} {!r} {!r}\".format(\n                    info, before.hex(), wanted.hex(), after.hex(), before, wanted, after\n                )\n            )\n\n        wanted = args.wanted\n        try:\n            if wanted.startswith(\"hex:\"):\n                wanted = hex_to_bin(wanted.removeprefix(\"hex:\"))\n            elif wanted.startswith(\"str:\"):\n                wanted = wanted.removeprefix(\"str:\").encode()\n            else:\n                raise ValueError(\"unsupported search term\")\n        except (ValueError, UnicodeEncodeError):\n            wanted = None\n        if not wanted:\n            raise CommandError(\"search term needs to be hex:123abc or str:foobar style\")\n\n        from ..crypto.key import key_factory\n\n        # set up the key without depending on a manifest obj\n        result = repository.list(limit=1, marker=None)\n        id, _ = result[0]\n        cdata = repository.get(id)\n        key = key_factory(repository, cdata)\n        repo_objs = RepoObj(key)\n\n        last_data = b\"\"\n        last_id = None\n        i = 0\n        for id, stored_size in repo_lister(repository, limit=LIST_SCAN_LIMIT):\n            cdata = repository.get(id)\n            _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE)\n\n            # try to locate wanted sequence crossing the border of last_data and data\n            boundary_data = last_data[-(len(wanted) - 1) :] + data[: len(wanted) - 1]\n            if wanted in boundary_data:\n                boundary_data = last_data[-(len(wanted) - 1 + context) :] + data[: len(wanted) - 1 + context]\n                offset = boundary_data.find(wanted)\n                info = \"%d %s | %s\" % (i, last_id.hex(), id.hex())\n                print_finding(info, wanted, boundary_data, offset)\n\n            # try to locate wanted sequence in data\n            count = data.count(wanted)\n            if count:\n                offset = data.find(wanted)  # only determine first occurrence's offset\n                info = \"%d %s #%d\" % (i, id.hex(), count)\n                print_finding(info, wanted, data, offset)\n\n            last_id, last_data = id, data\n            i += 1\n            if i % 10000 == 0:\n                print(\"%d objects processed.\" % i)\n        print(\"Done.\")\n\n    @with_repository(manifest=False)\n    def do_debug_get_obj(self, args, repository):\n        \"\"\"Gets object contents from the repository and writes them to a file.\"\"\"\n        hex_id = args.id\n        try:\n            id = hex_to_bin(hex_id, length=32)\n        except ValueError as err:\n            raise CommandError(f\"object id {hex_id} is invalid [{str(err)}].\")\n        try:\n            data = repository.get(id)\n        except Repository.ObjectNotFound:\n            raise RTError(\"object %s not found.\" % hex_id)\n        with open(args.path, \"wb\") as f:\n            f.write(data)\n        print(\"object %s fetched.\" % hex_id)\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_id_hash(self, args, repository, manifest):\n        \"\"\"Computes id-hash for file contents.\"\"\"\n        with open(args.path, \"rb\") as f:\n            data = f.read()\n        key = manifest.key\n        id = key.id_hash(data)\n        print(id.hex())\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_parse_obj(self, args, repository, manifest):\n        \"\"\"Parses a Borg object file into a metadata dict and data (decrypting, decompressing).\"\"\"\n\n        # get the object from id\n        hex_id = args.id\n        try:\n            id = hex_to_bin(hex_id, length=32)\n        except ValueError as err:\n            raise CommandError(f\"object id {hex_id} is invalid [{str(err)}].\")\n\n        with open(args.object_path, \"rb\") as f:\n            cdata = f.read()\n\n        repo_objs = manifest.repo_objs\n        meta, data = repo_objs.parse(id=id, cdata=cdata, ro_type=ROBJ_DONTCARE)\n\n        with open(args.json_path, \"w\") as f:\n            json.dump(meta, f)\n\n        with open(args.binary_path, \"wb\") as f:\n            f.write(data)\n\n    @with_repository(compatibility=Manifest.NO_OPERATION_CHECK)\n    def do_debug_format_obj(self, args, repository, manifest):\n        \"\"\"Formats file and metadata into a Borg object file.\"\"\"\n\n        # get the object from id\n        hex_id = args.id\n        try:\n            id = hex_to_bin(hex_id, length=32)\n        except ValueError as err:\n            raise CommandError(f\"object id {hex_id} is invalid [{str(err)}].\")\n\n        with open(args.binary_path, \"rb\") as f:\n            data = f.read()\n\n        with open(args.json_path) as f:\n            meta = json.load(f)\n\n        repo_objs = manifest.repo_objs\n        ro_type = meta.pop(\"type\", ROBJ_FILE_STREAM)\n        data_encrypted = repo_objs.format(id=id, meta=meta, data=data, ro_type=ro_type)\n\n        with open(args.object_path, \"wb\") as f:\n            f.write(data_encrypted)\n\n    @with_repository(manifest=False)\n    def do_debug_put_obj(self, args, repository):\n        \"\"\"Puts file contents into the repository.\"\"\"\n        with open(args.path, \"rb\") as f:\n            data = f.read()\n        hex_id = args.id\n        try:\n            id = hex_to_bin(hex_id, length=32)\n        except ValueError as err:\n            raise CommandError(f\"object id {hex_id} is invalid [{str(err)}].\")\n\n        repository.put(id, data)\n        print(\"object %s put.\" % hex_id)\n\n    @with_repository(manifest=False, exclusive=True)\n    def do_debug_delete_obj(self, args, repository):\n        \"\"\"Deletes the objects with the given IDs from the repository.\"\"\"\n        for hex_id in args.ids:\n            try:\n                id = hex_to_bin(hex_id, length=32)\n            except ValueError:\n                print(\"object id %s is invalid.\" % hex_id)\n            else:\n                try:\n                    repository.delete(id)\n                    print(\"object %s deleted.\" % hex_id)\n                except Repository.ObjectNotFound:\n                    print(\"object %s not found.\" % hex_id)\n        print(\"Done.\")\n\n    def do_debug_convert_profile(self, args):\n        \"\"\"Converts a Borg profile to a Python profile.\"\"\"\n        import marshal\n\n        with open(args.output, \"wb\") as wfd, open(args.input, \"rb\") as rfd:\n            marshal.dump(msgpack.unpack(rfd, use_list=False, raw=False), wfd)\n\n    def build_parser_debug(self, subparsers, common_parser, mid_common_parser):\n        debug_epilog = process_epilog(\n            \"\"\"\n        These commands are not intended for normal use and potentially very\n        dangerous if used incorrectly.\n\n        They exist to improve debugging capabilities without direct system access, e.g.\n        in case you ever run into some severe malfunction. Use them only if you know\n        what you are doing or if a trusted developer tells you what to do.\"\"\"\n        )\n\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=\"debugging command (not intended for normal use)\",\n            epilog=debug_epilog,\n        )\n        subparsers.add_subcommand(\"debug\", subparser, help=\"debugging command (not intended for normal use)\")\n\n        debug_parsers = subparser.add_subcommands(required=False, title=\"required arguments\", metavar=\"<command>\")\n\n        debug_info_epilog = process_epilog(\n            \"\"\"\n        This command displays some system information that might be useful for bug\n        reports and debugging problems. If a traceback happens, this information is\n        already appended at the end of the traceback.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_info.__doc__, epilog=debug_info_epilog\n        )\n        debug_parsers.add_subcommand(\"info\", subparser, help=\"show system infos for debugging / bug reports (debug)\")\n\n        debug_dump_archive_items_epilog = process_epilog(\n            \"\"\"\n        This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_dump_archive_items.__doc__,\n            epilog=debug_dump_archive_items_epilog,\n        )\n        debug_parsers.add_subcommand(\"dump-archive-items\", subparser, help=\"dump archive items (metadata) (debug)\")\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n\n        debug_dump_archive_epilog = process_epilog(\n            \"\"\"\n        This command dumps all metadata of an archive in a decoded form to a file.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_dump_archive.__doc__,\n            epilog=debug_dump_archive_epilog,\n        )\n        debug_parsers.add_subcommand(\"dump-archive\", subparser, help=\"dump decoded archive metadata (debug)\")\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\"path\", metavar=\"PATH\", type=str, help=\"file to dump data into\")\n\n        debug_dump_manifest_epilog = process_epilog(\n            \"\"\"\n        This command dumps manifest metadata of a repository in a decoded form to a file.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_dump_manifest.__doc__,\n            epilog=debug_dump_manifest_epilog,\n        )\n        debug_parsers.add_subcommand(\"dump-manifest\", subparser, help=\"dump decoded repository metadata (debug)\")\n        subparser.add_argument(\"path\", metavar=\"PATH\", type=str, help=\"file to dump data into\")\n\n        debug_dump_repo_objs_epilog = process_epilog(\n            \"\"\"\n        This command dumps raw (but decrypted and decompressed) repo objects to files.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_dump_repo_objs.__doc__,\n            epilog=debug_dump_repo_objs_epilog,\n        )\n        debug_parsers.add_subcommand(\"dump-repo-objs\", subparser, help=\"dump repo objects (debug)\")\n\n        debug_search_repo_objs_epilog = process_epilog(\n            \"\"\"\n        This command searches raw (but decrypted and decompressed) repo objects for a specific bytes sequence.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_search_repo_objs.__doc__,\n            epilog=debug_search_repo_objs_epilog,\n        )\n        debug_parsers.add_subcommand(\"search-repo-objs\", subparser, help=\"search repo objects (debug)\")\n        subparser.add_argument(\n            \"wanted\",\n            metavar=\"WANTED\",\n            type=str,\n            action=Highlander,\n            help=\"term to search the repo for, either 0x1234abcd hex term or a string\",\n        )\n        debug_id_hash_epilog = process_epilog(\n            \"\"\"\n                This command computes the id-hash for some file content.\n                \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_id_hash.__doc__, epilog=debug_id_hash_epilog\n        )\n        debug_parsers.add_subcommand(\"id-hash\", subparser, help=\"compute id-hash for some file content (debug)\")\n        subparser.add_argument(\n            \"path\", metavar=\"PATH\", type=str, help=\"content for which the id-hash shall get computed\"\n        )\n\n        # parse_obj\n        debug_parse_obj_epilog = process_epilog(\n            \"\"\"\n                This command parses the object file into metadata (as json) and uncompressed data.\n                \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_parse_obj.__doc__, epilog=debug_parse_obj_epilog\n        )\n        debug_parsers.add_subcommand(\"parse-obj\", subparser, help=\"parse borg object file into meta dict and data\")\n        subparser.add_argument(\"id\", metavar=\"ID\", type=str, help=\"hex object ID to get from the repo\")\n        subparser.add_argument(\n            \"object_path\", metavar=\"OBJECT_PATH\", type=str, help=\"path of the object file to parse data from\"\n        )\n        subparser.add_argument(\n            \"binary_path\", metavar=\"BINARY_PATH\", type=str, help=\"path of the file to write uncompressed data into\"\n        )\n        subparser.add_argument(\n            \"json_path\", metavar=\"JSON_PATH\", type=str, help=\"path of the json file to write metadata into\"\n        )\n\n        # format_obj\n        debug_format_obj_epilog = process_epilog(\n            \"\"\"\n                This command formats the file and metadata into a Borg object file.\n                \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_format_obj.__doc__, epilog=debug_format_obj_epilog\n        )\n        debug_parsers.add_subcommand(\"format-obj\", subparser, help=\"format file and metadata into a Borg object file\")\n        subparser.add_argument(\"id\", metavar=\"ID\", type=str, help=\"hex object ID to get from the repo\")\n        subparser.add_argument(\n            \"binary_path\", metavar=\"BINARY_PATH\", type=str, help=\"path of the file to convert into an object file\"\n        )\n        subparser.add_argument(\n            \"json_path\", metavar=\"JSON_PATH\", type=str, help=\"path of the json file to read metadata from\"\n        )\n        subparser.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n        subparser.add_argument(\n            \"object_path\",\n            metavar=\"OBJECT_PATH\",\n            type=str,\n            help=\"path of the object file to write compressed encrypted data into\",\n        )\n\n        debug_get_obj_epilog = process_epilog(\n            \"\"\"\n        This command gets an object from the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog\n        )\n        debug_parsers.add_subcommand(\"get-obj\", subparser, help=\"get object from repository (debug)\")\n        subparser.add_argument(\"id\", metavar=\"ID\", type=str, help=\"hex object ID to get from the repo\")\n        subparser.add_argument(\"path\", metavar=\"PATH\", type=str, help=\"file to write object data into\")\n\n        debug_put_obj_epilog = process_epilog(\n            \"\"\"\n        This command puts an object into the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog\n        )\n        debug_parsers.add_subcommand(\"put-obj\", subparser, help=\"put object to repository (debug)\")\n        subparser.add_argument(\"id\", metavar=\"ID\", type=str, help=\"hex object ID to put into the repo\")\n        subparser.add_argument(\"path\", metavar=\"PATH\", type=str, help=\"file to read and create object from\")\n\n        debug_delete_obj_epilog = process_epilog(\n            \"\"\"\n        This command deletes objects from the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog\n        )\n        debug_parsers.add_subcommand(\"delete-obj\", subparser, help=\"delete object from repository (debug)\")\n        subparser.add_argument(\n            \"ids\", metavar=\"IDs\", nargs=\"+\", type=str, help=\"hex object ID(s) to delete from the repo\"\n        )\n\n        debug_convert_profile_epilog = process_epilog(\n            \"\"\"\n        Convert a Borg profile to a Python cProfile compatible profile.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[mid_common_parser],\n            description=self.do_debug_convert_profile.__doc__,\n            epilog=debug_convert_profile_epilog,\n        )\n        debug_parsers.add_subcommand(\n            \"convert-profile\", subparser, help=\"convert Borg profile to Python profile (debug)\"\n        )\n        subparser.add_argument(\"input\", metavar=\"INPUT\", type=str, help=\"Borg profile\")\n        subparser.add_argument(\"output\", metavar=\"OUTPUT\", type=str, help=\"Output file\")\n"
  },
  {
    "path": "src/borg/archiver/delete_cmd.py",
    "content": "import logging\n\nfrom ._common import with_repository\nfrom ..constants import *  # NOQA\nfrom ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass DeleteMixIn:\n    @with_repository(manifest=False)\n    def do_delete(self, args, repository):\n        \"\"\"Deletes archives.\"\"\"\n        self.output_list = args.output_list\n        dry_run = args.dry_run\n        manifest = Manifest.load(repository, (Manifest.Operation.DELETE,))\n        if args.name:\n            archive_infos = [manifest.archives.get_one([args.name])]\n        else:\n            archive_infos = manifest.archives.list_considering(args)\n        archive_infos = [ai for ai in archive_infos if \"@PROT\" not in ai.tags]\n        count = len(archive_infos)\n        if count == 0:\n            return\n        if not args.name and not args.match_archives and args.first == 0 and args.last == 0:\n            raise CommandError(\n                \"Aborting: if you really want to delete all archives, please use -a 'sh:*' \"\n                \"or just delete the whole repository (might be much faster).\"\n            )\n\n        deleted = False\n        logger_list = logging.getLogger(\"borg.output.list\")\n        for i, archive_info in enumerate(archive_infos, 1):\n            name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)\n            # format early before deletion of the archive\n            archive_formatted = format_archive(archive_info)\n            try:\n                # this does NOT use Archive.delete, so this code hopefully even works in cases a corrupt archive\n                # would make the code in class Archive crash, so the user can at least get rid of such archives.\n                if not dry_run:\n                    manifest.archives.delete_by_id(id)\n            except KeyError:\n                self.print_warning(f\"Archive {name} {hex_id} not found ({i}/{count}).\")\n            else:\n                deleted = True\n                if self.output_list:\n                    msg = \"Would delete: {} ({}/{})\" if dry_run else \"Deleted archive: {} ({}/{})\"\n                    logger_list.info(msg.format(archive_formatted, i, count))\n        if dry_run:\n            logger.info(\"Finished dry-run.\")\n        elif deleted:\n            manifest.write()\n            self.print_warning('Done. Run \"borg compact\" to free space.', wc=None)\n        else:\n            self.print_warning(\"Aborted.\", wc=None)\n        return\n\n    def build_parser_delete(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog, define_archive_filters_group\n\n        delete_epilog = process_epilog(\n            \"\"\"\n        This command soft-deletes archives from the repository.\n\n        Important:\n\n        - The delete command will only mark archives for deletion (\"soft-deletion\"),\n          repository disk space is **not** freed until you run ``borg compact``.\n        - You can use ``borg undelete`` to undelete archives, but only until\n          you run ``borg compact``.\n\n        When in doubt, use ``--dry-run --list`` to see what would be deleted.\n\n        You can delete multiple archives by specifying a match pattern using\n        the ``--match-archives PATTERN`` option (for more information on these\n        patterns, see :ref:`borg_patterns`).\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_delete.__doc__, epilog=delete_epilog)\n        subparsers.add_subcommand(\"delete\", subparser, help=\"delete archives\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change the repository\"\n        )\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of archives\"\n        )\n        define_archive_filters_group(subparser)\n        subparser.add_argument(\n            \"name\", metavar=\"NAME\", nargs=\"?\", type=archivename_validator, help=\"specify the archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/diff_cmd.py",
    "content": "import textwrap\nimport json\nimport sys\nimport os\n\nfrom ._common import with_repository, build_matcher, Highlander\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder\nfrom ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates\nfrom ..helpers.argparsing import ArgumentParser, ArgumentTypeError\nfrom ..item import ItemDiff\nfrom ..manifest import Manifest\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass DiffMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    def do_diff(self, args, repository, manifest):\n        \"\"\"Finds differences between two archives.\"\"\"\n\n        def actual_change(j):\n            j = j.to_dict()\n            if j[\"type\"] == \"modified\":\n                # Added/removed keys will not exist if chunker params differ\n                # between the two archives. Err on the side of caution and assume\n                # a real modification in this case (short-circuiting retrieving\n                # non-existent keys).\n                return not {\"added\", \"removed\"} <= j.keys() or not (j[\"added\"] == 0 and j[\"removed\"] == 0)\n            else:\n                # All other change types are indeed changes.\n                return True\n\n        def print_json_output(diff):\n            print(\n                json.dumps(\n                    {\n                        \"path\": diff.path,\n                        \"changes\": [\n                            change.to_dict()\n                            for name, change in diff.changes().items()\n                            if actual_change(change) and (not args.content_only or (name not in DiffFormatter.METADATA))\n                        ],\n                    },\n                    sort_keys=True,\n                    cls=BorgJsonEncoder,\n                )\n            )\n\n        def print_text_output(diff, formatter):\n            actual_changes = {\n                name: change\n                for name, change in diff.changes().items()\n                if actual_change(change) and (not args.content_only or (name not in DiffFormatter.METADATA))\n            }\n            diff._changes = actual_changes\n            res: str = formatter.format_item(diff)\n            if res.strip():\n                sys.stdout.write(res)\n\n        if args.format is not None:\n            format = args.format\n        elif args.content_only:\n            format = \"{content}{link}{directory}{blkdev}{chrdev}{fifo} {path}{NL}\"\n        else:\n            format = os.environ.get(\"BORG_DIFF_FORMAT\", \"{change} {path}{NL}\")\n\n        archive1_info = manifest.archives.get_one([args.name])\n        archive2_info = manifest.archives.get_one([args.other_name])\n        archive1 = Archive(manifest, archive1_info.id)\n        archive2 = Archive(manifest, archive2_info.id)\n\n        can_compare_chunk_ids = (\n            archive1.metadata.get(\"chunker_params\", False) == archive2.metadata.get(\"chunker_params\", True)\n            or args.same_chunker_params\n        )\n        if not can_compare_chunk_ids:\n            self.print_warning(\n                \"--chunker-params might be different between archives, diff will be slow.\\n\"\n                \"If you know for certain that they are the same, pass --same-chunker-params \"\n                \"to override this check.\",\n                wc=None,\n            )\n\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(args.patterns, args.paths)\n\n        diffs_iter = Archive.compare_archives_iter(\n            archive1, archive2, matcher, can_compare_chunk_ids=can_compare_chunk_ids\n        )\n        # Filter out equal items early (keep as generator; listify only if sorting)\n        diffs = (diff for diff in diffs_iter if not diff.equal(args.content_only))\n\n        sort_specs = []\n        if args.sort_by:\n            for spec in args.sort_by.split(\",\"):\n                spec = spec.strip()\n                if spec:\n                    sort_specs.append(spec)\n\n        def key_for(field: str, d: \"ItemDiff\"):\n            # strip direction markers if present\n            if field and field[0] in (\"<\", \">\"):\n                field = field[1:]\n            # path\n            if field in (None, \"\", \"path\"):\n                return remove_surrogates(d.path)\n            # compute size_* from changes\n            if field in (\"size_diff\", \"size_added\", \"size_removed\"):\n                added = removed = 0\n                ch = d.changes().get(\"content\")\n                if ch is not None:\n                    info = ch.to_dict()\n                    t = info.get(\"type\")\n                    if t == \"modified\":\n                        added = info.get(\"added\", 0)\n                        removed = info.get(\"removed\", 0)\n                    elif t and t.startswith(\"added\"):\n                        added = info.get(\"added\", info.get(\"size\", 0))\n                        removed = 0\n                    elif t and t.startswith(\"removed\"):\n                        added = 0\n                        removed = info.get(\"removed\", info.get(\"size\", 0))\n                if field == \"size_diff\":\n                    return added - removed\n                if field == \"size_added\":\n                    return added\n                if field == \"size_removed\":\n                    return removed\n            # timestamp diffs\n            if field in (\"ctime_diff\", \"mtime_diff\"):\n                ts = field.split(\"_\")[0]\n                t1 = d._item1.get(ts, 0)\n                t2 = d._item2.get(ts, 0)\n                return t2 - t1\n            # size of item in archive2\n            if field == \"size\":\n                it = d._item2\n                if it is None or it.get(\"deleted\"):\n                    return 0\n                return it.get_size()\n            # direct attributes from current item (prefer item2)\n            it = d._item2 or d._item1\n            attr_defaults = {\"user\": \"\", \"group\": \"\", \"uid\": -1, \"gid\": -1, \"ctime\": 0, \"mtime\": 0}\n            if field in attr_defaults:\n                if it is None:\n                    return attr_defaults[field]\n                return it.get(field, attr_defaults[field])\n            raise ValueError(f\"Invalid field name: {field}\")\n\n        if sort_specs:\n            diffs = list(diffs)\n            # Apply stable sorts from last to first\n            for spec in reversed(sort_specs):\n                desc = False\n                field = spec\n                if field and field[0] in (\"<\", \">\"):\n                    desc = field[0] == \">\"\n                diffs.sort(key=lambda di: key_for(field, di), reverse=desc)\n\n        formatter = DiffFormatter(format, args.content_only)\n        for diff in diffs:\n            if args.json_lines:\n                print_json_output(diff)\n            else:\n                print_text_output(diff, formatter)\n\n        for pattern in matcher.get_unmatched_include_patterns():\n            self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern))\n\n    def build_parser_diff(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_exclusion_group\n\n        diff_epilog = (\n            process_epilog(\n                \"\"\"\n        This command finds differences (file contents, metadata) between ARCHIVE1 and ARCHIVE2.\n\n        For more help on include/exclude patterns, see the output of the :ref:`borg_patterns` command.\n\n        .. man NOTES\n\n        The FORMAT specifier syntax\n        +++++++++++++++++++++++++++\n\n        The ``--format`` option uses Python's `format string syntax\n        <https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\n        Examples:\n        ::\n\n            $ borg diff --format '{content:30} {path}{NL}' ArchiveFoo ArchiveBar\n            modified:  +4.1 kB  -1.0 kB    file-diff\n            ...\n\n            # {VAR:<NUMBER} - pad to NUMBER columns left-aligned.\n            # {VAR:>NUMBER} - pad to NUMBER columns right-aligned.\n            $ borg diff --format '{content:>30} {path}{NL}' ArchiveFoo ArchiveBar\n               modified:  +4.1 kB  -1.0 kB file-diff\n            ...\n\n        The following keys are always available:\n\n        \"\"\"\n            )\n            + BaseFormatter.keys_help()\n            + textwrap.dedent(\n                \"\"\"\n\n        Keys available only when showing differences between archives:\n\n        \"\"\"\n            )\n            + DiffFormatter.keys_help()\n            + textwrap.dedent(\n                \"\"\"\n\n        What is compared\n        +++++++++++++++++\n        For each matching item in both archives, Borg reports:\n\n        - Content changes: total added/removed bytes within files. If chunker parameters are comparable,\n          Borg compares chunk IDs quickly; otherwise, it compares the content.\n        - Metadata changes: user, group, mode, and other metadata shown inline, like\n          \"[old_mode -> new_mode]\" for mode changes. Use ``--content-only`` to suppress metadata changes.\n        - Added/removed items: printed as \"added SIZE path\" or \"removed SIZE path\".\n\n        Output formats\n        ++++++++++++++\n        The default (text) output shows one line per changed path, e.g.::\n\n            +135 B    -252 B [ -rw-r--r-- -> -rwxr-xr-x ] path/to/file\n\n        JSON Lines output (``--json-lines``) prints one JSON object per changed path, e.g.::\n\n            {\"path\": \"PATH\", \"changes\": [\n                {\"type\": \"modified\", \"added\": BYTES, \"removed\": BYTES},\n                {\"type\": \"mode\", \"old_mode\": \"-rw-r--r--\", \"new_mode\": \"-rwxr-xr-x\"},\n                {\"type\": \"added\", \"size\": SIZE},\n                {\"type\": \"removed\", \"size\": SIZE}\n            ]}\n\n        Sorting\n        ++++++++\n        Use ``--sort-by FIELDS`` where FIELDS is a comma-separated list of fields.\n        Sorts are applied stably from last to first in the given list. Prepend \">\" for\n        descending, \"<\" (or no prefix) for ascending, for example ``--sort-by=\">size_added,path\"``.\n        Supported fields include:\n\n        - path: the item path\n        - size_added: total bytes added for the item content\n        - size_removed: total bytes removed for the item content\n        - size_diff: size_added - size_removed (net content change)\n        - size: size of the item as stored in ARCHIVE2 (0 for removed items)\n        - user, group, uid, gid, ctime, mtime: taken from the item state in ARCHIVE2 when present\n        - ctime_diff, mtime_diff: timestamp difference (ARCHIVE2 - ARCHIVE1)\n\n        Performance considerations\n        ++++++++++++++++++++++++++\n        diff automatically detects whether the archives were created with the same chunker\n        parameters. If so, only chunk IDs are compared, which is very fast.\n        \"\"\"\n            )\n        )\n\n        def diff_sort_spec_validator(s):\n            if not isinstance(s, str):\n                raise ArgumentTypeError(\"unsupported sort field (not a string)\")\n            allowed = {\n                \"path\",\n                \"size_added\",\n                \"size_removed\",\n                \"size_diff\",\n                \"size\",\n                \"user\",\n                \"group\",\n                \"uid\",\n                \"gid\",\n                \"ctime\",\n                \"mtime\",\n                \"ctime_diff\",\n                \"mtime_diff\",\n            }\n            parts = [p.strip() for p in s.split(\",\") if p.strip()]\n            if not parts:\n                raise ArgumentTypeError(\"unsupported sort field: empty spec\")\n            for spec in parts:\n                field = spec[1:] if spec and spec[0] in (\">\", \"<\") else spec\n                if field not in allowed:\n                    raise ArgumentTypeError(f\"unsupported sort field: {field}\")\n            return \",\".join(parts)\n\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_diff.__doc__, epilog=diff_epilog)\n        subparsers.add_subcommand(\"diff\", subparser, help=\"find differences in archive contents\")\n        subparser.add_argument(\n            \"--numeric-ids\",\n            dest=\"numeric_ids\",\n            action=\"store_true\",\n            help=\"only consider numeric user and group identifiers\",\n        )\n        subparser.add_argument(\n            \"--same-chunker-params\",\n            dest=\"same_chunker_params\",\n            action=\"store_true\",\n            help=\"override the check of chunker parameters\",\n        )\n        subparser.add_argument(\n            \"--format\",\n            metavar=\"FORMAT\",\n            dest=\"format\",\n            action=Highlander,\n            help='specify format for differences between archives (default: \"{change} {path}{NL}\")',\n        )\n        subparser.add_argument(\"--json-lines\", action=\"store_true\", help=\"Format output as JSON Lines.\")\n        subparser.add_argument(\n            \"--sort-by\",\n            dest=\"sort_by\",\n            type=diff_sort_spec_validator,\n            help=\"Sort output by comma-separated fields (e.g., '>size_added,path').\",\n        )\n        subparser.add_argument(\n            \"--content-only\",\n            action=\"store_true\",\n            help=\"Only compare differences in content (exclude metadata differences)\",\n        )\n        subparser.add_argument(\"name\", metavar=\"ARCHIVE1\", type=archivename_validator, help=\"ARCHIVE1 name\")\n        subparser.add_argument(\"other_name\", metavar=\"ARCHIVE2\", type=archivename_validator, help=\"ARCHIVE2 name\")\n        subparser.add_argument(\n            \"paths\",\n            metavar=\"PATH\",\n            nargs=\"*\",\n            type=PathSpec,\n            help=\"paths of items inside the archives to compare; patterns are supported.\",\n        )\n        define_exclusion_group(subparser)\n"
  },
  {
    "path": "src/borg/archiver/extract_cmd.py",
    "content": "import sys\nimport logging\nimport stat\n\nfrom ._common import with_repository, with_archive\nfrom ._common import build_filter, build_matcher\nfrom ..archive import BackupError\nfrom ..constants import *  # NOQA\nfrom ..helpers import archivename_validator, PathSpec\nfrom ..helpers import remove_surrogates\nfrom ..helpers import HardLinkManager\nfrom ..helpers import ProgressIndicatorPercent\nfrom ..helpers import BackupWarning, IncludePatternNeverMatchedWarning\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass ExtractMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    @with_archive\n    def do_extract(self, args, repository, manifest, archive):\n        \"\"\"Extracts archive contents.\"\"\"\n        # be restrictive when restoring files, restore permissions later\n        if sys.getfilesystemencoding() == \"ascii\":\n            logger.warning('Warning: Filesystem encoding is \"ascii\"; extracting non-ASCII filenames is not supported.')\n            if sys.platform.startswith((\"linux\", \"freebsd\", \"netbsd\", \"openbsd\", \"darwin\")):\n                logger.warning(\n                    \"Hint: You likely need to fix your locale setup. \"\n                    \"For example, install locales and use: LANG=en_US.UTF-8\"\n                )\n\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(args.patterns, args.paths)\n\n        progress = args.progress\n        output_list = args.output_list\n        dry_run = args.dry_run\n        stdout = args.stdout\n        sparse = args.sparse\n        strip_components = args.strip_components\n        continue_extraction = args.continue_extraction\n        dirs = []\n        hlm = HardLinkManager(id_type=bytes, info_type=str)  # hlid -> path\n\n        filter = build_filter(matcher, strip_components)\n        if progress:\n            pi = ProgressIndicatorPercent(msg=\"%5.1f%% Extracting: %s\", step=0.1, msgid=\"extract\")\n            pi.output(\n                \"Calculating total archive size for the progress indicator (might take a long time for large archives)\"\n            )\n            extracted_size = sum(item.get_size() for item in archive.iter_items(filter))\n            pi.total = extracted_size\n        else:\n            pi = None\n\n        for item in archive.iter_items():\n            orig_path = item.path\n            if strip_components:\n                stripped_path = \"/\".join(orig_path.split(\"/\")[strip_components:])\n                if not stripped_path:\n                    continue\n                item.path = stripped_path\n\n            is_matched = matcher.match(orig_path)\n\n            if output_list:\n                log_prefix = \"+\" if is_matched else \"-\"\n                logging.getLogger(\"borg.output.list\").info(f\"{log_prefix} {remove_surrogates(item.path)}\")\n\n            if is_matched:\n                archive.preload_item_chunks(item, optimize_hardlinks=True)\n\n                if not dry_run:\n                    while dirs and not item.path.startswith(dirs[-1].path):\n                        dir_item = dirs.pop(-1)\n                        try:\n                            archive.extract_item(dir_item, stdout=stdout)\n                        except BackupError as e:\n                            self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))\n\n                try:\n                    if dry_run:\n                        archive.extract_item(item, dry_run=True, hlm=hlm, pi=pi)\n                    else:\n                        if stat.S_ISDIR(item.mode):\n                            dirs.append(item)\n                            archive.extract_item(item, stdout=stdout, restore_attrs=False)\n                        else:\n                            archive.extract_item(\n                                item,\n                                stdout=stdout,\n                                sparse=sparse,\n                                hlm=hlm,\n                                pi=pi,\n                                continue_extraction=continue_extraction,\n                            )\n                except BackupError as e:\n                    self.print_warning_instance(BackupWarning(remove_surrogates(orig_path), e))\n\n        if pi:\n            pi.finish()\n\n        if not args.dry_run:\n            pi = ProgressIndicatorPercent(\n                total=len(dirs), msg=\"Setting directory permissions %3.0f%%\", msgid=\"extract.permissions\"\n            )\n            while dirs:\n                pi.show()\n                dir_item = dirs.pop(-1)\n                try:\n                    archive.extract_item(dir_item, stdout=stdout)\n                except BackupError as e:\n                    self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))\n        for pattern in matcher.get_unmatched_include_patterns():\n            self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern))\n        if pi:\n            # clear progress output\n            pi.finish()\n\n    def build_parser_extract(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_exclusion_group\n\n        extract_epilog = process_epilog(\n            \"\"\"\n        This command extracts the contents of an archive.\n\n        By default, the entire archive is extracted, but a subset of files and directories\n        can be selected by passing a list of ``PATH`` arguments. The default interpretation\n        for the paths to extract is `pp:` which is a literal path-prefix match. If you want\n        to use e.g. a wildcard, you must select a different pattern style such as `sh:` or\n        `fm:`. See :ref:`borg_patterns` for more information.\n\n        The file selection can be further restricted by using the ``--exclude`` option.\n        For more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\n        By using ``--dry-run``, you can do all extraction steps except actually writing the\n        output data: reading metadata and data chunks from the repository, checking the hash/HMAC,\n        decrypting, and decompressing.\n\n        ``--progress`` can be slower than no progress display, since it makes one additional\n        pass over the archive metadata.\n\n        .. note::\n\n            Currently, extract always writes into the current working directory (\".\"),\n            so make sure you ``cd`` to the right place before calling ``borg extract``.\n\n            When parent directories are not extracted (because of using file/directory selection\n            or any other reason), Borg cannot restore parent directories' metadata, e.g., owner,\n            group, permissions, etc.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_extract.__doc__, epilog=extract_epilog)\n        subparsers.add_subcommand(\"extract\", subparser, help=\"extract archive contents\")\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of items (files, dirs, ...)\"\n        )\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not actually change any files\"\n        )\n        subparser.add_argument(\n            \"--numeric-ids\", dest=\"numeric_ids\", action=\"store_true\", help=\"only use numeric user and group identifiers\"\n        )\n        subparser.add_argument(\n            \"--noflags\", dest=\"noflags\", action=\"store_true\", help=\"do not extract/set flags (e.g. NODUMP, IMMUTABLE)\"\n        )\n        subparser.add_argument(\"--noacls\", dest=\"noacls\", action=\"store_true\", help=\"do not extract/set ACLs\")\n        subparser.add_argument(\"--noxattrs\", dest=\"noxattrs\", action=\"store_true\", help=\"do not extract/set xattrs\")\n        subparser.add_argument(\n            \"--stdout\", dest=\"stdout\", action=\"store_true\", help=\"write all extracted data to stdout\"\n        )\n        subparser.add_argument(\n            \"--sparse\",\n            dest=\"sparse\",\n            action=\"store_true\",\n            help=\"create holes in the output sparse file from all-zero chunks\",\n        )\n        subparser.add_argument(\n            \"--continue\",\n            dest=\"continue_extraction\",\n            action=\"store_true\",\n            help=\"continue a previously interrupted extraction of the same archive\",\n        )\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=PathSpec, help=\"paths to extract; patterns are supported\"\n        )\n        define_exclusion_group(subparser, strip_components=True)\n"
  },
  {
    "path": "src/borg/archiver/help_cmd.py",
    "content": "import collections\nimport textwrap\n\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..constants import *  # NOQA\nfrom ..helpers.nanorst import rst_to_terminal\n\n\nclass HelpMixIn:\n    helptext = collections.OrderedDict()\n    helptext[\"patterns\"] = textwrap.dedent(\n        \"\"\"\n        When specifying one or more file paths in a Borg command that supports\n        patterns for the respective option or argument, you can apply the\n        patterns described here to include only desired files and/or exclude\n        unwanted ones. Patterns can be used\n\n        - for ``--exclude`` option,\n        - in the file given with ``--exclude-from`` option,\n        - for ``--pattern`` option,\n        - in the file given with ``--patterns-from`` option and\n        - for ``PATH`` arguments that explicitly support them.\n\n        The path/filenames used as input for the pattern matching start with the\n        currently active recursion root. You usually give the recursion root(s)\n        when invoking borg and these can be either relative or absolute paths.\n\n        Be careful, your patterns must match the archived paths:\n\n        - Archived paths never start with a leading slash ('/'), nor with '.', nor with '..'.\n\n          - When you back up absolute paths like ``/home/user``, the archived\n            paths start with ``home/user``.\n          - When you back up relative paths like ``./src``, the archived paths\n            start with ``src``.\n          - When you back up relative paths like ``../../src``, the archived paths\n            start with ``src``.\n          - On native Windows, archived absolute paths look like ``C/Windows/System32``.\n\n        Borg supports different pattern styles. To define a non-default\n        style for a specific pattern, prefix it with two characters followed\n        by a colon ':' (i.e. ``fm:path/*``, ``sh:path/**``).\n\n        Note: Windows users must only use forward slashes in patterns, not backslashes.\n\n        The default pattern style for ``--exclude`` differs from ``--pattern``, see below.\n\n        `Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector ``fm:``\n            This is the default style for ``--exclude`` and ``--exclude-from``.\n            These patterns use a variant of shell pattern syntax, with '\\\\*' matching\n            any number of characters, '?' matching any single character, '[...]'\n            matching any single character specified, including ranges, and '[!...]'\n            matching any character not specified. For the purpose of these patterns,\n            the path separator (forward slash '/') is not treated specially.\n            Wrap meta-characters in brackets for a literal\n            match (i.e. ``[?]`` to match the literal character '?'). For a path\n            to match a pattern, the full path must match, or it must match\n            from the start of the full path to just before a path separator. Except\n            for the root path, paths will never end in the path separator when\n            matching is attempted.  Thus, if a given pattern ends in a path\n            separator, a '\\\\*' is appended before matching is attempted. A leading\n            path separator is always removed.\n\n        Shell-style patterns, selector ``sh:``\n            This is the default style for ``--pattern`` and ``--patterns-from``.\n            Like fnmatch patterns these are similar to shell patterns. The difference\n            is that the pattern may include ``**/`` for matching zero or more directory\n            levels, ``*`` for matching zero or more arbitrary characters with the\n            exception of any path separator, ``{}`` containing comma-separated\n            alternative patterns. A leading path separator is always removed.\n\n        `Regular expressions <https://docs.python.org/3/library/re.html>`_, selector ``re:``\n            Unlike shell patterns, regular expressions are not required to match the full\n            path and any substring match is sufficient. It is strongly recommended to\n            anchor patterns to the start ('^'), to the end ('$') or both.\n\n        Path prefix, selector ``pp:``\n            This pattern style is useful to match whole subdirectories. The pattern\n            ``pp:root/somedir`` matches ``root/somedir`` and everything therein.\n            A leading path separator is always removed.\n\n        Path full-match, selector ``pf:``\n            This pattern style is (only) useful to match full paths.\n            This is kind of a pseudo pattern as it cannot have any variable or\n            unspecified parts - the full path must be given. ``pf:root/file.ext``\n            matches ``root/file.ext`` only. A leading path separator is always\n            removed.\n\n            Implementation note: this is implemented via very time-efficient O(1)\n            hashtable lookups (this means you can have huge amounts of such patterns\n            without impacting performance much).\n            Due to that, this kind of pattern does not respect any context or order.\n            If you use such a pattern to include a file, it will always be included\n            (if the directory recursion encounters it).\n            Other include/exclude patterns that would normally match will be ignored.\n            Same logic applies for exclude.\n\n        .. note::\n\n            ``re:``, ``sh:`` and ``fm:`` patterns are all implemented on top of\n            the Python SRE engine. It is very easy to formulate patterns for each\n            of these types which requires an inordinate amount of time to match\n            paths. If untrusted users are able to supply patterns, ensure they\n            cannot supply ``re:`` patterns. Further, ensure that ``sh:`` and\n            ``fm:`` patterns only contain a handful of wildcards at most.\n\n        .. note::\n\n            **Windows path handling**: All paths in Borg archives use forward slashes (``/``)\n            as path separators, regardless of the platform. When creating archives on Windows,\n            backslashes from filesystem paths are automatically converted to forward slashes.\n\n        .. note::\n\n            **Windows reserved characters**: On Windows, when extracting archives created on\n            POSIX systems, paths may contain characters that are reserved from being used in\n            file or directory names (like: ``< > : \" \\\\ | ? *``).\n            These are replaced by characters in the unicode private use area (``U+F0xx``) like\n            the CIFS mapchars feature also does it. It won't be pretty, but at least it works.\n\n        Exclusions can be passed via the command line option ``--exclude``. When used\n        from within a shell, the patterns should be quoted to protect them from\n        expansion.\n\n        Patterns matching special characters, e.g. whitespace, within a shell may\n        require adjustments, such as putting quotation marks around the arguments.\n        Example:\n        Using bash, the following command line option would match and exclude \"item name\":\n        ``--pattern='-path/item name'``\n        Note that when patterns are used within a pattern file directly read by borg,\n        e.g. when using ``--exclude-from`` or ``--patterns-from``, there is no shell\n        involved and thus no quotation marks are required.\n\n        The ``--exclude-from`` option permits loading exclusion patterns from a text\n        file with one pattern per line. Lines empty or starting with the hash sign\n        '#' after removing whitespace on both ends are ignored. The optional style\n        selector prefix is also supported for patterns loaded from a file. Due to\n        whitespace removal, paths with whitespace at the beginning or end can only be\n        excluded using regular expressions.\n\n        To test your exclusion patterns without performing an actual backup you can\n        run ``borg create --list --dry-run ...``.\n\n        Examples::\n\n            # Exclude a directory anywhere in the tree named ``steamapps/common``\n            # (and everything below it), regardless of where it appears:\n            $ borg create -e 'sh:**/steamapps/common/**' archive /\n\n            # Exclude the contents of ``/home/user/.cache``:\n            $ borg create -e 'sh:home/user/.cache/**' archive /home/user\n            $ borg create -e home/user/.cache/ archive /home/user\n\n            # The file '/home/user/.cache/important' is *not* backed up:\n            $ borg create -e home/user/.cache/ archive / /home/user/.cache/important\n\n            # Exclude '/home/user/file.o' but not '/home/user/file.odt':\n            $ borg create -e '*.o' archive /\n\n            # Exclude '/home/user/junk' and '/home/user/subdir/junk' but\n            # not '/home/user/importantjunk' or '/etc/junk':\n            $ borg create -e 'home/*/junk' archive /\n\n            # The contents of directories in '/home' are not backed up when their name\n            # ends in '.tmp'\n            $ borg create --exclude 're:^home/[^/]+\\\\.tmp/' archive /\n\n            # Load exclusions from file\n            $ cat >exclude.txt <<EOF\n            # Comment line\n            home/*/junk\n            *.tmp\n            fm:aa:something/*\n            re:^home/[^/]+\\\\.tmp/\n            sh:home/*/.thumbnails\n            # Example with spaces, no need to escape as it is processed by borg\n            some file with spaces.txt\n            EOF\n            $ borg create --exclude-from exclude.txt archive /\n\n        A more general and easier to use way to define filename matching patterns\n        exists with the ``--pattern`` and ``--patterns-from`` options. Using\n        these, you may specify the backup roots, default pattern styles and\n        patterns for inclusion and exclusion.\n\n        Root path prefix ``R``\n            A recursion root path starts with the prefix ``R``, followed by a path\n            (a plain path, not a file pattern). Use this prefix to have the root\n            paths in the patterns file rather than as command line arguments.\n\n        Pattern style prefix ``P`` (only useful within patterns files)\n            To change the default pattern style, use the ``P`` prefix, followed by\n            the pattern style abbreviation (``fm``, ``pf``, ``pp``, ``re``, ``sh``).\n            All patterns following this line in the same patterns file will use this\n            style until another style is specified or the end of the file is reached.\n            When the current patterns file is finished, the default pattern style will\n            reset.\n\n        Exclude pattern prefix ``-``\n            Use the prefix ``-``, followed by a pattern, to define an exclusion.\n            This has the same effect as the ``--exclude`` option.\n\n        Exclude no-recurse pattern prefix ``!``\n            Use the prefix ``!``, followed by a pattern, to define an exclusion\n            that does not recurse into subdirectories. This saves time, but\n            prevents include patterns to match any files in subdirectories.\n\n        Include pattern prefix ``+``\n            Use the prefix ``+``, followed by a pattern, to define inclusions.\n            This is useful to include paths that are covered in an exclude\n            pattern and would otherwise not be backed up.\n\n        The first matching pattern is used, so if an include pattern matches\n        before an exclude pattern, the file is backed up. Note that a no-recurse\n        exclude stops examination of subdirectories so that potential includes\n        will not match - use normal excludes for such use cases.\n\n        Example::\n\n            # Define the recursion root\n            R /\n            # Exclude all iso files in any directory\n            - **/*.iso\n            # Explicitly include all inside etc and root\n            + etc/**\n            + root/**\n            # Exclude a specific directory under each user's home directories\n            - home/*/.cache\n            # Explicitly include everything in /home\n            + home/**\n            # Explicitly exclude some directories without recursing into them\n            ! re:^(dev|proc|run|sys|tmp)\n            # Exclude all other files and directories\n            # that are not specifically included earlier.\n            - **\n\n        **Tip: You can easily test your patterns with --dry-run and  --list**::\n\n            $ borg create --dry-run --list --patterns-from patterns.txt archive\n\n        This will list the considered files one per line, prefixed with a\n        character that indicates the action (e.g. 'x' for excluding, see\n        **Item flags** in `borg create` usage docs).\n\n        .. note::\n\n            It is possible that a subdirectory or file is matched while its parent\n            directories are not. In that case, parent directories are not backed\n            up and thus their user, group, permission, etc. cannot be restored.\n\n        Patterns (``--pattern``) and excludes (``--exclude``) from the command line are\n        considered first (in the order of appearance). Then patterns from ``--patterns-from``\n        are added. Exclusion patterns from ``--exclude-from`` files are appended last.\n\n        Examples::\n\n            # back up pics, but not the ones from 2018, except the good ones:\n            # note: using = is essential to avoid cmdline argument parsing issues.\n            borg create --pattern=+pics/2018/good --pattern=-pics/2018 archive pics\n\n            # back up only JPG/JPEG files (case insensitive) in all home directories:\n            borg create --pattern '+ re:\\\\.jpe?g(?i)$' archive /home\n\n            # back up homes, but exclude big downloads (like .ISO files) or hidden files:\n            borg create --exclude 're:\\\\.iso(?i)$' --exclude 'sh:home/**/.*' archive /home\n\n            # use a file with patterns (recursion root '/' via command line):\n            borg create --patterns-from patterns.lst archive /\n\n        The patterns.lst file could look like that::\n\n            # \"sh:\" pattern style is the default\n            # exclude caches\n            - home/*/.cache\n            # include susans home\n            + home/susan\n            # also back up this exact file\n            + pf:home/bobby/specialfile.txt\n            # don't back up the other home directories\n            - home/*\n            # don't even look in /dev, /proc, /run, /sys, /tmp (note: would exclude files like /device, too)\n            ! re:^(dev|proc|run|sys|tmp)\n\n        You can specify recursion roots either on the command line or in a patternfile::\n\n            # these two commands do the same thing\n            borg create --exclude home/bobby/junk archive /home/bobby /home/susan\n            borg create --patterns-from patternfile.lst archive\n\n        patternfile.lst::\n\n            # note that excludes use fm: by default and patternfiles use sh: by default.\n            # therefore, we need to specify fm: to have the same exact behavior.\n            P fm\n            R /home/bobby\n            R /home/susan\n            - home/bobby/junk\n\n        This allows you to share the same patterns between multiple repositories\n        without needing to specify them on the command line.\\n\\n\"\"\"\n    )\n    helptext[\"match-archives\"] = textwrap.dedent(\n        \"\"\"\n        The ``--match-archives`` option matches a given pattern against the list of all archives\n        in the repository. It can be given multiple times.\n\n        The patterns can have a prefix of:\n\n        - name: pattern match on the archive name (default)\n        - aid: prefix match on the archive id (only one result allowed)\n        - user: exact match on the username who created the archive\n        - host: exact match on the hostname where the archive was created\n        - tags: match on the archive tags\n\n        In case of a name pattern match,\n        it uses pattern styles similar to the ones described by ``borg help patterns``:\n\n        Identical match pattern, selector ``id:`` (default)\n            Simple string match, must fully match exactly as given.\n\n        Shell-style patterns, selector ``sh:``\n            Match like on the shell, wildcards like `*` and `?` work.\n\n        `Regular expressions <https://docs.python.org/3/library/re.html>`_, selector ``re:``\n            Full regular expression support.\n            This is very powerful, but can also get rather complicated.\n\n        Examples::\n\n            # name match, id: style\n            borg delete --match-archives 'id:archive-with-crap'\n            borg delete -a 'id:archive-with-crap'  # same, using short option\n            borg delete -a 'archive-with-crap'  # same, because 'id:' is the default\n\n            # name match, sh: style\n            borg delete -a 'sh:home-kenny-*'\n\n            # name match, re: style\n            borg delete -a 're:pc[123]-home-(user1|user2)-2022-09-.*'\n\n            # archive id prefix match:\n            borg delete -a 'aid:d34db33f'\n\n            # host or user match\n            borg delete -a 'user:kenny'\n            borg delete -a 'host:kenny-pc'\n\n            # tags match\n            borg delete -a 'tags:TAG1' -a 'tags:TAG2'\\n\\n\"\"\"\n    )\n    helptext[\"placeholders\"] = textwrap.dedent(\n        \"\"\"\n        Repository URLs, ``--name``, ``-a`` / ``--match-archives``, ``--comment``\n        and ``--remote-path`` values support these placeholders:\n\n        {hostname}\n            The (short) hostname of the machine.\n\n        {fqdn}\n            The full name of the machine.\n\n        {reverse-fqdn}\n            The full name of the machine in reverse domain name notation.\n\n        {now}\n            The current local date and time, by default in ISO-8601 format.\n            You can also supply your own `format string <https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior>`_, e.g. {now:%Y-%m-%d_%H:%M:%S}\n\n        {utcnow}\n            The current UTC date and time, by default in ISO-8601 format.\n            You can also supply your own `format string <https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior>`_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S}\n\n        {user}\n            The user name (or UID, if no name is available) of the user running borg.\n\n        {pid}\n            The current process ID.\n\n        {borgversion}\n            The version of borg, e.g.: 1.0.8rc1\n\n        {borgmajor}\n            The version of borg, only the major version, e.g.: 1\n\n        {borgminor}\n            The version of borg, only major and minor version, e.g.: 1.0\n\n        {borgpatch}\n            The version of borg, only major, minor and patch version, e.g.: 1.0.8\n\n        If literal curly braces need to be used, double them for escaping::\n\n            borg create --repo /path/to/repo {{literal_text}}\n\n        Examples::\n\n            borg create --repo /path/to/repo {hostname}-{user}-{utcnow} ...\n            borg create --repo /path/to/repo {hostname}-{now:%Y-%m-%d_%H:%M:%S%z} ...\n            borg prune -a 'sh:{hostname}-*' ...\n\n        .. note::\n            systemd uses a difficult, non-standard syntax for command lines in unit files (refer to\n            the `systemd.unit(5)` manual page).\n\n            When invoking borg from unit files, pay particular attention to escaping,\n            especially when using the now/utcnow placeholders, since systemd performs its own\n            %-based variable replacement even in quoted text. To avoid interference from systemd,\n            double all percent signs (``{hostname}-{now:%Y-%m-%d_%H:%M:%S}``\n            becomes ``{hostname}-{now:%%Y-%%m-%%d_%%H:%%M:%%S}``).\\n\\n\"\"\"\n    )\n    helptext[\"compression\"] = textwrap.dedent(\n        \"\"\"\n        It is no problem to mix different compression methods in one repository,\n        deduplication is done on the source data chunks (not on the compressed\n        or encrypted data).\n\n        If some specific chunk was once compressed and stored into the repository, creating\n        another backup that also uses this chunk will not change the stored chunk.\n        So if you use different compression specs for the backups, whichever stores a\n        chunk first determines its compression. See also ``borg recreate``.\n\n        Compression is lz4 by default. If you want something else, you have to specify what you want.\n\n        Valid compression specifiers are:\n\n        none\n            Do not compress.\n\n        lz4\n            Use lz4 compression. Very high speed, very low compression. (default)\n\n        zstd[,L]\n            Use zstd (\"zstandard\") compression, a modern wide-range algorithm.\n            If you do not explicitly give the compression level L (ranging from 1\n            to 22), it will use level 3.\n\n        zlib[,L]\n            Use zlib (\"gz\") compression. Medium speed, medium compression.\n            If you do not explicitly give the compression level L (ranging from 0\n            to 9), it will use level 6.\n            Giving level 0 (means \"no compression\", but still has zlib protocol\n            overhead) is usually pointless, you better use \"none\" compression.\n\n        lzma[,L]\n            Use lzma (\"xz\") compression. Low speed, high compression.\n            If you do not explicitly give the compression level L (ranging from 0\n            to 9), it will use level 6.\n            Giving levels above 6 is pointless and counterproductive because it does\n            not compress better due to the buffer size used by borg - but it wastes\n            lots of CPU cycles and RAM.\n\n        auto,C[,L]\n            Use a built-in heuristic to decide per chunk whether to compress or not.\n            The heuristic tries with lz4 whether the data is compressible.\n            For incompressible data, it will not use compression (uses \"none\").\n            For compressible data, it uses the given C[,L] compression - with C[,L]\n            being any valid compression specifier. This can be helpful for media files\n            which often cannot be compressed much more.\n\n        obfuscate,SPEC,C[,L]\n            Use compressed-size obfuscation to make fingerprinting attacks based on\n            the observable stored chunk size more difficult. Note:\n\n            - You must combine this with encryption, or it won't make any sense.\n            - Your repo size will be bigger, of course.\n            - A chunk is limited by the constant ``MAX_DATA_SIZE`` (cur. ~20MiB).\n\n            The SPEC value determines how the size obfuscation works:\n\n            *Relative random reciprocal size variation* (multiplicative)\n\n            Size will increase by a factor, relative to the compressed data size.\n            Smaller factors are used often, larger factors rarely.\n\n            Available factors::\n\n              1:     0.01 ..        100\n              2:     0.1  ..      1,000\n              3:     1    ..     10,000\n              4:    10    ..    100,000\n              5:   100    ..  1,000,000\n              6: 1,000    .. 10,000,000\n\n            Example probabilities for SPEC ``1``::\n\n              90   %  0.01 ..   0.1\n               9   %  0.1  ..   1\n               0.9 %  1    ..  10\n               0.09% 10    .. 100\n\n            *Randomly sized padding up to the given size* (additive)\n\n            ::\n\n              110: 1kiB (2 ^ (SPEC - 100))\n              ...\n              120: 1MiB\n              ...\n              123: 8MiB (max.)\n\n            *Padmé padding* (deterministic)\n\n            ::\n\n              250: pads to sums of powers of 2, max 12% overhead\n\n            Uses the Padmé algorithm to deterministically pad the compressed size to a sum of\n            powers of 2, limiting overhead to 12%. See https://lbarman.ch/blog/padme/ for details.\n\n        Examples::\n\n            borg create --compression lz4 --repo REPO ARCHIVE data\n            borg create --compression zstd --repo REPO ARCHIVE data\n            borg create --compression zstd,10 --repo REPO ARCHIVE data\n            borg create --compression zlib --repo REPO ARCHIVE data\n            borg create --compression zlib,1 --repo REPO ARCHIVE data\n            borg create --compression auto,lzma,6 --repo REPO ARCHIVE data\n            borg create --compression auto,lzma ...\n            borg create --compression obfuscate,110,none ...\n            borg create --compression obfuscate,3,auto,zstd,10 ...\n            borg create --compression obfuscate,2,zstd,6 ...\n            borg create --compression obfuscate,250,zstd,3 ...\\n\\n\"\"\"\n    )\n\n    def do_help(self, parser, args):\n        commands = getattr(parser, \"_subcommands_action\", None)\n        commands = commands._name_parser_map if commands else {}\n\n        if not args.topic:\n            parser.print_help()\n        elif args.topic in self.helptext:\n            print(rst_to_terminal(self.helptext[args.topic]))\n        elif args.topic in commands:\n            if args.epilog_only:\n                print(commands[args.topic].epilog)\n            elif args.usage_only:\n                commands[args.topic].epilog = None\n                commands[args.topic].print_help()\n            else:\n                commands[args.topic].print_help()\n        else:\n            msg_lines = []\n            msg_lines += [\"No help available on %s.\" % args.topic]\n            msg_lines += [\"Try one of the following:\"]\n            msg_lines += [\"    Commands: %s\" % \", \".join(sorted(commands.keys()))]\n            msg_lines += [\"    Topics: %s\" % \", \".join(sorted(self.helptext.keys()))]\n            parser.error(\"\\n\".join(msg_lines))\n\n    def do_subcommand_help(self, parser, args):\n        \"\"\"display infos about subcommand\"\"\"\n        parser.print_help()\n\n    do_maincommand_help = do_subcommand_help\n\n    def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser):\n        subparser = ArgumentParser(parents=[common_parser], description=\"Extra help\")\n        subparsers.add_subcommand(\"help\", subparser, help=\"Extra help\")\n        subparser.add_argument(\"--epilog-only\", dest=\"epilog_only\", action=\"store_true\")\n        subparser.add_argument(\"--usage-only\", dest=\"usage_only\", action=\"store_true\")\n        subparser.add_argument(\"topic\", metavar=\"TOPIC\", type=str, nargs=\"?\", help=\"additional help on TOPIC\")\n"
  },
  {
    "path": "src/borg/archiver/info_cmd.py",
    "content": "import textwrap\nfrom datetime import timedelta\n\nfrom ._common import with_repository\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass InfoMixIn:\n    @with_repository(cache=True, compatibility=(Manifest.Operation.READ,))\n    def do_info(self, args, repository, manifest, cache):\n        \"\"\"Show archive details such as disk space used\"\"\"\n\n        if args.name:\n            archive_infos = [manifest.archives.get_one([args.name])]\n        else:\n            archive_infos = manifest.archives.list_considering(args)\n\n        output_data = []\n\n        for i, archive_info in enumerate(archive_infos, 1):\n            archive = Archive(manifest, archive_info.id, cache=cache, iec=args.iec)\n            info = archive.info()\n            if args.json:\n                output_data.append(info)\n            else:\n                info[\"duration\"] = format_timedelta(timedelta(seconds=info[\"duration\"]))\n                info[\"tags\"] = \",\".join(info[\"tags\"])\n                print(\n                    textwrap.dedent(\n                        \"\"\"\n                Archive name: {name}\n                Archive fingerprint: {id}\n                Comment: {comment}\n                Hostname: {hostname}\n                Username: {username}\n                Tags: {tags}\n                Time (nominal): {time}\n                Time (start): {start}\n                Time (end): {end}\n                Duration: {duration}\n                Command line: {command_line}\n                Working Directory: {cwd}\n                Number of files: {stats[nfiles]}\n                Original size: {stats[original_size]}\n                \"\"\"\n                    )\n                    .strip()\n                    .format(**info)\n                )\n            if not args.json and len(archive_infos) - i:\n                print()\n\n        if args.json:\n            json_print(basic_json_data(manifest, cache=cache, extra={\"archives\": output_data}))\n\n    def build_parser_info(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog, define_archive_filters_group\n\n        info_epilog = process_epilog(\n            \"\"\"\n        This command displays detailed information about the specified archive.\n\n        Please note that the deduplicated sizes of the individual archives do not add\n        up to the deduplicated size of the repository (\"all archives\"), because the two\n        mean different things:\n\n        This archive / deduplicated size = amount of data stored ONLY for this archive\n        = unique chunks of this archive.\n        All archives / deduplicated size = amount of data stored in the repository\n        = all chunks in the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_info.__doc__, epilog=info_epilog)\n        subparsers.add_subcommand(\"info\", subparser, help=\"show repository or archive information\")\n        subparser.add_argument(\"--json\", action=\"store_true\", help=\"format output as JSON\")\n        define_archive_filters_group(subparser)\n        subparser.add_argument(\n            \"name\", metavar=\"NAME\", nargs=\"?\", type=archivename_validator, help=\"specify the archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/key_cmds.py",
    "content": "import os\n\nfrom ..constants import *  # NOQA\nfrom ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey\nfrom ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey\nfrom ..crypto.keymanager import KeyManager\nfrom ..helpers import PathSpec, CommandError\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ._common import with_repository\n\nfrom ..logger import create_logger\n\nlogger = create_logger(__name__)\n\n\nclass KeysMixIn:\n    @with_repository(compatibility=(Manifest.Operation.CHECK,))\n    def do_key_change_passphrase(self, args, repository, manifest):\n        \"\"\"Changes the repository key file passphrase.\"\"\"\n        key = manifest.key\n        if not hasattr(key, \"change_passphrase\"):\n            raise CommandError(\"This repository is not encrypted, cannot change the passphrase.\")\n        key.change_passphrase()\n        logger.info(\"Key updated\")\n        if hasattr(key, \"find_key\"):\n            # print key location to make backing it up easier\n            logger.info(\"Key location: %s\", key.find_key())\n\n    @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))\n    def do_key_change_location(self, args, repository, manifest, cache):\n        \"\"\"Changes the repository key location.\"\"\"\n        key = manifest.key\n        if not hasattr(key, \"change_passphrase\"):\n            raise CommandError(\"This repository is not encrypted, cannot change the key location.\")\n\n        if args.key_mode == \"keyfile\":\n            if isinstance(key, AESOCBRepoKey):\n                key_new = AESOCBKeyfileKey(repository)\n            elif isinstance(key, CHPORepoKey):\n                key_new = CHPOKeyfileKey(repository)\n            elif isinstance(key, Blake2AESOCBRepoKey):\n                key_new = Blake2AESOCBKeyfileKey(repository)\n            elif isinstance(key, Blake2CHPORepoKey):\n                key_new = Blake2CHPOKeyfileKey(repository)\n            else:\n                print(\"Change not needed or not supported.\")\n                return\n        if args.key_mode == \"repokey\":\n            if isinstance(key, AESOCBKeyfileKey):\n                key_new = AESOCBRepoKey(repository)\n            elif isinstance(key, CHPOKeyfileKey):\n                key_new = CHPORepoKey(repository)\n            elif isinstance(key, Blake2AESOCBKeyfileKey):\n                key_new = Blake2AESOCBRepoKey(repository)\n            elif isinstance(key, Blake2CHPOKeyfileKey):\n                key_new = Blake2CHPORepoKey(repository)\n            else:\n                print(\"Change not needed or not supported.\")\n                return\n\n        for name in (\"repository_id\", \"crypt_key\", \"id_key\", \"chunk_seed\", \"sessionid\", \"cipher\"):\n            value = getattr(key, name)\n            setattr(key_new, name, value)\n\n        key_new.target = key_new.get_new_target(args)\n        # save with same passphrase and algorithm\n        key_new.save(key_new.target, key._passphrase, create=True, algorithm=key._encrypted_key_algorithm)\n\n        # rewrite the manifest with the new key, so that the key-type byte of the manifest changes\n        manifest.key = key_new\n        manifest.repo_objs.key = key_new\n        manifest.write()\n\n        cache.key = key_new\n\n        loc = key_new.find_key() if hasattr(key_new, \"find_key\") else None\n        if args.keep:\n            logger.info(f\"Key copied to {loc}\")\n        else:\n            key.remove(key.target)  # remove key from current location\n            logger.info(f\"Key moved to {loc}\")\n\n    @with_repository(lock=False, manifest=False, cache=False)\n    def do_key_export(self, args, repository):\n        \"\"\"Exports the repository key for backup.\"\"\"\n        manager = KeyManager(repository)\n        manager.load_keyblob()\n        try:\n            if args.path is not None and os.path.isdir(args.path):\n                # on Windows, Python raises PermissionError instead of IsADirectoryError\n                # (like on Unix) if the file to open is actually a directory.\n                raise IsADirectoryError\n            if args.paper:\n                manager.export_paperkey(args.path)\n            elif args.qr:\n                manager.export_qr(args.path)\n            else:\n                manager.export(args.path)\n        except IsADirectoryError:\n            raise CommandError(f\"'{args.path}' must be a file, not a directory\")\n\n    @with_repository(lock=False, manifest=False, cache=False)\n    def do_key_import(self, args, repository):\n        \"\"\"Imports the repository key from backup.\"\"\"\n        manager = KeyManager(repository)\n        if args.paper:\n            if args.path:\n                raise CommandError(\"with --paper, import from file is not supported\")\n            manager.import_paperkey(args)\n        else:\n            if not args.path:\n                raise CommandError(\"expected input file to import the key from\")\n            if args.path != \"-\" and not os.path.exists(args.path):\n                raise CommandError(f\"input file does not exist: {args.path}\")\n            manager.import_keyfile(args)\n\n    def build_parser_keys(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        subparser = ArgumentParser(\n            parents=[mid_common_parser], description=\"Manage the keyfile or repokey of a repository\", epilog=\"\"\n        )\n        subparsers.add_subcommand(\"key\", subparser, help=\"manage the repository key\")\n\n        key_parsers = subparser.add_subcommands(required=False, title=\"required arguments\", metavar=\"<command>\")\n\n        key_export_epilog = process_epilog(\n            \"\"\"\n        This command backs up the borg key.\n\n        If repository encryption is used, the repository is inaccessible\n        without the borg key (and the passphrase that protects the borg key).\n        If a repository is not encrypted, but authenticated, the borg key is\n        still needed to access the repository normally.\n\n        For repositories using **keyfile** encryption the key is kept locally\n        on the system that is capable of doing backups. To guard against loss\n        or corruption of this key, the key needs to be backed up independently\n        of the main data backup.\n\n        For repositories using **repokey** encryption or **authenticated** mode\n        the key is kept in the repository. A backup is thus not strictly needed,\n        but guards against the repository becoming inaccessible if the key is\n        corrupted or lost.\n\n        Note that the backup produced does not include the passphrase itself\n        (i.e. the exported key stays encrypted). In order to regain access to a\n        repository, one needs both the exported key and the original passphrase.\n        Keep the exported key and the passphrase at safe places.\n\n        There are three backup formats. The normal backup format is suitable for\n        digital storage as a file. The ``--paper`` backup format is optimized\n        for printing and typing in while importing, with per line checks to\n        reduce problems with manual input. The ``--qr-html`` creates a printable\n        HTML template with a QR code and a copy of the ``--paper``-formatted key.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_key_export.__doc__, epilog=key_export_epilog\n        )\n        key_parsers.add_subcommand(\"export\", subparser, help=\"export the repository key for backup\")\n        subparser.add_argument(\"path\", metavar=\"PATH\", nargs=\"?\", type=PathSpec, help=\"where to store the backup\")\n        subparser.add_argument(\n            \"--paper\",\n            dest=\"paper\",\n            action=\"store_true\",\n            help=\"Create an export suitable for printing and later type-in\",\n        )\n        subparser.add_argument(\n            \"--qr-html\",\n            dest=\"qr\",\n            action=\"store_true\",\n            help=\"Create an HTML file suitable for printing and later type-in or QR scan\",\n        )\n\n        key_import_epilog = process_epilog(\n            \"\"\"\n        This command restores a key previously backed up with the export command.\n\n        If the ``--paper`` option is given, the import will be an interactive\n        process in which each line is checked for plausibility before\n        proceeding to the next line. For this format PATH must not be given.\n\n        For repositories using keyfile encryption, the key file which ``borg key\n        import`` writes to depends on several factors. If the ``BORG_KEY_FILE``\n        environment variable is set and non-empty, ``borg key import`` creates\n        or overwrites that file named by ``$BORG_KEY_FILE``. Otherwise, ``borg\n        key import`` searches in the ``$BORG_KEYS_DIR`` directory for a key file\n        associated with the repository. If a key file is found in\n        ``$BORG_KEYS_DIR``, ``borg key import`` overwrites it; otherwise, ``borg\n        key import`` creates a new key file in ``$BORG_KEYS_DIR``.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_key_import.__doc__, epilog=key_import_epilog\n        )\n        key_parsers.add_subcommand(\"import\", subparser, help=\"import the repository key from backup\")\n        subparser.add_argument(\n            \"path\", metavar=\"PATH\", nargs=\"?\", type=PathSpec, help=\"path to the backup ('-' to read from stdin)\"\n        )\n        subparser.add_argument(\n            \"--paper\",\n            dest=\"paper\",\n            action=\"store_true\",\n            help=\"interactively import from a backup done with ``--paper``\",\n        )\n\n        change_passphrase_epilog = process_epilog(\n            \"\"\"\n        The key files used for repository encryption are optionally passphrase\n        protected. This command can be used to change this passphrase.\n\n        Please note that this command only changes the passphrase, but not any\n        secret protected by it (like e.g. encryption/MAC keys or chunker seed).\n        Thus, changing the passphrase after passphrase and borg key got compromised\n        does not protect future (nor past) backups to the same repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog\n        )\n        key_parsers.add_subcommand(\"change-passphrase\", subparser, help=\"change the repository passphrase\")\n\n        change_location_epilog = process_epilog(\n            \"\"\"\n        Change the location of a Borg key. The key can be stored at different locations:\n\n        - keyfile: locally, usually in the home directory\n        - repokey: inside the repository (in the repository config)\n\n        Please note:\n\n        This command does NOT change the crypto algorithms, just the key location,\n        thus you must ONLY give the key location (keyfile or repokey).\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_key_change_location.__doc__, epilog=change_location_epilog\n        )\n        key_parsers.add_subcommand(\"change-location\", subparser, help=\"change the key location\")\n        subparser.add_argument(\n            \"key_mode\", metavar=\"KEY_LOCATION\", choices=(\"repokey\", \"keyfile\"), help=\"select key location\"\n        )\n        subparser.add_argument(\n            \"--keep\",\n            dest=\"keep\",\n            action=\"store_true\",\n            help=\"keep the key also at the current location (default: remove it)\",\n        )\n"
  },
  {
    "path": "src/borg/archiver/list_cmd.py",
    "content": "import os\nimport textwrap\nimport sys\n\nfrom ._common import with_repository, build_matcher, Highlander\nfrom ..archive import Archive\nfrom ..cache import Cache\nfrom ..constants import *  # NOQA\nfrom ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass ListMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    def do_list(self, args, repository, manifest):\n        \"\"\"List archive contents.\"\"\"\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(args.patterns, args.paths)\n        if args.format is not None:\n            format = args.format\n        elif args.short:\n            format = \"{path}{NL}\"\n        else:\n            format = os.environ.get(\"BORG_LIST_FORMAT\", \"{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\")\n\n        archive_info = manifest.archives.get_one([args.name])\n\n        def _list_inner(cache):\n            archive = Archive(manifest, archive_info.id, cache=cache)\n            formatter = ItemFormatter(archive, format)\n\n            def item_filter(item):\n                # Check if the item matches the patterns/paths.\n                if not matcher.match(item.path):\n                    return False\n                # If depth is specified, also check the depth of the path.\n                if args.depth is not None:\n                    # Count path separators to determine depth.\n                    # For paths like \"dir/subdir/file.txt\", the depth is 2.\n                    path_depth = item.path.count(\"/\")\n                    if path_depth > args.depth:\n                        return False\n                return True\n\n            for item in archive.iter_items(item_filter):\n                sys.stdout.write(formatter.format_item(item, args.json_lines, sort=True))\n\n        # Only load the cache if it will be used\n        if ItemFormatter.format_needs_cache(format):\n            with Cache(repository, manifest) as cache:\n                _list_inner(cache)\n        else:\n            _list_inner(cache=None)\n\n    def build_parser_list(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog, define_exclusion_group\n\n        list_epilog = (\n            process_epilog(\n                \"\"\"\n        This command lists the contents of an archive.\n\n        For more help on include/exclude patterns, see the output of :ref:`borg_patterns`.\n\n        .. man NOTES\n\n        The FORMAT specifier syntax\n        +++++++++++++++++++++++++++\n\n        The ``--format`` option uses Python's `format string syntax\n        <https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\n        Examples:\n        ::\n\n            $ borg list --format '{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}' ArchiveFoo\n            -rw-rw-r-- user   user       1024 Thu, 2021-12-09 10:22:17 file-foo\n            ...\n\n            # {VAR:<NUMBER} - pad to NUMBER columns left-aligned.\n            # {VAR:>NUMBER} - pad to NUMBER columns right-aligned.\n            $ borg list --format '{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}' ArchiveFoo\n            -rw-rw-r--   user   user 1024     Thu, 2021-12-09 10:22:17 file-foo\n            ...\n\n        The following keys are always available:\n\n        \"\"\"\n            )\n            + BaseFormatter.keys_help()\n            + textwrap.dedent(\n                \"\"\"\n\n        Keys available only when listing files in an archive:\n\n        \"\"\"\n            )\n            + ItemFormatter.keys_help()\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_list.__doc__, epilog=list_epilog)\n        subparsers.add_subcommand(\"list\", subparser, help=\"list archive contents\")\n        subparser.add_argument(\n            \"--short\", dest=\"short\", action=\"store_true\", help=\"only print file/directory names, nothing else\"\n        )\n        subparser.add_argument(\n            \"--format\",\n            metavar=\"FORMAT\",\n            dest=\"format\",\n            action=Highlander,\n            help=\"specify format for file listing \"\n            '(default: \"{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\")',\n        )\n        subparser.add_argument(\n            \"--json-lines\",\n            action=\"store_true\",\n            help=\"Format output as JSON Lines. \"\n            \"The form of ``--format`` is ignored, \"\n            \"but keys used in it are added to the JSON output. \"\n            \"Some keys are always present. Note: JSON can only represent text.\",\n        )\n        subparser.add_argument(\n            \"--depth\", metavar=\"N\", dest=\"depth\", type=int, help=\"only list files up to the specified directory depth\"\n        )\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=PathSpec, help=\"paths to list; patterns are supported\"\n        )\n        define_exclusion_group(subparser)\n"
  },
  {
    "path": "src/borg/archiver/lock_cmds.py",
    "content": "import subprocess\n\nfrom ._common import with_repository\nfrom ..cache import Cache\nfrom ..constants import *  # NOQA\nfrom ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner\nfrom ..helpers.argparsing import ArgumentParser, REMAINDER\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass LocksMixIn:\n    @with_repository(manifest=False, exclusive=True)\n    def do_with_lock(self, args, repository):\n        \"\"\"Runs a user-specified command with the repository lock held.\"\"\"\n        # the repository lock needs to get refreshed regularly, or it will be killed as stale.\n        # refreshing the lock is not part of the repository API, so we do it indirectly via repository.info.\n        lock_refreshing_thread = ThreadRunner(sleep_interval=60, target=repository.info)\n        lock_refreshing_thread.start()\n        env = prepare_subprocess_env(system=True)\n        try:\n            # we exit with the return code we get from the subprocess\n            rc = subprocess.call([args.command] + args.args, env=env)  # nosec B603\n            set_ec(rc)\n        except (FileNotFoundError, OSError, ValueError) as e:\n            raise CommandError(f\"Failed to execute command: {e}\")\n        finally:\n            lock_refreshing_thread.terminate()\n\n    @with_repository(lock=False, manifest=False)\n    def do_break_lock(self, args, repository):\n        \"\"\"Breaks the repository lock (for example, if it was left by a dead Borg process).\"\"\"\n        repository.break_lock()\n        Cache.break_lock(repository)\n\n    def build_parser_locks(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        break_lock_epilog = process_epilog(\n            \"\"\"\n        This command breaks the repository and cache locks.\n        Use with care and only when no Borg process (on any machine) is\n        trying to access the cache or the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_break_lock.__doc__, epilog=break_lock_epilog\n        )\n        subparsers.add_subcommand(\"break-lock\", subparser, help=\"break the repository and cache locks\")\n\n        with_lock_epilog = process_epilog(\n            \"\"\"\n        This command runs a user-specified command while locking the repository. For example:\n\n        ::\n\n            $ BORG_REPO=/mnt/borgrepo borg with-lock rsync -av /mnt/borgrepo /somewhere/else/borgrepo\n\n        It first tries to acquire the lock (make sure that no other operation is\n        running in the repository), then executes the given command as a subprocess and waits\n        for its termination, releases the lock, and returns the user command's return\n        code as Borg's return code.\n\n        .. note::\n\n            If you copy a repository with the lock held, the lock will be present in\n            the copy. Before using Borg on the copy from a different host,\n            you need to run ``borg break-lock`` on the copied repository, because\n            Borg is cautious and does not automatically remove stale locks made by a different host.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_with_lock.__doc__, epilog=with_lock_epilog\n        )\n        subparsers.add_subcommand(\"with-lock\", subparser, help=\"run a user command with the lock held\")\n        subparser.add_argument(\"command\", metavar=\"COMMAND\", help=\"command to run\")\n        subparser.add_argument(\"args\", metavar=\"ARGS\", nargs=REMAINDER, help=\"command arguments\")\n"
  },
  {
    "path": "src/borg/archiver/mount_cmds.py",
    "content": "import os\n\nfrom ._common import with_repository, Highlander\nfrom ..constants import *  # NOQA\nfrom ..helpers import RTError\nfrom ..helpers import PathSpec\nfrom ..helpers import umount\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\nfrom ..remote import cache_if_remote\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass MountMixIn:\n    def do_mount(self, args):\n        \"\"\"Mounts an archive or an entire repository as a FUSE filesystem.\"\"\"\n        # Perform these checks before opening the repository and asking for a passphrase.\n\n        from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL\n\n        if llfuse is None and not has_mfusepy:\n            raise RTError(\"borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s.\" % BORG_FUSE_IMPL)\n\n        if not os.path.isdir(args.mountpoint):\n            raise RTError(f\"{args.mountpoint}: Mountpoint must be an **existing directory**\")\n\n        if not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):\n            raise RTError(f\"{args.mountpoint}: Mountpoint must be a **writable** directory\")\n\n        self._do_mount(args)\n\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    def _do_mount(self, args, repository, manifest):\n        from ..fuse_impl import has_mfusepy\n\n        if has_mfusepy:\n            # Use mfusepy implementation\n            from ..hlfuse import borgfs\n\n            operations = borgfs(manifest, args, repository)\n            logger.info(\"Mounting filesystem\")\n            try:\n                operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)\n            except RuntimeError:\n                # Relevant error message already printed to stderr by FUSE\n                raise RTError(\"FUSE mount failed\")\n        else:\n            # Use llfuse/pyfuse3 implementation\n            from ..fuse import FuseOperations\n\n            with cache_if_remote(repository, decrypted_cache=manifest.repo_objs) as cached_repo:\n                operations = FuseOperations(manifest, args, cached_repo)\n                logger.info(\"Mounting filesystem\")\n                try:\n                    operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)\n                except RuntimeError:\n                    # Relevant error message already printed to stderr by FUSE\n                    raise RTError(\"FUSE mount failed\")\n\n    def do_umount(self, args):\n        \"\"\"Unmounts the FUSE filesystem.\"\"\"\n        umount(args.mountpoint)\n\n    def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        mount_epilog = process_epilog(\n            \"\"\"\n        This command mounts a repository or an archive as a FUSE filesystem.\n        This can be useful for browsing or restoring individual files.\n\n        When restoring, take into account that the current FUSE implementation does\n        not support special fs flags and ACLs.\n\n        When mounting a repository, the top directories will be named like the\n        archives and the directory structure below these will be loaded on-demand from\n        the repository when entering these directories, so expect some delay.\n\n        Care should be taken, as Borg backs up symlinks as-is. When an archive \n        or repository is mounted, it is possible to “jump” outside the mount point \n        by following a symlink. If this happens, files or directories (or versions of them)\n        that are not part of the archive or repository may appear to be within the mount point.\n\n        Unless the ``--foreground`` option is given, the command will run in the\n        background until the filesystem is ``unmounted``.\n\n        Performance tips:\n\n        - When doing a \"whole repository\" mount:\n          do not enter archive directories if not needed; this avoids on-demand loading.\n        - Only mount a specific archive, not the whole repository.\n        - Only mount specific paths in a specific archive, not the complete archive.\n\n        The command ``borgfs`` provides a wrapper for ``borg mount``. This can also be\n        used in fstab entries:\n        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0``\n\n        To allow a regular user to use fstab entries, add the ``user`` option:\n        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``\n\n        For FUSE configuration and mount options, see the mount.fuse(8) manual page.\n\n        Borg's default behavior is to use the archived user and group names of each\n        file and map them to the system's respective user and group IDs.\n        Alternatively, using ``numeric-ids`` will instead use the archived user and\n        group IDs without any mapping.\n\n        The ``uid`` and ``gid`` mount options (implemented by Borg) can be used to\n        override the user and group IDs of all files (i.e., ``borg mount -o\n        uid=1000,gid=1000``).\n\n        The man page references ``user_id`` and ``group_id`` mount options\n        (implemented by FUSE) which specify the user and group ID of the mount owner\n        (also known as the user who does the mounting). It is set automatically by libfuse (or\n        the filesystem if libfuse is not used). However, you should not specify these\n        manually. Unlike the ``uid`` and ``gid`` mount options, which affect all files,\n        ``user_id`` and ``group_id`` affect the user and group ID of the mounted\n        (base) directory.\n\n        Additional mount options supported by Borg:\n\n        - ``versions``: when used with a repository mount, this gives a merged, versioned\n          view of the files in the archives. EXPERIMENTAL; layout may change in the future.\n        - ``allow_damaged_files``: by default, damaged files (where chunks are missing)\n          will return EIO (I/O error) when trying to read the related parts of the file.\n          Set this option to replace the missing parts with all-zero bytes.\n        - ``ignore_permissions``: for security reasons the ``default_permissions`` mount\n          option is internally enforced by Borg. ``ignore_permissions`` can be given to\n          not enforce ``default_permissions``.\n\n        The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is intended for advanced users\n        to tweak performance. It sets the number of cached data chunks; additional\n        memory usage can be up to ~8 MiB times this number. The default is the number\n        of CPU cores.\n\n        When the daemonized process receives a signal or crashes, it does not unmount.\n        Unmounting in these cases could cause an active rsync or similar process\n        to delete data unintentionally.\n\n        When running in the foreground, ^C/SIGINT cleanly unmounts the filesystem,\n        but other signals or crashes do not.\n\n        Debugging:\n\n        ``borg mount`` usually daemonizes and the daemon process sends stdout/stderr\n        to /dev/null. Thus, you need to either use ``-f / --foreground`` to make it stay\n        in the foreground and not daemonize, or use ``BORG_LOGGING_CONF`` to reconfigure\n        the logger to output to a file.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_mount.__doc__, epilog=mount_epilog)\n        subparsers.add_subcommand(\"mount\", subparser, help=\"mount a repository\")\n        self._define_borg_mount(subparser)\n\n        umount_epilog = process_epilog(\n            \"\"\"\n        This command unmounts a FUSE filesystem that was mounted with ``borg mount``.\n\n        This is a convenience wrapper that just calls the platform-specific shell\n        command - usually this is either umount or fusermount -u.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_umount.__doc__, epilog=umount_epilog)\n        subparsers.add_subcommand(\"umount\", subparser, help=\"unmount a repository\")\n        subparser.add_argument(\n            \"mountpoint\", metavar=\"MOUNTPOINT\", type=str, help=\"mountpoint of the filesystem to unmount\"\n        )\n\n    def build_parser_borgfs(self, parser):\n        assert parser.prog == \"borgfs\"\n        parser.description = self.do_mount.__doc__\n        parser.epilog = \"For more information, see borg mount --help.\"\n        parser.help = \"mount a repository\"\n        self._define_borg_mount(parser)\n        return parser\n\n    def _define_borg_mount(self, parser):\n        from ._common import define_exclusion_group, define_archive_filters_group\n\n        parser.add_argument(\"mountpoint\", metavar=\"MOUNTPOINT\", type=str, help=\"where to mount the filesystem\")\n        parser.add_argument(\n            \"-f\", \"--foreground\", dest=\"foreground\", action=\"store_true\", help=\"stay in foreground, do not daemonize\"\n        )\n        parser.add_argument(\"-o\", dest=\"options\", type=str, action=Highlander, help=\"extra mount options\")\n        parser.add_argument(\n            \"--numeric-ids\",\n            dest=\"numeric_ids\",\n            action=\"store_true\",\n            help=\"use numeric user and group identifiers from archives\",\n        )\n        define_archive_filters_group(parser)\n        parser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=PathSpec, help=\"paths to extract; patterns are supported\"\n        )\n        define_exclusion_group(parser, strip_components=True)\n"
  },
  {
    "path": "src/borg/archiver/prune_cmd.py",
    "content": "from collections import OrderedDict\nfrom datetime import datetime, timezone, timedelta\nimport logging\nfrom operator import attrgetter\nimport os\n\nfrom ._common import with_repository, Highlander\nfrom ..constants import *  # NOQA\nfrom ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error\nfrom ..helpers import archivename_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\ndef prune_within(archives, seconds, kept_because):\n    target = datetime.now(timezone.utc) - timedelta(seconds=seconds)\n    kept_counter = 0\n    result = []\n    for a in archives:\n        if a.ts > target:\n            kept_counter += 1\n            kept_because[a.id] = (\"within\", kept_counter)\n            result.append(a)\n    return result\n\n\ndef default_period_func(pattern):\n    def inner(a):\n        # compute in local timezone\n        return a.ts.astimezone().strftime(pattern)\n\n    return inner\n\n\ndef quarterly_13weekly_period_func(a):\n    (year, week, _) = a.ts.astimezone().isocalendar()  # local time\n    if week <= 13:\n        # Weeks containing Jan 4th to Mar 28th (leap year) or 29th- 91 (13*7)\n        # days later.\n        return (year, 1)\n    elif 14 <= week <= 26:\n        # Weeks containing Apr 4th (leap year) or 5th to Jun 27th or 28th- 91\n        # days later.\n        return (year, 2)\n    elif 27 <= week <= 39:\n        # Weeks containing Jul 4th (leap year) or 5th to Sep 26th or 27th-\n        # at least 91 days later.\n        return (year, 3)\n    else:\n        # Everything else, Oct 3rd (leap year) or 4th onward, will always\n        # include week of Dec 26th (leap year) or Dec 27th, may also include\n        # up to possibly Jan 3rd of next year.\n        return (year, 4)\n\n\ndef quarterly_3monthly_period_func(a):\n    lt = a.ts.astimezone()  # local time\n    if lt.month <= 3:\n        # 1-1 to 3-31\n        return (lt.year, 1)\n    elif 4 <= lt.month <= 6:\n        # 4-1 to 6-30\n        return (lt.year, 2)\n    elif 7 <= lt.month <= 9:\n        # 7-1 to 9-30\n        return (lt.year, 3)\n    else:\n        # 10-1 to 12-31\n        return (lt.year, 4)\n\n\nPRUNING_PATTERNS = OrderedDict(\n    [\n        (\"secondly\", default_period_func(\"%Y-%m-%d %H:%M:%S\")),\n        (\"minutely\", default_period_func(\"%Y-%m-%d %H:%M\")),\n        (\"hourly\", default_period_func(\"%Y-%m-%d %H\")),\n        (\"daily\", default_period_func(\"%Y-%m-%d\")),\n        (\"weekly\", default_period_func(\"%G-%V\")),\n        (\"monthly\", default_period_func(\"%Y-%m\")),\n        (\"quarterly_13weekly\", quarterly_13weekly_period_func),\n        (\"quarterly_3monthly\", quarterly_3monthly_period_func),\n        (\"yearly\", default_period_func(\"%Y\")),\n    ]\n)\n\n\ndef prune_split(archives, rule, n, kept_because=None):\n    last = None\n    keep = []\n    period_func = PRUNING_PATTERNS[rule]\n    if kept_because is None:\n        kept_because = {}\n    if n == 0:\n        return keep\n\n    a = None\n    for a in sorted(archives, key=attrgetter(\"ts\"), reverse=True):\n        period = period_func(a)\n        if period != last:\n            last = period\n            if a.id not in kept_because:\n                keep.append(a)\n                kept_because[a.id] = (rule, len(keep))\n                if len(keep) == n:\n                    break\n    # Keep oldest archive if we didn't reach the target retention count\n    if a is not None and len(keep) < n and a.id not in kept_because:\n        keep.append(a)\n        kept_because[a.id] = (rule + \"[oldest]\", len(keep))\n    return keep\n\n\nclass PruneMixIn:\n    @with_repository(compatibility=(Manifest.Operation.DELETE,))\n    def do_prune(self, args, repository, manifest):\n        \"\"\"Prune archives according to specified rules.\"\"\"\n        if not any(\n            (\n                args.secondly,\n                args.minutely,\n                args.hourly,\n                args.daily,\n                args.weekly,\n                args.monthly,\n                args.quarterly_13weekly,\n                args.quarterly_3monthly,\n                args.yearly,\n                args.within,\n            )\n        ):\n            raise CommandError(\n                'At least one of the \"keep-within\", \"keep-last\", '\n                '\"keep-secondly\", \"keep-minutely\", \"keep-hourly\", \"keep-daily\", '\n                '\"keep-weekly\", \"keep-monthly\", \"keep-13weekly\", \"keep-3monthly\", '\n                'or \"keep-yearly\" settings must be specified.'\n            )\n\n        if args.format is not None:\n            format = args.format\n        elif args.short:\n            format = \"{archive}\"\n        else:\n            format = os.environ.get(\"BORG_PRUNE_FORMAT\", \"{archive:<36} {time} [{id}]\")\n        formatter = ArchiveFormatter(format, repository, manifest, manifest.key, iec=args.iec)\n\n        match = [args.name] if args.name else args.match_archives\n        archives = manifest.archives.list(match=match, sort_by=[\"ts\"], reverse=True)\n        archives = [ai for ai in archives if \"@PROT\" not in ai.tags]\n\n        keep = []\n        # collect the rule responsible for the keeping of each archive in this dict\n        # keys are archive ids, values are a tuple\n        #   (<rulename>, <how many archives were kept by this rule so far >)\n        kept_because = {}\n\n        # find archives which need to be kept because of the keep-within rule\n        if args.within:\n            keep += prune_within(archives, args.within, kept_because)\n\n        # find archives which need to be kept because of the various time period rules\n        for rule in PRUNING_PATTERNS.keys():\n            num = getattr(args, rule, None)\n            if num is not None:\n                keep += prune_split(archives, rule, num, kept_because)\n\n        to_delete = set(archives) - set(keep)\n        logger.info(\"Found %d archives.\", len(archives))\n        logger.info(\"Keeping %d archives, pruning %d archives.\", len(keep), len(to_delete))\n        list_logger = logging.getLogger(\"borg.output.list\")\n        # set up counters for the progress display\n        to_delete_len = len(to_delete)\n        archives_deleted = 0\n        pi = ProgressIndicatorPercent(total=len(to_delete), msg=\"Pruning archives %3.0f%%\", msgid=\"prune\")\n        for archive_info in archives:\n            if sig_int and sig_int.action_done():\n                break\n            # format_item may internally load the archive from the repository,\n            # so we must call it before deleting the archive.\n            archive_formatted = formatter.format_item(archive_info, jsonline=False)\n            if archive_info in to_delete:\n                pi.show()\n                if args.dry_run:\n                    log_message = \"Would prune:\"\n                else:\n                    log_message = \"Pruning archive (%d/%d):\" % (archives_deleted, to_delete_len)\n                    manifest.archives.delete_by_id(archive_info.id)\n                    archives_deleted += 1\n            else:\n                log_message = \"Keeping archive (rule: {rule} #{num}):\".format(\n                    rule=kept_because[archive_info.id][0], num=kept_because[archive_info.id][1]\n                )\n            if (\n                args.output_list\n                or (args.list_pruned and archive_info in to_delete)\n                or (args.list_kept and archive_info not in to_delete)\n            ):\n                list_logger.info(f\"{log_message:<44} {archive_formatted}\")\n        pi.finish()\n        if archives_deleted > 0:\n            manifest.write()\n            self.print_warning('Done. Run \"borg compact\" to free space.', wc=None)\n        if sig_int:\n            raise Error(\"Got Ctrl-C / SIGINT.\")\n\n    def build_parser_prune(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_archive_filters_group\n\n        prune_epilog = process_epilog(\n            \"\"\"\n        The prune command prunes a repository by soft-deleting all archives not\n        matching any of the specified retention options.\n\n        Important:\n\n        - The prune command will only mark archives for deletion (\"soft-deletion\"),\n          repository disk space is **not** freed until you run ``borg compact``.\n        - You can use ``borg undelete`` to undelete archives, but only until\n          you run ``borg compact``.\n\n        This command is normally used by automated backup scripts wanting to keep a\n        certain number of historic backups. This retention policy is commonly referred to as\n        `GFS <https://en.wikipedia.org/wiki/Backup_rotation_scheme#Grandfather-father-son>`_\n        (Grandfather-father-son) backup rotation scheme.\n\n        The recommended way to use prune is to give the archive series name to it via the\n        NAME argument (assuming you have the same name for all archives in a series).\n        Alternatively, you can also use --match-archives (-a), then only archives that\n        match the pattern are considered for deletion and only those archives count\n        towards the totals specified by the rules.\n        Otherwise, *all* archives in the repository are candidates for deletion!\n        There is no automatic distinction between archives representing different\n        contents. These need to be distinguished by specifying matching globs.\n\n        If you have multiple series of archives with different data sets (e.g.\n        from different machines) in one shared repository, use one prune call per\n        series.\n\n        The ``--keep-within`` option takes an argument of the form \"<int><char>\",\n        where char is \"y\", \"m\", \"w\", \"d\", \"H\", \"M\", or \"S\".  For example,\n        ``--keep-within 2d`` means to keep all archives that were created within\n        the past 2 days.  \"1m\" is taken to mean \"31d\". The archives kept with\n        this option do not count towards the totals specified by any other options.\n\n        A good procedure is to thin out more and more the older your backups get.\n        As an example, ``--keep-daily 7`` means to keep the latest backup on each day,\n        up to 7 most recent days with backups (days without backups do not count).\n        The rules are applied from secondly to yearly, and backups selected by previous\n        rules do not count towards those of later rules. The time that each backup\n        starts is used for pruning purposes. Dates and times are interpreted in the local\n        timezone of the system where borg prune runs, and weeks go from Monday to Sunday.\n        Specifying a negative number of archives to keep means that there is no limit.\n\n        Borg will retain the oldest archive if any of the secondly, minutely, hourly,\n        daily, weekly, monthly, quarterly, or yearly rules was not otherwise able to\n        meet its retention target. This enables the first chronological archive to\n        continue aging until it is replaced by a newer archive that meets the retention\n        criteria.\n\n        The ``--keep-13weekly`` and ``--keep-3monthly`` rules are two different\n        strategies for keeping archives every quarter year.\n\n        The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will\n        keep the last N archives under the assumption that you do not create more than one\n        backup archive in the same second).\n\n        You can influence how the ``--list`` output is formatted by using the ``--short``\n        option (less wide output) or by giving a custom format using ``--format`` (see\n        the ``borg repo-list`` description for more details about the format string).\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_prune.__doc__, epilog=prune_epilog)\n        subparsers.add_subcommand(\"prune\", subparser, help=\"prune archives\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change the repository\"\n        )\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of archives it keeps/prunes\"\n        )\n        subparser.add_argument(\"--short\", dest=\"short\", action=\"store_true\", help=\"use a less wide archive part format\")\n        subparser.add_argument(\n            \"--list-pruned\", dest=\"list_pruned\", action=\"store_true\", help=\"output verbose list of archives it prunes\"\n        )\n        subparser.add_argument(\n            \"--list-kept\", dest=\"list_kept\", action=\"store_true\", help=\"output verbose list of archives it keeps\"\n        )\n        subparser.add_argument(\n            \"--format\",\n            metavar=\"FORMAT\",\n            dest=\"format\",\n            action=Highlander,\n            help=\"specify format for the archive part \" '(default: \"{archive:<36} {time} [{id}]\")',\n        )\n        subparser.add_argument(\n            \"--keep-within\",\n            metavar=\"INTERVAL\",\n            dest=\"within\",\n            type=interval,\n            action=Highlander,\n            help=\"keep all archives within this time interval\",\n        )\n        subparser.add_argument(\n            \"--keep-last\",\n            \"--keep-secondly\",\n            dest=\"secondly\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of secondly archives to keep\",\n        )\n        subparser.add_argument(\n            \"--keep-minutely\",\n            dest=\"minutely\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of minutely archives to keep\",\n        )\n        subparser.add_argument(\n            \"-H\",\n            \"--keep-hourly\",\n            dest=\"hourly\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of hourly archives to keep\",\n        )\n        subparser.add_argument(\n            \"-d\",\n            \"--keep-daily\",\n            dest=\"daily\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of daily archives to keep\",\n        )\n        subparser.add_argument(\n            \"-w\",\n            \"--keep-weekly\",\n            dest=\"weekly\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of weekly archives to keep\",\n        )\n        subparser.add_argument(\n            \"-m\",\n            \"--keep-monthly\",\n            dest=\"monthly\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of monthly archives to keep\",\n        )\n        quarterly_group = subparser.add_mutually_exclusive_group()\n        quarterly_group.add_argument(\n            \"--keep-13weekly\",\n            dest=\"quarterly_13weekly\",\n            type=int,\n            default=0,\n            help=\"number of quarterly archives to keep (13 week strategy)\",\n        )\n        quarterly_group.add_argument(\n            \"--keep-3monthly\",\n            dest=\"quarterly_3monthly\",\n            type=int,\n            default=0,\n            help=\"number of quarterly archives to keep (3 month strategy)\",\n        )\n        subparser.add_argument(\n            \"-y\",\n            \"--keep-yearly\",\n            dest=\"yearly\",\n            type=int,\n            default=0,\n            action=Highlander,\n            help=\"number of yearly archives to keep\",\n        )\n        define_archive_filters_group(subparser, sort_by=False, first_last=False)\n        subparser.add_argument(\n            \"name\", metavar=\"NAME\", nargs=\"?\", type=archivename_validator, help=\"specify the archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/recreate_cmd.py",
    "content": "from ._common import with_repository, Highlander\nfrom ._common import build_matcher\nfrom ..archive import ArchiveRecreater\nfrom ..constants import *  # NOQA\nfrom ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex, CompressionSpec\nfrom ..helpers import timestamp\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RecreateMixIn:\n    @with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,))\n    def do_recreate(self, args, repository, manifest, cache):\n        \"\"\"Recreate archives.\"\"\"\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(args.patterns, args.paths)\n        self.output_list = args.output_list\n        self.output_filter = args.output_filter\n\n        recreater = ArchiveRecreater(\n            manifest,\n            cache,\n            matcher,\n            exclude_caches=args.exclude_caches,\n            exclude_if_present=args.exclude_if_present,\n            keep_exclude_tags=args.keep_exclude_tags,\n            chunker_params=args.chunker_params,\n            compression=args.compression,\n            progress=args.progress,\n            stats=args.stats,\n            file_status_printer=self.print_file_status,\n            dry_run=args.dry_run,\n            timestamp=args.timestamp,\n        )\n        archive_infos = manifest.archives.list_considering(args)\n        archive_infos = [ai for ai in archive_infos if \"@PROT\" not in ai.tags]\n        for archive_info in archive_infos:\n            if recreater.is_temporary_archive(archive_info.name):\n                continue\n            name, hex_id = archive_info.name, bin_to_hex(archive_info.id)\n            print(f\"Processing {name} {hex_id}\")\n            if args.target:\n                target = args.target\n                delete_original = False\n            else:\n                target = archive_info.name\n                delete_original = True\n            if not recreater.recreate(archive_info.id, target, delete_original, args.comment):\n                logger.info(f\"Skipped archive {name} {hex_id}: Nothing to do.\")\n        if not args.dry_run:\n            manifest.write()\n\n    def build_parser_recreate(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_exclusion_group, define_archive_filters_group\n\n        recreate_epilog = process_epilog(\n            \"\"\"\n        Recreate the contents of existing archives.\n\n        Recreate is a potentially dangerous function and might lead to data loss\n        (if used wrongly). BE VERY CAREFUL!\n\n        Important: Repository disk space is **not** freed until you run ``borg compact``.\n\n        ``--exclude``, ``--exclude-from``, ``--exclude-if-present``, ``--keep-exclude-tags``\n        and PATH have the exact same semantics as in \"borg create\", but they only check\n        files in the archives and not in the local filesystem. If paths are specified,\n        the resulting archives will contain only files from those paths.\n\n        Note that all paths in an archive are relative, therefore absolute patterns/paths\n        will *not* match (``--exclude``, ``--exclude-from``, PATHs).\n\n        ``--chunker-params`` will re-chunk all files in the archive, this can be\n        used to have upgraded Borg 0.xx archives deduplicate with Borg 1.x archives.\n\n        **USE WITH CAUTION.**\n        Depending on the paths and patterns given, recreate can be used to\n        delete files from archives permanently.\n        When in doubt, use ``--dry-run --verbose --list`` to see how patterns/paths are\n        interpreted. See :ref:`list_item_flags` in ``borg create`` for details.\n\n        The archive being recreated is only removed after the operation completes. The\n        archive that is built during the operation exists at the same time at\n        \"<ARCHIVE>.recreate\". The new archive will have a different archive ID.\n\n        With ``--target`` the original archive is not replaced, instead a new archive is created.\n\n        When rechunking, space usage can be substantial - expect\n        at least the entire deduplicated size of the archives using the previous\n        chunker params.\n\n        If your most recent borg check found missing chunks, please first run another\n        backup for the same data, before doing any rechunking. If you are lucky, that\n        will recreate the missing chunks. Optionally, do another borg check to see\n        if the chunks are still missing.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_recreate.__doc__, epilog=recreate_epilog\n        )\n        subparsers.add_subcommand(\"recreate\", subparser, help=self.do_recreate.__doc__)\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output verbose list of items (files, dirs, ...)\"\n        )\n        subparser.add_argument(\n            \"--filter\",\n            metavar=\"STATUSCHARS\",\n            dest=\"output_filter\",\n            action=Highlander,\n            help=\"only display items with the given status characters (listed in borg create --help)\",\n        )\n        subparser.add_argument(\"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change anything\")\n        subparser.add_argument(\"-s\", \"--stats\", dest=\"stats\", action=\"store_true\", help=\"print statistics at end\")\n\n        define_exclusion_group(subparser, tag_files=True)\n\n        archive_group = define_archive_filters_group(subparser)\n        archive_group.add_argument(\n            \"--target\",\n            dest=\"target\",\n            metavar=\"TARGET\",\n            default=None,\n            type=archivename_validator,\n            action=Highlander,\n            help=\"create a new archive with the name ARCHIVE, do not replace existing archive\",\n        )\n        archive_group.add_argument(\n            \"--comment\",\n            metavar=\"COMMENT\",\n            dest=\"comment\",\n            type=comment_validator,\n            default=None,\n            action=Highlander,\n            help=\"add a comment text to the archive\",\n        )\n        archive_group.add_argument(\n            \"--timestamp\",\n            metavar=\"TIMESTAMP\",\n            dest=\"timestamp\",\n            type=timestamp,\n            default=None,\n            action=Highlander,\n            help=\"manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, \"\n            \"(+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\",\n        )\n        archive_group.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n        archive_group.add_argument(\n            \"--chunker-params\",\n            metavar=\"PARAMS\",\n            dest=\"chunker_params\",\n            type=ChunkerParams,\n            default=None,\n            action=Highlander,\n            help=\"rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, \"\n            \"HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. \"\n            \"default: do not rechunk\",\n        )\n\n        subparser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=PathSpec, help=\"paths to recreate; patterns are supported\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/rename_cmd.py",
    "content": "from ._common import with_repository, with_archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import archivename_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RenameMixIn:\n    @with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,))\n    @with_archive\n    def do_rename(self, args, repository, manifest, cache, archive):\n        \"\"\"Rename an existing archive.\"\"\"\n        archive.rename(args.newname)\n        manifest.write()\n\n    def build_parser_rename(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        rename_epilog = process_epilog(\n            \"\"\"\n        This command renames an archive in the repository.\n\n        This results in a different archive ID.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_rename.__doc__, epilog=rename_epilog)\n        subparsers.add_subcommand(\"rename\", subparser, help=\"rename an archive\")\n        subparser.add_argument(\n            \"name\", metavar=\"OLDNAME\", type=archivename_validator, help=\"specify the current archive name\"\n        )\n        subparser.add_argument(\n            \"newname\", metavar=\"NEWNAME\", type=archivename_validator, help=\"specify the new archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/repo_compress_cmd.py",
    "content": "from collections import defaultdict\n\nfrom ._common import with_repository, Highlander\nfrom ..constants import *  # NOQA\nfrom ..compress import ObfuscateSize, Auto, COMPRESSOR_TABLE\nfrom ..hashindex import ChunkIndex\nfrom ..helpers import sig_int, ProgressIndicatorPercent, Error, CompressionSpec\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..repository import Repository\nfrom ..remote import RemoteRepository\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\ndef find_chunks(repository, repo_objs, cache, stats, ctype, clevel, olevel):\n    \"\"\"Find and flag chunks that need processing (usually: recompression).\"\"\"\n    compr_keys = stats[\"compr_keys\"] = set()\n    compr_wanted = ctype, clevel, olevel\n    recompress_count = 0\n    for id, cie in cache.chunks.iteritems():\n        chunk_no_data = repository.get(id, read_data=False)\n        meta = repo_objs.parse_meta(id, chunk_no_data, ro_type=ROBJ_DONTCARE)\n        compr_found = meta[\"ctype\"], meta[\"clevel\"], meta.get(\"olevel\", -1)\n        if compr_found != compr_wanted:\n            flags_compress = cie.flags | ChunkIndex.F_COMPRESS\n            cache.chunks[id] = cie._replace(flags=flags_compress)\n            recompress_count += 1\n        compr_keys.add(compr_found)\n        stats[compr_found] += 1\n        stats[\"checked_count\"] += 1\n    return recompress_count\n\n\ndef process_chunks(repository, repo_objs, stats, recompress_ids, olevel):\n    \"\"\"Process some chunks (usually: recompress).\"\"\"\n    compr_keys = stats[\"compr_keys\"]\n    if compr_keys == 0:  # work around defaultdict(int)\n        compr_keys = stats[\"compr_keys\"] = set()\n    for id, chunk in zip(recompress_ids, repository.get_many(recompress_ids, read_data=True)):\n        old_size = len(chunk)\n        stats[\"old_size\"] += old_size\n        meta, data = repo_objs.parse(id, chunk, ro_type=ROBJ_DONTCARE)\n        ro_type = meta.pop(\"type\", None)\n        compr_old = meta[\"ctype\"], meta[\"clevel\"], meta.get(\"olevel\", -1)\n        if olevel == -1:\n            # if the chunk was obfuscated, but should not be in future, remove related metadata\n            meta.pop(\"olevel\", None)\n            meta.pop(\"psize\", None)\n        chunk = repo_objs.format(id, meta, data, ro_type=ro_type)\n        compr_done = meta[\"ctype\"], meta[\"clevel\"], meta.get(\"olevel\", -1)\n        if compr_done != compr_old:\n            # we actually changed something\n            repository.put(id, chunk, wait=False)\n            repository.async_response(wait=False)\n            stats[\"new_size\"] += len(chunk)\n            compr_keys.add(compr_done)\n            stats[compr_done] += 1\n            stats[\"recompressed_count\"] += 1\n        else:\n            # It might be that the old chunk used compression none or lz4 (for whatever reason,\n            # including the old compressor being a DecidingCompressor) AND we used a\n            # DecidingCompressor now, which did NOT compress like we wanted, but decided\n            # to use the same compression (and obfuscation) we already had.\n            # In this case, we just keep the old chunk and do not rewrite it -\n            # This is important to avoid rewriting such chunks **again and again**.\n            stats[\"new_size\"] += old_size\n            compr_keys.add(compr_old)\n            stats[compr_old] += 1\n            stats[\"kept_count\"] += 1\n\n\ndef format_compression_spec(ctype, clevel, olevel):\n    obfuscation = \"\" if olevel == -1 else f\"obfuscate,{olevel},\"\n    for cname, cls in COMPRESSOR_TABLE.items():\n        if cls.ID == ctype:\n            cname = f\"{cname}\"\n            break\n    else:\n        cname = f\"{ctype}\"\n    clevel = f\",{clevel}\" if clevel != 255 else \"\"\n    return obfuscation + cname + clevel\n\n\nclass RepoCompressMixIn:\n    @with_repository(cache=True, manifest=True, compatibility=(Manifest.Operation.CHECK,))\n    def do_repo_compress(self, args, repository, manifest, cache):\n        \"\"\"Repository (re-)compression.\"\"\"\n\n        def get_csettings(c):\n            if isinstance(c, Auto):\n                return get_csettings(c.compressor)\n            if isinstance(c, ObfuscateSize):\n                ctype, clevel, _ = get_csettings(c.compressor)\n                olevel = c.level\n                return ctype, clevel, olevel\n            ctype, clevel, olevel = c.ID, c.level, -1\n            return ctype, clevel, olevel\n\n        if not isinstance(repository, (Repository, RemoteRepository)):\n            raise Error(\"repo-compress not supported for legacy repositories.\")\n\n        repo_objs = manifest.repo_objs\n        ctype, clevel, olevel = get_csettings(repo_objs.compressor)  # desired compression set by --compression\n\n        stats_find = defaultdict(int)\n        stats_process = defaultdict(int)\n        recompress_candidate_count = find_chunks(repository, repo_objs, cache, stats_find, ctype, clevel, olevel)\n\n        pi = ProgressIndicatorPercent(\n            total=recompress_candidate_count,\n            msg=\"Recompressing %3.1f%%\",\n            step=0.1,\n            msgid=\"repo_compress.process_chunks\",\n        )\n        for id, cie in cache.chunks.iteritems():\n            if sig_int and sig_int.action_done():\n                break\n            if cie.flags & ChunkIndex.F_COMPRESS:\n                process_chunks(repository, repo_objs, stats_process, [id], olevel)\n            pi.show()\n        pi.finish()\n        if sig_int:\n            # Ctrl-C / SIGINT: do not commit\n            raise Error(\"Got Ctrl-C / SIGINT.\")\n        else:\n            while repository.async_response(wait=True) is not None:\n                pass\n        if args.stats:\n            print()\n            print(\"Recompression stats:\")\n            print(f\"Size: previously {stats_process['old_size']} -> now {stats_process['new_size']} bytes.\")\n            print(\n                f\"Change: \"\n                f\"{stats_process['new_size'] - stats_process['old_size']} bytes == \"\n                f\"{100.0 * stats_process['new_size'] / stats_process['old_size']:3.2f}%\"\n            )\n            print(\"Found chunks stats (before processing):\")\n            for ck in stats_find[\"compr_keys\"]:\n                pretty_ck = format_compression_spec(*ck)\n                print(f\"{pretty_ck}: {stats_find[ck]}\")\n            print(f\"Total: {stats_find['checked_count']}\")\n\n            print(f\"Candidates for recompression: {recompress_candidate_count}\")\n\n            print(\"Processed chunks stats (after processing):\")\n            for ck in stats_process[\"compr_keys\"]:\n                pretty_ck = format_compression_spec(*ck)\n                print(f\"{pretty_ck}: {stats_process[ck]}\")\n            print(f\"Recompressed and rewritten: {stats_process['recompressed_count']}\")\n            print(f\"Kept as is: {stats_process['kept_count']}\")\n            print(f\"Total: {stats_process['recompressed_count'] + stats_process['kept_count']}\")\n\n    def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        repo_compress_epilog = process_epilog(\n            \"\"\"\n        Repository (re-)compression (and/or re-obfuscation).\n\n        Reads all chunks in the repository and recompresses them if they are not already\n        using the compression type/level and obfuscation level given via ``--compression``.\n\n        If the outcome of the chunk processing indicates a change in compression\n        type/level or obfuscation level, the processed chunk is written to the repository.\n        Please note that the outcome might not always be the desired compression\n        type/level - if no compression gives a shorter output, that might be chosen.\n\n        Please note that this command can not work in low (or zero) free disk space\n        conditions.\n\n        If the ``borg repo-compress`` process receives a SIGINT signal (Ctrl-C), the repo\n        will be committed and compacted and borg will terminate cleanly afterwards.\n\n        Both ``--progress`` and ``--stats`` are recommended when ``borg repo-compress``\n        is used interactively.\n\n        You do **not** need to run ``borg compact`` after ``borg repo-compress``.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_compress.__doc__, epilog=repo_compress_epilog\n        )\n        subparsers.add_subcommand(\"repo-compress\", subparser, help=self.do_repo_compress.__doc__)\n\n        subparser.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n\n        subparser.add_argument(\"-s\", \"--stats\", dest=\"stats\", action=\"store_true\", help=\"print statistics\")\n"
  },
  {
    "path": "src/borg/archiver/repo_create_cmd.py",
    "content": "from ._common import with_repository, with_other_repository, Highlander\nfrom ..cache import Cache\nfrom ..constants import *  # NOQA\nfrom ..crypto.key import key_creator, key_argument_names\nfrom ..helpers import CancelledByUser\nfrom ..helpers import location_validator, Location\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RepoCreateMixIn:\n    @with_repository(create=True, exclusive=True, manifest=False)\n    @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,))\n    def do_repo_create(self, args, repository, *, other_repository=None, other_manifest=None):\n        \"\"\"Creates a new, empty repository.\"\"\"\n        other_key = other_manifest.key if other_manifest is not None else None\n        path = args.location.canonical_path()\n        logger.info('Initializing repository at \"%s\"' % path)\n        if other_key is not None:\n            other_key.copy_crypt_key = args.copy_crypt_key\n        try:\n            key = key_creator(repository, args, other_key=other_key)\n        except (EOFError, KeyboardInterrupt):\n            repository.destroy()\n            raise CancelledByUser()\n        manifest = Manifest(key, repository)\n        manifest.key = key\n        manifest.write()\n        with Cache(repository, manifest, warn_if_unencrypted=False):\n            pass\n        if key.NAME != \"plaintext\":\n            logger.warning(\n                \"\\n\"\n                \"IMPORTANT: you will need both KEY AND PASSPHRASE to access this repository!\\n\"\n                \"\\n\"\n                \"Key storage location depends on the mode:\\n\"\n                \"- repokey modes: key is stored in the repository directory.\\n\"\n                \"- keyfile modes: key is stored in the home directory of this user.\\n\"\n                \"\\n\"\n                \"For any mode, you should:\\n\"\n                \"1. Export the Borg key and store the result in a safe place:\\n\"\n                \"   borg key export -r REPOSITORY           encrypted-key-backup\\n\"\n                \"   borg key export -r REPOSITORY --paper   encrypted-key-backup.txt\\n\"\n                \"   borg key export -r REPOSITORY --qr-html encrypted-key-backup.html\\n\"\n                \"2. Write down the Borg key passphrase and store it in a safe place.\"\n            )\n        logger.warning(\n            \"\\n\"\n            \"Reserve some repository storage space now for emergencies like 'disk full'\\n\"\n            \"by running:\\n\"\n            \"    borg repo-space --reserve 1G\"\n        )\n\n    def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        repo_create_epilog = process_epilog(\n            \"\"\"\n        This command creates a new, empty repository. A repository is a ``borgstore`` store\n        containing the deduplicated data from zero or more archives.\n\n        Repository creation can be quite slow for some kinds of stores (e.g. for ``sftp:``) -\n        this is due to borgstore pre-creating all directories needed, making usage of the\n        store faster.\n\n        Encryption mode TL;DR\n        +++++++++++++++++++++\n\n        The encryption mode can only be configured when creating a new repository - you can\n        neither configure it on a per-archive basis nor change the mode of an existing repository.\n        This example will likely NOT give optimum performance on your machine (performance\n        tips will come below):\n\n        ::\n\n            borg repo-create --encryption repokey-aes-ocb\n\n        Borg will:\n\n        1. Ask you to come up with a passphrase.\n        2. Create a borg key (which contains some random secrets. See :ref:`key_files`).\n        3. Derive a \"key encryption key\" from your passphrase\n        4. Encrypt and sign the key with the key encryption key\n        5. Store the encrypted borg key inside the repository directory (in the repo config).\n           This is why it is essential to use a secure passphrase.\n        6. Encrypt and sign your backups to prevent anyone from reading or forging them unless they\n           have the key and know the passphrase. Make sure to keep a backup of\n           your key **outside** the repository - do not lock yourself out by\n           \"leaving your keys inside your car\" (see :ref:`borg_key_export`).\n           The encryption is done locally - if you use a remote repository, the remote machine\n           never sees your passphrase, your unencrypted key or your unencrypted files.\n           Chunking and ID generation are also based on your key to improve\n           your privacy.\n        7. Use the key when extracting files to decrypt them and to verify that the contents of\n           the backups have not been accidentally or maliciously altered.\n\n        Picking a passphrase\n        ++++++++++++++++++++\n\n        Make sure you use a good passphrase. Not too short, not too simple. The real\n        encryption / decryption key is encrypted with / locked by your passphrase.\n        If an attacker gets your key, they cannot unlock and use it without knowing the\n        passphrase.\n\n        Be careful with special or non-ASCII characters in your passphrase:\n\n        - Borg processes the passphrase as Unicode (and encodes it as UTF-8),\n          so it does not have problems dealing with even the strangest characters.\n        - BUT: that does not necessarily apply to your OS/VM/keyboard configuration.\n\n        So better use a long passphrase made from simple ASCII characters than one that\n        includes non-ASCII stuff or characters that are hard or impossible to enter on\n        a different keyboard layout.\n\n        You can change your passphrase for existing repositories at any time; it will not affect\n        the encryption/decryption key or other secrets.\n\n        Choosing an encryption mode\n        +++++++++++++++++++++++++++\n\n        Depending on your hardware, hashing and crypto performance may vary widely.\n        The easiest way to find out what is fastest is to run ``borg benchmark cpu``.\n\n        `repokey` modes: if you want ease-of-use and \"passphrase\" security is good enough -\n        the key will be stored in the repository (in ``repo_dir/config``).\n\n        `keyfile` modes: if you want \"passphrase and having-the-key\" security -\n        the key will be stored in your home directory (in ``~/.config/borg/keys``).\n\n        The following table is roughly sorted in order of preference, the better ones are\n        in the upper part of the table, in the lower part is the old and/or unsafe(r) stuff:\n\n        .. nanorst: inline-fill\n\n        +-----------------------------------+--------------+----------------+--------------------+\n        | Mode (K = keyfile or repokey)     | ID-Hash      | Encryption     | Authentication     |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | K-blake2-chacha20-poly1305        | BLAKE2b      | CHACHA20       | POLY1305           |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | K-chacha20-poly1305               | HMAC-SHA-256 | CHACHA20       | POLY1305           |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | K-blake2-aes-ocb                  | BLAKE2b      | AES256-OCB     | AES256-OCB         |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | K-aes-ocb                         | HMAC-SHA-256 | AES256-OCB     | AES256-OCB         |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | authenticated-blake2              | BLAKE2b      | none           | BLAKE2b            |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | authenticated                     | HMAC-SHA-256 | none           | HMAC-SHA256        |\n        +-----------------------------------+--------------+----------------+--------------------+\n        | none                              | SHA-256      | none           | none               |\n        +-----------------------------------+--------------+----------------+--------------------+\n\n        .. nanorst: inline-replace\n\n        `none` mode uses no encryption and no authentication. You are advised NOT to use this mode\n        as it would expose you to all sorts of issues (DoS, confidentiality, tampering, ...) in\n        case of malicious activity in the repository.\n\n        If you do **not** want to encrypt the contents of your backups, but still want to detect\n        malicious tampering, use an `authenticated` mode. It is like `repokey` minus encryption.\n        To normally work with ``authenticated`` repositories, you will need the passphrase, but\n        there is an emergency workaround; see ``BORG_WORKAROUNDS=authenticated_no_key`` docs.\n\n        Creating a related repository\n        +++++++++++++++++++++++++++++\n\n        You can use ``borg repo-create --other-repo ORIG_REPO ...`` to create a related repository\n        that uses the same secret key material as the given other/original repository.\n\n        By default, only the ID key and chunker secret will be the same (these are important\n        for deduplication) and the AE crypto keys will be newly generated random keys.\n\n        Optionally, if you use ``--copy-crypt-key`` you can also keep the same crypt_key\n        (used for authenticated encryption). This might be desired, for example, if you want to have fewer\n        keys to manage.\n\n        Creating related repositories is useful, for example, if you want to use ``borg transfer`` later.\n\n        Creating a related repository for data migration from Borg 1.2 or 1.4\n        +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\n        You can use ``borg repo-create --other-repo ORIG_REPO --from-borg1 ...`` to create a related\n        repository that uses the same secret key material as the given other/original repository.\n\n        Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_create.__doc__, epilog=repo_create_epilog\n        )\n        subparsers.add_subcommand(\"repo-create\", subparser, help=\"create a new, empty repository\")\n        subparser.add_argument(\n            \"--other-repo\",\n            metavar=\"SRC_REPOSITORY\",\n            dest=\"other_location\",\n            type=location_validator(other=True),\n            default=Location(other=True),\n            action=Highlander,\n            help=\"reuse the key material from the other repository\",\n        )\n        subparser.add_argument(\n            \"--from-borg1\", dest=\"v1_or_v2\", action=\"store_true\", help=\"other repository is Borg 1.x\"\n        )\n        subparser.add_argument(\n            \"-e\",\n            \"--encryption\",\n            metavar=\"MODE\",\n            dest=\"encryption\",\n            required=True,\n            choices=key_argument_names(),\n            action=Highlander,\n            help=\"select encryption key mode **(required)**\",\n        )\n        subparser.add_argument(\n            \"--copy-crypt-key\",\n            dest=\"copy_crypt_key\",\n            action=\"store_true\",\n            help=\"copy the crypt_key (used for authenticated encryption) from the key of the other repository \"\n            \"(default: new random key).\",\n        )\n"
  },
  {
    "path": "src/borg/archiver/repo_delete_cmd.py",
    "content": "from ._common import with_repository\nfrom ..cache import Cache, SecurityManager\nfrom ..constants import *  # NOQA\nfrom ..helpers import CancelledByUser\nfrom ..helpers import format_archive\nfrom ..helpers import bin_to_hex\nfrom ..helpers import yes\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest, NoManifestError\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RepoDeleteMixIn:\n    @with_repository(exclusive=True, manifest=False)\n    def do_repo_delete(self, args, repository):\n        \"\"\"Deletes a repository.\"\"\"\n        self.output_list = args.output_list\n        dry_run = args.dry_run\n        keep_security_info = args.keep_security_info\n\n        if not args.cache_only:\n            if args.forced == 0:  # without --force, we let the user see the archives list and confirm.\n                id = bin_to_hex(repository.id)\n                location = repository._location.canonical_path()\n                msg = []\n                try:\n                    manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n                    n_archives = manifest.archives.count()\n                    msg.append(\n                        f\"You requested to DELETE the following repository completely \"\n                        f\"*including* {n_archives} archives it contains:\"\n                    )\n                except NoManifestError:\n                    n_archives = None\n                    msg.append(\n                        \"You requested to DELETE the following repository completely \"\n                        \"*including* all archives it may contain:\"\n                    )\n\n                msg.append(DASHES)\n                msg.append(f\"Repository ID: {id}\")\n                msg.append(f\"Location: {location}\")\n\n                if self.output_list:\n                    msg.append(\"\")\n                    msg.append(\"Archives:\")\n\n                    if n_archives is not None:\n                        if n_archives > 0:\n                            for archive_info in manifest.archives.list(sort_by=[\"ts\"]):\n                                msg.append(format_archive(archive_info))\n                        else:\n                            msg.append(\"This repository does not appear to have any archives.\")\n                    else:\n                        msg.append(\n                            \"This repository seems to have no manifest, so we cannot \"\n                            \"tell anything about its contents.\"\n                        )\n\n                msg.append(DASHES)\n                msg.append(\"Type 'YES' if you understand this and want to continue: \")\n                msg = \"\\n\".join(msg)\n                if not yes(\n                    msg,\n                    false_msg=\"Aborting.\",\n                    invalid_msg=\"Invalid answer, aborting.\",\n                    truish=(\"YES\",),\n                    retry=False,\n                    env_var_override=\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\",\n                ):\n                    raise CancelledByUser()\n            if not dry_run:\n                repository.destroy()\n                logger.info(\"Repository deleted.\")\n                if not keep_security_info:\n                    SecurityManager.destroy(repository)\n            else:\n                logger.info(\"Would delete repository.\")\n                logger.info(\"Would %s security info.\" % (\"keep\" if keep_security_info else \"delete\"))\n        if not dry_run:\n            Cache.destroy(repository)\n            logger.info(\"Cache deleted.\")\n        else:\n            logger.info(\"Would delete cache.\")\n\n    def build_parser_repo_delete(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        repo_delete_epilog = process_epilog(\n            \"\"\"\n        This command deletes a complete repository.\n\n        When you delete a complete repository, the security info and local cache for it\n        (if any) are also deleted. Alternatively, you can delete just the local cache\n        with the ``--cache-only`` option, or keep the security info with the\n        ``--keep-security-info`` option.\n\n        Always first use ``--dry-run --list`` to see what would be deleted.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_delete.__doc__, epilog=repo_delete_epilog\n        )\n        subparsers.add_subcommand(\"repo-delete\", subparser, help=\"delete a repository\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change the repository\"\n        )\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of archives\"\n        )\n        subparser.add_argument(\n            \"--force\",\n            dest=\"forced\",\n            action=\"count\",\n            default=0,\n            help=\"force deletion of corrupted archives; use ``--force --force`` if a single ``--force`` does not work.\",\n        )\n        subparser.add_argument(\n            \"--cache-only\",\n            dest=\"cache_only\",\n            action=\"store_true\",\n            help=\"delete only the local cache for the given repository\",\n        )\n        subparser.add_argument(\n            \"--keep-security-info\",\n            dest=\"keep_security_info\",\n            action=\"store_true\",\n            help=\"keep the local security info when deleting a repository\",\n        )\n"
  },
  {
    "path": "src/borg/archiver/repo_info_cmd.py",
    "content": "import textwrap\n\nfrom ._common import with_repository\nfrom ..constants import *  # NOQA\nfrom ..helpers import bin_to_hex, json_print, basic_json_data\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RepoInfoMixIn:\n    @with_repository(cache=True, compatibility=(Manifest.Operation.READ,))\n    def do_repo_info(self, args, repository, manifest, cache):\n        \"\"\"Show repository information.\"\"\"\n        key = manifest.key\n        info = basic_json_data(manifest, cache=cache, extra={\"security_dir\": cache.security_manager.dir})\n\n        if args.json:\n            json_print(info)\n        else:\n            encryption = \"Encrypted: \"\n            if key.NAME in (\"plaintext\", \"authenticated\"):\n                encryption += \"No\"\n            else:\n                encryption += \"Yes (%s)\" % key.NAME\n            if key.NAME.startswith(\"key file\"):\n                encryption += \"\\nKey file: %s\" % key.find_key()\n            info[\"encryption\"] = encryption\n\n            output = (\n                textwrap.dedent(\n                    \"\"\"\n            Repository ID: {id}\n            Location: {location}\n            Repository version: {version}\n            {encryption}\n            Security directory: {security_dir}\n            \"\"\"\n                )\n                .strip()\n                .format(\n                    id=bin_to_hex(repository.id),\n                    location=repository._location.canonical_path(),\n                    version=repository.version,\n                    encryption=info[\"encryption\"],\n                    security_dir=info[\"security_dir\"],\n                )\n            )\n\n            if hasattr(info[\"cache\"], \"path\"):\n                output += \"\\nCache: {cache.path}\\n\".format(**info)\n\n            print(output)\n\n    def build_parser_repo_info(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        repo_info_epilog = process_epilog(\n            \"\"\"\n        This command displays detailed information about the repository.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_info.__doc__, epilog=repo_info_epilog\n        )\n        subparsers.add_subcommand(\"repo-info\", subparser, help=\"show repository information\")\n        subparser.add_argument(\"--json\", action=\"store_true\", help=\"format output as JSON\")\n"
  },
  {
    "path": "src/borg/archiver/repo_list_cmd.py",
    "content": "import os\nimport textwrap\nimport sys\n\nfrom ._common import with_repository, Highlander\nfrom ..constants import *  # NOQA\nfrom ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RepoListMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    def do_repo_list(self, args, repository, manifest):\n        \"\"\"List the archives contained in a repository.\"\"\"\n        if args.format is not None:\n            format = args.format\n        elif args.short:\n            format = \"{id}{NL}\"\n        else:\n            format = os.environ.get(\n                \"BORG_REPO_LIST_FORMAT\",\n                \"{id:.8}  {time}  {archive:<15}  {tags:<10}  {username:<10}  {hostname:<10}  {comment:.40}{NL}\",\n            )\n        formatter = ArchiveFormatter(format, repository, manifest, manifest.key, iec=args.iec, deleted=args.deleted)\n\n        output_data = []\n\n        for archive_info in manifest.archives.list_considering(args):\n            if args.json:\n                output_data.append(formatter.get_item_data(archive_info, args.json))\n            else:\n                sys.stdout.write(formatter.format_item(archive_info, args.json))\n\n        if args.json:\n            json_print(basic_json_data(manifest, extra={\"archives\": output_data}))\n\n    def build_parser_repo_list(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog, define_archive_filters_group\n\n        repo_list_epilog = (\n            process_epilog(\n                \"\"\"\n        This command lists the archives contained in a repository.\n\n        .. man NOTES\n\n        The FORMAT specifier syntax\n        +++++++++++++++++++++++++++\n\n        The ``--format`` option uses Python's `format string syntax\n        <https://docs.python.org/3.10/library/string.html#formatstrings>`_.\n\n        Examples:\n        ::\n\n            $ borg repo-list --format '{archive}{NL}'\n            ArchiveFoo\n            ArchiveBar\n            ...\n\n            # {VAR:NUMBER} - pad to NUMBER columns.\n            # Strings are left-aligned, numbers are right-aligned.\n            # Note: time columns except ``isomtime``, ``isoctime`` and ``isoatime`` cannot be padded.\n            $ borg repo-list --format '{archive:36} {time} [{id}]{NL}' /path/to/repo\n            ArchiveFoo                           Thu, 2021-12-09 10:22:28 [0b8e9...3b274]\n            ...\n\n        The following keys are always available:\n\n        \"\"\"\n            )\n            + BaseFormatter.keys_help()\n            + textwrap.dedent(\n                \"\"\"\n\n        Keys available only when listing archives in a repository:\n\n        \"\"\"\n            )\n            + ArchiveFormatter.keys_help()\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_list.__doc__, epilog=repo_list_epilog\n        )\n        subparsers.add_subcommand(\"repo-list\", subparser, help=\"list repository contents\")\n        subparser.add_argument(\n            \"--short\", dest=\"short\", action=\"store_true\", help=\"only print the archive IDs, nothing else\"\n        )\n        subparser.add_argument(\n            \"--format\",\n            metavar=\"FORMAT\",\n            dest=\"format\",\n            action=Highlander,\n            help=\"specify format for archive listing \" '(default: \"{archive:<36} {time} [{id}]{NL}\")',\n        )\n        subparser.add_argument(\n            \"--json\",\n            action=\"store_true\",\n            help=\"Format output as JSON. \"\n            \"The form of ``--format`` is ignored, \"\n            \"but keys used in it are added to the JSON output. \"\n            \"Some keys are always present. Note: JSON can only represent text.\",\n        )\n        define_archive_filters_group(subparser, deleted=True)\n"
  },
  {
    "path": "src/borg/archiver/repo_space_cmd.py",
    "content": "import math\nimport os\n\nfrom borgstore.store import ItemInfo\n\nfrom ._common import with_repository, Highlander\nfrom ..constants import *  # NOQA\nfrom ..helpers import parse_file_size, format_file_size\nfrom ..helpers.argparsing import ArgumentParser\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass RepoSpaceMixIn:\n    @with_repository(lock=False, manifest=False)\n    def do_repo_space(self, args, repository):\n        \"\"\"Manages reserved space in the repository.\"\"\"\n        # we work without locking here because locks don't work with full disk.\n        if args.reserve_space > 0:\n            storage_space_reserve_object_size = 64 * 2**20  # 64 MiB per object\n            count = math.ceil(float(args.reserve_space) / storage_space_reserve_object_size)  # round up\n            size = 0\n            for i in range(count):\n                data = os.urandom(storage_space_reserve_object_size)  # counter-act fs compression/dedup\n                repository.store_store(f\"config/space-reserve.{i}\", data)\n                size += len(data)\n            print(f\"There is {format_file_size(size, iec=False)} reserved space in this repository now.\")\n        elif args.free_space:\n            infos = repository.store_list(\"config\")\n            size = 0\n            for info in infos:\n                info = ItemInfo(*info)  # RPC does not give namedtuple\n                if info.name.startswith(\"space-reserve.\"):\n                    size += info.size\n                    repository.store_delete(f\"config/{info.name}\")\n            print(f\"Freed {format_file_size(size, iec=False)} in the repository.\")\n            print(\"Now run borg prune or borg delete plus borg compact to free more space.\")\n            print(\"After that, do not forget to reserve space again for next time!\")\n        else:  # print amount currently reserved\n            infos = repository.store_list(\"config\")\n            size = 0\n            for info in infos:\n                info = ItemInfo(*info)  # RPC does not give namedtuple\n                if info.name.startswith(\"space-reserve.\"):\n                    size += info.size\n            print(f\"There is {format_file_size(size, iec=False)} reserved space in this repository.\")\n            print(\"In case you want to change the amount, use --free first to free all reserved space,\")\n            print(\"then use --reserve with the desired amount.\")\n\n    def build_parser_repo_space(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        repo_space_epilog = process_epilog(\n            \"\"\"\n        This command manages reserved space in a repository.\n\n        Borg cannot work in disk-full conditions (it cannot lock a repository and thus cannot\n        run prune/delete or compact operations to free disk space).\n\n        To avoid running into such dead-end situations, you can put some objects into a\n        repository that take up disk space. If you ever run into a disk-full situation, you\n        can free that space, and then Borg will be able to run normally so you can free more\n        disk space by using ``borg prune``/``borg delete``/``borg compact``. After that, do\n        not forget to reserve space again, in case you run into that situation again later.\n\n        Examples::\n\n            # Create a new repository:\n            $ borg repo-create ...\n            # Reserve approx. 1 GiB of space for emergencies:\n            $ borg repo-space --reserve 1G\n\n            # Check the amount of reserved space in the repository:\n            $ borg repo-space\n\n            # EMERGENCY! Free all reserved space to get things back to normal:\n            $ borg repo-space --free\n            $ borg prune ...\n            $ borg delete ...\n            $ borg compact -v  # only this actually frees space of deleted archives\n            $ borg repo-space --reserve 1G  # reserve space again for next time\n\n        Reserved space is always rounded up to full reservation blocks of 64 MiB.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_repo_space.__doc__, epilog=repo_space_epilog\n        )\n        subparsers.add_subcommand(\"repo-space\", subparser, help=\"manage reserved space in a repository\")\n        subparser.add_argument(\n            \"--reserve\",\n            metavar=\"SPACE\",\n            dest=\"reserve_space\",\n            default=0,\n            type=parse_file_size,\n            action=Highlander,\n            help=\"Amount of space to reserve (e.g. 100M, 1G). Default: 0.\",\n        )\n        subparser.add_argument(\n            \"--free\",\n            dest=\"free_space\",\n            action=\"store_true\",\n            help=\"Free all reserved space. Do not forget to reserve space again later.\",\n        )\n"
  },
  {
    "path": "src/borg/archiver/serve_cmd.py",
    "content": "from ..constants import *  # NOQA\nfrom ..remote import RepositoryServer\n\nfrom ..logger import create_logger\nfrom ..helpers.argparsing import ArgumentParser\n\nlogger = create_logger()\n\n\nclass ServeMixIn:\n    def do_serve(self, args):\n        \"\"\"Starts in server mode. This command is usually not used manually.\"\"\"\n        RepositoryServer(\n            restrict_to_paths=args.restrict_to_paths,\n            restrict_to_repositories=args.restrict_to_repositories,\n            use_socket=args.use_socket,\n            permissions=args.permissions,\n        ).serve()\n\n    def build_parser_serve(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        serve_epilog = process_epilog(\n            \"\"\"\n        This command starts a repository server process.\n\n        `borg serve` currently supports:\n\n        - Being automatically started via SSH when the borg client uses an ssh://...\n          remote repository. In this mode, `borg serve` will run until that SSH connection\n          is terminated.\n\n        - Being started by some other means (not by the borg client) as a long-running socket\n          server to be used for borg clients using a socket://... repository (see the `--socket`\n          option if you do not want to use the default path for the socket and PID file).\n\n        Please note that `borg serve` does not support providing a specific repository via the\n        `--repo` option or the `BORG_REPO` environment variable. It is always the borg client that\n        specifies the repository to use when communicating with `borg serve`.\n\n        The --permissions option enforces repository permissions:\n\n        - `all`: All permissions are granted. (Default; the permissions system is not used.)\n        - `no-delete`: Allow reading and writing; disallow deleting and overwriting data.\n          New archives can be created; existing archives cannot be deleted. New chunks can\n          be added; existing chunks cannot be deleted or overwritten.\n        - `write-only`: Allow writing; disallow reading data.\n          New archives can be created; existing archives cannot be read.\n          New chunks can be added; existing chunks cannot be read, deleted, or overwritten.\n        - `read-only`: Allow reading; disallow writing or deleting data.\n          Existing archives can be read, but no archives can be created or deleted.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_serve.__doc__, epilog=serve_epilog)\n        subparsers.add_subcommand(\"serve\", subparser, help=\"start the repository server process\")\n        subparser.add_argument(\n            \"--restrict-to-path\",\n            metavar=\"PATH\",\n            dest=\"restrict_to_paths\",\n            action=\"append\",\n            help=\"Restrict repository access to PATH. \"\n            \"Can be specified multiple times to allow the client access to several directories. \"\n            \"Access to all subdirectories is granted implicitly; PATH does not need to point directly to a repository.\",\n        )\n        subparser.add_argument(\n            \"--restrict-to-repository\",\n            metavar=\"PATH\",\n            dest=\"restrict_to_repositories\",\n            action=\"append\",\n            help=\"Restrict repository access. Only the repository located at PATH \"\n            \"(no subdirectories are considered) is accessible. \"\n            \"Can be specified multiple times to allow the client access to several repositories. \"\n            \"Unlike ``--restrict-to-path``, subdirectories are not accessible; \"\n            \"PATH must point directly to a repository location. \"\n            \"PATH may be an empty directory or the last element of PATH may not exist, in which case \"\n            \"the client may initialize a repository there.\",\n        )\n        subparser.add_argument(\n            \"--permissions\",\n            dest=\"permissions\",\n            choices=[\"all\", \"no-delete\", \"write-only\", \"read-only\"],\n            help=\"Set repository permission mode. Overrides BORG_REPO_PERMISSIONS environment variable.\",\n        )\n"
  },
  {
    "path": "src/borg/archiver/tag_cmd.py",
    "content": "from ._common import with_repository, define_archive_filters_group\nfrom ..archive import Archive\nfrom ..constants import *  # NOQA\nfrom ..helpers import bin_to_hex, archivename_validator, tag_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass TagMixIn:\n    @with_repository(cache=True, compatibility=(Manifest.Operation.WRITE,))\n    def do_tag(self, args, repository, manifest, cache):\n        \"\"\"Manage tags.\"\"\"\n\n        if args.name:\n            archive_infos = [manifest.archives.get_one([args.name])]\n        else:\n            archive_infos = manifest.archives.list_considering(args)\n\n        for archive_info in archive_infos:\n            archive = Archive(manifest, archive_info.id, cache=cache)\n            if args.set_tags is not None:\n                # avoid that --set (accidentally) erases existing special tags,\n                # but allow --set if the existing special tags are also given.\n                new_tags = set(args.set_tags)\n                existing_special = {tag for tag in archive.tags if tag.startswith(\"@\")}\n                clobber = not existing_special.issubset(new_tags)\n                if not clobber:\n                    archive.tags = new_tags\n            archive.tags |= set(args.add_tags or [])\n            archive.tags -= set(args.remove_tags or [])\n            old_id = archive.id\n            archive.set_meta(\"tags\", list(sorted(archive.tags)))\n            if old_id != archive.id:\n                manifest.archives.delete_by_id(old_id)\n            print(\n                f\"id: {bin_to_hex(old_id):.8} -> {bin_to_hex(archive.id):.8}, \"\n                f\"tags: {','.join(sorted(archive.tags))}.\"\n            )\n\n    def build_parser_tag(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        tag_epilog = process_epilog(\n            \"\"\"\n            Manage archive tags.\n\n            Borg archives can have a set of tags which can be used for matching archives.\n\n            You can set the tags to a specific set of tags or you can add or remove\n            tags from the current set of tags.\n\n            User-defined tags must not start with `@` because such tags are considered\n            special and users are only allowed to use known special tags:\n\n            ``@PROT``: protects archives against archive deletion or pruning.\n\n            Pre-existing special tags cannot be removed via ``--set``. You can still use\n            ``--set``, but you must also give pre-existing special tags (so they won't be\n            removed).\n            \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_tag.__doc__, epilog=tag_epilog)\n        subparsers.add_subcommand(\"tag\", subparser, help=\"tag archives\")\n        subparser.add_argument(\"--set\", dest=\"set_tags\", metavar=\"TAG\", type=tag_validator, nargs=\"*\", help=\"set tags\")\n        subparser.add_argument(\"--add\", dest=\"add_tags\", metavar=\"TAG\", type=tag_validator, nargs=\"*\", help=\"add tags\")\n        subparser.add_argument(\n            \"--remove\", dest=\"remove_tags\", metavar=\"TAG\", type=tag_validator, nargs=\"*\", help=\"remove tags\"\n        )\n        define_archive_filters_group(subparser)\n        subparser.add_argument(\n            \"name\", metavar=\"NAME\", nargs=\"?\", type=archivename_validator, help=\"specify the archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/tar_cmds.py",
    "content": "import base64\nimport logging\nimport os\nimport stat\nimport tarfile\n\nfrom ..archive import Archive, TarfileObjectProcessors, ChunksProcessor\nfrom ..constants import *  # NOQA\nfrom ..helpers import HardLinkManager, IncludePatternNeverMatchedWarning\nfrom ..helpers import ProgressIndicatorPercent\nfrom ..helpers import dash_open\nfrom ..helpers import msgpack\nfrom ..helpers import create_filter_process\nfrom ..helpers import ChunkIteratorFileWrapper\nfrom ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, CompressionSpec\nfrom ..helpers import remove_surrogates\nfrom ..helpers import timestamp, archive_ts_now\nfrom ..helpers import basic_json_data, json_print\nfrom ..helpers import log_multi\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ._common import with_repository, with_archive, Highlander, define_exclusion_group\nfrom ._common import build_matcher, build_filter\n\nfrom ..logger import create_logger\n\nlogger = create_logger(__name__)\n\n# Python 3.12+ gives a deprecation warning if TarFile.extraction_filter is None.\n# https://docs.python.org/3.12/library/tarfile.html#tarfile-extraction-filter\nif hasattr(tarfile, \"fully_trusted_filter\"):\n    tarfile.TarFile.extraction_filter = staticmethod(tarfile.fully_trusted_filter)  # type: ignore\n\n\ndef get_tar_filter(fname, decompress):\n    # Note that filter is None if fname is '-'.\n    if fname.endswith((\".tar.gz\", \".tgz\")):\n        filter = \"gzip -d\" if decompress else \"gzip\"\n    elif fname.endswith((\".tar.bz2\", \".tbz\")):\n        filter = \"bzip2 -d\" if decompress else \"bzip2\"\n    elif fname.endswith((\".tar.xz\", \".txz\")):\n        filter = \"xz -d\" if decompress else \"xz\"\n    elif fname.endswith((\".tar.lz4\",)):\n        filter = \"lz4 -d\" if decompress else \"lz4\"\n    elif fname.endswith((\".tar.zstd\", \".tar.zst\")):\n        filter = \"zstd -d\" if decompress else \"zstd\"\n    else:\n        filter = None\n    logger.debug(\"Automatically determined tar filter: %s\", filter)\n    return filter\n\n\nclass TarMixIn:\n    @with_repository(compatibility=(Manifest.Operation.READ,))\n    @with_archive\n    def do_export_tar(self, args, repository, manifest, archive):\n        \"\"\"Export archive contents as a tarball\"\"\"\n        self.output_list = args.output_list\n\n        # A quick note about the general design of tar_filter and tarfile;\n        # The tarfile module of Python can provide some compression mechanisms\n        # by itself, using the built-in gzip, bz2, and lzma modules (and \"tar modes\"\n        # such as \"w:xz\").\n        #\n        # Doing so would have three major drawbacks:\n        # For one the compressor runs on the same thread as the program using the\n        # tarfile, stealing valuable CPU time from Borg and thus reducing throughput.\n        # Then this limits the available options - what about lz4? Brotli? zstd?\n        # The third issue is that systems can ship more optimized versions than those\n        # built into Python, e.g. pigz or pxz, which can use more than one thread for\n        # compression.\n        #\n        # Therefore we externalize compression by using a filter program, which has\n        # none of these drawbacks. The only issue of using an external filter is\n        # that it has to be installed -- hardly a problem, considering that\n        # the decompressor must be installed as well to make use of the exported tarball!\n\n        filter = get_tar_filter(args.tarfile, decompress=False) if args.tar_filter == \"auto\" else args.tar_filter\n\n        tarstream = dash_open(args.tarfile, \"wb\")\n        tarstream_close = args.tarfile != \"-\"\n\n        with create_filter_process(filter, stream=tarstream, stream_close=tarstream_close, inbound=False) as _stream:\n            self._export_tar(args, archive, _stream)\n\n    def _export_tar(self, args, archive, tarstream):\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(args.patterns, args.paths)\n\n        progress = args.progress\n        output_list = args.output_list\n        strip_components = args.strip_components\n        hlm = HardLinkManager(id_type=bytes, info_type=str)  # hlid -> path\n\n        filter = build_filter(matcher, strip_components)\n\n        # The | (pipe) symbol instructs tarfile to use a streaming mode of operation\n        # where it never seeks on the passed fileobj.\n        tar_format = dict(GNU=tarfile.GNU_FORMAT, PAX=tarfile.PAX_FORMAT, BORG=tarfile.PAX_FORMAT)[args.tar_format]\n        tar = tarfile.open(fileobj=tarstream, mode=\"w|\", format=tar_format)\n\n        if progress:\n            pi = ProgressIndicatorPercent(msg=\"%5.1f%% Processing: %s\", step=0.1, msgid=\"extract\")\n            pi.output(\"Calculating size\")\n            extracted_size = sum(item.get_size() for item in archive.iter_items(filter))\n            pi.total = extracted_size\n        else:\n            pi = None\n\n        def item_content_stream(item):\n            \"\"\"\n            Return a file-like object that reads from the chunks of *item*.\n            \"\"\"\n            chunk_iterator = archive.pipeline.fetch_many(item.chunks, is_preloaded=True, ro_type=ROBJ_FILE_STREAM)\n            if pi:\n                info = [remove_surrogates(item.path)]\n                return ChunkIteratorFileWrapper(\n                    chunk_iterator, lambda read_bytes: pi.show(increase=len(read_bytes), info=info)\n                )\n            else:\n                return ChunkIteratorFileWrapper(chunk_iterator)\n\n        def item_to_tarinfo(item, original_path):\n            \"\"\"\n            Transform a Borg *item* into a tarfile.TarInfo object.\n\n            Return a tuple (tarinfo, stream), where stream may be a file-like object that represents\n            the file contents, if any, and is None otherwise. When *tarinfo* is None, the *item*\n            cannot be represented as a TarInfo object and should be skipped.\n            \"\"\"\n            stream = None\n            tarinfo = tarfile.TarInfo()\n            tarinfo.name = item.path\n            tarinfo.mtime = item.mtime / 1e9\n            tarinfo.mode = stat.S_IMODE(item.mode)\n            tarinfo.uid = item.get(\"uid\", 0)\n            tarinfo.gid = item.get(\"gid\", 0)\n            tarinfo.uname = item.get(\"user\", \"\")\n            tarinfo.gname = item.get(\"group\", \"\")\n            # The linkname in tar has 2 uses:\n            # for symlinks it means the destination, while for hard links it refers to the file.\n            # Since hard links in tar have a different type code (LNKTYPE) the format might\n            # support hardlinking arbitrary objects (including symlinks and directories), but\n            # whether implementations actually support that is a whole different question...\n            tarinfo.linkname = \"\"\n\n            modebits = stat.S_IFMT(item.mode)\n            if modebits == stat.S_IFREG:\n                tarinfo.type = tarfile.REGTYPE\n                if \"hlid\" in item:\n                    linkname = hlm.retrieve(id=item.hlid)\n                    if linkname is not None:\n                        # the first hard link was already added to the archive, add a tar-hard-link reference to it.\n                        tarinfo.type = tarfile.LNKTYPE\n                        tarinfo.linkname = linkname\n                    else:\n                        tarinfo.size = item.get_size()\n                        stream = item_content_stream(item)\n                        hlm.remember(id=item.hlid, info=item.path)\n                else:\n                    tarinfo.size = item.get_size()\n                    stream = item_content_stream(item)\n            elif modebits == stat.S_IFDIR:\n                tarinfo.type = tarfile.DIRTYPE\n            elif modebits == stat.S_IFLNK:\n                tarinfo.type = tarfile.SYMTYPE\n                tarinfo.linkname = item.target\n            elif modebits == stat.S_IFBLK:\n                tarinfo.type = tarfile.BLKTYPE\n                tarinfo.devmajor = os.major(item.rdev)\n                tarinfo.devminor = os.minor(item.rdev)\n            elif modebits == stat.S_IFCHR:\n                tarinfo.type = tarfile.CHRTYPE\n                tarinfo.devmajor = os.major(item.rdev)\n                tarinfo.devminor = os.minor(item.rdev)\n            elif modebits == stat.S_IFIFO:\n                tarinfo.type = tarfile.FIFOTYPE\n            else:\n                self.print_warning(\n                    \"%s: unsupported file type %o for tar export\", remove_surrogates(item.path), modebits\n                )\n                return None, stream\n            return tarinfo, stream\n\n        def item_to_paxheaders(format, item):\n            \"\"\"\n            Transform (parts of) a Borg *item* into a pax_headers dict.\n            \"\"\"\n            # PAX format\n            # ----------\n            # When using the PAX (POSIX) format, we can support some things that aren't possible\n            # with classic tar formats, including GNU tar, such as:\n            # - atime, ctime (DONE)\n            # - possibly Linux capabilities, security.* xattrs (TODO)\n            # - various additions supported by GNU tar in POSIX mode (TODO)\n            #\n            # BORG format\n            # -----------\n            # This is based on PAX, but additionally adds BORG.* pax headers.\n            # Additionally to the standard tar / PAX metadata and data, it transfers\n            # ALL borg item metadata in a BORG specific way.\n            #\n            ph = {}\n            # note: for mtime this is a bit redundant as it is already done by tarfile module,\n            #       but we just do it in our way to be consistent for sure.\n            for name in \"atime\", \"ctime\", \"mtime\":\n                if hasattr(item, name):\n                    ns = getattr(item, name)\n                    ph[name] = str(ns / 1e9)\n            if hasattr(item, \"xattrs\"):\n                for bkey, bvalue in item.xattrs.items():\n                    # we have bytes key and bytes value, but the tarfile code\n                    # expects str key and str value.\n                    key = SCHILY_XATTR + bkey.decode(\"utf-8\", errors=\"surrogateescape\")\n                    value = bvalue.decode(\"utf-8\", errors=\"surrogateescape\")\n                    ph[key] = value\n            # Add POSIX access and default ACL if present\n            acl_access = item.get(\"acl_access\")\n            if acl_access is not None:\n                ph[SCHILY_ACL_ACCESS] = acl_access.decode(\"utf-8\", errors=\"surrogateescape\")\n            acl_default = item.get(\"acl_default\")\n            if acl_default is not None:\n                ph[SCHILY_ACL_DEFAULT] = acl_default.decode(\"utf-8\", errors=\"surrogateescape\")\n            if format == \"BORG\":  # BORG format additions\n                ph[\"BORG.item.version\"] = \"1\"\n                # BORG.item.meta - just serialize all metadata we have:\n                meta_bin = msgpack.packb(item.as_dict())\n                meta_text = base64.b64encode(meta_bin).decode()\n                ph[\"BORG.item.meta\"] = meta_text\n            return ph\n\n        for item in archive.iter_items(filter):\n            archive.preload_item_chunks(item, optimize_hardlinks=True)\n            orig_path = item.path\n            if strip_components:\n                item.path = os.sep.join(orig_path.split(os.sep)[strip_components:])\n            tarinfo, stream = item_to_tarinfo(item, orig_path)\n            if tarinfo:\n                if args.tar_format in (\"BORG\", \"PAX\"):\n                    tarinfo.pax_headers = item_to_paxheaders(args.tar_format, item)\n                if output_list:\n                    logging.getLogger(\"borg.output.list\").info(remove_surrogates(orig_path))\n                tar.addfile(tarinfo, stream)\n\n        if pi:\n            pi.finish()\n\n        # This does not close the fileobj (tarstream) we passed to it -- a side effect of the | mode.\n        tar.close()\n\n        for pattern in matcher.get_unmatched_include_patterns():\n            self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern))\n\n    @with_repository(cache=True, compatibility=(Manifest.Operation.WRITE,))\n    def do_import_tar(self, args, repository, manifest, cache):\n        \"\"\"Create a backup archive from a tarball\"\"\"\n        self.output_filter = args.output_filter\n        self.output_list = args.output_list\n\n        filter = get_tar_filter(args.tarfile, decompress=True) if args.tar_filter == \"auto\" else args.tar_filter\n\n        tarstream = dash_open(args.tarfile, \"rb\")\n        tarstream_close = args.tarfile != \"-\"\n\n        with create_filter_process(filter, stream=tarstream, stream_close=tarstream_close, inbound=True) as _stream:\n            self._import_tar(args, repository, manifest, manifest.key, cache, _stream)\n\n    def _import_tar(self, args, repository, manifest, key, cache, tarstream):\n        t0 = archive_ts_now()\n\n        archive = Archive(\n            manifest,\n            args.name,\n            cache=cache,\n            create=True,\n            progress=args.progress,\n            chunker_params=args.chunker_params,\n            start=t0,\n            log_json=args.log_json,\n        )\n        cp = ChunksProcessor(cache=cache, key=key, add_item=archive.add_item, rechunkify=False)\n        tfo = TarfileObjectProcessors(\n            cache=cache,\n            key=key,\n            process_file_chunks=cp.process_file_chunks,\n            add_item=archive.add_item,\n            chunker_params=args.chunker_params,\n            show_progress=args.progress,\n            log_json=args.log_json,\n            iec=args.iec,\n            file_status_printer=self.print_file_status,\n        )\n\n        tar = tarfile.open(fileobj=tarstream, mode=\"r|\", ignore_zeros=args.ignore_zeros)\n\n        while True:\n            tarinfo = tar.next()\n            if not tarinfo:\n                break\n            if tarinfo.isreg():\n                status = tfo.process_file(tarinfo=tarinfo, status=\"A\", type=stat.S_IFREG, tar=tar)\n                archive.stats.nfiles += 1\n            elif tarinfo.isdir():\n                status = tfo.process_dir(tarinfo=tarinfo, status=\"d\", type=stat.S_IFDIR)\n            elif tarinfo.issym():\n                status = tfo.process_symlink(tarinfo=tarinfo, status=\"s\", type=stat.S_IFLNK)\n            elif tarinfo.islnk():\n                # tar uses a hard link model like: the first instance of a hard link is stored as a regular file,\n                # later instances are special entries referencing back to the first instance.\n                status = tfo.process_hardlink(tarinfo=tarinfo, status=\"h\", type=stat.S_IFREG)\n            elif tarinfo.isblk():\n                status = tfo.process_dev(tarinfo=tarinfo, status=\"b\", type=stat.S_IFBLK)\n            elif tarinfo.ischr():\n                status = tfo.process_dev(tarinfo=tarinfo, status=\"c\", type=stat.S_IFCHR)\n            elif tarinfo.isfifo():\n                status = tfo.process_fifo(tarinfo=tarinfo, status=\"f\", type=stat.S_IFIFO)\n            else:\n                status = \"E\"\n                self.print_warning(\"%s: Unsupported tarinfo type %s\", tarinfo.name, tarinfo.type)\n            self.print_file_status(status, tarinfo.name)\n\n        # This does not close the fileobj (tarstream) we passed to it -- a side effect of the | mode.\n        tar.close()\n\n        if args.progress:\n            archive.stats.show_progress(final=True)\n        archive.stats += tfo.stats\n        archive.save(comment=args.comment, timestamp=args.timestamp)\n        args.stats |= args.json\n        if args.stats:\n            if args.json:\n                json_print(basic_json_data(archive.manifest, cache=archive.cache, extra={\"archive\": archive}))\n            else:\n                log_multi(str(archive), str(archive.stats), logger=logging.getLogger(\"borg.output.stats\"))\n\n    def build_parser_tar(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        export_tar_epilog = process_epilog(\n            \"\"\"\n        This command creates a tarball from an archive.\n\n        When giving '-' as the output FILE, Borg will write a tar stream to standard output.\n\n        By default (``--tar-filter=auto``) Borg will detect whether the FILE should be compressed\n        based on its file extension and pipe the tarball through an appropriate filter\n        before writing it to FILE:\n\n        - .tar.gz or .tgz: gzip\n        - .tar.bz2 or .tbz: bzip2\n        - .tar.xz or .txz: xz\n        - .tar.zstd or .tar.zst: zstd\n        - .tar.lz4: lz4\n\n        Alternatively, a ``--tar-filter`` program may be explicitly specified. It should\n        read the uncompressed tar stream from stdin and write a compressed/filtered\n        tar stream to stdout.\n\n        Depending on the ``--tar-format`` option, these formats are created:\n\n        +--------------+---------------------------+----------------------------+\n        | --tar-format | Specification             | Metadata                   |\n        +--------------+---------------------------+----------------------------+\n        | BORG         | BORG specific, like PAX   | all as supported by borg   |\n        +--------------+---------------------------+----------------------------+\n        | PAX          | POSIX.1-2001 (pax) format | GNU + atime/ctime/mtime ns |\n        |              |                           | + xattrs                   |\n        +--------------+---------------------------+----------------------------+\n        | GNU          | GNU tar format            | mtime s, no atime/ctime,   |\n        |              |                           | no ACLs/xattrs/bsdflags    |\n        +--------------+---------------------------+----------------------------+\n\n        A ``--sparse`` option (as found in borg extract) is not supported.\n\n        By default the entire archive is extracted but a subset of files and directories\n        can be selected by passing a list of ``PATHs`` as arguments.\n        The file selection can further be restricted by using the ``--exclude`` option.\n\n        For more help on include/exclude patterns, see the :ref:`borg_patterns` command output.\n\n        ``--progress`` can be slower than no progress display, since it makes one additional\n        pass over the archive metadata.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_export_tar.__doc__, epilog=export_tar_epilog\n        )\n        subparsers.add_subcommand(\"export-tar\", subparser, help=\"create tarball from archive\")\n        subparser.add_argument(\n            \"--tar-filter\",\n            dest=\"tar_filter\",\n            default=\"auto\",\n            action=Highlander,\n            help=\"filter program to pipe data through\",\n        )\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output verbose list of items (files, dirs, ...)\"\n        )\n        subparser.add_argument(\n            \"--tar-format\",\n            metavar=\"FMT\",\n            dest=\"tar_format\",\n            default=\"PAX\",\n            choices=(\"BORG\", \"PAX\", \"GNU\"),\n            action=Highlander,\n            help=\"select tar format: BORG, PAX or GNU\",\n        )\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\"tarfile\", metavar=\"FILE\", help='output tar file. \"-\" to write to stdout instead.')\n        subparser.add_argument(\n            \"paths\", metavar=\"PATH\", nargs=\"*\", type=PathSpec, help=\"paths to extract; patterns are supported\"\n        )\n        define_exclusion_group(subparser, strip_components=True)\n\n        import_tar_epilog = process_epilog(\n            \"\"\"\n        This command creates a backup archive from a tarball.\n\n        When giving '-' as path, Borg will read a tar stream from standard input.\n\n        By default (--tar-filter=auto) Borg will detect whether the file is compressed\n        based on its file extension and pipe the file through an appropriate filter:\n\n        - .tar.gz or .tgz: gzip -d\n        - .tar.bz2 or .tbz: bzip2 -d\n        - .tar.xz or .txz: xz -d\n        - .tar.zstd or .tar.zst: zstd -d\n        - .tar.lz4: lz4 -d\n\n        Alternatively, a --tar-filter program may be explicitly specified. It should\n        read compressed data from stdin and output an uncompressed tar stream on\n        stdout.\n\n        Most documentation of borg create applies. Note that this command does not\n        support excluding files.\n\n        A ``--sparse`` option (as found in borg create) is not supported.\n\n        About tar formats and metadata conservation or loss, please see ``borg export-tar``.\n\n        import-tar reads these tar formats:\n\n        - BORG: borg specific (PAX-based)\n        - PAX: POSIX.1-2001\n        - GNU: GNU tar\n        - POSIX.1-1988 (ustar)\n        - UNIX V7 tar\n        - SunOS tar with extended attributes\n\n        To import multiple tarballs into a single archive, they can be simply\n        concatenated (e.g. using \"cat\") into a single file, and imported with an\n        ``--ignore-zeros`` option to skip through the stop markers between them.\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_import_tar.__doc__, epilog=import_tar_epilog\n        )\n        subparsers.add_subcommand(\"import-tar\", subparser, help=self.do_import_tar.__doc__)\n        subparser.add_argument(\n            \"--tar-filter\",\n            dest=\"tar_filter\",\n            default=\"auto\",\n            action=Highlander,\n            help=\"filter program to pipe data through\",\n        )\n        subparser.add_argument(\n            \"-s\",\n            \"--stats\",\n            dest=\"stats\",\n            action=\"store_true\",\n            default=False,\n            help=\"print statistics for the created archive\",\n        )\n        subparser.add_argument(\n            \"--list\",\n            dest=\"output_list\",\n            action=\"store_true\",\n            default=False,\n            help=\"output verbose list of items (files, dirs, ...)\",\n        )\n        subparser.add_argument(\n            \"--filter\",\n            dest=\"output_filter\",\n            metavar=\"STATUSCHARS\",\n            action=Highlander,\n            help=\"only display items with the given status characters\",\n        )\n        subparser.add_argument(\"--json\", action=\"store_true\", help=\"output stats as JSON (implies --stats)\")\n        subparser.add_argument(\n            \"--ignore-zeros\",\n            dest=\"ignore_zeros\",\n            action=\"store_true\",\n            help=\"ignore zero-filled blocks in the input tarball\",\n        )\n\n        archive_group = subparser.add_argument_group(\"Archive options\")\n        archive_group.add_argument(\n            \"--comment\",\n            metavar=\"COMMENT\",\n            dest=\"comment\",\n            type=comment_validator,\n            default=\"\",\n            action=Highlander,\n            help=\"add a comment text to the archive\",\n        )\n        archive_group.add_argument(\n            \"--timestamp\",\n            dest=\"timestamp\",\n            type=timestamp,\n            default=None,\n            action=Highlander,\n            metavar=\"TIMESTAMP\",\n            help=\"manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, \"\n            \"(+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.\",\n        )\n        archive_group.add_argument(\n            \"--chunker-params\",\n            dest=\"chunker_params\",\n            type=ChunkerParams,\n            default=CHUNKER_PARAMS,\n            action=Highlander,\n            metavar=\"PARAMS\",\n            help=\"specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, \"\n            \"HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %s,%d,%d,%d,%d\" % CHUNKER_PARAMS,\n        )\n        archive_group.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n\n        subparser.add_argument(\"name\", metavar=\"NAME\", type=archivename_validator, help=\"specify the archive name\")\n        subparser.add_argument(\"tarfile\", metavar=\"TARFILE\", help='input tar file. \"-\" to read from stdin instead.')\n"
  },
  {
    "path": "src/borg/archiver/transfer_cmd.py",
    "content": "from ._common import with_repository, with_other_repository, Highlander\nfrom ..archive import Archive, cached_hash, DownloadPipeline\nfrom ..chunkers import get_chunker\nfrom ..constants import *  # NOQA\nfrom ..crypto.key import uses_same_id_hash, uses_same_chunker_secret\nfrom ..helpers import Error\nfrom ..helpers import location_validator, Location, archivename_validator, comment_validator\nfrom ..helpers import format_file_size, bin_to_hex\nfrom ..helpers import ChunkerParams, ChunkIteratorFileWrapper, CompressionSpec\nfrom ..helpers.argparsing import ArgumentParser, ArgumentTypeError\nfrom ..item import ChunkListEntry\nfrom ..manifest import Manifest\nfrom ..legacyrepository import LegacyRepository\nfrom ..repository import Repository\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\ndef transfer_chunks(\n    upgrader,\n    other_repository,\n    other_manifest,\n    other_chunks,\n    archive,\n    cache,\n    manifest,\n    recompress,\n    dry_run,\n    chunker_params=None,\n):\n    \"\"\"\n    Transfer chunks from another repository to the current repository.\n\n    If chunker_params is provided, the chunks will be re-chunked using the specified parameters.\n    \"\"\"\n    transfer = 0\n    present = 0\n    chunks = []\n\n    # Determine if re-chunking is needed\n    rechunkify = chunker_params is not None\n\n    if rechunkify:\n        # Similar to ArchiveRecreater.iter_chunks\n        pipeline = DownloadPipeline(other_manifest.repository, other_manifest.repo_objs)\n        chunk_iterator = pipeline.fetch_many(other_chunks, ro_type=ROBJ_FILE_STREAM)\n        file = ChunkIteratorFileWrapper(chunk_iterator)\n\n        # Create a chunker with the specified parameters\n        chunker = get_chunker(*chunker_params, key=manifest.key, sparse=False)\n        for chunk in chunker.chunkify(file):\n            if not dry_run:\n                chunk_id, data = cached_hash(chunk, archive.key.id_hash)\n                size = len(data)\n                # Check if the chunk is already in the repository\n                chunk_present = cache.seen_chunk(chunk_id, size)\n                if chunk_present:\n                    chunk_entry = cache.reuse_chunk(chunk_id, size, archive.stats)\n                    present += size\n                else:\n                    # Add the new chunk to the repository\n                    chunk_entry = cache.add_chunk(\n                        chunk_id, {}, data, stats=archive.stats, wait=False, ro_type=ROBJ_FILE_STREAM\n                    )\n                    cache.repository.async_response(wait=False)\n                    transfer += size\n                chunks.append(chunk_entry)\n            else:\n                # In dry-run mode, just estimate the size\n                size = len(chunk.data) if chunk.data is not None else chunk.size\n                transfer += size\n    else:\n        # Original implementation without re-chunking\n        for chunk_id, size in other_chunks:\n            chunk_present = cache.seen_chunk(chunk_id, size)\n            if not chunk_present:  # target repo does not yet have this chunk\n                if not dry_run:\n                    try:\n                        cdata = other_repository.get(chunk_id)\n                    except (Repository.ObjectNotFound, LegacyRepository.ObjectNotFound):\n                        # A missing correct chunk in other_repository (source) will result in\n                        # a missing chunk in repository (destination).\n                        # We do NOT want to transfer all-zero replacement chunks from Borg 1 repositories.\n                        # But we want to have a correct chunks list entry. That will be useful in case the\n                        # chunk reappears, and also we could dynamically generate an all-zero replacement\n                        # of the correct size for reading / extracting, if desired.\n                        chunk_entry = ChunkListEntry(chunk_id, size)\n                    else:\n                        if recompress == \"never\":\n                            # Keep the compressed payload the same; verify via assert_id (that will\n                            # decompress, but avoids needing to compress it again):\n                            meta, data = other_manifest.repo_objs.parse(\n                                chunk_id, cdata, decompress=True, want_compressed=True, ro_type=ROBJ_FILE_STREAM\n                            )\n                            meta, data = upgrader.upgrade_compressed_chunk(meta, data)\n                            chunk_entry = cache.add_chunk(\n                                chunk_id,\n                                meta,\n                                data,\n                                stats=archive.stats,\n                                wait=False,\n                                compress=False,\n                                size=size,\n                                ctype=meta[\"ctype\"],\n                                clevel=meta[\"clevel\"],\n                                ro_type=ROBJ_FILE_STREAM,\n                            )\n                        elif recompress == \"always\":\n                            # always decompress and re-compress file data chunks\n                            meta, data = other_manifest.repo_objs.parse(chunk_id, cdata, ro_type=ROBJ_FILE_STREAM)\n                            chunk_entry = cache.add_chunk(\n                                chunk_id, meta, data, stats=archive.stats, wait=False, ro_type=ROBJ_FILE_STREAM\n                            )\n                        else:\n                            raise ValueError(f\"unsupported recompress mode: {recompress}\")\n                    cache.repository.async_response(wait=False)\n                    chunks.append(chunk_entry)\n                transfer += size\n            else:\n                if not dry_run:\n                    chunk_entry = cache.reuse_chunk(chunk_id, size, archive.stats)\n                    chunks.append(chunk_entry)\n                present += size\n\n    return chunks, transfer, present\n\n\nclass TransferMixIn:\n    @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,))\n    @with_repository(manifest=True, cache=True, compatibility=(Manifest.Operation.WRITE,))\n    def do_transfer(self, args, *, repository, manifest, cache, other_repository=None, other_manifest=None):\n        \"\"\"archives transfer from other repository, optionally upgrade data format\"\"\"\n        key = manifest.key\n        other_key = other_manifest.key\n        if not uses_same_id_hash(other_key, key):\n            raise Error(\n                \"You must keep the same ID hash ([HMAC-]SHA256 or BLAKE2b) or deduplication will break. \"\n                \"Use a related repository!\"\n            )\n        if not uses_same_chunker_secret(other_key, key):\n            raise Error(\n                \"You must use the same chunker secret or deduplication will break. \" \"Use a related repository!\"\n            )\n\n        dry_run = args.dry_run\n        archive_infos = other_manifest.archives.list_considering(args)\n        count = len(archive_infos)\n        if count == 0:\n            return\n\n        an_errors = []\n        for archive_info in archive_infos:\n            try:\n                archivename_validator(archive_info.name)\n            except ArgumentTypeError as err:\n                an_errors.append(str(err))\n        if an_errors:\n            an_errors.insert(0, \"Invalid archive names detected, please rename them before transfer:\")\n            raise Error(\"\\n\".join(an_errors))\n\n        ac_errors = []\n        for archive_info in archive_infos:\n            archive = Archive(other_manifest, archive_info.id)\n            try:\n                comment_validator(archive.metadata.get(\"comment\", \"\"))\n            except ArgumentTypeError as err:\n                ac_errors.append(f\"{archive_info.name}: {err}\")\n        if ac_errors:\n            ac_errors.insert(0, \"Invalid archive comments detected, please fix them before transfer:\")\n            raise Error(\"\\n\".join(ac_errors))\n\n        from .. import upgrade as upgrade_mod\n\n        v1_or_v2 = getattr(args, \"v1_or_v2\", False)\n        upgrader = args.upgrader\n        if upgrader == \"NoOp\" and v1_or_v2:\n            upgrader = \"From12To20\"\n\n        try:\n            UpgraderCls = getattr(upgrade_mod, f\"Upgrader{upgrader}\")\n        except AttributeError:\n            raise Error(f\"No such upgrader: {upgrader}\")\n\n        if UpgraderCls is not upgrade_mod.UpgraderFrom12To20 and other_manifest.repository.version == 1:\n            raise Error(\"To transfer from a borg 1.x repo, you need to use: --upgrader=From12To20\")\n\n        upgrader = UpgraderCls(cache=cache, args=args)\n\n        for archive_info in archive_infos:\n            name, id, ts = archive_info.name, archive_info.id, archive_info.ts\n            id_hex, ts_str = bin_to_hex(id), ts.isoformat()\n            transfer_size = 0\n            present_size = 0\n            # At least for Borg 1.x -> Borg 2 transfers, we cannot use the ID to check for\n            # already transferred archives (due to upgrade of the metadata stream, the ID will be\n            # different anyway). So we use the archive name and timestamp.\n            # The name alone might be sufficient for Borg 1.x -> 2 transfers, but it isn't\n            # for 2 -> 2 transfers, because Borg 2 allows duplicate names (\"series\" feature).\n            # So, the best is to check for both name/ts and name/id.\n            if not dry_run and manifest.archives.exists_name_and_ts(name, archive_info.ts):\n                # Useful for Borg 1.x -> 2 transfers; we have unique names in Borg 1.x.\n                # Also useful for Borg 2 -> 2 transfers with metadata changes (ID changes).\n                print(f\"{name} {ts_str}: archive is already present in destination repo, skipping.\")\n            elif not dry_run and manifest.archives.exists_name_and_id(name, id):\n                # Useful for Borg 2 -> 2 transfers without changes (ID stays the same)\n                print(f\"{name} {id_hex}: archive is already present in destination repo, skipping.\")\n            else:\n                if not dry_run:\n                    print(f\"{name} {ts_str} {id_hex}: copying archive to destination repo...\")\n                other_archive = Archive(other_manifest, id)\n                archive = (\n                    Archive(manifest, name, cache=cache, create=True, progress=args.progress) if not dry_run else None\n                )\n                upgrader.new_archive(archive=archive)\n                for item in other_archive.iter_items():\n                    is_part = bool(item.get(\"part\", False))\n                    if is_part:\n                        # Borg 1.x created part files while checkpointing (in addition to the full\n                        # file in the final archive), like <filename>.borg_part_<part> with item.part >= 1.\n                        # Borg 2 archives do not have such special part items anymore.\n                        # So let's remove them from old archives also, considering there is no\n                        # code anymore that deals with them in special ways (e.g., to get stats right).\n                        continue\n                    if \"chunks_healthy\" in item:  # legacy\n                        other_chunks = item.chunks_healthy  # chunks_healthy has the CORRECT chunks list, if present.\n                    elif \"chunks\" in item:\n                        other_chunks = item.chunks\n                    else:\n                        other_chunks = None\n                    if other_chunks is not None:\n                        chunks, transfer, present = transfer_chunks(\n                            upgrader,\n                            other_repository,\n                            other_manifest,\n                            other_chunks,\n                            archive,\n                            cache,\n                            manifest,\n                            args.recompress,\n                            dry_run,\n                            args.chunker_params,\n                        )\n                        if not dry_run:\n                            item.chunks = chunks\n                            archive.stats.nfiles += 1\n                        transfer_size += transfer\n                        present_size += present\n                    if not dry_run:\n                        item = upgrader.upgrade_item(item=item)\n                        archive.add_item(item, show_progress=args.progress)\n                if not dry_run:\n                    if args.progress:\n                        archive.stats.show_progress(final=True)\n                    additional_metadata = upgrader.upgrade_archive_metadata(metadata=other_archive.metadata)\n                    archive.save(additional_metadata=additional_metadata)\n                    print(\n                        f\"{name} {ts_str} {id_hex}: finished. \"\n                        f\"transfer_size: {format_file_size(transfer_size)} \"\n                        f\"present_size: {format_file_size(present_size)}\"\n                    )\n                else:\n                    print(\n                        f\"{name} {ts_str} {id_hex}: completed\"\n                        if transfer_size == 0\n                        else f\"{name} {ts_str} {id_hex}: incomplete, \"\n                        f\"transfer_size: {format_file_size(transfer_size)} \"\n                        f\"present_size: {format_file_size(present_size)}\"\n                    )\n\n    def build_parser_transfer(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n        from ._common import define_archive_filters_group\n\n        transfer_epilog = process_epilog(\n            \"\"\"\n        This command transfers archives from one repository to another repository.\n        Optionally, it can also upgrade the transferred data.\n        Optionally, it can also recompress the transferred data.\n        Optionally, it can also re-chunk the transferred data using different chunker parameters.\n\n        It is easiest (and fastest) to give ``--compression=COMPRESSION --recompress=never`` using\n        the same COMPRESSION mode as in the SRC_REPO - borg will use that COMPRESSION for metadata (in\n        any case) and keep data compressed \"as is\" (saves time as no data compression is needed).\n\n        If you want to globally change compression while transferring archives to the DST_REPO,\n        give ``--compress=WANTED_COMPRESSION --recompress=always``.\n\n        The default is to transfer all archives.\n\n        You could use the misc. archive filter options to limit which archives it will\n        transfer, e.g. using the ``-a`` option. This is recommended for big\n        repositories with multiple data sets to keep the runtime per invocation lower.\n\n        General purpose archive transfer\n        ++++++++++++++++++++++++++++++++\n\n        Transfer borg2 archives into a related other borg2 repository::\n\n            # create a related DST_REPO (reusing key material from SRC_REPO), so that\n            # chunking and chunk id generation will work in the same way as before.\n            borg --repo=DST_REPO repo-create --encryption=DST_ENC --other-repo=SRC_REPO\n\n            # transfer archives from SRC_REPO to DST_REPO\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run  # check what it would do\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO            # do it!\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run  # check! anything left?\n\n        Data migration / upgrade from borg 1.x\n        ++++++++++++++++++++++++++++++++++++++\n\n        To migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar\n        to the above, but you need the ``--from-borg1`` option::\n\n            borg --repo=DST_REPO repocreate --encryption=DST_ENC --other-repo=SRC_REPO --from-borg1\n\n            # to continue using lz4 compression as you did in SRC_REPO:\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\\\\n                 --compress=lz4 --recompress=never\n\n            # alternatively, to recompress everything to zstd,3:\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\\\\n                 --compress=zstd,3 --recompress=always\n\n            # to re-chunk using different chunker parameters:\n            borg --repo=DST_REPO transfer --other-repo=SRC_REPO \\\\\n                 --chunker-params=buzhash,19,23,21,4095\n\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_transfer.__doc__, epilog=transfer_epilog\n        )\n        subparsers.add_subcommand(\"transfer\", subparser, help=\"transfer of archives from another repository\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change repository, just check\"\n        )\n        subparser.add_argument(\n            \"--other-repo\",\n            metavar=\"SRC_REPOSITORY\",\n            dest=\"other_location\",\n            type=location_validator(other=True),\n            default=Location(other=True),\n            action=Highlander,\n            help=\"transfer archives from the other repository\",\n        )\n        subparser.add_argument(\n            \"--from-borg1\", dest=\"v1_or_v2\", action=\"store_true\", help=\"other repository is borg 1.x\"\n        )\n        subparser.add_argument(\n            \"--upgrader\",\n            metavar=\"UPGRADER\",\n            dest=\"upgrader\",\n            type=str,\n            choices=(\"NoOp\", \"From12To20\"),\n            default=\"NoOp\",\n            action=Highlander,\n            help=\"use the upgrader to convert transferred data (default: no conversion)\",\n        )\n        subparser.add_argument(\n            \"-C\",\n            \"--compression\",\n            metavar=\"COMPRESSION\",\n            dest=\"compression\",\n            type=CompressionSpec,\n            default=CompressionSpec(\"lz4\"),\n            action=Highlander,\n            help=\"select compression algorithm, see the output of the \" '\"borg help compression\" command for details.',\n        )\n        subparser.add_argument(\n            \"--recompress\",\n            metavar=\"MODE\",\n            dest=\"recompress\",\n            nargs=\"?\",\n            default=\"never\",\n            const=\"always\",\n            choices=(\"never\", \"always\"),\n            action=Highlander,\n            help=\"recompress data chunks according to `MODE` and ``--compression``. \"\n            \"Possible modes are \"\n            \"`always`: recompress unconditionally; and \"\n            \"`never`: do not recompress (faster: re-uses compressed data chunks w/o change).\"\n            \"If no MODE is given, `always` will be used. \"\n            'Not passing --recompress is equivalent to \"--recompress never\".',\n        )\n        subparser.add_argument(\n            \"--chunker-params\",\n            metavar=\"PARAMS\",\n            dest=\"chunker_params\",\n            type=ChunkerParams,\n            default=None,\n            action=Highlander,\n            help=\"rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, \"\n            \"HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. \"\n            \"default: do not rechunk\",\n        )\n\n        define_archive_filters_group(subparser)\n"
  },
  {
    "path": "src/borg/archiver/undelete_cmd.py",
    "content": "import logging\n\nfrom ._common import with_repository\nfrom ..constants import *  # NOQA\nfrom ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..manifest import Manifest\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass UnDeleteMixIn:\n    @with_repository(manifest=False)\n    def do_undelete(self, args, repository):\n        \"\"\"Undeletes archives.\"\"\"\n        self.output_list = args.output_list\n        dry_run = args.dry_run\n        manifest = Manifest.load(repository, (Manifest.Operation.DELETE,))\n        if args.name:\n            archive_infos = [manifest.archives.get_one([args.name], deleted=True)]\n        else:\n            args.deleted = True\n            archive_infos = manifest.archives.list_considering(args)\n        count = len(archive_infos)\n        if count == 0:\n            return\n        if not args.name and not args.match_archives and args.first == 0 and args.last == 0:\n            raise CommandError(\"Aborting: if you really want to undelete all archives, please use -a 'sh:*'.\")\n\n        undeleted = False\n        logger_list = logging.getLogger(\"borg.output.list\")\n        for i, archive_info in enumerate(archive_infos, 1):\n            name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)\n            try:\n                if not dry_run:\n                    manifest.archives.undelete_by_id(id)\n            except KeyError:\n                self.print_warning(f\"Archive {name} {hex_id} not found ({i}/{count}).\")\n            else:\n                undeleted = True\n                if self.output_list:\n                    msg = \"Would undelete: {} ({}/{})\" if dry_run else \"Undeleted archive: {} ({}/{})\"\n                    logger_list.info(msg.format(format_archive(archive_info), i, count))\n        if dry_run:\n            logger.info(\"Finished dry-run.\")\n        elif undeleted:\n            manifest.write()\n            self.print_warning(\"Done.\", wc=None)\n        else:\n            self.print_warning(\"Aborted.\", wc=None)\n        return\n\n    def build_parser_undelete(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog, define_archive_filters_group\n\n        undelete_epilog = process_epilog(\n            \"\"\"\n        This command undeletes archives in the repository.\n\n        Important: Undeleting archives is only possible before compacting.\n        Once ``borg compact`` has run, all disk space occupied only by the\n        soft-deleted archives will be freed, and undeleting is no longer\n        possible.\n\n        When in doubt, use ``--dry-run --list`` to see what would be\n        undeleted.\n\n        You can undelete multiple archives by specifying a match pattern using\n        the ``--match-archives PATTERN`` option (for more information on these\n        patterns, see :ref:`borg_patterns`).\n        \"\"\"\n        )\n        subparser = ArgumentParser(\n            parents=[common_parser], description=self.do_undelete.__doc__, epilog=undelete_epilog\n        )\n        subparsers.add_subcommand(\"undelete\", subparser, help=\"undelete archives\")\n        subparser.add_argument(\n            \"-n\", \"--dry-run\", dest=\"dry_run\", action=\"store_true\", help=\"do not change the repository\"\n        )\n        subparser.add_argument(\n            \"--list\", dest=\"output_list\", action=\"store_true\", help=\"output a verbose list of archives\"\n        )\n        define_archive_filters_group(subparser)\n        subparser.add_argument(\n            \"name\", metavar=\"NAME\", nargs=\"?\", type=archivename_validator, help=\"specify the archive name\"\n        )\n"
  },
  {
    "path": "src/borg/archiver/version_cmd.py",
    "content": "from .. import __version__\nfrom ..constants import *  # NOQA\nfrom ..helpers.argparsing import ArgumentParser\nfrom ..remote import RemoteRepository\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass VersionMixIn:\n    def do_version(self, args):\n        \"\"\"Displays the Borg client and server versions.\"\"\"\n        from borg.version import parse_version, format_version\n\n        client_version = parse_version(__version__)\n        if args.location.proto in (\"ssh\", \"socket\"):\n            with RemoteRepository(args.location, lock=False, args=args) as repository:\n                server_version = repository.server_version\n        else:\n            server_version = client_version\n        print(f\"{format_version(client_version)} / {format_version(server_version)}\")\n\n    def build_parser_version(self, subparsers, common_parser, mid_common_parser):\n        from ._common import process_epilog\n\n        version_epilog = process_epilog(\n            \"\"\"\n        This command displays the Borg client and server versions.\n\n        If a local repository is given, the client code directly accesses the repository,\n        so the client version is also shown as the server version.\n\n        If a remote repository is given (e.g., ssh:), the remote Borg is queried, and\n        its version is displayed as the server version.\n\n        Examples::\n\n            # local repository (client uses 1.4.0 alpha version)\n            $ borg version /mnt/backup\n            1.4.0a / 1.4.0a\n\n            # remote repository (client uses 1.4.0 alpha, server uses 1.2.7 release)\n            $ borg version ssh://borg@borgbackup:repo\n            1.4.0a / 1.2.7\n\n        Due to the version tuple format used in Borg client/server negotiation, only\n        a simplified version is displayed (as provided by borg.version.format_version).\n\n        You can also use ``borg --version`` to display a potentially more precise client version.\n        \"\"\"\n        )\n        subparser = ArgumentParser(parents=[common_parser], description=self.do_version.__doc__, epilog=version_epilog)\n        subparsers.add_subcommand(\"version\", subparser, help=\"display the Borg client and server versions\")\n"
  },
  {
    "path": "src/borg/cache.py",
    "content": "import configparser\nimport io\nimport os\nimport shutil\nimport stat\nfrom collections import namedtuple\nfrom datetime import datetime, timezone, timedelta\nfrom pathlib import Path\nfrom time import perf_counter\n\nfrom xxhash import xxh64\n\nfrom borgstore.backends.errors import PermissionDenied\n\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfiles_cache_logger = create_logger(\"borg.debug.files_cache\")\n\nfrom borgstore.store import ItemInfo\n\nfrom .constants import CACHE_README, FILES_CACHE_MODE_DISABLED, ROBJ_FILE_STREAM, TIME_DIFFERS2_NS\nfrom .hashindex import ChunkIndex, ChunkIndexEntry\nfrom .helpers import Error\nfrom .helpers import get_cache_dir, get_security_dir\nfrom .helpers import hex_to_bin, bin_to_hex, parse_stringified_list\nfrom .helpers import format_file_size, safe_encode\nfrom .helpers import safe_ns\nfrom .helpers import yes\nfrom .helpers import ProgressIndicatorMessage\nfrom .helpers import msgpack\nfrom .helpers.msgpack import int_to_timestamp, timestamp_to_int\nfrom .item import ChunkListEntry\nfrom .crypto.key import PlaintextKey\nfrom .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError\nfrom .manifest import Manifest\nfrom .platform import SaveFile\nfrom .remote import RemoteRepository\nfrom .repository import LIST_SCAN_LIMIT, Repository, StoreObjectNotFound, repo_lister\n\n\ndef files_cache_name(archive_name, files_cache_name=\"files\"):\n    \"\"\"\n    Return the name of the files cache file for the given archive name.\n\n    :param archive_name: name of the archive (ideally a series name)\n    :param files_cache_name: base name of the files cache file\n    :return: name of the files cache file\n    \"\"\"\n    suffix = os.environ.get(\"BORG_FILES_CACHE_SUFFIX\", \"\")\n    # when using archive series, we automatically make up a separate cache file per series.\n    # when not, the user may manually do that by using the env var.\n    if not suffix:\n        # avoid issues with too complex or long archive_name by hashing it:\n        suffix = xxh64(archive_name.encode()).hexdigest()\n    return files_cache_name + \".\" + suffix\n\n\ndef discover_files_cache_names(path, files_cache_name=\"files\"):\n    \"\"\"\n    Return a list of all files cache file names in the given directory.\n\n    :param path: path to the directory to search in\n    :param files_cache_name: base name of the files cache files\n    :return: list of files cache file names\n    \"\"\"\n    return [p.name for p in path.iterdir() if p.name.startswith(files_cache_name + \".\")]\n\n\n# chunks is a list of ChunkListEntry\nFileCacheEntry = namedtuple(\"FileCacheEntry\", \"age inode size ctime mtime chunks\")\n\n\nclass SecurityManager:\n    \"\"\"\n    Tracks repositories. Ensures that nothing bad happens (repository swaps,\n    replay attacks, unknown repositories, etc.).\n\n    This is complicated by the cache being initially used for this, while\n    only some commands actually use the cache, which meant that other commands\n    did not perform these checks.\n\n    Further complications were created by the cache being a cache, so it\n    could be legitimately deleted, which is annoying because Borg did not\n    recognize repositories after that.\n\n    Therefore, a second location, the security database (see get_security_dir),\n    was introduced, which stores this information. However, this means that\n    the code has to deal with a cache existing but no security database entry,\n    or inconsistencies between the security database and the cache which have to\n    be reconciled, and also with no cache existing but a security database entry.\n    \"\"\"\n\n    def __init__(self, repository):\n        self.repository = repository\n        self.dir = Path(get_security_dir(repository.id_str, legacy=(repository.version == 1)))\n        self.key_type_file = self.dir / \"key-type\"\n        self.location_file = self.dir / \"location\"\n        self.manifest_ts_file = self.dir / \"manifest-timestamp\"\n\n    @staticmethod\n    def destroy(repository, path=None):\n        \"\"\"Destroys the security directory for ``repository`` or at ``path``.\"\"\"\n        path = path or get_security_dir(repository.id_str, legacy=(repository.version == 1))\n        if Path(path).exists():\n            shutil.rmtree(path)\n\n    def known(self):\n        return all(f.exists() for f in (self.key_type_file, self.location_file, self.manifest_ts_file))\n\n    def key_matches(self, key):\n        if not self.known():\n            return False\n        try:\n            with self.key_type_file.open() as fd:\n                type = fd.read()\n                return type == str(key.TYPE)\n        except OSError as exc:\n            logger.warning(\"Could not read/parse key type file: %s\", exc)\n\n    def save(self, manifest, key):\n        logger.debug(\"security: saving state for %s to %s\", self.repository.id_str, str(self.dir))\n        current_location = self.repository._location.canonical_path()\n        logger.debug(\"security: current location   %s\", current_location)\n        logger.debug(\"security: key type           %s\", str(key.TYPE))\n        logger.debug(\"security: manifest timestamp %s\", manifest.timestamp)\n        with SaveFile(self.location_file) as fd:\n            fd.write(current_location)\n        with SaveFile(self.key_type_file) as fd:\n            fd.write(str(key.TYPE))\n        with SaveFile(self.manifest_ts_file) as fd:\n            fd.write(manifest.timestamp)\n\n    def assert_location_matches(self):\n        # Warn user before sending data to a relocated repository\n        try:\n            with self.location_file.open() as fd:\n                previous_location = fd.read()\n            logger.debug(\"security: read previous location %r\", previous_location)\n        except FileNotFoundError:\n            logger.debug(\"security: previous location file %s not found\", self.location_file)\n            previous_location = None\n        except OSError as exc:\n            logger.warning(\"Could not read previous location file: %s\", exc)\n            previous_location = None\n\n        repository_location = self.repository._location.canonical_path()\n        if previous_location and previous_location != repository_location:\n            msg = (\n                \"Warning: The repository at location {} was previously located at {}\\n\".format(\n                    repository_location, previous_location\n                )\n                + \"Do you want to continue? [yN] \"\n            )\n            if not yes(\n                msg,\n                false_msg=\"Aborting.\",\n                invalid_msg=\"Invalid answer, aborting.\",\n                retry=False,\n                env_var_override=\"BORG_RELOCATED_REPO_ACCESS_IS_OK\",\n            ):\n                raise Cache.RepositoryAccessAborted()\n            # adapt on-disk config immediately if the new location was accepted\n            logger.debug(\"security: updating location stored in security dir\")\n            with SaveFile(self.location_file) as fd:\n                fd.write(repository_location)\n\n    def assert_no_manifest_replay(self, manifest, key):\n        try:\n            with self.manifest_ts_file.open() as fd:\n                timestamp = fd.read()\n            logger.debug(\"security: read manifest timestamp %r\", timestamp)\n        except FileNotFoundError:\n            logger.debug(\"security: manifest timestamp file %s not found\", self.manifest_ts_file)\n            timestamp = \"\"\n        except OSError as exc:\n            logger.warning(\"Could not read previous location file: %s\", exc)\n            timestamp = \"\"\n        logger.debug(\"security: determined newest manifest timestamp as %s\", timestamp)\n        # If repository is older than the cache or security dir something fishy is going on\n        if timestamp and timestamp > manifest.timestamp:\n            if isinstance(key, PlaintextKey):\n                raise Cache.RepositoryIDNotUnique()\n            else:\n                raise Cache.RepositoryReplay()\n\n    def assert_key_type(self, key):\n        # Make sure an encrypted repository has not been swapped for an unencrypted repository\n        if self.known() and not self.key_matches(key):\n            raise Cache.EncryptionMethodMismatch()\n\n    def assert_secure(self, manifest, key, *, warn_if_unencrypted=True):\n        # warn_if_unencrypted=False is only used for initializing a new repository.\n        # Thus, avoiding asking about a repository that's currently initializing.\n        self.assert_access_unknown(warn_if_unencrypted, manifest, key)\n        self._assert_secure(manifest, key)\n        logger.debug(\"security: repository checks ok, allowing access\")\n\n    def _assert_secure(self, manifest, key):\n        self.assert_location_matches()\n        self.assert_key_type(key)\n        self.assert_no_manifest_replay(manifest, key)\n        if not self.known():\n            logger.debug(\"security: remembering previously unknown repository\")\n            self.save(manifest, key)\n\n    def assert_access_unknown(self, warn_if_unencrypted, manifest, key):\n        # warn_if_unencrypted=False is only used for initializing a new repository.\n        # Thus, avoiding asking about a repository that's currently initializing.\n        if not key.logically_encrypted and not self.known():\n            msg = (\n                \"Warning: Attempting to access a previously unknown unencrypted repository!\\n\"\n                + \"Do you want to continue? [yN] \"\n            )\n            allow_access = not warn_if_unencrypted or yes(\n                msg,\n                false_msg=\"Aborting.\",\n                invalid_msg=\"Invalid answer, aborting.\",\n                retry=False,\n                env_var_override=\"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK\",\n            )\n            if allow_access:\n                if warn_if_unencrypted:\n                    logger.debug(\"security: remembering unknown unencrypted repository (explicitly allowed)\")\n                else:\n                    logger.debug(\"security: initializing unencrypted repository\")\n                self.save(manifest, key)\n            else:\n                raise Cache.CacheInitAbortedError()\n\n\ndef assert_secure(repository, manifest):\n    sm = SecurityManager(repository)\n    sm.assert_secure(manifest, manifest.key)\n\n\ndef cache_dir(repository, path=None):\n    return Path(path) if path else Path(get_cache_dir()) / repository.id_str\n\n\nclass CacheConfig:\n    def __init__(self, repository, path=None):\n        self.repository = repository\n        self.path = cache_dir(repository, path)\n        logger.debug(\"Using %s as cache\", self.path)\n        self.config_path = self.path / \"config\"\n\n    def __enter__(self):\n        self.open()\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def exists(self):\n        return self.config_path.exists()\n\n    def create(self):\n        assert not self.exists()\n        config = configparser.ConfigParser(interpolation=None)\n        config.add_section(\"cache\")\n        config.set(\"cache\", \"version\", \"1\")\n        config.set(\"cache\", \"repository\", self.repository.id_str)\n        config.set(\"cache\", \"manifest\", \"\")\n        config.add_section(\"integrity\")\n        config.set(\"integrity\", \"manifest\", \"\")\n        with SaveFile(self.config_path) as fd:\n            config.write(fd)\n\n    def open(self):\n        self.load()\n\n    def load(self):\n        self._config = configparser.ConfigParser(interpolation=None)\n        with self.config_path.open() as fd:\n            self._config.read_file(fd)\n        self._check_upgrade(self.config_path)\n        self.id = self._config.get(\"cache\", \"repository\")\n        self.manifest_id = hex_to_bin(self._config.get(\"cache\", \"manifest\"))\n        self.ignored_features = set(parse_stringified_list(self._config.get(\"cache\", \"ignored_features\", fallback=\"\")))\n        self.mandatory_features = set(\n            parse_stringified_list(self._config.get(\"cache\", \"mandatory_features\", fallback=\"\"))\n        )\n        try:\n            self.integrity = dict(self._config.items(\"integrity\"))\n            if self._config.get(\"cache\", \"manifest\") != self.integrity.pop(\"manifest\"):\n                # The cache config file is updated (parsed with ConfigParser, the state of the ConfigParser\n                # is modified and then written out.), not re-created.\n                # Thus, older versions will leave our [integrity] section alone, making the section's data invalid.\n                # Therefore, we also add the manifest ID to this section and\n                # can discern whether an older version interfered by comparing the manifest IDs of this section\n                # and the main [cache] section.\n                self.integrity = {}\n                logger.warning(\"Cache integrity data not available: old Borg version modified the cache.\")\n        except configparser.NoSectionError:\n            logger.debug(\"Cache integrity: No integrity data found (files, chunks). Cache is from old version.\")\n            self.integrity = {}\n\n    def save(self, manifest=None):\n        if manifest:\n            self._config.set(\"cache\", \"manifest\", manifest.id_str)\n            self._config.set(\"cache\", \"ignored_features\", \",\".join(self.ignored_features))\n            self._config.set(\"cache\", \"mandatory_features\", \",\".join(self.mandatory_features))\n            if not self._config.has_section(\"integrity\"):\n                self._config.add_section(\"integrity\")\n            for file, integrity_data in self.integrity.items():\n                self._config.set(\"integrity\", file, integrity_data)\n            self._config.set(\"integrity\", \"manifest\", manifest.id_str)\n        with SaveFile(self.config_path) as fd:\n            self._config.write(fd)\n\n    def close(self):\n        pass\n\n    def _check_upgrade(self, config_path):\n        try:\n            cache_version = self._config.getint(\"cache\", \"version\")\n            wanted_version = 1\n            if cache_version != wanted_version:\n                self.close()\n                raise Exception(\n                    \"%s has unexpected cache version %d (wanted: %d).\" % (config_path, cache_version, wanted_version)\n                )\n        except configparser.NoSectionError:\n            self.close()\n            raise Exception(\"%s does not look like a Borg cache.\" % config_path) from None\n\n\nclass Cache:\n    \"\"\"Client Side cache\"\"\"\n\n    class CacheInitAbortedError(Error):\n        \"\"\"Cache initialization aborted\"\"\"\n\n        exit_mcode = 60\n\n    class EncryptionMethodMismatch(Error):\n        \"\"\"Repository encryption method changed since last access, refusing to continue\"\"\"\n\n        exit_mcode = 61\n\n    class RepositoryAccessAborted(Error):\n        \"\"\"Repository access aborted\"\"\"\n\n        exit_mcode = 62\n\n    class RepositoryIDNotUnique(Error):\n        \"\"\"Cache is newer than repository - do you have multiple, independently updated repos with same ID?\"\"\"\n\n        exit_mcode = 63\n\n    class RepositoryReplay(Error):\n        \"\"\"Cache, or information obtained from the security directory is newer than repository - this is either an attack or unsafe (multiple repos with same ID)\"\"\"\n\n        exit_mcode = 64\n\n    @staticmethod\n    def break_lock(repository, path=None):\n        pass\n\n    @staticmethod\n    def destroy(repository, path=None):\n        \"\"\"destroy the cache for ``repository`` or at ``path``\"\"\"\n        path = cache_dir(repository, path)\n        config = path / \"config\"\n        if config.exists():\n            config.unlink()  # kill config first\n            shutil.rmtree(path)\n\n    def __new__(\n        cls,\n        repository,\n        manifest,\n        path=None,\n        sync=True,\n        warn_if_unencrypted=True,\n        progress=False,\n        cache_mode=FILES_CACHE_MODE_DISABLED,\n        iec=False,\n        archive_name=None,\n        start_backup=None,\n    ):\n        return AdHocWithFilesCache(\n            manifest=manifest,\n            path=path,\n            warn_if_unencrypted=warn_if_unencrypted,\n            progress=progress,\n            iec=iec,\n            cache_mode=cache_mode,\n            archive_name=archive_name,\n            start_backup=start_backup,\n        )\n\n\nclass FilesCacheMixin:\n    \"\"\"\n    Massively accelerate processing of unchanged files.\n    We read the \"files cache\" (either from cache directory or from previous archive\n    in repo) that has metadata for all \"already stored\" files, like size, ctime/mtime,\n    inode number and chunks id/size list.\n    When finding a file on disk, we use the metadata to determine if the file is unchanged.\n    If so, we use the cached chunks list and skip reading/chunking the file contents.\n    \"\"\"\n\n    FILES_CACHE_NAME = \"files\"\n\n    def __init__(self, cache_mode, archive_name=None, start_backup=None):\n        self.archive_name = archive_name  # ideally a SERIES name\n        assert not (\"c\" in cache_mode and \"m\" in cache_mode)\n        assert \"d\" in cache_mode or \"c\" in cache_mode or \"m\" in cache_mode\n        self.cache_mode = cache_mode\n        self._files = None\n        self._newest_cmtime = 0\n        self._newest_path_hashes = set()\n        self.start_backup = start_backup\n\n    def compress_entry(self, entry):\n        \"\"\"\n        compress a files cache entry:\n\n        - use the ChunkIndex to \"compress\" the entry's chunks list (256bit key + 32bit size -> 32bit index).\n        - use msgpack to pack the entry (reduce memory usage by packing and having less python objects).\n\n        Note: the result is only valid while the ChunkIndex is in memory!\n        \"\"\"\n        assert isinstance(self.chunks, ChunkIndex), f\"{self.chunks} is not a ChunkIndex\"\n        assert isinstance(entry, FileCacheEntry)\n        compressed_chunks = []\n        for id, size in entry.chunks:\n            cie = self.chunks[id]  # may raise KeyError if chunk id is not in repo\n            if cie.size == 0:  # size is not known in the chunks index yet\n                self.chunks[id] = cie._replace(size=size)\n            else:\n                assert size == cie.size, f\"{size} != {cie.size}\"\n            idx = self.chunks.k_to_idx(id)\n            compressed_chunks.append(idx)\n        entry = entry._replace(chunks=compressed_chunks)\n        return msgpack.packb(entry)\n\n    def decompress_entry(self, entry_packed):\n        \"\"\"reverse operation of compress_entry\"\"\"\n        assert isinstance(self.chunks, ChunkIndex), f\"{self.chunks} is not a ChunkIndex\"\n        assert isinstance(entry_packed, bytes)\n        entry = msgpack.unpackb(entry_packed)\n        entry = FileCacheEntry(*entry)\n        chunks = []\n        for idx in entry.chunks:\n            assert isinstance(idx, int), f\"{idx} is not an int\"\n            id = self.chunks.idx_to_k(idx)\n            cie = self.chunks[id]\n            assert cie.size > 0\n            chunks.append((id, cie.size))\n        entry = entry._replace(chunks=chunks)\n        return entry\n\n    @property\n    def files(self):\n        if self._files is None:\n            self._files = self._read_files_cache()  # try loading from cache dir\n        if self._files is None:\n            self._files = self._build_files_cache()  # try loading from repository\n        if self._files is None:\n            self._files = {}  # start from scratch\n        return self._files\n\n    def _build_files_cache(self):\n        \"\"\"rebuild the files cache by reading previous archive from repository\"\"\"\n        if \"d\" in self.cache_mode:  # d(isabled)\n            return\n\n        if not self.archive_name:\n            return\n\n        from .archive import Archive\n\n        # get the latest archive with the IDENTICAL name, supporting archive series:\n        try:\n            archives = self.manifest.archives.list(match=[self.archive_name], sort_by=[\"ts\"], last=1)\n        except PermissionDenied:  # maybe repo is in write-only mode?\n            archives = None\n        if not archives:\n            # nothing found\n            return\n        prev_archive = archives[0]\n\n        files = {}\n        logger.debug(\n            f\"Building files cache from {prev_archive.name} {prev_archive.ts} {bin_to_hex(prev_archive.id)} ...\"\n        )\n        files_cache_logger.debug(\"FILES-CACHE-BUILD: starting...\")\n        archive = Archive(self.manifest, prev_archive.id)\n        for item in archive.iter_items():\n            # only put regular files' infos into the files cache:\n            if stat.S_ISREG(item.mode):\n                path_hash = self.key.id_hash(safe_encode(item.path))\n                # keep track of the key(s) for the most recent timestamp(s):\n                ctime_ns = item.ctime\n                if ctime_ns > self._newest_cmtime:\n                    self._newest_cmtime = ctime_ns\n                    self._newest_path_hashes = {path_hash}\n                elif ctime_ns == self._newest_cmtime:\n                    self._newest_path_hashes.add(path_hash)\n                mtime_ns = item.mtime\n                if mtime_ns > self._newest_cmtime:\n                    self._newest_cmtime = mtime_ns\n                    self._newest_path_hashes = {path_hash}\n                elif mtime_ns == self._newest_cmtime:\n                    self._newest_path_hashes.add(path_hash)\n                # add the file to the in-memory files cache\n                entry = FileCacheEntry(\n                    age=0,\n                    inode=item.get(\"inode\", 0),\n                    size=item.size,\n                    ctime=int_to_timestamp(ctime_ns),\n                    mtime=int_to_timestamp(mtime_ns),\n                    chunks=item.chunks,\n                )\n                # note: if the repo is an a valid state, next line should not fail with KeyError:\n                files[path_hash] = self.compress_entry(entry)\n        # deal with special snapshot / timestamp granularity case, see FAQ:\n        for path_hash in self._newest_path_hashes:\n            del files[path_hash]\n        files_cache_logger.debug(\"FILES-CACHE-BUILD: finished, %d entries loaded.\", len(files))\n        return files\n\n    def files_cache_name(self):\n        return files_cache_name(self.archive_name, self.FILES_CACHE_NAME)\n\n    def discover_files_cache_names(self, path):\n        return discover_files_cache_names(path, self.FILES_CACHE_NAME)\n\n    def _read_files_cache(self):\n        \"\"\"read files cache from cache directory\"\"\"\n        if \"d\" in self.cache_mode:  # d(isabled)\n            return\n\n        files = {}\n        logger.debug(\"Reading files cache ...\")\n        files_cache_logger.debug(\"FILES-CACHE-LOAD: starting...\")\n        msg = None\n        try:\n            with IntegrityCheckedFile(\n                path=str(self.path / self.files_cache_name()),\n                write=False,\n                integrity_data=self.cache_config.integrity.get(self.files_cache_name()),\n            ) as fd:\n                u = msgpack.Unpacker(use_list=True)\n                while True:\n                    data = fd.read(64 * 1024)\n                    if not data:\n                        break\n                    u.feed(data)\n                    try:\n                        for path_hash, entry in u:\n                            entry = FileCacheEntry(*entry)\n                            entry = entry._replace(age=entry.age + 1)\n                            try:\n                                files[path_hash] = self.compress_entry(entry)\n                            except KeyError:\n                                # repo is missing a chunk referenced from entry\n                                logger.debug(f\"compress_entry failed for {entry}, skipping.\")\n                    except (TypeError, ValueError) as exc:\n                        msg = \"The files cache seems invalid. [%s]\" % str(exc)\n                        break\n        except OSError as exc:\n            msg = \"The files cache can't be read. [%s]\" % str(exc)\n        except FileIntegrityError as fie:\n            msg = \"The files cache is corrupted. [%s]\" % str(fie)\n        if msg is not None:\n            logger.debug(msg)\n            files = None\n        files_cache_logger.debug(\"FILES-CACHE-LOAD: finished, %d entries loaded.\", len(files or {}))\n        return files\n\n    def _write_files_cache(self, files):\n        \"\"\"write files cache to cache directory\"\"\"\n        max_time_ns = 2**63 - 1  # nanoseconds, good until y2262\n        # _self._newest_cmtime might be None if it was never set because no files were modified/added.\n        newest_cmtime = self._newest_cmtime if self._newest_cmtime is not None else max_time_ns\n        start_backup_time = self.start_backup - TIME_DIFFERS2_NS if self.start_backup is not None else max_time_ns\n        # we don't want to persist files cache entries of potentially problematic files:\n        discard_after = min(newest_cmtime, start_backup_time)\n        ttl = int(os.environ.get(\"BORG_FILES_CACHE_TTL\", 2))\n        files_cache_logger.debug(\"FILES-CACHE-SAVE: starting...\")\n        cache_path = str(self.path / self.files_cache_name())\n        with SaveFile(cache_path, binary=True) as sync_file:\n            with IntegrityCheckedFile(path=cache_path, write=True, override_fd=sync_file) as fd:\n                entries = 0\n                age_discarded = 0\n                race_discarded = 0\n                for path_hash, entry in files.items():\n                    entry = self.decompress_entry(entry)\n                    if entry.age == 0:  # current entries\n                        if max(timestamp_to_int(entry.ctime), timestamp_to_int(entry.mtime)) < discard_after:\n                            # Only keep files seen in this backup that old enough not to suffer race conditions\n                            # relating to filesystem snapshots and ctime/mtime granularity or being modified\n                            # while we read them.\n                            keep = True\n                        else:\n                            keep = False\n                            race_discarded += 1\n                    else:  # old entries\n                        if entry.age < ttl:\n                            # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet.\n                            keep = True\n                        else:\n                            keep = False\n                            age_discarded += 1\n                    if keep:\n                        msgpack.pack((path_hash, entry), fd)\n                        entries += 1\n            integrity_data = fd.integrity_data\n        files_cache_logger.debug(f\"FILES-CACHE-KILL: removed {age_discarded} entries with age >= TTL [{ttl}]\")\n        t_str = datetime.fromtimestamp(discard_after / 1e9, timezone.utc).isoformat()\n        files_cache_logger.debug(f\"FILES-CACHE-KILL: removed {race_discarded} entries with ctime/mtime >= {t_str}\")\n        files_cache_logger.debug(f\"FILES-CACHE-SAVE: finished, {entries} remaining entries saved.\")\n        return integrity_data\n\n    def file_known_and_unchanged(self, hashed_path, path_hash, st):\n        \"\"\"\n        Check if we know the file that has this path_hash (know == it is in our files cache) and\n        whether it is unchanged (the size/inode number/cmtime is same for stuff we check in this cache_mode).\n\n        :param hashed_path: the file's path as we gave it to hash(hashed_path)\n        :param path_hash: hash(hashed_path), to save some memory in the files cache\n        :param st: the file's stat() result\n        :return: known, chunks (known is True if we have infos about this file in the cache,\n                               chunks is a list[ChunkListEntry] IF the file has not changed, otherwise None).\n        \"\"\"\n        if not stat.S_ISREG(st.st_mode):\n            return False, None\n        cache_mode = self.cache_mode\n        if \"d\" in cache_mode:  # d(isabled)\n            files_cache_logger.debug(\"UNKNOWN: files cache disabled\")\n            return False, None\n        # note: r(echunk) does not need the files cache in this method, but the files cache will\n        # be updated and saved to disk to memorize the files. To preserve previous generations in\n        # the cache, this means that it also needs to get loaded from disk first.\n        if \"r\" in cache_mode:  # r(echunk)\n            files_cache_logger.debug(\"UNKNOWN: rechunking enforced\")\n            return False, None\n        entry = self.files.get(path_hash)\n        if not entry:\n            files_cache_logger.debug(\"UNKNOWN: no file metadata in cache for: %r\", hashed_path)\n            return False, None\n        # we know the file!\n        entry = self.decompress_entry(entry)\n        if \"s\" in cache_mode and entry.size != st.st_size:\n            files_cache_logger.debug(\"KNOWN-CHANGED: file size has changed: %r\", hashed_path)\n            return True, None\n        if \"i\" in cache_mode and entry.inode != st.st_ino:\n            files_cache_logger.debug(\"KNOWN-CHANGED: file inode number has changed: %r\", hashed_path)\n            return True, None\n        ctime = int_to_timestamp(safe_ns(st.st_ctime_ns))\n        if \"c\" in cache_mode and entry.ctime != ctime:\n            files_cache_logger.debug(\"KNOWN-CHANGED: file ctime has changed: %r\", hashed_path)\n            return True, None\n        mtime = int_to_timestamp(safe_ns(st.st_mtime_ns))\n        if \"m\" in cache_mode and entry.mtime != mtime:\n            files_cache_logger.debug(\"KNOWN-CHANGED: file mtime has changed: %r\", hashed_path)\n            return True, None\n        # V = any of the inode number, mtime, ctime values.\n        # we ignored V in the comparison above or it is still the same value.\n        # if it is still the same, replacing it in the tuple doesn't change it.\n        # if we ignored it, a reason for doing that is that files were moved/copied to\n        # a new disk / new fs (so a one-time change of V is expected) and we wanted\n        # to avoid everything getting chunked again. to be able to re-enable the\n        # V comparison in a future backup run (and avoid chunking everything again at\n        # that time), we need to update V in the cache with what we see in the filesystem.\n        entry = entry._replace(inode=st.st_ino, ctime=ctime, mtime=mtime, age=0)\n        self.files[path_hash] = self.compress_entry(entry)\n        chunks = [ChunkListEntry(*chunk) for chunk in entry.chunks]  # convert to list of namedtuple\n        return True, chunks\n\n    def memorize_file(self, hashed_path, path_hash, st, chunks):\n        if not stat.S_ISREG(st.st_mode):\n            return\n        # note: r(echunk) modes will update the files cache, d(isabled) mode won't\n        if \"d\" in self.cache_mode:\n            files_cache_logger.debug(\"FILES-CACHE-NOUPDATE: files cache disabled\")\n            return\n        ctime_ns = safe_ns(st.st_ctime_ns)\n        mtime_ns = safe_ns(st.st_mtime_ns)\n        entry = FileCacheEntry(\n            age=0,\n            inode=st.st_ino,\n            size=st.st_size,\n            ctime=int_to_timestamp(ctime_ns),\n            mtime=int_to_timestamp(mtime_ns),\n            chunks=chunks,\n        )\n        self.files[path_hash] = self.compress_entry(entry)\n        self._newest_cmtime = max(self._newest_cmtime or 0, ctime_ns)\n        self._newest_cmtime = max(self._newest_cmtime or 0, mtime_ns)\n        files_cache_logger.debug(\n            \"FILES-CACHE-UPDATE: put %r <- %r\", entry._replace(chunks=\"[%d entries]\" % len(entry.chunks)), hashed_path\n        )\n\n\ndef list_chunkindex_hashes(repository):\n    hashes = []\n    for info in repository.store_list(\"cache\"):\n        info = ItemInfo(*info)  # RPC does not give namedtuple\n        if info.name.startswith(\"chunks.\"):\n            hash = info.name.removeprefix(\"chunks.\")\n            hashes.append(hash)\n    hashes = sorted(hashes)\n    logger.debug(f\"cached chunk indexes: {hashes}\")\n    return hashes\n\n\ndef delete_chunkindex_cache(repository):\n    hashes = list_chunkindex_hashes(repository)\n    for hash in hashes:\n        cache_name = f\"cache/chunks.{hash}\"\n        try:\n            repository.store_delete(cache_name)\n        except StoreObjectNotFound:\n            pass\n    logger.debug(f\"cached chunk indexes deleted: {hashes}\")\n\n\nCHUNKINDEX_HASH_SEED = 3\n\n\ndef write_chunkindex_to_repo_cache(\n    repository, chunks, *, incremental=True, clear=False, force_write=False, delete_other=False, delete_these=None\n):\n    # for now, we don't want to serialize the flags or the size, just the keys (chunk IDs):\n    cleaned_value = ChunkIndexEntry(flags=ChunkIndex.F_NONE, size=0)\n    chunks_to_write = ChunkIndex()\n    # incremental==True:\n    # the borghash code has no means to only serialize the F_NEW table entries,\n    # thus we copy only the new entries to a temporary table.\n    # incremental==False:\n    # maybe copying the stuff into a new ChunkIndex is not needed here,\n    # but for simplicity, we do it anyway.\n    for key, _ in chunks.iteritems(only_new=incremental):\n        chunks_to_write[key] = cleaned_value\n    with io.BytesIO() as f:\n        chunks_to_write.write(f)\n        data = f.getvalue()\n    logger.debug(f\"caching {len(chunks_to_write)} chunks (incremental={incremental}).\")\n    chunks_to_write.clear()  # free memory of the temporary table\n    if clear:\n        # if we don't need the in-memory chunks index anymore:\n        chunks.clear()  # free memory, immediately\n    new_hash = xxh64(data, seed=CHUNKINDEX_HASH_SEED).hexdigest()\n    cached_hashes = list_chunkindex_hashes(repository)\n    if force_write or new_hash not in cached_hashes:\n        # when an updated chunks index is stored into the cache, we also store its hash as part of the name.\n        # when a client is loading the chunks index from a cache, it has to compare its xxh64\n        # hash against the hash in its name. if it is the same, the cache is valid.\n        # if it is different, the cache is either corrupted or out of date and has to be discarded.\n        # when some functionality is DELETING chunks from the repository, it has to delete\n        # all existing cache/chunks.* and maybe write a new, valid cache/chunks.<hash>,\n        # so that all clients will discard any client-local chunks index caches.\n        cache_name = f\"cache/chunks.{new_hash}\"\n        logger.debug(f\"caching chunks index as {cache_name} in repository...\")\n        repository.store_store(cache_name, data)\n        # we have successfully stored to the repository, so we can clear all F_NEW flags now:\n        chunks.clear_new()\n        # delete some not needed cached chunk indexes, but never the one we just wrote:\n        if delete_other:\n            delete_these = set(cached_hashes) - {new_hash}\n        elif delete_these:\n            delete_these = set(delete_these) - {new_hash}\n        else:\n            delete_these = set()\n        for hash in delete_these:\n            cache_name = f\"cache/chunks.{hash}\"\n            try:\n                repository.store_delete(cache_name)\n            except StoreObjectNotFound:\n                pass\n        if delete_these:\n            logger.debug(f\"cached chunk indexes deleted: {delete_these}\")\n    return new_hash\n\n\ndef read_chunkindex_from_repo_cache(repository, hash):\n    cache_name = f\"cache/chunks.{hash}\"\n    logger.debug(f\"trying to load {cache_name} from the repo...\")\n    try:\n        chunks_data = repository.store_load(cache_name)\n    except StoreObjectNotFound:\n        logger.debug(f\"{cache_name} not found in the repository.\")\n    else:\n        if xxh64(chunks_data, seed=CHUNKINDEX_HASH_SEED).digest() == hex_to_bin(hash):\n            logger.debug(f\"{cache_name} is valid.\")\n            with io.BytesIO(chunks_data) as f:\n                chunks = ChunkIndex.read(f)\n            return chunks\n        else:\n            logger.debug(f\"{cache_name} is invalid.\")\n\n\ndef build_chunkindex_from_repo(repository, *, disable_caches=False, cache_immediately=False):\n    # first, try to build a fresh, mostly complete chunk index from centrally cached chunk indexes:\n    if not disable_caches:\n        hashes = list_chunkindex_hashes(repository)\n        if hashes:  # we have at least one cached chunk index!\n            merged = 0\n            chunks = ChunkIndex()  # we'll merge all we find into this\n            for hash in hashes:\n                chunks_to_merge = read_chunkindex_from_repo_cache(repository, hash)\n                if chunks_to_merge is not None:\n                    logger.debug(f\"cached chunk index {hash} gets merged...\")\n                    for k, v in chunks_to_merge.items():\n                        chunks[k] = v\n                    merged += 1\n                    chunks_to_merge.clear()\n            if merged > 0:\n                if merged > 1 and cache_immediately:\n                    # immediately update cache/chunks, so we don't have to merge these again:\n                    write_chunkindex_to_repo_cache(\n                        repository, chunks, clear=False, force_write=True, delete_these=hashes\n                    )\n                else:\n                    chunks.clear_new()\n                return chunks\n    # if we didn't get anything from the cache, compute the ChunkIndex the slow way:\n    logger.debug(\"querying the chunk IDs list from the repo...\")\n    chunks = ChunkIndex()\n    t0 = perf_counter()\n    num_chunks = 0\n    # The repo says it has these chunks, so we assume they are referenced/used chunks.\n    # We do not know the plaintext size (!= stored_size), thus we set size = 0.\n    init_entry = ChunkIndexEntry(flags=ChunkIndex.F_USED, size=0)\n    for id, stored_size in repo_lister(repository, limit=LIST_SCAN_LIMIT):\n        num_chunks += 1\n        chunks[id] = init_entry\n    # Cache does not contain the manifest.\n    if not isinstance(repository, (Repository, RemoteRepository)):\n        del chunks[Manifest.MANIFEST_ID]\n    duration = perf_counter() - t0 or 0.001\n    # Chunk IDs in a list are encoded in 34 bytes: 1 byte msgpack header, 1 byte length, 32 ID bytes.\n    # Protocol overhead is neglected in this calculation.\n    speed = format_file_size(num_chunks * 34 / duration)\n    logger.debug(f\"queried {num_chunks} chunk IDs in {duration} s, ~{speed}/s\")\n    if cache_immediately:\n        # immediately update cache/chunks, so we only rarely have to do it the slow way:\n        write_chunkindex_to_repo_cache(repository, chunks, clear=False, force_write=True, delete_other=True)\n    return chunks\n\n\nclass ChunksMixin:\n    \"\"\"\n    Chunks index related code for misc. Cache implementations.\n    \"\"\"\n\n    def __init__(self):\n        self._chunks = None\n        self.last_refresh_dt = datetime.now(timezone.utc)\n        self.refresh_td = timedelta(seconds=60)\n        self.chunks_cache_last_write = datetime.now(timezone.utc)\n        self.chunks_cache_write_td = timedelta(seconds=600)\n\n    @property\n    def chunks(self):\n        if self._chunks is None:\n            self._chunks = build_chunkindex_from_repo(self.repository, cache_immediately=True)\n        return self._chunks\n\n    def seen_chunk(self, id, size=None):\n        entry = self.chunks.get(id)\n        entry_exists = entry is not None\n        if entry_exists and size is not None:\n            if entry.size == 0:\n                # AdHocWithFilesCache:\n                # Here *size* is used to update the chunk's size information, which will be zero for existing chunks.\n                self.chunks[id] = entry._replace(size=size)\n            else:\n                # in case we already have a size information in the entry, check consistency:\n                assert size == entry.size\n        return entry_exists\n\n    def reuse_chunk(self, id, size, stats):\n        assert isinstance(size, int) and size > 0\n        stats.update(size, False)\n        return ChunkListEntry(id, size)\n\n    def add_chunk(\n        self,\n        id,\n        meta,\n        data,\n        *,\n        stats,\n        wait=True,\n        compress=True,\n        size=None,\n        ctype=None,\n        clevel=None,\n        ro_type=ROBJ_FILE_STREAM,\n    ):\n        assert ro_type is not None\n        if size is None:\n            if compress:\n                size = len(data)  # data is still uncompressed\n            else:\n                raise ValueError(\"when giving compressed data for a chunk, the uncompressed size must be given also\")\n        now = datetime.now(timezone.utc)\n        self._maybe_write_chunks_cache(now)\n        exists = self.seen_chunk(id, size)\n        if exists:\n            # if borg create is processing lots of unchanged files (no content and not metadata changes),\n            # there could be a long time without any repository operations and the repo lock would get stale.\n            self.refresh_lock(now)\n            return self.reuse_chunk(id, size, stats)\n        cdata = self.repo_objs.format(\n            id, meta, data, compress=compress, size=size, ctype=ctype, clevel=clevel, ro_type=ro_type\n        )\n        self.repository.put(id, cdata, wait=wait)\n        self.last_refresh_dt = now  # .put also refreshed the lock\n        self.chunks.add(id, size)\n        stats.update(size, not exists)\n        return ChunkListEntry(id, size)\n\n    def _maybe_write_chunks_cache(self, now, force=False, clear=False):\n        if force or now > self.chunks_cache_last_write + self.chunks_cache_write_td:\n            if self._chunks is not None:\n                write_chunkindex_to_repo_cache(self.repository, self._chunks, clear=clear)\n            self.chunks_cache_last_write = now\n\n    def refresh_lock(self, now):\n        if now > self.last_refresh_dt + self.refresh_td:\n            # the repository lock needs to get refreshed regularly, or it will be killed as stale.\n            # refreshing the lock is not part of the repository API, so we do it indirectly via repository.info.\n            self.repository.info()\n            self.last_refresh_dt = now\n\n\nclass AdHocWithFilesCache(FilesCacheMixin, ChunksMixin):\n    \"\"\"\n    An ad-hoc chunks and files cache.\n\n    Chunks: it does not maintain accurate reference count.\n    Chunks that were not added during the current lifetime won't have correct size set (0 bytes)\n    and will have an infinite reference count (MAX_VALUE).\n\n    Files: if a previous_archive_id is given, ad-hoc build a in-memory files cache from that archive.\n    \"\"\"\n\n    def __init__(\n        self,\n        manifest,\n        path=None,\n        warn_if_unencrypted=True,\n        progress=False,\n        cache_mode=FILES_CACHE_MODE_DISABLED,\n        iec=False,\n        archive_name=None,\n        start_backup=None,\n    ):\n        \"\"\"\n        :param warn_if_unencrypted: print warning if accessing unknown unencrypted repository\n        :param cache_mode: what shall be compared in the file stat infos vs. cached stat infos comparison\n        \"\"\"\n        FilesCacheMixin.__init__(self, cache_mode, archive_name, start_backup)\n        ChunksMixin.__init__(self)\n        assert isinstance(manifest, Manifest)\n        self.manifest = manifest\n        self.repository = manifest.repository\n        self.key = manifest.key\n        self.repo_objs = manifest.repo_objs\n        self.progress = progress\n\n        self.path = cache_dir(self.repository, path)\n        self.security_manager = SecurityManager(self.repository)\n        self.cache_config = CacheConfig(self.repository, self.path)\n\n        # Warn user before sending data to a never seen before unencrypted repository\n        if not self.path.exists():\n            self.security_manager.assert_access_unknown(warn_if_unencrypted, manifest, self.key)\n            self.create()\n\n        self.open()\n        try:\n            self.security_manager.assert_secure(manifest, self.key)\n\n            if not self.check_cache_compatibility():\n                self.wipe_cache()\n\n            self.update_compatibility()\n        except:  # noqa\n            self.close()\n            raise\n\n    def __enter__(self):\n        self._chunks = None\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n        self._chunks = None\n\n    def create(self):\n        \"\"\"Create a new empty cache at `self.path`\"\"\"\n        self.path.mkdir(parents=True, exist_ok=True)\n        with open(self.path / \"README\", \"w\") as fd:\n            fd.write(CACHE_README)\n        self.cache_config.create()\n\n    def open(self):\n        if not self.path.is_dir():\n            raise Exception(\"%s Does not look like a Borg cache\" % self.path)\n        self.cache_config.open()\n        self.cache_config.load()\n\n    def close(self):\n        self.security_manager.save(self.manifest, self.key)\n        pi = ProgressIndicatorMessage(msgid=\"cache.close\")\n        if self._files is not None:\n            pi.output(\"Saving files cache\")\n            integrity_data = self._write_files_cache(self._files)\n            self.cache_config.integrity[self.files_cache_name()] = integrity_data\n        if self._chunks is not None:\n            for key, value in sorted(self._chunks.stats.items()):\n                logger.debug(f\"Chunks index stats: {key}: {value}\")\n            pi.output(\"Saving chunks cache\")\n            # note: cache/chunks.* in repo has a different integrity mechanism\n            now = datetime.now(timezone.utc)\n            self._maybe_write_chunks_cache(now, force=True, clear=True)\n            self._chunks = None  # nothing there (cleared!)\n        pi.output(\"Saving cache config\")\n        self.cache_config.save(self.manifest)\n        self.cache_config.close()\n        pi.finish()\n        self.cache_config = None\n\n    def check_cache_compatibility(self):\n        my_features = Manifest.SUPPORTED_REPO_FEATURES\n        if self.cache_config.ignored_features & my_features:\n            # The cache might not contain references of chunks that need a feature that is mandatory for some operation\n            # and which this version supports. To avoid corruption while executing that operation force rebuild.\n            return False\n        if not self.cache_config.mandatory_features <= my_features:\n            # The cache was build with consideration to at least one feature that this version does not understand.\n            # This client might misinterpret the cache. Thus force a rebuild.\n            return False\n        return True\n\n    def wipe_cache(self):\n        logger.warning(\"Discarding incompatible cache and forcing a cache rebuild\")\n        self._chunks = ChunkIndex()\n        self.cache_config.manifest_id = \"\"\n        self.cache_config._config.set(\"cache\", \"manifest\", \"\")\n\n        self.cache_config.ignored_features = set()\n        self.cache_config.mandatory_features = set()\n\n    def update_compatibility(self):\n        operation_to_features_map = self.manifest.get_all_mandatory_features()\n        my_features = Manifest.SUPPORTED_REPO_FEATURES\n        repo_features = set()\n        for operation, features in operation_to_features_map.items():\n            repo_features.update(features)\n\n        self.cache_config.ignored_features.update(repo_features - my_features)\n        self.cache_config.mandatory_features.update(repo_features & my_features)\n"
  },
  {
    "path": "src/borg/checksums.pyi",
    "content": "def crc32(data: bytes, value: int = 0) -> int: ...\n"
  },
  {
    "path": "src/borg/checksums.pyx",
    "content": "import zlib\n\n\n# Borg 2.0 repositories do not compute CRC32 over large amounts of data,\n# so speed does not matter much anymore, and we can just use zlib.crc32.\ncrc32 = zlib.crc32\n"
  },
  {
    "path": "src/borg/chunkers/__init__.py",
    "content": "from .buzhash import Chunker\nfrom .buzhash64 import ChunkerBuzHash64\nfrom .failing import ChunkerFailing\nfrom .fixed import ChunkerFixed\nfrom .reader import *  # noqa\n\n\ndef get_chunker(algo, *params, **kw):\n    key = kw.get(\"key\", None)\n    sparse = kw.get(\"sparse\", False)\n    # key.chunk_seed only has 32 bits\n    seed = key.chunk_seed if key is not None else 0\n    # for buzhash64, we want a much longer key, so we derive it from the id key\n    bh64_key = (\n        key.derive_key(salt=b\"\", domain=b\"buzhash64\", size=32, from_id_key=True) if key is not None else b\"\\0\" * 32\n    )\n    if algo == \"buzhash\":\n        return Chunker(seed, *params, sparse=sparse)\n    if algo == \"buzhash64\":\n        return ChunkerBuzHash64(bh64_key, *params, sparse=sparse)\n    if algo == \"fixed\":\n        return ChunkerFixed(*params, sparse=sparse)\n    if algo == \"fail\":\n        return ChunkerFailing(*params)\n    raise TypeError(\"unsupported chunker algo %r\" % algo)\n"
  },
  {
    "path": "src/borg/chunkers/buzhash.pyi",
    "content": "from typing import List, Iterator, BinaryIO\n\nfrom .reader import fmap_entry\n\ndef buzhash(data: bytes, seed: int) -> int: ...\ndef buzhash_update(sum: int, remove: int, add: int, len: int, seed: int) -> int: ...\n\nclass Chunker:\n    def __init__(\n        self,\n        seed: int,\n        chunk_min_exp: int,\n        chunk_max_exp: int,\n        hash_mask_bits: int,\n        hash_window_size: int,\n        sparse: bool = False,\n    ) -> None: ...\n    def chunkify(self, fd: BinaryIO = None, fh: int = -1, fmap: List[fmap_entry] = None) -> Iterator: ...\n"
  },
  {
    "path": "src/borg/chunkers/buzhash.pyx",
    "content": "# cython: language_level=3\n\n\n\nimport cython\nimport time\nfrom cpython.bytes cimport PyBytes_AsString\nfrom libc.stdint cimport uint8_t, uint32_t\nfrom libc.stdlib cimport malloc, free\nfrom libc.string cimport memcpy, memmove, memset\n\nfrom ..constants import CH_DATA, CH_ALLOC, CH_HOLE, zeros\nfrom .reader import FileReader, Chunk\n\n# Cyclic polynomial / buzhash\n#\n# https://en.wikipedia.org/wiki/Rolling_hash\n#\n# http://www.serve.net/buz/Notes.1st.year/HTML/C6/rand.012.html (by \"BUZ\", the inventor)\n#\n# http://www.dcs.gla.ac.uk/~hamer/cakes-talk.pdf (see buzhash slide)\n#\n# Some properties of buzhash / of this implementation:\n#\n# (1) the hash is designed for inputs <= 32 bytes, but the chunker uses it on a 4095 byte window;\n#     any repeating bytes at distance 32 within those 4095 bytes can cause cancellation within\n#     the hash function, e.g. in \"X <any 31 bytes> X\", the last X would cancel out the influence\n#     of the first X on the hash value.\n#\n# (2) the hash table is supposed to have (according to the BUZ) exactly a 50% distribution of\n#     0/1 bit values per position, but the hard coded table below doesn't fit that property.\n#\n# (3) if you would use a window size divisible by 64, the seed would cancel itself out completely.\n#     this is why we use a window size of 4095 bytes.\n#\n# Another quirk is that, even with the 4095 byte window, XORing the entire table by a constant\n# is equivalent to XORing the hash output with a different constant. but since the seed is stored\n# encrypted, i think it still serves its purpose.\n\ncdef uint32_t table_base[256]\ntable_base = [\n    0xe7f831ec, 0xf4026465, 0xafb50cae, 0x6d553c7a, 0xd639efe3, 0x19a7b895, 0x9aba5b21, 0x5417d6d4,\n    0x35fd2b84, 0xd1f6a159, 0x3f8e323f, 0xb419551c, 0xf444cebf, 0x21dc3b80, 0xde8d1e36, 0x84a32436,\n    0xbeb35a9d, 0xa36f24aa, 0xa4e60186, 0x98d18ffe, 0x3f042f9e, 0xdb228bcd, 0x096474b7, 0x5c20c2f7,\n    0xf9eec872, 0xe8625275, 0xb9d38f80, 0xd48eb716, 0x22a950b4, 0x3cbaaeaa, 0xc37cddd3, 0x8fea6f6a,\n    0x1d55d526, 0x7fd6d3b3, 0xdaa072ee, 0x4345ac40, 0xa077c642, 0x8f2bd45b, 0x28509110, 0x55557613,\n    0xffc17311, 0xd961ffef, 0xe532c287, 0xaab95937, 0x46d38365, 0xb065c703, 0xf2d91d0f, 0x92cd4bb0,\n    0x4007c712, 0xf35509dd, 0x505b2f69, 0x557ead81, 0x310f4563, 0xbddc5be8, 0x9760f38c, 0x701e0205,\n    0x00157244, 0x14912826, 0xdc4ca32b, 0x67b196de, 0x5db292e8, 0x8c1b406b, 0x01f34075, 0xfa2520f7,\n    0x73bc37ab, 0x1e18bc30, 0xfe2c6cb3, 0x20c522d0, 0x5639e3db, 0x942bda35, 0x899af9d1, 0xced44035,\n    0x98cc025b, 0x255f5771, 0x70fefa24, 0xe928fa4d, 0x2c030405, 0xb9325590, 0x20cb63bd, 0xa166305d,\n    0x80e52c0a, 0xa8fafe2f, 0x1ad13f7d, 0xcfaf3685, 0x6c83a199, 0x7d26718a, 0xde5dfcd9, 0x79cf7355,\n    0x8979d7fb, 0xebf8c55e, 0xebe408e4, 0xcd2affba, 0xe483be6e, 0xe239d6de, 0x5dc1e9e0, 0x0473931f,\n    0x851b097c, 0xac5db249, 0x09c0f9f2, 0xd8d2f134, 0xe6f38e41, 0xb1c71bf1, 0x52b6e4db, 0x07224424,\n    0x6cf73e85, 0x4f25d89c, 0x782a7d74, 0x10a68dcd, 0x3a868189, 0xd570d2dc, 0x69630745, 0x9542ed86,\n    0x331cd6b2, 0xa84b5b28, 0x07879c9d, 0x38372f64, 0x7185db11, 0x25ba7c83, 0x01061523, 0xe6792f9f,\n    0xe5df07d1, 0x4321b47f, 0x7d2469d8, 0x1a3a4f90, 0x48be29a3, 0x669071af, 0x8ec8dd31, 0x0810bfbf,\n    0x813a06b4, 0x68538345, 0x65865ddc, 0x43a71b8e, 0x78619a56, 0x5a34451d, 0x5bdaa3ed, 0x71edc7e9,\n    0x17ac9a20, 0x78d10bfa, 0x6c1e7f35, 0xd51839d9, 0x240cbc51, 0x33513cc1, 0xd2b4f795, 0xccaa8186,\n    0x0babe682, 0xa33cf164, 0x18c643ea, 0xc1ca105f, 0x9959147a, 0x6d3d94de, 0x0b654fbe, 0xed902ca0,\n    0x7d835cb5, 0x99ba1509, 0x6445c922, 0x495e76c2, 0xf07194bc, 0xa1631d7e, 0x677076a5, 0x89fffe35,\n    0x1a49bcf3, 0x8e6c948a, 0x0144c917, 0x8d93aea1, 0x16f87ddf, 0xc8f25d49, 0x1fb11297, 0x27e750cd,\n    0x2f422da1, 0xdee89a77, 0x1534c643, 0x457b7b8b, 0xaf172f7a, 0x6b9b09d6, 0x33573f7f, 0xf14e15c4,\n    0x526467d5, 0xaf488241, 0x87c3ee0d, 0x33be490c, 0x95aa6e52, 0x43ec242e, 0xd77de99b, 0xd018334f,\n    0x5b78d407, 0x498eb66b, 0xb1279fa8, 0xb38b0ea6, 0x90718376, 0xe325dee2, 0x8e2f2cba, 0xcaa5bdec,\n    0x9d652c56, 0xad68f5cb, 0xa77591af, 0x88e37ee8, 0xf8faa221, 0xfcbbbe47, 0x4f407786, 0xaf393889,\n    0xf444a1d9, 0x15ae1a2f, 0x40aa7097, 0x6f9486ac, 0x29d232a3, 0xe47609e9, 0xe8b631ff, 0xba8565f4,\n    0x11288749, 0x46c9a838, 0xeb1b7cd8, 0xf516bbb1, 0xfb74fda0, 0x010996e6, 0x4c994653, 0x1d889512,\n    0x53dcd9a3, 0xdd074697, 0x1e78e17c, 0x637c98bf, 0x930bb219, 0xcf7f75b0, 0xcb9355fb, 0x9e623009,\n    0xe466d82c, 0x28f968d3, 0xfeb385d9, 0x238e026c, 0xb8ed0560, 0x0c6a027a, 0x3d6fec4b, 0xbb4b2ec2,\n    0xe715031c, 0xeded011d, 0xcdc4d3b9, 0xc456fc96, 0xdd0eea20, 0xb3df8ec9, 0x12351993, 0xd9cbb01c,\n    0x603147a2, 0xcf37d17d, 0xf7fcd9dc, 0xd8556fa3, 0x104c8131, 0x13152774, 0xb4715811, 0x6a72c2c9,\n    0xc5ae37bb, 0xa76ce12a, 0x8150d8f3, 0x2ec29218, 0xa35f0984, 0x48c0647e, 0x0b5ff98c, 0x71893f7b\n]\n\n# This seems to be the most reliable way to inline this code, using a C preprocessor macro:\ncdef extern from *:\n   \"\"\"\n   #define BARREL_SHIFT(v, shift) (((v) << (shift)) | ((v) >> (((32 - (shift)) & 0x1f))))\n   \"\"\"\n   uint32_t BARREL_SHIFT(uint32_t v, uint32_t shift)\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\ncdef uint32_t* buzhash_init_table(uint32_t seed):\n    \"\"\"Initialize the buzhash table with the given seed.\"\"\"\n    cdef int i\n    cdef uint32_t* table = <uint32_t*>malloc(1024)  # 256 * sizeof(uint32_t)\n    for i in range(256):\n        table[i] = table_base[i] ^ seed\n    return table\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\n@cython.cdivision(True)  # Use C division/modulo semantics for integer division.\ncdef uint32_t _buzhash(const unsigned char* data, size_t len, const uint32_t* h):\n    \"\"\"Calculate the buzhash of the given data.\"\"\"\n    cdef uint32_t i\n    cdef uint32_t sum = 0, imod\n    for i in range(len - 1, 0, -1):\n        imod = i & 0x1f\n        sum ^= BARREL_SHIFT(h[data[0]], imod)\n        data += 1\n    return sum ^ h[data[0]]\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\n@cython.cdivision(True)  # Use C division/modulo semantics for integer division.\ncdef uint32_t _buzhash_update(uint32_t sum, unsigned char remove, unsigned char add, size_t len, const uint32_t* h):\n    \"\"\"Update the buzhash with a new byte.\"\"\"\n    cdef uint32_t lenmod = len & 0x1f\n    return BARREL_SHIFT(sum, 1) ^ BARREL_SHIFT(h[remove], lenmod) ^ h[add]\n\n\ncdef class Chunker:\n    \"\"\"\n    Content-Defined Chunker, variable chunk sizes.\n\n    This chunker makes quite some effort to cut mostly chunks of the same-content, even if\n    the content moves to a different offset inside the file. It uses the buzhash\n    rolling-hash algorithm to identify the chunk cutting places by looking at the\n    content inside the moving window and computing the rolling hash value over the\n    window contents. If the last n bits of the rolling hash are 0, a chunk is cut.\n    Additionally it obeys some more criteria, like a minimum and maximum chunk size.\n    It also uses a per-repo random seed to avoid some chunk length fingerprinting attacks.\n    \"\"\"\n    cdef uint32_t chunk_mask\n    cdef uint32_t* table\n    cdef uint8_t* data\n    cdef object _fd  # Python object for file descriptor\n    cdef int fh\n    cdef int done, eof\n    cdef size_t min_size, buf_size, window_size, remaining, position, last\n    cdef long long bytes_read, bytes_yielded  # off_t in C, using long long for compatibility\n    cdef readonly float chunking_time\n    cdef object file_reader  # FileReader instance\n    cdef size_t reader_block_size\n    cdef bint sparse\n\n    def __cinit__(self, int seed, int chunk_min_exp, int chunk_max_exp, int hash_mask_bits, int hash_window_size, bint sparse=False):\n        min_size = 1 << chunk_min_exp\n        max_size = 1 << chunk_max_exp\n        assert max_size <= len(zeros)\n        # see chunker_process, first while loop condition, first term must be able to get True:\n        assert hash_window_size + min_size + 1 <= max_size, \"too small max_size\"\n\n        self.window_size = hash_window_size\n        self.chunk_mask = (1 << hash_mask_bits) - 1\n        self.min_size = min_size\n        self.table = buzhash_init_table(seed & 0xffffffff)\n        self.buf_size = max_size\n        self.data = <uint8_t*>malloc(self.buf_size)\n        self.fh = -1\n        self.done = 0\n        self.eof = 0\n        self.remaining = 0\n        self.position = 0\n        self.last = 0\n        self.bytes_read = 0\n        self.bytes_yielded = 0\n        self._fd = None\n        self.chunking_time = 0.0\n        self.reader_block_size = 1024 * 1024\n        self.sparse = sparse\n\n    def __dealloc__(self):\n        \"\"\"Free the chunker's resources.\"\"\"\n        if self.table != NULL:\n            free(self.table)\n            self.table = NULL\n        if self.data != NULL:\n            free(self.data)\n            self.data = NULL\n\n    cdef int fill(self) except 0:\n        \"\"\"Fill the chunker's buffer with more data.\"\"\"\n        cdef ssize_t n\n        cdef object chunk\n\n        # Move remaining data to the beginning of the buffer\n        memmove(self.data, self.data + self.last, self.position + self.remaining - self.last)\n        self.position -= self.last\n        self.last = 0\n        n = self.buf_size - self.position - self.remaining\n\n        if self.eof or n == 0:\n            return 1\n\n        # Use FileReader to read data\n        chunk = self.file_reader.read(n)\n        n = chunk.meta[\"size\"]\n\n        if n > 0:\n            # Only copy data if it's not a hole\n            if chunk.meta[\"allocation\"] == CH_DATA:\n                # Copy data from chunk to our buffer\n                memcpy(self.data + self.position + self.remaining, <const unsigned char*>PyBytes_AsString(chunk.data), n)\n            else:\n                # For holes, fill with zeros using memset\n                memset(self.data + self.position + self.remaining, 0, n)\n\n            self.remaining += n\n            self.bytes_read += n\n        else:\n            self.eof = 1\n\n        return 1\n\n    cdef object process(self) except *:\n        \"\"\"Process the chunker's buffer and return the next chunk.\"\"\"\n        cdef uint32_t sum, chunk_mask = self.chunk_mask\n        cdef size_t n, old_last, min_size = self.min_size, window_size = self.window_size\n        cdef uint8_t* p\n        cdef uint8_t* stop_at\n        cdef size_t did_bytes\n\n        if self.done:\n            if self.bytes_read == self.bytes_yielded:\n                raise StopIteration\n            else:\n                raise Exception(\"chunkifier byte count mismatch\")\n\n        while self.remaining < min_size + window_size + 1 and not self.eof:  # see assert in Chunker init\n            if not self.fill():\n                return None\n\n        # Here we either are at eof...\n        if self.eof:\n            self.done = 1\n            if self.remaining:\n                self.bytes_yielded += self.remaining\n                # Return a memory view of the remaining data\n                return memoryview((self.data + self.position)[:self.remaining])\n            else:\n                if self.bytes_read == self.bytes_yielded:\n                    raise StopIteration\n                else:\n                    raise Exception(\"chunkifier byte count mismatch\")\n\n        # ... or we have at least min_size + window_size + 1 bytes remaining.\n        # We do not want to \"cut\" a chunk smaller than min_size and the hash\n        # window starts at the potential cutting place.\n        self.position += min_size\n        self.remaining -= min_size\n        sum = _buzhash(self.data + self.position, window_size, self.table)\n\n        while self.remaining > window_size and (sum & chunk_mask) and not (self.eof and self.remaining <= window_size):\n            p = self.data + self.position\n            stop_at = p + self.remaining - window_size\n\n            while p < stop_at and (sum & chunk_mask):\n                sum = _buzhash_update(sum, p[0], p[window_size], window_size, self.table)\n                p += 1\n\n            did_bytes = p - (self.data + self.position)\n            self.position += did_bytes\n            self.remaining -= did_bytes\n\n            if self.remaining <= window_size:\n                if not self.fill():\n                    return None\n\n        if self.remaining <= window_size:\n            self.position += self.remaining\n            self.remaining = 0\n\n        old_last = self.last\n        self.last = self.position\n        n = self.last - old_last\n        self.bytes_yielded += n\n\n        # Return a memory view of the chunk\n        return memoryview((self.data + old_last)[:n])\n\n    def chunkify(self, fd, fh=-1, fmap=None):\n        \"\"\"\n        Cut a file into chunks.\n\n        :param fd: Python file object\n        :param fh: OS-level file handle (if available),\n                   defaults to -1 which means not to use OS-level fd.\n        :param fmap: a file map, same format as generated by sparsemap\n        \"\"\"\n        self._fd = fd\n        self.fh = fh\n        self.file_reader = FileReader(fd=fd, fh=fh, read_size=self.reader_block_size, sparse=self.sparse, fmap=fmap)\n        self.done = 0\n        self.remaining = 0\n        self.bytes_read = 0\n        self.bytes_yielded = 0\n        self.position = 0\n        self.last = 0\n        self.eof = 0\n        return self\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        started_chunking = time.monotonic()\n        data = self.process()\n        got = len(data)\n        # we do not have SEEK_DATA/SEEK_HOLE support in chunker_process C code,\n        # but we can just check if data was all-zero (and either came from a hole\n        # or from stored zeros - we can not detect that here).\n        if zeros.startswith(data):\n            data = None\n            allocation = CH_ALLOC\n        else:\n            allocation = CH_DATA\n        self.chunking_time += time.monotonic() - started_chunking\n        return Chunk(data, size=got, allocation=allocation)\n\n\ndef buzhash(data, unsigned long seed):\n    cdef uint32_t *table\n    cdef uint32_t sum\n    table = buzhash_init_table(seed & 0xffffffff)\n    sum = _buzhash(<const unsigned char *> data, len(data), table)\n    free(table)\n    return sum\n\n\ndef buzhash_update(uint32_t sum, unsigned char remove, unsigned char add, size_t len, unsigned long seed):\n    cdef uint32_t *table\n    table = buzhash_init_table(seed & 0xffffffff)\n    sum = _buzhash_update(sum, remove, add, len, table)\n    free(table)\n    return sum\n"
  },
  {
    "path": "src/borg/chunkers/buzhash64.pyi",
    "content": "from typing import List, Iterator, BinaryIO\n\nfrom .reader import fmap_entry\n\ndef buzhash64(data: bytes, key: bytes) -> int: ...\ndef buzhash64_update(sum: int, remove: int, add: int, len: int, key: bytes) -> int: ...\ndef buzhash64_get_table(key: bytes) -> List[int]: ...\n\nclass ChunkerBuzHash64:\n    def __init__(\n        self,\n        key: bytes,\n        chunk_min_exp: int,\n        chunk_max_exp: int,\n        hash_mask_bits: int,\n        hash_window_size: int,\n        sparse: bool = False,\n    ) -> None: ...\n    def chunkify(self, fd: BinaryIO = None, fh: int = -1, fmap: List[fmap_entry] = None) -> Iterator: ...\n"
  },
  {
    "path": "src/borg/chunkers/buzhash64.pyx",
    "content": "# cython: language_level=3\n\n\n\nimport cython\nimport time\n\nfrom cpython.bytes cimport PyBytes_AsString\nfrom libc.stdint cimport uint8_t, uint64_t\nfrom libc.stdlib cimport malloc, free\nfrom libc.string cimport memcpy, memmove, memset\n\nfrom ..crypto.low_level import CSPRNG\n\nfrom ..constants import CH_DATA, CH_ALLOC, CH_HOLE, zeros\nfrom .reader import FileReader, Chunk\n\n# Cyclic polynomial / buzhash\n#\n# https://en.wikipedia.org/wiki/Rolling_hash\n#\n# http://www.serve.net/buz/Notes.1st.year/HTML/C6/rand.012.html (by \"BUZ\", the inventor)\n#\n# http://www.dcs.gla.ac.uk/~hamer/cakes-talk.pdf (see buzhash slide)\n#\n# Some properties of buzhash / of this implementation:\n#\n# (1) the hash is designed for inputs <= 64 bytes, but the chunker uses it on a 4095 byte window;\n#     any repeating bytes at distance 64 within those 4095 bytes can cause cancellation within\n#     the hash function, e.g. in \"X <any 63 bytes> X\", the last X would cancel out the influence\n#     of the first X on the hash value.\n\n# This seems to be the most reliable way to inline this code, using a C preprocessor macro:\ncdef extern from *:\n   \"\"\"\n   #define BARREL_SHIFT64(v, shift) (((v) << (shift)) | ((v) >> (((64 - (shift)) & 0x3f))))\n   \"\"\"\n   uint64_t BARREL_SHIFT64(uint64_t v, uint64_t shift)\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\ncdef uint64_t* buzhash64_init_table(bytes key):\n    \"\"\"\n    Generate a balanced pseudo-random table deterministically from a 256-bit key.\n    Balanced means that for each bit position 0..63, exactly 50% of the table values have the bit set to 1.\n    \"\"\"\n    # Create deterministic random number generator\n    rng = CSPRNG(key)\n\n    cdef int i, j, bit_pos\n    cdef uint64_t* table = <uint64_t*>malloc(2048)  # 256 * sizeof(uint64_t)\n\n    # Initialize all values to 0\n    for i in range(256):\n        table[i] = 0\n\n    # For each bit position, deterministically assign exactly 128 positions to have that bit set\n    for bit_pos in range(64):\n        # Create a list of indices and shuffle deterministically\n        indices = list(range(256))\n        rng.shuffle(indices)\n\n        # Set the bit at bit_pos for the first 128 shuffled indices\n        for i in range(128):\n            j = indices[i]\n            table[j] |= (1ULL << bit_pos)\n\n    return table\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\n@cython.cdivision(True)  # Use C division/modulo semantics for integer division.\ncdef uint64_t _buzhash64(const unsigned char* data, size_t len, const uint64_t* h):\n    \"\"\"Calculate the buzhash of the given data.\"\"\"\n    cdef uint64_t i\n    cdef uint64_t sum = 0, imod\n    for i in range(len - 1, 0, -1):\n        imod = i & 0x3f\n        sum ^= BARREL_SHIFT64(h[data[0]], imod)\n        data += 1\n    return sum ^ h[data[0]]\n\n\n@cython.boundscheck(False)  # Deactivate bounds checking\n@cython.wraparound(False)  # Deactivate negative indexing.\n@cython.cdivision(True)  # Use C division/modulo semantics for integer division.\ncdef uint64_t _buzhash64_update(uint64_t sum, unsigned char remove, unsigned char add, size_t len, const uint64_t* h):\n    \"\"\"Update the buzhash with a new byte.\"\"\"\n    cdef uint64_t lenmod = len & 0x3f\n    return BARREL_SHIFT64(sum, 1) ^ BARREL_SHIFT64(h[remove], lenmod) ^ h[add]\n\n\ncdef class ChunkerBuzHash64:\n    \"\"\"\n    Content-Defined Chunker, variable chunk sizes.\n\n    This chunker makes quite some effort to cut mostly chunks of the same-content, even if\n    the content moves to a different offset inside the file. It uses the buzhash\n    rolling-hash algorithm to identify the chunk cutting places by looking at the\n    content inside the moving window and computing the rolling hash value over the\n    window contents. If the last n bits of the rolling hash are 0, a chunk is cut.\n    Additionally it obeys some more criteria, like a minimum and maximum chunk size.\n    It also uses a per-repo random seed to avoid some chunk length fingerprinting attacks.\n    \"\"\"\n    cdef uint64_t chunk_mask\n    cdef uint64_t* table\n    cdef uint8_t* data\n    cdef object _fd  # Python object for file descriptor\n    cdef int fh\n    cdef int done, eof\n    cdef size_t min_size, buf_size, window_size, remaining, position, last\n    cdef long long bytes_read, bytes_yielded  # off_t in C, using long long for compatibility\n    cdef readonly float chunking_time\n    cdef object file_reader  # FileReader instance\n    cdef size_t reader_block_size\n    cdef bint sparse\n\n    def __cinit__(self, bytes key, int chunk_min_exp, int chunk_max_exp, int hash_mask_bits, int hash_window_size, bint sparse=False):\n        min_size = 1 << chunk_min_exp\n        max_size = 1 << chunk_max_exp\n        assert max_size <= len(zeros)\n        # see chunker_process, first while loop condition, first term must be able to get True:\n        assert hash_window_size + min_size + 1 <= max_size, \"too small max_size\"\n\n        self.window_size = hash_window_size\n        self.chunk_mask = (1 << hash_mask_bits) - 1\n        self.min_size = min_size\n        self.table = buzhash64_init_table(key)\n        self.buf_size = max_size\n        self.data = <uint8_t*>malloc(self.buf_size)\n        self.fh = -1\n        self.done = 0\n        self.eof = 0\n        self.remaining = 0\n        self.position = 0\n        self.last = 0\n        self.bytes_read = 0\n        self.bytes_yielded = 0\n        self._fd = None\n        self.chunking_time = 0.0\n        self.reader_block_size = 1024 * 1024\n        self.sparse = sparse\n\n    def __dealloc__(self):\n        \"\"\"Free the chunker's resources.\"\"\"\n        if self.table != NULL:\n            free(self.table)\n            self.table = NULL\n        if self.data != NULL:\n            free(self.data)\n            self.data = NULL\n\n    cdef int fill(self) except 0:\n        \"\"\"Fill the chunker's buffer with more data.\"\"\"\n        cdef ssize_t n\n        cdef object chunk\n\n        # Move remaining data to the beginning of the buffer\n        memmove(self.data, self.data + self.last, self.position + self.remaining - self.last)\n        self.position -= self.last\n        self.last = 0\n        n = self.buf_size - self.position - self.remaining\n\n        if self.eof or n == 0:\n            return 1\n\n        # Use FileReader to read data\n        chunk = self.file_reader.read(n)\n        n = chunk.meta[\"size\"]\n\n        if n > 0:\n            # Only copy data if it's not a hole\n            if chunk.meta[\"allocation\"] == CH_DATA:\n                # Copy data from chunk to our buffer\n                memcpy(self.data + self.position + self.remaining, <const unsigned char*>PyBytes_AsString(chunk.data), n)\n            else:\n                # For holes, fill with zeros using memset\n                memset(self.data + self.position + self.remaining, 0, n)\n\n            self.remaining += n\n            self.bytes_read += n\n        else:\n            self.eof = 1\n\n        return 1\n\n    cdef object process(self) except *:\n        \"\"\"Process the chunker's buffer and return the next chunk.\"\"\"\n        cdef uint64_t sum, chunk_mask = self.chunk_mask\n        cdef size_t n, old_last, min_size = self.min_size, window_size = self.window_size\n        cdef uint8_t* p\n        cdef uint8_t* stop_at\n        cdef size_t did_bytes\n\n        if self.done:\n            if self.bytes_read == self.bytes_yielded:\n                raise StopIteration\n            else:\n                raise Exception(\"chunkifier byte count mismatch\")\n\n        while self.remaining < min_size + window_size + 1 and not self.eof:  # see assert in Chunker init\n            if not self.fill():\n                return None\n\n        # Here we either are at eof...\n        if self.eof:\n            self.done = 1\n            if self.remaining:\n                self.bytes_yielded += self.remaining\n                # Return a memory view of the remaining data\n                return memoryview((self.data + self.position)[:self.remaining])\n            else:\n                if self.bytes_read == self.bytes_yielded:\n                    raise StopIteration\n                else:\n                    raise Exception(\"chunkifier byte count mismatch\")\n\n        # ... or we have at least min_size + window_size + 1 bytes remaining.\n        # We do not want to \"cut\" a chunk smaller than min_size and the hash\n        # window starts at the potential cutting place.\n        self.position += min_size\n        self.remaining -= min_size\n        sum = _buzhash64(self.data + self.position, window_size, self.table)\n\n        while self.remaining > window_size and (sum & chunk_mask) and not (self.eof and self.remaining <= window_size):\n            p = self.data + self.position\n            stop_at = p + self.remaining - window_size\n\n            while p < stop_at and (sum & chunk_mask):\n                sum = _buzhash64_update(sum, p[0], p[window_size], window_size, self.table)\n                p += 1\n\n            did_bytes = p - (self.data + self.position)\n            self.position += did_bytes\n            self.remaining -= did_bytes\n\n            if self.remaining <= window_size:\n                if not self.fill():\n                    return None\n\n        if self.remaining <= window_size:\n            self.position += self.remaining\n            self.remaining = 0\n\n        old_last = self.last\n        self.last = self.position\n        n = self.last - old_last\n        self.bytes_yielded += n\n\n        # Return a memory view of the chunk\n        return memoryview((self.data + old_last)[:n])\n\n    def chunkify(self, fd, fh=-1, fmap=None):\n        \"\"\"\n        Cut a file into chunks.\n\n        :param fd: Python file object\n        :param fh: OS-level file handle (if available),\n                   defaults to -1 which means not to use OS-level fd.\n        :param fmap: a file map, same format as generated by sparsemap\n        \"\"\"\n        self._fd = fd\n        self.fh = fh\n        self.file_reader = FileReader(fd=fd, fh=fh, read_size=self.reader_block_size, sparse=self.sparse, fmap=fmap)\n        self.done = 0\n        self.remaining = 0\n        self.bytes_read = 0\n        self.bytes_yielded = 0\n        self.position = 0\n        self.last = 0\n        self.eof = 0\n        return self\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        started_chunking = time.monotonic()\n        data = self.process()\n        got = len(data)\n        # we do not have SEEK_DATA/SEEK_HOLE support in chunker_process C code,\n        # but we can just check if data was all-zero (and either came from a hole\n        # or from stored zeros - we can not detect that here).\n        if zeros.startswith(data):\n            data = None\n            allocation = CH_ALLOC\n        else:\n            allocation = CH_DATA\n        self.chunking_time += time.monotonic() - started_chunking\n        return Chunk(data, size=got, allocation=allocation)\n\n\ndef buzhash64(data, bytes key):\n    cdef uint64_t *table\n    cdef uint64_t sum\n    table = buzhash64_init_table(key)\n    sum = _buzhash64(<const unsigned char *> data, len(data), table)\n    free(table)\n    return sum\n\n\ndef buzhash64_update(uint64_t sum, unsigned char remove, unsigned char add, size_t len, bytes key):\n    cdef uint64_t *table\n    table = buzhash64_init_table(key)\n    sum = _buzhash64_update(sum, remove, add, len, table)\n    free(table)\n    return sum\n\n\ndef buzhash64_get_table(bytes key):\n    \"\"\"Get the buzhash table generated from <key>.\"\"\"\n    cdef uint64_t *table\n    cdef int i\n    table = buzhash64_init_table(key)\n    try:\n        return [table[i] for i in range(256)]\n    finally:\n        free(table)\n"
  },
  {
    "path": "src/borg/chunkers/failing.py",
    "content": "import os\nimport errno\nfrom typing import BinaryIO, Iterator\n\nfrom ..constants import CH_DATA\nfrom .reader import Chunk\n\n\nclass ChunkerFailing:\n    \"\"\"\n    This is a very simple chunker for testing purposes.\n\n    Reads block_size chunks. The map parameter controls behavior per block:\n    'R' = successful read, 'E' = I/O Error. Blocks beyond the map length\n    will have the same behavior as the last map character.\n    \"\"\"\n\n    def __init__(self, block_size: int, map: str) -> None:\n        self.block_size = block_size\n        # one char per block: r/R = successful read, e/E = I/O Error, e.g.: \"rrrrErrrEEr\"\n        # blocks beyond the map will have the same behavior as the last map char indicates.\n        map = map.upper()\n        if not set(map).issubset({\"R\", \"E\"}):\n            raise ValueError(\"unsupported map character\")\n        self.map = map\n        self.count = 0\n        self.chunking_time = 0.0  # not updated, just provided so that caller does not crash\n\n    def chunkify(self, fd: BinaryIO | None = None, fh: int = -1) -> Iterator:\n        \"\"\"\n        Cut a file into chunks.\n\n        :param fd: Python file object\n        :param fh: OS-level file handle (if available),\n                   defaults to -1 which means not to use OS-level fd.\n        \"\"\"\n        use_fh = fh >= 0\n        wanted = self.block_size\n        while True:\n            data = os.read(fh, wanted) if use_fh else fd.read(wanted)\n            got = len(data)\n            if got > 0:\n                idx = self.count if self.count < len(self.map) else -1\n                behavior = self.map[idx]\n                if behavior == \"E\":\n                    self.count += 1\n                    fname = None if use_fh else getattr(fd, \"name\", None)\n                    raise OSError(errno.EIO, \"simulated I/O error\", fname)\n                elif behavior == \"R\":\n                    self.count += 1\n                    yield Chunk(data, size=got, allocation=CH_DATA)\n                else:\n                    raise ValueError(\"unsupported map character\")\n            if got < wanted:\n                # we did not get enough data, looks like EOF.\n                return\n"
  },
  {
    "path": "src/borg/chunkers/fixed.py",
    "content": "from typing import Iterator, BinaryIO\n\n\nimport time\n\nfrom .reader import FileReader\n\n\nclass ChunkerFixed:\n    \"\"\"\n    This is a simple chunker for input data with data usually staying at the same\n    offset and/or with known block/record sizes:\n\n    - raw disk images\n    - block devices\n    - database files with simple header + fixed-size records layout\n\n    It optionally supports:\n\n    - a header block of different size\n    - using a sparsemap to read only data ranges and seek over hole ranges\n      for sparse files.\n    - using an externally given filemap to read only specific ranges from\n      a file.\n\n    Note: the last block of a data or hole range may be less than the block size,\n          this is supported and not considered to be an error.\n    \"\"\"\n\n    def __init__(self, block_size: int, header_size: int = 0, sparse: bool = False) -> None:\n        self.block_size = block_size\n        self.header_size = header_size\n        self.chunking_time = 0.0  # likely will stay close to zero - not much to do here.\n        self.reader_block_size = 1024 * 1024\n        self.reader: FileReader | None = None\n        self.sparse = sparse\n\n    def chunkify(self, fd: BinaryIO | None = None, fh: int = -1, fmap: list | None = None) -> Iterator:\n        \"\"\"\n        Cut a file into chunks.\n\n        :param fd: Python file object\n        :param fh: OS-level file handle (if available),\n                   defaults to -1 which means not to use OS-level fd.\n        :param fmap: a file map, same format as generated by sparsemap\n        \"\"\"\n        # Initialize the reader with the file descriptors\n        self.reader = FileReader(fd=fd, fh=fh, read_size=self.reader_block_size, sparse=self.sparse, fmap=fmap)\n\n        # Handle header if present\n        if self.header_size > 0:\n            # Read the header block using read\n            started_chunking = time.monotonic()\n            header_chunk = self.reader.read(self.header_size)\n            self.chunking_time += time.monotonic() - started_chunking\n\n            if header_chunk.meta[\"size\"] > 0:\n                # Yield the header chunk\n                yield header_chunk\n\n        # Process the rest of the file using read\n        while True:\n            started_chunking = time.monotonic()\n            chunk = self.reader.read(self.block_size)\n            self.chunking_time += time.monotonic() - started_chunking\n            size = chunk.meta[\"size\"]\n            if size == 0:\n                break  # EOF\n            assert size <= self.block_size\n            yield chunk\n"
  },
  {
    "path": "src/borg/chunkers/reader.pyi",
    "content": "from typing import NamedTuple, Tuple, Dict, List, Any, Type, BinaryIO, Iterator\n\nhas_seek_hole: bool\n\nclass _Chunk(NamedTuple):\n    data: bytes | None\n    meta: Dict[str, Any]\n\ndef Chunk(data: bytes | None, **meta) -> Type[_Chunk]: ...\n\nfmap_entry = Tuple[int, int, bool]\n\ndef sparsemap(fd: BinaryIO = None, fh: int = -1) -> List[fmap_entry]: ...\n\nclass FileFMAPReader:\n    def __init__(\n        self,\n        *,\n        fd: BinaryIO = None,\n        fh: int = -1,\n        read_size: int = 0,\n        sparse: bool = False,\n        fmap: List[fmap_entry] = None,\n    ) -> None: ...\n    def _build_fmap(self) -> List[fmap_entry]: ...\n    def blockify(self) -> Iterator: ...\n\nclass FileReader:\n    def __init__(\n        self,\n        *,\n        fd: BinaryIO = None,\n        fh: int = -1,\n        read_size: int = 0,\n        sparse: bool = False,\n        fmap: List[fmap_entry] = None,\n    ) -> None: ...\n    def _fill_buffer(self) -> bool: ...\n    def read(self, size: int) -> Type[_Chunk]: ...\n"
  },
  {
    "path": "src/borg/chunkers/reader.pyx",
    "content": "# cython: language_level=3\n\n\n\nimport os\nimport errno\nimport time\nfrom collections import namedtuple\n\nfrom ..platform import safe_fadvise\nfrom ..constants import CH_DATA, CH_ALLOC, CH_HOLE, zeros\n\n# this will be True if Python's seek implementation supports data/holes seeking.\n# this does not imply that it will actually work on the filesystem,\n# because the FS also needs to support this.\nhas_seek_hole = hasattr(os, 'SEEK_DATA') and hasattr(os, 'SEEK_HOLE')\n\n_Chunk = namedtuple('_Chunk', 'meta data')\n_Chunk.__doc__ = \"\"\"\\\n    Chunk namedtuple\n\n    meta is always a dictionary, data depends on allocation.\n\n    data chunk read from a DATA range of a file (not from a sparse hole):\n        meta = {'allocation' = CH_DATA, 'size' = size_of_chunk }\n        data = read_data [bytes or memoryview]\n\n    all-zero chunk read from a DATA range of a file (not from a sparse hole, but detected to be all-zero):\n        meta = {'allocation' = CH_ALLOC, 'size' = size_of_chunk }\n        data = None\n\n    all-zero chunk from a HOLE range of a file (from a sparse hole):\n        meta = {'allocation' = CH_HOLE, 'size' = size_of_chunk }\n        data = None\n\"\"\"\n\ndef Chunk(data, **meta):\n    return _Chunk(meta, data)\n\n\ndef dread(offset, size, fd=None, fh=-1):\n    use_fh = fh >= 0\n    if use_fh:\n        data = os.read(fh, size)\n        safe_fadvise(fh, offset, len(data), \"DONTNEED\")\n        return data\n    else:\n        return fd.read(size)\n\n\ndef dseek(amount, whence, fd=None, fh=-1):\n    use_fh = fh >= 0\n    if use_fh:\n        return os.lseek(fh, amount, whence)\n    else:\n        return fd.seek(amount, whence)\n\n\ndef dpos_curr_end(fd=None, fh=-1):\n    \"\"\"\n    determine current position, file end position (== file length)\n    \"\"\"\n    curr = dseek(0, os.SEEK_CUR, fd, fh)\n    end = dseek(0, os.SEEK_END, fd, fh)\n    dseek(curr, os.SEEK_SET, fd, fh)\n    return curr, end\n\n\ndef sparsemap(fd=None, fh=-1):\n    \"\"\"\n    Generator yielding (start, length, is_data) tuples for each range.\n    is_data indicates data ranges (True) or hole ranges (False).\n\n    Note:\n    The map is generated starting from the current seek position (it\n    is not required to be 0, i.e., the start of the file) and works from there up to the end of the file.\n    When the generator is finished, the file pointer position will be\n    reset to where it was before calling this function.\n    \"\"\"\n    curr, file_len = dpos_curr_end(fd, fh)\n    start = curr\n    try:\n        whence = os.SEEK_HOLE\n        while True:\n            is_data = whence == os.SEEK_HOLE  # True: range with data, False: range is a hole\n            try:\n                end = dseek(start, whence, fd, fh)\n            except OSError as e:\n                if e.errno == errno.ENXIO:\n                    if not is_data and start < file_len:\n                        # if there is a hole at the end of a file, we can not find the file end by SEEK_DATA\n                        # (because we run into ENXIO), thus we must manually deal with this case:\n                        end = file_len\n                        yield (start, end - start, is_data)\n                    break\n                else:\n                    raise\n            # we do not want to yield zero-length ranges with start == end:\n            if end > start:\n                yield (start, end - start, is_data)\n            start = end\n            whence = os.SEEK_DATA if is_data else os.SEEK_HOLE\n    finally:\n        # seek to same position as before calling this function\n        dseek(curr, os.SEEK_SET, fd, fh)\n\n\nclass FileFMAPReader:\n    \"\"\"\n    This is for reading blocks from a file.\n\n    It optionally supports:\n\n    - using a sparsemap to read only data ranges and seek over hole ranges\n      for sparse files.\n    - using an externally given filemap to read only specific ranges from\n      a file.\n\n    Note: the last block of a data or hole range may be less than the read_size,\n          this is supported and not considered to be an error.\n    \"\"\"\n    def __init__(self, *, fd=None, fh=-1, read_size=0, sparse=False, fmap=None):\n        assert fd is not None or fh >= 0\n        self.fd = fd\n        self.fh = fh\n        assert 0 < read_size <= len(zeros)\n        self.read_size = read_size  # how much data we want to read at once\n        self.reading_time = 0.0  # time spent in reading/seeking\n        # should borg try to do sparse input processing?\n        # whether it actually can be done depends on the input file being seekable.\n        self.try_sparse = sparse and has_seek_hole\n        self.fmap = fmap\n\n    def _build_fmap(self):\n        started_fmap = time.monotonic()\n        fmap = None\n        if self.try_sparse:\n            try:\n                fmap = list(sparsemap(self.fd, self.fh))\n            except OSError as err:\n                # seeking did not work\n                pass\n\n        if fmap is None:\n            # either sparse processing (building the fmap) was not tried or it failed.\n            # in these cases, we just build a \"fake fmap\" that considers the whole file\n            # as range(s) of data (no holes), so we can use the same code.\n            fmap = [(0, 2 ** 62, True), ]\n        self.reading_time += time.monotonic() - started_fmap\n        return fmap\n\n    def blockify(self):\n        \"\"\"\n        Read <read_size> sized blocks from a file.\n        \"\"\"\n        if self.fmap is None:\n            self.fmap = self._build_fmap()\n\n        offset = 0\n        for range_start, range_size, is_data in self.fmap:\n            if range_start != offset:\n                # this is for the case when the fmap does not cover the file completely,\n                # e.g. it could be without the ranges of holes or of unchanged data.\n                offset = range_start\n                dseek(offset, os.SEEK_SET, self.fd, self.fh)\n            while range_size:\n                started_reading = time.monotonic()\n                wanted = min(range_size, self.read_size)\n                if is_data:\n                    # read block from the range\n                    data = dread(offset, wanted, self.fd, self.fh)\n                    got = len(data)\n                    if zeros.startswith(data):\n                        data = None\n                        allocation = CH_ALLOC\n                    else:\n                        allocation = CH_DATA\n                else:  # hole\n                    # seek over block from the range\n                    pos = dseek(wanted, os.SEEK_CUR, self.fd, self.fh)\n                    got = pos - offset\n                    data = None\n                    allocation = CH_HOLE\n                self.reading_time += time.monotonic() - started_reading\n                if got > 0:\n                    offset += got\n                    range_size -= got\n                    yield Chunk(data, size=got, allocation=allocation)\n                if got < wanted:\n                    # We did not get enough data; looks like EOF.\n                    return\n\n\nclass FileReader:\n    \"\"\"\n    This is a buffered reader for file data.\n\n    It maintains a buffer that is filled with Chunks from the FileFMAPReader.blockify generator.\n    The data in that buffer is consumed by clients calling FileReader.read, which returns a Chunk.\n\n    Most complexity in here comes from the desired size when a user calls FileReader.read does\n    not need to match the Chunk sizes we got from the FileFMAPReader.\n    \"\"\"\n    def __init__(self, *, fd=None, fh=-1, read_size=0, sparse=False, fmap=None):\n        assert read_size > 0\n        self.reader = FileFMAPReader(fd=fd, fh=fh, read_size=read_size, sparse=sparse, fmap=fmap)\n        self.buffer = []  # list of Chunk objects\n        self.offset = 0  # offset into the first buffer object's data\n        self.remaining_bytes = 0  # total bytes available in buffer\n        self.blockify_gen = None  # generator from FileFMAPReader.blockify\n        self.fd = fd\n        self.fh = fh\n        self.fmap = fmap\n\n    def _fill_buffer(self):\n        \"\"\"\n        Fill the buffer with more data from the blockify generator.\n        Returns True if more data was added, False if EOF.\n        \"\"\"\n        if self.blockify_gen is None:\n            return False\n\n        try:\n            chunk = next(self.blockify_gen)\n            # Store the Chunk object directly in the buffer\n            self.buffer.append(chunk)\n            self.remaining_bytes += chunk.meta[\"size\"]\n            return True\n        except StopIteration:\n            self.blockify_gen = None\n            return False\n\n    def read(self, size):\n        \"\"\"\n        Read a Chunk of up to 'size' bytes from the file.\n\n        This method tries to yield a Chunk of the requested size, if possible, by considering\n        multiple chunks from the buffer.\n\n        The allocation type of the resulting chunk depends on the allocation types of the contributing chunks:\n        - If one of the chunks is CH_DATA, it will create all-zero bytes for other chunks that are not CH_DATA\n        - If all contributing chunks are CH_HOLE, the resulting chunk will also be CH_HOLE\n        - If the contributing chunks are a mix of CH_HOLE and CH_ALLOC, the resulting chunk will be CH_HOLE\n\n        :param size: Number of bytes to read\n        :return: Chunk object containing the read data.\n                 If no data is available, returns Chunk(None, size=0, allocation=CH_ALLOC).\n                 If less than requested bytes were available (at EOF), the returned chunk might be smaller\n                 than requested.\n        \"\"\"\n        # Initialize if not already done\n        if self.blockify_gen is None:\n            self.buffer = []\n            self.offset = 0\n            self.remaining_bytes = 0\n            self.blockify_gen = self.reader.blockify()\n\n        # If we don't have enough data in the buffer, try to fill it\n        while self.remaining_bytes < size:\n            if not self._fill_buffer():\n                # No more data available, return what we have\n                break\n\n        # If we have no data at all, return an empty Chunk\n        if not self.buffer:\n            return Chunk(b\"\", size=0, allocation=CH_DATA)\n\n        # Prepare to collect the requested data\n        result = bytearray()\n        bytes_to_read = min(size, self.remaining_bytes)\n        bytes_read = 0\n\n        # Track if we've seen different allocation types\n        has_data = False\n        has_hole = False\n        has_alloc = False\n\n        # Read data from the buffer, combining chunks as needed\n        while bytes_read < bytes_to_read and self.buffer:\n            chunk = self.buffer[0]\n            chunk_size = chunk.meta[\"size\"]\n            allocation = chunk.meta[\"allocation\"]\n            data = chunk.data\n\n            # Track allocation types\n            if allocation == CH_DATA:\n                has_data = True\n            elif allocation == CH_HOLE:\n                has_hole = True\n            elif allocation == CH_ALLOC:\n                has_alloc = True\n            else:\n                raise ValueError(f\"Invalid allocation type: {allocation}\")\n\n            # Calculate how much we can read from this chunk\n            available = chunk_size - self.offset\n            to_read = min(available, bytes_to_read - bytes_read)\n\n            # Process the chunk based on its allocation type\n            if allocation == CH_DATA:\n                assert data is not None\n                # For data chunks, add the actual data\n                result.extend(data[self.offset:self.offset + to_read])\n            else:\n                # For non-data chunks, always add zeros to the result.\n                # We will only yield a CH_DATA chunk with the result bytes,\n                # if there was at least one CH_DATA chunk contributing to the result,\n                # otherwise we will yield a CH_HOLE or CH_ALLOC chunk.\n                result.extend(b'\\0' * to_read)\n\n            bytes_read += to_read\n\n            # Update offset or remove chunk if fully consumed\n            if to_read < available:\n                self.offset += to_read\n            else:\n                self.offset = 0\n                self.buffer.pop(0)\n\n            self.remaining_bytes -= to_read\n\n        # Determine the allocation type of the resulting chunk\n        if has_data:\n            # If any chunk was CH_DATA, the result is CH_DATA\n            return Chunk(bytes(result), size=bytes_read, allocation=CH_DATA)\n        elif has_hole:\n            # If any chunk was CH_HOLE (and none were CH_DATA), the result is CH_HOLE\n            return Chunk(None, size=bytes_read, allocation=CH_HOLE)\n        else:\n            # Otherwise, all chunks were CH_ALLOC\n            return Chunk(None, size=bytes_read, allocation=CH_ALLOC)\n\n\n"
  },
  {
    "path": "src/borg/cockpit/__init__.py",
    "content": "\"\"\"\nBorg Cockpit - Terminal User Interface for BorgBackup.\n\nThis module contains the TUI implementation using Textual.\n\"\"\"\n"
  },
  {
    "path": "src/borg/cockpit/app.py",
    "content": "\"\"\"\nBorg Cockpit - Application Entry Point.\n\"\"\"\n\nimport asyncio\nimport time\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nfrom textual.containers import Horizontal, Container\n\nfrom .theme import theme\n\n\nclass BorgCockpitApp(App):\n    \"\"\"The main TUI Application class for Borg Cockpit.\"\"\"\n\n    from .. import __version__ as BORG_VERSION\n\n    TITLE = f\"Cockpit for BorgBackup {BORG_VERSION}\"\n    CSS_PATH = \"cockpit.tcss\"\n    BINDINGS = [(\"q\", \"quit\", \"Quit\"), (\"ctrl+c\", \"quit\", \"Quit\"), (\"t\", \"toggle_translator\", \"Toggle Translator\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        from .widgets import LogoPanel, StatusPanel, StandardLog\n\n        yield Header(show_clock=True)\n\n        with Container(id=\"main-grid\"):\n            with Horizontal(id=\"top-row\"):\n                yield LogoPanel(id=\"logopanel\")\n                yield StatusPanel(id=\"status\")\n\n            yield StandardLog(id=\"standard-log\")\n\n        yield Footer()\n\n    def get_theme_variable_defaults(self):\n        # make these variables available to ALL themes\n        return {\n            \"pulsar-color\": \"#ffffff\",\n            \"pulsar-dim-color\": \"#000000\",\n            \"star-color\": \"#888888\",\n            \"star-bright-color\": \"#ffffff\",\n            \"logo-color\": \"#00dd00\",\n        }\n\n    def on_load(self) -> None:\n        \"\"\"Initialize theme before UI.\"\"\"\n        self.register_theme(theme)\n        self.theme = theme.name\n\n    def on_mount(self) -> None:\n        \"\"\"Initialize components.\"\"\"\n        self.query_one(\"#logo\").styles.animate(\"opacity\", 1, duration=1)\n        self.query_one(\"#slogan\").styles.animate(\"opacity\", 1, duration=1)\n\n        # Delay runner start until after widgets are fully mounted\n        self.call_after_refresh(self.start_runner)\n\n    def start_runner(self) -> None:\n        \"\"\"Start the Borg runner after all widgets are mounted.\"\"\"\n        from .runner import BorgRunner\n\n        # Speed tracking\n        self.total_lines_processed = 0\n        self.last_lines_processed = 0\n        self.speed_timer = self.set_interval(1.0, self.compute_speed)\n\n        self.start_time = time.monotonic()\n        self.process_running = True\n        args = getattr(self, \"borg_args\", [\"--version\"])  # Default to safe command if none passed\n        self.runner = BorgRunner(args, self.handle_log_event)\n        self.runner_task = asyncio.create_task(self.runner.start())\n\n    def compute_speed(self) -> None:\n        \"\"\"Calculate and update speed (lines per second).\"\"\"\n        current_lines = self.total_lines_processed\n        lines_per_second = float(current_lines - self.last_lines_processed)\n        self.last_lines_processed = current_lines\n\n        status_panel = self.query_one(\"#status\")\n        status_panel.update_speed(lines_per_second / 1000)\n        if self.process_running:\n            status_panel.elapsed_time = time.monotonic() - self.start_time\n\n    async def on_unmount(self) -> None:\n        \"\"\"Cleanup resources on app shutdown.\"\"\"\n        if hasattr(self, \"runner\"):\n            await self.runner.stop()\n\n    async def action_quit(self) -> None:\n        \"\"\"Handle quit action.\"\"\"\n        if hasattr(self, \"speed_timer\"):\n            self.speed_timer.stop()\n        if hasattr(self, \"runner\"):\n            await self.runner.stop()\n        if hasattr(self, \"runner_task\"):\n            await self.runner_task\n        self.query_one(\"#logo\").styles.animate(\"opacity\", 0, duration=2)\n        self.query_one(\"#slogan\").styles.animate(\"opacity\", 0, duration=2)\n        await asyncio.sleep(2)  # give the user a chance the see the borg RC\n        self.exit()\n\n    def action_toggle_translator(self) -> None:\n        \"\"\"Toggle the universal translator.\"\"\"\n        from .translator import TRANSLATOR\n\n        TRANSLATOR.toggle()\n        # Refresh dynamic UI elements\n        self.query_one(\"#status\").refresh_ui_labels()\n        self.query_one(\"#standard-log\").update_title()\n        self.query_one(\"#slogan\").update_slogan()\n\n    def handle_log_event(self, data: dict):\n        \"\"\"Process a event from BorgRunner.\"\"\"\n        msg_type = data.get(\"type\", \"log\")\n\n        if msg_type == \"stream_line\":\n            self.total_lines_processed += 1\n            line = data.get(\"line\", \"\")\n            widget = self.query_one(\"#standard-log\")\n            widget.add_line(line)\n\n        elif msg_type == \"process_finished\":\n            self.process_running = False\n            rc = data.get(\"rc\", 0)\n            self.query_one(\"#status\").rc = rc\n"
  },
  {
    "path": "src/borg/cockpit/cockpit.tcss",
    "content": "/* Borg Cockpit Stylesheet */\n\nScreen {\n    background: $surface;\n}\n\nHeader {\n    dock: top;\n    background: $primary;\n    color: $secondary;\n    text-style: bold;\n}\n\nHeader * {\n    background: $primary;\n    color: $secondary;\n    text-style: bold;\n}\n\n.header--clock, .header--title, .header--icon {\n    background: $primary;\n    color: $secondary;\n    text-style: bold;\n}\n\n.header--clock {\n    dock: right;\n}\n\nFooter {\n    background: $background;\n    color: $primary;\n    dock: bottom;\n}\n\n.footer--key {\n    background: $background;\n    color: $primary;\n    text-style: bold;\n}\n\n.footer--description {\n    background: $background;\n    color: $primary;\n    text-style: bold;\n}\n\n.footer--highlight {\n    background: $primary;\n    color: $secondary;\n}\n\n#standard-log-content {\n    scrollbar-background: $background;\n    scrollbar-color: $primary;\n    /* Hide horizontal scrollbar and clip long lines at the right */\n    overflow-x: hidden;\n    text-wrap: nowrap;\n}\n\n#standard-log {\n    border: double $primary;\n}\n\n#main-grid {\n    /* Simple vertical stack: top row content-sized, log fills remaining space */\n    layout: vertical;\n    /* Fill available area between header and footer */\n    height: 1fr;\n    /* Allow shrinking when space is tight */\n    min-height: 0;\n    margin: 0 1;\n}\n\n#top-row {\n    border: double $primary;\n    /* If content grows too large, scroll rather than pushing the log off-screen */\n    overflow-y: auto;\n    /* Adjust this if status or logo panel shall get more/less height. */\n    height: 16;\n}\n\n#logopanel {\n    width: 50%;\n    /* Stretch to the full height of the top row so the separator spans fully */\n    height: 100%;\n    border-right: double $primary;\n    text-align: center;\n    layers: base overlay;\n    /* Make logo panel not influence row height beyond status; clip overflow */\n    overflow: hidden;\n}\n\nStarfield {\n    layer: base;\n    width: 100%;\n    /* Size to content and get clipped by the panel */\n    height: 100%;\n    min-height: 0;\n}\n\nPulsar {\n    layer: overlay;\n    width: 3;\n    height: 3;\n    content-align: center middle;\n    color: $pulsar-color;\n    transition: color 4s linear;\n}\n\nSlogan {\n    layer: overlay;\n    width: auto;\n    height: 1;\n    content-align: center middle;\n    color: #00ff00;\n    transition: color 1s linear;\n    opacity: 0;\n    max-height: 100%;\n    overflow: hidden;\n}\n\nLogo {\n    layer: overlay;\n    width: auto;\n    /* Size to its intrinsic content, clipped by the panel */\n    height: auto;\n    opacity: 0;\n    max-height: 100%;\n    overflow: hidden;\n}\n\nSlogan.dim {\n    color: #005500;\n}\n\nPulsar.dim {\n    color: $pulsar-dim-color;\n}\n\n#status {\n    width: 50%;\n    /* Let height be determined by content so the row can size to content */\n    height: auto;\n    /* Prevent internal content from forcing excessive height; allow scrolling */\n    overflow-y: auto;\n}\n\n/* Ensure the log always keeps at least 5 rows visible */\n#standard-log {\n    min-height: 5;\n    /* Explicitly claim the remaining space in the grid */\n    height: 1fr;\n}\n\n/* Within the log panel (a Vertical container), keep the title to 1 line and let content fill the rest */\n#standard-log-title {\n    height: 1;\n}\n\n#standard-log-content {\n    /* Allow the RichLog to expand within the log panel */\n    height: 1fr;\n}\n\n.panel-title {\n    background: $primary;\n    color: $secondary;\n    padding: 0 1;\n    text-style: bold;\n}\n\n#speed-sparkline {\n    width: 100%;\n    height: 4;\n    margin-bottom: 1;\n}\n\n.status {\n    color: $primary;\n}\n\n.errors-ok {\n    color: $success;\n}\n\n.errors-warning {\n    color: $warning;\n}\n\n.rc-ok {\n    color: $success;\n}\n\n.rc-warning {\n    color: $warning;\n}\n\n.rc-error {\n    color: $error;\n}\n"
  },
  {
    "path": "src/borg/cockpit/runner.py",
    "content": "\"\"\"\nBorg Runner - Manages Borg subprocess execution and output parsing.\n\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport sys\nfrom typing import Optional, Callable, List\n\n\nclass BorgRunner:\n    \"\"\"\n    Manages the execution of the borg subprocess and parses its JSON output.\n    \"\"\"\n\n    def __init__(self, command: List[str], log_callback: Callable[[dict], None]):\n        self.command = command\n        self.log_callback = log_callback\n        self.process: Optional[asyncio.subprocess.Process] = None\n        self.logger = logging.getLogger(__name__)\n\n    async def start(self):\n        \"\"\"\n        Starts the Borg subprocess and processes its output.\n        \"\"\"\n        if self.process is not None:\n            self.logger.warning(\"Borg process already running.\")\n            return\n\n        if getattr(sys, \"frozen\", False):\n            cmd = [sys.executable] + self.command  # executable == pyinstaller binary\n        else:\n            cmd = [sys.executable, \"-m\", \"borg\"] + self.command  # executable == python interpreter\n\n        self.logger.info(f\"Starting Borg process: {cmd}\")\n\n        env = os.environ.copy()\n        env[\"PYTHONUNBUFFERED\"] = \"1\"\n\n        try:\n            self.process = await asyncio.create_subprocess_exec(\n                *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env\n            )\n\n            async def read_stream(stream, stream_name):\n                while True:\n                    line = await stream.readline()\n                    if not line:\n                        break\n                    decoded_line = line.decode(\"utf-8\", errors=\"replace\").rstrip()\n                    if decoded_line:\n                        self.log_callback({\"type\": \"stream_line\", \"stream\": stream_name, \"line\": decoded_line})\n\n            # Read both streams concurrently\n            await asyncio.gather(read_stream(self.process.stdout, \"stdout\"), read_stream(self.process.stderr, \"stderr\"))\n\n            rc = await self.process.wait()\n            self.log_callback({\"type\": \"process_finished\", \"rc\": rc})\n\n        except Exception as e:\n            self.logger.error(f\"Failed to run Borg process: {e}\")\n            self.log_callback({\"type\": \"process_finished\", \"rc\": -1, \"error\": str(e)})\n        finally:\n            self.process = None\n\n    async def stop(self):\n        \"\"\"\n        Stops the Borg subprocess if it is running.\n        \"\"\"\n        if self.process and self.process.returncode is None:\n            self.logger.info(\"Terminating Borg process...\")\n            try:\n                self.process.terminate()\n                await self.process.wait()\n            except ProcessLookupError:\n                pass  # Process already dead\n"
  },
  {
    "path": "src/borg/cockpit/theme.py",
    "content": "\"\"\"\nBorg Theme Definition.\n\"\"\"\n\nfrom textual.theme import Theme\n\ntheme = Theme(\n    name=\"borg\",\n    primary=\"#00FF00\",\n    secondary=\"#000000\",  # text on top of $primary background\n    error=\"#FF0000\",\n    warning=\"#FFA500\",\n    success=\"#00FF00\",\n    accent=\"#00FF00\",  # highlighted interactive elements\n    foreground=\"#00FF00\",  # default text color\n    background=\"#000000\",\n    surface=\"#000000\",  # bg col of lowest layer\n    panel=\"#444444\",  # bg col of panels, containers, cards, sidebars, modal dialogs, etc.\n    dark=True,\n    variables={\n        \"block-cursor-text-style\": \"none\",\n        \"input-selection-background\": \"#00FF00 35%\",\n        \"pulsar-color\": \"#ffffff\",\n        \"pulsar-dim-color\": \"#000000\",\n        \"star-color\": \"#888888\",\n        \"star-bright-color\": \"#ffffff\",\n        \"logo-color\": \"#00dd00\",\n    },\n)\n"
  },
  {
    "path": "src/borg/cockpit/translator.py",
    "content": "\"\"\"\nUniversal Translator - Converts standard English into Borg Speak.\n\"\"\"\n\nBORG_DICTIONARY = {  # English -> Borg\n    # UI Strings\n    \"**** You're welcome! ****\": \"You will be assimilated! \",\n    \"Files: \": \"Drones: \",\n    \"Unchanged: \": \"Unchanged: \",\n    \"Modified: \": \"Modified: \",\n    \"Added: \": \"Assimilated: \",\n    \"Other: \": \"Other: \",\n    \"Errors: \": \"Escaped: \",\n    \"RC: \": \"Termination Code: \",\n    \"Log\": \"Subspace Transmissions\",\n}\n\n\nclass UniversalTranslator:\n    \"\"\"\n    Handles translation of log messages.\n    \"\"\"\n\n    def __init__(self, enabled: bool = True):\n        # self.enabled is the opposite of \"Translator active\" on the TUI,\n        # because in the source, we translate English to Borg.\n        self.enabled = enabled  # True: English -> Borg\n\n    def toggle(self):\n        \"\"\"Toggle translation state.\"\"\"\n        self.enabled = not self.enabled\n        return self.enabled\n\n    def translate(self, message: str) -> str:\n        \"\"\"Translate a message if enabled.\"\"\"\n        if not self.enabled:\n            return message\n\n        # Full matching first\n        if message in BORG_DICTIONARY:\n            return BORG_DICTIONARY[message]\n\n        # Substring matching next\n        for key, value in BORG_DICTIONARY.items():\n            if key in message:\n                return message.replace(key, value)\n\n        return message\n\n\n# Global Instance\nTRANSLATOR = UniversalTranslator(enabled=False)\n\n# Global translation function\nT = TRANSLATOR.translate\n"
  },
  {
    "path": "src/borg/cockpit/widgets.py",
    "content": "\"\"\"\nBorg Cockpit - UI Widgets.\n\"\"\"\n\nimport random\nimport time\n\nfrom rich.markup import escape\nfrom textual.app import ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Static, RichLog\nfrom textual.containers import Vertical, Container\nfrom ..helpers import classify_ec\nfrom .translator import T, TRANSLATOR\n\n\nclass StatusPanel(Static):\n    elapsed_time = reactive(0.0, init=False)\n    files_count = reactive(0, init=False)  # unchanged + modified + added + other + error\n    unchanged_count = reactive(0, init=False)\n    modified_count = reactive(0, init=False)\n    added_count = reactive(0, init=False)\n    other_count = reactive(0, init=False)\n    error_count = reactive(0, init=False)\n    rc = reactive(None, init=False)\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.speed_history = [0.0] * SpeedSparkline.HISTORY_SIZE\n\n    def compose(self) -> ComposeResult:\n        with Vertical():\n            yield SpeedSparkline(self.speed_history, id=\"speed-sparkline\")\n            yield Static(T(\"Speed: 0/s\"), id=\"status-speed\")\n\n            with Vertical(id=\"statuses\"):\n                yield Static(T(\"Elapsed: 00d 00:00:00\"), classes=\"status\", id=\"status-elapsed\")\n                yield Static(T(\"Files: 0\"), classes=\"status\", id=\"status-files\")\n                yield Static(T(\"Unchanged: 0\"), classes=\"status\", id=\"status-unchanged\")\n                yield Static(T(\"Modified: 0\"), classes=\"status\", id=\"status-modified\")\n                yield Static(T(\"Added: 0\"), classes=\"status\", id=\"status-added\")\n                yield Static(T(\"Other: 0\"), classes=\"status\", id=\"status-other\")\n                yield Static(T(\"Errors: 0\"), classes=\"status error-ok\", id=\"status-errors\")\n                yield Static(T(\"RC: RUNNING\"), classes=\"status\", id=\"status-rc\")\n\n    def update_speed(self, kfiles_per_second: float):\n        self.speed_history.append(kfiles_per_second)\n        self.speed_history = self.speed_history[-SpeedSparkline.HISTORY_SIZE :]\n        # Use our custom update method\n        self.query_one(\"#speed-sparkline\").update_data(self.speed_history)\n        self.query_one(\"#status-speed\").update(T(f\"Speed: {int(kfiles_per_second * 1000)}/s\"))\n\n    def watch_error_count(self, count: int) -> None:\n        sw = self.query_one(\"#status-errors\")\n        if count == 0:\n            sw.remove_class(\"errors-warning\")\n            sw.add_class(\"errors-ok\")\n        else:\n            sw.remove_class(\"errors-ok\")\n            sw.add_class(\"errors-warning\")\n        sw.update(T(f\"Errors: {count}\"))\n\n    def watch_files_count(self, count: int) -> None:\n        self.query_one(\"#status-files\").update(T(f\"Files: {count}\"))\n\n    def watch_unchanged_count(self, count: int) -> None:\n        self.query_one(\"#status-unchanged\").update(T(f\"Unchanged: {count}\"))\n\n    def watch_modified_count(self, count: int) -> None:\n        self.query_one(\"#status-modified\").update(T(f\"Modified: {count}\"))\n\n    def watch_added_count(self, count: int) -> None:\n        self.query_one(\"#status-added\").update(T(f\"Added: {count}\"))\n\n    def watch_other_count(self, count: int) -> None:\n        self.query_one(\"#status-other\").update(T(f\"Other: {count}\"))\n\n    def watch_rc(self, rc: int):\n        label = self.query_one(\"#status-rc\")\n        if rc is None:\n            label.update(T(\"RC: RUNNING\"))\n            return\n\n        label.remove_class(\"rc-ok\")\n        label.remove_class(\"rc-warning\")\n        label.remove_class(\"rc-error\")\n\n        status = classify_ec(rc)\n        if status == \"success\":\n            label.add_class(\"rc-ok\")\n        elif status == \"warning\":\n            label.add_class(\"rc-warning\")\n        else:  # error, signal\n            label.add_class(\"rc-error\")\n\n        label.update(T(f\"RC: {rc}\"))\n\n    def watch_elapsed_time(self, elapsed: float) -> None:\n        if TRANSLATOR.enabled:\n            # There seems to be no official formula for stardates, so we make something up.\n            # When showing the stardate, it is an absolute time, not relative \"elapsed time\".\n            ut = time.time()\n            sd = (ut - 1735689600) / 60.0  # Minutes since 2025-01-01 00:00.00 UTC\n            msg = f\"Stardate {sd:.1f}\"\n        else:\n            seconds = int(elapsed)\n            days, seconds = divmod(seconds, 86400)\n            h, m, s = seconds // 3600, (seconds % 3600) // 60, seconds % 60\n            msg = f\"Elapsed: {days:02d}d {h:02d}:{m:02d}:{s:02d}\"\n        self.query_one(\"#status-elapsed\").update(msg)\n\n    def refresh_ui_labels(self):\n        \"\"\"Update static UI labels with current translation.\"\"\"\n        self.watch_elapsed_time(self.elapsed_time)\n        self.query_one(\"#status-files\").update(T(f\"Files: {self.files_count}\"))\n        self.query_one(\"#status-unchanged\").update(T(f\"Unchanged: {self.unchanged_count}\"))\n        self.query_one(\"#status-modified\").update(T(f\"Modified: {self.modified_count}\"))\n        self.query_one(\"#status-added\").update(T(f\"Added: {self.added_count}\"))\n        self.query_one(\"#status-other\").update(T(f\"Other: {self.other_count}\"))\n        self.query_one(\"#status-errors\").update(T(f\"Errors: {self.error_count}\"))\n\n        if self.rc is not None:\n            self.watch_rc(self.rc)\n        else:\n            self.query_one(\"#status-rc\").update(T(\"RC: RUNNING\"))\n\n\nclass StandardLog(Vertical):\n    def compose(self) -> ComposeResult:\n        yield Static(T(\"Log\"), classes=\"panel-title\", id=\"standard-log-title\")\n        yield RichLog(id=\"standard-log-content\", highlight=False, markup=True, auto_scroll=True, max_lines=None)\n\n    def update_title(self):\n        self.query_one(\"#standard-log-title\").update(T(\"Log\"))\n\n    def add_line(self, line: str):\n        # TODO: make this more generic, use json output from borg.\n        # currently, this is only really useful for borg create/extract --list\n        line = line.rstrip()\n        if len(line) == 0:\n            return\n\n        markup_tag = None\n        if len(line) >= 2:\n            if line[1] == \" \" and line[0] in \"EAMUdcbs+-\":\n                # looks like from borg create/extract --list\n                status_panel = self.app.query_one(\"#status\")\n                status_panel.files_count += 1\n                status = line[0]\n                if status == \"E\":\n                    status_panel.error_count += 1\n                elif status in \"U-\":\n                    status_panel.unchanged_count += 1\n                elif status in \"M\":\n                    status_panel.modified_count += 1\n                elif status in \"A+\":\n                    status_panel.added_count += 1\n                elif status in \"dcbs\":\n                    status_panel.other_count += 1\n\n                markup_tag = {\n                    \"E\": \"red\",  # Error\n                    \"A\": \"white\",  # Added regular file (cache miss, slow!)\n                    \"M\": \"white\",  # Modified regular file (cache hit, but different, slow!)\n                    \"U\": \"green\",  # Updated regular file (cache hit)\n                    \"d\": \"green\",  # directory\n                    \"c\": \"green\",  # char device\n                    \"b\": \"green\",  # block device\n                    \"s\": \"green\",  # socket\n                    \"-\": \"white\",  # excluded\n                    \"+\": \"green\",  # included\n                }.get(status)\n\n        log_widget = self.query_one(\"#standard-log-content\")\n\n        safe_line = escape(line)\n        if markup_tag:\n            safe_line = f\"[{markup_tag}]{safe_line}[/]\"\n\n        log_widget.write(safe_line)\n\n\nclass Starfield(Static):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # Generate a unique seed for this instance to ensure random\n        # distribution per session but stable appearance during resize.\n        self._seed = random.randint(0, 1000000)  # nosec B311 - UI-only randomness, not for crypto\n\n    def on_mount(self) -> None:\n        self.call_after_refresh(self._update_art)\n\n    def on_resize(self, event) -> None:\n        self._update_art()\n\n    def _update_art(self) -> None:\n        \"\"\"Render starfield.\"\"\"\n        w, h = self.size\n        # Don't try to render if too small\n        if w < 10 or h < 5:\n            return\n\n        # Use our instance seed to keep stars \"static\" (same pattern) during resize\n        random.seed(self._seed)\n\n        star_density = 0.1\n        big_star_chance = 0.1\n\n        from .theme import theme\n\n        star_color = f\"[{theme.variables['star-color']}]\"\n        star_bright_color = f\"[{theme.variables['star-bright-color']}]\"\n\n        # 1. Create canvas (Starfield)\n        canvas = [[(\" \", \"\")] * w for _ in range(h)]\n        for y in range(h):\n            for x in range(w):\n                if random.random() < star_density:  # nosec B311 - visual effect randomness\n                    if random.random() < big_star_chance:  # nosec B311 - visual effect randomness\n                        char = \"*\"\n                        color = star_bright_color\n                    else:\n                        char = random.choice([\".\", \"·\"])  # nosec B311 - visual effect randomness\n                        color = star_color\n                    canvas[y][x] = (char, color)\n\n        # 2. Render to string\n        c_reset = \"[/]\"\n        final_lines = []\n        for row in canvas:\n            line_str = \"\"\n            for char, color in row:\n                if char == \" \":\n                    line_str += \" \"\n                else:\n                    line_str += f\"{color}{escape(char)}{c_reset}\"\n            final_lines.append(line_str)\n\n        art_str = \"\\n\".join(final_lines)\n        self.update(art_str)\n\n\nclass Pulsar(Static):\n    PULSAR_ART = \"\\n\".join([\" │ \", \"─*─\", \" │ \"])\n    H = 3\n    W = 3\n\n    def on_mount(self) -> None:\n        self.set_interval(4.0, self.pulse)\n        self.update_art()\n\n    def pulse(self) -> None:\n        self.toggle_class(\"dim\")\n\n    def update_art(self) -> None:\n        self.update(self.PULSAR_ART)\n\n\nclass Slogan(Static):\n    SLOGAN = \"**** You're welcome! ****\"\n    H = 1\n    W = len(SLOGAN)\n\n    def on_mount(self) -> None:\n        self.update(self.SLOGAN)\n        self.set_interval(1.0, self.pulse)\n\n    def pulse(self) -> None:\n        self.toggle_class(\"dim\")\n\n    def update_slogan(self):\n        self.update(T(self.SLOGAN))\n\n\nclass Logo(Static):\n    BORG_ART = [\n        \"██████╗  ██████╗ ██████╗  ██████╗ \",\n        \"██╔══██╗██╔═══██╗██╔══██╗██╔════╝ \",\n        \"██████╔╝██║   ██║██████╔╝██║  ███╗\",\n        \"██╔══██╗██║   ██║██╔══██╗██║   ██║\",\n        \"██████╔╝╚██████╔╝██║  ██║╚██████╔╝\",\n        \"╚═════╝  ╚═════╝ ╚═╝  ╚═╝ ╚═════╝ \",\n    ]\n    H = len(BORG_ART)\n    W = max(len(line) for line in BORG_ART)\n\n    def on_mount(self) -> None:\n        from .theme import theme\n\n        logo_color = theme.variables[\"logo-color\"]\n\n        lines = []\n        for line in self.BORG_ART:\n            lines.append(f\"[bold {logo_color}]{escape(line)}[/]\")\n        self.update(\"\\n\".join(lines))\n\n\nclass LogoPanel(Container):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._seed = random.randint(0, 1000000)  # nosec B311 - UI-only randomness, not for crypto\n\n    def compose(self) -> ComposeResult:\n        yield Starfield()\n        yield Logo(id=\"logo\")\n        yield Slogan(id=\"slogan\")\n        yield Pulsar()\n\n    def on_resize(self, event) -> None:\n        w, h = self.size\n        # Needs enough space to position reasonably\n        if w > 4 and h > 4:\n            random.seed(self._seed)\n\n            # Exclusion Zone Calculation\n            # --------------------------\n\n            # Logo top-left\n            logo_y = (h - Logo.H) // 2 - 1\n            logo_x = (w - Logo.W) // 2\n\n            # Slogan top-left\n            slogan_y = logo_y + Logo.H + 2\n            slogan_x = (w - Slogan.W) // 2\n\n            # Forbidden area\n            # --------------\n            # Combined rect over Logo and Slogan\n            f_y1 = logo_y\n            f_y2 = slogan_y + Slogan.H\n            f_x1 = min(logo_x, slogan_x)\n            f_x2 = max(logo_x + Logo.W, slogan_x + Slogan.W)\n\n            # Update Logo and Slogan position\n            # Note: In the overlay layer, widgets stack vertically.\n            # Logo is at y=0 (height Logo.H).\n            # Slogan is at y=Logo.H (height Slogan.H).\n            # Pulsar is at y=Logo.H+Slogan.H (height Pulsar.H)\n            # We must subtract these flow positions from the desired absolute positions.\n            self.query_one(Logo).styles.offset = (logo_x, logo_y)\n            self.query_one(Slogan).styles.offset = (slogan_x, slogan_y - Logo.H)\n\n            # Pulsar: styles.offset moves the top-left corner.\n            # So if offset is (px, py), it occupies x=[px, px+Pulsar.W), y=[py, py+Pulsar.H).\n\n            # Find a valid Pulsar position\n            for _ in range(20):\n                # Random position\n                max_x = max(0, w - Pulsar.W)\n                max_y = max(0, h - Pulsar.H)\n\n                px = random.randint(0, max_x)  # nosec B311 - visual placement randomness\n                py = random.randint(0, max_y)  # nosec B311 - visual placement randomness\n\n                # Pulsar Rect:\n                p_x1, p_y1 = px, py\n                p_x2, p_y2 = px + Pulsar.W, py + Pulsar.H\n\n                # Check intersection with forbidden rect\n                overlap_x = (p_x1 < f_x2) and (p_x2 > f_x1)\n                overlap_y = (p_y1 < f_y2) and (p_y2 > f_y1)\n\n                if overlap_x and overlap_y:\n                    continue  # Try again\n\n                # No overlap!\n                offset_x, offset_y = px, py - (Logo.H + Slogan.H)\n                break\n            else:\n                # Fallback if no safe spot found (e.g. screen too small):\n                # Place top-left or keep last valid. random 0,0 is safe-ish.\n                offset_x, offset_y = 0, 0 - (Logo.H + Slogan.H)\n            self.query_one(Pulsar).styles.offset = (offset_x, offset_y)\n\n\nclass SpeedSparkline(Static):\n    \"\"\"\n    Custom 4-line height sparkline.\n    \"\"\"\n\n    HISTORY_SIZE = 99\n    BLOCKS = [\".\", \" \", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"]\n\n    def __init__(self, data: list[float] = None, **kwargs):\n        super().__init__(**kwargs)\n        self._data = data or []\n\n    def update_data(self, data: list[float]):\n        self._data = data\n        self.refresh_chart()\n\n    def refresh_chart(self):\n        if not self._data:\n            self.update(\"\")\n            return\n\n        width = self.size.width or self.HISTORY_SIZE\n        # Slice data to width\n        dataset = self._data[-width:]\n        if not dataset:\n            self.update(\"\")\n            return\n\n        max_val = max(dataset) if dataset else 1.0\n        max_val = max(max_val, 1.0)  # Avoid div by zero\n\n        # We have 4 lines, each can take 8 levels. Total 32 levels.\n        # Normalize each data point to 0..32\n\n        lines = [[], [], [], []]\n\n        for val in dataset:\n            # Scale to 0-32\n            scaled = (val / max_val) * 32\n\n            # Generate 4 stacked chars\n            for i in range(4):\n                # i=0 is top line, i=3 is bottom line\n                # Thresholds: Top(24), Mid-High(16), Mid-Low(8), Low(0)\n                threshold = (3 - i) * 8\n                level = int(scaled - threshold)\n                level = max(0, min(8, level))\n                lines[i].append(self.BLOCKS[level])\n\n        # Join lines\n        rows = [\"\".join(line) for line in lines]\n        self.update(\"\\n\".join(rows))\n\n    def on_resize(self, event):\n        self.refresh_chart()\n"
  },
  {
    "path": "src/borg/compress.pyi",
    "content": "from typing import Any, Type, Dict, Tuple\n\ndef get_compressor(name: str, **kwargs) -> Any: ...\n\nclass Compressor:\n    def __init__(self, name: Any = ..., **kwargs) -> None: ...\n    def compress(self, meta: Dict, data: bytes) -> Tuple[Dict, bytes]: ...\n    def decompress(self, meta: Dict, data: bytes) -> Tuple[Dict, bytes]: ...\n    @staticmethod\n    def detect(data: bytes) -> Any: ...\n\nclass CompressorBase:\n    ID: bytes = ...\n    name: str = ...\n    @classmethod\n    def detect(self, data: bytes) -> bool: ...\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    def decide(self, data: bytes) -> Any: ...\n    def compress(self, data: bytes) -> bytes: ...\n    def decompress(self, data: bytes) -> bytes: ...\n\nclass Auto(CompressorBase):\n    def __init__(self, compressor: Any) -> None: ...\n\nclass DecidingCompressor(CompressorBase):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    def decide_compress(self, data: bytes) -> Any: ...\n\nclass CNONE(CompressorBase):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n\nclass ObfuscateSize(CompressorBase):\n    def __init__(self, level: int = ..., compressor: Any = ...) -> None: ...\n\nclass ZLIB_legacy(CompressorBase):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    level: int\n\nclass ZLIB(CompressorBase):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    level: int\n\nclass LZ4(DecidingCompressor):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n\nclass LZMA(DecidingCompressor):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    level: int\n\nclass ZSTD(DecidingCompressor):\n    def __init__(self, level: int = ..., **kwargs) -> None: ...\n    level: int\n\nLZ4_COMPRESSOR: Type[LZ4]\nNONE_COMPRESSOR: Type[CNONE]\n\nCOMPRESSOR_TABLE: dict\n"
  },
  {
    "path": "src/borg/compress.pyx",
    "content": "\"\"\"\nborg.compress\n=============\n\nCompression is applied to chunks after ID hashing (so the ID is a direct function of the\nplain chunk, compression is irrelevant to it), and of course before encryption.\n\nThe \"auto\" mode (e.g. --compression auto,lzma,4) is implemented as a meta Compressor,\nmeaning that Auto acts like a Compressor, but defers actual work to others (namely\nLZ4 as a heuristic whether compression is worth it, and the specified Compressor\nfor the actual compression).\n\nDecompression is normally handled through Compressor.decompress which will detect\nwhich compressor has been used to compress the data and dispatch to the correct\ndecompressor.\n\"\"\"\n\nimport math\nimport random\nfrom struct import Struct\nimport sys\nimport zlib\n\ntry:\n    import lzma\nexcept ImportError:\n    lzma = None\n\nfrom .constants import MAX_DATA_SIZE, ROBJ_FILE_STREAM\nfrom .helpers import Buffer, DecompressionError\nfrom .helpers.argparsing import ArgumentTypeError\n\nif sys.version_info >= (3, 14):\n    from compression import zstd\nelse:\n    from backports import zstd\n\ncdef extern from \"lz4.h\":\n    int LZ4_compress_default(const char* source, char* dest, int inputSize, int maxOutputSize) nogil\n    int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil\n    int LZ4_compressBound(int inputSize) nogil\n\nbuffer = Buffer(bytearray, size=0)\n\ncdef class CompressorBase:\n    \"\"\"\n    base class for all (de)compression classes,\n    also handles compression format auto detection and\n    adding/stripping the ID header (which enables auto-detection).\n    \"\"\"\n    ID = 0xFF  # reserved and not used\n               # overwrite with a unique 1-byte bytestring in child classes\n    name = 'baseclass'\n\n    @classmethod\n    def detect(cls, data):\n        return data and data[0] == cls.ID\n\n    def __init__(self, level=255, legacy_mode=False, **kwargs):\n        assert 0 <= level <= 255\n        self.level = level\n        self.legacy_mode = legacy_mode  # True: support prefixed ctype/clevel bytes\n\n    def decide(self, data):\n        \"\"\"\n        Return which compressor will perform the actual compression for *data*.\n\n        This exists for a very specific case: If borg recreate is instructed to recompress\n        using Auto compression it needs to determine the _actual_ target compression of a chunk\n        in order to detect whether it should be recompressed.\n\n        Any compressor may return a compressor other than *self*, like e.g. the CNONE compressor,\n        and should actually do so if *data* would become larger on compression.\n        \"\"\"\n        return self\n\n    def compress(self, meta, data):\n        \"\"\"\n        Compress *data* (bytes) and return compression metadata and compressed bytes.\n        \"\"\"\n        if not isinstance(data, bytes):\n            data = bytes(data)  # code below does not work with memoryview\n        if self.legacy_mode:\n            return None, bytes((self.ID, self.level)) + data\n        else:\n            meta[\"ctype\"] = self.ID\n            meta[\"clevel\"] = self.level\n            meta[\"csize\"] = len(data)\n            return meta, data\n\n    def decompress(self, meta, data):\n        \"\"\"\n        Decompress *data* (preferably a memoryview, bytes also acceptable) and return bytes result.\n\n        Legacy mode: The leading Compressor ID bytes need to be present.\n\n        Only handles input generated by _this_ Compressor - for a general purpose\n        decompression method see *Compressor.decompress*.\n        \"\"\"\n        if self.legacy_mode:\n            assert meta is None\n            meta = {}\n            meta[\"ctype\"] = data[0]\n            meta[\"clevel\"] = data[1]\n            meta[\"csize\"] = len(data)\n            return meta, data[2:]\n        else:\n            assert isinstance(meta, dict)\n            assert \"ctype\" in meta\n            assert \"clevel\" in meta\n            return meta, data\n\n    def check_fix_size(self, meta, data):\n        if \"size\" in meta:\n            assert meta[\"size\"] == len(data)\n        elif self.legacy_mode:\n            meta[\"size\"] = len(data)\n        else:\n            pass  # raise ValueError(\"size not present and not in legacy mode\")\n\ncdef class DecidingCompressor(CompressorBase):\n    \"\"\"\n    base class for (de)compression classes that (based on an internal _decide\n    method) decide whether and how to compress data.\n    \"\"\"\n    name = 'decidebaseclass'\n\n    def __init__(self, level=255, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)\n\n    def _decide(self, meta, data):\n        \"\"\"\n        Decides what to do with *data*. Returns (compressor, meta, compressed_data).\n\n        *compressed_data* can be the result of *data* being processed by *compressor*,\n        if that is generated as a side-effect of the decision process, or None otherwise.\n\n        This private method allows for more efficient implementation of compress()\n        and decide_compress() making use of *compressed_data*, if already generated.\n        \"\"\"\n        raise NotImplementedError\n\n    def decide(self, meta, data):\n        return self._decide(meta, data)[0]\n\n    def decide_compress(self, meta, data):\n        \"\"\"\n        Decides what to do with *data* and handle accordingly. Returns (compressor, compressed_data).\n\n        *compressed_data* is the result of *data* being processed by *compressor*.\n        \"\"\"\n        compressor, (meta, compressed_data) = self._decide(meta, data)\n\n        if compressed_data is None:\n            meta, compressed_data = compressor.compress(meta, data)\n\n        if compressor is self:\n            # call super class to add ID bytes\n            return self, super().compress(meta, compressed_data)\n\n        return compressor, (meta, compressed_data)\n\n    def compress(self, meta, data):\n        meta[\"size\"] = len(data)\n        return self.decide_compress(meta, data)[1]\n\nclass CNONE(CompressorBase):\n    \"\"\"\n    none - no compression, just pass through data\n    \"\"\"\n    ID = 0x00\n    name = 'none'\n\n    def __init__(self, level=255, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)  # no defined levels for CNONE, so just say \"unknown\"\n\n    def compress(self, meta, data):\n        meta[\"size\"] = len(data)\n        return super().compress(meta, data)\n\n    def decompress(self, meta, data):\n        meta, data = super().decompress(meta, data)\n        if not isinstance(data, bytes):\n            data = bytes(data)\n        self.check_fix_size(meta, data)\n        return meta, data\n\nclass LZ4(DecidingCompressor):\n    \"\"\"\n    raw LZ4 compression / decompression (liblz4).\n\n    Features:\n        - lz4 is super fast\n        - wrapper releases CPython's GIL to support multithreaded code\n        - uses safe lz4 methods that never go beyond the end of the output buffer\n    \"\"\"\n    ID = 0x01\n    name = 'lz4'\n\n    def __init__(self, level=255, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)  # no defined levels for LZ4, so just say \"unknown\"\n\n    def _decide(self, meta, idata):\n        \"\"\"\n        Decides what to do with *data*. Returns (compressor, lz4_data).\n\n        *lz4_data* is the LZ4 result if *compressor* is LZ4 as well, otherwise it is None.\n        \"\"\"\n        if not isinstance(idata, bytes):\n            idata = bytes(idata)  # code below does not work with memoryview\n        cdef int isize = len(idata)\n        cdef int osize\n        cdef char *source = idata\n        cdef char *dest\n        osize = LZ4_compressBound(isize)\n        buf = buffer.get(osize)\n        dest = <char *> buf\n        with nogil:\n            osize = LZ4_compress_default(source, dest, isize, osize)\n        if not osize:\n            raise Exception('lz4 compress failed')\n        # only compress if the result actually is smaller\n        if osize < isize:\n            return self, (meta, dest[:osize])\n        else:\n            return NONE_COMPRESSOR, (meta, None)\n\n    def decompress(self, meta, data):\n        meta, idata = super().decompress(meta, data)\n        if not isinstance(idata, bytes):\n            idata = bytes(idata)  # code below does not work with memoryview\n        cdef int isize = len(idata)\n        cdef int osize\n        cdef int rsize\n        cdef char *source = idata\n        cdef char *dest\n        # a bit more than 8MB is enough for the usual data sizes yielded by the chunker.\n        # allocate more if isize * 3 is already bigger, to avoid having to resize often.\n        osize = max(int(1.1 * 2**23), isize * 3)\n        while True:\n            try:\n                buf = buffer.get(osize)\n            except MemoryError:\n                raise DecompressionError('MemoryError')\n            dest = <char *> buf\n            with nogil:\n                rsize = LZ4_decompress_safe(source, dest, isize, osize)\n            if rsize >= 0:\n                break\n            if osize > 2 ** 27:  # 128MiB (should be enough, considering max. repo obj size and very good compression)\n                # this is insane, get out of here\n                raise DecompressionError('lz4 decompress failed')\n            # likely the buffer was too small, get a bigger one:\n            osize = int(1.5 * osize)\n        data = dest[:rsize]\n        self.check_fix_size(meta, data)\n        return meta, data\n\nclass LZMA(DecidingCompressor):\n    \"\"\"\n    lzma compression / decompression\n    \"\"\"\n    ID = 0x02\n    name = 'lzma'\n\n    def __init__(self, level=6, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)\n        self.level = level\n        if lzma is None:\n            raise ValueError('No lzma support found.')\n\n    def _decide(self, meta, data):\n        \"\"\"\n        Decides what to do with *data*. Returns (compressor, lzma_data).\n\n        *lzma_data* is the LZMA result if *compressor* is LZMA as well, otherwise it is None.\n        \"\"\"\n        # we do not need integrity checks in lzma, we do that already\n        lzma_data = lzma.compress(data, preset=self.level, check=lzma.CHECK_NONE)\n        if len(lzma_data) < len(data):\n            return self, (meta, lzma_data)\n        else:\n            return NONE_COMPRESSOR, (meta, None)\n\n    def decompress(self, meta, data):\n        meta, data = super().decompress(meta, data)\n        try:\n            data = lzma.decompress(data)\n            self.check_fix_size(meta, data)\n            return meta, data\n        except lzma.LZMAError as e:\n            raise DecompressionError(str(e)) from None\n\nclass ZSTD(DecidingCompressor):\n    \"\"\"\n    zstd compression / decompression (python stdlib (python >= 3.14))\n    \"\"\"\n    ID = 0x03\n    name = 'zstd'\n\n    def __init__(self, level=3, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)\n        self.level = level\n\n    def _decide(self, meta, data):\n        zstd_data = zstd.compress(data, self.level)\n        if len(zstd_data) < len(data):\n            return self, (meta, zstd_data)\n        else:\n            return NONE_COMPRESSOR, (meta, None)\n    \n    def decompress(self, meta, data):\n        meta, data = super().decompress(meta, data)\n        try:\n            data = zstd.decompress(data)\n            self.check_fix_size(meta, data)\n            return meta, data\n        except zstd.ZstdError as e:\n            raise DecompressionError(str(e)) from None\n\nclass ZLIB(DecidingCompressor):\n    \"\"\"\n    zlib compression / decompression (python stdlib)\n    \"\"\"\n    ID = 0x05\n    name = 'zlib'\n\n    def __init__(self, level=6, legacy_mode=False, **kwargs):\n        super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)\n        self.level = level\n\n    def _decide(self, meta, data):\n        \"\"\"\n        Decides what to do with *data*. Returns (compressor, zlib_data).\n\n        *zlib_data* is the ZLIB result if *compressor* is ZLIB as well, otherwise it is None.\n        \"\"\"\n        zlib_data = zlib.compress(data, self.level)\n        if len(zlib_data) < len(data):\n            return self, (meta, zlib_data)\n        else:\n            return NONE_COMPRESSOR, (meta, None)\n\n    def decompress(self, meta, data):\n        meta, data = super().decompress(meta, data)\n        try:\n            data = zlib.decompress(data)\n            self.check_fix_size(meta, data)\n            return meta, data\n        except zlib.error as e:\n            raise DecompressionError(str(e)) from None\n\nclass ZLIB_legacy(CompressorBase):\n    \"\"\"\n    zlib compression / decompression (python stdlib)\n\n    Note: This is the legacy ZLIB support as used by borg < 1.3.\n          It still suffers from attic *only* supporting zlib and not having separate\n          ID bytes to differentiate between differently compressed chunks.\n          This just works because zlib compressed stuff always starts with 0x.8.. bytes.\n          Newer borg uses the ZLIB class that has separate ID bytes (as all the other\n          compressors) and does not need this hack.\n    \"\"\"\n    ID = 0x08  # not used here, see detect()\n    # avoid all 0x.8 IDs elsewhere!\n    name = 'zlib_legacy'\n\n    @classmethod\n    def detect(cls, data):\n        # matches misc. patterns 0x.8.. used by zlib\n        cmf, flg = data[:2]\n        is_deflate = cmf & 0x0f == 8\n        check_ok = (cmf * 256 + flg) % 31 == 0\n        return check_ok and is_deflate\n\n    def __init__(self, level=6, **kwargs):\n        super().__init__(level=level, **kwargs)\n        self.level = level\n\n    def compress(self, meta, data):\n        # note: for compatibility no super call, do not add ID bytes\n        return None, zlib.compress(data, self.level)\n\n    def decompress(self, meta, data):\n        # note: for compatibility no super call, do not strip ID bytes\n        assert self.legacy_mode  # only borg 1.x repos have the legacy ZLIB format\n        assert meta is None\n        meta = {}\n        meta[\"ctype\"] = ZLIB.ID  # change to non-legacy ZLIB id\n        meta[\"clevel\"] = 255  # we do not know the compression level\n        meta[\"csize\"] = len(data)\n        try:\n            data = zlib.decompress(data)\n            self.check_fix_size(meta, data)\n            return meta, data\n        except zlib.error as e:\n            raise DecompressionError(str(e)) from None\n\nclass Auto(CompressorBase):\n    \"\"\"\n    Meta-Compressor that decides which compression to use based on LZ4's ratio.\n\n    As a meta-Compressor the actual compression is deferred to other Compressors,\n    therefore this Compressor has no ID, no detect() and no decompress().\n    \"\"\"\n\n    ID = None\n    name = 'auto'\n\n    def __init__(self, compressor):\n        super().__init__()\n        self.compressor = compressor\n\n    def _decide(self, meta, data):\n        \"\"\"\n        Decides what to do with *data*. Returns (compressor, compressed_data).\n\n        *compressor* is the compressor that is decided to be best suited to compress\n        *data*, *compressed_data* is the result of *data* being compressed by a\n        compressor, which may or may not be *compressor*!\n\n        There are three possible outcomes of the decision process:\n        * *data* compresses well enough for trying the more expensive compressor\n          set on instantiation to make sense.\n          In this case, (expensive_compressor_class, lz4_compressed_data) is\n          returned.\n        * *data* compresses only slightly using the LZ4 compressor, thus trying\n          the more expensive compressor for potentially little gain does not\n          make sense.\n          In this case, (LZ4_COMPRESSOR, lz4_compressed_data) is returned.\n        * *data* does not compress at all using LZ4, in this case\n          (NONE_COMPRESSOR, none_compressed_data) is returned.\n\n        Note: While it makes no sense, the expensive compressor may well be set\n        to the LZ4 compressor.\n        \"\"\"\n        compressor, (meta, compressed_data) = LZ4_COMPRESSOR.decide_compress(meta, data)\n        # compressed_data includes the compression type header, while data does not yet\n        ratio = len(compressed_data) / (len(data) + 2)\n        if ratio < 0.97:\n            return self.compressor, (meta, compressed_data)\n        else:\n            return compressor, (meta, compressed_data)\n\n    def decide(self, meta, data):\n        return self._decide(meta, data)[0]\n\n    def compress(self, meta, data):\n        def get_meta(from_meta, to_meta):\n            for key in \"ctype\", \"clevel\", \"csize\":\n                if key in from_meta:\n                    to_meta[key] = from_meta[key]\n\n        compressor, (cheap_meta, cheap_compressed_data) = self._decide(dict(meta), data)\n        if compressor in (LZ4_COMPRESSOR, NONE_COMPRESSOR):\n            # we know that trying to compress with expensive compressor is likely pointless,\n            # so we fallback to return the cheap compressed data.\n            get_meta(cheap_meta, meta)\n            return meta, cheap_compressed_data\n        # if we get here, the decider decided to try the expensive compressor.\n        # we also know that the compressed data returned by the decider is lz4 compressed.\n        expensive_meta, expensive_compressed_data = compressor.compress(dict(meta), data)\n        ratio = len(expensive_compressed_data) / len(cheap_compressed_data)\n        if ratio < 0.99:\n            # the expensive compressor managed to squeeze the data significantly better than lz4.\n            get_meta(expensive_meta, meta)\n            return meta, expensive_compressed_data\n        else:\n            # otherwise let's just store the lz4 data, which decompresses extremely fast.\n            get_meta(cheap_meta, meta)\n            return meta, cheap_compressed_data\n\n    def decompress(self, meta, data):\n        raise NotImplementedError\n\n    @classmethod\n    def detect(cls, data):\n        raise NotImplementedError\n\nclass ObfuscateSize(CompressorBase):\n    \"\"\"\n    Meta-Compressor that obfuscates the compressed data size.\n    \"\"\"\n    ID = 0x04\n    name = 'obfuscate'\n\n    header_fmt = Struct('<I')\n    header_len = len(header_fmt.pack(0))\n\n    def __init__(self, level=None, compressor=None, legacy_mode=False):\n        super().__init__(level=level, legacy_mode=legacy_mode)  # data will be encrypted, so we can tell the level\n        self.compressor = compressor\n        self.level = level\n        if level is None:\n            pass  # decompression\n        elif 1 <= level <= 6:\n            self._obfuscate = self._relative_random_reciprocal_obfuscate\n            self.factor = 0.001 * 10 ** level\n            self.min_r = 0.0001\n        elif 110 <= level <= 123:\n            self._obfuscate = self._random_padding_obfuscate\n            self.max_padding_size = 2 ** (level - 100)  # 1kiB .. 8MiB\n        elif level == 250:  # Padmé\n            self._obfuscate = self._padme_obfuscate\n\n    def _obfuscate(self, compr_size):\n        # implementations need to return the size of obfuscation data,\n        # that the caller shall add.\n        raise NotImplementedError\n\n    def _relative_random_reciprocal_obfuscate(self, compr_size):\n        # effect for SPEC 1:\n        # f = 0.01 .. 0.1 for r in 1.0 .. 0.1 == in 90% of cases\n        # f = 0.1 .. 1.0 for r in 0.1 .. 0.01 == in 9% of cases\n        # f = 1.0 .. 10.0 for r in 0.01 .. 0.001 = in 0.9% of cases\n        # f = 10.0 .. 100.0 for r in 0.001 .. 0.0001 == in 0.09% of cases\n        r = max(self.min_r, random.random())  # 0..1, but don't get too close to 0\n        f = self.factor / r\n        return int(compr_size * f)\n\n    def _random_padding_obfuscate(self, compr_size):\n        return int(self.max_padding_size * random.random())\n\n    def _padme_obfuscate(self, compr_size):\n        if compr_size < 2:\n            return 0\n\n        E = math.floor(math.log2(compr_size))  # Get exponent (power of 2)\n        S = math.floor(math.log2(E)) + 1       # Second log component\n        lastBits = E - S                       # Bits to be zeroed\n        bitMask = (2 ** lastBits - 1)          # Mask for rounding\n\n        padded_size = (compr_size + bitMask) & ~bitMask  # Apply rounding\n\n        return padded_size - compr_size  # Return only the additional padding size\n\n    def compress(self, meta, data):\n        assert not self.legacy_mode  # we never call this in legacy mode\n        meta, compressed_data = self.compressor.compress(meta, data)  # compress data\n        compr_size = len(compressed_data)\n        assert \"csize\" in meta, repr(meta)\n        meta[\"psize\"] = meta[\"csize\"]  # psize (payload size) is the csize (compressed size) of the inner compressor\n        # we only want to obfuscate the size of file content chunks (ROBJ_FILE_STREAM repo objects).\n        # for all other types of repo objects (e.g. borg metadata chunks), add 0 length:\n        addtl_size = self._obfuscate(compr_size) if meta[\"type\"] == ROBJ_FILE_STREAM else 0\n        addtl_size = max(0, addtl_size)  # we can only make it longer, not shorter!\n        addtl_size = min(MAX_DATA_SIZE - 1024 - compr_size, addtl_size)  # stay away from MAX_DATA_SIZE\n        trailer = bytes(addtl_size)\n        obfuscated_data = compressed_data + trailer\n        meta[\"csize\"] = len(obfuscated_data)  # csize is the overall output size of this \"obfuscation compressor\"\n        meta[\"olevel\"] = self.level  # remember the obfuscation level, useful for repo-compress\n        return meta, obfuscated_data  # for borg2 it is enough that we have the payload size in meta[\"psize\"]\n\n    def decompress(self, meta, data):\n        assert self.legacy_mode  # borg2 never dispatches to this, only used for legacy mode\n        meta, obfuscated_data = super().decompress(meta, data)  # remove obfuscator ID header\n        compr_size = self.header_fmt.unpack(obfuscated_data[0:self.header_len])[0]\n        compressed_data = obfuscated_data[self.header_len:self.header_len+compr_size]\n        if self.compressor is None:\n            compressor_cls = Compressor.detect(compressed_data)[0]\n            self.compressor = compressor_cls()\n        return self.compressor.decompress(meta, compressed_data)  # decompress data\n\n# Maps valid compressor names to their class\nCOMPRESSOR_TABLE = {\n    CNONE.name: CNONE,\n    LZ4.name: LZ4,\n    ZLIB.name: ZLIB,\n    ZLIB_legacy.name: ZLIB_legacy,\n    LZMA.name: LZMA,\n    Auto.name: Auto,\n    ZSTD.name: ZSTD,\n    ObfuscateSize.name: ObfuscateSize,\n}\n# List of possible compression types. Does not include Auto, since it is a meta-Compressor.\nCOMPRESSOR_LIST = [LZ4, ZSTD, CNONE, ZLIB, ZLIB_legacy, LZMA, ObfuscateSize, ]  # check fast stuff first\n\ndef get_compressor(name, **kwargs):\n    cls = COMPRESSOR_TABLE[name]\n    return cls(**kwargs)\n\n# compressor instances to be used by all other compressors\nNONE_COMPRESSOR = get_compressor('none')\nLZ4_COMPRESSOR = get_compressor('lz4')\n\nclass Compressor:\n    \"\"\"\n    compresses using a compressor with given name and parameters\n    decompresses everything we can handle (autodetect)\n    \"\"\"\n    def __init__(self, name='null', **kwargs):\n        self.params = kwargs\n        self.compressor = get_compressor(name, **self.params)\n\n    def compress(self, meta, data):\n        return self.compressor.compress(meta, data)\n\n    def decompress(self, meta, data):\n        if self.compressor.legacy_mode:\n            hdr = data[:2]\n        else:\n            ctype = meta[\"ctype\"]\n            clevel = meta[\"clevel\"]\n            hdr = bytes((ctype, clevel))\n        compressor_cls = self.detect(hdr)[0]\n        return compressor_cls(**self.params).decompress(meta, data)\n\n    @staticmethod\n    def detect(data):\n        hdr = bytes(data[:2])  # detect() does not work with memoryview\n        level = hdr[1]  # usually the level, but not for zlib_legacy\n        for cls in COMPRESSOR_LIST:\n            if cls.detect(hdr):\n                return cls, (255 if cls.name == 'zlib_legacy' else level)\n        else:\n            raise ValueError('No decompressor for this data found: %r.', data[:2])\n"
  },
  {
    "path": "src/borg/conftest.py",
    "content": "import os\nimport shutil\n\nimport pytest\n\nif hasattr(pytest, \"register_assert_rewrite\"):\n    pytest.register_assert_rewrite(\"borg.testsuite\")\n\n# Ensure that the loggers exist for all tests\nfrom borg.logger import setup_logging  # noqa: E402\n\nsetup_logging()\n\nfrom borg.archiver import Archiver  # noqa: E402\nfrom borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3, has_mfusepy  # noqa: E402\nfrom borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported  # noqa: E402\nfrom borg.testsuite.archiver import BORG_EXES\nfrom borg.testsuite.platform.platform_test import fakeroot_detected  # noqa: E402\n\n\n@pytest.fixture(autouse=True)\ndef clean_env(tmpdir_factory, monkeypatch):\n    # also avoid to use anything from the outside environment:\n    keys = [key for key in os.environ if key.startswith(\"BORG_\") and key not in (\"BORG_FUSE_IMPL\",)]\n    for key in keys:\n        monkeypatch.delenv(key, raising=False)\n    # avoid that we access / modify the user's normal .config / .cache directory:\n    base_dir = tmpdir_factory.mktemp(\"borg-base-dir\")\n    monkeypatch.setenv(\"BORG_BASE_DIR\", str(base_dir))\n    # Speed up tests\n    monkeypatch.setenv(\"BORG_TESTONLY_WEAKEN_KDF\", \"1\")\n    monkeypatch.setenv(\"BORG_STORE_DATA_LEVELS\", \"0\")  # flat storage for few objects\n    yield\n    shutil.rmtree(str(base_dir), ignore_errors=True)  # clean up\n\n\ndef pytest_report_header(config, start_path):\n    tests = {\n        \"BSD flags\": has_lchflags,\n        \"llfuse\": has_llfuse,\n        \"pyfuse3\": has_pyfuse3,\n        \"mfusepy\": has_mfusepy,\n        \"root\": not fakeroot_detected(),\n        \"symlinks\": are_symlinks_supported(),\n        \"hardlinks\": are_hardlinks_supported(),\n        \"atime/mtime\": is_utime_fully_supported(),\n        \"modes\": \"BORG_TESTS_IGNORE_MODES\" not in os.environ,\n    }\n    enabled = []\n    disabled = []\n    for test in tests:\n        if tests[test]:\n            enabled.append(test)\n        else:\n            disabled.append(test)\n    output = \"Tests enabled: \" + \", \".join(enabled) + \"\\n\"\n    output += \"Tests disabled: \" + \", \".join(disabled)\n    return output\n\n\n@pytest.fixture()\ndef set_env_variables():\n    os.environ[\"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\"] = \"YES\"\n    os.environ[\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\"] = \"YES\"\n    os.environ[\"BORG_PASSPHRASE\"] = \"waytooeasyonlyfortests\"  # nosec B105\n    os.environ[\"BORG_SELFTEST\"] = \"disabled\"\n\n\n@pytest.fixture(scope=\"session\")\ndef backup_files(tmp_path_factory):\n    # create a relatively simple / minimal set of test files\n    path = tmp_path_factory.mktemp(\"backup\")\n    (path / \"empty\").write_bytes(b\"\")\n    (path / \"dir1\").mkdir()\n    (path / \"dir1\" / \"text.txt\").write_text(\"text content\")\n    (path / \"dir2\").mkdir()\n    (path / \"dir2\" / \"binary.bin\").write_bytes(b\"\\x00\\x01\\x02\\x03\")\n    return str(path)\n\n\nclass ArchiverSetup:\n    EXE: str = None  # python source based\n    FORK_DEFAULT = False\n    BORG_EXES: list[str] = []\n\n    def __init__(self):\n        self.archiver = None\n        self.tmpdir: str | None = None\n        self.repository_path: str | None = None\n        self.repository_location: str | None = None\n        self.input_path: str | None = None\n        self.output_path: str | None = None\n        self.keys_path: str | None = None\n        self.cache_path: str | None = None\n        self.exclude_file_path: str | None = None\n        self.patterns_file_path: str | None = None\n\n    def get_kind(self) -> str:\n        if self.repository_location.startswith(\"ssh://__testsuite__\"):\n            return \"remote\"\n        elif self.EXE == \"borg.exe\":\n            return \"binary\"\n        else:\n            return \"local\"\n\n\n@pytest.fixture()\ndef archiver(tmp_path, set_env_variables):\n    archiver = ArchiverSetup()\n    archiver.archiver = not archiver.FORK_DEFAULT and Archiver() or None\n    archiver.tmpdir = tmp_path\n    archiver.repository_path = os.fspath(tmp_path / \"repository\")\n    archiver.repository_location = archiver.repository_path\n    archiver.input_path = os.fspath(tmp_path / \"input\")\n    archiver.output_path = os.fspath(tmp_path / \"output\")\n    archiver.keys_path = os.fspath(tmp_path / \"keys\")\n    archiver.cache_path = os.fspath(tmp_path / \"cache\")\n    archiver.exclude_file_path = os.fspath(tmp_path / \"excludes\")\n    archiver.patterns_file_path = os.fspath(tmp_path / \"patterns\")\n    os.environ[\"BORG_KEYS_DIR\"] = archiver.keys_path\n    os.environ[\"BORG_CACHE_DIR\"] = archiver.cache_path\n    os.mkdir(archiver.input_path)\n    # avoid troubles with fakeroot / FUSE:\n    os.chmod(archiver.input_path, 0o777)  # nosec B103\n    os.mkdir(archiver.output_path)\n    os.mkdir(archiver.keys_path)\n    os.mkdir(archiver.cache_path)\n    with open(archiver.exclude_file_path, \"wb\") as fd:\n        fd.write(b\"input/file2\\n# A comment line, then a blank line\\n\\n\")\n    with open(archiver.patterns_file_path, \"wb\") as fd:\n        fd.write(b\"+input/file_important\\n- input/file*\\n# A comment line, then a blank line\\n\\n\")\n    old_wd = os.getcwd()\n    os.chdir(archiver.tmpdir)\n    yield archiver\n    os.chdir(old_wd)\n    shutil.rmtree(archiver.tmpdir, ignore_errors=True)  # clean up\n\n\n@pytest.fixture()\ndef remote_archiver(archiver):\n    archiver.repository_location = \"ssh://__testsuite__/\" + str(archiver.repository_path)\n    yield archiver\n\n\n@pytest.fixture()\ndef binary_archiver(archiver):\n    if \"binary\" not in BORG_EXES:\n        pytest.skip(\"No borg.exe binary available\")\n    archiver.EXE = \"borg.exe\"\n    archiver.FORK_DEFAULT = True\n    yield archiver\n"
  },
  {
    "path": "src/borg/constants.py",
    "content": "# this set must be kept complete, otherwise the RobustUnpacker might malfunction:\n# fmt: off\nITEM_KEYS = frozenset(['path', 'source', 'target', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', 'hlid',\n                       'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size', 'inode',\n                       'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended',\n                       'part'])\n# fmt: on\n\n# this is the set of keys that are always present in items:\nREQUIRED_ITEM_KEYS = frozenset([\"path\", \"mtime\"])\n\n# this set must be kept complete, otherwise rebuild_manifest might malfunction:\n# fmt: off\nARCHIVE_KEYS = frozenset(['version', 'name', 'hostname', 'username',\n                          'time',  # v2+ archives AND borg 1.x archives\n                          'time_end',  # only legacy borg 1.x\n                          'start', 'end',  # v2+ archives\n                          'tags',  # v2+ archives\n                          'items',  # legacy v1 archives\n                          'item_ptrs',  # v2+ archives\n                          'comment', 'chunker_params',\n                          'command_line', 'recreate_command_line',  # v2+ archives\n                          'cmdline', 'recreate_cmdline',  # legacy\n                          'recreate_source_id', 'recreate_args', 'recreate_partial_chunks',  # used in 1.1.0b1 .. b2\n                          'size', 'nfiles',\n                          'size_parts', 'nfiles_parts',  # legacy v1 archives\n                          'cwd',\n                          ])\n# fmt: on\n\n# this is the set of keys that are always present in archives:\nREQUIRED_ARCHIVE_KEYS = frozenset([\"version\", \"name\", \"item_ptrs\", \"command_line\", \"time\"])\n\n# default umask, overridden by --umask, defaults to read/write only for owner\nUMASK_DEFAULT = 0o077\n\n# default file mode to store stdin data, defaults to read/write for owner and group\n# forcing to 0o100XXX later\nSTDIN_MODE_DEFAULT = 0o660\n\n# RepoObj types\nROBJ_MANIFEST = \"M\"  # Manifest (directory of archives, other metadata) object\nROBJ_ARCHIVE_META = \"A\"  # main archive metadata object\nROBJ_ARCHIVE_CHUNKIDS = \"C\"  # objects with a list of archive metadata stream chunkids\nROBJ_ARCHIVE_STREAM = \"S\"  # archive metadata stream chunk (containing items)\nROBJ_FILE_STREAM = \"F\"  # file content stream chunk (containing user data)\nROBJ_DONTCARE = \"*\"  # used to parse without type assertion (= accept any type)\n\n# in borg < 1.3, this has been defined like this:\n# 20 MiB minus 41 bytes for a PUT header (because the \"size\" field in the Repository includes\n# the header, and the total size was set to precisely 20 MiB for borg < 1.3).\nMAX_DATA_SIZE = 20971479\n\n# MAX_OBJECT_SIZE = MAX_DATA_SIZE + len(PUT2 header)\n# note: for borg >= 1.3, this makes the MAX_OBJECT_SIZE grow slightly over the precise 20 MiB used by\n# borg < 1.3, but this is not expected to cause any issues.\nMAX_OBJECT_SIZE = MAX_DATA_SIZE + 41 + 8  # see assertion at end of repository module\n\n# How many segment files Borg puts into a single directory by default.\nDEFAULT_SEGMENTS_PER_DIR = 1000\n\n# A large, but not unreasonably large segment size. Always less than 2 GiB (for legacy filesystems). We choose\n# 500 MiB, which means that no indirection from the inode is needed for typical Linux filesystems.\n# Note that this is a soft limit and can be exceeded (worst case) by a full maximum chunk size and some metadata\n# bytes. That's why it's 500 MiB instead of 512 MiB.\nDEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024\n\n# repo config max_segment_size value must be below this limit to stay within uint32 offsets:\nMAX_SEGMENT_SIZE_LIMIT = 2**32 - MAX_OBJECT_SIZE\n\n# How many metadata stream chunk IDs do we store in a \"pointer chunk\" of the ArchiveItem.item_ptrs list?\nIDS_PER_CHUNK = MAX_DATA_SIZE // 40\n\n# have one all-zero bytes object\n# we use it in all places where we need to detect or create all-zero buffers\nzeros = bytes(MAX_DATA_SIZE)\n\n# borg.remote read() buffer size\nBUFSIZE = 10 * 1024 * 1024\n\n# To use a safe, limited unpacker, we need to set an upper limit to the archive count in the manifest.\n# this does not mean that you can always really reach that number, because it also needs to be less than\n# MAX_DATA_SIZE or it will trigger the check for that.\nMAX_ARCHIVES = 400000\n\n# repo.list() result count limit used by the Borg client\nLIST_SCAN_LIMIT = 100000\n\nFD_MAX_AGE = 4 * 60  # 4 minutes\n\n# Some bounds on segment / segment_dir indexes\nMIN_SEGMENT_INDEX = 0\nMAX_SEGMENT_INDEX = 2**32 - 1\nMIN_SEGMENT_DIR_INDEX = 0\nMAX_SEGMENT_DIR_INDEX = 2**32 - 1\n\n# chunker algorithms\nCH_BUZHASH = \"buzhash\"\nCH_BUZHASH64 = \"buzhash64\"\nCH_FIXED = \"fixed\"\nCH_FAIL = \"fail\"\n\n# buzhash chunker params\nCHUNK_MIN_EXP = 19  # 2**19 == 512 KiB\nCHUNK_MAX_EXP = 23  # 2**23 == 8 MiB\nHASH_WINDOW_SIZE = 0xFFF  # 4095 B\nHASH_MASK_BITS = 21  # results in ~2 MiB chunks statistically\n\n# defaults, use --chunker-params to override\nCHUNKER_PARAMS = (CH_BUZHASH, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE)\nCHUNKER64_PARAMS = (CH_BUZHASH64, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE)\n\n# chunker params for the items metadata stream, finer granularity\nITEMS_CHUNKER_PARAMS = (CH_BUZHASH, 15, 19, 17, HASH_WINDOW_SIZE)\n\n# normal on-disk data, allocated (but not written, all zeros), not allocated hole (all zeros)\nCH_DATA, CH_ALLOC, CH_HOLE = 0, 1, 2\n\n# operating mode of the files cache (for fast skipping of unchanged files)\nFILES_CACHE_MODE_UI_DEFAULT = \"ctime,size,inode\"  # default for \"borg create\" command (CLI UI)\nFILES_CACHE_MODE_DISABLED = \"d\"  # most borg commands do not use the files cache at all (disable)\n\n# account for clocks being slightly out-of-sync, timestamps granularity.\n# we can't go much higher here (like e.g. to 2s) without causing issues.\nTIME_DIFFERS1_NS = 20000000\n\n# similar to above, but for bigger granularity / clock differences\nTIME_DIFFERS2_NS = 3000000000\n\n# tar related\nSCHILY_XATTR = \"SCHILY.xattr.\"  # xattr key prefix in tar PAX headers\nSCHILY_ACL_ACCESS = \"SCHILY.acl.access\"  # POSIX access ACL in tar PAX headers\nSCHILY_ACL_DEFAULT = \"SCHILY.acl.default\"  # POSIX default ACL in tar PAX headers\n\n# special tags\n# @PROT protects archives against accidental deletion or modification by delete, prune, or recreate.\nSPECIAL_TAGS = frozenset([\"@PROT\"])\n\n# return codes returned by Borg command\nEXIT_SUCCESS = 0  # everything done, no problems\nEXIT_WARNING = 1  # reached normal end of operation, but there were issues (generic warning)\nEXIT_ERROR = 2  # terminated abruptly, did not reach end of operation (generic error)\nEXIT_ERROR_BASE = 3  # specific error codes are 3..99 (enabled by BORG_EXIT_CODES=modern)\nEXIT_WARNING_BASE = 100  # specific warning codes are 100..127 (enabled by BORG_EXIT_CODES=modern)\nEXIT_SIGNAL_BASE = 128  # terminated due to signal, rc = 128 + sig_no\n\nISO_FORMAT_NO_USECS = \"%Y-%m-%dT%H:%M:%S\"\nISO_FORMAT = ISO_FORMAT_NO_USECS + \".%f\"\n\nDASHES = \"-\" * 78\n\nPBKDF2_ITERATIONS = 100000\n\n# https://www.rfc-editor.org/rfc/rfc9106.html#section-4-6.2\nARGON2_ARGS = {\"time_cost\": 3, \"memory_cost\": 2**16, \"parallelism\": 4, \"type\": \"id\"}\nARGON2_SALT_BYTES = 16\n\n# Maps the CLI argument to our internal identifier for the format\nKEY_ALGORITHMS = {\n    # encrypt-and-MAC, kdf: PBKDF2(HMAC−SHA256), encryption: AES256-CTR, authentication: HMAC-SHA256\n    \"pbkdf2\": \"sha256\",\n    # encrypt-then-MAC, kdf: argon2, encryption: chacha20, authentication: poly1305\n    \"argon2\": \"argon2 chacha20-poly1305\",\n}\n\n\nclass KeyBlobStorage:\n    NO_STORAGE = \"no_storage\"\n    KEYFILE = \"keyfile\"\n    REPO = \"repository\"\n\n\nclass KeyType:\n    # legacy crypto\n    # upper 4 bits are ciphersuite, 0 == legacy AES-CTR\n    KEYFILE = 0x00\n    # repos with PASSPHRASE mode could not be created any more since borg 1.0, see #97.\n    # in borg 2. all of its code and also the \"borg key migrate-to-repokey\" command was removed.\n    # if you still need to, you can use \"borg key migrate-to-repokey\" with borg 1.0, 1.1 and 1.2.\n    # Nowadays, we just dispatch this to RepoKey and assume the passphrase was migrated to a repokey.\n    PASSPHRASE = 0x01  # legacy, borg < 1.0\n    PLAINTEXT = 0x02\n    REPO = 0x03\n    BLAKE2KEYFILE = 0x04\n    BLAKE2REPO = 0x05\n    BLAKE2AUTHENTICATED = 0x06\n    AUTHENTICATED = 0x07\n    # new crypto\n    # upper 4 bits are ciphersuite, lower 4 bits are keytype\n    AESOCBKEYFILE = 0x10\n    AESOCBREPO = 0x11\n    CHPOKEYFILE = 0x20\n    CHPOREPO = 0x21\n    BLAKE2AESOCBKEYFILE = 0x30\n    BLAKE2AESOCBREPO = 0x31\n    BLAKE2CHPOKEYFILE = 0x40\n    BLAKE2CHPOREPO = 0x41\n\n\nCACHE_TAG_NAME = \"CACHEDIR.TAG\"\nCACHE_TAG_CONTENTS = b\"Signature: 8a477f597d28d172789f06886806bc55\"\n\nREPOSITORY_README = \"\"\"This is a Borg Backup repository.\nSee https://borgbackup.readthedocs.io/\n\"\"\"\n\nCACHE_README = \"\"\"This is a Borg Backup cache.\nSee https://borgbackup.readthedocs.io/\n\"\"\"\n"
  },
  {
    "path": "src/borg/crypto/__init__.py",
    "content": ""
  },
  {
    "path": "src/borg/crypto/file_integrity.py",
    "content": "import hashlib\nimport io\nimport json\nfrom hmac import compare_digest\nfrom collections.abc import Callable\nfrom pathlib import Path\n\nfrom xxhash import xxh64\n\nfrom ..helpers import IntegrityError\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass FileLikeWrapper:\n    def __init__(self, fd):\n        self.fd = fd\n\n    def __enter__(self):\n        self.fd.__enter__()\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.fd.__exit__(exc_type, exc_val, exc_tb)\n\n    def tell(self):\n        return self.fd.tell()\n\n    def seek(self, offset, whence=io.SEEK_SET):\n        return self.fd.seek(offset, whence)\n\n    def write(self, data):\n        return self.fd.write(data)\n\n    def read(self, n=None):\n        return self.fd.read(n)\n\n    def flush(self):\n        self.fd.flush()\n\n    def fileno(self):\n        return self.fd.fileno()\n\n\nclass FileHashingWrapper(FileLikeWrapper):\n    \"\"\"\n    Wrapper for file-like objects that computes a hash on-the-fly while reading/writing.\n\n    WARNING: Seeks should only be used to query the size of the file, not\n    to skip data, because skipped data is not read and not hashed into the digest.\n\n    Similarly, skipping while writing to create sparse files is not supported.\n\n    Data has to be read/written in a symmetric fashion, otherwise different\n    digests will be generated.\n\n    Note: When used as a context manager read/write operations outside the enclosed scope\n    are illegal.\n    \"\"\"\n\n    ALGORITHM: str = None\n    FACTORY: Callable = None\n\n    def __init__(self, backing_fd, write):\n        super().__init__(backing_fd)\n        self.writing = write\n        self.hash = self.FACTORY()\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if exc_type is None:\n            self.hash_length()\n        super().__exit__(exc_type, exc_val, exc_tb)\n\n    def write(self, data):\n        \"\"\"\n        Write *data* to backing file and update internal state.\n        \"\"\"\n        n = super().write(data)\n        self.hash.update(data)\n        return n\n\n    def read(self, n=None):\n        \"\"\"\n        Read *data* from backing file (*n* has the usual meaning) and update internal state.\n        \"\"\"\n        data = super().read(n)\n        self.hash.update(data)\n        return data\n\n    def hexdigest(self):\n        \"\"\"\n        Return current digest bytes as a hex string.\n\n        Note: This can be called multiple times.\n        \"\"\"\n        return self.hash.hexdigest()\n\n    def update(self, data: bytes):\n        self.hash.update(data)\n\n    def hash_length(self, seek_to_end=False):\n        if seek_to_end:\n            # Add length of file to the hash to avoid problems if only a prefix is read.\n            self.seek(0, io.SEEK_END)\n        self.hash.update(str(self.tell()).encode())\n\n\nclass SHA512FileHashingWrapper(FileHashingWrapper):\n    ALGORITHM = \"SHA512\"\n    FACTORY = hashlib.sha512\n\n\nclass XXH64FileHashingWrapper(FileHashingWrapper):\n    ALGORITHM = \"XXH64\"\n    FACTORY = xxh64\n\n\nSUPPORTED_ALGORITHMS = {\n    SHA512FileHashingWrapper.ALGORITHM: SHA512FileHashingWrapper,\n    XXH64FileHashingWrapper.ALGORITHM: XXH64FileHashingWrapper,\n}\n\n\nclass FileIntegrityError(IntegrityError):\n    \"\"\"File failed integrity check: {}\"\"\"\n\n    exit_mcode = 91\n\n\nclass IntegrityCheckedFile(FileLikeWrapper):\n    def __init__(self, path, write, filename=None, override_fd=None, integrity_data=None):\n        self.path = path\n        self.writing = write\n        mode = \"wb\" if write else \"rb\"\n        self.file_fd = override_fd or open(path, mode)\n        self.file_opened = override_fd is None\n        self.digests = {}\n\n        hash_cls = XXH64FileHashingWrapper\n\n        if not write:\n            algorithm_and_digests = self.load_integrity_data(path, integrity_data)\n            if algorithm_and_digests:\n                algorithm, self.digests = algorithm_and_digests\n                hash_cls = SUPPORTED_ALGORITHMS[algorithm]\n\n            # TODO: When we're reading but don't have any digests, i.e. no integrity file existed,\n            # TODO: then we could just short-circuit.\n\n        self.hasher = hash_cls(backing_fd=self.file_fd, write=write)\n        super().__init__(self.hasher)\n        self.hash_filename(filename)\n\n    def load_integrity_data(self, path, integrity_data):\n        if integrity_data is not None:\n            return self.parse_integrity_data(path, integrity_data)\n\n    def hash_filename(self, filename=None):\n        # Hash the name of the file, but only the basename, ie. not the path.\n        # In Borg the name itself encodes the context (eg. index.N, cache, files),\n        # while the path doesn't matter, and moving e.g. a repository or cache directory is supported.\n        # Changing the name however imbues a change of context that is not permissible.\n        # While Borg does not use anything except ASCII in these file names, it's important to use\n        # the same encoding everywhere for portability. Using os.fsencode() would be wrong.\n        filename = Path(filename or self.path).name\n        self.hasher.update((\"%10d\" % len(filename)).encode())\n        self.hasher.update(filename.encode())\n\n    @classmethod\n    def parse_integrity_data(cls, path: str, data: str):\n        try:\n            integrity_data = json.loads(data)\n            # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled.\n            algorithm = integrity_data[\"algorithm\"]\n            if algorithm not in SUPPORTED_ALGORITHMS:\n                logger.warning(\"Cannot verify integrity of %s: Unknown algorithm %r\", path, algorithm)\n                return\n            digests = integrity_data[\"digests\"]\n            # Require at least presence of the final digest\n            digests[\"final\"]\n            return algorithm, digests\n        except (ValueError, TypeError, KeyError) as e:\n            logger.warning(\"Could not parse integrity data for %s: %s\", path, e)\n            raise FileIntegrityError(path)\n\n    def hash_part(self, partname, is_final=False):\n        if not self.writing and not self.digests:\n            return\n        self.hasher.update((\"%10d\" % len(partname)).encode())\n        self.hasher.update(partname.encode())\n        self.hasher.hash_length(seek_to_end=is_final)\n        digest = self.hasher.hexdigest()\n        if self.writing:\n            self.digests[partname] = digest\n        elif self.digests and not compare_digest(self.digests.get(partname, \"\"), digest):\n            raise FileIntegrityError(self.path)\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        exception = exc_type is not None\n\n        try:\n            if not exception:\n                self.hash_part(\"final\", is_final=True)\n            self.hasher.__exit__(exc_type, exc_val, exc_tb)\n        finally:\n            if self.file_opened:\n                self.file_fd.close()\n        if exception:\n            return\n        if self.writing:\n            self.store_integrity_data(json.dumps({\"algorithm\": self.hasher.ALGORITHM, \"digests\": self.digests}))\n        elif self.digests:\n            logger.debug(\"Verified integrity of %s\", self.path)\n\n    def store_integrity_data(self, data: str):\n        self.integrity_data = data\n\n\nclass DetachedIntegrityCheckedFile(IntegrityCheckedFile):\n    def __init__(self, path, write, filename=None, override_fd=None):\n        super().__init__(path, write, filename, override_fd)\n        path_obj = Path(path)\n        filename = filename or path_obj.name\n        self.output_integrity_file = self.integrity_file_path(path_obj.parent / filename)\n\n    def load_integrity_data(self, path, integrity_data):\n        assert not integrity_data, \"Cannot pass explicit integrity_data to DetachedIntegrityCheckedFile\"\n        return self.read_integrity_file(self.path)\n\n    @staticmethod\n    def integrity_file_path(path):\n        return Path(str(path) + \".integrity\")\n\n    @classmethod\n    def read_integrity_file(cls, path):\n        try:\n            with open(cls.integrity_file_path(path)) as fd:\n                return cls.parse_integrity_data(path, fd.read())\n        except FileNotFoundError:\n            logger.info(\"No integrity file found for %s\", path)\n        except OSError as e:\n            logger.warning(\"Could not read integrity file for %s: %s\", path, e)\n            raise FileIntegrityError(path)\n\n    def store_integrity_data(self, data: str):\n        with self.output_integrity_file.open(\"w\") as fd:\n            fd.write(data)\n"
  },
  {
    "path": "src/borg/crypto/key.py",
    "content": "import binascii\nimport hmac\nimport os\nimport textwrap\nfrom hashlib import sha256, pbkdf2_hmac\nfrom pathlib import Path\nfrom typing import Literal, ClassVar\nfrom collections.abc import Callable\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\nimport argon2.low_level\n\nfrom ..constants import *  # NOQA\nfrom ..helpers import StableDict\nfrom ..helpers import Error, IntegrityError\nfrom ..helpers import get_keys_dir\nfrom ..helpers import get_limited_unpacker\nfrom ..helpers import bin_to_hex\nfrom ..helpers.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong\nfrom ..helpers import msgpack\nfrom ..helpers import workarounds\nfrom ..item import Key, EncryptedKey\nfrom ..manifest import Manifest\nfrom ..platform import SaveFile\nfrom ..repoobj import RepoObj\n\n\nfrom .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256\nfrom .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305\nfrom . import low_level\n\n# workaround for lost passphrase or key in \"authenticated\" or \"authenticated-blake2\" mode\nAUTHENTICATED_NO_KEY = \"authenticated_no_key\" in workarounds\n\n\nclass UnsupportedPayloadError(Error):\n    \"\"\"Unsupported payload type {}. A newer version is required to access this repository.\"\"\"\n\n    exit_mcode = 48\n\n\nclass UnsupportedManifestError(Error):\n    \"\"\"Unsupported manifest envelope. A newer version is required to access this repository.\"\"\"\n\n    exit_mcode = 27\n\n\nclass KeyfileNotFoundError(Error):\n    \"\"\"No key file for repository {} found in {}.\"\"\"\n\n    exit_mcode = 42\n\n\nclass KeyfileInvalidError(Error):\n    \"\"\"Invalid key data for repository {} found in {}.\"\"\"\n\n    exit_mcode = 40\n\n\nclass KeyfileMismatchError(Error):\n    \"\"\"Mismatch between repository {} and key file {}.\"\"\"\n\n    exit_mcode = 41\n\n\nclass RepoKeyNotFoundError(Error):\n    \"\"\"No key entry found in the config of repository {}.\"\"\"\n\n    exit_mcode = 44\n\n\nclass UnsupportedKeyFormatError(Error):\n    \"\"\"Your Borg key is stored in an unsupported format. Try using a newer version of Borg.\"\"\"\n\n    exit_mcode = 49\n\n\ndef key_creator(repository, args, *, other_key=None):\n    for key in AVAILABLE_KEY_TYPES:\n        if key.ARG_NAME == args.encryption:\n            assert key.ARG_NAME is not None\n            return key.create(repository, args, other_key=other_key)\n    else:\n        raise ValueError('Invalid encryption mode \"%s\"' % args.encryption)\n\n\ndef key_argument_names():\n    return [key.ARG_NAME for key in AVAILABLE_KEY_TYPES if key.ARG_NAME]\n\n\ndef identify_key(manifest_data):\n    key_type = manifest_data[0]\n    if key_type == KeyType.PASSPHRASE:  # legacy, see comment in KeyType class.\n        return RepoKey\n\n    for key in LEGACY_KEY_TYPES + AVAILABLE_KEY_TYPES:\n        if key.TYPE == key_type:\n            return key\n    else:\n        raise UnsupportedPayloadError(key_type)\n\n\ndef key_factory(repository, manifest_chunk, *, other=False, ro_cls=RepoObj):\n    manifest_data = ro_cls.extract_crypted_data(manifest_chunk)\n    assert manifest_data, \"manifest data must not be zero bytes long\"\n    return identify_key(manifest_data).detect(repository, manifest_data, other=other)\n\n\ndef uses_same_chunker_secret(other_key, key):\n    \"\"\"is the chunker secret the same?\"\"\"\n    # avoid breaking the deduplication by a different chunker secret\n    same_chunker_secret = other_key.chunk_seed == key.chunk_seed\n    return same_chunker_secret\n\n\ndef uses_same_id_hash(other_key, key):\n    \"\"\"other_key -> key upgrade: is the id hash the same?\"\"\"\n    # avoid breaking the deduplication by changing the id hash\n    old_sha256_ids = (PlaintextKey,)\n    new_sha256_ids = (PlaintextKey,)\n    old_hmac_sha256_ids = (RepoKey, KeyfileKey, AuthenticatedKey)\n    new_hmac_sha256_ids = (AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey, AuthenticatedKey)\n    old_blake2_ids = (Blake2RepoKey, Blake2KeyfileKey, Blake2AuthenticatedKey)\n    new_blake2_ids = (\n        Blake2AESOCBRepoKey,\n        Blake2AESOCBKeyfileKey,\n        Blake2CHPORepoKey,\n        Blake2CHPOKeyfileKey,\n        Blake2AuthenticatedKey,\n    )\n    same_ids = (\n        isinstance(other_key, old_hmac_sha256_ids + new_hmac_sha256_ids)\n        and isinstance(key, new_hmac_sha256_ids)\n        or isinstance(other_key, old_blake2_ids + new_blake2_ids)\n        and isinstance(key, new_blake2_ids)\n        or isinstance(other_key, old_sha256_ids + new_sha256_ids)\n        and isinstance(key, new_sha256_ids)\n    )\n    return same_ids\n\n\nclass KeyBase:\n    # Numeric key type ID, must fit in one byte.\n    TYPE: int = None  # override in subclasses\n    # set of key type IDs the class can handle as input\n    TYPES_ACCEPTABLE: set[int] = None  # override in subclasses\n\n    # Human-readable name\n    NAME = \"UNDEFINED\"\n\n    # Name used in command line / API (e.g. borg init --encryption=...)\n    ARG_NAME = \"UNDEFINED\"\n\n    # Storage type (no key blob storage / keyfile / repo)\n    STORAGE: ClassVar[str] = KeyBlobStorage.NO_STORAGE\n\n    # Seed for the buzhash chunker (borg.algorithms.chunker.Chunker)\n    # type is int\n    chunk_seed: int = None\n\n    # crypt_key dummy, needs to be overwritten by subclass\n    crypt_key: bytes = None\n\n    # id_key dummy, needs to be overwritten by subclass\n    id_key: bytes = None\n\n    # Whether this *particular instance* is encrypted from a practical point of view,\n    # i.e. when it's using encryption with a empty passphrase, then\n    # that may be *technically* called encryption, but for all intents and purposes\n    # that's as good as not encrypting in the first place, and this member should be False.\n    #\n    # The empty passphrase is also special because Borg tries it first when no passphrase\n    # was supplied, and if an empty passphrase works, then Borg won't ask for one.\n    logically_encrypted = False\n\n    def __init__(self, repository):\n        self.TYPE_STR = bytes([self.TYPE])\n        self.repository = repository\n        self.target = None  # key location file path / repo obj\n        self.copy_crypt_key = False\n\n    def id_hash(self, data):\n        \"\"\"Return HMAC hash using the \"id\" HMAC key\"\"\"\n        raise NotImplementedError\n\n    def encrypt(self, id, data):\n        pass\n\n    def decrypt(self, id, data):\n        pass\n\n    def assert_id(self, id, data):\n        if id and id != Manifest.MANIFEST_ID:\n            id_computed = self.id_hash(data)\n            if not hmac.compare_digest(id_computed, id):\n                raise IntegrityError(\"Chunk %s: id verification failed\" % bin_to_hex(id))\n\n    def assert_type(self, type_byte, id=None):\n        if type_byte not in self.TYPES_ACCEPTABLE:\n            id_str = bin_to_hex(id) if id is not None else \"(unknown)\"\n            raise IntegrityError(f\"Chunk {id_str}: Invalid encryption envelope\")\n\n    def derive_key(self, *, salt, domain, size, from_id_key=False):\n        \"\"\"\n        create a new crypto key (<size> bytes long) from existing key material, a given salt and domain.\n        from_id_key == False: derive from self.crypt_key (default)\n        from_id_key == True: derive from self.id_key (note: related repos have same ID key)\n        \"\"\"\n        from_key = self.id_key if from_id_key else self.crypt_key\n        assert isinstance(from_key, bytes)\n        assert isinstance(salt, bytes)\n        assert isinstance(domain, bytes)\n        assert size <= 32  # sha256 gives us 32 bytes\n        # Because crypt_key is already a PRK, we do not need KDF security here, PRF security is good enough.\n        # See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf section 4 \"one-step KDF\".\n        return sha256(from_key + salt + domain).digest()[:size]\n\n    def pack_metadata(self, metadata_dict):\n        metadata_dict = StableDict(metadata_dict)\n        return msgpack.packb(metadata_dict)\n\n    def unpack_manifest(self, data):\n        \"\"\"Unpack msgpacked *data* and return manifest.\"\"\"\n        if data.startswith(b\"\\xc1\" * 4):\n            # This is a manifest from the future, we can't read it.\n            raise UnsupportedManifestError()\n        data = bytearray(data)\n        unpacker = get_limited_unpacker(\"manifest\")\n        unpacker.feed(data)\n        unpacked = unpacker.unpack()\n        unpacked.pop(\"tam\", None)  # legacy\n        return unpacked\n\n    def unpack_archive(self, data):\n        \"\"\"Unpack msgpacked *data* and return archive metadata dict.\"\"\"\n        data = bytearray(data)\n        unpacker = get_limited_unpacker(\"archive\")\n        unpacker.feed(data)\n        unpacked = unpacker.unpack()\n        unpacked.pop(\"tam\", None)  # legacy\n        return unpacked\n\n\nclass PlaintextKey(KeyBase):\n    TYPE = KeyType.PLAINTEXT\n    TYPES_ACCEPTABLE = {TYPE}\n    NAME = \"plaintext\"\n    ARG_NAME = \"none\"\n\n    chunk_seed = 0\n    crypt_key = b\"\"  # makes .derive_key() work, nothing secret here\n    id_key = b\"\"  # makes .derive_key() work, nothing secret here\n\n    logically_encrypted = False\n\n    @classmethod\n    def create(cls, repository, args, **kw):\n        logger.info('Encryption NOT enabled.\\nUse the \"--encryption=repokey|keyfile\" to enable encryption.')\n        return cls(repository)\n\n    @classmethod\n    def detect(cls, repository, manifest_data, *, other=False):\n        return cls(repository)\n\n    def id_hash(self, data):\n        return sha256(data).digest()\n\n    def encrypt(self, id, data):\n        return b\"\".join([self.TYPE_STR, data])\n\n    def decrypt(self, id, data):\n        self.assert_type(data[0], id)\n        return memoryview(data)[1:]\n\n\ndef random_blake2b_256_key():\n    # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.\n    # Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b\n    # has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a\n    # \"local wide pipe\" design, because the compression function transforms (block, state) => state,\n    # and len(block) >= len(state), hence wide.)\n    # In other words, a key longer than 64 bytes would have simply no advantage, since the function\n    # has no way of propagating more than 64 bytes of entropy internally.\n    # It's padded to a full block so that the key is never buffered internally by blake2b_update, ie.\n    # it remains in a single memory location that can be tracked and could be erased securely, if we\n    # wanted to.\n    return os.urandom(64) + bytes(64)\n\n\nclass ID_BLAKE2b_256:\n    \"\"\"\n    Key mix-in class for using BLAKE2b-256 for the id key.\n\n    The id_key length must be 32 bytes.\n    \"\"\"\n\n    def id_hash(self, data):\n        return blake2b_256(self.id_key, data)\n\n    def init_from_random_data(self):\n        super().init_from_random_data()\n        enc_key = os.urandom(32)\n        enc_hmac_key = random_blake2b_256_key()\n        self.crypt_key = enc_key + enc_hmac_key\n        self.id_key = random_blake2b_256_key()\n\n\nclass ID_HMAC_SHA_256:\n    \"\"\"\n    Key mix-in class for using HMAC-SHA-256 for the id key.\n\n    The id_key length must be 32 bytes.\n    \"\"\"\n\n    def id_hash(self, data):\n        return hmac_sha256(self.id_key, data)\n\n\nclass AESKeyBase(KeyBase):\n    \"\"\"\n    Chunks are encrypted using 256bit AES in Counter Mode (CTR)\n\n    Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT\n\n    To reduce payload size only 8 bytes of the 16 bytes nonce is saved\n    in the payload, the first 8 bytes are always zeros. This does not\n    affect security but limits the maximum repository capacity to\n    only 295 exabytes!\n    \"\"\"\n\n    PAYLOAD_OVERHEAD = 1 + 32 + 8  # TYPE + HMAC + NONCE\n\n    CIPHERSUITE: Callable = None  # override in derived class\n\n    logically_encrypted = True\n\n    def encrypt(self, id, data):\n        # legacy, this is only used by the tests.\n        next_iv = self.cipher.next_iv()\n        return self.cipher.encrypt(data, header=self.TYPE_STR, iv=next_iv)\n\n    def decrypt(self, id, data):\n        self.assert_type(data[0], id)\n        try:\n            return self.cipher.decrypt(data)\n        except IntegrityError as e:\n            raise IntegrityError(f\"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]\")\n\n    def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):\n        assert len(crypt_key) in (32 + 32, 32 + 128)\n        assert len(id_key) in (32, 128)\n        assert isinstance(chunk_seed, int)\n        self.crypt_key = crypt_key\n        self.id_key = id_key\n        self.chunk_seed = chunk_seed\n\n    def init_from_random_data(self):\n        data = os.urandom(100)\n        chunk_seed = bytes_to_int(data[96:100])\n        # Convert to signed int32\n        if chunk_seed & 0x80000000:\n            chunk_seed = chunk_seed - 0xFFFFFFFF - 1\n        self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)\n\n    def init_ciphers(self, manifest_data=None):\n        enc_key, enc_hmac_key = self.crypt_key[0:32], self.crypt_key[32:]\n        self.cipher = self.CIPHERSUITE(mac_key=enc_hmac_key, enc_key=enc_key, header_len=1, aad_offset=1)\n        if manifest_data is None:\n            nonce = 0\n        else:\n            self.assert_type(manifest_data[0])\n            # manifest_blocks is a safe upper bound on the amount of cipher blocks needed\n            # to encrypt the manifest. depending on the ciphersuite and overhead, it might\n            # be a bit too high, but that does not matter.\n            manifest_blocks = num_cipher_blocks(len(manifest_data))\n            nonce = self.cipher.extract_iv(manifest_data) + manifest_blocks\n        self.cipher.set_iv(nonce)\n\n\nclass FlexiKey:\n    FILE_ID = \"BORG_KEY\"\n    STORAGE: ClassVar[str] = KeyBlobStorage.NO_STORAGE  # override in subclass\n\n    @classmethod\n    def detect(cls, repository, manifest_data, *, other=False):\n        key = cls(repository)\n        target = key.find_key()\n        prompt = \"Enter passphrase for key %s: \" % target\n        passphrase = Passphrase.env_passphrase(other=other)\n        if passphrase is None:\n            passphrase = Passphrase()\n            if not key.load(target, passphrase):\n                for retry in range(0, 3):\n                    passphrase = Passphrase.getpass(prompt)\n                    if key.load(target, passphrase):\n                        break\n                    Passphrase.display_debug_info(passphrase)\n                else:\n                    raise PasswordRetriesExceeded\n        else:\n            if not key.load(target, passphrase):\n                Passphrase.display_debug_info(passphrase)\n                raise PassphraseWrong\n        key.init_ciphers(manifest_data)\n        key._passphrase = passphrase\n        return key\n\n    def _load(self, key_data, passphrase):\n        try:\n            key = binascii.a2b_base64(key_data)\n        except (ValueError, binascii.Error):\n            raise KeyfileInvalidError(self.repository._location.canonical_path(), \"(repokey)\") from None\n        if len(key) < 20:\n            # this is in no way a precise check, usually we have about 400b key data.\n            raise KeyfileInvalidError(self.repository._location.canonical_path(), \"(repokey)\")\n        data = self.decrypt_key_file(key, passphrase)\n        if data:\n            data = msgpack.unpackb(data)\n            key = Key(internal_dict=data)\n            if key.version not in (1, 2):  # legacy: item.Key can still process v1 keys\n                raise UnsupportedKeyFormatError()\n            self.repository_id = key.repository_id\n            self.crypt_key = key.crypt_key\n            self.id_key = key.id_key\n            self.chunk_seed = key.chunk_seed\n            return True\n        return False\n\n    def decrypt_key_file(self, data, passphrase):\n        unpacker = get_limited_unpacker(\"key\")\n        unpacker.feed(data)\n        data = unpacker.unpack()\n        encrypted_key = EncryptedKey(internal_dict=data)\n        if encrypted_key.version != 1:\n            raise UnsupportedKeyFormatError()\n        else:\n            self._encrypted_key_algorithm = encrypted_key.algorithm\n            if encrypted_key.algorithm == \"sha256\":\n                return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)\n            elif encrypted_key.algorithm == \"argon2 chacha20-poly1305\":\n                return self.decrypt_key_file_argon2(encrypted_key, passphrase)\n            else:\n                raise UnsupportedKeyFormatError()\n\n    @staticmethod\n    def pbkdf2(passphrase, salt, iterations, output_len_in_bytes):\n        if os.environ.get(\"BORG_TESTONLY_WEAKEN_KDF\") == \"1\":\n            iterations = 1\n        return pbkdf2_hmac(\"sha256\", passphrase.encode(\"utf-8\"), salt, iterations, output_len_in_bytes)\n\n    @staticmethod\n    def argon2(\n        passphrase: str,\n        output_len_in_bytes: int,\n        salt: bytes,\n        time_cost: int,\n        memory_cost: int,\n        parallelism: int,\n        type: Literal[\"i\", \"d\", \"id\"],\n    ) -> bytes:\n        if os.environ.get(\"BORG_TESTONLY_WEAKEN_KDF\") == \"1\":\n            time_cost = 1\n            parallelism = 1\n            # 8 is the smallest value that avoids the \"Memory cost is too small\" exception\n            memory_cost = 8\n        type_map = {\"i\": argon2.low_level.Type.I, \"d\": argon2.low_level.Type.D, \"id\": argon2.low_level.Type.ID}\n        key = argon2.low_level.hash_secret_raw(\n            secret=passphrase.encode(\"utf-8\"),\n            hash_len=output_len_in_bytes,\n            salt=salt,\n            time_cost=time_cost,\n            memory_cost=memory_cost,\n            parallelism=parallelism,\n            type=type_map[type],\n        )\n        return key\n\n    def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):\n        key = self.pbkdf2(passphrase, encrypted_key.salt, encrypted_key.iterations, 32)\n        data = AES(key, b\"\\0\" * 16).decrypt(encrypted_key.data)\n        if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):\n            return data\n        return None\n\n    def decrypt_key_file_argon2(self, encrypted_key, passphrase):\n        key = self.argon2(\n            passphrase,\n            output_len_in_bytes=32,\n            salt=encrypted_key.salt,\n            time_cost=encrypted_key.argon2_time_cost,\n            memory_cost=encrypted_key.argon2_memory_cost,\n            parallelism=encrypted_key.argon2_parallelism,\n            type=encrypted_key.argon2_type,\n        )\n        ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)\n        try:\n            return ae_cipher.decrypt(encrypted_key.data)\n        except low_level.IntegrityError:\n            return None\n\n    def encrypt_key_file(self, data, passphrase, algorithm):\n        if algorithm == \"sha256\":\n            return self.encrypt_key_file_pbkdf2(data, passphrase)\n        elif algorithm == \"argon2 chacha20-poly1305\":\n            return self.encrypt_key_file_argon2(data, passphrase)\n        else:\n            raise ValueError(f\"Unexpected algorithm: {algorithm}\")\n\n    def encrypt_key_file_pbkdf2(self, data, passphrase):\n        salt = os.urandom(32)\n        iterations = PBKDF2_ITERATIONS\n        key = self.pbkdf2(passphrase, salt, iterations, 32)\n        hash = hmac_sha256(key, data)\n        cdata = AES(key, b\"\\0\" * 16).encrypt(data)\n        enc_key = EncryptedKey(version=1, salt=salt, iterations=iterations, algorithm=\"sha256\", hash=hash, data=cdata)\n        return msgpack.packb(enc_key.as_dict())\n\n    def encrypt_key_file_argon2(self, data, passphrase):\n        salt = os.urandom(ARGON2_SALT_BYTES)\n        key = self.argon2(passphrase, output_len_in_bytes=32, salt=salt, **ARGON2_ARGS)\n        ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)\n        encrypted_key = EncryptedKey(\n            version=1,\n            algorithm=\"argon2 chacha20-poly1305\",\n            salt=salt,\n            data=ae_cipher.encrypt(data),\n            **{\"argon2_\" + k: v for k, v in ARGON2_ARGS.items()},\n        )\n        return msgpack.packb(encrypted_key.as_dict())\n\n    def _save(self, passphrase, algorithm):\n        key = Key(\n            version=2,\n            repository_id=self.repository_id,\n            crypt_key=self.crypt_key,\n            id_key=self.id_key,\n            chunk_seed=self.chunk_seed,\n        )\n        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase, algorithm)\n        key_data = \"\\n\".join(textwrap.wrap(binascii.b2a_base64(data).decode(\"ascii\")))\n        return key_data\n\n    def change_passphrase(self, passphrase=None):\n        if passphrase is None:\n            passphrase = Passphrase.new(allow_empty=True)\n        self.save(self.target, passphrase, algorithm=self._encrypted_key_algorithm)\n\n    @classmethod\n    def create(cls, repository, args, *, other_key=None):\n        key = cls(repository)\n        key.repository_id = repository.id\n        if other_key is not None:\n            if isinstance(other_key, PlaintextKey):\n                raise Error(\"Copying key material from an unencrypted repository is not possible.\")\n            if isinstance(key, AESKeyBase):\n                # user must use an AEADKeyBase subclass (AEAD modes with session keys)\n                raise Error(\"Copying key material to an AES-CTR based mode is insecure and unsupported.\")\n            if not uses_same_id_hash(other_key, key):\n                raise Error(\"You must keep the same ID hash (HMAC-SHA256 or BLAKE2b) or deduplication will break.\")\n            if other_key.copy_crypt_key:\n                # give the user the option to use the same authenticated encryption (AE) key\n                crypt_key = other_key.crypt_key\n            else:\n                # borg transfer re-encrypts all data anyway, thus we can default to a new, random AE key\n                crypt_key = os.urandom(64)\n            key.init_from_given_data(crypt_key=crypt_key, id_key=other_key.id_key, chunk_seed=other_key.chunk_seed)\n        else:\n            key.init_from_random_data()\n        passphrase = Passphrase.new(allow_empty=True)\n        key.init_ciphers()\n        target = key.get_new_target(args)\n        key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS[\"argon2\"])\n        logger.info('Key in \"%s\" created.' % target)\n        logger.info(\"Keep this key safe. Your data will be inaccessible without it.\")\n        return key\n\n    def sanity_check(self, filename, id):\n        file_id = self.FILE_ID.encode() + b\" \"\n        repo_id = bin_to_hex(id).encode(\"ascii\")\n        with open(filename, \"rb\") as fd:\n            # we do the magic / id check in binary mode to avoid stumbling over\n            # decoding errors if somebody has binary files in the keys dir for some reason.\n            if fd.read(len(file_id)) != file_id:\n                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)\n            if fd.read(len(repo_id)) != repo_id:\n                raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)\n        # we get here if it really looks like a borg key for this repo,\n        # do some more checks that are close to how borg reads/parses the key.\n        with open(filename) as fd:\n            lines = fd.readlines()\n            if len(lines) < 2:\n                logger.warning(f\"borg key sanity check: expected 2+ lines total. [{filename}]\")\n                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)\n            if len(lines[0].rstrip()) > len(file_id) + len(repo_id):\n                logger.warning(f\"borg key sanity check: key line 1 seems too long. [{filename}]\")\n                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)\n            key_b64 = \"\".join(lines[1:])\n            try:\n                key = binascii.a2b_base64(key_b64)\n            except (ValueError, binascii.Error):\n                logger.warning(f\"borg key sanity check: key line 2+ does not look like base64. [{filename}]\")\n                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename) from None\n            if len(key) < 20:\n                # this is in no way a precise check, usually we have about 400b key data.\n                logger.warning(\n                    f\"borg key sanity check: binary encrypted key data from key line 2+ suspiciously short.\"\n                    f\" [{filename}]\"\n                )\n                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)\n        # looks good!\n        return filename\n\n    def find_key(self):\n        if self.STORAGE == KeyBlobStorage.KEYFILE:\n            keyfile = self._find_key_file_from_environment()\n            if keyfile is not None:\n                return self.sanity_check(keyfile, self.repository.id)\n            keyfile = self._find_key_in_keys_dir()\n            if keyfile is not None:\n                return keyfile\n            raise KeyfileNotFoundError(self.repository._location.canonical_path(), get_keys_dir())\n        elif self.STORAGE == KeyBlobStorage.REPO:\n            loc = self.repository._location.canonical_path()\n            key = self.repository.load_key()\n            if not key:\n                # if we got an empty key, it means there is no key.\n                raise RepoKeyNotFoundError(loc) from None\n            return loc\n        else:\n            raise TypeError(\"Unsupported borg key storage type\")\n\n    def get_existing_or_new_target(self, args):\n        keyfile = self._find_key_file_from_environment()\n        if keyfile is not None:\n            return keyfile\n        keyfile = self._find_key_in_keys_dir()\n        if keyfile is not None:\n            return keyfile\n        return self._get_new_target_in_keys_dir(args)\n\n    def _find_key_in_keys_dir(self):\n        id = self.repository.id\n        keys_path = Path(get_keys_dir())\n        for entry in keys_path.iterdir():\n            filename = keys_path / entry.name\n            try:\n                return self.sanity_check(str(filename), id)\n            except (KeyfileInvalidError, KeyfileMismatchError):\n                pass\n\n    def get_new_target(self, args):\n        if self.STORAGE == KeyBlobStorage.KEYFILE:\n            keyfile = self._find_key_file_from_environment()\n            if keyfile is not None:\n                return keyfile\n            return self._get_new_target_in_keys_dir(args)\n        elif self.STORAGE == KeyBlobStorage.REPO:\n            return self.repository\n        else:\n            raise TypeError(\"Unsupported borg key storage type\")\n\n    def _find_key_file_from_environment(self):\n        keyfile = os.environ.get(\"BORG_KEY_FILE\")\n        if keyfile:\n            return os.path.abspath(keyfile)\n\n    def _get_new_target_in_keys_dir(self, args):\n        filename = args.location.to_key_filename()\n        path = Path(filename)\n        i = 1\n        while path.exists():\n            i += 1\n            path = Path(filename + \".%d\" % i)\n        return str(path)\n\n    def load(self, target, passphrase):\n        if self.STORAGE == KeyBlobStorage.KEYFILE:\n            with open(target) as fd:\n                key_data = \"\".join(fd.readlines()[1:])\n        elif self.STORAGE == KeyBlobStorage.REPO:\n            # While the repository is encrypted, we consider a repokey repository with a blank\n            # passphrase an unencrypted repository.\n            self.logically_encrypted = passphrase != \"\"  # nosec B105\n\n            # what we get in target is just a repo location, but we already have the repo obj:\n            target = self.repository\n            key_data = target.load_key()\n            if not key_data:\n                # if we got an empty key, it means there is no key.\n                loc = target._location.canonical_path()\n                raise RepoKeyNotFoundError(loc) from None\n            key_data = key_data.decode(\"utf-8\")  # remote repo: msgpack issue #99, getting bytes\n        else:\n            raise TypeError(\"Unsupported borg key storage type\")\n        success = self._load(key_data, passphrase)\n        if success:\n            self.target = target\n        return success\n\n    def save(self, target, passphrase, algorithm, create=False):\n        key_data = self._save(passphrase, algorithm)\n        if self.STORAGE == KeyBlobStorage.KEYFILE:\n            if create and os.path.isfile(target):\n                # if a new keyfile key repository is created, ensure that an existing keyfile of another\n                # keyfile key repo is not accidentally overwritten by careless use of the BORG_KEY_FILE env var.\n                # see issue #6036\n                raise Error('Aborting because key in \"%s\" already exists.' % target)\n            with SaveFile(target) as fd:\n                fd.write(f\"{self.FILE_ID} {bin_to_hex(self.repository_id)}\\n\")\n                fd.write(key_data)\n                fd.write(\"\\n\")\n        elif self.STORAGE == KeyBlobStorage.REPO:\n            self.logically_encrypted = passphrase != \"\"  # nosec B105\n            key_data = key_data.encode(\"utf-8\")  # remote repo: msgpack issue #99, giving bytes\n            target.save_key(key_data)\n        else:\n            raise TypeError(\"Unsupported borg key storage type\")\n        self.target = target\n\n    def remove(self, target):\n        if self.STORAGE == KeyBlobStorage.KEYFILE:\n            os.remove(target)\n        elif self.STORAGE == KeyBlobStorage.REPO:\n            target.save_key(b\"\")  # save empty key (no new api at remote repo necessary)\n        else:\n            raise TypeError(\"Unsupported borg key storage type\")\n\n\nclass KeyfileKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}\n    TYPE = KeyType.KEYFILE\n    NAME = \"key file\"\n    ARG_NAME = \"keyfile\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = AES256_CTR_HMAC_SHA256\n\n\nclass RepoKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}\n    TYPE = KeyType.REPO\n    NAME = \"repokey\"\n    ARG_NAME = \"repokey\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = AES256_CTR_HMAC_SHA256\n\n\nclass Blake2KeyfileKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}\n    TYPE = KeyType.BLAKE2KEYFILE\n    NAME = \"key file BLAKE2b\"\n    ARG_NAME = \"keyfile-blake2\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = AES256_CTR_BLAKE2b\n\n\nclass Blake2RepoKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}\n    TYPE = KeyType.BLAKE2REPO\n    NAME = \"repokey BLAKE2b\"\n    ARG_NAME = \"repokey-blake2\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = AES256_CTR_BLAKE2b\n\n\nclass AuthenticatedKeyBase(AESKeyBase, FlexiKey):\n    STORAGE = KeyBlobStorage.REPO\n\n    # It's only authenticated, not encrypted.\n    logically_encrypted = False\n\n    def _load(self, key_data, passphrase):\n        if AUTHENTICATED_NO_KEY:\n            # fake _load if we have no key or passphrase\n            NOPE = bytes(32)  # 256 bit all-zero\n            self.repository_id = NOPE\n            self.enc_key = NOPE\n            self.enc_hmac_key = NOPE\n            self.id_key = NOPE\n            self.chunk_seed = 0\n            return True\n        return super()._load(key_data, passphrase)\n\n    def load(self, target, passphrase):\n        success = super().load(target, passphrase)\n        self.logically_encrypted = False\n        return success\n\n    def save(self, target, passphrase, algorithm, create=False):\n        super().save(target, passphrase, algorithm, create=create)\n        self.logically_encrypted = False\n\n    def init_ciphers(self, manifest_data=None):\n        if manifest_data is not None:\n            self.assert_type(manifest_data[0])\n\n    def encrypt(self, id, data):\n        return b\"\".join([self.TYPE_STR, data])\n\n    def decrypt(self, id, data):\n        self.assert_type(data[0], id)\n        return memoryview(data)[1:]\n\n\nclass AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase):\n    TYPE = KeyType.AUTHENTICATED\n    TYPES_ACCEPTABLE = {TYPE}\n    NAME = \"authenticated\"\n    ARG_NAME = \"authenticated\"\n\n\nclass Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):\n    TYPE = KeyType.BLAKE2AUTHENTICATED\n    TYPES_ACCEPTABLE = {TYPE}\n    NAME = \"authenticated BLAKE2b\"\n    ARG_NAME = \"authenticated-blake2\"\n\n\n# ------------ new crypto ------------\n\n\nclass AEADKeyBase(KeyBase):\n    \"\"\"\n    Chunks are encrypted and authenticated using some AEAD ciphersuite\n\n    Layout: suite:4 keytype:4 reserved:8 messageIV:48 sessionID:192 auth_tag:128 payload:... [bits]\n            ^-------------------- AAD ----------------------------^\n    Offsets:0                 1          2            8             32           48 [bytes]\n\n    suite: 1010b for new AEAD crypto, 0000b is old crypto\n    keytype: see constants.KeyType (suite+keytype)\n    reserved: all-zero, for future use\n    messageIV: a counter starting from 0 for all new encrypted messages of one session\n    sessionID: 192bit random, computed once per session (the session key is derived from this)\n    auth_tag: authentication tag output of the AEAD cipher (computed over payload and AAD)\n    payload: encrypted chunk data\n    \"\"\"\n\n    PAYLOAD_OVERHEAD = 1 + 1 + 6 + 24 + 16  # [bytes], see Layout\n\n    CIPHERSUITE: Callable = None  # override in subclass\n\n    logically_encrypted = True\n\n    MAX_IV = 2**48 - 1\n\n    def assert_id(self, id, data):\n        # Comparing the id hash here would not be needed any more for the new AEAD crypto **IF** we\n        # could be sure that chunks were created by normal (not tampered, not evil) borg code:\n        # We put the id into AAD when storing the chunk, so it gets into the authentication tag computation.\n        # when decrypting, we provide the id we **want** as AAD for the auth tag verification, so\n        # decrypting only succeeds if we got the ciphertext we wrote **for that chunk id**.\n        # So, basically the **repository** can not cheat on us by giving us a different chunk.\n        #\n        # **BUT**, if chunks are created by tampered, evil borg code, the borg client code could put\n        # a wrong chunkid into AAD and then AEAD-encrypt-and-auth this and store it into the\n        # repository using this bad chunkid as key (violating the usual chunkid == id_hash(data)).\n        # Later, when reading such a bad chunk, AEAD-auth-and-decrypt would not notice any\n        # issue and decrypt successfully.\n        # Thus, to notice such evil borg activity, we must check for such violations here:\n        if id and id != Manifest.MANIFEST_ID:\n            id_computed = self.id_hash(data)\n            if not hmac.compare_digest(id_computed, id):\n                raise IntegrityError(\"Chunk %s: id verification failed\" % bin_to_hex(id))\n\n    def encrypt(self, id, data):\n        # to encrypt new data in this session we use always self.cipher and self.sessionid\n        reserved = b\"\\0\"\n        iv = self.cipher.next_iv()\n        if iv > self.MAX_IV:  # see the data-structures docs about why the IV range is enough\n            raise IntegrityError(\"IV overflow, should never happen.\")\n        iv_48bit = iv.to_bytes(6, \"big\")\n        header = self.TYPE_STR + reserved + iv_48bit + self.sessionid\n        return self.cipher.encrypt(data, header=header, iv=iv, aad=id)\n\n    def decrypt(self, id, data):\n        # to decrypt existing data, we need to get a cipher configured for the sessionid and iv from header\n        self.assert_type(data[0], id)\n        iv_48bit = data[2:8]\n        sessionid = bytes(data[8:32])\n        iv = int.from_bytes(iv_48bit, \"big\")\n        cipher = self._get_cipher(sessionid, iv)\n        try:\n            return cipher.decrypt(data, aad=id)\n        except IntegrityError as e:\n            raise IntegrityError(f\"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]\")\n\n    def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):\n        assert len(crypt_key) in (32 + 32, 32 + 128)\n        assert len(id_key) in (32, 128)\n        assert isinstance(chunk_seed, int)\n        self.crypt_key = crypt_key\n        self.id_key = id_key\n        self.chunk_seed = chunk_seed\n\n    def init_from_random_data(self):\n        data = os.urandom(100)\n        chunk_seed = bytes_to_int(data[96:100])\n        # Convert to signed int32\n        if chunk_seed & 0x80000000:\n            chunk_seed = chunk_seed - 0xFFFFFFFF - 1\n        self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)\n\n    def _get_session_key(self, sessionid, domain=None):\n        \"\"\"\n        Derive a session key from the secret long-term static crypt_key (which is a fully random PRK)\n        and the session id (which is fully random also).\n        Optionally, a domain can be given for domain separation (defaults to a different binary string\n        per cipher suite).\n        \"\"\"\n        # Performance note:\n        # While this is only invoked once per session to generate a new key for encrypting new data, it is invoked\n        # frequently (per encrypted repo object) to compute the corresponding key for decrypting existing data.\n        assert len(sessionid) == 24  # 192bit\n        if domain is None:\n            domain = b\"borg-session-key-\" + self.CIPHERSUITE.__name__.encode()\n        return self.derive_key(salt=sessionid, domain=domain, size=32)  # 256bit\n\n    def _get_cipher(self, sessionid, iv):\n        assert isinstance(iv, int)\n        key = self._get_session_key(sessionid)\n        cipher = self.CIPHERSUITE(key=key, iv=iv, header_len=1 + 1 + 6 + 24, aad_offset=0)\n        return cipher\n\n    def init_ciphers(self, manifest_data=None, iv=0):\n        # in every new session we start with a fresh sessionid and at iv == 0, manifest_data and iv params are ignored\n        self.sessionid = os.urandom(24)\n        self.cipher = self._get_cipher(self.sessionid, iv=0)\n\n\nclass AESOCBKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}\n    TYPE = KeyType.AESOCBKEYFILE\n    NAME = \"key file AES-OCB\"\n    ARG_NAME = \"keyfile-aes-ocb\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = AES256_OCB\n\n\nclass AESOCBRepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}\n    TYPE = KeyType.AESOCBREPO\n    NAME = \"repokey AES-OCB\"\n    ARG_NAME = \"repokey-aes-ocb\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = AES256_OCB\n\n\nclass CHPOKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}\n    TYPE = KeyType.CHPOKEYFILE\n    NAME = \"key file ChaCha20-Poly1305\"\n    ARG_NAME = \"keyfile-chacha20-poly1305\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = CHACHA20_POLY1305\n\n\nclass CHPORepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}\n    TYPE = KeyType.CHPOREPO\n    NAME = \"repokey ChaCha20-Poly1305\"\n    ARG_NAME = \"repokey-chacha20-poly1305\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = CHACHA20_POLY1305\n\n\nclass Blake2AESOCBKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}\n    TYPE = KeyType.BLAKE2AESOCBKEYFILE\n    NAME = \"key file BLAKE2b AES-OCB\"\n    ARG_NAME = \"keyfile-blake2-aes-ocb\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = AES256_OCB\n\n\nclass Blake2AESOCBRepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}\n    TYPE = KeyType.BLAKE2AESOCBREPO\n    NAME = \"repokey BLAKE2b AES-OCB\"\n    ARG_NAME = \"repokey-blake2-aes-ocb\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = AES256_OCB\n\n\nclass Blake2CHPOKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}\n    TYPE = KeyType.BLAKE2CHPOKEYFILE\n    NAME = \"key file BLAKE2b ChaCha20-Poly1305\"\n    ARG_NAME = \"keyfile-blake2-chacha20-poly1305\"\n    STORAGE = KeyBlobStorage.KEYFILE\n    CIPHERSUITE = CHACHA20_POLY1305\n\n\nclass Blake2CHPORepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):\n    TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}\n    TYPE = KeyType.BLAKE2CHPOREPO\n    NAME = \"repokey BLAKE2b ChaCha20-Poly1305\"\n    ARG_NAME = \"repokey-blake2-chacha20-poly1305\"\n    STORAGE = KeyBlobStorage.REPO\n    CIPHERSUITE = CHACHA20_POLY1305\n\n\nLEGACY_KEY_TYPES = (\n    # legacy (AES-CTR based) crypto\n    KeyfileKey,\n    RepoKey,\n    Blake2KeyfileKey,\n    Blake2RepoKey,\n)\n\nAVAILABLE_KEY_TYPES = (\n    # these are available encryption modes for new repositories\n    # not encrypted modes\n    PlaintextKey,\n    AuthenticatedKey,\n    Blake2AuthenticatedKey,\n    # new crypto\n    AESOCBKeyfileKey,\n    AESOCBRepoKey,\n    CHPOKeyfileKey,\n    CHPORepoKey,\n    Blake2AESOCBKeyfileKey,\n    Blake2AESOCBRepoKey,\n    Blake2CHPOKeyfileKey,\n    Blake2CHPORepoKey,\n)\n"
  },
  {
    "path": "src/borg/crypto/keymanager.py",
    "content": "import binascii\nimport pkgutil\nimport textwrap\nfrom hashlib import sha256\n\nfrom ..helpers import Error, yes, bin_to_hex, hex_to_bin, dash_open\nfrom ..repoobj import RepoObj\n\n\nfrom .key import CHPOKeyfileKey, RepoKeyNotFoundError, KeyBlobStorage, identify_key\n\n\nclass NotABorgKeyFile(Error):\n    \"\"\"This file is not a Borg key backup, aborting.\"\"\"\n\n    exit_mcode = 43\n\n\nclass RepoIdMismatch(Error):\n    \"\"\"This key backup seems to be for a different backup repository, aborting.\"\"\"\n\n    exit_mcode = 45\n\n\nclass UnencryptedRepo(Error):\n    \"\"\"Key management not available for unencrypted repositories.\"\"\"\n\n    exit_mcode = 46\n\n\nclass UnknownKeyType(Error):\n    \"\"\"Key type {0} is unknown.\"\"\"\n\n    exit_mcode = 47\n\n\ndef sha256_truncated(data, num):\n    h = sha256()\n    h.update(data)\n    return h.hexdigest()[:num]\n\n\nclass KeyManager:\n    def __init__(self, repository):\n        self.repository = repository\n        self.keyblob = None\n        self.keyblob_storage = None\n\n        manifest_chunk = repository.get_manifest()\n        manifest_data = RepoObj.extract_crypted_data(manifest_chunk)\n        key = identify_key(manifest_data)\n        self.keyblob_storage = key.STORAGE\n        if self.keyblob_storage == KeyBlobStorage.NO_STORAGE:\n            raise UnencryptedRepo()\n\n    def load_keyblob(self):\n        if self.keyblob_storage == KeyBlobStorage.KEYFILE:\n            k = CHPOKeyfileKey(self.repository)\n            target = k.find_key()\n            with open(target) as fd:\n                self.keyblob = \"\".join(fd.readlines()[1:])\n\n        elif self.keyblob_storage == KeyBlobStorage.REPO:\n            key_data = self.repository.load_key().decode()\n            if not key_data:\n                # if we got an empty key, it means there is no key.\n                loc = self.repository._location.canonical_path()\n                raise RepoKeyNotFoundError(loc) from None\n            self.keyblob = key_data\n\n    def store_keyblob(self, args):\n        if self.keyblob_storage == KeyBlobStorage.KEYFILE:\n            k = CHPOKeyfileKey(self.repository)\n            target = k.get_existing_or_new_target(args)\n\n            self.store_keyfile(target)\n        elif self.keyblob_storage == KeyBlobStorage.REPO:\n            self.repository.save_key(self.keyblob.encode(\"utf-8\"))\n\n    def get_keyfile_data(self):\n        data = f\"{CHPOKeyfileKey.FILE_ID} {bin_to_hex(self.repository.id)}\\n\"\n        data += self.keyblob\n        if not self.keyblob.endswith(\"\\n\"):\n            data += \"\\n\"\n        return data\n\n    def store_keyfile(self, target):\n        with dash_open(target, \"w\") as fd:\n            fd.write(self.get_keyfile_data())\n\n    def export(self, path):\n        if path is None:\n            path = \"-\"\n\n        self.store_keyfile(path)\n\n    def export_qr(self, path):\n        if path is None:\n            path = \"-\"\n\n        with dash_open(path, \"wb\") as fd:\n            key_data = self.get_keyfile_data()\n            html = pkgutil.get_data(\"borg\", \"paperkey.html\")\n            html = html.replace(b\"</textarea>\", key_data.encode() + b\"</textarea>\")\n            fd.write(html)\n\n    def export_paperkey(self, path):\n        if path is None:\n            path = \"-\"\n\n        def grouped(s):\n            ret = \"\"\n            i = 0\n            for ch in s:\n                if i and i % 6 == 0:\n                    ret += \" \"\n                ret += ch\n                i += 1\n            return ret\n\n        export = \"To restore key use borg key import --paper /path/to/repo\\n\\n\"\n\n        binary = binascii.a2b_base64(self.keyblob)\n        export += \"BORG PAPER KEY v1\\n\"\n        lines = (len(binary) + 17) // 18\n        repoid = bin_to_hex(self.repository.id)[:18]\n        complete_checksum = sha256_truncated(binary, 12)\n        export += \"id: {:d} / {} / {} - {}\\n\".format(\n            lines,\n            grouped(repoid),\n            grouped(complete_checksum),\n            sha256_truncated((str(lines) + \"/\" + repoid + \"/\" + complete_checksum).encode(\"ascii\"), 2),\n        )\n        idx = 0\n        while len(binary):\n            idx += 1\n            binline = binary[:18]\n            checksum = sha256_truncated(idx.to_bytes(2, byteorder=\"big\") + binline, 2)\n            export += f\"{idx:2d}: {grouped(bin_to_hex(binline))} - {checksum}\\n\"\n            binary = binary[18:]\n\n        with dash_open(path, \"w\") as fd:\n            fd.write(export)\n\n    def import_keyfile(self, args):\n        file_id = CHPOKeyfileKey.FILE_ID\n        first_line = file_id + \" \" + bin_to_hex(self.repository.id) + \"\\n\"\n        with dash_open(args.path, \"r\") as fd:\n            file_first_line = fd.read(len(first_line))\n            if file_first_line != first_line:\n                if not file_first_line.startswith(file_id):\n                    raise NotABorgKeyFile()\n                else:\n                    raise RepoIdMismatch()\n            self.keyblob = fd.read()\n\n        self.store_keyblob(args)\n\n    def import_paperkey(self, args):\n        try:\n            # imported here because it has global side effects\n            import readline  # noqa\n        except ImportError:\n            print(\"Note: No line editing available due to missing readline support\")\n\n        repoid = bin_to_hex(self.repository.id)[:18]\n        try:\n            while True:  # used for repeating on overall checksum mismatch\n                # id line input\n                while True:\n                    idline = input(\"id: \").replace(\" \", \"\")\n                    if idline == \"\":\n                        if yes(\"Abort import? [yN]:\"):\n                            raise EOFError()\n\n                    try:\n                        (data, checksum) = idline.split(\"-\")\n                    except ValueError:\n                        print(\"each line must contain exactly one '-', try again\")\n                        continue\n                    try:\n                        (id_lines, id_repoid, id_complete_checksum) = data.split(\"/\")\n                    except ValueError:\n                        print(\"the id line must contain exactly two '/', try again\")\n                        continue\n                    if sha256_truncated(data.lower().encode(\"ascii\"), 2) != checksum:\n                        print(\"line checksum did not match, try same line again\")\n                        continue\n                    try:\n                        lines = int(id_lines)\n                    except ValueError:\n                        print(\"internal error while parsing length\")\n\n                    break\n\n                if repoid != id_repoid:\n                    raise RepoIdMismatch()\n\n                result = b\"\"\n                idx = 1\n                # body line input\n                while True:\n                    inline = input(f\"{idx:2d}: \")\n                    inline = inline.replace(\" \", \"\")\n                    if inline == \"\":\n                        if yes(\"Abort import? [yN]:\"):\n                            raise EOFError()\n                    try:\n                        (data, checksum) = inline.split(\"-\")\n                    except ValueError:\n                        print(\"each line must contain exactly one '-', try again\")\n                        continue\n                    try:\n                        part = hex_to_bin(data)\n                    except ValueError as e:\n                        print(f\"only characters 0-9 and a-f and '-' are valid, try again [{e}]\")\n                        continue\n                    if sha256_truncated(idx.to_bytes(2, byteorder=\"big\") + part, 2) != checksum:\n                        print(f\"line checksum did not match, try line {idx} again\")\n                        continue\n                    result += part\n                    if idx == lines:\n                        break\n                    idx += 1\n\n                if sha256_truncated(result, 12) != id_complete_checksum:\n                    print(\"The overall checksum did not match, retry or enter a blank line to abort.\")\n                    continue\n\n                self.keyblob = \"\\n\".join(textwrap.wrap(binascii.b2a_base64(result).decode(\"ascii\"))) + \"\\n\"\n                self.store_keyblob(args)\n                break\n\n        except EOFError:\n            print(\"\\n - aborted\")\n            return\n"
  },
  {
    "path": "src/borg/crypto/low_level.pyi",
    "content": "# Type stubs for borg.crypto.low_level\n# This file provides type hints for the Cython extension module\n\nfrom typing import Optional, Union\n\n# Module-level functions\ndef num_cipher_blocks(length: int, blocksize: int = 16) -> int:\n    \"\"\"Return the number of cipher blocks required to encrypt/decrypt <length> bytes of data.\"\"\"\n    ...\n\ndef bytes_to_int(x: bytes, offset: int = 0) -> int: ...\ndef bytes_to_long(x: bytes, offset: int = 0) -> int: ...\ndef long_to_bytes(x: int) -> bytes: ...\ndef hmac_sha256(key: bytes, data: bytes) -> bytes: ...\ndef blake2b_256(key: bytes, data: bytes) -> bytes: ...\ndef blake2b_128(data: bytes) -> bytes: ...\n\n# Exception classes\nclass CryptoError(Exception):\n    \"\"\"Malfunction in the crypto module.\"\"\"\n\n    ...\n\nclass IntegrityError(CryptoError):\n    \"\"\"Integrity checks failed. Corrupted or tampered data.\"\"\"\n\n    ...\n\n# Cipher classes\nclass UNENCRYPTED:\n    \"\"\"Unencrypted cipher suite (no encryption, no MAC).\"\"\"\n\n    header_len: int\n    iv: Optional[Union[int, bytes]]\n\n    def __init__(\n        self,\n        mac_key: None,\n        enc_key: None,\n        iv: Optional[Union[int, bytes]] = None,\n        header_len: int = 1,\n        aad_offset: int = 1,\n    ) -> None: ...\n    def encrypt(\n        self, data: bytes, header: bytes = b\"\", iv: Optional[Union[int, bytes]] = None, aad: Optional[bytes] = None\n    ) -> bytes: ...\n    def decrypt(self, envelope: bytes, aad: Optional[bytes] = None) -> memoryview: ...\n    def block_count(self, length: int) -> int: ...\n    def set_iv(self, iv: Union[int, bytes]) -> None: ...\n    def next_iv(self) -> Union[int, bytes]: ...\n    def extract_iv(self, envelope: bytes) -> int: ...\n\nclass AES256_CTR_BASE:\n    \"\"\"Base class for AES-256-CTR based cipher suites.\"\"\"\n\n    @classmethod\n    def requirements_check(cls) -> None: ...\n    def __init__(\n        self,\n        mac_key: bytes,\n        enc_key: bytes,\n        iv: Optional[Union[int, bytes]] = None,\n        header_len: int = 1,\n        aad_offset: int = 1,\n    ) -> None: ...\n    def encrypt(\n        self, data: bytes, header: bytes = b\"\", iv: Optional[Union[int, bytes]] = None, aad: Optional[bytes] = None\n    ) -> bytes: ...\n    def decrypt(self, envelope: bytes, aad: Optional[bytes] = None) -> bytes: ...\n    def block_count(self, length: int) -> int: ...\n    def set_iv(self, iv: Union[int, bytes]) -> None: ...\n    def next_iv(self) -> int: ...\n    def extract_iv(self, envelope: bytes) -> int: ...\n\nclass AES256_CTR_HMAC_SHA256(AES256_CTR_BASE):\n    \"\"\"AES-256-CTR with HMAC-SHA256 authentication.\"\"\"\n\n    def __init__(\n        self,\n        mac_key: bytes,\n        enc_key: bytes,\n        iv: Optional[Union[int, bytes]] = None,\n        header_len: int = 1,\n        aad_offset: int = 1,\n    ) -> None: ...\n\nclass AES256_CTR_BLAKE2b(AES256_CTR_BASE):\n    \"\"\"AES-256-CTR with BLAKE2b authentication.\"\"\"\n\n    def __init__(\n        self,\n        mac_key: bytes,\n        enc_key: bytes,\n        iv: Optional[Union[int, bytes]] = None,\n        header_len: int = 1,\n        aad_offset: int = 1,\n    ) -> None: ...\n\nclass _AEAD_BASE:\n    \"\"\"Base class for AEAD cipher suites.\"\"\"\n\n    @classmethod\n    def requirements_check(cls) -> None:\n        \"\"\"Check whether library requirements for this ciphersuite are satisfied.\"\"\"\n        ...\n\n    def __init__(\n        self, key: bytes, iv: Optional[Union[int, bytes]] = None, header_len: int = 0, aad_offset: int = 0\n    ) -> None: ...\n    def encrypt(\n        self, data: bytes, header: bytes = b\"\", iv: Optional[Union[int, bytes]] = None, aad: bytes = b\"\"\n    ) -> bytes: ...\n    def decrypt(self, envelope: bytes, aad: bytes = b\"\") -> bytes: ...\n    def block_count(self, length: int) -> int: ...\n    def set_iv(self, iv: Union[int, bytes]) -> None: ...\n    def next_iv(self) -> int: ...\n\nclass AES256_OCB(_AEAD_BASE):\n    \"\"\"AES-256-OCB AEAD cipher suite.\"\"\"\n\n    @classmethod\n    def requirements_check(cls) -> None: ...\n    def __init__(\n        self, key: bytes, iv: Optional[Union[int, bytes]] = None, header_len: int = 0, aad_offset: int = 0\n    ) -> None: ...\n\nclass CHACHA20_POLY1305(_AEAD_BASE):\n    \"\"\"ChaCha20-Poly1305 AEAD cipher suite.\"\"\"\n\n    @classmethod\n    def requirements_check(cls) -> None: ...\n    def __init__(\n        self, key: bytes, iv: Optional[Union[int, bytes]] = None, header_len: int = 0, aad_offset: int = 0\n    ) -> None: ...\n\nclass AES:\n    \"\"\"A thin wrapper around the OpenSSL EVP cipher API - for legacy code, like key file encryption.\"\"\"\n\n    def __init__(self, enc_key: bytes, iv: Optional[Union[int, bytes]] = None) -> None: ...\n    def encrypt(self, data: bytes, iv: Optional[Union[int, bytes]] = None) -> bytes: ...\n    def decrypt(self, data: bytes) -> bytes: ...\n    def block_count(self, length: int) -> int: ...\n    def set_iv(self, iv: Union[int, bytes]) -> None: ...\n    def next_iv(self) -> int: ...\n\nclass CSPRNG:\n    \"\"\"\n    Cryptographically Secure Pseudo-Random Number Generator based on AES-CTR mode.\n\n    This class provides methods for generating random bytes and shuffling lists\n    using a deterministic algorithm seeded with a 256-bit key.\n    \"\"\"\n\n    def __init__(self, seed_key: bytes) -> None:\n        \"\"\"\n        Initialize the CSPRNG with a 256-bit key.\n\n        :param seed_key: A 32-byte key used as the seed for the CSPRNG\n        \"\"\"\n        ...\n\n    def random_bytes(self, n: int) -> bytes:\n        \"\"\"\n        Generate n random bytes.\n\n        :param n: Number of bytes to generate\n        :return: a bytes object containing the random bytes\n        \"\"\"\n        ...\n\n    def random_int(self, n: int) -> int:\n        \"\"\"\n        Generate a random integer in the range [0, n).\n\n        :param n: Upper bound (exclusive)\n        :return: Random integer\n        \"\"\"\n        ...\n\n    def shuffle(self, items: list) -> None:\n        \"\"\"\n        Shuffle a list in-place using the Fisher-Yates algorithm.\n\n        :param items: List to shuffle\n        \"\"\"\n        ...\n"
  },
  {
    "path": "src/borg/crypto/low_level.pyx",
    "content": "\"\"\"An AEAD-style OpenSSL wrapper\n\nAPI:\n\n    encrypt(data, header=b'', aad_offset=0) -> envelope\n    decrypt(envelope, header_len=0, aad_offset=0) -> data\n\nEnvelope layout:\n\n|<--------------------------- envelope ------------------------------------------>|\n|<------------ header ----------->|<---------- ciphersuite specific ------------->|\n|<-- not auth data -->|<-- aad -->|<-- e.g.:  S(aad, iv, E(data)), iv, E(data) -->|\n\n|--- #aad_offset ---->|\n|------------- #header_len ------>|\n\nS means a cryptographic signature function (like HMAC or GMAC).\nE means an encryption function (like AES).\niv is the initialization vector / nonce, if needed.\n\nThe split of header into not-authenticated data and AAD (additional authenticated\ndata) is done to support the legacy envelope layout as used in Attic and early Borg\n(where the TYPE byte was not authenticated) and avoid unneeded memcpy and string\ngarbage.\n\nNewly designed envelope layouts can just authenticate the whole header.\n\nIV handling:\n\n    iv = ...  # just never repeat!\n    cs = CS(hmac_key, enc_key, iv=iv)\n    envelope = cs.encrypt(data, header, aad_offset)\n    iv = cs.next_iv(len(data))\n    (repeat)\n\"\"\"\n\nimport hashlib\nimport hmac\nfrom math import ceil\n\nfrom cpython cimport PyMem_Malloc, PyMem_Free\nfrom cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release\nfrom cpython.bytes cimport PyBytes_FromStringAndSize, PyBytes_AsString\nfrom libc.stdlib cimport malloc, free\nfrom libc.stdint cimport uint8_t, uint32_t, uint64_t\nfrom libc.string cimport memset, memcpy\n\n\n\ncdef extern from \"openssl/crypto.h\":\n    int CRYPTO_memcmp(const void *a, const void *b, size_t len)\n\ncdef extern from \"openssl/opensslv.h\":\n    long OPENSSL_VERSION_NUMBER\n\ncdef extern from \"openssl/evp.h\":\n    ctypedef struct EVP_MD:\n        pass\n    ctypedef struct EVP_CIPHER:\n        pass\n    ctypedef struct EVP_CIPHER_CTX:\n        pass\n    ctypedef struct ENGINE:\n        pass\n\n    const EVP_CIPHER *EVP_aes_256_ctr()\n    const EVP_CIPHER *EVP_aes_256_ocb()\n    const EVP_CIPHER *EVP_chacha20_poly1305()\n\n    EVP_CIPHER_CTX *EVP_CIPHER_CTX_new()\n    void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *a)\n    void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a)\n    void EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *a)\n\n    int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl,\n                           const unsigned char *key, const unsigned char *iv)\n    int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl,\n                           const unsigned char *key, const unsigned char *iv)\n    int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,\n                          const unsigned char *in_, int inl)\n    int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,\n                          const unsigned char *in_, int inl)\n    int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl)\n    int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl)\n\n    int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, int arg, void *ptr)\n    int EVP_CTRL_AEAD_GET_TAG\n    int EVP_CTRL_AEAD_SET_TAG\n    int EVP_CTRL_AEAD_SET_IVLEN\n\n\nimport struct\n\n_int = struct.Struct('>I')\n_long = struct.Struct('>Q')\n\nbytes_to_int = lambda x, offset=0: _int.unpack_from(x, offset)[0]\nbytes_to_long = lambda x, offset=0: _long.unpack_from(x, offset)[0]\nlong_to_bytes = lambda x: _long.pack(x)\n\n\ndef num_cipher_blocks(length, blocksize=16):\n    \"\"\"Return the number of cipher blocks required to encrypt/decrypt <length> bytes of data.\n\n    For a precise computation, <blocksize> must be the used cipher's block size (AES: 16, CHACHA20: 64).\n\n    For a safe-upper-boundary computation, <blocksize> must be the MINIMUM of the block sizes (in\n    bytes) of ALL supported ciphers. This can be used to adjust a counter if the used cipher is not\n    known (yet).\n    The default value of blocksize must be adjusted so it reflects this minimum, so a call of this\n    function without a blocksize is \"safe-upper-boundary by default\".\n\n    Padding cipher modes are not supported.\n    \"\"\"\n    return (length + blocksize - 1) // blocksize\n\n\nclass CryptoError(Exception):\n    \"\"\"Malfunction in the crypto module.\"\"\"\n\n\nclass IntegrityError(CryptoError):\n    \"\"\"Integrity checks failed. Corrupted or tampered data.\"\"\"\n\n\ncdef Py_buffer ro_buffer(object data) except *:\n    cdef Py_buffer view\n    PyObject_GetBuffer(data, &view, PyBUF_SIMPLE)\n    return view\n\n\nclass UNENCRYPTED:\n    # Layout: HEADER + PlainText\n\n    def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        assert mac_key is None\n        assert enc_key is None\n        self.header_len = header_len\n        self.set_iv(iv)\n\n    def encrypt(self, data, header=b'', iv=None, aad=None):\n        \"\"\"\n        IMPORTANT: it is called encrypt to satisfy the crypto api naming convention,\n        but this does NOT encrypt and it does NOT compute and store a MAC either.\n        \"\"\"\n        if iv is not None:\n            self.set_iv(iv)\n        assert self.iv is not None, 'iv needs to be set before encrypt is called'\n        return header + data\n\n    def decrypt(self, envelope, aad=None):\n        \"\"\"\n        IMPORTANT: it is called decrypt to satisfy the crypto api naming convention,\n        but this does NOT decrypt and it does NOT verify a MAC either, because data\n        is not encrypted and there is no MAC.\n        \"\"\"\n        return memoryview(envelope)[self.header_len:]\n\n    def block_count(self, length):\n        return 0\n\n    def set_iv(self, iv):\n        self.iv = iv\n\n    def next_iv(self):\n        return self.iv\n\n    def extract_iv(self, envelope):\n        return 0\n\n\ncdef class AES256_CTR_BASE:\n    # Layout: HEADER + MAC 32 + IV 8 + CT (same as attic / borg < 2.0 IF HEADER = TYPE_BYTE, no AAD)\n\n    cdef EVP_CIPHER_CTX *ctx\n    cdef unsigned char enc_key[32]\n    cdef int cipher_blk_len\n    cdef int iv_len, iv_len_short\n    cdef int aad_offset\n    cdef int header_len\n    cdef int mac_len\n    cdef unsigned char iv[16]\n    cdef long long blocks\n\n    @classmethod\n    def requirements_check(cls):\n        pass\n\n    def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        self.requirements_check()\n        assert isinstance(enc_key, bytes) and len(enc_key) == 32\n        self.cipher_blk_len = 16\n        self.iv_len = sizeof(self.iv)\n        self.iv_len_short = 8\n        assert aad_offset <= header_len\n        self.aad_offset = aad_offset\n        self.header_len = header_len\n        self.mac_len = 32\n        self.enc_key = enc_key\n        if iv is not None:\n            self.set_iv(iv)\n        else:\n            self.blocks = -1  # make sure set_iv is called before encrypt\n\n    def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        self.ctx = EVP_CIPHER_CTX_new()\n\n    def __dealloc__(self):\n        EVP_CIPHER_CTX_free(self.ctx)\n\n    cdef mac_compute(self, const unsigned char *data1, int data1_len,\n                     const unsigned char *data2, int data2_len,\n                     unsigned char *mac_buf):\n        raise NotImplementedError\n\n    cdef mac_verify(self, const unsigned char *data1, int data1_len,\n                    const unsigned char *data2, int data2_len,\n                    unsigned char *mac_buf, const unsigned char *mac_wanted):\n        \"\"\"\n        Calculate MAC of *data1*, *data2*, write result to *mac_buf*, and verify against *mac_wanted.*\n        \"\"\"\n        raise NotImplementedError\n\n    def encrypt(self, data, header=b'', iv=None, aad=None):\n        \"\"\"\n        encrypt data, compute mac over aad + iv + cdata, prepend header.\n        aad_offset is the offset into the header where aad starts.\n        \"\"\"\n        if iv is not None:\n            self.set_iv(iv)\n        assert self.blocks == 0, 'iv needs to be set before encrypt is called'\n        cdef int ilen = len(data)\n        cdef int hlen = len(header)\n        assert hlen == self.header_len\n        cdef int aoffset = self.aad_offset\n        cdef int alen = hlen - aoffset\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(hlen + self.mac_len + self.iv_len_short +\n                                                                  ilen + self.cipher_blk_len)  # play safe, 1 extra blk\n        if not odata:\n            raise MemoryError\n        cdef int olen = 0\n        cdef int offset\n        cdef Py_buffer idata = ro_buffer(data)\n        cdef Py_buffer hdata = ro_buffer(header)\n        try:\n            offset = 0\n            for i in range(hlen):\n                odata[offset+i] = header[i]\n            offset += hlen\n            offset += self.mac_len\n            self.store_iv(odata+offset, self.iv)\n            offset += self.iv_len_short\n            if not EVP_EncryptInit_ex(self.ctx, EVP_aes_256_ctr(), NULL, self.enc_key, self.iv):\n                raise CryptoError('EVP_EncryptInit_ex failed')\n            if not EVP_EncryptUpdate(self.ctx, odata+offset, &olen, <const unsigned char*> idata.buf, ilen):\n                raise CryptoError('EVP_EncryptUpdate failed')\n            offset += olen\n            if not EVP_EncryptFinal_ex(self.ctx, odata+offset, &olen):\n                raise CryptoError('EVP_EncryptFinal_ex failed')\n            offset += olen\n            self.mac_compute(<const unsigned char *> hdata.buf+aoffset, alen,\n                              odata+hlen+self.mac_len, offset-hlen-self.mac_len,\n                              odata+hlen)\n            self.blocks += self.block_count(ilen)\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&hdata)\n            PyBuffer_Release(&idata)\n\n    def decrypt(self, envelope, aad=None):\n        \"\"\"\n        authenticate aad + iv + cdata, decrypt cdata, ignore header bytes up to aad_offset.\n        \"\"\"\n        cdef int ilen = len(envelope)\n        cdef int hlen = self.header_len\n        cdef int aoffset = self.aad_offset\n        cdef int alen = hlen - aoffset\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len)  # play safe, 1 extra blk\n        if not odata:\n            raise MemoryError\n        cdef int olen = 0\n        cdef int offset\n        cdef unsigned char mac_buf[32]\n        assert sizeof(mac_buf) == self.mac_len\n        cdef Py_buffer idata = ro_buffer(envelope)\n        try:\n            self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,\n                             <const unsigned char *> idata.buf+hlen+self.mac_len, ilen-hlen-self.mac_len,\n                             mac_buf, <const unsigned char *> idata.buf+hlen)\n            iv = self.fetch_iv(<unsigned char *> idata.buf+hlen+self.mac_len)\n            self.set_iv(iv)\n            if not EVP_DecryptInit_ex(self.ctx, EVP_aes_256_ctr(), NULL, self.enc_key, iv):\n                raise CryptoError('EVP_DecryptInit_ex failed')\n            offset = 0\n            if not EVP_DecryptUpdate(self.ctx, odata+offset, &olen,\n                                     <const unsigned char*> idata.buf+hlen+self.mac_len+self.iv_len_short,\n                                     ilen-hlen-self.mac_len-self.iv_len_short):\n                raise CryptoError('EVP_DecryptUpdate failed')\n            offset += olen\n            if not EVP_DecryptFinal_ex(self.ctx, odata+offset, &olen):\n                raise CryptoError('EVP_DecryptFinal_ex failed')\n            offset += olen\n            self.blocks += self.block_count(offset)\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&idata)\n\n    def block_count(self, length):\n        return num_cipher_blocks(length, self.cipher_blk_len)\n\n    def set_iv(self, iv):\n        # set_iv needs to be called before each encrypt() call\n        if isinstance(iv, int):\n            iv = iv.to_bytes(self.iv_len, byteorder='big')\n        assert isinstance(iv, bytes) and len(iv) == self.iv_len\n        self.iv = iv\n        self.blocks = 0  # how many AES blocks got encrypted with this IV?\n\n    def next_iv(self):\n        # call this after encrypt() to get the next iv (int) for the next encrypt() call\n        iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')\n        return iv + self.blocks\n\n    cdef fetch_iv(self, unsigned char * iv_in):\n        # fetch lower self.iv_len_short bytes of iv and add upper zero bytes\n        return b'\\0' * (self.iv_len - self.iv_len_short) + iv_in[0:self.iv_len_short]\n\n    cdef store_iv(self, unsigned char * iv_out, unsigned char * iv):\n        # store only lower self.iv_len_short bytes, upper bytes are assumed to be 0\n        cdef int i\n        for i in range(self.iv_len_short):\n            iv_out[i] = iv[(self.iv_len-self.iv_len_short)+i]\n\n    def extract_iv(self, envelope):\n        offset = self.header_len + self.mac_len\n        return bytes_to_long(envelope[offset:offset+self.iv_len_short])\n\n\ncdef class AES256_CTR_HMAC_SHA256(AES256_CTR_BASE):\n    cdef unsigned char mac_key[32]\n\n    def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        assert isinstance(mac_key, bytes) and len(mac_key) == 32\n        self.mac_key = mac_key\n        super().__init__(mac_key, enc_key, iv=iv, header_len=header_len, aad_offset=aad_offset)\n\n    def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        pass\n\n    def __dealloc__(self):\n        pass\n\n    cdef mac_compute(self, const unsigned char *data1, int data1_len,\n                     const unsigned char *data2, int data2_len,\n                     unsigned char *mac_buf):\n        data = data1[:data1_len] + data2[:data2_len]\n        mac = hmac.digest(self.mac_key[:self.mac_len], data, 'sha256')\n        for i in range(self.mac_len):\n            mac_buf[i] = mac[i]\n\n    cdef mac_verify(self, const unsigned char *data1, int data1_len,\n                    const unsigned char *data2, int data2_len,\n                    unsigned char *mac_buf, const unsigned char *mac_wanted):\n        self.mac_compute(data1, data1_len, data2, data2_len, mac_buf)\n        if CRYPTO_memcmp(mac_buf, mac_wanted, self.mac_len):\n            raise IntegrityError('MAC Authentication failed')\n\n\ncdef class AES256_CTR_BLAKE2b(AES256_CTR_BASE):\n    cdef unsigned char mac_key[128]\n\n    def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        assert isinstance(mac_key, bytes) and len(mac_key) == 128\n        self.mac_key = mac_key\n        super().__init__(mac_key, enc_key, iv=iv, header_len=header_len, aad_offset=aad_offset)\n\n    def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):\n        pass\n\n    def __dealloc__(self):\n        pass\n\n    cdef mac_compute(self, const unsigned char *data1, int data1_len,\n                     const unsigned char *data2, int data2_len,\n                     unsigned char *mac_buf):\n        data = self.mac_key[:128] + data1[:data1_len] + data2[:data2_len]\n        mac = hashlib.blake2b(data, digest_size=self.mac_len).digest()\n        for i in range(self.mac_len):\n            mac_buf[i] = mac[i]\n\n    cdef mac_verify(self, const unsigned char *data1, int data1_len,\n                    const unsigned char *data2, int data2_len,\n                    unsigned char *mac_buf, const unsigned char *mac_wanted):\n        self.mac_compute(data1, data1_len, data2, data2_len, mac_buf)\n        if CRYPTO_memcmp(mac_buf, mac_wanted, self.mac_len):\n            raise IntegrityError('MAC Authentication failed')\n\n\nctypedef const EVP_CIPHER * (* CIPHER)()\n\n\ncdef class _AEAD_BASE:\n    # new crypto used in borg >= 2.0\n    # Layout: HEADER + MAC 16 + CT\n\n    cdef CIPHER cipher\n    cdef EVP_CIPHER_CTX *ctx\n    cdef unsigned char key[32]\n    cdef int cipher_blk_len\n    cdef int iv_len\n    cdef int aad_offset\n    cdef int header_len_expected\n    cdef int mac_len\n    cdef unsigned char iv[12]\n    cdef long long blocks\n\n    @classmethod\n    def requirements_check(cls):\n        \"\"\"check whether library requirements for this ciphersuite are satisfied\"\"\"\n        raise NotImplemented  # override / implement in child class\n\n    def __init__(self, key, iv=None, header_len=0, aad_offset=0):\n        \"\"\"\n        init AEAD crypto\n\n        :param key: 256bit encrypt-then-mac key\n        :param iv: 96bit initialisation vector / nonce\n        :param header_len: expected length of header\n        :param aad_offset: where in the header the authenticated data starts\n        \"\"\"\n        assert isinstance(key, bytes) and len(key) == 32\n        self.iv_len = sizeof(self.iv)\n        self.header_len_expected = header_len\n        assert aad_offset <= header_len\n        self.aad_offset = aad_offset\n        self.mac_len = 16\n        self.key = key\n        if iv is not None:\n            self.set_iv(iv)\n        else:\n            self.blocks = -1  # make sure set_iv is called before encrypt\n\n    def __cinit__(self, key, iv=None, header_len=0, aad_offset=0):\n        self.ctx = EVP_CIPHER_CTX_new()\n\n    def __dealloc__(self):\n        EVP_CIPHER_CTX_free(self.ctx)\n\n    def encrypt(self, data, header=b'', iv=None, aad=b''):\n        \"\"\"\n        encrypt data, compute auth tag over aad + header + cdata.\n        return header + auth tag + cdata.\n        aad_offset is the offset into the header where the authenticated header part starts.\n        aad is additional authenticated data, which won't be included in the returned data,\n        but only used for the auth tag computation.\n        \"\"\"\n        if iv is not None:\n            self.set_iv(iv)\n        assert self.blocks == 0, 'iv needs to be set before encrypt is called'\n        # AES-OCB, CHACHA20 ciphers all add a internal 32bit counter to the 96bit (12Byte)\n        # IV we provide, thus we must not encrypt more than 2^32 cipher blocks with same IV).\n        block_count = self.block_count(len(data))\n        if block_count > 2**32:\n            raise ValueError('too much data, would overflow internal 32bit counter')\n        cdef int ilen = len(data)\n        cdef int hlen = len(header)\n        assert hlen == self.header_len_expected\n        cdef int aoffset = self.aad_offset\n        cdef int alen = hlen - aoffset\n        cdef int aadlen = len(aad)\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(hlen + self.mac_len +\n                                                                  ilen + self.cipher_blk_len)\n        if not odata:\n            raise MemoryError\n        cdef int olen = 0\n        cdef int offset\n        cdef Py_buffer idata = ro_buffer(data)\n        cdef Py_buffer hdata = ro_buffer(header)\n        cdef Py_buffer aadata = ro_buffer(aad)\n        try:\n            offset = 0\n            for i in range(hlen):\n                odata[offset+i] = header[i]\n            offset += hlen\n            offset += self.mac_len\n            if not EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL):\n                raise CryptoError('EVP_EncryptInit_ex failed')\n            if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_IVLEN, self.iv_len, NULL):\n                raise CryptoError('EVP_CIPHER_CTX_ctrl SET IVLEN failed')\n            if not EVP_EncryptInit_ex(self.ctx, NULL, NULL, self.key, self.iv):\n                raise CryptoError('EVP_EncryptInit_ex failed')\n            if not EVP_EncryptUpdate(self.ctx, NULL, &olen, <const unsigned char*> aadata.buf, aadlen):\n                raise CryptoError('EVP_EncryptUpdate failed')\n            if not EVP_EncryptUpdate(self.ctx, NULL, &olen, <const unsigned char*> hdata.buf+aoffset, alen):\n                raise CryptoError('EVP_EncryptUpdate failed')\n            if not EVP_EncryptUpdate(self.ctx, odata+offset, &olen, <const unsigned char*> idata.buf, ilen):\n                raise CryptoError('EVP_EncryptUpdate failed')\n            offset += olen\n            if not EVP_EncryptFinal_ex(self.ctx, odata+offset, &olen):\n                raise CryptoError('EVP_EncryptFinal_ex failed')\n            offset += olen\n            if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_GET_TAG, self.mac_len, odata + hlen):\n                raise CryptoError('EVP_CIPHER_CTX_ctrl GET TAG failed')\n            self.blocks = block_count\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&hdata)\n            PyBuffer_Release(&idata)\n            PyBuffer_Release(&aadata)\n\n    def decrypt(self, envelope, aad=b''):\n        \"\"\"\n        authenticate aad + header + cdata (from envelope), ignore header bytes up to aad_offset.,\n        return decrypted cdata.\n        \"\"\"\n        # AES-OCB, CHACHA20 ciphers all add a internal 32bit counter to the 96bit (12Byte)\n        # IV we provide, thus we must not decrypt more than 2^32 cipher blocks with same IV):\n        approx_block_count = self.block_count(len(envelope))  # sloppy, but good enough for borg\n        if approx_block_count > 2**32:\n            raise ValueError('too much data, would overflow internal 32bit counter')\n        cdef int ilen = len(envelope)\n        cdef int hlen = self.header_len_expected\n        cdef int aoffset = self.aad_offset\n        cdef int alen = hlen - aoffset\n        cdef int aadlen = len(aad)\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len)\n        if not odata:\n            raise MemoryError\n        cdef int olen = 0\n        cdef int offset\n        cdef Py_buffer idata = ro_buffer(envelope)\n        cdef Py_buffer aadata = ro_buffer(aad)\n        try:\n            if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL):\n                raise CryptoError('EVP_DecryptInit_ex failed')\n            if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_IVLEN, self.iv_len, NULL):\n                raise CryptoError('EVP_CIPHER_CTX_ctrl SET IVLEN failed')\n            if not EVP_DecryptInit_ex(self.ctx, NULL, NULL, self.key, self.iv):\n                raise CryptoError('EVP_DecryptInit_ex failed')\n            if not EVP_DecryptUpdate(self.ctx, NULL, &olen, <const unsigned char*> aadata.buf, aadlen):\n                raise CryptoError('EVP_DecryptUpdate failed')\n            if not EVP_DecryptUpdate(self.ctx, NULL, &olen, <const unsigned char*> idata.buf+aoffset, alen):\n                raise CryptoError('EVP_DecryptUpdate failed')\n            offset = 0\n            if not EVP_DecryptUpdate(self.ctx, odata+offset, &olen,\n                                     <const unsigned char*> idata.buf+hlen+self.mac_len,\n                                     ilen-hlen-self.mac_len):\n                raise CryptoError('EVP_DecryptUpdate failed')\n            offset += olen\n            if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_TAG, self.mac_len, <unsigned char *> idata.buf + hlen):\n                raise CryptoError('EVP_CIPHER_CTX_ctrl SET TAG failed')\n            if not EVP_DecryptFinal_ex(self.ctx, odata+offset, &olen):\n                # a failure here means corrupted or tampered tag (mac) or data.\n                raise IntegrityError('Authentication / EVP_DecryptFinal_ex failed')\n            offset += olen\n            self.blocks = self.block_count(offset)\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&idata)\n            PyBuffer_Release(&aadata)\n\n    def block_count(self, length):\n        return num_cipher_blocks(length, self.cipher_blk_len)\n\n    def set_iv(self, iv):\n        # set_iv needs to be called before each encrypt() call,\n        # because encrypt does a full initialisation of the cipher context.\n        if isinstance(iv, int):\n            iv = iv.to_bytes(self.iv_len, byteorder='big')\n        assert isinstance(iv, bytes) and len(iv) == self.iv_len\n        self.iv = iv\n        self.blocks = 0  # number of cipher blocks encrypted with this IV\n\n    def next_iv(self):\n        # call this after encrypt() to get the next iv (int) for the next encrypt() call\n        # AES-OCB, CHACHA20 ciphers all add a internal 32bit counter to the 96bit\n        # (12 byte) IV we provide, thus we only need to increment the IV by 1.\n        iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')\n        return iv + 1\n\n\ncdef class AES256_OCB(_AEAD_BASE):\n    @classmethod\n    def requirements_check(cls):\n        pass\n\n    def __init__(self, key, iv=None, header_len=0, aad_offset=0):\n        self.requirements_check()\n        self.cipher = EVP_aes_256_ocb\n        self.cipher_blk_len = 16\n        super().__init__(key, iv=iv, header_len=header_len, aad_offset=aad_offset)\n\n\ncdef class CHACHA20_POLY1305(_AEAD_BASE):\n    @classmethod\n    def requirements_check(cls):\n        pass\n\n    def __init__(self, key, iv=None, header_len=0, aad_offset=0):\n        self.requirements_check()\n        self.cipher = EVP_chacha20_poly1305\n        self.cipher_blk_len = 64\n        super().__init__(key, iv=iv, header_len=header_len, aad_offset=aad_offset)\n\n\ncdef class AES:  # legacy\n    \"\"\"A thin wrapper around the OpenSSL EVP cipher API - for legacy code, like key file encryption\"\"\"\n    cdef CIPHER cipher\n    cdef EVP_CIPHER_CTX *ctx\n    cdef unsigned char enc_key[32]\n    cdef int cipher_blk_len\n    cdef int iv_len\n    cdef unsigned char iv[16]\n    cdef long long blocks\n\n    def __init__(self, enc_key, iv=None):\n        assert isinstance(enc_key, bytes) and len(enc_key) == 32\n        self.enc_key = enc_key\n        self.iv_len = 16\n        assert sizeof(self.iv) == self.iv_len\n        self.cipher = EVP_aes_256_ctr\n        self.cipher_blk_len = 16\n        if iv is not None:\n            self.set_iv(iv)\n        else:\n            self.blocks = -1  # make sure set_iv is called before encrypt\n\n    def __cinit__(self, enc_key, iv=None):\n        self.ctx = EVP_CIPHER_CTX_new()\n\n    def __dealloc__(self):\n        EVP_CIPHER_CTX_free(self.ctx)\n\n    def encrypt(self, data, iv=None):\n        if iv is not None:\n            self.set_iv(iv)\n        assert self.blocks == 0, 'iv needs to be set before encrypt is called'\n        cdef Py_buffer idata = ro_buffer(data)\n        cdef int ilen = len(data)\n        cdef int offset\n        cdef int olen = 0\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len)\n        if not odata:\n            raise MemoryError\n        try:\n            if not EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, self.enc_key, self.iv):\n                raise Exception('EVP_EncryptInit_ex failed')\n            offset = 0\n            if not EVP_EncryptUpdate(self.ctx, odata, &olen, <const unsigned char*> idata.buf, ilen):\n                raise Exception('EVP_EncryptUpdate failed')\n            offset += olen\n            if not EVP_EncryptFinal_ex(self.ctx, odata+offset, &olen):\n                raise Exception('EVP_EncryptFinal failed')\n            offset += olen\n            self.blocks = self.block_count(offset)\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&idata)\n\n    def decrypt(self, data):\n        cdef Py_buffer idata = ro_buffer(data)\n        cdef int ilen = len(data)\n        cdef int offset\n        cdef int olen = 0\n        cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len)\n        if not odata:\n            raise MemoryError\n        try:\n            # Set cipher type and mode\n            if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, self.enc_key, self.iv):\n                raise Exception('EVP_DecryptInit_ex failed')\n            offset = 0\n            if not EVP_DecryptUpdate(self.ctx, odata, &olen, <const unsigned char*> idata.buf, ilen):\n                raise Exception('EVP_DecryptUpdate failed')\n            offset += olen\n            if not EVP_DecryptFinal_ex(self.ctx, odata+offset, &olen):\n                # this error check is very important for modes with padding or\n                # authentication. for them, a failure here means corrupted data.\n                # CTR mode does not use padding nor authentication.\n                raise Exception('EVP_DecryptFinal failed')\n            offset += olen\n            self.blocks = self.block_count(ilen)\n            return odata[:offset]\n        finally:\n            PyMem_Free(odata)\n            PyBuffer_Release(&idata)\n\n    def block_count(self, length):\n        return num_cipher_blocks(length, self.cipher_blk_len)\n\n    def set_iv(self, iv):\n        # set_iv needs to be called before each encrypt() call,\n        # because encrypt does a full initialisation of the cipher context.\n        if isinstance(iv, int):\n            iv = iv.to_bytes(self.iv_len, byteorder='big')\n        assert isinstance(iv, bytes) and len(iv) == self.iv_len\n        self.iv = iv\n        self.blocks = 0  # number of cipher blocks encrypted with this IV\n\n    def next_iv(self):\n        # call this after encrypt() to get the next iv (int) for the next encrypt() call\n        iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')\n        return iv + self.blocks\n\n\ndef hmac_sha256(key, data):\n    return hmac.digest(key, data, 'sha256')\n\n\ndef blake2b_256(key, data):\n    return hashlib.blake2b(key+data, digest_size=32).digest()\n\n\ndef blake2b_128(data):\n    return hashlib.blake2b(data, digest_size=16).digest()\n\n\ncdef class CSPRNG:\n    \"\"\"\n    Cryptographically Secure Pseudo-Random Number Generator based on AES-CTR mode.\n\n    This class provides methods for generating random bytes and shuffling lists\n    using a deterministic algorithm seeded with a 256-bit key.\n\n    The implementation uses AES-256 in CTR mode, which is a well-established\n    method for creating a CSPRNG.\n    \"\"\"\n    cdef EVP_CIPHER_CTX *ctx\n    cdef uint8_t key[32]\n    cdef uint8_t iv[16]\n    cdef uint8_t zeros[4096]  # Static buffer for zeros\n    cdef uint8_t buffer[4096]  # Static buffer for random bytes\n    cdef size_t buffer_size\n    cdef size_t buffer_pos\n\n    def __cinit__(self, bytes seed_key):\n        \"\"\"\n        Initialize the CSPRNG with a 256-bit key.\n\n        :param seed_key: A 32-byte key used as the seed for the CSPRNG\n        \"\"\"\n        if len(seed_key) != 32:\n            raise ValueError(\"Seed key must be 32 bytes (256 bits)\")\n\n        # Initialize context\n        self.ctx = EVP_CIPHER_CTX_new()\n        if self.ctx == NULL:\n            raise MemoryError(\"Failed to allocate cipher context\")\n\n        self.key = seed_key[:32]\n\n        # Initialize to zeros\n        memset(self.iv, 0, 16)\n        memset(self.zeros, 0, 4096)\n\n        self.buffer_size = 4096\n        self.buffer_pos = self.buffer_size  # Force refill on first use\n\n        # Initialize the cipher\n        if not EVP_EncryptInit_ex(self.ctx, EVP_aes_256_ctr(), NULL, self.key, self.iv):\n            EVP_CIPHER_CTX_free(self.ctx)\n            raise CryptoError(\"Failed to initialize AES-CTR cipher\")\n\n    def __dealloc__(self):\n        \"\"\"Free resources when the object is deallocated.\"\"\"\n        if self.ctx != NULL:\n            EVP_CIPHER_CTX_free(self.ctx)\n            self.ctx = NULL\n\n    cdef _refill_buffer(self):\n        \"\"\"Refill the internal buffer with random bytes.\"\"\"\n        cdef int outlen = 0\n\n        # Encrypt zeros to get random bytes\n        if not EVP_EncryptUpdate(self.ctx, self.buffer, &outlen, self.zeros, self.buffer_size):\n            raise CryptoError(\"Failed to generate random bytes\")\n        if outlen != self.buffer_size:\n            raise CryptoError(\"Unexpected length of random bytes\")\n\n        self.buffer_pos = 0\n\n    def random_bytes(self, size_t n):\n        \"\"\"\n        Generate n random bytes.\n\n        :param n: Number of bytes to generate\n        :return: a bytes object containing the random bytes\n        \"\"\"\n        # Directly create a Python bytes object of the required size\n        cdef object py_bytes = PyBytes_FromStringAndSize(NULL, n)\n        cdef uint8_t *result = <uint8_t *>PyBytes_AsString(py_bytes)\n        cdef size_t remaining\n        cdef size_t pos\n        cdef size_t to_copy\n        cdef size_t available\n\n        remaining = n\n        pos = 0\n\n        while remaining > 0:\n            if self.buffer_pos >= self.buffer_size:\n                self._refill_buffer()\n\n            # Calculate how many bytes we can copy\n            available = self.buffer_size - self.buffer_pos\n            to_copy = remaining if remaining < available else available\n\n            # Copy bytes from buffer to result\n            memcpy(result + pos, &self.buffer[self.buffer_pos], to_copy)\n\n            self.buffer_pos += to_copy\n            pos += to_copy\n            remaining -= to_copy\n\n        return py_bytes\n\n    def random_int(self, n):\n        \"\"\"\n        Generate a random integer in the range [0, n).\n\n        :param n: Upper bound (exclusive)\n        :return: Random integer\n        \"\"\"\n        if n <= 0:\n            raise ValueError(\"Upper bound must be positive\")\n        if n == 1:\n            return 0\n\n        # Calculate the number of bits and bytes needed\n        bits_needed = 0\n        temp = n - 1\n        while temp > 0:\n            bits_needed += 1\n            temp >>= 1\n        bytes_needed = (bits_needed + 7) // 8\n\n        # Generate random bytes\n        mask = (1 << bits_needed) - 1\n        max_attempts = 1000  # Prevent infinite loop\n\n        # Rejection sampling to avoid bias\n        attempts = 0\n        while attempts < max_attempts:\n            attempts += 1\n            random_data = self.random_bytes(bytes_needed)\n            result = int.from_bytes(random_data, byteorder='big')\n\n            # Apply mask to get the right number of bits\n            result &= mask\n            if result < n:\n                return result\n\n        # If we reach here, we've made too many attempts\n        # Fall back to a slightly biased but guaranteed-to-terminate method\n        random_data = self.random_bytes(bytes_needed)\n        result = int.from_bytes(random_data, byteorder='big')\n        return result % n\n\n    def shuffle(self, list items):\n        \"\"\"\n        Shuffle a list in-place using the Fisher-Yates algorithm.\n\n        :param items: List to shuffle\n        \"\"\"\n        cdef size_t n = len(items)\n        cdef size_t i, j\n\n        for i in range(n - 1, 0, -1):\n            # Generate random index j such that 0 <= j <= i\n            j = self.random_int(i + 1)\n\n            # Swap items[i] and items[j]\n            items[i], items[j] = items[j], items[i]\n"
  },
  {
    "path": "src/borg/fslocking.py",
    "content": "import errno\nimport json\nimport os\nimport tempfile\nimport time\nfrom pathlib import Path\n\nfrom . import platform\nfrom .helpers import Error, ErrorWithTraceback\nfrom .logger import create_logger\n\nADD, REMOVE, REMOVE2 = \"add\", \"remove\", \"remove2\"\nSHARED, EXCLUSIVE = \"shared\", \"exclusive\"\n\nlogger = create_logger(__name__)\n\n\nclass TimeoutTimer:\n    \"\"\"\n    A timer for timeout checks (can also deal with \"never time out\").\n    It can also compute and optionally execute a reasonable sleep time (e.g. to avoid\n    polling too often or to support thread/process rescheduling).\n    \"\"\"\n\n    def __init__(self, timeout=None, sleep=None):\n        \"\"\"\n        Initialize a timer.\n\n        :param timeout: timeout interval [s] or None (never time out, wait forever) [default]\n        :param sleep: sleep interval [s] (>= 0: do sleep call, <0: don't call sleep)\n                      or None (autocompute: use 10% of timeout [but not more than 60s],\n                      or 1s for \"never timeout\" mode)\n        \"\"\"\n        if timeout is not None and timeout < 0:\n            raise ValueError(\"timeout must be >= 0\")\n        self.timeout_interval = timeout\n        if sleep is None:\n            if timeout is None:\n                sleep = 1.0\n            else:\n                sleep = min(60.0, timeout / 10.0)\n        self.sleep_interval = sleep\n        self.start_time = None\n        self.end_time = None\n\n    def __repr__(self):\n        return \"<{}: start={!r} end={!r} timeout={!r} sleep={!r}>\".format(\n            self.__class__.__name__, self.start_time, self.end_time, self.timeout_interval, self.sleep_interval\n        )\n\n    def start(self):\n        self.start_time = time.time()\n        if self.timeout_interval is not None:\n            self.end_time = self.start_time + self.timeout_interval\n        return self\n\n    def sleep(self):\n        if self.sleep_interval >= 0:\n            time.sleep(self.sleep_interval)\n\n    def timed_out(self):\n        return self.end_time is not None and time.time() >= self.end_time\n\n    def timed_out_or_sleep(self):\n        if self.timed_out():\n            return True\n        else:\n            self.sleep()\n            return False\n\n\nclass LockError(Error):\n    \"\"\"Failed to acquire the lock {}.\"\"\"\n\n    exit_mcode = 70\n\n\nclass LockErrorT(ErrorWithTraceback):\n    \"\"\"Failed to acquire the lock {}.\"\"\"\n\n    exit_mcode = 71\n\n\nclass LockFailed(LockErrorT):\n    \"\"\"Failed to create/acquire the lock {} ({}).\"\"\"\n\n    exit_mcode = 72\n\n\nclass LockTimeout(LockError):\n    \"\"\"Failed to create/acquire the lock {} (timeout).\"\"\"\n\n    exit_mcode = 73\n\n\nclass NotLocked(LockErrorT):\n    \"\"\"Failed to release the lock {} (was not locked).\"\"\"\n\n    exit_mcode = 74\n\n\nclass NotMyLock(LockErrorT):\n    \"\"\"Failed to release the lock {} (was/is locked, but not by me).\"\"\"\n\n    exit_mcode = 75\n\n\nclass ExclusiveLock:\n    \"\"\"An exclusive lock based on mkdir filesystem operation being atomic.\n\n    If possible, try to use the context manager here like::\n\n        with ExclusiveLock(...) as lock:\n            ...\n\n    This makes sure the lock is released again if the block is left, no\n    matter how (e.g. if an exception occurred).\n    \"\"\"\n\n    def __init__(self, path, timeout=None, sleep=None, id=None):\n        self.timeout = timeout\n        self.sleep = sleep\n        self.path = Path(path).absolute()\n        self.id = id or platform.get_process_id()\n        self.unique_name = self.path / (\"%s.%d-%x\" % self.id)\n        self.kill_stale_locks = True\n        self.stale_warning_printed = False\n\n    def __enter__(self):\n        return self.acquire()\n\n    def __exit__(self, *exc):\n        self.release()\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__}: {str(self.unique_name)!r}>\"\n\n    def acquire(self, timeout=None, sleep=None):\n        if timeout is None:\n            timeout = self.timeout\n        if sleep is None:\n            sleep = self.sleep\n        parent_path, base_name = str(self.path.parent), self.path.name\n        unique_base_name = self.unique_name.name\n        temp_path = None\n        try:\n            temp_path = tempfile.mkdtemp(\".tmp\", base_name + \".\", parent_path)\n            temp_unique_name = Path(temp_path) / unique_base_name\n            with temp_unique_name.open(\"wb\"):\n                pass\n        except OSError as err:\n            raise LockFailed(str(self.path), str(err)) from None\n        else:\n            timer = TimeoutTimer(timeout, sleep).start()\n            while True:\n                try:\n                    Path(temp_path).replace(str(self.path))\n                except OSError:  # already locked\n                    if self.by_me():\n                        return self\n                    self.kill_stale_lock()\n                    if timer.timed_out_or_sleep():\n                        raise LockTimeout(str(self.path)) from None\n                else:\n                    temp_path = None  # see finally:-block below\n                    return self\n        finally:\n            if temp_path is not None:\n                # Renaming failed for some reason, so temp_dir still exists and\n                # should be cleaned up anyway. Try to clean up, but don't crash.\n                try:\n                    os.unlink(temp_unique_name)\n                except:  # nosec B110 # noqa\n                    pass\n                try:\n                    os.rmdir(temp_path)\n                except:  # nosec B110 # noqa\n                    pass\n\n    def release(self):\n        if not self.is_locked():\n            raise NotLocked(str(self.path))\n        if not self.by_me():\n            raise NotMyLock(str(self.path))\n        self.unique_name.unlink()\n        for retry in range(42):\n            try:\n                self.path.rmdir()\n            except OSError as err:\n                if err.errno in (errno.EACCES,):\n                    # windows behaving strangely? -> just try again.\n                    continue\n                if err.errno not in (errno.ENOTEMPTY, errno.EEXIST, errno.ENOENT):\n                    # EACCES or EIO or ... = we cannot operate anyway, so re-throw\n                    raise err\n                # else:\n                # Directory is not empty or doesn't exist any more.\n                # this means we lost the race to somebody else -- which is ok.\n            return\n\n    def is_locked(self):\n        return self.path.exists()\n\n    def by_me(self):\n        return self.unique_name.exists()\n\n    def kill_stale_lock(self):\n        try:\n            names = [p.name for p in self.path.iterdir()]\n        except FileNotFoundError:  # another process did our job in the meantime.\n            return False\n        except PermissionError:  # win32 might throw this.\n            return False\n        else:\n            for name in names:\n                try:\n                    host_pid, thread_str = name.rsplit(\"-\", 1)\n                    host, pid_str = host_pid.rsplit(\".\", 1)\n                    pid = int(pid_str)\n                    thread = int(thread_str, 16)\n                except ValueError:\n                    # Malformed lock name? Or just some new format we don't understand?\n                    logger.error(\"Found malformed lock %s in %s. Please check/fix manually.\", name, str(self.path))\n                    return False\n\n                if platform.process_alive(host, pid, thread):\n                    return False\n\n                if not self.kill_stale_locks:\n                    if not self.stale_warning_printed:\n                        # Log this at warning level to hint the user at the ability\n                        logger.warning(\n                            \"Found stale lock %s, but not deleting because self.kill_stale_locks = False.\", name\n                        )\n                        self.stale_warning_printed = True\n                    return False\n\n                try:\n                    (self.path / name).unlink()\n                    logger.warning(\"Killed stale lock %s.\", name)\n                except OSError as err:\n                    if not self.stale_warning_printed:\n                        # This error will bubble up and likely result in locking failure\n                        logger.error(\"Found stale lock %s, but cannot delete due to %s\", name, str(err))\n                        self.stale_warning_printed = True\n                    return False\n\n        try:\n            self.path.rmdir()\n        except OSError as err:\n            if err.errno in (errno.ENOTEMPTY, errno.EEXIST, errno.ENOENT):\n                # Directory is not empty or doesn't exist any more = we lost the race to somebody else--which is ok.\n                return False\n            # EACCES or EIO or ... = we cannot operate anyway\n            logger.error(\"Failed to remove lock dir: %s\", str(err))\n            return False\n\n        return True\n\n    def break_lock(self):\n        if self.is_locked():\n            for path_obj in self.path.iterdir():\n                path_obj.unlink()\n            self.path.rmdir()\n\n    def migrate_lock(self, old_id, new_id):\n        \"\"\"migrate the lock ownership from old_id to new_id\"\"\"\n        assert self.id == old_id\n        new_unique_name = self.path / (\"%s.%d-%x\" % new_id)\n        if self.is_locked() and self.by_me():\n            with new_unique_name.open(\"wb\"):\n                pass\n            self.unique_name.unlink()\n        self.id, self.unique_name = new_id, new_unique_name\n\n\nclass LockRoster:\n    \"\"\"\n    A Lock Roster to track shared/exclusive lockers.\n\n    Note: you usually should call the methods with an exclusive lock held,\n    to avoid conflicting access by multiple threads/processes/machines.\n    \"\"\"\n\n    def __init__(self, path, id=None):\n        assert isinstance(path, Path)\n        self.path = path\n        self.id = id or platform.get_process_id()\n        self.kill_stale_locks = True\n\n    def load(self):\n        try:\n            with self.path.open() as f:\n                data = json.load(f)\n\n            # Just nuke the stale locks early on load\n            if self.kill_stale_locks:\n                for key in (SHARED, EXCLUSIVE):\n                    try:\n                        entries = data[key]\n                    except KeyError:\n                        continue\n                    elements = set()\n                    for host, pid, thread in entries:\n                        if platform.process_alive(host, pid, thread):\n                            elements.add((host, pid, thread))\n                        else:\n                            logger.warning(\n                                \"Removed stale %s roster lock for host %s pid %d thread %d.\", key, host, pid, thread\n                            )\n                    data[key] = list(elements)\n        except (FileNotFoundError, ValueError):\n            # no or corrupt/empty roster file?\n            data = {}\n        return data\n\n    def save(self, data):\n        with self.path.open(\"w\") as f:\n            json.dump(data, f)\n\n    def remove(self):\n        try:\n            self.path.unlink()\n        except FileNotFoundError:\n            pass\n\n    def get(self, key):\n        roster = self.load()\n        return {tuple(e) for e in roster.get(key, [])}\n\n    def empty(self, *keys):\n        return all(not self.get(key) for key in keys)\n\n    def modify(self, key, op):\n        roster = self.load()\n        try:\n            elements = {tuple(e) for e in roster[key]}\n        except KeyError:\n            elements = set()\n        if op == ADD:\n            elements.add(self.id)\n        elif op == REMOVE:\n            # note: we ignore it if the element is already not present anymore.\n            # this has been frequently seen in teardowns involving Repository.__del__ and Repository.__exit__.\n            elements.discard(self.id)\n        elif op == REMOVE2:\n            # needed for callers that do not want to ignore.\n            elements.remove(self.id)\n        else:\n            raise ValueError(\"Unknown LockRoster op %r\" % op)\n        roster[key] = list(list(e) for e in elements)\n        self.save(roster)\n\n    def migrate_lock(self, key, old_id, new_id):\n        \"\"\"migrate the lock ownership from old_id to new_id\"\"\"\n        assert self.id == old_id\n        # need to switch off stale lock killing temporarily as we want to\n        # migrate rather than kill them (at least the one made by old_id).\n        killing, self.kill_stale_locks = self.kill_stale_locks, False\n        try:\n            try:\n                self.modify(key, REMOVE2)\n            except KeyError:\n                # entry was not there, so no need to add a new one, but still update our id\n                self.id = new_id\n            else:\n                # old entry removed, update our id and add a updated entry\n                self.id = new_id\n                self.modify(key, ADD)\n        finally:\n            self.kill_stale_locks = killing\n\n\nclass Lock:\n    \"\"\"\n    A Lock for a resource that can be accessed in a shared or exclusive way.\n    Typically, write access to a resource needs an exclusive lock (1 writer,\n    no one is allowed reading) and read access to a resource needs a shared\n    lock (multiple readers are allowed).\n\n    If possible, try to use the contextmanager here like::\n\n        with Lock(...) as lock:\n            ...\n\n    This makes sure the lock is released again if the block is left, no\n    matter how (e.g. if an exception occurred).\n    \"\"\"\n\n    def __init__(self, path, exclusive=False, sleep=None, timeout=None, id=None):\n        self.path = path\n        self.is_exclusive = exclusive\n        self.sleep = sleep\n        self.timeout = timeout\n        self.id = id or platform.get_process_id()\n        # globally keeping track of shared and exclusive lockers:\n        self._roster = LockRoster(Path(path + \".roster\"), id=id)\n        # an exclusive lock, used for:\n        # - holding while doing roster queries / updates\n        # - holding while the Lock itself is exclusive\n        self._lock = ExclusiveLock(str(Path(path + \".exclusive\")), id=id, timeout=timeout)\n\n    def __enter__(self):\n        return self.acquire()\n\n    def __exit__(self, *exc):\n        self.release()\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__}: {self.id!r}>\"\n\n    def acquire(self, exclusive=None, remove=None, sleep=None):\n        if exclusive is None:\n            exclusive = self.is_exclusive\n        sleep = sleep or self.sleep or 0.2\n        if exclusive:\n            self._wait_for_readers_finishing(remove, sleep)\n            self._roster.modify(EXCLUSIVE, ADD)\n        else:\n            with self._lock:\n                if remove is not None:\n                    self._roster.modify(remove, REMOVE)\n                self._roster.modify(SHARED, ADD)\n        self.is_exclusive = exclusive\n        return self\n\n    def _wait_for_readers_finishing(self, remove, sleep):\n        timer = TimeoutTimer(self.timeout, sleep).start()\n        while True:\n            self._lock.acquire()\n            try:\n                if remove is not None:\n                    self._roster.modify(remove, REMOVE)\n                if len(self._roster.get(SHARED)) == 0:\n                    return  # we are the only one and we keep the lock!\n                # restore the roster state as before (undo the roster change):\n                if remove is not None:\n                    self._roster.modify(remove, ADD)\n            except:  # noqa\n                # avoid orphan lock when an exception happens here, e.g. Ctrl-C!\n                self._lock.release()\n                raise\n            else:\n                self._lock.release()\n            if timer.timed_out_or_sleep():\n                raise LockTimeout(self.path)\n\n    def release(self):\n        if self.is_exclusive:\n            self._roster.modify(EXCLUSIVE, REMOVE)\n            if self._roster.empty(EXCLUSIVE, SHARED):\n                self._roster.remove()\n            self._lock.release()\n        else:\n            with self._lock:\n                self._roster.modify(SHARED, REMOVE)\n                if self._roster.empty(EXCLUSIVE, SHARED):\n                    self._roster.remove()\n\n    def upgrade(self):\n        # WARNING: if multiple read-lockers want to upgrade, it will deadlock because they\n        # all will wait until the other read locks go away - and that won't happen.\n        if not self.is_exclusive:\n            self.acquire(exclusive=True, remove=SHARED)\n\n    def downgrade(self):\n        if self.is_exclusive:\n            self.acquire(exclusive=False, remove=EXCLUSIVE)\n\n    def got_exclusive_lock(self):\n        return self.is_exclusive and self._lock.is_locked() and self._lock.by_me()\n\n    def break_lock(self):\n        self._roster.remove()\n        self._lock.break_lock()\n\n    def migrate_lock(self, old_id, new_id):\n        assert self.id == old_id\n        self.id = new_id\n        if self.is_exclusive:\n            self._lock.migrate_lock(old_id, new_id)\n            self._roster.migrate_lock(EXCLUSIVE, old_id, new_id)\n        else:\n            with self._lock:\n                self._lock.migrate_lock(old_id, new_id)\n                self._roster.migrate_lock(SHARED, old_id, new_id)\n"
  },
  {
    "path": "src/borg/fuse.py",
    "content": "\"\"\"\nFUSE filesystem implementation for `borg mount`.\n\nIMPORTANT\n=========\n\nThis code is only safe for single-threaded and synchronous (non-async) usage.\n\n- llfuse is synchronous and used with workers=1, so there is only 1 thread,\n  and we are safe.\n- pyfuse3 uses Trio, which only uses 1 thread, but could use this code in an\n  asynchronous manner. However, as long as we do not use any asynchronous\n  operations (like using \"await\") in this code, it is still de facto\n  synchronous, and we are safe.\n\"\"\"\n\nimport errno\nimport functools\nimport io\nimport os\nimport stat\nimport struct\nimport sys\nimport tempfile\nimport time\nfrom collections import defaultdict, Counter\nfrom signal import SIGINT\nfrom typing import TYPE_CHECKING\n\nfrom .constants import ROBJ_FILE_STREAM, zeros\n\nif TYPE_CHECKING:\n    # For type checking, assume llfuse is available\n    # This allows mypy to understand llfuse.Operations\n    import llfuse\n    from .fuse_impl import has_pyfuse3, ENOATTR\nelse:\n    from .fuse_impl import llfuse, has_pyfuse3, ENOATTR\n\nif has_pyfuse3:\n    import trio\n\n    def async_wrapper(fn):\n        @functools.wraps(fn)\n        async def wrapper(*args, **kwargs):\n            return fn(*args, **kwargs)\n\n        return wrapper\n\nelse:\n    trio = None\n\n    def async_wrapper(fn):\n        return fn\n\n\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfrom .crypto.low_level import blake2b_128\nfrom .archiver._common import build_matcher, build_filter\nfrom .archive import Archive, get_item_uid_gid\nfrom .hashindex import FuseVersionsIndex\nfrom .helpers import daemonize, daemonizing, signal_handler, format_file_size, bin_to_hex\nfrom .helpers import HardLinkManager\nfrom .helpers import msgpack\nfrom .helpers.lrucache import LRUCache\nfrom .item import Item\nfrom .platform import uid2user, gid2group\nfrom .platformflags import is_darwin\nfrom .repository import Repository\nfrom .remote import RemoteRepository\n\n\ndef fuse_main():\n    if has_pyfuse3:\n        try:\n            trio.run(llfuse.main)\n        except KeyboardInterrupt:\n            return SIGINT\n        except:  # noqa\n            return -1  # avoid colliding with signal numbers\n        else:\n            return None\n    else:\n        return llfuse.main(workers=1)\n\n\n# size of some LRUCaches (1 element per simultaneously open file)\n# note: _inode_cache might have rather large elements - Item.chunks can be large!\n#       also, simultaneously reading too many files should be avoided anyway.\n#       thus, do not set FILES to high values.\nFILES = 4\n\n\nclass ItemCache:\n    \"\"\"\n    This is the \"meat\" of the filesystem's metadata storage.\n\n    This class generates inode numbers that efficiently index items in archives\n    and retrieves items from these inode numbers.\n    \"\"\"\n\n    # 2 MiB are approximately ~230000 items (depends on the average number of items per metadata chunk).\n    #\n    # Since growing a bytearray has to copy it, growing it will converge to O(n^2), however,\n    # this is not yet relevant due to the swiftness of copying memory. If it becomes an issue,\n    # use an anonymous mmap and just resize that (or, if on 64 bit, make it so big you never need\n    # to resize it in the first place; that's free).\n    GROW_META_BY = 2 * 1024 * 1024\n\n    indirect_entry_struct = struct.Struct(\"=cII\")\n    assert indirect_entry_struct.size == 9\n\n    def __init__(self, decrypted_repository):\n        self.decrypted_repository = decrypted_repository\n        # self.meta, the \"meta-array\" is a densely packed array of metadata about where items can be found.\n        # It is indexed by the inode number minus self.offset. (This is in a way eerily similar to how the first\n        # unices did this).\n        # The meta-array contains chunk IDs and item entries (described in iter_archive_items).\n        # The chunk IDs are referenced by item entries through relative offsets,\n        # which are bounded by the metadata chunk size.\n        self.meta = bytearray()\n        # The current write offset in self.meta\n        self.write_offset = 0\n\n        # Offset added to meta-indices, resulting in inodes,\n        # or subtracted from inodes, resulting in meta-indices.\n        # XXX: Merge FuseOperations.items and ItemCache to avoid\n        #      this implicit limitation / hack (on the number of synthetic inodes, degenerate\n        #      cases can inflate their number far beyond the number of archives).\n        self.offset = 1000000\n\n        # A temporary file that contains direct items, i.e. items directly cached in this layer.\n        # These are items that span more than one chunk and thus cannot be efficiently cached\n        # by the object cache (self.decrypted_repository), which would require variable-length structures;\n        # possible but not worth the effort, see iter_archive_items.\n        self.fd = tempfile.TemporaryFile(prefix=\"borg-tmp\")\n\n        # A small LRU cache for chunks requested by ItemCache.get() from the object cache,\n        # this significantly speeds up directory traversal and similar operations which\n        # tend to re-read the same chunks over and over.\n        # The capacity is kept low because increasing it does not provide any significant advantage,\n        # but makes LRUCache's square behaviour noticeable and consumes more memory.\n        self.chunks = LRUCache(capacity=10)\n\n        # Instrumentation\n        # Count of indirect items, i.e. data is cached in the object cache, not directly in this cache\n        self.indirect_items = 0\n        # Count of direct items, i.e. data is in self.fd\n        self.direct_items = 0\n\n    def get(self, inode):\n        offset = inode - self.offset\n        if offset < 0:\n            raise ValueError(\"ItemCache.get() called with an invalid inode number\")\n        if self.meta[offset] == ord(b\"I\"):\n            _, chunk_id_relative_offset, chunk_offset = self.indirect_entry_struct.unpack_from(self.meta, offset)\n            chunk_id_offset = offset - chunk_id_relative_offset\n            # bytearray slices are bytearrays as well, explicitly convert to bytes()\n            chunk_id = bytes(self.meta[chunk_id_offset : chunk_id_offset + 32])\n            chunk = self.chunks.get(chunk_id)\n            if not chunk:\n                csize, chunk = next(self.decrypted_repository.get_many([chunk_id]))\n                self.chunks[chunk_id] = chunk\n            data = memoryview(chunk)[chunk_offset:]\n            unpacker = msgpack.Unpacker()\n            unpacker.feed(data)\n            return Item(internal_dict=next(unpacker))\n        elif self.meta[offset] == ord(b\"S\"):\n            fd_offset = int.from_bytes(self.meta[offset + 1 : offset + 9], \"little\")\n            self.fd.seek(fd_offset, io.SEEK_SET)\n            return Item(internal_dict=next(msgpack.Unpacker(self.fd, read_size=1024)))\n        else:\n            raise ValueError(\"Invalid entry type in self.meta\")\n\n    def iter_archive_items(self, archive_item_ids, filter=None):\n        unpacker = msgpack.Unpacker()\n\n        # Current offset in the metadata stream, which consists of all metadata chunks glued together\n        stream_offset = 0\n        # Offset of the current chunk in the metadata stream\n        chunk_begin = 0\n        # Length of the chunk preceding the current chunk\n        last_chunk_length = 0\n        msgpacked_bytes = b\"\"\n\n        write_offset = self.write_offset\n        meta = self.meta\n        pack_indirect_into = self.indirect_entry_struct.pack_into\n\n        for key, (csize, data) in zip(archive_item_ids, self.decrypted_repository.get_many(archive_item_ids)):\n            # Store the chunk ID in the meta-array\n            if write_offset + 32 >= len(meta):\n                meta.extend(bytes(self.GROW_META_BY))\n            meta[write_offset : write_offset + 32] = key\n            current_id_offset = write_offset\n            write_offset += 32\n\n            chunk_begin += last_chunk_length\n            last_chunk_length = len(data)\n\n            unpacker.feed(data)\n            while True:\n                try:\n                    item = unpacker.unpack()\n                    need_more_data = False\n                except msgpack.OutOfData:\n                    need_more_data = True\n\n                start = stream_offset - chunk_begin\n                # tell() is not helpful for the need_more_data case, but we know it is the remainder\n                # of the data in that case. in the other case, tell() works as expected.\n                length = (len(data) - start) if need_more_data else (unpacker.tell() - stream_offset)\n                msgpacked_bytes += data[start : start + length]\n                stream_offset += length\n\n                if need_more_data:\n                    # Need more data, feed the next chunk\n                    break\n\n                item = Item(internal_dict=item)\n                if filter and not filter(item):\n                    msgpacked_bytes = b\"\"\n                    continue\n\n                current_item = msgpacked_bytes\n                current_item_length = len(current_item)\n                current_spans_chunks = stream_offset - current_item_length < chunk_begin\n                msgpacked_bytes = b\"\"\n\n                if write_offset + 9 >= len(meta):\n                    meta.extend(bytes(self.GROW_META_BY))\n\n                # item entries in the meta-array come in two different flavours, both nine bytes long.\n                # (1) for items that span chunks:\n                #\n                #     'S' + 8 byte offset into the self.fd file, where the msgpacked item starts.\n                #\n                # (2) for items that are completely contained in one chunk, which usually is the great majority\n                #     (about 700:1 for system backups)\n                #\n                #     'I' + 4 byte offset where the chunk ID is + 4 byte offset in the chunk\n                #     where the msgpacked items starts\n                #\n                #     The chunk ID offset is the number of bytes _back_ from the start of the entry, i.e.:\n                #\n                #     |Chunk ID| ....          |S1234abcd|\n                #      ^------ offset ----------^\n\n                if current_spans_chunks:\n                    pos = self.fd.seek(0, io.SEEK_END)\n                    self.fd.write(current_item)\n                    meta[write_offset : write_offset + 9] = b\"S\" + pos.to_bytes(8, \"little\")\n                    self.direct_items += 1\n                else:\n                    item_offset = stream_offset - current_item_length - chunk_begin\n                    pack_indirect_into(meta, write_offset, b\"I\", write_offset - current_id_offset, item_offset)\n                    self.indirect_items += 1\n                inode = write_offset + self.offset\n                write_offset += 9\n\n                yield inode, item\n\n        self.write_offset = write_offset\n\n\nclass FuseBackend:\n    \"\"\"Virtual filesystem based on archive(s) to provide information to fuse\"\"\"\n\n    def __init__(self, manifest, args, decrypted_repository):\n        self._args = args\n        self.numeric_ids = args.numeric_ids\n        self._manifest = manifest\n        self.repo_objs = manifest.repo_objs\n        self.repository_uncached = manifest.repository\n        # Maps inode numbers to Item instances. This is used for synthetic inodes, i.e. file-system objects that are\n        # made up and are not contained in the archives. For example archive directories or intermediate directories\n        # not contained in archives.\n        self._items = {}\n        # cache up to <FILES> Items\n        self._inode_cache = LRUCache(capacity=FILES)\n        # _inode_count is the current count of synthetic inodes, i.e. those in self._items\n        self.inode_count = 0\n        # Maps inode numbers to the inode number of the parent\n        self.parent = {}\n        # Maps inode numbers to a dictionary mapping byte directory entry names to their inode numbers,\n        # i.e. this contains all dirents of everything that is mounted. (It becomes really big).\n        self.contents = defaultdict(dict)\n        self.default_uid = os.getuid()\n        self.default_gid = os.getgid()\n        self.default_dir = None\n        # Archives to be loaded when first accessed, mapped by their placeholder inode\n        self.pending_archives = {}\n        self.cache = ItemCache(decrypted_repository)\n        self.allow_damaged_files = False\n        self.versions = False\n        self.uid_forced = None\n        self.gid_forced = None\n        self.umask = 0\n        self.archive_root_dir = {}  # archive ID --> directory name\n\n    def _create_filesystem(self):\n        self._create_dir(parent=1)  # first call, create root dir (inode == 1)\n        self.versions_index = FuseVersionsIndex()\n        archives = self._manifest.archives.list_considering(self._args)\n        name_counter = Counter(a.name for a in archives)\n        duplicate_names = {a.name for a in archives if name_counter[a.name] > 1}\n        for archive in archives:\n            name = f\"{archive.name}\"\n            if name in duplicate_names:\n                name += f\"-{bin_to_hex(archive.id):.8}\"\n            self.archive_root_dir[archive.id] = name\n        for archive in archives:\n            if self.versions:\n                # process archives immediately\n                self._process_archive(archive.id)\n            else:\n                # lazily load archives, create archive placeholder inode\n                archive_inode = self._create_dir(parent=1, mtime=int(archive.ts.timestamp() * 1e9))\n                name = self.archive_root_dir[archive.id]\n                self.contents[1][os.fsencode(name)] = archive_inode\n                self.pending_archives[archive_inode] = archive\n\n    def get_item(self, inode):\n        item = self._inode_cache.get(inode)\n        if item is not None:\n            return item\n        try:\n            # this is a cheap get-from-dictionary operation, no need to cache the result.\n            return self._items[inode]\n        except KeyError:\n            # while self.cache does some internal caching, it has still quite some overhead, so we cache the result.\n            item = self.cache.get(inode)\n            self._inode_cache[inode] = item\n            return item\n\n    def check_pending_archive(self, inode):\n        # Check if this is an archive we need to load\n        archive_info = self.pending_archives.pop(inode, None)\n        if archive_info is not None:\n            self._process_archive(archive_info.id, [os.fsencode(self.archive_root_dir[archive_info.id])])\n\n    def _allocate_inode(self):\n        self.inode_count += 1\n        return self.inode_count\n\n    def _create_dir(self, parent, mtime=None):\n        \"\"\"Create directory\"\"\"\n        ino = self._allocate_inode()\n        if mtime is not None:\n            self._items[ino] = Item(internal_dict=self.default_dir.as_dict())\n            self._items[ino].mtime = mtime\n        else:\n            self._items[ino] = self.default_dir\n        self.parent[ino] = parent\n        return ino\n\n    def find_inode(self, path, prefix=[]):\n        segments = prefix + path.split(b\"/\")\n        inode = 1\n        for segment in segments:\n            inode = self.contents[inode][segment]\n        return inode\n\n    def _process_archive(self, archive_id, prefix=[]):\n        \"\"\"Build FUSE inode hierarchy from archive metadata\"\"\"\n        self.file_versions = {}  # for versions mode: original path -> version\n        t0 = time.perf_counter()\n        archive = Archive(self._manifest, archive_id)\n        strip_components = self._args.strip_components\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(self._args.patterns, self._args.paths)\n        hlm = HardLinkManager(id_type=bytes, info_type=str)  # hlid -> path\n\n        filter = build_filter(matcher, strip_components)\n        for item_inode, item in self.cache.iter_archive_items(archive.metadata.items, filter=filter):\n            if strip_components:\n                item.path = os.sep.join(item.path.split(os.sep)[strip_components:])\n            path = os.fsencode(item.path)\n            is_dir = stat.S_ISDIR(item.mode)\n            if is_dir:\n                try:\n                    # This can happen if an archive was created with a command line like\n                    # $ borg create ... dir1/file dir1\n                    # In this case the code below will have created a default_dir inode for dir1 already.\n                    inode = self.find_inode(path, prefix)\n                except KeyError:\n                    pass\n                else:\n                    self._items[inode] = item\n                    continue\n            segments = prefix + path.split(b\"/\")\n            parent = 1\n            for segment in segments[:-1]:\n                parent = self._process_inner(segment, parent)\n            self._process_leaf(segments[-1], item, parent, prefix, is_dir, item_inode, hlm)\n        duration = time.perf_counter() - t0\n        logger.debug(\"fuse: _process_archive completed in %.1f s for archive %s\", duration, archive.name)\n\n    def _process_leaf(self, name, item, parent, prefix, is_dir, item_inode, hlm):\n        path = item.path\n        del item.path  # save some space\n\n        def file_version(item, path):\n            if \"chunks\" in item:\n                file_id = blake2b_128(path)\n                current_version, previous_id = self.versions_index.get(file_id, (0, None))\n\n                contents_id = blake2b_128(b\"\".join(chunk_id for chunk_id, _ in item.chunks))\n\n                if contents_id != previous_id:\n                    current_version += 1\n                    self.versions_index[file_id] = current_version, contents_id\n\n                return current_version\n\n        def make_versioned_name(name, version, add_dir=False):\n            if add_dir:\n                # add intermediate directory with same name as filename\n                path_fname = name.rsplit(b\"/\", 1)\n                name += b\"/\" + path_fname[-1]\n            # keep original extension at end to avoid confusing tools\n            name, ext = os.path.splitext(name)\n            version_enc = os.fsencode(\".%05d\" % version)\n            return name + version_enc + ext\n\n        if \"hlid\" in item:\n            link_target = hlm.retrieve(id=item.hlid, default=None)\n            if link_target is not None:\n                # Hard link was extracted previously, just link\n                link_target = os.fsencode(link_target)\n                if self.versions:\n                    # adjust link target name with version\n                    version = self.file_versions[link_target]\n                    link_target = make_versioned_name(link_target, version, add_dir=True)\n                try:\n                    inode = self.find_inode(link_target, prefix)\n                except KeyError:\n                    logger.warning(\"Skipping broken hard link: %s -> %s\", path, link_target)\n                    return\n                item = self.get_item(inode)\n                item.nlink = item.get(\"nlink\", 1) + 1\n                self._items[inode] = item\n            else:\n                inode = item_inode\n                self._items[inode] = item\n                # remember extracted item path, so that following hard links don't extract twice.\n                hlm.remember(id=item.hlid, info=path)\n        else:\n            inode = item_inode\n\n        if self.versions and not is_dir:\n            parent = self._process_inner(name, parent)\n            enc_path = os.fsencode(path)\n            version = file_version(item, enc_path)\n            if version is not None:\n                # regular file, with contents\n                name = make_versioned_name(name, version)\n                self.file_versions[enc_path] = version\n\n        self.parent[inode] = parent\n        if name:\n            self.contents[parent][name] = inode\n\n    def _process_inner(self, name, parent_inode):\n        dir = self.contents[parent_inode]\n        if name in dir:\n            inode = dir[name]\n        else:\n            inode = self._create_dir(parent_inode)\n            if name:\n                dir[name] = inode\n        return inode\n\n\nclass FuseOperations(llfuse.Operations, FuseBackend):\n    \"\"\"Export archive as a FUSE filesystem\"\"\"\n\n    def __init__(self, manifest, args, decrypted_repository):\n        llfuse.Operations.__init__(self)\n        FuseBackend.__init__(self, manifest, args, decrypted_repository)\n        self.decrypted_repository = decrypted_repository\n        data_cache_capacity = int(os.environ.get(\"BORG_MOUNT_DATA_CACHE_ENTRIES\", os.cpu_count() or 1))\n        logger.debug(\"mount data cache capacity: %d chunks\", data_cache_capacity)\n        self.data_cache = LRUCache(capacity=data_cache_capacity)\n        self._last_pos = LRUCache(capacity=FILES)\n\n    def sig_info_handler(self, sig_no, stack):\n        logger.debug(\n            \"fuse: %d synth inodes, %d edges (%s)\",\n            self.inode_count,\n            len(self.parent),\n            # getsizeof is the size of the dict itself; key and value are two small-ish integers,\n            # which are shared due to code structure (this has been verified).\n            format_file_size(sys.getsizeof(self.parent) + len(self.parent) * sys.getsizeof(self.inode_count)),\n        )\n        logger.debug(\"fuse: %d pending archives\", len(self.pending_archives))\n        logger.debug(\n            \"fuse: ItemCache %d entries (%d direct, %d indirect), meta-array size %s, direct items size %s\",\n            self.cache.direct_items + self.cache.indirect_items,\n            self.cache.direct_items,\n            self.cache.indirect_items,\n            format_file_size(sys.getsizeof(self.cache.meta)),\n            format_file_size(os.stat(self.cache.fd.fileno()).st_size),\n        )\n        logger.debug(\n            \"fuse: data cache: %d/%d entries, %s\",\n            len(self.data_cache.items()),\n            self.data_cache._capacity,\n            format_file_size(sum(len(chunk) for key, chunk in self.data_cache.items())),\n        )\n        self.decrypted_repository.log_instrumentation()\n\n    def mount(self, mountpoint, mount_options, foreground=False, show_rc=False):\n        \"\"\"Mount filesystem on *mountpoint* with *mount_options*.\"\"\"\n\n        def pop_option(options, key, present, not_present, wanted_type, int_base=0):\n            assert isinstance(options, list)  # we mutate this\n            for idx, option in enumerate(options):\n                if option == key:\n                    options.pop(idx)\n                    return present\n                if option.startswith(key + \"=\"):\n                    options.pop(idx)\n                    value = option.split(\"=\", 1)[1]\n                    if wanted_type is bool:\n                        v = value.lower()\n                        if v in (\"y\", \"yes\", \"true\", \"1\"):\n                            return True\n                        if v in (\"n\", \"no\", \"false\", \"0\"):\n                            return False\n                        raise ValueError(\"unsupported value in option: %s\" % option)\n                    if wanted_type is int:\n                        try:\n                            return int(value, base=int_base)\n                        except ValueError:\n                            raise ValueError(\"unsupported value in option: %s\" % option) from None\n                    try:\n                        return wanted_type(value)\n                    except ValueError:\n                        raise ValueError(\"unsupported value in option: %s\" % option) from None\n            else:\n                return not_present\n\n        # default_permissions enables permission checking by the kernel. Without\n        # this, any umask (or uid/gid) would not have an effect and this could\n        # cause security issues if used with allow_other mount option.\n        # When not using allow_other or allow_root, access is limited to the\n        # mounting user anyway.\n        options = [\"fsname=borgfs\", \"ro\", \"default_permissions\"]\n        if mount_options:\n            options.extend(mount_options.split(\",\"))\n        if is_darwin:\n            # macFUSE supports a volname mount option to give what finder displays on desktop / in directory list.\n            volname = pop_option(options, \"volname\", \"\", \"\", str)\n            # if the user did not specify it, we make something up,\n            # because otherwise it would be \"macFUSE Volume 0 (Python)\", #7690.\n            volname = volname or f\"{os.path.basename(mountpoint)} (borgfs)\"\n            options.append(f\"volname={volname}\")\n        ignore_permissions = pop_option(options, \"ignore_permissions\", True, False, bool)\n        if ignore_permissions:\n            # in case users have a use-case that requires NOT giving \"default_permissions\",\n            # this is enabled by the custom \"ignore_permissions\" mount option which just\n            # removes \"default_permissions\" again:\n            pop_option(options, \"default_permissions\", True, False, bool)\n        self.allow_damaged_files = pop_option(options, \"allow_damaged_files\", True, False, bool)\n        self.versions = pop_option(options, \"versions\", True, False, bool)\n        self.uid_forced = pop_option(options, \"uid\", None, None, int)\n        self.gid_forced = pop_option(options, \"gid\", None, None, int)\n        self.umask = pop_option(options, \"umask\", 0, 0, int, int_base=8)  # umask is octal, e.g. 222 or 0222\n        dir_uid = self.uid_forced if self.uid_forced is not None else self.default_uid\n        dir_gid = self.gid_forced if self.gid_forced is not None else self.default_gid\n        dir_user = uid2user(dir_uid)\n        dir_group = gid2group(dir_gid)\n        assert isinstance(dir_user, str)\n        assert isinstance(dir_group, str)\n        dir_mode = 0o40755 & ~self.umask\n        self.default_dir = Item(\n            mode=dir_mode, mtime=int(time.time() * 1e9), user=dir_user, group=dir_group, uid=dir_uid, gid=dir_gid\n        )\n        self._create_filesystem()\n        llfuse.init(self, mountpoint, options)\n        logger.warning(\n            \"Warning: The mounted archive is capable of containing symlinks that point outside the archive tree. \"\n            \"When following such symlinks you may see files and directories within the mountpoint \"\n            \"that do not reflect the archive content.\"\n        )\n        if not foreground:\n            if isinstance(self.repository_uncached, RemoteRepository):\n                daemonize()\n            else:\n                with daemonizing(show_rc=show_rc) as (old_id, new_id):\n                    # local repo: the locking process' PID is changing, migrate it:\n                    logger.debug(\"fuse: mount local repo, going to background: migrating lock.\")\n                    self.repository_uncached.migrate_lock(old_id, new_id)\n\n        # If the file system crashes, we do not want to umount because in that\n        # case the mountpoint suddenly appears to become empty. This can have\n        # nasty consequences, imagine the user has e.g. an active rsync mirror\n        # job - seeing the mountpoint empty, rsync would delete everything in the\n        # mirror.\n        umount = False\n        try:\n            with signal_handler(\"SIGUSR1\", self.sig_info_handler), signal_handler(\"SIGINFO\", self.sig_info_handler):\n                signal = fuse_main()\n            # no crash and no signal (or it's ^C and we're in the foreground) -> umount request\n            umount = signal is None or (signal == SIGINT and foreground)\n        finally:\n            llfuse.close(umount)\n\n    @async_wrapper\n    def statfs(self, ctx=None):\n        stat_ = llfuse.StatvfsData()\n        stat_.f_bsize = 512  # Filesystem block size\n        stat_.f_frsize = 512  # Fragment size\n        stat_.f_blocks = 0  # Size of fs in f_frsize units\n        stat_.f_bfree = 0  # Number of free blocks\n        stat_.f_bavail = 0  # Number of free blocks for unprivileged users\n        stat_.f_files = 0  # Number of inodes\n        stat_.f_ffree = 0  # Number of free inodes\n        stat_.f_favail = 0  # Number of free inodes for unprivileged users\n        stat_.f_namemax = 255  # == NAME_MAX (depends on archive source OS / FS)\n        return stat_\n\n    def _getattr(self, inode, ctx=None):\n        item = self.get_item(inode)\n        entry = llfuse.EntryAttributes()\n        entry.st_ino = inode\n        entry.generation = 0\n        entry.entry_timeout = 300\n        entry.attr_timeout = 300\n        entry.st_mode = item.mode & ~self.umask\n        entry.st_nlink = item.get(\"nlink\", 1)\n        entry.st_uid, entry.st_gid = get_item_uid_gid(\n            item,\n            numeric=self.numeric_ids,\n            uid_default=self.default_uid,\n            gid_default=self.default_gid,\n            uid_forced=self.uid_forced,\n            gid_forced=self.gid_forced,\n        )\n        entry.st_rdev = item.get(\"rdev\", 0)\n        entry.st_size = item.get_size()\n        entry.st_blksize = 512\n        entry.st_blocks = (entry.st_size + entry.st_blksize - 1) // entry.st_blksize\n        # note: older archives only have mtime (not atime nor ctime)\n        entry.st_mtime_ns = mtime_ns = item.mtime\n        entry.st_atime_ns = item.get(\"atime\", mtime_ns)\n        entry.st_ctime_ns = item.get(\"ctime\", mtime_ns)\n        entry.st_birthtime_ns = item.get(\"birthtime\", mtime_ns)\n        return entry\n\n    @async_wrapper\n    def getattr(self, inode, ctx=None):\n        return self._getattr(inode, ctx=ctx)\n\n    @async_wrapper\n    def listxattr(self, inode, ctx=None):\n        item = self.get_item(inode)\n        return item.get(\"xattrs\", {}).keys()\n\n    @async_wrapper\n    def getxattr(self, inode, name, ctx=None):\n        item = self.get_item(inode)\n        try:\n            return item.get(\"xattrs\", {})[name] or b\"\"\n        except KeyError:\n            raise llfuse.FUSEError(ENOATTR) from None\n\n    @async_wrapper\n    def lookup(self, parent_inode, name, ctx=None):\n        self.check_pending_archive(parent_inode)\n        if name == b\".\":\n            inode = parent_inode\n        elif name == b\"..\":\n            inode = self.parent[parent_inode]\n        else:\n            inode = self.contents[parent_inode].get(name)\n            if not inode:\n                raise llfuse.FUSEError(errno.ENOENT)\n        return self._getattr(inode)\n\n    @async_wrapper\n    def open(self, inode, flags, ctx=None):\n        return llfuse.FileInfo(fh=inode) if has_pyfuse3 else inode\n\n    @async_wrapper\n    def opendir(self, inode, ctx=None):\n        self.check_pending_archive(inode)\n        return inode\n\n    @async_wrapper\n    def read(self, fh, offset, size):\n        parts = []\n        item = self.get_item(fh)\n\n        # optimize for linear reads:\n        # we cache the chunk number and the in-file offset of the chunk in _last_pos[fh]\n        chunk_no, chunk_offset = self._last_pos.get(fh, (0, 0))\n        if chunk_offset > offset:\n            # this is not a linear read, so we lost track and need to start from beginning again...\n            chunk_no, chunk_offset = (0, 0)\n\n        offset -= chunk_offset\n        chunks = item.chunks\n        # note: using index iteration to avoid frequently copying big (sub)lists by slicing\n        for idx in range(chunk_no, len(chunks)):\n            id, s = chunks[idx]\n            if s < offset:\n                offset -= s\n                chunk_offset += s\n                chunk_no += 1\n                continue\n            n = min(size, s - offset)\n            if id in self.data_cache:\n                data = self.data_cache[id]\n                if offset + n == len(data):\n                    # evict fully read chunk from cache\n                    del self.data_cache[id]\n            else:\n                try:\n                    cdata = self.repository_uncached.get(id)\n                except Repository.ObjectNotFound:\n                    if self.allow_damaged_files:\n                        data = zeros[:s]\n                        assert len(data) == s\n                    else:\n                        raise llfuse.FUSEError(errno.EIO) from None\n                else:\n                    _, data = self.repo_objs.parse(id, cdata, ro_type=ROBJ_FILE_STREAM)\n                if offset + n < len(data):\n                    # chunk was only partially read, cache it\n                    self.data_cache[id] = data\n            parts.append(data[offset : offset + n])\n            offset = 0\n            size -= n\n            if not size:\n                if fh in self._last_pos:\n                    self._last_pos.replace(fh, (chunk_no, chunk_offset))\n                else:\n                    self._last_pos[fh] = (chunk_no, chunk_offset)\n                break\n        return b\"\".join(parts)\n\n    # note: we can't have a generator (with yield) and not a generator (async) in the same method\n    if has_pyfuse3:\n\n        async def readdir(self, fh, off, token):  # type: ignore[misc]\n            entries = [(b\".\", fh), (b\"..\", self.parent[fh])]\n            entries.extend(self.contents[fh].items())\n            for i, (name, inode) in enumerate(entries[off:], off):\n                attrs = self._getattr(inode)\n                if not llfuse.readdir_reply(token, name, attrs, i + 1):\n                    break\n\n    else:\n\n        def readdir(self, fh, off):  # type: ignore[misc]\n            entries = [(b\".\", fh), (b\"..\", self.parent[fh])]\n            entries.extend(self.contents[fh].items())\n            for i, (name, inode) in enumerate(entries[off:], off):\n                attrs = self._getattr(inode)\n                yield name, attrs, i + 1\n\n    @async_wrapper\n    def readlink(self, inode, ctx=None):\n        item = self.get_item(inode)\n        return os.fsencode(item.target)\n"
  },
  {
    "path": "src/borg/fuse_impl.py",
    "content": "\"\"\"\nLoads the library for the FUSE implementation.\n\"\"\"\n\nimport os\nimport types\n\nfrom .platform import ENOATTR  # noqa\n\nBORG_FUSE_IMPL = os.environ.get(\"BORG_FUSE_IMPL\", \"mfusepy,pyfuse3,llfuse\")\n\nhlfuse: types.ModuleType | None = None\nllfuse: types.ModuleType | None = None\n\nfor FUSE_IMPL in BORG_FUSE_IMPL.split(\",\"):\n    FUSE_IMPL = FUSE_IMPL.strip()\n    if FUSE_IMPL == \"pyfuse3\":\n        try:\n            import pyfuse3\n        except ImportError:\n            pass\n        else:\n            llfuse = pyfuse3\n            has_llfuse = False\n            has_pyfuse3 = True\n            has_mfusepy = False\n            has_any_fuse = True\n            hlfuse = None  # noqa\n            break\n    elif FUSE_IMPL == \"llfuse\":\n        try:\n            import llfuse as llfuse_module\n        except ImportError:\n            pass\n        else:\n            llfuse = llfuse_module\n            has_llfuse = True\n            has_pyfuse3 = False\n            has_mfusepy = False\n            has_any_fuse = True\n            hlfuse = None  # noqa\n            break\n    elif FUSE_IMPL == \"mfusepy\":\n        try:\n            import mfusepy\n        except ImportError:\n            pass\n        else:\n            hlfuse = mfusepy\n            has_llfuse = False\n            has_pyfuse3 = False\n            has_mfusepy = True\n            has_any_fuse = True\n            break\n    elif FUSE_IMPL == \"none\":\n        pass\n    else:\n        raise RuntimeError(\"Unknown FUSE implementation in BORG_FUSE_IMPL: '%s'.\" % BORG_FUSE_IMPL)\nelse:\n    has_llfuse = False\n    has_pyfuse3 = False\n    has_mfusepy = False\n    has_any_fuse = False\n"
  },
  {
    "path": "src/borg/hashindex.pyi",
    "content": "from typing import NamedTuple, Tuple, Type, IO, Iterator, Any\n\nPATH_OR_FILE = str | IO\n\nclass ChunkIndexEntry(NamedTuple):\n    flags: int\n    size: int\n\nCIE = Tuple[int, int] | Type[ChunkIndexEntry]\n\nclass ChunkIndex:\n    F_NONE: int\n    F_USED: int\n    F_COMPRESS: int\n    F_NEW: int\n    M_USER: int\n    M_SYSTEM: int\n    def add(self, key: bytes, size: int) -> None: ...\n    def iteritems(self, *, only_new: bool = ...) -> Iterator: ...\n    def clear_new(self) -> None: ...\n    def __contains__(self, key: bytes) -> bool: ...\n    def __getitem__(self, key: bytes) -> Type[ChunkIndexEntry]: ...\n    def __setitem__(self, key: bytes, value: CIE) -> None: ...\n\nclass NSIndex1Entry(NamedTuple):\n    segment: int\n    offset: int\n\nclass NSIndex1:  # legacy\n    def iteritems(self, *args, **kwargs) -> Iterator: ...\n    def __contains__(self, key: bytes) -> bool: ...\n    def __getitem__(self, key: bytes) -> Any: ...\n    def __setitem__(self, key: bytes, value: Any) -> None: ...\n\nclass FuseVersionsIndexEntry(NamedTuple):\n    version: int\n    hash: bytes\n\nclass FuseVersionsIndex:\n    def __contains__(self, key: bytes) -> bool: ...\n    def __getitem__(self, key: bytes) -> Any: ...\n    def __setitem__(self, key: bytes, value: Any) -> None: ...\n"
  },
  {
    "path": "src/borg/hashindex.pyx",
    "content": "from collections.abc import MutableMapping\nfrom collections import namedtuple\nimport os\nimport struct\n\nfrom borghash import HashTableNT\n\n\n\ncdef _NoDefault = object()\n\n\nclass HTProxyMixin:\n    def __setitem__(self, key, value):\n        self.ht[key] = value\n\n    def __getitem__(self, key):\n        return self.ht[key]\n\n    def __delitem__(self, key):\n        del self.ht[key]\n\n    def __contains__(self, key):\n        return key in self.ht\n\n    def __len__(self):\n        return len(self.ht)\n\n    def __iter__(self):\n        for key, value in self.ht.items():\n            yield key\n\n    def clear(self):\n        self.ht.clear()\n\n\nChunkIndexEntry = namedtuple('ChunkIndexEntry', 'flags size')\nChunkIndexEntryFormatT = namedtuple('ChunkIndexEntryFormatT', 'flags size')\nChunkIndexEntryFormat = ChunkIndexEntryFormatT(flags=\"I\", size=\"I\")\n\n\nclass ChunkIndex(HTProxyMixin, MutableMapping):\n    \"\"\"\n    Mapping from key256 to (flags32, size32) to track chunks in the repository.\n    \"\"\"\n    # .flags related values:\n    F_NONE = 0  # all flags cleared\n    M_USER = 0x00ffffff  # mask for user flags\n    M_SYSTEM = 0xff000000  # mask for system flags\n    # user flags:\n    F_USED = 2 ** 0  # chunk is used/referenced\n    F_COMPRESS = 2 ** 1  # chunk shall get (re-)compressed\n    # system flags (internal use, always 0 to user, not changeable by user):\n    F_NEW = 2 ** 24  # a new chunk that is not present in repo/cache/chunks.* yet.\n\n    def __init__(self, capacity=1000, path=None, usable=None):\n        if path:\n            self.ht = HashTableNT.read(path)\n        else:\n            if usable is not None:\n                capacity = usable * 2  # load factor 0.5\n            self.ht = HashTableNT(key_size=32, value_type=ChunkIndexEntry, value_format=ChunkIndexEntryFormat,\n                                  capacity=capacity)\n\n    def hide_system_flags(self, value):\n        user_flags = value.flags & self.M_USER\n        return value._replace(flags=user_flags)\n\n    def iteritems(self, *, only_new=False):\n        \"\"\"Iterates items (optionally only new items); hides system flags.\"\"\"\n        for key, value in self.ht.items():\n            if not only_new or (value.flags & self.F_NEW):\n                yield key, self.hide_system_flags(value)\n\n    def add(self, key, size):\n        v = self.get(key)\n        if v is None:\n            flags = self.F_USED\n        else:\n            flags = v.flags | self.F_USED\n            assert v.size == 0 or v.size == size\n        self[key] = ChunkIndexEntry(flags=flags, size=size)\n\n    def __getitem__(self, key):\n        \"\"\"Specialized __getitem__ that hides system flags.\"\"\"\n        value = self.ht[key]\n        return self.hide_system_flags(value)\n\n    def __setitem__(self, key, value):\n        \"\"\"Specialized __setitem__ that protects system flags and manages the F_NEW flag.\"\"\"\n        try:\n            prev = self.ht[key]\n        except KeyError:\n            prev_flags = self.F_NONE\n            is_new = True\n        else:\n            prev_flags = prev.flags\n            is_new = bool(prev_flags & self.F_NEW)  # was new? stays new!\n        system_flags = prev_flags & self.M_SYSTEM\n        if is_new:\n            system_flags |= self.F_NEW\n        else:\n            system_flags &= ~self.F_NEW\n        user_flags = value.flags & self.M_USER\n        self.ht[key] = value._replace(flags=system_flags | user_flags)\n\n    def clear_new(self):\n        \"\"\"Clears the F_NEW flag of all items.\"\"\"\n        for key, value in self.ht.items():\n            if value.flags & self.F_NEW:\n                flags = value.flags & ~self.F_NEW\n                self.ht[key] = value._replace(flags=flags)\n\n    @classmethod\n    def read(cls, path):\n        return cls(path=path)\n\n    def write(self, path):\n        self.ht.write(path)\n\n    def size(self):\n        return self.ht.size()\n\n    @property\n    def stats(self):\n        return self.ht.stats\n\n    def k_to_idx(self, key):\n        return self.ht.k_to_idx(key)\n\n    def idx_to_k(self, idx):\n        return self.ht.idx_to_k(idx)\n\n\nFuseVersionsIndexEntry = namedtuple('FuseVersionsIndexEntry', 'version hash')\nFuseVersionsIndexEntryFormatT = namedtuple('FuseVersionsIndexEntryFormatT', 'version hash')\nFuseVersionsIndexEntryFormat = FuseVersionsIndexEntryFormatT(version=\"I\", hash=\"16s\")\n\n\nclass FuseVersionsIndex(HTProxyMixin, MutableMapping):\n    \"\"\"\n    Mapping from key128 to (file_version32, file_content_hash128) to support the FUSE versions view.\n    \"\"\"\n    def __init__(self):\n        self.ht = HashTableNT(key_size=16, value_type=FuseVersionsIndexEntry, value_format=FuseVersionsIndexEntryFormat)\n\n\nNSIndex1Entry = namedtuple('NSIndex1Entry', 'segment offset')\nNSIndex1EntryFormatT = namedtuple('NSIndex1EntryFormatT', 'segment offset')\nNSIndex1EntryFormat = NSIndex1EntryFormatT(segment=\"I\", offset=\"I\")\n\n\nclass NSIndex1(HTProxyMixin, MutableMapping):\n    \"\"\"\n    Mapping from key256 to (segment32, offset32), as used by the legacy repository index of Borg 1.x.\n    \"\"\"\n    MAX_VALUE = 2**32 - 1  # borghash has the full uint32_t range\n    MAGIC = b\"BORG_IDX\"  # borg 1.x\n    HEADER_FMT = \"<8sIIBB\"  # magic, entries, buckets, ksize, vsize\n    KEY_SIZE = 32\n    VALUE_SIZE = 8\n\n    def __init__(self, capacity=1000, path=None, usable=None):\n        if usable is not None:\n            capacity = usable * 2  # load factor 0.5\n        self.ht = HashTableNT(key_size=self.KEY_SIZE, value_type=NSIndex1Entry, value_format=NSIndex1EntryFormat,\n                              capacity=capacity)\n        if path:\n            self._read(path)\n\n    def iteritems(self, marker=None):\n        do_yield = marker is None\n        for key, value in self.ht.items():\n            if do_yield:\n                yield key, value\n            else:\n                do_yield = key == marker\n\n    @classmethod\n    def read(cls, path):\n        return cls(path=path)\n\n    def size(self):\n        return self.ht.size()  # not quite correct as this is not the on-disk read-only format.\n\n    def write(self, path):\n        if isinstance(path, str):\n            with open(path, 'wb') as fd:\n                self._write_fd(fd)\n        else:\n            self._write_fd(path)\n\n    def _read(self, path):\n        if isinstance(path, str):\n            with open(path, 'rb') as fd:\n                self._read_fd(fd)\n        else:\n            self._read_fd(path)\n\n    def _write_fd(self, fd):\n        used = len(self.ht)\n        header_bytes = struct.pack(self.HEADER_FMT, self.MAGIC, used, used, self.KEY_SIZE, self.VALUE_SIZE)\n        fd.write(header_bytes)\n        # record the header as a separate integrity-hash part if supported\n        hash_part = getattr(fd, \"hash_part\", None)\n        if hash_part:\n            hash_part(\"HashHeader\")\n        count = 0\n        for key, _ in self.ht.items():\n            value = self.ht._get_raw(key)\n            fd.write(key)\n            fd.write(value)\n            count += 1\n        assert count == used\n\n    def _read_fd(self, fd):\n        header_size = struct.calcsize(self.HEADER_FMT)\n        header_bytes = fd.read(header_size)\n        if len(header_bytes) < header_size:\n            raise ValueError(f\"Invalid file: file is too short (header).\")\n        # verify the header as a separate integrity-hash part if supported\n        hash_part = getattr(fd, \"hash_part\", None)\n        if hash_part:\n            hash_part(\"HashHeader\")\n        magic, entries, buckets, ksize, vsize = struct.unpack(self.HEADER_FMT, header_bytes)\n        if magic != self.MAGIC:\n            raise ValueError(f\"Invalid file: magic {self.MAGIC.decode()} not found.\")\n        assert ksize == self.KEY_SIZE, \"invalid key size\"\n        assert vsize == self.VALUE_SIZE, \"invalid value size\"\n        buckets_size = buckets * (ksize + vsize)\n        current_pos = fd.tell()\n        end_of_file = fd.seek(0, os.SEEK_END)\n        if current_pos + buckets_size != end_of_file:\n            raise ValueError(f\"Invalid file: file size does not match (buckets).\")\n        fd.seek(current_pos)\n        for i in range(buckets):\n            key = fd.read(ksize)\n            value = fd.read(vsize)\n            if value.startswith(b'\\xFF\\xFF\\xFF\\xFF'):  # LE for 0xffffffff (empty/unused bucket)\n                continue\n            if value.startswith(b'\\xFE\\xFF\\xFF\\xFF'):  # LE for 0xfffffffe (deleted/tombstone bucket)\n                continue\n            self.ht._set_raw(key, value)\n        pos = fd.tell()\n        assert pos == end_of_file\n"
  },
  {
    "path": "src/borg/helpers/__init__.py",
    "content": "\"\"\"\nThis package contains helper and utility functionality that did not fit elsewhere.\n\nCode used to be in borg/helpers.py but was split into modules in this\npackage, which are imported here for compatibility.\n\"\"\"\n\nimport os\nimport logging\nfrom collections import namedtuple\n\nfrom ..constants import *  # NOQA\n\nfrom .datastruct import StableDict, Buffer, EfficientCollectionQueue\nfrom .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError, CancelledByUser, CommandError\nfrom .errors import RTError, modern_ec\nfrom .errors import BorgWarning, FileChangedWarning, BackupWarning, IncludePatternNeverMatchedWarning\nfrom .errors import BackupError, BackupOSError, BackupRaceConditionError, BackupItemExcluded\nfrom .errors import BackupPermissionError, BackupIOError, BackupFileNotFoundError\nfrom .fs import ensure_dir, join_base_dir, get_socket_filename\nfrom .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir\nfrom .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder\nfrom .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, get_strip_prefix, umount, slashify\nfrom .fs import O_, flags_dir, flags_special_follow, flags_special, flags_base, flags_normal, flags_noatime\nfrom .fs import HardLinkManager\nfrom .misc import sysinfo, log_multi, consume\nfrom .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper\nfrom .parseformat import octal_int, bin_to_hex, hex_to_bin, safe_encode, safe_decode\nfrom .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd\nfrom .parseformat import eval_escapes, decode_dict, interval\nfrom .parseformat import (\n    PathSpec,\n    FilesystemPathSpec,\n    SortBySpec,\n    CompressionSpec,\n    ChunkerParams,\n    FilesCacheMode,\n    partial_format,\n    DatetimeWrapper,\n)\nfrom .parseformat import format_file_size, parse_file_size, FileSize\nfrom .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator\nfrom .parseformat import format_line, replace_placeholders, PlaceholderError, relative_time_marker_validator\nfrom .parseformat import format_archive, parse_stringified_list, clean_lines\nfrom .parseformat import location_validator, archivename_validator, comment_validator, tag_validator\nfrom .parseformat import BaseFormatter, ArchiveFormatter, ItemFormatter, DiffFormatter, file_status\nfrom .parseformat import swidth_slice, ellipsis_truncate\nfrom .parseformat import BorgJsonEncoder, basic_json_data, json_print, json_dump, prepare_dump_dict\nfrom .parseformat import Highlander, MakePathSafeAction\nfrom .process import daemonize, daemonizing, ThreadRunner\nfrom .process import signal_handler, raising_signal_handler, sig_int, ignore_sigint, SigHup, SigTerm\nfrom .process import popen_with_error_handling, is_terminal, prepare_subprocess_env, create_filter_process\nfrom .progress import ProgressIndicatorPercent, ProgressIndicatorMessage\nfrom .time import parse_timestamp, timestamp, safe_timestamp, safe_s, safe_ns, MAX_S, SUPPORT_32BIT_PLATFORMS\nfrom .time import format_time, format_timedelta, OutputTimestamp, archive_ts_now\nfrom .yes_no import yes, TRUISH, FALSISH, DEFAULTISH\n\nfrom .msgpack import is_slow_msgpack, is_supported_msgpack, get_limited_unpacker\nfrom . import msgpack\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\n# generic mechanism to enable users to invoke workarounds by setting the\n# BORG_WORKAROUNDS environment variable to a list of comma-separated strings.\n# see the docs for a list of known workaround strings.\nworkarounds = tuple(os.environ.get(\"BORG_WORKAROUNDS\", \"\").split(\",\"))\n\n\n# element data type for warnings_list:\nwarning_info = namedtuple(\"warning_info\", \"wc,msg,args,wt\")\n\n\"\"\"\nThe global warnings_list variable is used to collect warning_info elements while Borg is running.\n\"\"\"\n_warnings_list: list[warning_info] = []\n\n\ndef add_warning(msg, *args, **kwargs):\n    global _warnings_list\n    warning_code = kwargs.get(\"wc\", EXIT_WARNING)\n    assert isinstance(warning_code, int)\n    warning_type = kwargs.get(\"wt\", \"percent\")\n    assert warning_type in (\"percent\", \"curly\")\n    _warnings_list.append(warning_info(warning_code, msg, args, warning_type))\n\n\n\"\"\"\nThe global exit_code variable allows modules other than archiver to increase the program's exit code if a\nwarning or error occurs during their operation.\n\"\"\"\n_exit_code = EXIT_SUCCESS\n\n\ndef classify_ec(ec):\n    if not isinstance(ec, int):\n        raise TypeError(\"ec must be of type int\")\n    if EXIT_SIGNAL_BASE <= ec <= 255:\n        return \"signal\"\n    elif ec == EXIT_ERROR or EXIT_ERROR_BASE <= ec < EXIT_WARNING_BASE:\n        return \"error\"\n    elif ec == EXIT_WARNING or EXIT_WARNING_BASE <= ec < EXIT_SIGNAL_BASE:\n        return \"warning\"\n    elif ec == EXIT_SUCCESS:\n        return \"success\"\n    else:\n        raise ValueError(f\"invalid error code: {ec}\")\n\n\ndef max_ec(ec1, ec2):\n    \"\"\"Return the more severe error code of ec1 and ec2.\"\"\"\n    # note: usually, there can be only 1 error-class ec, the other ec is then either success or warning.\n    ec1_class = classify_ec(ec1)\n    ec2_class = classify_ec(ec2)\n    if ec1_class == \"signal\":\n        return ec1\n    if ec2_class == \"signal\":\n        return ec2\n    if ec1_class == \"error\":\n        return ec1\n    if ec2_class == \"error\":\n        return ec2\n    if ec1_class == \"warning\":\n        return ec1\n    if ec2_class == \"warning\":\n        return ec2\n    assert ec1 == ec2 == EXIT_SUCCESS\n    return EXIT_SUCCESS\n\n\ndef set_ec(ec):\n    \"\"\"\n    Set the exit code of the program to ec if ec is more severe than the current exit code.\n    \"\"\"\n    global _exit_code\n    _exit_code = max_ec(_exit_code, ec)\n\n\ndef init_ec_warnings(ec=EXIT_SUCCESS, warnings=None):\n    \"\"\"\n    (Re-)initialize the globals for the exit code and the warnings list.\n    \"\"\"\n    global _exit_code, _warnings_list\n    _exit_code = ec\n    warnings = [] if warnings is None else warnings\n    assert isinstance(warnings, list)\n    _warnings_list = warnings\n\n\ndef get_ec(ec=None):\n    \"\"\"\n    Compute the final return code of the Borg process.\n    \"\"\"\n    if ec is not None:\n        set_ec(ec)\n\n    global _exit_code\n    exit_code_class = classify_ec(_exit_code)\n    if exit_code_class in (\"signal\", \"error\", \"warning\"):\n        # there was a signal/error/warning, return its exit code\n        return _exit_code\n    assert exit_code_class == \"success\"\n    global _warnings_list\n    if not _warnings_list:\n        # we do not have any warnings in warnings list, return success exit code\n        return _exit_code\n    # looks like we have some warning(s)\n    rcs = sorted({w_info.wc for w_info in _warnings_list})\n    logger.debug(f\"rcs: {rcs!r}\")\n    if len(rcs) == 1:\n        # easy: there was only one kind of warning, so we can be specific\n        return rcs[0]\n    # there were different kinds of warnings\n    return EXIT_WARNING  # generic warning rc, user has to look into the logs\n\n\ndef get_reset_ec(ec=None):\n    \"\"\"Like get_ec, but reinitialize ec/warnings afterwards.\"\"\"\n    rc = get_ec(ec)\n    init_ec_warnings()\n    return rc\n\n\ndef do_show_rc(exit_code):\n    \"\"\"Log the program return code using the dedicated 'borg.output.show-rc' logger.\n\n    Uses INFO/WARNING/ERROR levels depending on the classified exit code.\n\n    This helper is robust: it swallows any exceptions to avoid interfering with\n    program exit behavior. Callers do not need to guard it with try/except.\n    \"\"\"\n    try:\n        exit_msg = \"terminating with %s status, rc %d\"\n        rc_logger = logging.getLogger(\"borg.output.show-rc\")\n        try:\n            ec_class = classify_ec(exit_code)\n        except ValueError:\n            rc_logger.error(exit_msg % (\"abnormal\", exit_code or 666))\n        else:\n            if ec_class == \"success\":\n                rc_logger.info(exit_msg % (ec_class, exit_code))\n            elif ec_class == \"warning\":\n                rc_logger.warning(exit_msg % (ec_class, exit_code))\n            elif ec_class in (\"error\", \"signal\"):\n                rc_logger.error(exit_msg % (ec_class, exit_code))\n    except Exception:  # nosec B110\n        # Never let logging issues interfere with exit behaviour\n        pass\n"
  },
  {
    "path": "src/borg/helpers/argparsing.py",
    "content": "\"\"\"\nBorg argument-parsing layer\n===========================\n\nAll imports of ``ArgumentParser``, ``Namespace``, ``SUPPRESS``, etc. come\nfrom this module.  It is the single seam between borg and the underlying\nparser library (jsonargparse).\n\nLibrary choice\n--------------\nBorg uses **jsonargparse** instead of plain argparse.  jsonargparse is a\nsuperset of argparse that additionally supports:\n\n* reading arguments from YAML/JSON config files (``--config``)\n* reading arguments from environment variables\n* nested namespaces for subcommands (each subcommand's arguments live in\n  their own ``Namespace`` object rather than the flat top-level namespace)\n\nParser hierarchy\n----------------\nBorg's command line has up to three levels::\n\n    borg [common-opts] <command> [common-opts] [<subcommand> [common-opts] [args]]\n\n    e.g.  borg --info create ...\n          borg create --info ...\n          borg debug info --debug ...\n\nThree ``ArgumentParser`` instances are constructed in ``build_parser()``:\n\n``parser`` (top-level)\n    The root parser.  Common options are registered here **with real\n    defaults** (``provide_defaults=True``).\n\n``common_parser``\n    A helper parser (``add_help=False``) passed as ``parents=[common_parser]``\n    to every *leaf* subcommand parser (e.g. ``create``, ``repo-create``, …).\n    Common options are registered here **with** ``default=SUPPRESS`` so that\n    an option not given on the command line leaves no attribute at all in the\n    subcommand namespace.\n\n``mid_common_parser``\n    Same as ``common_parser`` but used as the parent for *group* subcommand\n    parsers that introduce a second level (e.g. ``debug``, ``key``,\n    ``benchmark``).  Their *leaf* subcommand parsers also use\n    ``mid_common_parser`` as a parent.\n\nCommon options (``--info``, ``--debug``, ``--repo``, ``--lock-wait``, …)\nare managed by ``Archiver.CommonOptions``, which calls\n``define_common_options()`` once per parser so the same options appear at\nevery level with identical ``dest`` names.\n\nNamespace flattening and precedence\n-------------------------------------\njsonargparse stores each subcommand's parsed values in a nested\n``Namespace`` object::\n\n    # borg --info create --debug ...\n    Namespace(\n        log_level = \"info\",          # top-level\n        subcommand = \"create\",\n        create = Namespace(\n            log_level = \"debug\",     # subcommand level\n            ...\n        )\n    )\n\nAfter ``parser.parse_args()`` returns, ``flatten_namespace()`` collapses\nthis tree into a single ``Namespace`` that borg's dispatch and command\nimplementations expect.\n\nPrecedence rule:  the **most-specific** (innermost) value wins.\n``flatten_namespace`` uses ``Namespace.as_flat()`` (provided by jsonargparse)\nto linearise the nested tree into a flat dict with dotted keys encoding\ndepth, for example::\n\n    log_level            = \"info\"     # top-level (0 dots)\n    create.log_level     = \"debug\"    # one level deep (1 dot)\n    debug.info.log_level = \"critical\" # two levels deep (2 dots)\n\nThe entries are then sorted deepest-first so the most-specific value is\nencountered first and wins.  Shallower values only fill in if the key\nhas not been set yet.\n\nSpecial case — append-action options (e.g. ``--debug-topic``):\nIf a key already holds a list and the outer level also supplies a list,\nthe two lists are **merged** (outer values first, inner values last) so\nthat ``borg --debug-topic foo create --debug-topic bar`` accumulates\n``[\"foo\", \"bar\"]`` rather than losing one of the values.\n\nThe ``SUPPRESS`` default on sub-parsers is essential: if a common option\nis not given at the subcommand level, it simply produces no attribute in\nthe subcommand namespace and the outer (top-level) default flows through\nunchanged.\n\"\"\"\n\nfrom typing import Any\n\n# here are the only imports from argparse and jsonargparse,\n# all other imports of these names import them from here:\nfrom argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter  # noqa: F401\nfrom jsonargparse import ArgumentParser as _ArgumentParser  # we subclass that to add custom behavior\nfrom jsonargparse import Namespace, ActionSubCommands, SUPPRESS, REMAINDER  # noqa: F401\nfrom jsonargparse.typing import register_type, PositiveInt  # noqa: F401\n\n\nclass ArgumentParser(_ArgumentParser):\n    # the borg code always uses RawDescriptionHelpFormatter and add_help=False:\n    def __init__(self, *args, formatter_class=RawDescriptionHelpFormatter, add_help=False, **kwargs):\n        super().__init__(*args, formatter_class=formatter_class, add_help=add_help, **kwargs)\n\n\ndef flatten_namespace(ns: Any) -> Namespace:\n    \"\"\"\n    Flattens the nested namespace jsonargparse produces for subcommands into a\n    single-level namespace that borg's dispatch and command implementations expect.\n\n    Inner (subcommand) values take precedence over outer (top-level) values.\n    For list-typed values (append-action options like --debug-topic) that appear\n    at multiple levels, the lists are merged: outer values first, inner values last.\n    \"\"\"\n    flat = Namespace()\n\n    # Extract the joined subcommand path from the nested namespace tree.\n    subcmds = []\n    current = ns\n    while current and hasattr(current, \"subcommand\") and current.subcommand:\n        subcmds.append(current.subcommand)\n        current = getattr(current, current.subcommand, None)\n\n    if subcmds:\n        flat.subcommand = \" \".join(subcmds)\n\n    # as_flat() linearises the nested tree into dotted-key entries, e.g.:\n    #   log_level='info'               (outer, 0 dots)\n    #   create.log_level='debug'       (subcommand, 1 dot)\n    #   debug.info.log_level='crit'    (two-level subcommand, 2 dots)\n    # Sorting deepest-first ensures the most-specific value is processed first and therefore wins (\"inner wins\" rule).\n    all_items = sorted(vars(ns.as_flat()).items(), key=lambda kv: kv[0].count(\".\"), reverse=True)\n\n    for dotted_key, value in all_items:\n        dest = dotted_key.rsplit(\".\", 1)[-1]  # e.g. \"create.log_level\" -> \"log_level\"\n        if dest == \"subcommand\":\n            continue\n        existing = getattr(flat, dest, None)\n        if existing is None:\n            setattr(flat, dest, value)\n        elif isinstance(existing, list) and isinstance(value, list):\n            # Append-action options (e.g. --debug-topic): outer values come first.\n            setattr(flat, dest, list(value) + list(existing))\n\n    return flat\n"
  },
  {
    "path": "src/borg/helpers/datastruct.py",
    "content": "from .errors import Error\n\n\nclass StableDict(dict):\n    \"\"\"A dict subclass with stable items() ordering.\"\"\"\n\n    def items(self):\n        return sorted(super().items())\n\n\nclass Buffer:\n    \"\"\"\n    Provides a managed, resizable buffer.\n    \"\"\"\n\n    class MemoryLimitExceeded(Error, OSError):\n        \"\"\"Requested buffer size {} is above the limit of {}.\"\"\"\n\n    def __init__(self, allocator, size=4096, limit=None):\n        \"\"\"\n        Initialize the buffer by using allocator(size) to allocate a buffer.\n        Optionally, set an upper limit for the buffer size.\n        \"\"\"\n        assert callable(allocator), \"must give alloc(size) function as first param\"\n        assert limit is None or size <= limit, \"initial size must be <= limit\"\n        self.allocator = allocator\n        self.limit = limit\n        self.resize(size, init=True)\n\n    def __len__(self):\n        return len(self.buffer)\n\n    def resize(self, size, init=False):\n        \"\"\"\n        Resize the buffer. To avoid frequent reallocation, we usually grow (if needed).\n        By giving init=True it is possible to initialize for the first time or shrink the buffer.\n        If a buffer size beyond the limit is requested, raise Buffer.MemoryLimitExceeded (OSError).\n        \"\"\"\n        size = int(size)\n        if self.limit is not None and size > self.limit:\n            raise Buffer.MemoryLimitExceeded(size, self.limit)\n        if init or len(self) < size:\n            self.buffer = self.allocator(size)\n\n    def get(self, size=None, init=False):\n        \"\"\"\n        Return a buffer of at least the requested size (None: any current size).\n        init=True can be given to trigger shrinking of the buffer to the given size.\n        \"\"\"\n        if size is not None:\n            self.resize(size, init)\n        return self.buffer\n\n\nclass EfficientCollectionQueue:\n    \"\"\"\n    An efficient FIFO queue that splits received elements into chunks.\n    \"\"\"\n\n    class SizeUnderflow(Error):\n        \"\"\"Could not pop the first {} elements; collection only has {} elements.\"\"\"\n\n    def __init__(self, split_size, member_type):\n        \"\"\"\n        Initialize an empty queue.\n        split_size defines the maximum chunk size.\n        member_type is the type that defines what the base collection looks like.\n        \"\"\"\n        self.buffers = []\n        self.size = 0\n        self.split_size = split_size\n        self.member_type = member_type\n\n    def peek_front(self):\n        \"\"\"\n        Return the first chunk from the queue without removing it.\n        The returned collection will have between 1 and split_size elements.\n        Returns an empty collection when nothing is queued.\n        \"\"\"\n        if not self.buffers:\n            return self.member_type()\n        buffer = self.buffers[0]\n        return buffer\n\n    def pop_front(self, size):\n        \"\"\"\n        Remove the first `size` elements from the queue.\n        Raises an error if the requested removal size is larger than the entire queue.\n        \"\"\"\n        if size > self.size:\n            raise EfficientCollectionQueue.SizeUnderflow(size, self.size)\n        while size > 0:\n            buffer = self.buffers[0]\n            to_remove = min(size, len(buffer))\n            buffer = buffer[to_remove:]\n            if buffer:\n                self.buffers[0] = buffer\n            else:\n                del self.buffers[0]\n            size -= to_remove\n            self.size -= to_remove\n\n    def push_back(self, data):\n        \"\"\"\n        Add data at the end of the queue.\n        Chunks data into elements of size up to split_size.\n        \"\"\"\n        if not self.buffers:\n            self.buffers = [self.member_type()]\n        while data:\n            buffer = self.buffers[-1]\n            if len(buffer) >= self.split_size:\n                buffer = self.member_type()\n                self.buffers.append(buffer)\n\n            to_add = min(len(data), self.split_size - len(buffer))\n            buffer += data[:to_add]\n            data = data[to_add:]\n            self.buffers[-1] = buffer\n            self.size += to_add\n\n    def __len__(self):\n        \"\"\"\n        Return the current queue length for all elements across all chunks.\n        \"\"\"\n        return self.size\n\n    def __bool__(self):\n        \"\"\"\n        Return True if the queue is not empty.\n        \"\"\"\n        return self.size != 0\n"
  },
  {
    "path": "src/borg/helpers/errors.py",
    "content": "import os\n\nfrom ..constants import *  # NOQA\n\nfrom ..crypto.low_level import IntegrityError as IntegrityErrorBase\n\n\nmodern_ec = os.environ.get(\"BORG_EXIT_CODES\", \"modern\") == \"modern\"  # \"legacy\" was used by borg < 2\n\n\nclass ErrorBase(Exception):\n    \"\"\"ErrorBase: {}\"\"\"\n\n    # Error base class\n\n    # if we raise such an Error and it is only caught by the uppermost\n    # exception handler (that exits short after with the given exit_code),\n    # it is always a (fatal and abrupt) error, never just a warning.\n    exit_mcode = EXIT_ERROR  # modern, more specific exit code (defaults to EXIT_ERROR)\n\n    # show a traceback?\n    traceback = False\n\n    def __init__(self, *args):\n        super().__init__(*args)\n        self.args = args\n\n    def get_message(self):\n        return type(self).__doc__.format(*self.args)\n\n    __str__ = get_message\n\n    @property\n    def exit_code(self):\n        # legacy: borg used to always use rc 2 (EXIT_ERROR) for all errors.\n        # modern: users can opt in to more specific return codes, using BORG_EXIT_CODES:\n        return self.exit_mcode if modern_ec else EXIT_ERROR\n\n\nclass Error(ErrorBase):\n    \"\"\"Error: {}\"\"\"\n\n\nclass ErrorWithTraceback(Error):\n    \"\"\"Error: {}\"\"\"\n\n    # like Error, but show a traceback also\n    traceback = True\n\n\nclass IntegrityError(ErrorWithTraceback, IntegrityErrorBase):\n    \"\"\"Data integrity error: {}\"\"\"\n\n    exit_mcode = 90\n\n\nclass DecompressionError(IntegrityError):\n    \"\"\"Decompression error: {}\"\"\"\n\n    exit_mcode = 92\n\n\nclass CancelledByUser(Error):\n    \"\"\"Cancelled by user.\"\"\"\n\n    exit_mcode = 3\n\n\nclass RTError(Error):\n    \"\"\"Runtime error: {}\"\"\"\n\n\nclass CommandError(Error):\n    \"\"\"Command error: {}\"\"\"\n\n    exit_mcode = 4\n\n\nclass BorgWarning:\n    \"\"\"Warning: {}\"\"\"\n\n    # Warning base class\n\n    # please note that this class and its subclasses are NOT exceptions, we do not raise them.\n    # so this is just to have inheritance, inspectability and the exit_code property.\n    exit_mcode = EXIT_WARNING  # modern, more specific exit code (defaults to EXIT_WARNING)\n\n    def __init__(self, *args):\n        self.args = args\n\n    def get_message(self):\n        return type(self).__doc__.format(*self.args)\n\n    __str__ = get_message\n\n    @property\n    def exit_code(self):\n        # legacy: borg used to always use rc 1 (EXIT_WARNING) for all warnings.\n        # modern: users can opt in to more specific return codes, using BORG_EXIT_CODES:\n        return self.exit_mcode if modern_ec else EXIT_WARNING\n\n\nclass FileChangedWarning(BorgWarning):\n    \"\"\"{}: file changed while we backed it up.\"\"\"\n\n    exit_mcode = 100\n\n\nclass IncludePatternNeverMatchedWarning(BorgWarning):\n    \"\"\"Include pattern '{}' never matched.\"\"\"\n\n    exit_mcode = 101\n\n\nclass BackupWarning(BorgWarning):\n    \"\"\"{}: {}\"\"\"\n\n    # this is to wrap a caught BackupError exception, so it can be given to print_warning_instance\n\n    @property\n    def exit_code(self):\n        if not modern_ec:\n            return EXIT_WARNING\n        exc = self.args[1]\n        assert isinstance(exc, BackupError)\n        return exc.exit_mcode\n\n\nclass BackupError(ErrorBase):\n    \"\"\"{}: backup error\"\"\"\n\n    # Exception raised for non-OSError-based exceptions while accessing backup files.\n    exit_mcode = 102\n\n\nclass BackupRaceConditionError(BackupError):\n    \"\"\"{}: file type or inode changed while we backed it up (race condition, skipped file)\"\"\"\n\n    # Exception raised when encountering a critical race condition while trying to back up a file.\n    exit_mcode = 103\n\n\nclass BackupOSError(BackupError):\n    \"\"\"{}: {}\"\"\"\n\n    # Wrapper for OSError raised while accessing backup files.\n    #\n    # Borg does different kinds of IO, and IO failures have different consequences.\n    # This wrapper represents failures of input file or extraction IO.\n    # These are non-critical and are only reported (warnings).\n    #\n    # Any unwrapped IO error is critical and aborts execution (for example repository IO failure).\n    exit_mcode = 104\n\n    def __init__(self, op, os_error):\n        self.op = op\n        self.os_error = os_error\n        self.errno = os_error.errno\n        self.strerror = os_error.strerror\n        self.filename = os_error.filename\n\n    def __str__(self):\n        if self.op:\n            return f\"{self.op}: {self.os_error}\"\n        else:\n            return str(self.os_error)\n\n\nclass BackupPermissionError(BackupOSError):\n    \"\"\"{}: {}\"\"\"\n\n    exit_mcode = 105\n\n\nclass BackupIOError(BackupOSError):\n    \"\"\"{}: {}\"\"\"\n\n    exit_mcode = 106\n\n\nclass BackupFileNotFoundError(BackupOSError):\n    \"\"\"{}: {}\"\"\"\n\n    exit_mcode = 107\n\n\nclass BackupItemExcluded(Exception):\n    \"\"\"Used internally to skip an item from processing when it is excluded.\"\"\"\n"
  },
  {
    "path": "src/borg/helpers/fs.py",
    "content": "import errno\nimport hashlib\nimport os\nimport posixpath\nimport re\nimport stat\nimport subprocess\nimport sys\nimport textwrap\nfrom pathlib import Path\n\nimport platformdirs\n\nfrom .errors import Error\n\nfrom .process import prepare_subprocess_env\nfrom ..platformflags import is_win32\n\nfrom ..constants import *  # NOQA\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\ndef ensure_dir(path, mode=stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO, pretty_deadly=True):\n    \"\"\"\n    Ensure that the directory exists with the correct permissions.\n\n    1) Create the directory in a race-free manner.\n    2) If mode is not None and the directory was created, set the correct\n       permissions on the leaf directory (masking out the current umask first).\n    3) If pretty_deadly is True, catch exceptions and re-raise them with a clearer\n       message.\n\n    Returns normally if the directory exists (with correct permissions) or was created.\n    Raises an exception otherwise. If a fatal exception occurs, it is re-raised.\n    \"\"\"\n    try:\n        Path(path).mkdir(mode=mode, parents=True, exist_ok=True)\n    except OSError as e:\n        if pretty_deadly:\n            raise Error(str(e))\n        else:\n            raise\n\n\ndef get_base_dir(*, legacy=False):\n    \"\"\"Get home directory / base directory for Borg:\n\n    - BORG_BASE_DIR, if set\n    - HOME, if set\n    - ~$USER, if USER is set\n    - ~\n    \"\"\"\n    if legacy:\n        base_dir = os.environ.get(\"BORG_BASE_DIR\") or os.environ.get(\"HOME\")\n        # Path.expanduser() behaves differently for '~' and '~someuser' as\n        # parameters: when called with an explicit username, the possibly set\n        # environment variable HOME is no longer respected. So we have to check if\n        # it is set and only expand the user's home directory if HOME is unset.\n        if not base_dir:\n            base_dir = str(Path(f\"~{os.environ.get('USER', '')}\").expanduser())\n    else:\n        # we only care for BORG_BASE_DIR here, as it can be used to override the base dir\n        # and not use any more or less platform specific way to determine the base dir.\n        base_dir = os.environ.get(\"BORG_BASE_DIR\")\n    return base_dir\n\n\ndef join_base_dir(*paths, **kw):\n    legacy = kw.get(\"legacy\", True)\n    base_dir = get_base_dir(legacy=legacy)\n    return None if base_dir is None else str(Path(base_dir).joinpath(*paths))\n\n\ndef get_keys_dir(*, legacy=False, create=True):\n    \"\"\"Determine where to store repository keys.\"\"\"\n    keys_dir = os.environ.get(\"BORG_KEYS_DIR\")\n    if keys_dir is None:\n        # note: do not just give this as default to the environment.get(), see issue #5979.\n        keys_dir = str(Path(get_config_dir(legacy=legacy)) / \"keys\")\n    if create:\n        ensure_dir(keys_dir)\n    return keys_dir\n\n\ndef get_security_dir(repository_id=None, *, legacy=False, create=True):\n    \"\"\"Determine where to store local security information.\"\"\"\n    security_dir = os.environ.get(\"BORG_SECURITY_DIR\")\n    if security_dir is None:\n        get_dir = get_config_dir if legacy else get_data_dir\n        # note: do not just give this as default to the environment.get(), see issue #5979.\n        security_dir = str(Path(get_dir(legacy=legacy)) / \"security\")\n    if repository_id:\n        security_dir = str(Path(security_dir) / repository_id)\n    if create:\n        ensure_dir(security_dir)\n    return security_dir\n\n\ndef get_data_dir(*, legacy=False, create=True):\n    \"\"\"Determine where to store borg changing data on the client\"\"\"\n    assert legacy is False, \"there is no legacy variant of the borg data dir\"\n    data_dir = os.environ.get(\n        \"BORG_DATA_DIR\", join_base_dir(\".local\", \"share\", \"borg\", legacy=legacy) or platformdirs.user_data_dir(\"borg\")\n    )\n    if create:\n        ensure_dir(data_dir)\n    return data_dir\n\n\ndef get_runtime_dir(*, legacy=False, create=True):\n    \"\"\"Determine where to store runtime files, like sockets, PID files, ...\"\"\"\n    assert legacy is False, \"there is no legacy variant of the borg runtime dir\"\n    runtime_dir = os.environ.get(\n        \"BORG_RUNTIME_DIR\", join_base_dir(\".cache\", \"borg\", legacy=legacy) or platformdirs.user_runtime_dir(\"borg\")\n    )\n    if create:\n        ensure_dir(runtime_dir)\n    return runtime_dir\n\n\ndef get_socket_filename():\n    return str(Path(get_runtime_dir()) / \"borg.sock\")\n\n\ndef get_cache_dir(*, legacy=False, create=True):\n    \"\"\"Determine where to store Borg cache data.\"\"\"\n\n    if legacy:\n        # Get cache home path\n        cache_home = join_base_dir(\".cache\", legacy=legacy)\n        # Try to use XDG_CACHE_HOME instead if BORG_BASE_DIR isn't explicitly set\n        if not os.environ.get(\"BORG_BASE_DIR\"):\n            cache_home = os.environ.get(\"XDG_CACHE_HOME\", cache_home)\n        # Use BORG_CACHE_DIR if set, otherwise assemble final path from cache home path\n        cache_dir = os.environ.get(\"BORG_CACHE_DIR\", str(Path(cache_home) / \"borg\"))\n    else:\n        cache_dir = os.environ.get(\n            \"BORG_CACHE_DIR\", join_base_dir(\".cache\", \"borg\", legacy=legacy) or platformdirs.user_cache_dir(\"borg\")\n        )\n    if create:\n        ensure_dir(cache_dir)\n        cache_tag_fn = Path(cache_dir) / CACHE_TAG_NAME\n        if not cache_tag_fn.exists():\n            cache_tag_contents = (\n                CACHE_TAG_CONTENTS\n                + textwrap.dedent(\n                    \"\"\"\n            # This file is a cache directory tag created by Borg.\n            # For information about cache directory tags, see:\n            #       https://www.bford.info/cachedir/spec.html\n            \"\"\"\n                ).encode(\"ascii\")\n            )\n            from ..platform import SaveFile\n\n            with SaveFile(cache_tag_fn, binary=True) as fd:\n                fd.write(cache_tag_contents)\n    return cache_dir\n\n\ndef get_config_dir(*, legacy=False, create=True):\n    \"\"\"Determine where to store the configuration.\"\"\"\n\n    # Get config home path\n    if legacy:\n        config_home = join_base_dir(\".config\", legacy=legacy)\n        # Try to use XDG_CONFIG_HOME instead if BORG_BASE_DIR isn't explicitly set\n        if not os.environ.get(\"BORG_BASE_DIR\"):\n            config_home = os.environ.get(\"XDG_CONFIG_HOME\", config_home)\n        # Use BORG_CONFIG_DIR if set, otherwise assemble final path from config home path\n        config_dir = os.environ.get(\"BORG_CONFIG_DIR\", str(Path(config_home) / \"borg\"))\n    else:\n        config_dir = os.environ.get(\n            \"BORG_CONFIG_DIR\", join_base_dir(\".config\", \"borg\", legacy=legacy) or platformdirs.user_config_dir(\"borg\")\n        )\n    if create:\n        ensure_dir(config_dir)\n    return config_dir\n\n\ndef dir_is_cachedir(path=None, dir_fd=None):\n    \"\"\"Determines whether the specified directory is a cache directory (and\n    therefore should potentially be excluded from the backup) according to\n    the CACHEDIR.TAG protocol (https://www.bford.info/cachedir/spec.html).\n\n    If dir_fd is provided, operations will be based on the directory file descriptor.\n    Otherwise (path is provided), operations will be based on the directory path.\n    \"\"\"\n    tag_fd = None\n    try:\n        if dir_fd is not None:\n            tag_fd = os.open(CACHE_TAG_NAME, os.O_RDONLY, dir_fd=dir_fd)\n        else:\n            tag_fd = os.open(str(Path(path) / CACHE_TAG_NAME), os.O_RDONLY)\n        return os.read(tag_fd, len(CACHE_TAG_CONTENTS)) == CACHE_TAG_CONTENTS\n    except (FileNotFoundError, OSError):\n        return False\n    finally:\n        if tag_fd is not None:\n            os.close(tag_fd)\n\n\ndef dir_is_tagged(path=None, exclude_caches=None, exclude_if_present=None, dir_fd=None):\n    \"\"\"Determines whether the specified path is excluded by being a cache\n    directory or containing user-specified tag files/directories. Returns a\n    list of the names of the tag files/directories (either CACHEDIR.TAG or the\n    matching user-specified files/directories).\n\n    If dir_fd is provided, operations will be based on the directory file descriptor.\n    Otherwise (path is provided), operations will be based on the directory path.\n    \"\"\"\n    tag_names = []\n\n    if dir_fd is not None:\n        # Use file descriptor-based operations\n        if exclude_caches and dir_is_cachedir(dir_fd=dir_fd):\n            tag_names.append(CACHE_TAG_NAME)\n        if exclude_if_present is not None:\n            for tag in exclude_if_present:\n                try:\n                    os.stat(tag, dir_fd=dir_fd)\n                    tag_names.append(tag)\n                except FileNotFoundError:\n                    pass\n    else:\n        # Use path-based operations (for backward compatibility)\n        if exclude_caches and dir_is_cachedir(path=path):\n            tag_names.append(CACHE_TAG_NAME)\n        if exclude_if_present is not None:\n            for tag in exclude_if_present:\n                tag_path = Path(path) / tag\n                if tag_path.exists():\n                    tag_names.append(tag)\n\n    return tag_names\n\n\ndef make_path_safe(path):\n    \"\"\"\n    Make path safe by making it relative and normalized.\n\n    `path` is sanitized by making it relative, removing\n    consecutive slashes (e.g. '//'), removing '.' elements,\n    and removing trailing slashes.\n\n    For reasons of security, a ValueError is raised should\n    `path` contain any '..' elements.\n    \"\"\"\n    if \"\\\\..\" in path or \"..\\\\\" in path:\n        raise ValueError(f\"unexpected '..' element in path {path!r}\")\n\n    path = slashify(path)\n\n    if is_win32 and len(path) >= 2 and path[1] == \":\":\n        # Handle drive letters: C:/path -> C/path\n        path = path[0].upper() + path[2:]\n\n    path = map_chars(path)\n\n    path = path.lstrip(\"/\")\n    if path.startswith(\"../\") or \"/../\" in path or path.endswith(\"/..\") or path == \"..\":\n        raise ValueError(f\"unexpected '..' element in path {path!r}\")\n    path = posixpath.normpath(path)\n    return path\n\n\ndef slashify(path):\n    \"\"\"\n    Replace backslashes with forward slashes if running on Windows.\n\n    Use case: we always want to use forward slashes, even on Windows.\n    \"\"\"\n    return path.replace(\"\\\\\", \"/\") if is_win32 else path\n\n\n# Bijective mapping to Unicode Private Use Area (like cifs mapchars)\nWINDOWS_MAP_CHARS = str.maketrans(\n    {\n        \"<\": \"\\uF03C\",\n        \">\": \"\\uF03E\",\n        \":\": \"\\uF03A\",\n        '\"': \"\\uF022\",\n        \"\\\\\": \"\\uF05C\",\n        \"|\": \"\\uF07C\",\n        \"?\": \"\\uF03F\",\n        \"*\": \"\\uF02A\",\n    }\n)\n\n\ndef map_chars(path):\n    \"\"\"\n    Map reserved characters if running on Windows.\n\n    Use case: if an archived path contains reserved characters (that are not reserved on POSIX)\n    we need to replace them with replacements to make the path usable on Windows.\n    \"\"\"\n    if not is_win32:\n        return path\n\n    return path.translate(WINDOWS_MAP_CHARS)\n\n\ndef get_strip_prefix(path):\n    # similar to how rsync does it, we allow users to give paths like:\n    # /this/gets/stripped/./this/is/kept\n    # the whole path is what is used to read from the fs,\n    # the strip_prefix will be /this/gets/stripped/ and\n    # this/is/kept is the path being archived.\n    pos = path.find(\"/./\")  # detect slashdot hack\n    if pos > 0:\n        # found a prefix to strip! make sure it ends with one \"/\"!\n        return posixpath.normpath(path[:pos]) + \"/\"\n    else:\n        # no or empty prefix, nothing to strip!\n        return None\n\n\n_dotdot_re = re.compile(r\"^(\\.\\./)+\")\n\n\ndef remove_dotdot_prefixes(path):\n    \"\"\"\n    Remove '../'s at the beginning of `path`. Additionally, the path is made relative.\n\n    `path` is expected to be normalized already (e.g. via `posixpath.normpath()`).\n    \"\"\"\n    if is_win32:\n        if len(path) > 1 and path[1] == \":\":\n            path = path.replace(\":\", \"\", 1)\n\n    path = path.lstrip(\"/\")\n    path = _dotdot_re.sub(\"\", path)\n    if path in [\"\", \"..\"]:\n        return \".\"\n    return path\n\n\ndef assert_sanitized_path(path):\n    assert isinstance(path, str)\n    # `path` should have been sanitized earlier. Some features,\n    # like pattern matching rely on a sanitized path. As a\n    # precaution we check here again.\n    if make_path_safe(path) != path:\n        raise ValueError(f\"path {path!r} is not sanitized\")\n    return path\n\n\ndef to_sanitized_path(path):\n    assert isinstance(path, str)\n    # Legacy versions of Borg still allowed non-sanitized paths\n    # to be stored. So, we sanitize them when reading.\n    #\n    # Borg 2 ensures paths are safe before storing them. Thus, when\n    # support for reading Borg 1 archives is dropped, this should be\n    # changed to a simple check to verify paths aren't malicious.\n    # Namely, absolute paths and paths containing '..' elements must\n    # be rejected.\n    #\n    # Also checks for '..' elements in `path` for reasons of security.\n    return make_path_safe(path)\n\n\nclass HardLinkManager:\n    \"\"\"\n    Manage hard links (and avoid code duplication doing so).\n\n    A) When creating a borg2 archive from the filesystem, we have to maintain a mapping like:\n       (dev, ino) -> (hlid, chunks)  # for fs_hl_targets\n       If we encounter the same (dev, ino) again later, we'll just re-use the hlid and chunks list.\n\n    B) When extracting a borg2 archive to the filesystem, we have to maintain a mapping like:\n       hlid -> path\n       If we encounter the same hlid again later, we hard link to the path of the already extracted\n       content of same hlid.\n\n    C) When transferring from a borg1 archive, we need:\n       path -> chunks_correct  # for borg1_hl_targets, chunks_correct must be either from .chunks_healthy or .chunks.\n       If we encounter a regular file item with source == path later, we reuse chunks_correct\n       and create the same hlid = hardlink_id_from_path(source).\n\n    D) When importing a tar file (simplified 1-pass way for now, not creating borg hard link items):\n       path -> chunks\n       If we encounter a LNK tar entry later with linkname==path, we re-use the chunks and create a regular file item.\n       For better hard link support (including the very first hard link item for each group of same-target hard links),\n       we would need a 2-pass processing, which is not yet implemented.\n    \"\"\"\n\n    def __init__(self, *, id_type, info_type):\n        self._map = {}\n        self.id_type = id_type\n        self.info_type = info_type  # can be a single type or a tuple of types\n\n    def borg1_hardlinkable(self, mode):  # legacy\n        return stat.S_ISREG(mode) or stat.S_ISBLK(mode) or stat.S_ISCHR(mode) or stat.S_ISFIFO(mode)\n\n    def borg1_hardlink_master(self, item):  # legacy\n        return item.get(\"hardlink_master\", False) and \"source\" not in item and self.borg1_hardlinkable(item.mode)\n\n    def borg1_hardlink_slave(self, item):  # legacy\n        return \"source\" in item and self.borg1_hardlinkable(item.mode)\n\n    def hardlink_id_from_path(self, path):\n        \"\"\"compute a hard link id from a path\"\"\"\n        assert isinstance(path, str)\n        return hashlib.sha256(path.encode(\"utf-8\", errors=\"surrogateescape\")).digest()\n\n    def hardlink_id_from_inode(self, *, ino, dev):\n        \"\"\"compute a hard link id from an inode\"\"\"\n        assert isinstance(ino, int)\n        assert isinstance(dev, int)\n        return hashlib.sha256(f\"{ino}/{dev}\".encode()).digest()\n\n    def remember(self, *, id, info):\n        \"\"\"\n        remember stuff from a (usually contentful) item.\n\n        :param id: some id used to reference to the contentful item, could be:\n                   a path (tar style, old borg style) [bytes]\n                   a hlid (new borg style) [bytes]\n                   a (dev, inode) tuple (filesystem)\n        :param info: information to remember, could be:\n                     chunks list\n                     hlid\n        \"\"\"\n        assert isinstance(id, self.id_type), f\"id is {id!r}, not of type {self.id_type}\"\n        assert isinstance(info, self.info_type), f\"info is {info!r}, not of type {self.info_type}\"\n        self._map[id] = info\n\n    def retrieve(self, id, *, default=None):\n        \"\"\"\n        retrieve stuff to use it in a (usually contentless) item.\n        \"\"\"\n        assert isinstance(id, self.id_type), f\"id is {id!r}, not of type {self.id_type}\"\n        return self._map.get(id, default)\n\n\ndef scandir_keyfunc(dirent):\n    try:\n        return (0, dirent.inode())\n    except OSError as e:\n        # maybe a permission denied error while doing a stat() on the dirent\n        logger.debug(\"scandir_inorder: Unable to stat %s: %s\", dirent.path, e)\n        # order this dirent after all the others lexically by file name\n        # we may not break the whole scandir just because of an exception in one dirent\n        # ignore the exception for now, since another stat will be done later anyways\n        # (or the entry will be skipped by an exclude pattern)\n        return (1, dirent.name)\n\n\ndef scandir_inorder(*, path, fd=None):\n    arg = fd if fd is not None else path\n    return sorted(os.scandir(arg), key=scandir_keyfunc)\n\n\ndef secure_erase(path, *, avoid_collateral_damage):\n    \"\"\"Attempt to erase a file securely by writing random data over it before deleting it.\n\n    If avoid_collateral_damage is True, we only secure erase if the total link count is 1,\n    otherwise we just do a normal \"delete\" (unlink) without first overwriting it with random.\n    This avoids other hard links pointing to same inode as <path> getting damaged, but might be less secure.\n    A typical scenario where this is useful are quick \"hard link copies\" of bigger directories.\n\n    If avoid_collateral_damage is False, we always secure erase.\n    If there are hard links pointing to the same inode as <path>, they will contain random garbage afterwards.\n    \"\"\"\n    path_obj = Path(path)\n    with path_obj.open(\"r+b\") as fd:\n        st = os.stat(fd.fileno())\n        if not (st.st_nlink > 1 and avoid_collateral_damage):\n            fd.write(os.urandom(st.st_size))\n            fd.flush()\n            os.fsync(fd.fileno())\n    path_obj.unlink()\n\n\ndef safe_unlink(path):\n    \"\"\"\n    Safely unlink (delete) *path*.\n\n    If we run out of space while deleting the file, we try truncating it first.\n    BUT we truncate only if path is the only hard link referring to this content.\n\n    Use this when deleting potentially large files when recovering\n    from a VFS error such as ENOSPC. It can help a full file system\n    recover. Refer to the \"File system interaction\" section\n    in legacyrepository.py for further explanations.\n    \"\"\"\n    path_obj = Path(path)\n    try:\n        path_obj.unlink()\n    except OSError as unlink_err:\n        if unlink_err.errno != errno.ENOSPC:\n            # not free space related, give up here.\n            raise\n        # we ran out of space while trying to delete the file.\n        st = path_obj.stat()\n        if st.st_nlink > 1:\n            # rather give up here than cause collateral damage to the other hard link.\n            raise\n        # no other hard link! try to recover free space by truncating this file.\n        try:\n            # Do not create *path* if it does not exist, open for truncation in r+b mode (=O_RDWR|O_BINARY).\n            with open(path, \"r+b\") as fd:\n                fd.truncate()\n        except OSError:\n            # truncate didn't work, so we still have the original unlink issue - give up:\n            raise unlink_err\n        else:\n            # successfully truncated the file, try again deleting it:\n            path_obj.unlink()\n\n\ndef dash_open(path, mode):\n    assert \"+\" not in mode  # the streams are either r or w, but never both\n    if path == \"-\":\n        stream = sys.stdin if \"r\" in mode else sys.stdout\n        return stream.buffer if \"b\" in mode else stream\n    else:\n        return open(path, mode)\n\n\ndef O_(*flags):\n    result = 0\n    for flag in flags:\n        result |= getattr(os, \"O_\" + flag, 0)\n    return result\n\n\nflags_base = O_(\"BINARY\", \"NOCTTY\", \"RDONLY\")\nflags_special = flags_base | O_(\"NOFOLLOW\")  # BLOCK == wait when reading devices or fifos\nflags_special_follow = flags_base  # BLOCK == wait when reading symlinked devices or fifos\nflags_normal = flags_base | O_(\"NONBLOCK\", \"NOFOLLOW\")\nflags_noatime = flags_normal | O_(\"NOATIME\")\nflags_dir = O_(\"DIRECTORY\", \"RDONLY\", \"NOFOLLOW\")\n\n\ndef os_open(*, flags, path=None, parent_fd=None, name=None, noatime=False):\n    \"\"\"\n    Use os.open to open a fs item.\n\n    If parent_fd and name are given, they are preferred and openat will be used,\n    path is not used in this case.\n\n    :param path: full (but not necessarily absolute) path\n    :param parent_fd: open directory file descriptor\n    :param name: name relative to parent_fd\n    :param flags: open flags for os.open() (int)\n    :param noatime: True if access time shall be preserved\n    :return: file descriptor\n    \"\"\"\n    if name and parent_fd is not None:\n        # name is neither None nor empty, parent_fd given.\n        fname = name  # use name relative to parent_fd\n    else:\n        fname, parent_fd = path, None  # just use the path\n    if is_win32 and os.path.isdir(fname):\n        # Directories can not be opened on Windows.\n        return None\n    _flags_normal = flags\n    if noatime:\n        _flags_noatime = _flags_normal | O_(\"NOATIME\")\n        try:\n            # if we have O_NOATIME, this likely will succeed if we are root or owner of file:\n            fd = os.open(fname, _flags_noatime, dir_fd=parent_fd)\n        except PermissionError:\n            if _flags_noatime == _flags_normal:\n                # we do not have O_NOATIME, no need to try again:\n                raise\n            # Was this EPERM due to the O_NOATIME flag? Try again without it:\n            fd = os.open(fname, _flags_normal, dir_fd=parent_fd)\n        except OSError as exc:\n            # O_NOATIME causes EROFS when accessing a volume shadow copy in WSL1\n            from . import workarounds\n\n            if \"retry_erofs\" in workarounds and exc.errno == errno.EROFS and _flags_noatime != _flags_normal:\n                fd = os.open(fname, _flags_normal, dir_fd=parent_fd)\n            else:\n                raise\n    else:\n        fd = os.open(fname, _flags_normal, dir_fd=parent_fd)\n    return fd\n\n\ndef os_stat(*, path=None, parent_fd=None, name=None, follow_symlinks=False):\n    \"\"\"\n    Use os.stat to open a fs item.\n\n    If parent_fd and name are given, they are preferred and statat will be used,\n    path is not used in this case.\n\n    :param path: full (but not necessarily absolute) path\n    :param parent_fd: open directory file descriptor\n    :param name: name relative to parent_fd\n    :return: stat info\n    \"\"\"\n    if name and parent_fd is not None:\n        # name is neither None nor empty, parent_fd given.\n        fname = name  # use name relative to parent_fd\n    else:\n        fname, parent_fd = path, None  # just use the path\n    return os.stat(fname, dir_fd=parent_fd, follow_symlinks=follow_symlinks)\n\n\ndef umount(mountpoint):\n    from . import set_ec\n\n    env = prepare_subprocess_env(system=True)\n    try:\n        rc = subprocess.call([\"fusermount\", \"-u\", mountpoint], env=env)  # nosec B603, B607\n    except FileNotFoundError:\n        rc = subprocess.call([\"umount\", mountpoint], env=env)  # nosec B603, B607\n    set_ec(rc)\n\n\n# below is a slightly modified tempfile.mkstemp that has an additional mode parameter.\n# see https://github.com/borgbackup/borg/issues/6933 and https://github.com/borgbackup/borg/issues/6400\n\nimport os as _os  # NOQA\nimport sys as _sys  # NOQA\nimport errno as _errno  # NOQA\nfrom tempfile import _sanitize_params, _get_candidate_names  # type: ignore[attr-defined] # NOQA\nfrom tempfile import TMP_MAX, _text_openflags, _bin_openflags  # type: ignore[attr-defined] # NOQA\n\n\ndef _mkstemp_inner(dir, pre, suf, flags, output_type, mode=0o600):\n    \"\"\"Code common to mkstemp, TemporaryFile, and NamedTemporaryFile.\"\"\"\n\n    dir = _os.path.abspath(dir)\n    names = _get_candidate_names()\n    if output_type is bytes:\n        names = map(_os.fsencode, names)\n\n    for seq in range(TMP_MAX):\n        name = next(names)\n        file = _os.path.join(dir, pre + name + suf)\n        _sys.audit(\"tempfile.mkstemp\", file)\n        try:\n            fd = _os.open(file, flags, mode)\n        except FileExistsError:\n            continue  # try again\n        except PermissionError:\n            # This exception is thrown when a directory with the chosen name\n            # already exists on windows.\n            if _os.name == \"nt\" and _os.path.isdir(dir) and _os.access(dir, _os.W_OK):\n                continue\n            else:\n                raise\n        return fd, file\n\n    raise FileExistsError(_errno.EEXIST, \"No usable temporary file name found\")\n\n\ndef mkstemp_mode(suffix=None, prefix=None, dir=None, text=False, mode=0o600):\n    \"\"\"User-callable function to create and return a unique temporary\n    file.  The return value is a pair (fd, name) where fd is the\n    file descriptor returned by os.open, and name is the filename.\n    If 'suffix' is not None, the file name will end with that suffix,\n    otherwise there will be no suffix.\n    If 'prefix' is not None, the file name will begin with that prefix,\n    otherwise a default prefix is used.\n    If 'dir' is not None, the file will be created in that directory,\n    otherwise a default directory is used.\n    If 'text' is specified and true, the file is opened in text\n    mode.  Else (the default) the file is opened in binary mode.\n    If any of 'suffix', 'prefix' and 'dir' are not None, they must be the\n    same type.  If they are bytes, the returned name will be bytes; str\n    otherwise.\n    The file is readable and writable only by the creating user ID.\n    If the operating system uses permission bits to indicate whether a\n    file is executable, the file is executable by no one. The file\n    descriptor is not inherited by children of this process.\n    Caller is responsible for deleting the file when done with it.\n    \"\"\"\n\n    prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)\n\n    if text:\n        flags = _text_openflags\n    else:\n        flags = _bin_openflags\n\n    return _mkstemp_inner(dir, prefix, suffix, flags, output_type, mode)\n"
  },
  {
    "path": "src/borg/helpers/lrucache.py",
    "content": "from collections import OrderedDict\nfrom collections.abc import Callable, ItemsView, Iterator, KeysView, MutableMapping, ValuesView\nfrom typing import TypeVar\n\nK = TypeVar(\"K\")\nV = TypeVar(\"V\")\n\n\nclass LRUCache(MutableMapping[K, V]):\n    \"\"\"\n    Mapping which maintains a maximum size by removing the least recently used value.\n    Items are passed to dispose before being removed and setting an item which is\n    already in the cache has to be done using the replace method.\n    \"\"\"\n\n    _cache: OrderedDict[K, V]\n\n    _capacity: int\n\n    _dispose: Callable[[V], None]\n\n    def __init__(self, capacity: int, dispose: Callable[[V], None] = lambda _: None):\n        self._cache = OrderedDict()\n        self._capacity = capacity\n        self._dispose = dispose\n\n    def __setitem__(self, key: K, value: V) -> None:\n        assert (\n            key not in self._cache\n        ), \"Unexpected attempt to replace a cached item without first deleting the old item.\"\n        while len(self._cache) >= self._capacity:\n            self._dispose(self._cache.popitem(last=False)[1])\n        self._cache[key] = value  # add new entry at the end\n\n    def __getitem__(self, key: K) -> V:\n        self._cache.move_to_end(key)  # raise KeyError if not found\n        return self._cache[key]\n\n    def __delitem__(self, key: K) -> None:\n        self._dispose(self._cache.pop(key))\n\n    def __contains__(self, key: object) -> bool:\n        return key in self._cache\n\n    def __len__(self) -> int:\n        return len(self._cache)\n\n    def replace(self, key: K, value: V) -> None:\n        \"\"\"Replace an item that is already present, not disposing it in the process.\"\"\"\n        # this method complements __setitem__ which should be used for the normal use case.\n        assert key in self._cache, \"Unexpected attempt to update a non-existing item.\"\n        self._cache[key] = value\n\n    def clear(self) -> None:\n        for value in self._cache.values():\n            self._dispose(value)\n        self._cache.clear()\n\n    def __iter__(self) -> Iterator[K]:\n        return iter(self._cache)\n\n    def keys(self) -> KeysView[K]:\n        return self._cache.keys()\n\n    def values(self) -> ValuesView[V]:\n        return self._cache.values()\n\n    def items(self) -> ItemsView[K, V]:\n        return self._cache.items()\n"
  },
  {
    "path": "src/borg/helpers/misc.py",
    "content": "import logging\nimport io\nimport os\nimport platform  # python stdlib import - if this fails, check that cwd != src/borg/\nimport sys\nfrom collections import deque\nfrom itertools import islice\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\nfrom . import msgpack\nfrom .. import __version__ as borg_version\nfrom ..constants import ROBJ_FILE_STREAM\n\n\ndef sysinfo():\n    show_sysinfo = os.environ.get(\"BORG_SHOW_SYSINFO\", \"yes\").lower()\n    if show_sysinfo == \"no\":\n        return \"\"\n\n    python_implementation = platform.python_implementation()\n    python_version = platform.python_version()\n    # platform.uname() does a shell call internally to get processor info,\n    # creating #3732 issue, so rather use os.uname().\n    try:\n        uname = os.uname()\n    except AttributeError:\n        uname = None\n    if sys.platform.startswith(\"linux\"):\n        linux_distribution = (\"Unknown Linux\", \"\", \"\")\n    else:\n        linux_distribution = None\n    try:\n        msgpack_version = \".\".join(str(v) for v in msgpack.version)\n    except:  # noqa\n        msgpack_version = \"unknown\"\n    from ..fuse_impl import llfuse, BORG_FUSE_IMPL\n\n    llfuse_name = llfuse.__name__ if llfuse else \"None\"\n    llfuse_version = (\" %s\" % llfuse.__version__) if llfuse else \"\"\n    llfuse_info = f\"{llfuse_name}{llfuse_version} [{BORG_FUSE_IMPL}]\"\n    info = []\n    if uname is not None:\n        info.append(\"Platform: {}\".format(\" \".join(uname)))\n    if linux_distribution is not None:\n        info.append(\"Linux: %s %s %s\" % linux_distribution)\n    info.append(\n        \"Borg: {}  Python: {} {} msgpack: {} fuse: {}\".format(\n            borg_version, python_implementation, python_version, msgpack_version, llfuse_info\n        )\n    )\n    info.append(\"PID: %d  CWD: %s\" % (os.getpid(), os.getcwd()))\n    info.append(\"sys.argv: %r\" % sys.argv)\n    info.append(\"SSH_ORIGINAL_COMMAND: %r\" % os.environ.get(\"SSH_ORIGINAL_COMMAND\"))\n    info.append(\"\")\n    return \"\\n\".join(info)\n\n\ndef log_multi(*msgs, level=logging.INFO, logger=logger):\n    \"\"\"\n    Log multiple lines of text, emitting each line via a separate logging call for cosmetic reasons.\n\n    Each positional argument may be a single or multiple lines (separated by newlines) of text.\n    \"\"\"\n    lines = []\n    for msg in msgs:\n        lines.extend(msg.splitlines())\n    for line in lines:\n        logger.log(level, line)\n\n\nclass ChunkIteratorFileWrapper:\n    \"\"\"File-like wrapper for chunk iterators.\"\"\"\n\n    def __init__(self, chunk_iterator, read_callback=None):\n        \"\"\"\n        *chunk_iterator* should be an iterator yielding bytes. These will be buffered\n        internally as necessary to satisfy .read() calls.\n\n        *read_callback* will be called with one argument, a byte string that has\n        just been read and will subsequently be returned to a caller of .read().\n        The callback can be used to update a progress display.\n        \"\"\"\n        self.chunk_iterator = chunk_iterator\n        self.chunk_offset = 0\n        self.chunk = b\"\"\n        self.exhausted = False\n        self.read_callback = read_callback\n\n    def _refill(self):\n        remaining = len(self.chunk) - self.chunk_offset\n        if not remaining:\n            try:\n                chunk = next(self.chunk_iterator)\n                self.chunk = memoryview(chunk)\n            except StopIteration:\n                self.exhausted = True\n                return 0  # EOF\n            self.chunk_offset = 0\n            remaining = len(self.chunk)\n        return remaining\n\n    def _read(self, nbytes):\n        if not nbytes:\n            return b\"\"\n        remaining = self._refill()\n        will_read = min(remaining, nbytes)\n        self.chunk_offset += will_read\n        return self.chunk[self.chunk_offset - will_read : self.chunk_offset]\n\n    def read(self, nbytes):\n        parts = []\n        while nbytes and not self.exhausted:\n            read_data = self._read(nbytes)\n            nbytes -= len(read_data)\n            parts.append(read_data)\n            if self.read_callback:\n                self.read_callback(read_data)\n        return b\"\".join(parts)\n\n\ndef open_item(archive, item):\n    \"\"\"Return file-like object for archived item (with chunks).\"\"\"\n    chunk_iterator = archive.pipeline.fetch_many(item.chunks, ro_type=ROBJ_FILE_STREAM)\n    return ChunkIteratorFileWrapper(chunk_iterator)\n\n\ndef chunkit(it, size):\n    \"\"\"\n    Chunk an iterator into pieces of the given size.\n\n    >>> list(chunkit('ABCDEFG', 3))\n    [['A', 'B', 'C'], ['D', 'E', 'F'], ['G']]\n    \"\"\"\n    iterable = iter(it)\n    return iter(lambda: list(islice(iterable, size)), [])\n\n\ndef consume(iterator, n=None):\n    \"\"\"Advance the iterator n steps. If n is None, consume it entirely.\"\"\"\n    # Use functions that consume iterators at C speed.\n    if n is None:\n        # feed the entire iterator into a zero-length deque\n        deque(iterator, maxlen=0)\n    else:\n        # advance to the empty slice starting at position n\n        next(islice(iterator, n, n), None)\n\n\nclass ErrorIgnoringTextIOWrapper(io.TextIOWrapper):\n    def read(self, n):\n        if not self.closed:\n            try:\n                return super().read(n)\n            except BrokenPipeError:\n                try:\n                    super().close()\n                except OSError:\n                    pass\n        return \"\"\n\n    def write(self, s):\n        if not self.closed:\n            try:\n                return super().write(s)\n            except BrokenPipeError:\n                try:\n                    super().close()\n                except OSError:\n                    pass\n        return len(s)\n\n\ndef iter_separated(fd, sep=None, read_size=4096):\n    \"\"\"Iterate over chunks of the open file ``fd`` delimited by ``sep``. Does not trim.\"\"\"\n    buf = fd.read(read_size)\n    is_str = isinstance(buf, str)\n    part = \"\" if is_str else b\"\"\n    sep = sep or (\"\\n\" if is_str else b\"\\n\")\n    while len(buf) > 0:\n        part2, *items = buf.split(sep)\n        *full, part = (part + part2, *items)  # type: ignore\n        yield from full\n        buf = fd.read(read_size)\n    # won't yield an empty part if stream ended with `sep`\n    # or if there was no data before EOF\n    if len(part) > 0:  # type: ignore[arg-type]\n        yield part\n"
  },
  {
    "path": "src/borg/helpers/msgpack.py",
    "content": "\"\"\"\nWrapping msgpack\n================\n\nWe wrap ``msgpack`` here as needed to avoid clutter in the calling code.\n\nPacking\n-------\n- use_bin_type = True (used by Borg since Borg 2.0)\n  This is used to generate output according to new msgpack 2.0 spec.\n  This cleanly keeps bytes and str types apart.\n\n- use_bin_type = False (used by Borg < 1.3)\n  This creates output according to the older msgpack spec.\n  BAD: str and bytes were packed into same \"raw\" representation.\n\n- unicode_errors = 'surrogateescape'\n  Backup applications are one of the rare cases where this is necessary.\n  It is needed because Borg also needs to deal with data that does not cleanly encode or decode using UTF-8.\n  There is a lot of problematic data out there (e.g., in filenames), and as a backup tool,\n  we must preserve them as faithfully as possible.\n\nUnpacking\n---------\n- raw = False (used by Borg since Borg 2.0)\n  We already can use this with borg 2.0 due to the type conversion to the desired type in item.py update_internal\n  methods. This type conversion code can be removed in future, when we do not have to deal with data any more\n  that was packed the old way.\n  It will then unpack according to the msgpack 2.0 spec format and directly output bytes or str.\n\n- raw = True (the old way, used by Borg < 1.3)\n\n- unicode_errors = 'surrogateescape' -> see description above (will be used when raw is False).\n\nAs of Borg 2.0, we have fixed most of the `msgpack`` str/bytes issues (#968).\nBorg still needs to read old repositories, archives, keys, etc., so we cannot yet fix it completely.\nFrom now on, Borg only writes new data according to the msgpack 2.0 spec,\nthus we can remove some legacy support in a later Borg release (some places are marked with \"legacy\").\n\nCurrent behavior in msgpack terms\n---------------------------------\n\n- pack with use_bin_type=True (according to the msgpack 2.0 spec)\n- packs str -> raw and bytes -> bin\n- unpack with raw=False (according to the msgpack 2.0 spec, using unicode_errors='surrogateescape')\n- unpacks bin to bytes and raw to str (thus we need to convert to desired type if we want bytes from \"raw\")\n\"\"\"\n\nimport os\n\nfrom .datastruct import StableDict\nfrom ..constants import *  # NOQA\n\nfrom msgpack import Packer as mp_Packer\nfrom msgpack import packb as mp_packb\nfrom msgpack import pack as mp_pack\nfrom msgpack import Unpacker as mp_Unpacker\nfrom msgpack import unpackb as mp_unpackb\nfrom msgpack import unpack as mp_unpack\nfrom msgpack import version as mp_version\n\nfrom msgpack import ExtType, Timestamp\nfrom msgpack import OutOfData\n\n\nversion = mp_version\n\nUSE_BIN_TYPE = True\nRAW = False\nUNICODE_ERRORS = \"surrogateescape\"\n\n\nclass PackException(Exception):\n    \"\"\"Exception during msgpack packing.\"\"\"\n\n\nclass UnpackException(Exception):\n    \"\"\"Exception during msgpack unpacking.\"\"\"\n\n\nclass Packer(mp_Packer):\n    def __init__(\n        self,\n        *,\n        default=None,\n        unicode_errors=UNICODE_ERRORS,\n        use_single_float=False,\n        autoreset=True,\n        use_bin_type=USE_BIN_TYPE,\n        strict_types=False,\n    ):\n        assert unicode_errors == UNICODE_ERRORS\n        super().__init__(\n            default=default,\n            unicode_errors=unicode_errors,\n            use_single_float=use_single_float,\n            autoreset=autoreset,\n            use_bin_type=use_bin_type,\n            strict_types=strict_types,\n        )\n\n    def pack(self, obj):\n        try:\n            return super().pack(obj)\n        except Exception as e:\n            raise PackException(e)\n\n\ndef packb(o, *, use_bin_type=USE_BIN_TYPE, unicode_errors=UNICODE_ERRORS, **kwargs):\n    assert unicode_errors == UNICODE_ERRORS\n    try:\n        return mp_packb(o, use_bin_type=use_bin_type, unicode_errors=unicode_errors, **kwargs)\n    except Exception as e:\n        raise PackException(e)\n\n\ndef pack(o, stream, *, use_bin_type=USE_BIN_TYPE, unicode_errors=UNICODE_ERRORS, **kwargs):\n    assert unicode_errors == UNICODE_ERRORS\n    try:\n        return mp_pack(o, stream, use_bin_type=use_bin_type, unicode_errors=unicode_errors, **kwargs)\n    except Exception as e:\n        raise PackException(e)\n\n\nclass Unpacker(mp_Unpacker):\n    def __init__(\n        self,\n        file_like=None,\n        *,\n        read_size=0,\n        use_list=True,\n        raw=RAW,\n        object_hook=None,\n        object_pairs_hook=None,\n        list_hook=None,\n        unicode_errors=UNICODE_ERRORS,\n        max_buffer_size=0,\n        ext_hook=ExtType,\n        strict_map_key=False,\n    ):\n        assert raw == RAW\n        assert unicode_errors == UNICODE_ERRORS\n        kw = dict(\n            file_like=file_like,\n            read_size=read_size,\n            use_list=use_list,\n            raw=raw,\n            object_hook=object_hook,\n            object_pairs_hook=object_pairs_hook,\n            list_hook=list_hook,\n            unicode_errors=unicode_errors,\n            max_buffer_size=max_buffer_size,\n            ext_hook=ext_hook,\n            strict_map_key=strict_map_key,\n        )\n        super().__init__(**kw)\n\n    def unpack(self):\n        try:\n            return super().unpack()\n        except OutOfData:\n            raise\n        except Exception as e:\n            raise UnpackException(e)\n\n    def __next__(self):\n        try:\n            return super().__next__()\n        except StopIteration:\n            raise\n        except Exception as e:\n            raise UnpackException(e)\n\n    next = __next__\n\n\ndef unpackb(packed, *, raw=RAW, unicode_errors=UNICODE_ERRORS, strict_map_key=False, **kwargs):\n    assert raw == RAW\n    assert unicode_errors == UNICODE_ERRORS\n    try:\n        kw = dict(raw=raw, unicode_errors=unicode_errors, strict_map_key=strict_map_key)\n        kw |= kwargs\n        return mp_unpackb(packed, **kw)\n    except Exception as e:\n        raise UnpackException(e)\n\n\ndef unpack(stream, *, raw=RAW, unicode_errors=UNICODE_ERRORS, strict_map_key=False, **kwargs):\n    assert raw == RAW\n    assert unicode_errors == UNICODE_ERRORS\n    try:\n        kw = dict(raw=raw, unicode_errors=unicode_errors, strict_map_key=strict_map_key)\n        kw |= kwargs\n        return mp_unpack(stream, **kw)\n    except Exception as e:\n        raise UnpackException(e)\n\n\n# msgpacking related utilities -----------------------------------------------\n\n\ndef is_slow_msgpack():\n    import msgpack\n    import msgpack.fallback\n\n    return msgpack.Packer is msgpack.fallback.Packer\n\n\ndef is_supported_msgpack():\n    # DO NOT CHANGE OR REMOVE! See also requirements and comments in pyproject.toml.\n    # This function now also respects the env var BORG_MSGPACK_VERSION_CHECK.\n    # Set BORG_MSGPACK_VERSION_CHECK=no to disable the version check at your own risk.\n    import msgpack\n\n    version_check = os.environ.get(\"BORG_MSGPACK_VERSION_CHECK\", \"yes\").strip().lower()\n    if version_check == \"no\":\n        return True\n\n    if msgpack.version in []:  # < add bad releases here to deny list\n        return False\n    return (1, 0, 3) <= msgpack.version[:3] <= (1, 1, 2)\n\n\ndef get_limited_unpacker(kind):\n    \"\"\"return a limited Unpacker because we should not trust msgpack data received from remote\"\"\"\n    # Note: msgpack >= 0.6.1 auto-computes DoS-safe max values from len(data) for\n    #       unpack(data) or from max_buffer_size for Unpacker(max_buffer_size=N).\n    args = dict(use_list=False, max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE))  # return tuples, not lists\n    if kind in (\"server\", \"client\"):\n        args |= dict(max_buffer_size=0)  # 0 means \"maximum\" here, ~4GiB - needed for store_load/save\n    elif kind in (\"manifest\", \"archive\", \"key\"):\n        args |= dict(use_list=True, object_hook=StableDict)  # default value\n    else:\n        raise ValueError('kind must be \"server\", \"client\", \"manifest\", \"archive\" or \"key\"')\n    return Unpacker(**args)\n\n\ndef int_to_timestamp(ns):\n    assert isinstance(ns, int)\n    return Timestamp.from_unix_nano(ns)\n\n\ndef timestamp_to_int(ts):\n    assert isinstance(ts, Timestamp)\n    return ts.to_unix_nano()\n"
  },
  {
    "path": "src/borg/helpers/nanorst.py",
    "content": "import io\nimport sys\n\nfrom . import is_terminal\n\n\nclass TextPecker:\n    def __init__(self, s):\n        self.str = s\n        self.i = 0\n\n    def read(self, n):\n        self.i += n\n        return self.str[self.i - n : self.i]\n\n    def peek(self, n):\n        if n >= 0:\n            return self.str[self.i : self.i + n]\n        else:\n            return self.str[self.i + n - 1 : self.i - 1]\n\n    def peekline(self):\n        out = \"\"\n        i = self.i\n        while i < len(self.str) and self.str[i] != \"\\n\":\n            out += self.str[i]\n            i += 1\n        return out\n\n    def readline(self):\n        out = self.peekline()\n        self.i += len(out)\n        return out\n\n\ndef process_directive(directive, arguments, out, state_hook):\n    if directive == \"container\" and arguments == \"experimental\":\n        state_hook(\"text\", \"**\", out)\n        out.write(\"++ Experimental ++\")\n        state_hook(\"**\", \"text\", out)\n    else:\n        state_hook(\"text\", \"**\", out)\n        out.write(directive.title())\n        out.write(\":\\n\")\n        state_hook(\"**\", \"text\", out)\n        if arguments:\n            out.write(arguments)\n            out.write(\"\\n\")\n\n\ndef rst_to_text(text, state_hook=None, references=None):\n    \"\"\"\n    Convert rST to a more human-friendly text form.\n\n    This is a very loose conversion. No advanced rST features are supported.\n    The generated output depends directly on the input (for example, the\n    indentation of admonitions).\n    \"\"\"\n    state_hook = state_hook or (lambda old_state, new_state, out: None)\n    references = references or {}\n    state = \"text\"\n    inline_mode = \"replace\"\n    text = TextPecker(text)\n    out = io.StringIO()\n\n    inline_single = (\"*\", \"`\")\n\n    while True:\n        char = text.read(1)\n        if not char:\n            break\n        next = text.peek(1)  # type: str\n\n        if state == \"text\":\n            if char == \"\\\\\" and text.peek(1) in inline_single:\n                continue\n            if text.peek(-1) != \"\\\\\":\n                if char in inline_single and next != char:\n                    state_hook(state, char, out)\n                    state = char\n                    continue\n                if char == next == \"*\":\n                    state_hook(state, \"**\", out)\n                    state = \"**\"\n                    text.read(1)\n                    continue\n                if char == next == \"`\":\n                    state_hook(state, \"``\", out)\n                    state = \"``\"\n                    text.read(1)\n                    continue\n                if text.peek(-1).isspace() and char == \":\" and text.peek(5) == \"ref:`\":\n                    # translate reference\n                    text.read(5)\n                    ref = \"\"\n                    while True:\n                        char = text.peek(1)\n                        if char == \"`\":\n                            text.read(1)\n                            break\n                        if char == \"\\n\":\n                            text.read(1)\n                            continue  # merge line breaks in :ref:`...\\n...`\n                        ref += text.read(1)\n                    try:\n                        out.write(references[ref])\n                    except KeyError:\n                        raise ValueError(\n                            \"Undefined reference in Archiver help: %r — please add reference \"\n                            \"substitution to 'rst_plain_text_references'\" % ref\n                        )\n                    continue\n                if char == \":\" and text.peek(2) == \":\\n\":  # End of line code block\n                    text.read(2)\n                    state_hook(state, \"code-block\", out)\n                    state = \"code-block\"\n                    out.write(\":\\n\")\n                    continue\n            if text.peek(-2) in (\"\\n\\n\", \"\") and char == next == \".\":\n                text.read(2)\n                directive, is_directive, arguments = text.readline().partition(\"::\")\n                text.read(1)\n                if not is_directive:\n                    # partition: if the separator is not in the text, the leftmost output is the entire input\n                    if directive == \"nanorst: inline-fill\":\n                        inline_mode = \"fill\"\n                    elif directive == \"nanorst: inline-replace\":\n                        inline_mode = \"replace\"\n                    continue\n                process_directive(directive, arguments.strip(), out, state_hook)\n                continue\n        if state in inline_single and char == state:\n            state_hook(state, \"text\", out)\n            state = \"text\"\n            if inline_mode == \"fill\":\n                out.write(2 * \" \")\n            continue\n        if state == \"``\" and char == next == \"`\":\n            state_hook(state, \"text\", out)\n            state = \"text\"\n            text.read(1)\n            if inline_mode == \"fill\":\n                out.write(4 * \" \")\n            continue\n        if state == \"**\" and char == next == \"*\":\n            state_hook(state, \"text\", out)\n            state = \"text\"\n            text.read(1)\n            continue\n        if state == \"code-block\" and char == next == \"\\n\" and text.peek(5)[1:] != \"    \":\n            # Foo::\n            #\n            #     *stuff* *code* *ignore .. all markup*\n            #\n            #     More arcane stuff\n            #\n            # Regular text...\n            state_hook(state, \"text\", out)\n            state = \"text\"\n        out.write(char)\n\n    assert state == \"text\", \"Invalid final state %r (This usually indicates unmatched */**)\" % state\n    return out.getvalue()\n\n\nclass RstToTextLazy:\n    def __init__(self, str, state_hook=None, references=None):\n        self.str = str\n        self.state_hook = state_hook\n        self.references = references\n        self._rst = None\n\n    @property\n    def rst(self):\n        if self._rst is None:\n            self._rst = rst_to_text(self.str, self.state_hook, self.references)\n        return self._rst\n\n    def __getattr__(self, item):\n        return getattr(self.rst, item)\n\n    def __str__(self):\n        return self.rst\n\n    def __add__(self, other):\n        return self.rst + other\n\n    def __iter__(self):\n        return iter(self.rst)\n\n    def __contains__(self, item):\n        return item in self.rst\n\n\ndef ansi_escapes(old_state, new_state, out):\n    if old_state == \"text\" and new_state in (\"*\", \"`\", \"``\"):\n        out.write(\"\\033[4m\")\n    if old_state == \"text\" and new_state == \"**\":\n        out.write(\"\\033[1m\")\n    if old_state in (\"*\", \"`\", \"``\", \"**\") and new_state == \"text\":\n        out.write(\"\\033[0m\")\n\n\ndef rst_to_terminal(rst, references=None, destination=sys.stdout):\n    \"\"\"\n    Convert *rst* to a lazy string.\n\n    If *destination* is a file-like object connected to a terminal,\n    enrich the text with suitable ANSI escapes. Otherwise, return plain text.\n    \"\"\"\n    if is_terminal(destination):\n        rst_state_hook = ansi_escapes\n    else:\n        rst_state_hook = None\n    return RstToTextLazy(rst, rst_state_hook, references)\n"
  },
  {
    "path": "src/borg/helpers/parseformat.py",
    "content": "import abc\nimport base64\nimport binascii\nimport hashlib\nimport json\nimport os\nimport os.path\nimport re\nimport shlex\nimport stat\nimport uuid\nfrom pathlib import Path\nfrom typing import ClassVar, Any, TYPE_CHECKING, Literal\nfrom collections import OrderedDict\nfrom datetime import datetime, timezone\nfrom functools import partial\nfrom hashlib import sha256\nfrom string import Formatter\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\nimport yaml\n\nfrom .errors import Error\nfrom .fs import get_keys_dir, make_path_safe, slashify\nfrom .argparsing import Action, ArgumentError, ArgumentTypeError, register_type\nfrom .msgpack import Timestamp\nfrom .time import OutputTimestamp, format_time, safe_timestamp\nfrom .. import __version__ as borg_version\nfrom .. import __version_tuple__ as borg_version_tuple\nfrom ..constants import *  # NOQA\nfrom ..platformflags import is_win32\n\nif TYPE_CHECKING:\n    from ..item import ItemDiff\n\n\ndef octal_int(s):\n    if isinstance(s, int):\n        return s\n    return int(s, 8)\n\n\ndef bin_to_hex(binary):\n    return binascii.hexlify(binary).decode(\"ascii\")\n\n\ndef hex_to_bin(hex, length=None):\n    try:\n        binary = binascii.unhexlify(hex)\n        binary_len = len(binary)\n        if length is not None and binary_len != length:\n            raise ValueError(f\"Expected {length} bytes ({2 * length} hex digits), got {binary_len} bytes.\")\n    except binascii.Error as e:\n        raise ValueError(str(e)) from None\n    return binary\n\n\ndef safe_decode(s, coding=\"utf-8\", errors=\"surrogateescape\"):\n    \"\"\"Decode bytes to str, with round-tripping of \"invalid\" bytes.\"\"\"\n    if s is None:\n        return None\n    return s.decode(coding, errors)\n\n\ndef safe_encode(s, coding=\"utf-8\", errors=\"surrogateescape\"):\n    \"\"\"Encode str to bytes, with round-tripping of \"invalid\" bytes.\"\"\"\n    if s is None:\n        return None\n    return s.encode(coding, errors)\n\n\ndef remove_surrogates(s, errors=\"replace\"):\n    \"\"\"Replace surrogates generated by fsdecode with '?'.\"\"\"\n    return s.encode(\"utf-8\", errors).decode(\"utf-8\")\n\n\ndef binary_to_json(key, value):\n    assert isinstance(key, str)\n    assert isinstance(value, bytes)\n    return {key + \"_b64\": base64.b64encode(value).decode(\"ascii\")}\n\n\ndef text_to_json(key, value):\n    \"\"\"\n    Return a dict made from key/value that can be fed safely into a JSON encoder.\n\n    JSON can only contain pure, valid unicode (but not: unicode with surrogate escapes).\n\n    But sometimes we have to deal with such values and we do it like this:\n    - <key>: value as pure unicode text (surrogate escapes, if any, replaced by ?)\n    - <key>_b64: value as base64 encoded binary representation (only set if value has surrogate-escapes)\n    \"\"\"\n    coding = \"utf-8\"\n    assert isinstance(key, str)\n    assert isinstance(value, str)  # str might contain surrogate escapes\n    data = {}\n    try:\n        value.encode(coding, errors=\"strict\")  # check if pure unicode\n    except UnicodeEncodeError:\n        # value has surrogate escape sequences\n        data[key] = remove_surrogates(value)\n        value_bytes = value.encode(coding, errors=\"surrogateescape\")\n        data |= binary_to_json(key, value_bytes)\n    else:\n        # value is pure unicode\n        data[key] = value\n        # we do not give the b64 representation, not needed\n    return data\n\n\ndef join_cmd(argv, rs=False):\n    cmd = shlex.join(argv)\n    return remove_surrogates(cmd) if rs else cmd\n\n\ndef eval_escapes(s):\n    \"\"\"Evaluate literal escape sequences in a string (e.g. `\\\\n` -> `\\n`).\"\"\"\n    return s.encode(\"ascii\", \"backslashreplace\").decode(\"unicode-escape\")\n\n\ndef decode_dict(d, keys, encoding=\"utf-8\", errors=\"surrogateescape\"):\n    for key in keys:\n        if isinstance(d.get(key), bytes):\n            d[key] = d[key].decode(encoding, errors)\n    return d\n\n\ndef interval(s):\n    \"\"\"Convert a string representing a valid interval to a number of seconds.\"\"\"\n    if isinstance(s, int):\n        return s\n    seconds_in_a_minute = 60\n    seconds_in_an_hour = 60 * seconds_in_a_minute\n    seconds_in_a_day = 24 * seconds_in_an_hour\n    seconds_in_a_week = 7 * seconds_in_a_day\n    seconds_in_a_month = 31 * seconds_in_a_day\n    seconds_in_a_year = 365 * seconds_in_a_day\n    multiplier = dict(\n        y=seconds_in_a_year,\n        m=seconds_in_a_month,\n        w=seconds_in_a_week,\n        d=seconds_in_a_day,\n        H=seconds_in_an_hour,\n        M=seconds_in_a_minute,\n        S=1,\n    )\n\n    if s.endswith(tuple(multiplier.keys())):\n        number = s[:-1]\n        suffix = s[-1]\n    else:\n        raise ArgumentTypeError(f'Unexpected time unit \"{s[-1]}\": choose from {\", \".join(multiplier)}')\n\n    try:\n        seconds = int(number) * multiplier[suffix]\n    except ValueError:\n        seconds = -1\n\n    if seconds <= 0:\n        raise ArgumentTypeError(f'Invalid number \"{number}\": expected positive integer')\n\n    return seconds\n\n\nclass CompressionSpec:\n    def __init__(self, s):\n        if isinstance(s, CompressionSpec):\n            self.__dict__.update(s.__dict__)\n            return\n        values = s.split(\",\")\n        count = len(values)\n        if count < 1:\n            raise ArgumentTypeError(\"not enough arguments\")\n        # --compression algo[,level]\n        self.name = values[0]\n        if self.name in (\"none\", \"lz4\"):\n            return\n        elif self.name in (\"zlib\", \"lzma\", \"zlib_legacy\"):  # zlib_legacy just for testing\n            if count < 2:\n                level = 6  # default compression level in py stdlib\n            elif count == 2:\n                level = int(values[1])\n                if not 0 <= level <= 9:\n                    raise ArgumentTypeError(\"level must be >= 0 and <= 9\")\n            else:\n                raise ArgumentTypeError(\"too many arguments\")\n            self.level = level\n        elif self.name in (\"zstd\",):\n            if count < 2:\n                level = 3  # default compression level in zstd\n            elif count == 2:\n                level = int(values[1])\n                if not 1 <= level <= 22:\n                    raise ArgumentTypeError(\"level must be >= 1 and <= 22\")\n            else:\n                raise ArgumentTypeError(\"too many arguments\")\n            self.level = level\n        elif self.name == \"auto\":\n            if 2 <= count <= 3:\n                compression = \",\".join(values[1:])\n            else:\n                raise ArgumentTypeError(\"bad arguments\")\n            self.inner = CompressionSpec(compression)\n        elif self.name == \"obfuscate\":\n            if 3 <= count <= 5:\n                level = int(values[1])\n                if not ((1 <= level <= 6) or (110 <= level <= 123) or (level == 250)):\n                    raise ArgumentTypeError(\"level must be (inclusively) within 1...6, 110...123 or equal to 250\")\n                self.level = level\n                compression = \",\".join(values[2:])\n            else:\n                raise ArgumentTypeError(\"bad arguments\")\n            self.inner = CompressionSpec(compression)\n        else:\n            raise ArgumentTypeError(\"unsupported compression type\")\n\n    @property\n    def compressor(self):\n        from ..compress import get_compressor\n\n        if self.name in (\"none\", \"lz4\"):\n            return get_compressor(self.name)\n        elif self.name in (\"zlib\", \"lzma\", \"zstd\", \"zlib_legacy\"):\n            return get_compressor(self.name, level=self.level)\n        elif self.name == \"auto\":\n            return get_compressor(self.name, compressor=self.inner.compressor)\n        elif self.name == \"obfuscate\":\n            return get_compressor(self.name, level=self.level, compressor=self.inner.compressor)\n\n    def __str__(self):\n        if self.name in (\"none\", \"lz4\"):\n            return f\"{self.name}\"\n        elif self.name in (\"zlib\", \"lzma\", \"zstd\", \"zlib_legacy\"):\n            return f\"{self.name},{self.level}\"\n        elif self.name == \"auto\":\n            return f\"auto,{self.inner}\"\n        elif self.name == \"obfuscate\":\n            return f\"obfuscate,{self.level},{self.inner}\"\n        else:\n            raise ValueError(f\"unsupported compression type: {self.name}\")\n\n\ndef ChunkerParams(s):\n    if isinstance(s, (list, tuple)):\n        return tuple(s)\n    params = s.strip().split(\",\")\n    count = len(params)\n    if count == 0:\n        raise ArgumentTypeError(\"no chunker params given\")\n    algo = params[0].lower()\n    if algo == CH_FAIL and count == 3:\n        block_size = int(params[1])\n        fail_map = str(params[2])\n        return algo, block_size, fail_map\n    if algo == CH_FIXED and 2 <= count <= 3:  # fixed, block_size[, header_size]\n        block_size = int(params[1])\n        header_size = int(params[2]) if count == 3 else 0\n        if block_size < 64:\n            # we are only disallowing the most extreme cases of abuse here - this does NOT imply\n            # that cutting chunks of the minimum allowed size is efficient concerning storage\n            # or in-memory chunk management.\n            # choose the block (chunk) size wisely: if you have a lot of data and you cut\n            # it into very small chunks, you are asking for trouble!\n            raise ArgumentTypeError(\"block_size must not be less than 64 Bytes\")\n        if block_size > MAX_DATA_SIZE or header_size > MAX_DATA_SIZE:\n            raise ArgumentTypeError(\"block_size and header_size must not exceed MAX_DATA_SIZE [%d]\" % MAX_DATA_SIZE)\n        return algo, block_size, header_size\n    if algo == \"default\" and count == 1:  # default\n        return CHUNKER_PARAMS\n    if algo == CH_BUZHASH64 and count == 5:  # buzhash64, chunk_min, chunk_max, chunk_mask, window_size\n        chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[1:])\n        if not (chunk_min <= chunk_mask <= chunk_max):\n            raise ArgumentTypeError(\"required: chunk_min <= chunk_mask <= chunk_max\")\n        if chunk_min < 6:\n            # see comment in 'fixed' algo check\n            raise ArgumentTypeError(\"min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)\")\n        if chunk_max > 23:\n            raise ArgumentTypeError(\"max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)\")\n        # note that for buzhash64, there is no problem with even window_size.\n        return CH_BUZHASH64, chunk_min, chunk_max, chunk_mask, window_size\n    # this must stay last as it deals with old-style compat mode (no algorithm, 4 params, buzhash):\n    if algo == CH_BUZHASH and count == 5 or count == 4:  # [buzhash, ]chunk_min, chunk_max, chunk_mask, window_size\n        chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[count - 4 :])\n        if not (chunk_min <= chunk_mask <= chunk_max):\n            raise ArgumentTypeError(\"required: chunk_min <= chunk_mask <= chunk_max\")\n        if chunk_min < 6:\n            # see comment in 'fixed' algo check\n            raise ArgumentTypeError(\"min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)\")\n        if chunk_max > 23:\n            raise ArgumentTypeError(\"max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)\")\n        if window_size % 2 == 0:\n            raise ArgumentTypeError(\"window_size must be an uneven (odd) number\")\n        return CH_BUZHASH, chunk_min, chunk_max, chunk_mask, window_size\n    raise ArgumentTypeError(\"invalid chunker params\")\n\n\ndef FilesCacheMode(s):\n    ENTRIES_MAP = dict(ctime=\"c\", mtime=\"m\", size=\"s\", inode=\"i\", rechunk=\"r\", disabled=\"d\")\n    VALID_MODES = (\"cis\", \"ims\", \"cs\", \"ms\", \"cr\", \"mr\", \"d\", \"s\")  # letters in alpha order\n    if s in VALID_MODES:\n        return s\n    entries = set(s.strip().split(\",\"))\n    if not entries <= set(ENTRIES_MAP):\n        raise ArgumentTypeError(\"cache mode must be a comma-separated list of: %s\" % \",\".join(sorted(ENTRIES_MAP)))\n    short_entries = {ENTRIES_MAP[entry] for entry in entries}\n    mode = \"\".join(sorted(short_entries))\n    if mode not in VALID_MODES:\n        raise ArgumentTypeError(\"cache mode short must be one of: %s\" % \",\".join(VALID_MODES))\n    return mode\n\n\ndef partial_format(format, mapping):\n    \"\"\"\n    Apply format.format_map(mapping) while preserving unknown keys\n\n    Does not support attribute access, indexing and ![rsa] conversions\n    \"\"\"\n    for key, value in mapping.items():\n        key = re.escape(key)\n        format = re.sub(\n            rf\"(?<!\\{{)((\\{{{key}\\}})|(\\{{{key}:[^\\}}]*\\}}))\", lambda match: match.group(1).format_map(mapping), format\n        )\n    return format\n\n\nclass DatetimeWrapper:\n    def __init__(self, dt):\n        self.dt = dt\n\n    def __format__(self, format_spec):\n        if format_spec == \"\":\n            format_spec = ISO_FORMAT_NO_USECS\n        return self.dt.__format__(format_spec)\n\n\nclass PlaceholderError(Error):\n    \"\"\"Formatting Error: \"{}\".format({}): {}({})\"\"\"\n\n    exit_mcode = 5\n\n\nclass InvalidPlaceholder(PlaceholderError):\n    \"\"\"Invalid placeholder \"{}\" in string: {}\"\"\"\n\n    exit_mcode = 6\n\n\ndef format_line(format, data):\n    for _, key, _, conversion in Formatter().parse(format):\n        if not key:\n            continue\n        if conversion or key not in data:\n            raise InvalidPlaceholder(key, format)\n    try:\n        return format.format_map(data)\n    except Exception as e:\n        raise PlaceholderError(format, data, e.__class__.__name__, str(e))\n\n\ndef _replace_placeholders(text, overrides={}):\n    \"\"\"Replace placeholders in text with their values.\"\"\"\n    from ..platform import fqdn, hostname, getosusername\n\n    current_time = datetime.now(timezone.utc)\n    data = {\n        \"pid\": os.getpid(),\n        \"fqdn\": fqdn,\n        \"reverse-fqdn\": \".\".join(reversed(fqdn.split(\".\"))),\n        \"hostname\": hostname,\n        \"now\": DatetimeWrapper(current_time.astimezone()),\n        \"utcnow\": DatetimeWrapper(current_time),\n        \"unixtime\": int(current_time.timestamp()),\n        \"user\": getosusername(),\n        \"uuid4\": str(uuid.uuid4()),\n        \"borgversion\": borg_version,\n        \"borgmajor\": \"%d\" % borg_version_tuple[:1],\n        \"borgminor\": \"%d.%d\" % borg_version_tuple[:2],\n        \"borgpatch\": \"%d.%d.%d\" % borg_version_tuple[:3],\n        **overrides,\n    }\n    return format_line(text, data)\n\n\nclass PlaceholderReplacer:\n    def __init__(self):\n        self.reset()\n\n    def override(self, key, value):\n        self.overrides[key] = value\n\n    def reset(self):\n        self.overrides = {}\n\n    def __call__(self, text, overrides=None):\n        ovr = self.overrides | (overrides or {})\n        return _replace_placeholders(text, overrides=ovr)\n\n\nreplace_placeholders = PlaceholderReplacer()\n\n\ndef PathSpec(text):\n    if not text:\n        raise ArgumentTypeError(\"Empty strings are not accepted as paths.\")\n    return text\n\n\ndef FilesystemPathSpec(text):\n    if not text:\n        raise ArgumentTypeError(\"Empty strings are not accepted as paths.\")\n    return slashify(text)\n\n\ndef SortBySpec(text):\n    from ..manifest import AI_HUMAN_SORT_KEYS\n\n    for sort_key in text.split(\",\"):\n        if sort_key not in AI_HUMAN_SORT_KEYS and sort_key != \"ts\":  # idempotency: do not reject ts\n            raise ArgumentTypeError(\"Invalid sort key: %s\" % sort_key)\n    return text.replace(\"timestamp\", \"ts\").replace(\"archive\", \"name\")\n\n\ndef format_file_size(v, precision=2, sign=False, iec=False):\n    \"\"\"Format file size into a human friendly format\"\"\"\n    fn = sizeof_fmt_iec if iec else sizeof_fmt_decimal\n    return fn(v, suffix=\"B\", sep=\" \", precision=precision, sign=sign)\n\n\nclass FileSize(int):\n    def __new__(cls, value, iec=False):\n        obj = int.__new__(cls, value)\n        obj.iec = iec\n        return obj\n\n    def __format__(self, format_spec):\n        return format_file_size(int(self), iec=self.iec).__format__(format_spec)\n\n\ndef parse_file_size(s):\n    \"\"\"Return int from file size (1234, 55G, 1.7T).\"\"\"\n    if isinstance(s, int):\n        return s\n    if not s:\n        return int(s)  # will raise\n    s = s.upper()\n    suffix = s[-1]\n    power = 1000\n    try:\n        factor = {\"K\": power, \"M\": power**2, \"G\": power**3, \"T\": power**4, \"P\": power**5}[suffix]\n        s = s[:-1]\n    except KeyError:\n        factor = 1\n    return int(float(s) * factor)\n\n\ndef sizeof_fmt(num, suffix=\"B\", units=None, power=None, sep=\"\", precision=2, sign=False):\n    sign = \"+\" if sign and num > 0 else \"\"\n    fmt = \"{0:{1}.{2}f}{3}{4}{5}\"\n    prec = 0\n    for unit in units[:-1]:\n        if abs(round(num, precision)) < power:\n            break\n        num /= float(power)\n        prec = precision\n    else:\n        unit = units[-1]\n    return fmt.format(num, sign, prec, sep, unit, suffix)\n\n\ndef sizeof_fmt_iec(num, suffix=\"B\", sep=\"\", precision=2, sign=False):\n    return sizeof_fmt(\n        num,\n        suffix=suffix,\n        sep=sep,\n        precision=precision,\n        sign=sign,\n        units=[\"\", \"Ki\", \"Mi\", \"Gi\", \"Ti\", \"Pi\", \"Ei\", \"Zi\", \"Yi\"],\n        power=1024,\n    )\n\n\ndef sizeof_fmt_decimal(num, suffix=\"B\", sep=\"\", precision=2, sign=False):\n    return sizeof_fmt(\n        num,\n        suffix=suffix,\n        sep=sep,\n        precision=precision,\n        sign=sign,\n        units=[\"\", \"k\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\"],\n        power=1000,\n    )\n\n\ndef format_archive(archive):\n    return \"%-36s %s [%s]\" % (archive.name, format_time(archive.ts), bin_to_hex(archive.id))\n\n\ndef parse_stringified_list(s):\n    items = re.split(\" *, *\", s)\n    return [item for item in items if item != \"\"]\n\n\nclass Location:\n    \"\"\"Object representing a repository location\"\"\"\n\n    # user@ (optional)\n    # user must not contain \"@\", \":\" or \"/\".\n    # Quoting adduser error message:\n    # \"To avoid problems, the username should consist only of letters, digits,\n    # underscores, periods, at signs and dashes, and not start with a dash\n    # (as defined by IEEE Std 1003.1-2001).\"\n    # We use \"@\" as separator between username and hostname, so we must\n    # disallow it within the pure username part.\n    optional_user_re = r\"(?:(?P<user>[^@:/]+)@)?\"\n\n    # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)\n    host_re = r\"\"\"\n        (?P<host>(\n            (?!\\[)[^:/]+(?<!\\])     # hostname or v4 addr, not containing : or / (does not match v6 addr: no brackets!)\n            |\n            \\[[0-9a-fA-F:.]+\\])     # ipv6 address in brackets\n        )\n    \"\"\"\n\n    # :port (optional)\n    optional_port_re = r\"(?::(?P<port>\\d+))?\"\n\n    # path may contain any chars. to avoid ambiguities with other regexes,\n    # it must not start with \"//\" nor with \"scheme://\" nor with \"rclone:\".\n    local_path_re = r\"\"\"\n        (?!(//|(ssh|socket|sftp|file)://|(rclone|s3|b2):))\n        (?P<path>.+)\n    \"\"\"\n\n    # abs_path must start with a slash (or drive letter on Windows).\n    abs_path_re = r\"(?P<path>[A-Za-z]:/.+)\" if is_win32 else r\"(?P<path>/.+)\"\n\n    # path may or may not start with a slash.\n    abs_or_rel_path_re = r\"(?P<path>.+)\"\n\n    # regexes for misc. kinds of supported location specifiers:\n    ssh_or_sftp_re = re.compile(\n        r\"(?P<proto>(ssh|sftp))://\"\n        + optional_user_re\n        + host_re\n        + optional_port_re\n        + r\"/\"  # this is the separator, not part of the path!\n        + abs_or_rel_path_re,\n        re.VERBOSE,\n    )\n\n    # BorgStore REST server\n    # (http|https)://user:pass@host:port/\n    http_re = re.compile(\n        r\"(?P<proto>http|https)://\"\n        + r\"((?P<user>[^:@]+):(?P<pass>[^@]+)@)?\"\n        + host_re\n        + optional_port_re\n        + r\"(?P<path>/)\",\n        re.VERBOSE,\n    )\n\n    # (s3|b2):[(profile|(access_key_id:access_key_secret))@][scheme://hostname[:port]]/bucket/path\n    s3_re = re.compile(\n        r\"\"\"\n        (?P<s3type>(s3|b2)):\n        ((\n            (?P<profile>[^@:]+)  # profile (no colons allowed)\n            |\n            (?P<access_key_id>[^:@]+):(?P<access_key_secret>[^@]+)  # access key and secret\n        )@)?  # optional authentication\n        (\n            [^:/]+://  # scheme (often https)\n            (?P<hostname>[^:/]+)\n            (:(?P<port>\\d+))?\n        )?  # optional endpoint\n        /\n        (?P<bucket>[^/]+)/  # bucket name\n        (?P<path>.+)  # path\n    \"\"\",\n        re.VERBOSE,\n    )\n\n    rclone_re = re.compile(r\"(?P<proto>rclone):(?P<path>(.*))\", re.VERBOSE)\n\n    sl = \"/\" if is_win32 else \"\"\n    file_or_socket_re = re.compile(r\"(?P<proto>(file|socket))://\" + sl + abs_path_re, re.VERBOSE)\n\n    local_re = re.compile(local_path_re, re.VERBOSE)\n\n    def __init__(self, text=\"\", overrides={}, other=False):\n        if isinstance(text, Location):\n            self.__dict__.update(text.__dict__)\n            return\n        self.repo_env_var = \"BORG_OTHER_REPO\" if other else \"BORG_REPO\"\n        self.valid = False\n        self.proto = None\n        self.user = None\n        self._pass = None\n        self._host = None\n        self.port = None\n        self.path = None\n        self.raw = None\n        self.processed = None\n        self.parse(text, overrides)\n\n    def parse(self, text, overrides={}):\n        if not text:\n            # we did not get a text to parse, so we try to fetch from the environment\n            text = os.environ.get(self.repo_env_var)\n            if not text:  # None or \"\"\n                return\n\n        self.raw = text  # as given by user, might contain placeholders\n        self.processed = replace_placeholders(self.raw, overrides)  # after placeholder replacement\n        valid = self._parse(self.processed)\n        if valid:\n            self.valid = True\n        else:\n            raise ValueError('Invalid location format: \"%s\"' % self.processed)\n\n    def _parse(self, text):\n        m = self.ssh_or_sftp_re.match(text)\n        if m:\n            self.proto = m.group(\"proto\")\n            self.user = m.group(\"user\")\n            self._host = m.group(\"host\")\n            self.port = m.group(\"port\") and int(m.group(\"port\")) or None\n            self.path = os.path.normpath(m.group(\"path\"))\n            return True\n        m = self.http_re.match(text)\n        if m:\n            self.proto = m.group(\"proto\")\n            self.user = m.group(\"user\")\n            self._pass = True if m.group(\"pass\") else False\n            self._host = m.group(\"host\")\n            self.port = m.group(\"port\") and int(m.group(\"port\")) or None\n            self.path = m.group(\"path\")\n            return True\n        m = self.rclone_re.match(text)\n        if m:\n            self.proto = m.group(\"proto\")\n            self.path = m.group(\"path\")\n            return True\n        m = self.file_or_socket_re.match(text)\n        if m:\n            self.proto = m.group(\"proto\")\n            self.path = os.path.normpath(m.group(\"path\"))\n            return True\n        m = self.s3_re.match(text)\n        if m:\n            self.proto = m.group(\"s3type\")\n            self.user = m.group(\"profile\") if m.group(\"profile\") else m.group(\"access_key_id\")\n            self._pass = True if m.group(\"access_key_secret\") else False\n            self._host = m.group(\"hostname\")\n            self.port = m.group(\"port\") and int(m.group(\"port\")) or None\n            self.path = m.group(\"bucket\") + \"/\" + m.group(\"path\")\n            return True\n        m = self.local_re.match(text)\n        if m:\n            self.proto = \"file\"\n            path = m.group(\"path\")\n            self.path = slashify(os.path.abspath(path)) if is_win32 else os.path.abspath(path)\n            return True\n        return False\n\n    def __str__(self):\n        items = [\n            \"proto=%r\" % self.proto,\n            \"user=%r\" % self.user,\n            \"pass=%r\" % (\"REDACTED\" if self._pass else None),\n            \"host=%r\" % self.host,\n            \"port=%r\" % self.port,\n            \"path=%r\" % self.path,\n        ]\n        return \", \".join(items)\n\n    def to_key_filename(self):\n        name = re.sub(r\"[^\\w]\", \"_\", self.path.rstrip(\"/\"))\n        if self.proto not in (\"file\", \"socket\", \"rclone\"):\n            name = re.sub(r\"[^\\w]\", \"_\", self.host) + \"__\" + name\n        if len(name) > 120:\n            # Limit file names to some reasonable length. Most file systems\n            # limit them to 255 [unit of choice]; due to variations in unicode\n            # handling we truncate to 100 *characters*.\n            name = name[:120]\n        return os.path.join(get_keys_dir(), name)\n\n    def __repr__(self):\n        return \"Location(%s)\" % self\n\n    @property\n    def host(self):\n        # strip square brackets used for IPv6 addrs\n        if self._host is not None:\n            return self._host.lstrip(\"[\").rstrip(\"]\")\n\n    def canonical_path(self):\n        if self.proto in (\"file\", \"socket\"):\n            return self.path\n        if self.proto == \"rclone\":\n            return f\"{self.proto}:{self.path}\"\n        if self.proto in (\"sftp\", \"ssh\", \"s3\", \"b2\", \"http\", \"https\"):\n            return (\n                f\"{self.proto}://\"\n                f\"{(self.user + '@') if self.user else ''}\"\n                f\"{self._host if self._host else ''}\"\n                f\"{self.port if self.port else ''}/\"\n                f\"{self.path}\"\n            )\n        raise NotImplementedError(self.proto)\n\n    def with_timestamp(self, timestamp):\n        # note: this only affects the repository URL/path, not the archive name!\n        return Location(\n            self.raw,\n            overrides={\n                \"now\": DatetimeWrapper(timestamp),\n                \"utcnow\": DatetimeWrapper(timestamp.astimezone(timezone.utc)),\n            },\n        )\n\n\ndef location_validator(proto=None, other=False):\n    def validator(text):\n        try:\n            loc = Location(text, other=other)\n        except ValueError as err:\n            raise ArgumentTypeError(str(err)) from None\n        if proto is not None and loc.proto != proto:\n            if proto == \"file\":\n                raise ArgumentTypeError('\"%s\": Repository must be local' % text)\n            else:\n                raise ArgumentTypeError('\"%s\": Repository must be remote' % text)\n        return loc\n\n    return validator\n\n\n# Register types with jsonargparse so they can be represented in config files\n# (e.g. for --print_config). Two things are needed:\n# 1. A YAML representer so yaml.safe_dump can serialize Location objects to strings.\n# 2. A jsonargparse register_type so it knows how to deserialize strings back to Location.\n\nyaml.SafeDumper.add_representer(Location, lambda dumper, loc: dumper.represent_str(loc.raw or \"\"))\nregister_type(Location, serializer=lambda loc: loc.raw or \"\")\n\nyaml.SafeDumper.add_representer(CompressionSpec, lambda dumper, cs: dumper.represent_str(str(cs)))\nregister_type(CompressionSpec)\n\n\ndef relative_time_marker_validator(text: str):\n    time_marker_regex = r\"^\\d+[ymwdHMS]$\"\n    match = re.compile(time_marker_regex).search(text)\n    if not match:\n        raise ArgumentTypeError(f\"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S\")\n    else:\n        return text\n\n\ndef text_validator(*, name, max_length, min_length=0, invalid_ctrl_chars=\"\\0\", invalid_chars=\"\", no_blanks=False):\n    def validator(text):\n        assert isinstance(text, str)\n        if len(text) < min_length:\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [length < {min_length}]')\n        if len(text) > max_length:\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [length > {max_length}]')\n        if invalid_ctrl_chars and re.search(f\"[{re.escape(invalid_ctrl_chars)}]\", text):\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [invalid control chars detected]')\n        if invalid_chars and re.search(f\"[{re.escape(invalid_chars)}]\", text):\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [invalid chars detected matching \"{invalid_chars}\"]')\n        if no_blanks and (text.startswith(\" \") or text.endswith(\" \")):\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [leading or trailing blanks detected]')\n        try:\n            text.encode(\"utf-8\", errors=\"strict\")\n        except UnicodeEncodeError:\n            # looks like text contains surrogate-escapes\n            raise ArgumentTypeError(f'Invalid {name}: \"{text}\" [contains non-unicode characters]')\n        return text\n\n    return validator\n\n\ncomment_validator = text_validator(name=\"comment\", max_length=10000)\nvalidate_tag_text = text_validator(name=\"tag\", min_length=1, max_length=10, invalid_chars=\" ,$\")\n\n\ndef tag_validator(text):\n    validated_text = validate_tag_text(text)\n    if validated_text.startswith(\"@\") and validated_text not in SPECIAL_TAGS:\n        raise ArgumentTypeError(\"Unknown special tags given.\")\n    return validated_text\n\n\ndef archivename_validator(text):\n    # we make sure that the archive name can be used as directory name (for borg mount)\n    MAX_PATH = 260  # Windows default. Since Win10, there is a registry setting LongPathsEnabled to get more.\n    MAX_DIRNAME = MAX_PATH - len(\"12345678.123\")\n    SAFETY_MARGIN = 48  # borgfs path: mountpoint / archivename / dir / dir / ... / file\n    MAX_ARCHIVENAME = MAX_DIRNAME - SAFETY_MARGIN\n    invalid_ctrl_chars = \"\".join(chr(i) for i in range(32))\n    # note: \":\" is also an invalid path char on windows, but we can not blacklist it,\n    # because e.g. our {now} placeholder creates ISO-8601 like output like 2022-12-10T20:47:42 .\n    invalid_chars = r\"/\" + r\"\\\"<|>?*\"  # posix + windows\n    validate_text = text_validator(\n        name=\"archive name\",\n        min_length=1,\n        max_length=MAX_ARCHIVENAME,\n        invalid_ctrl_chars=invalid_ctrl_chars,\n        invalid_chars=invalid_chars,\n        no_blanks=True,\n    )\n    return validate_text(text)\n\n\nclass BaseFormatter(metaclass=abc.ABCMeta):\n    format: str\n    static_data: dict[str, Any]\n    FIXED_KEYS: ClassVar[dict[str, str]] = {\n        # Formatting aids\n        \"LF\": \"\\n\",\n        \"SPACE\": \" \",\n        \"TAB\": \"\\t\",\n        \"CR\": \"\\r\",\n        \"NUL\": \"\\0\",\n        \"NEWLINE\": \"\\n\",\n        \"NL\": \"\\n\",  # \\n is automatically converted to os.linesep on write\n    }\n    KEY_DESCRIPTIONS: ClassVar[dict[str, str]] = {\n        \"NEWLINE\": \"OS dependent line separator\",\n        \"NL\": \"alias of NEWLINE\",\n        \"NUL\": \"NUL character for creating print0 / xargs -0 like output\",\n        \"SPACE\": \"space character\",\n        \"TAB\": \"tab character\",\n        \"CR\": \"carriage return character\",\n        \"LF\": \"line feed character\",\n    }\n    KEY_GROUPS: ClassVar[tuple[tuple[str, ...], ...]] = ((\"NEWLINE\", \"NL\", \"NUL\", \"SPACE\", \"TAB\", \"CR\", \"LF\"),)\n\n    def __init__(self, format: str, static: dict[str, Any]) -> None:\n        self.format = partial_format(format, static)\n        self.static_data = static\n\n    @abc.abstractmethod\n    def get_item_data(self, item, jsonline=False) -> dict:\n        raise NotImplementedError\n\n    def format_item(self, item, jsonline=False, sort=False):\n        data = self.get_item_data(item, jsonline)\n        return (\n            f\"{json.dumps(data, cls=BorgJsonEncoder, sort_keys=sort)}\\n\" if jsonline else self.format.format_map(data)\n        )\n\n    @classmethod\n    def keys_help(cls):\n        help = []\n        keys: set[str] = set()\n        keys.update(cls.KEY_DESCRIPTIONS.keys())\n        keys.update(key for group in cls.KEY_GROUPS for key in group)\n\n        for group in cls.KEY_GROUPS:\n            for key in group:\n                keys.remove(key)\n                text = \"- \" + key\n                if key in cls.KEY_DESCRIPTIONS:\n                    text += \": \" + cls.KEY_DESCRIPTIONS[key]\n                help.append(text)\n            help.append(\"\")\n        assert not keys, str(keys)\n        return \"\\n\".join(help)\n\n\nclass ArchiveFormatter(BaseFormatter):\n    KEY_DESCRIPTIONS = {\n        \"archive\": \"archive name\",\n        \"name\": 'alias of \"archive\"',\n        \"comment\": \"archive comment\",\n        \"tags\": \"archive tags\",\n        \"time\": \"nominal time of the archive\",\n        \"start\": \"start time of the archive operation\",\n        \"end\": \"end time of the archive operation\",\n        \"command_line\": \"command line which was used to create the archive\",\n        \"id\": \"internal ID of the archive\",\n        \"hostname\": \"hostname of host on which this archive was created\",\n        \"username\": \"username of user who created this archive\",\n        \"size\": \"size of this archive (data plus metadata, not considering compression and deduplication)\",\n        \"nfiles\": \"count of files in this archive\",\n    }\n    KEY_GROUPS = (\n        (\"archive\", \"name\", \"comment\", \"id\", \"tags\"),\n        (\"time\", \"start\", \"end\", \"command_line\"),\n        (\"hostname\", \"username\"),\n        (\"size\", \"nfiles\"),\n    )\n\n    def __init__(self, format, repository, manifest, key, *, iec=False, deleted=False):\n        static_data = {} | self.FIXED_KEYS  # here could be stuff on repo level, above archive level\n        super().__init__(format, static_data)\n        self.repository = repository\n        self.manifest = manifest\n        self.key = key\n        self.name = None\n        self.id = None\n        self._archive = None\n        self.deleted = deleted  # True if we want to deal with deleted archives.\n        self.iec = iec\n        self.format_keys = {f[1] for f in Formatter().parse(format)}\n        self.call_keys = {\n            \"hostname\": partial(self.get_meta, \"hostname\", \"\"),\n            \"username\": partial(self.get_meta, \"username\", \"\"),\n            \"comment\": partial(self.get_meta, \"comment\", \"\"),\n            \"command_line\": partial(self.get_meta, \"command_line\", \"\"),\n            \"size\": partial(self.get_meta, \"size\", 0),\n            \"nfiles\": partial(self.get_meta, \"nfiles\", 0),\n            \"start\": self.get_ts_start,\n            \"end\": self.get_ts_end,\n            \"tags\": self.get_tags,\n        }\n        self.used_call_keys = set(self.call_keys) & self.format_keys\n\n    def get_item_data(self, archive_info, jsonline=False):\n        self.name = archive_info.name\n        self.id = archive_info.id\n        item_data = {}\n        item_data |= {} if jsonline else self.static_data\n        item_data |= {\n            \"name\": archive_info.name,\n            \"archive\": archive_info.name,\n            \"id\": bin_to_hex(archive_info.id),\n            \"time\": self.format_time(archive_info.ts),\n        }\n        for key in self.used_call_keys:\n            item_data[key] = self.call_keys[key]()\n\n        # Note: name and comment are validated, should never contain surrogate escapes.\n        # But unsure whether hostname, username, command_line could contain surrogate escapes, play safe:\n        for key in \"hostname\", \"username\", \"command_line\":\n            if key in item_data:\n                item_data |= text_to_json(key, item_data[key])\n        return item_data\n\n    @property\n    def archive(self):\n        \"\"\"lazy load / update loaded archive\"\"\"\n        if self._archive is None or self._archive.id != self.id:\n            from ..archive import Archive\n\n            self._archive = Archive(self.manifest, self.id, iec=self.iec, deleted=self.deleted)\n        return self._archive\n\n    def get_meta(self, key, default=None):\n        return self.archive.metadata.get(key, default)\n\n    def get_ts_start(self):\n        return self.format_time(self.archive.ts_start)\n\n    def get_ts_end(self):\n        return self.format_time(self.archive.ts_end)\n\n    def format_time(self, ts):\n        return OutputTimestamp(ts)\n\n    def get_tags(self):\n        return \",\".join(sorted(self.archive.tags))\n\n\nclass ItemFormatter(BaseFormatter):\n    # we provide the hash algos from python stdlib (except shake_*) and additionally xxh64.\n    # shake_* is not provided because it uses an incompatible .digest() method to support variable length.\n    hash_algorithms = set(hashlib.algorithms_guaranteed).union({\"xxh64\"}).difference({\"shake_128\", \"shake_256\"})\n    KEY_DESCRIPTIONS = {\n        \"type\": \"file type (file, dir, symlink, ...)\",\n        \"mode\": \"file mode (as in stat)\",\n        \"uid\": \"user id of file owner\",\n        \"gid\": \"group id of file owner\",\n        \"user\": \"user name of file owner\",\n        \"group\": \"group name of file owner\",\n        \"path\": \"file path\",\n        \"target\": \"link target for symlinks\",\n        \"hlid\": \"hard link identity (same if hardlinking same fs object)\",\n        \"inode\": \"inode number\",\n        \"flags\": \"file flags\",\n        \"extra\": 'prepends {target} with \" -> \" for soft links and \" link to \" for hard links',\n        \"size\": \"file size\",\n        \"num_chunks\": \"number of chunks in this file\",\n        \"mtime\": \"file modification time\",\n        \"ctime\": \"file change time\",\n        \"atime\": \"file access time\",\n        \"isomtime\": \"file modification time (ISO 8601 format)\",\n        \"isoctime\": \"file change time (ISO 8601 format)\",\n        \"isoatime\": \"file access time (ISO 8601 format)\",\n        \"xxh64\": \"XXH64 checksum of this file (note: this is NOT a cryptographic hash!)\",\n        \"fingerprint\": \"Fingerprint of the file content (may have false negatives), format: H(conditions)-H(chunk_ids)\",\n        \"archiveid\": \"internal ID of the archive\",\n        \"archivename\": \"name of the archive\",\n    }\n    KEY_GROUPS = (\n        (\"type\", \"mode\", \"uid\", \"gid\", \"user\", \"group\", \"path\", \"target\", \"hlid\", \"inode\", \"flags\"),\n        (\"size\", \"num_chunks\"),\n        (\"mtime\", \"ctime\", \"atime\", \"isomtime\", \"isoctime\", \"isoatime\"),\n        tuple([\"fingerprint\"] + sorted(hash_algorithms)),\n        (\"archiveid\", \"archivename\", \"extra\"),\n    )\n\n    KEYS_REQUIRING_CACHE = ()\n\n    @classmethod\n    def format_needs_cache(cls, format):\n        format_keys = {f[1] for f in Formatter().parse(format)}\n        return any(key in cls.KEYS_REQUIRING_CACHE for key in format_keys)\n\n    def __init__(self, archive, format):\n        from xxhash import xxh64\n\n        static_data = {\"archivename\": archive.name, \"archiveid\": archive.fpr} | self.FIXED_KEYS\n        super().__init__(format, static_data)\n        self.xxh64 = xxh64\n        self.archive = archive\n        # track which keys were requested in the format string\n        self.format_keys = {f[1] for f in Formatter().parse(format)}\n\n        # we want a hash over the conditions that influence the chunk ID list for a given file content:\n        # - the id algorithm and key\n        # - the chunker seed (if any - buzhash64 derives seed from id_key)\n        # - the chunker params\n        key = archive.key\n        conditions = f\"{key.TYPE_STR!r}{key.id_key!r}{key.chunk_seed!r}{archive.metadata.get('chunker_params')!r}\"\n        self.conditions_hash = sha256(conditions.encode()).hexdigest()\n\n        self.call_keys = {\n            \"size\": self.calculate_size,\n            \"num_chunks\": self.calculate_num_chunks,\n            \"isomtime\": partial(self.format_iso_time, \"mtime\"),\n            \"isoctime\": partial(self.format_iso_time, \"ctime\"),\n            \"isoatime\": partial(self.format_iso_time, \"atime\"),\n            \"mtime\": partial(self.format_time, \"mtime\"),\n            \"ctime\": partial(self.format_time, \"ctime\"),\n            \"atime\": partial(self.format_time, \"atime\"),\n            \"fingerprint\": self.calculate_fingerprint,\n        }\n        for hash_function in self.hash_algorithms:\n            self.call_keys[hash_function] = partial(self.hash_item, hash_function)\n        self.used_call_keys = set(self.call_keys) & self.format_keys\n\n    def get_item_data(self, item, jsonline=False):\n        item_data = {}\n        item_data |= {} if jsonline else self.static_data\n\n        item_data |= text_to_json(\"path\", item.path)\n        target = item.get(\"target\", \"\")\n        item_data |= text_to_json(\"target\", target)\n        if not jsonline:\n            item_data[\"extra\"] = \"\" if not target else f\" -> {item_data['target']}\"\n\n        hlid = item.get(\"hlid\")\n        hlid = bin_to_hex(hlid) if hlid else \"\"\n        item_data[\"hlid\"] = hlid\n\n        mode = stat.filemode(item.mode)\n        item_type = mode[0]\n        item_data[\"type\"] = item_type\n        item_data[\"mode\"] = mode\n\n        item_data[\"uid\"] = item.get(\"uid\")  # int or None\n        item_data[\"gid\"] = item.get(\"gid\")  # int or None\n        item_data |= text_to_json(\"user\", item.get(\"user\", str(item_data[\"uid\"])))\n        item_data |= text_to_json(\"group\", item.get(\"group\", str(item_data[\"gid\"])))\n\n        item_data[\"flags\"] = item.get(\"bsdflags\")  # int if flags known, else (if flags unknown) None\n        # inode number from source filesystem (may be absent on some platforms)\n        item_data[\"inode\"] = item.get(\"inode\")\n        for key in self.used_call_keys:\n            item_data[key] = self.call_keys[key](item)\n        # When producing JSON lines, include selected static archive-level keys if they were\n        # requested via --format. This mirrors text output behavior and fixes #9095.\n        if jsonline:\n            # Include selected static archive-level keys when requested via --format.\n            # Keep implementation style aligned with 1.4-maint.\n            for k in (\"archivename\", \"archiveid\"):\n                if k in self.format_keys:\n                    item_data[k] = self.static_data[k]\n        return item_data\n\n    def calculate_num_chunks(self, item):\n        return len(item.get(\"chunks\", []))\n\n    def calculate_size(self, item):\n        # note: does not support hard link slaves, they will be size 0\n        return item.get_size()\n\n    def calculate_fingerprint(self, item):\n        # calculate a very fast file contents fingerprint\n        chunks = item.get(\"chunks\")\n        if chunks is None:\n            return \"\"\n        chunks_hash = sha256(b\"\".join(c.id for c in chunks)).hexdigest()\n        # we do not encounter many different conditions hashes, so the collision probability is low.\n        # thus, we can keep it short and only return 64 bits from the conditions hash.\n        return f\"{self.conditions_hash[:16]}-{chunks_hash}\"\n\n    def hash_item(self, hash_function, item):\n        if \"chunks\" not in item:\n            return \"\"\n        if hash_function == \"xxh64\":\n            hash = self.xxh64()\n        elif hash_function in self.hash_algorithms:\n            hash = hashlib.new(hash_function)\n        for data in self.archive.pipeline.fetch_many(item.chunks, ro_type=ROBJ_FILE_STREAM):\n            hash.update(data)\n        return hash.hexdigest()\n\n    def format_time(self, key, item):\n        return OutputTimestamp(safe_timestamp(item.get(key) or item.mtime))\n\n    def format_iso_time(self, key, item):\n        return self.format_time(key, item).isoformat()\n\n\nclass DiffFormatter(BaseFormatter):\n    KEY_DESCRIPTIONS = {\n        \"path\": \"archived file path\",\n        \"change\": \"all available changes\",\n        \"content\": \"file content change\",\n        \"mode\": \"file mode change\",\n        \"type\": \"file type change\",\n        \"owner\": \"file owner (user/group) change\",\n        \"user\": \"file user change\",\n        \"group\": \"file group change\",\n        \"link\": \"file link change\",\n        \"directory\": \"file directory change\",\n        \"blkdev\": \"file block device change\",\n        \"chrdev\": \"file character device change\",\n        \"fifo\": \"file fifo change\",\n        \"mtime\": \"file modification time change\",\n        \"ctime\": \"file change time change\",\n        \"isomtime\": \"file modification time change (ISO 8601)\",\n        \"isoctime\": \"file creation time change (ISO 8601)\",\n    }\n    KEY_GROUPS = (\n        (\"path\", \"change\"),\n        (\"content\", \"mode\", \"type\", \"owner\", \"group\", \"user\"),\n        (\"link\", \"directory\", \"blkdev\", \"chrdev\", \"fifo\"),\n        (\"mtime\", \"ctime\", \"isomtime\", \"isoctime\"),\n    )\n    METADATA = (\"mode\", \"type\", \"owner\", \"group\", \"user\", \"mtime\", \"ctime\")\n\n    def __init__(self, format, content_only=False):\n        static_data = {} | self.FIXED_KEYS\n        super().__init__(format or \"{content}{link}{directory}{blkdev}{chrdev}{fifo} {path}{NL}\", static_data)\n        self.content_only = content_only\n        self.format_keys = {f[1] for f in Formatter().parse(format)}\n        self.call_keys = {\n            \"content\": self.format_content,\n            \"mode\": self.format_mode,\n            \"type\": partial(self.format_mode, filetype=True),\n            \"owner\": partial(self.format_owner),\n            \"group\": partial(self.format_owner, spec=\"group\"),\n            \"user\": partial(self.format_owner, spec=\"user\"),\n            \"link\": partial(self.format_other, \"link\"),\n            \"directory\": partial(self.format_other, \"directory\"),\n            \"blkdev\": partial(self.format_other, \"blkdev\"),\n            \"chrdev\": partial(self.format_other, \"chrdev\"),\n            \"fifo\": partial(self.format_other, \"fifo\"),\n            \"mtime\": partial(self.format_time, \"mtime\"),\n            \"ctime\": partial(self.format_time, \"ctime\"),\n            \"isomtime\": partial(self.format_iso_time, \"mtime\"),\n            \"isoctime\": partial(self.format_iso_time, \"ctime\"),\n        }\n        self.used_call_keys = set(self.call_keys) & self.format_keys\n        if self.content_only:\n            self.used_call_keys -= set(self.METADATA)\n\n    def get_item_data(self, item: \"ItemDiff\", jsonline=False) -> dict:\n        diff_data = {}\n        for key in self.used_call_keys:\n            diff_data[key] = self.call_keys[key](item)\n\n        change = []\n        for key in self.call_keys:\n            if key in (\"isomtime\", \"isoctime\"):\n                continue\n            if self.content_only and key in self.METADATA:\n                continue\n            change.append(self.call_keys[key](item))\n        diff_data[\"change\"] = \" \".join([v for v in change if v])\n        diff_data[\"path\"] = item.path\n        diff_data |= {} if jsonline else self.static_data\n        return diff_data\n\n    def format_other(self, key, diff: \"ItemDiff\"):\n        change = diff.changes().get(key)\n        return f\"{change.diff_type}\".ljust(27) if change else \"\"  # 27 is the length of the content change\n\n    def format_mode(self, diff: \"ItemDiff\", filetype=False):\n        change = diff.type() if filetype else diff.mode()\n        return f\"[{change.diff_data['item1']} -> {change.diff_data['item2']}]\" if change else \"\"\n\n    def format_owner(self, diff: \"ItemDiff\", spec: Literal[\"owner\", \"user\", \"group\"] = \"owner\"):\n        if spec == \"user\":\n            change = diff.user()\n            return f\"[{change.diff_data['item1']} -> {change.diff_data['item2']}]\" if change else \"\"\n        if spec == \"group\":\n            change = diff.group()\n            return f\"[{change.diff_data['item1']} -> {change.diff_data['item2']}]\" if change else \"\"\n        if spec != \"owner\":\n            raise ValueError(f\"Invalid owner spec: {spec}\")\n        change = diff.owner()\n        if change:\n            return \"[{}:{} -> {}:{}]\".format(\n                change.diff_data[\"item1\"][0],\n                change.diff_data[\"item1\"][1],\n                change.diff_data[\"item2\"][0],\n                change.diff_data[\"item2\"][1],\n            )\n        return \"\"\n\n    def format_content(self, diff: \"ItemDiff\"):\n        change = diff.content()\n        if change:\n            if change.diff_type == \"added\":\n                return \"{}: {:>20}\".format(change.diff_type, format_file_size(change.diff_data[\"added\"]))\n            if change.diff_type == \"removed\":\n                return \"{}: {:>18}\".format(change.diff_type, format_file_size(change.diff_data[\"removed\"]))\n            if \"added\" not in change.diff_data and \"removed\" not in change.diff_data:\n                return \"modified:  (can't get size)\"\n            return \"{}: {:>8} {:>8}\".format(\n                change.diff_type,\n                format_file_size(change.diff_data[\"added\"], precision=1, sign=True),\n                format_file_size(-change.diff_data[\"removed\"], precision=1, sign=True),\n            )\n        return \"\"\n\n    def format_time(self, key, diff: \"ItemDiff\"):\n        change = diff.changes().get(key)\n        return f\"[{key}: {change.diff_data['item1']} -> {change.diff_data['item2']}]\" if change else \"\"\n\n    def format_iso_time(self, key, diff: \"ItemDiff\"):\n        change = diff.changes().get(key)\n        return (\n            f\"[{key}: {change.diff_data['item1'].isoformat()} -> {change.diff_data['item2'].isoformat()}]\"\n            if change\n            else \"\"\n        )\n\n\ndef file_status(mode):\n    if stat.S_ISREG(mode):\n        return \"A\"\n    elif stat.S_ISDIR(mode):\n        return \"d\"\n    elif stat.S_ISBLK(mode):\n        return \"b\"\n    elif stat.S_ISCHR(mode):\n        return \"c\"\n    elif stat.S_ISLNK(mode):\n        return \"s\"\n    elif stat.S_ISFIFO(mode):\n        return \"f\"\n    return \"?\"\n\n\ndef clean_lines(lines, lstrip=None, rstrip=None, remove_empty=True, remove_comments=True):\n    \"\"\"\n    clean lines (usually read from a config file):\n\n    1. strip whitespace (left and right), 2. remove empty lines, 3. remove comments.\n\n    note: only \"pure comment lines\" are supported, no support for \"trailing comments\".\n\n    :param lines: input line iterator (e.g. list or open text file) that gives unclean input lines\n    :param lstrip: lstrip call arguments or False, if lstripping is not desired\n    :param rstrip: rstrip call arguments or False, if rstripping is not desired\n    :param remove_comments: remove comment lines (lines starting with \"#\")\n    :param remove_empty: remove empty lines\n    :return: yields processed lines\n    \"\"\"\n    for line in lines:\n        if lstrip is not False:\n            line = line.lstrip(lstrip)\n        if rstrip is not False:\n            line = line.rstrip(rstrip)\n        if remove_empty and not line:\n            continue\n        if remove_comments and line.startswith(\"#\"):\n            continue\n        yield line\n\n\ndef swidth_slice(string, max_width):\n    \"\"\"\n    Return a slice of *max_width* cells from *string*.\n\n    Negative *max_width* means from the end of string.\n\n    *max_width* is in units of character cells (or \"columns\").\n    Latin characters are usually one cell wide, many CJK characters are two cells wide.\n    \"\"\"\n    from ..platform import swidth\n\n    reverse = max_width < 0\n    max_width = abs(max_width)\n    if reverse:\n        string = reversed(string)\n    current_swidth = 0\n    result = []\n    for character in string:\n        current_swidth += swidth(character)\n        if current_swidth > max_width:\n            break\n        result.append(character)\n    if reverse:\n        result.reverse()\n    return \"\".join(result)\n\n\ndef ellipsis_truncate(msg, space):\n    \"\"\"\n    shorten a long string by adding ellipsis between it and return it, example:\n    this_is_a_very_long_string -------> this_is..._string\n    \"\"\"\n    from ..platform import swidth\n\n    ellipsis_width = swidth(\"...\")\n    msg_width = swidth(msg)\n    if space < 8:\n        # if there is very little space, just show ...\n        return \"...\" + \" \" * (space - ellipsis_width)\n    if space < ellipsis_width + msg_width:\n        return f\"{swidth_slice(msg, space // 2 - ellipsis_width)}...{swidth_slice(msg, -space // 2)}\"\n    return msg + \" \" * (space - msg_width)\n\n\nclass BorgJsonEncoder(json.JSONEncoder):\n    def default(self, o):\n        from ..legacyrepository import LegacyRepository\n        from ..repository import Repository\n        from ..legacyremote import LegacyRemoteRepository\n        from ..remote import RemoteRepository\n        from ..archive import Archive\n        from ..cache import AdHocWithFilesCache\n\n        if isinstance(o, (LegacyRepository, LegacyRemoteRepository)) or isinstance(o, (Repository, RemoteRepository)):\n            return {\"id\": bin_to_hex(o.id), \"location\": o._location.canonical_path()}\n        if isinstance(o, Archive):\n            return o.info()\n        if isinstance(o, (AdHocWithFilesCache,)):\n            return {\"path\": o.path}\n        if isinstance(o, Path):\n            return str(o)\n        if callable(getattr(o, \"to_json\", None)):\n            return o.to_json()\n        return super().default(o)\n\n\ndef basic_json_data(manifest, *, cache=None, extra=None):\n    key = manifest.key\n    data = extra or {}\n    data |= {\"repository\": BorgJsonEncoder().default(manifest.repository), \"encryption\": {\"mode\": key.ARG_NAME}}\n    data[\"repository\"][\"last_modified\"] = OutputTimestamp(manifest.last_timestamp)\n    if key.NAME.startswith(\"key file\"):\n        data[\"encryption\"][\"keyfile\"] = key.find_key()\n    if cache:\n        data[\"cache\"] = cache\n    return data\n\n\ndef json_dump(obj):\n    \"\"\"Dump using BorgJSONEncoder.\"\"\"\n    return json.dumps(obj, sort_keys=True, indent=4, cls=BorgJsonEncoder)\n\n\ndef json_print(obj):\n    print(json_dump(obj))\n\n\ndef prepare_dump_dict(d):\n    def decode_bytes(value):\n        # this should somehow be reversible later, but usual strings should\n        # look nice and chunk ids should mostly show in hex. Use a special\n        # inband signaling character (ASCII DEL) to distinguish between\n        # decoded and hex mode.\n        if not value.startswith(b\"\\x7f\"):\n            try:\n                value = value.decode()\n                return value\n            except UnicodeDecodeError:\n                pass\n        return \"\\u007f\" + bin_to_hex(value)\n\n    def decode_tuple(t):\n        res = []\n        for value in t:\n            if isinstance(value, dict):\n                value = decode(value)\n            elif isinstance(value, tuple) or isinstance(value, list):\n                value = decode_tuple(value)\n            elif isinstance(value, bytes):\n                value = decode_bytes(value)\n            res.append(value)\n        return res\n\n    def decode(d):\n        res = OrderedDict()\n        for key, value in d.items():\n            if isinstance(value, dict):\n                value = decode(value)\n            elif isinstance(value, (tuple, list)):\n                value = decode_tuple(value)\n            elif isinstance(value, bytes):\n                value = decode_bytes(value)\n            elif isinstance(value, Timestamp):\n                value = value.to_unix_nano()\n            if isinstance(key, bytes):\n                key = key.decode()\n            res[key] = value\n        return res\n\n    return decode(d)\n\n\nclass Highlander(Action):\n    \"\"\"make sure some option is only given once\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self.__called = False\n        super().__init__(*args, **kwargs)\n\n    def __call__(self, parser, namespace, values, option_string=None):\n        if self.__called:\n            raise ArgumentError(self, \"There can be only one.\")\n        self.__called = True\n        setattr(namespace, self.dest, values)\n\n\nclass MakePathSafeAction(Highlander):\n    def __call__(self, parser, namespace, path, option_string=None):\n        try:\n            sanitized_path = make_path_safe(path)\n        except ValueError as e:\n            raise ArgumentError(self, e)\n        if sanitized_path == \".\":\n            raise ArgumentError(self, f\"{path!r} is not a valid file name\")\n        setattr(namespace, self.dest, sanitized_path)\n"
  },
  {
    "path": "src/borg/helpers/passphrase.py",
    "content": "import getpass\nimport os\nimport shlex\nimport subprocess\nimport sys\nimport textwrap\n\nfrom . import bin_to_hex\nfrom . import Error\nfrom . import yes\nfrom . import prepare_subprocess_env\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass NoPassphraseFailure(Error):\n    \"\"\"Cannot acquire a passphrase: {}.\"\"\"\n\n    exit_mcode = 50\n\n\nclass PasscommandFailure(Error):\n    \"\"\"Passcommand supplied in BORG_PASSCOMMAND failed: {}.\"\"\"\n\n    exit_mcode = 51\n\n\nclass PassphraseWrong(Error):\n    \"\"\"Passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND, or via BORG_PASSPHRASE_FD is incorrect.\"\"\"\n\n    exit_mcode = 52\n\n\nclass PasswordRetriesExceeded(Error):\n    \"\"\"Exceeded the maximum password retries.\"\"\"\n\n    exit_mcode = 53\n\n\nclass Passphrase(str):\n    @classmethod\n    def _check_ambiguity(cls, vars):\n        set_vars = [v for v in vars if v in os.environ]\n        if len(set_vars) > 1:\n            raise Error(\"More than one passphrase environment variable is set: \" + \", \".join(set_vars))\n\n    @classmethod\n    def _env_passphrase(cls, env_var, default=None):\n        passphrase = os.environ.get(env_var, default)\n        if passphrase is not None:\n            return cls(passphrase)\n\n    @classmethod\n    def env_passphrase(cls, default=None, other=False):\n        if other:\n            cls._check_ambiguity([\"BORG_OTHER_PASSPHRASE\", \"BORG_OTHER_PASSCOMMAND\", \"BORG_OTHER_PASSPHRASE_FD\"])\n        else:\n            cls._check_ambiguity([\"BORG_PASSPHRASE\", \"BORG_PASSCOMMAND\", \"BORG_PASSPHRASE_FD\"])\n        env_var = \"BORG_OTHER_PASSPHRASE\" if other else \"BORG_PASSPHRASE\"\n        passphrase = cls._env_passphrase(env_var, default)\n        if passphrase is not None:\n            return passphrase\n        passphrase = cls.env_passcommand(other=other)\n        if passphrase is not None:\n            return passphrase\n        passphrase = cls.fd_passphrase(other=other)\n        if passphrase is not None:\n            return passphrase\n\n    @classmethod\n    def env_passcommand(cls, default=None, other=False):\n        env_var = \"BORG_OTHER_PASSCOMMAND\" if other else \"BORG_PASSCOMMAND\"\n        passcommand = os.environ.get(env_var, None)\n        if passcommand is not None:\n            # passcommand is a system command (not inside pyinstaller env)\n            env = prepare_subprocess_env(system=True)\n            try:\n                passphrase = subprocess.check_output(shlex.split(passcommand), text=True, env=env)  # nosec B603\n            except (subprocess.CalledProcessError, FileNotFoundError) as e:\n                raise PasscommandFailure(e)\n            return cls(passphrase.rstrip(\"\\n\"))\n\n    @classmethod\n    def fd_passphrase(cls, other=False):\n        env_var = \"BORG_OTHER_PASSPHRASE_FD\" if other else \"BORG_PASSPHRASE_FD\"\n        try:\n            fd = int(os.environ.get(env_var))\n        except (ValueError, TypeError):\n            return None\n        with os.fdopen(fd, mode=\"r\") as f:\n            passphrase = f.read()\n        return cls(passphrase.rstrip(\"\\n\"))\n\n    @classmethod\n    def env_new_passphrase(cls, default=None):\n        return cls._env_passphrase(\"BORG_NEW_PASSPHRASE\", default)\n\n    @classmethod\n    def getpass(cls, prompt):\n        try:\n            pw = getpass.getpass(prompt)\n        except EOFError:\n            if prompt:\n                print()  # avoid err msg appearing right of prompt\n            msg = []\n            for env_var in \"BORG_PASSPHRASE\", \"BORG_PASSCOMMAND\":\n                env_var_set = os.environ.get(env_var) is not None\n                msg.append(\"{} is {}.\".format(env_var, \"set\" if env_var_set else \"not set\"))\n            msg.append(\"Interactive password query failed.\")\n            raise NoPassphraseFailure(\" \".join(msg)) from None\n        else:\n            return cls(pw)\n\n    @classmethod\n    def verification(cls, passphrase):\n        msg = \"Do you want your passphrase to be displayed for verification? [yN]: \"\n        if yes(\n            msg,\n            retry_msg=msg,\n            invalid_msg=\"Invalid answer, try again.\",\n            retry=True,\n            env_var_override=\"BORG_DISPLAY_PASSPHRASE\",\n        ):\n            pw_msg = textwrap.dedent(\n                f\"\"\"\\\n            Your passphrase (between double-quotes): \"{passphrase}\"\n            Make sure the passphrase displayed above is exactly what you wanted.\n            Your passphrase (UTF-8 encoding in hex): {bin_to_hex(passphrase.encode(\"utf-8\"))}\n            It is recommended to keep the UTF-8 encoding in hex together with the passphrase in a safe place.\n            In case you should ever run into passphrase issues, it could sometimes help debug them.\n            \"\"\"\n            )\n            print(pw_msg, file=sys.stderr)\n\n    @staticmethod\n    def display_debug_info(passphrase):\n        def fmt_var(env_var):\n            env_var_value = os.environ.get(env_var)\n            if env_var_value is not None:\n                return f'{env_var} = \"{env_var_value}\"'\n            else:\n                return f\"# {env_var} is not set\"\n\n        if os.environ.get(\"BORG_DEBUG_PASSPHRASE\") == \"YES\":\n            passphrase_info = textwrap.dedent(\n                f\"\"\"\\\n                Incorrect passphrase!\n                Passphrase used (between double-quotes): \"{passphrase}\"\n                Same, UTF-8 encoded, in hex: {bin_to_hex(passphrase.encode('utf-8'))}\n                Relevant Environment Variables:\n                {fmt_var(\"BORG_PASSPHRASE\")}\n                {fmt_var(\"BORG_PASSCOMMAND\")}\n                {fmt_var(\"BORG_PASSPHRASE_FD\")}\n                {fmt_var(\"BORG_OTHER_PASSPHRASE\")}\n                {fmt_var(\"BORG_OTHER_PASSCOMMAND\")}\n                {fmt_var(\"BORG_OTHER_PASSPHRASE_FD\")}\n                \"\"\"\n            )\n            print(passphrase_info, file=sys.stderr)\n\n    @classmethod\n    def new(cls, allow_empty=False):\n        passphrase = cls.env_new_passphrase()\n        if passphrase is not None:\n            return passphrase\n        passphrase = cls.env_passphrase()\n        if passphrase is not None:\n            return passphrase\n        for retry in range(1, 11):\n            passphrase = cls.getpass(\"Enter new passphrase: \")\n            if allow_empty or passphrase:\n                passphrase2 = cls.getpass(\"Enter same passphrase again: \")\n                if passphrase == passphrase2:\n                    cls.verification(passphrase)\n                    logger.info(\"Remember your passphrase. Your data will be inaccessible without it.\")\n                    return passphrase\n                else:\n                    print(\"Passphrases do not match\", file=sys.stderr)\n            else:\n                print(\"Passphrase must not be blank\", file=sys.stderr)\n        else:\n            raise PasswordRetriesExceeded\n\n    def __repr__(self):\n        return '<Passphrase \"***hidden***\">'\n"
  },
  {
    "path": "src/borg/helpers/process.py",
    "content": "import contextlib\nimport os\nimport shlex\nimport signal\nimport subprocess\nimport sys\nimport time\nimport threading\nimport traceback\n\nfrom .. import __version__\n\nfrom ..platformflags import is_win32\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\nfrom ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_SIGNAL_BASE, Error\n\n\n@contextlib.contextmanager\ndef _daemonize():\n    from ..platform import get_process_id\n\n    old_id = get_process_id()\n    pid = os.fork()\n    if pid:\n        exit_code = EXIT_SUCCESS\n        try:\n            yield old_id, None\n        except _ExitCodeException as e:\n            exit_code = e.exit_code\n        finally:\n            logger.debug(\"Daemonizing: Foreground process (%s, %s, %s) is now dying.\" % old_id)\n            os._exit(exit_code)\n    os.setsid()\n    pid = os.fork()\n    if pid:\n        os._exit(0)\n    os.chdir(\"/\")\n    os.close(0)\n    os.close(1)\n    fd = os.open(os.devnull, os.O_RDWR)\n    os.dup2(fd, 0)\n    os.dup2(fd, 1)\n    new_id = get_process_id()\n    try:\n        yield old_id, new_id\n    finally:\n        # Close / redirect stderr to /dev/null only now\n        # for the case that we want to log something before yield returns.\n        os.close(2)\n        os.dup2(fd, 2)\n\n\ndef daemonize():\n    \"\"\"Detach the process from the controlling terminal and run in the background.\n\n    Returns: old and new get_process_id tuples.\n    \"\"\"\n    with _daemonize() as (old_id, new_id):\n        return old_id, new_id\n\n\n@contextlib.contextmanager\ndef daemonizing(*, timeout=5, show_rc=False):\n    \"\"\"Like daemonize(), but as a context manager.\n\n    The with-body is executed in the background process,\n    while the foreground process survives until the body is left\n    or the given timeout is exceeded. In the latter case a warning is\n    reported by the foreground.\n    Context variable is (old_id, new_id) get_process_id tuples.\n    An exception raised in the body is reported by the foreground\n    as a warning as well as propagated outside the body in the background.\n    In case of a warning, the foreground exits with exit code EXIT_WARNING\n    instead of EXIT_SUCCESS.\n    \"\"\"\n    with _daemonize() as (old_id, new_id):\n        if new_id is None:\n            # The original / parent process, waiting for a signal to die.\n            logger.debug(\"Daemonizing: Foreground process (%s, %s, %s) is waiting for background process...\" % old_id)\n            exit_code = EXIT_SUCCESS\n            # Indeed, SIGHUP and SIGTERM handlers should have been set on archiver.run(). Just in case...\n            with (\n                signal_handler(\"SIGINT\", raising_signal_handler(KeyboardInterrupt)),\n                signal_handler(\"SIGHUP\", raising_signal_handler(SigHup)),\n                signal_handler(\"SIGTERM\", raising_signal_handler(SigTerm)),\n            ):\n                try:\n                    if timeout > 0:\n                        time.sleep(timeout)\n                except SigTerm:\n                    # Normal termination; expected from grandchild, see 'os.kill()' below\n                    pass\n                except SigHup:\n                    # Background wants to indicate a problem; see 'os.kill()' below,\n                    # log message will come from grandchild.\n                    exit_code = EXIT_WARNING\n                except KeyboardInterrupt:\n                    # Manual termination.\n                    logger.debug(\"Daemonizing: Foreground process (%s, %s, %s) received SIGINT.\" % old_id)\n                    exit_code = EXIT_SIGNAL_BASE + 2\n                except BaseException as e:\n                    # Just in case...\n                    logger.warning(\n                        \"Daemonizing: Foreground process received an exception while waiting:\\n\"\n                        + \"\".join(traceback.format_exception(e.__class__, e, e.__traceback__))\n                    )\n                    exit_code = EXIT_WARNING\n                else:\n                    logger.warning(\"Daemonizing: Background process did not respond (timeout). Is it alive?\")\n                    exit_code = EXIT_WARNING\n                finally:\n                    # Before terminating the foreground process, honor --show-rc by logging the rc here as well.\n                    # This is mostly a consistency fix and not very useful considering that the main action\n                    # happens in the daemon process.\n                    if show_rc:\n                        from ..helpers import do_show_rc\n\n                        do_show_rc(exit_code)\n                    # Don't call with-body, but die immediately!\n                    # return would be sufficient, but we want to pass the exit code.\n                    raise _ExitCodeException(exit_code)\n\n        # The background / grandchild process.\n        sig_to_foreground = signal.SIGTERM\n        logger.debug(\"Daemonizing: Background process (%s, %s, %s) is starting...\" % new_id)\n        try:\n            yield old_id, new_id\n        except BaseException as e:\n            sig_to_foreground = signal.SIGHUP\n            logger.warning(\n                \"Daemonizing: Background process raised an exception while starting:\\n\"\n                + \"\".join(traceback.format_exception(e.__class__, e, e.__traceback__))\n            )\n            raise e\n        else:\n            logger.debug(\"Daemonizing: Background process (%s, %s, %s) has started.\" % new_id)\n        finally:\n            try:\n                os.kill(old_id[1], sig_to_foreground)\n            except BaseException as e:\n                logger.error(\n                    \"Daemonizing: Trying to kill the foreground process raised an exception:\\n\"\n                    + \"\".join(traceback.format_exception(e.__class__, e, e.__traceback__))\n                )\n\n\nclass _ExitCodeException(BaseException):\n    def __init__(self, exit_code):\n        self.exit_code = exit_code\n\n\nclass SignalException(BaseException):\n    \"\"\"Base class for all signal-based exceptions.\"\"\"\n\n\nclass SigHup(SignalException):\n    \"\"\"Raised on SIGHUP signal.\"\"\"\n\n\nclass SigTerm(SignalException):\n    \"\"\"Raised on SIGTERM signal.\"\"\"\n\n\n@contextlib.contextmanager\ndef signal_handler(sig, handler):\n    \"\"\"\n    When entering the context, set up signal handler <handler> for signal <sig>.\n    When leaving the context, restore the original signal handler.\n\n    <sig> can be either a str (the name of a signal.SIGXXX attribute; it\n    will not crash if the attribute name does not exist, as some names are platform\n    specific) or an int (a signal number).\n\n    <handler> is any handler value accepted by signal.signal(sig, handler).\n    \"\"\"\n    if isinstance(sig, str):\n        sig = getattr(signal, sig, None)\n    if sig is not None:\n        orig_handler = signal.signal(sig, handler)\n    try:\n        yield\n    finally:\n        if sig is not None:\n            signal.signal(sig, orig_handler)\n\n\ndef raising_signal_handler(exc_cls):\n    def handler(sig_no, frame):\n        # setting SIG_IGN avoids that an incoming second signal of this\n        # kind would raise a 2nd exception while we still process the\n        # exception handler for exc_cls for the 1st signal.\n        signal.signal(sig_no, signal.SIG_IGN)\n        raise exc_cls\n\n    return handler\n\n\nclass SigIntManager:\n    def __init__(self):\n        self._sig_int_triggered = False\n        self._action_triggered = False\n        self._action_done = False\n        self.ctx = signal_handler(\"SIGINT\", self.handler)\n        self.debounce_interval = 20000000  # ns\n        self.last = None  # monotonic time when we last processed SIGINT\n\n    def __bool__(self):\n        # this will be True (and stay True) after the first Ctrl-C/SIGINT\n        return self._sig_int_triggered\n\n    def action_triggered(self):\n        # this is True to indicate that the action shall be done\n        return self._action_triggered\n\n    def action_done(self):\n        # this will be True after the action has completed\n        return self._action_done\n\n    def action_completed(self):\n        # this must be called when the action triggered is completed,\n        # to avoid repeatedly triggering the action.\n        self._action_triggered = False\n        self._action_done = True\n\n    def handler(self, sig_no, stack):\n        # Ignore a SIGINT if it comes too quickly after the last one, e.g. because it\n        # was caused by the same Ctrl-C key press and a parent process forwarded it to us.\n        # This can easily happen for the pyinstaller-made binaries because the bootloader\n        # process and the borg process are in same process group (see #8155), but maybe also\n        # under other circumstances.\n        now = time.monotonic_ns()\n        if self.last is None:  # first SIGINT\n            self.last = now\n            self._sig_int_triggered = True\n            self._action_triggered = True\n        elif now - self.last >= self.debounce_interval:  # second SIGINT\n            # restore the original signal handler for the 3rd+ SIGINT -\n            # this implies that this handler here loses control!\n            self.__exit__(None, None, None)\n            # handle 2nd SIGINT like the default handler would do it:\n            raise KeyboardInterrupt  # python docs say this might show up at an arbitrary place.\n\n    def __enter__(self):\n        self.ctx.__enter__()\n\n    def __exit__(self, exception_type, exception_value, traceback):\n        # restore the original ctrl-c handler, so the next ctrl-c / SIGINT does the normal thing:\n        if self.ctx:\n            self.ctx.__exit__(exception_type, exception_value, traceback)\n            self.ctx = None\n\n\n# global flag which might trigger some special behaviour on first ctrl-c / SIGINT.\nsig_int = SigIntManager()\n\n\ndef ignore_sigint():\n    \"\"\"\n    Ignore SIGINT (see also issue #6912).\n\n    Ctrl-C will send a SIGINT to both the main process (borg) and subprocesses\n    (e.g., ssh for remote ssh:// repositories), but often we do not want the subprocess\n    to be killed (e.g., because it is still needed to shut down borg cleanly).\n\n    To avoid this, use: Popen(..., preexec_fn=ignore_sigint)\n    \"\"\"\n    signal.signal(signal.SIGINT, signal.SIG_IGN)\n\n\ndef popen_with_error_handling(cmd_line: str, log_prefix=\"\", **kwargs):\n    \"\"\"\n    Handle typical errors raised by subprocess.Popen. Return None if an error occurred,\n    otherwise return the Popen object.\n\n    *cmd_line* is split using shlex (e.g. 'gzip -9' => ['gzip', '-9']).\n\n    Log messages will be prefixed with *log_prefix*; if set, it should end with a space\n    (e.g. log_prefix='--some-option: ').\n\n    Does not change the exit code.\n    \"\"\"\n    assert not kwargs.get(\"shell\"), \"Sorry pal, shell mode is a no-no\"\n    try:\n        command = shlex.split(cmd_line)\n        if not command:\n            raise ValueError(\"an empty command line is not permitted\")\n    except ValueError as ve:\n        logger.error(\"%s%s\", log_prefix, ve)\n        return\n    logger.debug(\"%scommand line: %s\", log_prefix, command)\n    try:\n        return subprocess.Popen(command, **kwargs)  # nosec B603\n    except FileNotFoundError:\n        logger.error(\"%sexecutable not found: %s\", log_prefix, command[0])\n        return\n    except PermissionError:\n        logger.error(\"%spermission denied: %s\", log_prefix, command[0])\n        return\n\n\ndef is_terminal(fd=sys.stdout):\n    return hasattr(fd, \"isatty\") and fd.isatty() and (not is_win32 or \"ANSICON\" in os.environ)\n\n\ndef prepare_subprocess_env(system, env=None):\n    \"\"\"\n    Prepare the environment for a subprocess we are going to create.\n\n    :param system: True for preparing to invoke system-installed binaries,\n                   False for stuff inside the PyInstaller environment (like borg, python).\n    :param env: optionally provide an environment dict here. If not given, defaults to os.environ.\n    :return: a modified copy of the environment.\n    \"\"\"\n    env = dict(env if env is not None else os.environ)\n    if system:\n        # a pyinstaller binary's bootloader modifies LD_LIBRARY_PATH=/tmp/_MEIXXXXXX,\n        # but we do not want that system binaries (like ssh or other) pick up\n        # (non-matching) libraries from there.\n        # thus we install the original LDLP, before pyinstaller has modified it:\n        lp_key = \"LD_LIBRARY_PATH\"\n        lp_orig = env.get(lp_key + \"_ORIG\")  # pyinstaller >= 20160820 / v3.2.1 has this\n        if lp_orig is not None:\n            env[lp_key] = lp_orig\n        else:\n            # We get here in 2 cases:\n            # 1. when not running a pyinstaller-made binary.\n            #    in this case, we must not kill LDLP.\n            # 2. when running a pyinstaller-made binary and there was no LDLP\n            #    in the original env (in this case, the pyinstaller bootloader\n            #    does *not* put ..._ORIG into the env either).\n            #    in this case, we must kill LDLP.\n            #    We can recognize this via sys.frozen and sys._MEIPASS being set.\n            lp = env.get(lp_key)\n            if lp is not None and getattr(sys, \"frozen\", False) and hasattr(sys, \"_MEIPASS\"):\n                env.pop(lp_key)\n    # security: do not give secrets to subprocess\n    env.pop(\"BORG_PASSPHRASE\", None)\n    # for information, give borg version to the subprocess\n    env[\"BORG_VERSION\"] = __version__\n    return env\n\n\n@contextlib.contextmanager\ndef create_filter_process(cmd, stream, stream_close, inbound=True):\n    if cmd:\n        # put a filter process between stream and us (e.g. a [de]compression command)\n        # inbound: <stream> --> filter --> us\n        # outbound: us --> filter --> <stream>\n        filter_stream = stream\n        filter_stream_close = stream_close\n        env = prepare_subprocess_env(system=True)\n        # There is no deadlock potential here (the subprocess docs warn about this), because\n        # communication with the process is a one-way road, i.e. the process can never block\n        # for us to do something while we block on the process for something different.\n        if inbound:\n            proc = popen_with_error_handling(\n                cmd,\n                stdout=subprocess.PIPE,\n                stdin=filter_stream,\n                log_prefix=\"filter-process: \",\n                env=env,\n                preexec_fn=None if is_win32 else ignore_sigint,\n            )\n        else:\n            proc = popen_with_error_handling(\n                cmd,\n                stdin=subprocess.PIPE,\n                stdout=filter_stream,\n                log_prefix=\"filter-process: \",\n                env=env,\n                preexec_fn=None if is_win32 else ignore_sigint,\n            )\n        if not proc:\n            raise Error(f\"filter {cmd}: process creation failed\")\n        stream = proc.stdout if inbound else proc.stdin\n        # inbound: do not close the pipe (this is the task of the filter process [== writer])\n        # outbound: close the pipe, otherwise the filter process would not notice when we are done.\n        stream_close = not inbound\n\n    try:\n        yield stream\n\n    except Exception:\n        # something went wrong with processing the stream by borg\n        logger.debug(\"Exception, killing the filter...\")\n        if cmd:\n            proc.kill()\n        borg_succeeded = False\n        raise\n    else:\n        borg_succeeded = True\n    finally:\n        if stream_close:\n            stream.close()\n\n        if cmd:\n            logger.debug(\"Done, waiting for filter to die...\")\n            rc = proc.wait()\n            logger.debug(\"filter cmd exited with code %d\", rc)\n            if filter_stream_close:\n                filter_stream.close()\n            if borg_succeeded and rc:\n                # if borg did not succeed, we know that we killed the filter process\n                raise Error(\"filter %s failed, rc=%d\" % (cmd, rc))\n\n\nclass ThreadRunner:\n    def __init__(self, sleep_interval, target, *args, **kwargs):\n        \"\"\"\n        Initialize the ThreadRunner with a target function and its arguments.\n\n        :param sleep_interval: The interval (in seconds) to sleep between executions of the target function.\n        :param target: The target function to be run in the thread.\n        :param args: The positional arguments to be passed to the target function.\n        :param kwargs: The keyword arguments to be passed to the target function.\n        \"\"\"\n        self._target = target\n        self._args = args\n        self._kwargs = kwargs\n        self._sleep_interval = sleep_interval\n        self._thread = None\n        self._keep_running = threading.Event()\n        self._keep_running.set()\n\n    def _run_with_termination(self):\n        \"\"\"\n        Wrapper function to check if the thread should keep running.\n        \"\"\"\n        while self._keep_running.is_set():\n            self._target(*self._args, **self._kwargs)\n            # sleep up to self._sleep_interval, but end the sleep early if we shall not keep running:\n            count = 1000\n            micro_sleep = float(self._sleep_interval) / count\n            while self._keep_running.is_set() and count > 0:\n                time.sleep(micro_sleep)\n                count -= 1\n\n    def start(self):\n        \"\"\"\n        Start the thread.\n        \"\"\"\n        self._thread = threading.Thread(target=self._run_with_termination)\n        self._thread.start()\n\n    def terminate(self):\n        \"\"\"\n        Signal the thread to stop and wait for it to finish.\n        \"\"\"\n        if self._thread is not None:\n            self._keep_running.clear()\n            self._thread.join()\n"
  },
  {
    "path": "src/borg/helpers/progress.py",
    "content": "import logging\nimport json\nimport time\n\nfrom ..logger import create_logger\n\nlogger = create_logger()\n\n\nclass ProgressIndicatorBase:\n    LOGGER = \"borg.output.progress\"\n    JSON_TYPE: str = None\n\n    operation_id_counter = 0\n\n    @classmethod\n    def operation_id(cls):\n        \"\"\"Unique number, can be used by receiving applications to distinguish different operations.\"\"\"\n        cls.operation_id_counter += 1\n        return cls.operation_id_counter\n\n    def __init__(self, msgid=None):\n        self.logger = logging.getLogger(self.LOGGER)\n        self.id = self.operation_id()\n        self.msgid = msgid\n\n    def make_json(self, *, finished=False, **kwargs):\n        kwargs |= dict(operation=self.id, msgid=self.msgid, type=self.JSON_TYPE, finished=finished, time=time.time())\n        return json.dumps(kwargs)\n\n    def finish(self):\n        j = self.make_json(message=\"\", finished=True)\n        self.logger.info(j)\n\n\nclass ProgressIndicatorMessage(ProgressIndicatorBase):\n    JSON_TYPE = \"progress_message\"\n\n    def output(self, msg):\n        j = self.make_json(message=msg)\n        self.logger.info(j)\n\n\nclass ProgressIndicatorPercent(ProgressIndicatorBase):\n    JSON_TYPE = \"progress_percent\"\n\n    def __init__(self, total=0, step=5, start=0, msg=\"%3.0f%%\", msgid=None):\n        \"\"\"\n        Percentage-based progress indicator.\n\n        :param total: total amount of items.\n        :param step: step size in percent.\n        :param start: at which percent value to start.\n        :param msg: output message; must contain one %f placeholder for the percentage.\n        \"\"\"\n        self.counter = 0  # 0 .. (total-1)\n        self.total = total\n        self.trigger_at = start  # output next percentage value when reaching (at least) this\n        self.step = step\n        self.msg = msg\n\n        super().__init__(msgid=msgid)\n\n    def progress(self, current=None, increase=1):\n        if current is not None:\n            self.counter = current\n        pct = self.counter * 100 / self.total\n        self.counter += increase\n        if pct >= self.trigger_at:\n            self.trigger_at += self.step\n            return pct\n\n    def show(self, current=None, increase=1, info=None):\n        \"\"\"\n        Show and output the progress message.\n\n        :param current: Set the current percentage. [None]\n        :param increase: Increase the current percentage. [None]\n        :param info: Array of strings to be formatted with msg. [None]\n        \"\"\"\n        pct = self.progress(current, increase)\n        if pct is not None:\n            if info is not None:\n                return self.output(self.msg % tuple([pct] + info), info=info)\n            else:\n                return self.output(self.msg % pct)\n\n    def output(self, message, info=None):\n        j = self.make_json(message=message, current=self.counter, total=self.total, info=info)\n        self.logger.info(j)\n"
  },
  {
    "path": "src/borg/helpers/shellpattern.py",
    "content": "import os\nimport re\nfrom queue import LifoQueue\n\n\ndef translate(pat, match_end=r\"\\Z\"):\n    \"\"\"Translate a shell-style pattern to a regular expression.\n\n    The pattern may include ``**<sep>`` (<sep> stands for the platform-specific path separator; \"/\" on POSIX systems)\n    for matching zero or more directory levels and \"*\" for matching zero or more arbitrary characters except any path\n    separator. Wrap meta-characters in brackets for a literal match (i.e., \"[?]\" to match the literal character \"?\").\n\n    Using ``match_end=REGEX`` you can provide a regular expression that is appended after the pattern-derived\n    expression. The default is to match the end of the string.\n\n    This function is derived from the \"fnmatch\" module distributed with the Python standard library.\n\n    :copyright: 2001-2016 Python Software Foundation. All rights reserved.\n    :license: PSFLv2\n    \"\"\"\n    pat = _translate_alternatives(pat)\n\n    sep = os.path.sep\n    n = len(pat)\n    i = 0\n    res = \"\"\n\n    while i < n:\n        c = pat[i]\n        i += 1\n\n        if c == \"*\":\n            if i + 1 < n and pat[i] == \"*\" and pat[i + 1] == sep:\n                # **/ == wildcard for 0+ full (relative) directory names with trailing slashes; the forward slash stands\n                # for the platform-specific path separator\n                res += rf\"(?:[^\\{sep}]*\\{sep})*\"\n                i += 2\n            else:\n                # * == wildcard for name parts (does not cross path separator)\n                res += r\"[^\\%s]*\" % sep\n        elif c == \"?\":\n            # ? == any single character excluding path separator\n            res += r\"[^\\%s]\" % sep\n        elif c == \"[\":\n            j = i\n            if j < n and pat[j] == \"!\":\n                j += 1\n            if j < n and pat[j] == \"]\":\n                j += 1\n            while j < n and pat[j] != \"]\":\n                j += 1\n            if j >= n:\n                res += \"\\\\[\"\n            else:\n                stuff = pat[i:j].replace(\"\\\\\", \"\\\\\\\\\")\n                i = j + 1\n                if stuff[0] == \"!\":\n                    stuff = \"^\" + stuff[1:]\n                elif stuff[0] == \"^\":\n                    stuff = \"\\\\\" + stuff\n                res += \"[%s]\" % stuff\n        elif c in \"(|)\":\n            if i > 0 and pat[i - 1] != \"\\\\\":\n                res += c\n        else:\n            res += re.escape(c)\n\n    return \"(?ms)\" + res + match_end\n\n\ndef _parse_braces(pat):\n    \"\"\"Return the index pairs of matched braces in `pat` as a list of tuples.\n\n    The dictionary's keys are the indices corresponding to opening braces. Initially,\n    they are set to a value of `None`. Once a corresponding closing brace is found,\n    the value is updated. All dictionary keys with a positive integer value are valid pairs.\n\n    We cannot rely on re.match(\"[^\\\\(\\\\\\\\)*]?{.*[^\\\\(\\\\\\\\)*]}\") because, while it\n    handles unpaired braces and nested pairs of braces, it misses sequences\n    of paired braces. For example: \"{foo,bar}{bar,baz}\" would translate, incorrectly, to\n    \"(foo|bar\\\\}\\\\{bar|baz)\" instead of, correctly, to \"(foo|bar)(bar|baz)\".\n\n    Therefore, this function parses left-to-right, tracking pairs with a LIFO\n    queue: pushing opening braces on and popping them off when finding a closing\n    brace.\n    \"\"\"\n    curly_q = LifoQueue()\n    pairs: dict[int, int] = dict()\n\n    for idx, c in enumerate(pat):\n        if c == \"{\":\n            if idx == 0 or pat[idx - 1] != \"\\\\\":\n                # Opening brace is not escaped.\n                # Add to dict\n                pairs[idx] = None\n                # Add to queue\n                curly_q.put(idx)\n        if c == \"}\" and curly_q.qsize():\n            # If queue is empty, then cannot close pair.\n            if idx > 0 and pat[idx - 1] != \"\\\\\":\n                # Closing brace is not escaped.\n                # Pop off the index of the corresponding opening brace, which\n                # provides the key in the dict of pairs, and set its value.\n                pairs[curly_q.get()] = idx\n    return [(opening, closing) for opening, closing in pairs.items() if closing is not None]\n\n\ndef _translate_alternatives(pat):\n    \"\"\"Translates the shell-style alternative portions of the pattern to regular expression groups.\n\n    For example: {alt1,alt2} -> (alt1|alt2)\n    \"\"\"\n    # Parse pattern for paired braces.\n    brace_pairs = _parse_braces(pat)\n\n    pat_list = list(pat)  # Convert to list in order to subscript characters.\n\n    # Convert non-escaped commas within groups to pipes.\n    # Passing, e.g. \"{a\\,b}.txt\" to the shell expands to \"{a,b}.txt\", whereas\n    # \"{a\\,,b}.txt\" expands to \"a,.txt\" and \"b.txt\"\n    for opening, closing in brace_pairs:\n        commas = 0\n\n        for i in range(opening + 1, closing):  # Convert non-escaped commas to pipes.\n            if pat_list[i] == \",\":\n                if i == opening or pat_list[i - 1] != \"\\\\\":\n                    pat_list[i] = \"|\"\n                    commas += 1\n            elif pat_list[i] == \"|\" and (i == opening or pat_list[i - 1] != \"\\\\\"):\n                # Nested groups have their commas converted to pipes when traversing the parent group.\n                # So in order to confirm the presence of a comma in the original, shell-style pattern,\n                # we must also check for a pipe.\n                commas += 1\n\n        # Convert paired braces into parentheses, but only if at least one comma is present.\n        if commas > 0:\n            pat_list[opening] = \"(\"\n            pat_list[closing] = \")\"\n\n    return \"\".join(pat_list)\n"
  },
  {
    "path": "src/borg/helpers/time.py",
    "content": "import os\nimport re\nfrom datetime import datetime, timezone, timedelta\n\n\ndef parse_timestamp(timestamp, tzinfo=timezone.utc):\n    \"\"\"Parse an ISO 8601 timestamp string.\n\n    For naive/unaware datetime objects, assume they are in the tzinfo timezone (default: UTC).\n    \"\"\"\n    dt = datetime.fromisoformat(timestamp)\n    if dt.tzinfo is None:\n        dt = dt.replace(tzinfo=tzinfo)\n    return dt\n\n\ndef parse_local_timestamp(timestamp, tzinfo=None):\n    \"\"\"Parse an ISO 8601 timestamp string.\n\n    For naive/unaware datetime objects, assume the local timezone.\n    Convert to the tzinfo timezone (the default None means: local timezone).\n    \"\"\"\n    dt = datetime.fromisoformat(timestamp)\n    if dt.tzinfo is None:\n        dt = dt.astimezone(tz=tzinfo)\n    return dt\n\n\n_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)\n\n\ndef utcfromtimestampns(ts_ns: int) -> datetime:\n    # similar to datetime.fromtimestamp, but works with ns and avoids floating point.\n    # also, it would avoid an overflow on 32bit platforms with old glibc.\n    return _EPOCH + timedelta(microseconds=ts_ns // 1000)\n\n\ndef timestamp(s):\n    \"\"\"Convert a --timestamp=s argument to a datetime object.\"\"\"\n    if isinstance(s, datetime):\n        return s\n    try:\n        # is it pointing to a file / directory?\n        ts_ns = safe_ns(os.stat(s).st_mtime_ns)\n        return utcfromtimestampns(ts_ns)\n    except OSError:\n        # didn't work, try parsing as an ISO timestamp. if no TZ is given, we assume local timezone.\n        return parse_local_timestamp(s)\n\n\n# Not too rarely, we get crappy timestamps from the fs, that overflow some computations.\n# As they are crap anyway (valid filesystem timestamps always refer to the past up to\n# the present, but never to the future), nothing is lost if we just clamp them to the\n# maximum value we can support.\n# As long as people are using borg on 32bit platforms to access borg archives, we must\n# keep this value True. But we can expect that we can stop supporting 32bit platforms\n# well before coming close to the year 2038, so this will never be a practical problem.\nSUPPORT_32BIT_PLATFORMS = False  # set this to False before y2038.\n\nif SUPPORT_32BIT_PLATFORMS:\n    # second timestamps will fit into a signed int32 (platform time_t limit).\n    # nanosecond timestamps thus will naturally fit into a signed int64.\n    # subtract last 48h to avoid any issues that could be caused by tz calculations.\n    # this is in the year 2038, so it is also less than y9999 (which is a datetime internal limit).\n    # msgpack can pack up to uint64.\n    MAX_S = 2**31 - 1 - 48 * 3600\n    MAX_NS = MAX_S * 1000000000\nelse:\n    # nanosecond timestamps will fit into a signed int64.\n    # subtract last 48h to avoid any issues that could be caused by tz calculations.\n    # this is in the year 2262, so it is also less than y9999 (which is a datetime internal limit).\n    # round down to 1e9 multiple, so MAX_NS corresponds precisely to a integer MAX_S.\n    # msgpack can pack up to uint64.\n    MAX_NS = (2**63 - 1 - 48 * 3600 * 1000000000) // 1000000000 * 1000000000\n    MAX_S = MAX_NS // 1000000000\n\n\ndef safe_s(ts):\n    if 0 <= ts <= MAX_S:\n        return ts\n    elif ts < 0:\n        return 0\n    else:\n        return MAX_S\n\n\ndef safe_ns(ts):\n    if 0 <= ts <= MAX_NS:\n        return ts\n    elif ts < 0:\n        return 0\n    else:\n        return MAX_NS\n\n\ndef safe_timestamp(item_timestamp_ns):\n    t_ns = safe_ns(item_timestamp_ns)\n    return utcfromtimestampns(t_ns)  # return tz-aware utc datetime obj\n\n\ndef format_time(ts: datetime, format_spec=\"\"):\n    \"\"\"\n    Convert *ts* to a human-friendly format with textual weekday (in local timezone).\n    \"\"\"\n    return ts.astimezone().strftime(\"%a, %Y-%m-%d %H:%M:%S %z\" if format_spec == \"\" else format_spec)\n\n\ndef format_timedelta(td):\n    \"\"\"Format a timedelta in a human-friendly format.\"\"\"\n    ts = td.total_seconds()\n    s = ts % 60\n    m = int(ts / 60) % 60\n    h = int(ts / 3600) % 24\n    txt = \"%.3f seconds\" % s\n    if m:\n        txt = \"%d minutes %s\" % (m, txt)\n    if h:\n        txt = \"%d hours %s\" % (h, txt)\n    if td.days:\n        txt = \"%d days %s\" % (td.days, txt)\n    return txt\n\n\ndef calculate_relative_offset(format_string, from_ts, earlier=False):\n    \"\"\"\n    Calculate an offset based on a relative marker (e.g., 7d for 7 days, 8m for 8 months).\n\n    earlier indicates whether the offset should be applied towards an earlier time.\n    \"\"\"\n    if from_ts is None:\n        from_ts = archive_ts_now()\n\n    if format_string is not None:\n        offset_regex = re.compile(r\"(?P<offset>\\d+)(?P<unit>[ymwdHMS])\")\n        match = offset_regex.search(format_string)\n\n        if match:\n            unit = match.group(\"unit\")\n            offset = int(match.group(\"offset\"))\n            offset *= -1 if earlier else 1\n\n            if unit == \"y\":\n                return from_ts.replace(year=from_ts.year + offset)\n            elif unit == \"m\":\n                return offset_n_months(from_ts, offset)\n            elif unit == \"w\":\n                return from_ts + timedelta(days=offset * 7)\n            elif unit == \"d\":\n                return from_ts + timedelta(days=offset)\n            elif unit == \"H\":\n                return from_ts + timedelta(seconds=offset * 60 * 60)\n            elif unit == \"M\":\n                return from_ts + timedelta(seconds=offset * 60)\n            elif unit == \"S\":\n                return from_ts + timedelta(seconds=offset)\n\n    raise ValueError(f\"Invalid relative ts offset format: {format_string}\")\n\n\ndef offset_n_months(from_ts, n_months):\n    def get_month_and_year_from_total(total_completed_months):\n        month = (total_completed_months % 12) + 1\n        year = total_completed_months // 12\n        return month, year\n\n    # Calculate target month and year by getting completed total_months until target_month\n    total_months = (from_ts.year * 12) + from_ts.month + n_months - 1\n    target_month, target_year = get_month_and_year_from_total(total_months)\n\n    # calculate the max days of the target month by subtracting a day from the next month\n    following_month, year_of_following_month = get_month_and_year_from_total(total_months + 1)\n    max_days_in_month = (datetime(year_of_following_month, following_month, 1) - timedelta(1)).day\n\n    return datetime(day=min(from_ts.day, max_days_in_month), month=target_month, year=target_year).replace(\n        tzinfo=from_ts.tzinfo\n    )\n\n\nclass OutputTimestamp:\n    def __init__(self, ts: datetime):\n        self.ts = ts\n\n    def __format__(self, format_spec):\n        # we want to output a timestamp in the user's local timezone\n        return format_time(self.ts.astimezone(), format_spec=format_spec)\n\n    def __str__(self):\n        return self.isoformat()\n\n    def isoformat(self):\n        # we want to output a timestamp in the user's local timezone\n        return self.ts.astimezone().isoformat(timespec=\"microseconds\")\n\n    to_json = isoformat\n\n\ndef archive_ts_now():\n    \"\"\"return tz-aware datetime obj for current time for usage as archive timestamp\"\"\"\n    return datetime.now(timezone.utc)  # utc time / utc timezone\n"
  },
  {
    "path": "src/borg/helpers/yes_no.py",
    "content": "import logging\nimport json\nimport os\nimport os.path\nimport sys\n\nFALSISH = (\"No\", \"NO\", \"no\", \"N\", \"n\", \"0\")\nTRUISH = (\"Yes\", \"YES\", \"yes\", \"Y\", \"y\", \"1\")\nDEFAULTISH = (\"Default\", \"DEFAULT\", \"default\", \"D\", \"d\", \"\")\n\nERROR = \"error\"\nassert ERROR not in TRUISH + FALSISH + DEFAULTISH\n\n\ndef yes(\n    msg=None,\n    false_msg=None,\n    true_msg=None,\n    default_msg=None,\n    retry_msg=None,\n    invalid_msg=None,\n    env_msg=\"{} (from {})\",\n    falsish=FALSISH,\n    truish=TRUISH,\n    defaultish=DEFAULTISH,\n    default=False,\n    retry=True,\n    env_var_override=None,\n    ofile=None,\n    input=input,\n    prompt=True,\n    msgid=None,\n):\n    \"\"\"Output msg (usually a question) and let the user input an answer.\n    Classify the answer according to falsish, truish, and defaultish as True, False, or the default.\n    If it did not qualify and retry is False (no retries wanted), return the default (which\n    defaults to False). If retry is True, let the user retry answering until the answer is qualified.\n\n    If env_var_override is given and this variable is present in the environment, do not ask\n    the user, but use the environment variable's contents as the answer as if it was typed in.\n    Otherwise, read input from stdin and proceed as normal.\n    If EOF is received instead of input, or an invalid input without a retry possibility,\n    return the default.\n\n    :param msg: introductory message to output on ofile; no \\n is added [None]\n    :param retry_msg: retry message to output on ofile; no \\n is added [None]\n    :param false_msg: message to output before returning False [None]\n    :param true_msg: message to output before returning True [None]\n    :param default_msg: message to output before returning the default [None]\n    :param invalid_msg: message to output after an invalid answer is given [None]\n    :param env_msg: message to output when using input from env_var_override ['{} (from {})'],\n           must have two placeholders for the answer and the environment variable name\n    :param falsish: sequence of answers qualifying as False\n    :param truish: sequence of answers qualifying as True\n    :param defaultish: sequence of answers qualifying as the default\n    :param default: default return value (defaultish answer was given or no-answer condition) [False]\n    :param retry: if True and input is incorrect, retry; otherwise return the default [True]\n    :param env_var_override: environment variable name [None]\n    :param ofile: output stream [sys.stderr]\n    :param input: input function [input from builtins]\n    :return: boolean answer value, True or False\n    \"\"\"\n\n    def output(msg, msg_type, is_prompt=False, **kwargs):\n        json_output = getattr(logging.getLogger(\"borg\"), \"json\", False)\n        if json_output:\n            kwargs |= dict(type=\"question_%s\" % msg_type, msgid=msgid, message=msg)\n            print(json.dumps(kwargs), file=sys.stderr)\n        else:\n            if is_prompt:\n                print(msg, file=ofile, end=\"\", flush=True)\n            else:\n                print(msg, file=ofile)\n\n    msgid = msgid or env_var_override\n    # note: we do not assign sys.stderr as the default above, so it is\n    # really evaluated NOW, not at function definition time.\n    if ofile is None:\n        ofile = sys.stderr\n    if default not in (True, False):\n        raise ValueError(\"invalid default value, must be True or False\")\n    if msg:\n        output(msg, \"prompt\", is_prompt=True)\n    while True:\n        answer = None\n        if env_var_override:\n            answer = os.environ.get(env_var_override)\n            if answer is not None and env_msg:\n                output(env_msg.format(answer, env_var_override), \"env_answer\", env_var=env_var_override)\n        if answer is None:\n            if not prompt:\n                return default\n            try:\n                answer = input()  # this may raise UnicodeDecodeError, #6984\n                if answer == ERROR:  # for testing purposes\n                    raise UnicodeDecodeError(\"?\", b\"?\", 0, 1, \"?\")  # args don't matter\n            except EOFError:\n                # avoid defaultish[0], defaultish could be empty\n                answer = truish[0] if default else falsish[0]\n            except UnicodeDecodeError:\n                answer = ERROR\n        if answer in defaultish:\n            if default_msg:\n                output(default_msg, \"accepted_default\")\n            return default\n        if answer in truish:\n            if true_msg:\n                output(true_msg, \"accepted_true\")\n            return True\n        if answer in falsish:\n            if false_msg:\n                output(false_msg, \"accepted_false\")\n            return False\n        # if we get here, the answer was invalid\n        if invalid_msg:\n            output(invalid_msg, \"invalid_answer\")\n        if not retry:\n            return default\n        if retry_msg:\n            output(retry_msg, \"prompt_retry\", is_prompt=True)\n        # in case we used an environment variable and it gave an invalid answer, do not use it again:\n        env_var_override = None\n"
  },
  {
    "path": "src/borg/hlfuse.py",
    "content": "import datetime\nimport errno\nimport hashlib\nimport os\nimport stat\nimport time\nfrom collections import Counter\nfrom typing import TYPE_CHECKING\n\nfrom .constants import ROBJ_FILE_STREAM, zeros, ROBJ_DONTCARE\n\nif TYPE_CHECKING:\n    # For type checking, assume mfusepy is available\n    # This allows mypy to understand hlfuse.Operations\n    import mfusepy as hlfuse\n    from .fuse_impl import ENOATTR\nelse:\n    from .fuse_impl import hlfuse, ENOATTR\n\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfrom .archiver._common import build_matcher, build_filter\nfrom .archive import Archive, get_item_uid_gid\nfrom .hashindex import FuseVersionsIndex\nfrom .helpers import daemonize, daemonizing, signal_handler, bin_to_hex\nfrom .helpers import HardLinkManager\nfrom .helpers import msgpack\nfrom .helpers.lrucache import LRUCache\nfrom .item import Item\nfrom .platform import uid2user, gid2group\nfrom .platformflags import is_darwin\nfrom .repository import Repository\nfrom .remote import RemoteRepository\n\nBLOCK_SIZE = 512  # Standard filesystem block size for st_blocks and statfs\nDEBUG_LOG: str | None = None  # os.path.join(os.getcwd(), \"fuse_debug.log\")\n\n\ndef debug_log(msg):\n    \"\"\"Append debug message to fuse_debug.log\"\"\"\n    if DEBUG_LOG:\n        timestamp = datetime.datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n        with open(DEBUG_LOG, \"a\") as f:\n            f.write(f\"{timestamp} {msg}\\n\")\n\n\nclass DirEntry:\n    __slots__ = (\"ino\", \"parent\", \"children\")\n\n    def __init__(self, ino, parent=None):\n        self.ino = ino  # inode number\n        self.parent = parent\n        self.children = None  # name (bytes) -> DirEntry, lazily allocated\n\n    def add_child(self, name, child):\n        \"\"\"Add a child entry, lazily allocating the children dict if needed.\"\"\"\n        if self.children is None:\n            self.children = {}\n        self.children[name] = child\n\n    def get_child(self, name):\n        \"\"\"Get a child entry by name, returns None if not found.\"\"\"\n        if self.children is None:\n            return None\n        return self.children.get(name)\n\n    def has_child(self, name):\n        \"\"\"Check if a child with the given name exists.\"\"\"\n        if self.children is None:\n            return False\n        return name in self.children\n\n    def iter_children(self):\n        \"\"\"Iterate over (name, child) pairs.\"\"\"\n        if self.children is None:\n            return iter([])\n        return self.children.items()\n\n\nclass FuseBackend:\n    \"\"\"Virtual filesystem based on archive(s) to provide information to fuse\"\"\"\n\n    def __init__(self, manifest, args, repository):\n        self._args = args\n        self.numeric_ids = args.numeric_ids\n        self._manifest = manifest\n        self.repo_objs = manifest.repo_objs\n        self.repository = repository\n\n        self.default_uid = os.getuid()\n        self.default_gid = os.getgid()\n        self.default_dir = None\n\n        self.current_ino = 0\n        self.inodes = {}  # node.ino -> packed item\n        self.root = self._create_node()\n        self.pending_archives = {}  # DirEntry -> Archive\n\n        self.allow_damaged_files = False\n        self.versions = False\n        self.uid_forced = None\n        self.gid_forced = None\n        self.umask = 0\n        self.archive_root_dir = {}  # archive ID --> directory name\n\n        # Cache for file handles\n        self.handles = {}\n        self.handle_count = 0\n\n        # Cache for chunks (moved from ItemCache)\n        self.chunks_cache = LRUCache(capacity=10)\n\n    def _create_node(self, item=None, parent=None):\n        self.current_ino += 1\n        if item is not None:\n            self.set_inode(self.current_ino, item)\n        return DirEntry(self.current_ino, parent)\n\n    def get_inode(self, ino):\n        packed = self.inodes.get(ino)\n        if packed is None:\n            return None\n        return Item(internal_dict=msgpack.unpackb(packed))\n\n    def set_inode(self, ino, item):\n        if item is None:\n            self.inodes.pop(ino, None)\n        else:\n            # Remove path from the item dict before packing to save memory.\n            # The path is already encoded in the DirEntry tree structure.\n            item_dict = item.as_dict()\n            item_dict.pop(\"path\", None)\n            self.inodes[ino] = msgpack.packb(item_dict)\n\n    def _create_filesystem(self):\n        self.set_inode(self.root.ino, self.default_dir)\n        self.versions_index = FuseVersionsIndex()\n\n        if getattr(self._args, \"name\", None):\n            archives = [self._manifest.archives.get(self._args.name)]\n        else:\n            archives = self._manifest.archives.list_considering(self._args)\n\n        name_counter = Counter(a.name for a in archives)\n        duplicate_names = {a.name for a in archives if name_counter[a.name] > 1}\n\n        for archive in archives:\n            name = f\"{archive.name}\"\n            if name in duplicate_names:\n                name += f\"-{bin_to_hex(archive.id):.8}\"\n            self.archive_root_dir[archive.id] = name\n\n        for archive in archives:\n            if self.versions:\n                self._process_archive(archive.id)\n            else:\n                # Create placeholder for archive\n                name = self.archive_root_dir[archive.id]\n                name_bytes = os.fsencode(name)\n\n                archive_node = self._create_node(parent=self.root)\n                # Create a directory item for the archive\n                item = Item(internal_dict=self.default_dir.as_dict())\n                item.mtime = int(archive.ts.timestamp() * 1e9)\n                self.set_inode(archive_node.ino, item)\n\n                self.root.add_child(name_bytes, archive_node)\n                self.pending_archives[archive_node] = archive\n\n    def check_pending_archive(self, node):\n        archive_info = self.pending_archives.pop(node, None)\n        if archive_info is not None:\n            self._process_archive(archive_info.id, node)\n\n    def _iter_archive_items(self, archive_item_ids, filter=None):\n        unpacker = msgpack.Unpacker()\n        for id, cdata in zip(archive_item_ids, self.repository.get_many(archive_item_ids)):\n            _, data = self.repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE)\n            unpacker.feed(data)\n            for item in unpacker:\n                item = Item(internal_dict=item)\n                if filter and not filter(item):\n                    continue\n                yield item\n\n    def _process_archive(self, archive_id, root_node=None):\n        if root_node is None:\n            root_node = self.root\n\n        self.file_versions = {}  # for versions mode: original path -> version\n\n        archive = Archive(self._manifest, archive_id)\n        strip_components = self._args.strip_components\n        # omitting args.pattern_roots here, restricting to paths only by cli args.paths:\n        matcher = build_matcher(self._args.patterns, self._args.paths)\n        hlm = HardLinkManager(id_type=bytes, info_type=str)\n\n        filter = build_filter(matcher, strip_components)\n\n        for item in self._iter_archive_items(archive.metadata.items, filter=filter):\n            if strip_components:\n                item.path = os.sep.join(item.path.split(os.sep)[strip_components:])\n\n            path = os.fsencode(item.path)\n            segments = path.split(b\"/\")\n            is_dir = stat.S_ISDIR(item.mode)\n\n            # For versions mode, handle files differently\n            if self.versions and not is_dir:\n                self._process_leaf_versioned(segments, item, root_node, hlm)\n            else:\n                # Original non-versions logic\n                node = root_node\n                # Traverse/Create directories\n                for segment in segments[:-1]:\n                    if not node.has_child(segment):\n                        new_node = self._create_node(parent=node)\n                        # We might need a default directory item if it's an implicit directory\n                        self.set_inode(new_node.ino, Item(internal_dict=self.default_dir.as_dict()))\n                        node.add_child(segment, new_node)\n                    node = node.get_child(segment)\n\n                # Leaf (file or explicit directory)\n                leaf_name = segments[-1]\n                if node.has_child(leaf_name):\n                    # Already exists (e.g. implicit dir became explicit)\n                    child = node.get_child(leaf_name)\n                    self.set_inode(child.ino, item)  # Update item\n                    node = child\n                else:\n                    new_node = self._create_node(item, parent=node)\n                    node.add_child(leaf_name, new_node)\n                    node = new_node\n\n                # Handle hardlinks (non-versions mode)\n                if \"hlid\" in item:\n                    link_target = hlm.retrieve(id=item.hlid, default=None)\n                    if link_target is not None:\n                        target_path = os.fsencode(link_target)\n                        target_node = self._find_node_from_root(root_node, target_path)\n                        if target_node:\n                            # Reuse ino and item from target\n                            node.ino = target_node.ino\n                            # node.item = target_node.item  # implicitly shared via ID\n                            item = self.get_inode(node.ino)\n                            if \"nlink\" not in item:\n                                item.nlink = 1\n                            item.nlink += 1\n                            self.set_inode(node.ino, item)\n                        else:\n                            logger.warning(\"Hardlink target not found: %s\", link_target)\n                    else:\n                        hlm.remember(id=item.hlid, info=item.path)\n\n    def _process_leaf_versioned(self, segments, item, root_node, hlm):\n        \"\"\"Process a file leaf node in versions mode\"\"\"\n        path = b\"/\".join(segments)\n        original_path = item.path\n\n        # Handle hardlinks in versions mode - check if we've seen this hardlink before\n        is_hardlink = \"hlid\" in item\n        link_target = None\n        if is_hardlink:\n            link_target = hlm.retrieve(id=item.hlid, default=None)\n            if link_target is None:\n                # First occurrence of this hardlink\n                hlm.remember(id=item.hlid, info=original_path)\n\n        # Calculate version for this file\n        # If it's a hardlink to a previous file, use that version\n        if is_hardlink and link_target is not None:\n            link_target_enc = os.fsencode(link_target)\n            version = self.file_versions.get(link_target_enc)\n        else:\n            version = self._file_version(item, path)\n\n        # Store version for this path\n        if version is not None:\n            self.file_versions[path] = version\n\n        # Navigate to parent directory\n        node = root_node\n        for segment in segments[:-1]:\n            if not node.has_child(segment):\n                new_node = self._create_node(parent=node)\n                self.set_inode(new_node.ino, Item(internal_dict=self.default_dir.as_dict()))\n                node.add_child(segment, new_node)\n            node = node.get_child(segment)\n\n        # Create intermediate directory with the filename\n        leaf_name = segments[-1]\n        if not node.has_child(leaf_name):\n            intermediate_node = self._create_node(parent=node)\n            self.set_inode(intermediate_node.ino, Item(internal_dict=self.default_dir.as_dict()))\n            node.add_child(leaf_name, intermediate_node)\n        else:\n            intermediate_node = node.get_child(leaf_name)\n\n        # Create versioned filename\n        if version is not None:\n            versioned_name = self._make_versioned_name(leaf_name, version)\n\n            # If this is a hardlink to a previous file, reuse that node\n            if is_hardlink and link_target is not None:\n                link_target_enc = os.fsencode(link_target)\n                link_segments = link_target_enc.split(b\"/\")\n                link_version = self.file_versions.get(link_target_enc)\n                if link_version is not None:\n                    # Navigate to the link target\n                    target_node = root_node\n                    for seg in link_segments[:-1]:\n                        if target_node.has_child(seg):\n                            target_node = target_node.get_child(seg)\n                        else:\n                            break\n                    else:\n                        # Get intermediate dir\n                        link_leaf = link_segments[-1]\n                        if target_node.has_child(link_leaf):\n                            target_intermediate = target_node.get_child(link_leaf)\n                            target_versioned = self._make_versioned_name(link_leaf, link_version)\n                            if target_intermediate.has_child(target_versioned):\n                                original_node = target_intermediate.get_child(target_versioned)\n                                # Create new node but reuse the ino and item from original\n                                item = self.get_inode(original_node.ino)\n                                file_node = self._create_node(item, parent=intermediate_node)\n                                file_node.ino = original_node.ino\n                                # Update nlink count\n                                item = self.get_inode(file_node.ino)\n                                if \"nlink\" not in item:\n                                    item.nlink = 1\n                                item.nlink += 1\n                                self.set_inode(file_node.ino, item)\n                                intermediate_node.add_child(versioned_name, file_node)\n                                return\n\n            # Not a hardlink or first occurrence - create new node\n            file_node = self._create_node(item, parent=intermediate_node)\n            intermediate_node.add_child(versioned_name, file_node)\n\n    def _file_version(self, item, path):\n        \"\"\"Calculate version number for a file based on its contents\"\"\"\n        if \"chunks\" not in item:\n            return None\n\n        # note: using sha256 here because nowadays it is often hw accelerated.\n        #       shortening the hashes to 16 bytes to save some memory.\n        file_id = hashlib.sha256(path).digest()[:16]\n        current_version, previous_id = self.versions_index.get(file_id, (0, None))\n\n        contents_id = hashlib.sha256(b\"\".join(chunk_id for chunk_id, _ in item.chunks)).digest()[:16]\n\n        if contents_id != previous_id:\n            current_version += 1\n            self.versions_index[file_id] = current_version, contents_id\n\n        return current_version\n\n    def _make_versioned_name(self, name, version):\n        \"\"\"Generate versioned filename like 'file.00001.txt'\"\"\"\n        # keep original extension at end to avoid confusing tools\n        name_str = name.decode(\"utf-8\", \"surrogateescape\") if isinstance(name, bytes) else name\n        name_part, ext = os.path.splitext(name_str)\n        version_str = \".%05d\" % version\n        versioned = name_part + version_str + ext\n        return versioned.encode(\"utf-8\", \"surrogateescape\") if isinstance(name, bytes) else versioned\n\n    def _find_node_from_root(self, root, path):\n        if path == b\"\" or path == b\".\":\n            return root\n        segments = path.split(b\"/\")\n        node = root\n        for segment in segments:\n            child = node.get_child(segment)\n            if child is not None:\n                node = child\n            else:\n                return None\n        return node\n\n    def _find_node(self, path):\n        if isinstance(path, str):\n            path = os.fsencode(path)\n        if path == b\"/\" or path == b\"\":\n            return self.root\n        if path.startswith(b\"/\"):\n            path = path[1:]\n\n        segments = path.split(b\"/\")\n        node = self.root\n        for segment in segments:\n            if node in self.pending_archives:\n                self.check_pending_archive(node)\n            child = node.get_child(segment)\n            if child is not None:\n                node = child\n            else:\n                return None\n\n        if node in self.pending_archives:\n            self.check_pending_archive(node)\n\n        return node\n\n    def _get_handle(self, node):\n        self.handle_count += 1\n        self.handles[self.handle_count] = node\n        return self.handle_count\n\n    def _get_node_from_handle(self, fh):\n        return self.handles.get(fh)\n\n    def _make_stat_dict(self, node):\n        \"\"\"Create a stat dictionary from a node.\"\"\"\n        item = self.get_inode(node.ino)\n        st = {}\n        st[\"st_ino\"] = node.ino\n        st[\"st_mode\"] = item.mode & ~self.umask\n        st[\"st_nlink\"] = item.get(\"nlink\", 1)\n        if stat.S_ISDIR(st[\"st_mode\"]):\n            st[\"st_nlink\"] = max(st[\"st_nlink\"], 2)\n        st[\"st_uid\"], st[\"st_gid\"] = get_item_uid_gid(\n            item,\n            numeric=self.numeric_ids,\n            uid_default=self.default_uid,\n            gid_default=self.default_gid,\n            uid_forced=self.uid_forced,\n            gid_forced=self.gid_forced,\n        )\n        st[\"st_rdev\"] = item.get(\"rdev\", 0)\n        st[\"st_size\"] = item.get_size()\n        st[\"st_blocks\"] = (st[\"st_size\"] + BLOCK_SIZE - 1) // BLOCK_SIZE\n        if getattr(self, \"use_ns\", False):\n            st[\"st_mtime\"] = item.mtime\n            st[\"st_atime\"] = item.get(\"atime\", item.mtime)\n            st[\"st_ctime\"] = item.get(\"ctime\", item.mtime)\n        else:\n            st[\"st_mtime\"] = item.mtime / 1e9\n            st[\"st_atime\"] = item.get(\"atime\", item.mtime) / 1e9\n            st[\"st_ctime\"] = item.get(\"ctime\", item.mtime) / 1e9\n        return st\n\n\nclass borgfs(hlfuse.Operations, FuseBackend):\n    \"\"\"Export archive as a FUSE filesystem\"\"\"\n\n    use_ns = True\n\n    def __init__(self, manifest, args, repository):\n        hlfuse.Operations.__init__(self)\n        FuseBackend.__init__(self, manifest, args, repository)\n        data_cache_capacity = int(os.environ.get(\"BORG_MOUNT_DATA_CACHE_ENTRIES\", os.cpu_count() or 1))\n        logger.debug(\"mount data cache capacity: %d chunks\", data_cache_capacity)\n        self.data_cache = LRUCache(capacity=data_cache_capacity)\n        self._last_pos = LRUCache(capacity=4)\n\n    def sig_info_handler(self, sig_no, stack):\n        # Simplified instrumentation\n        logger.debug(\"fuse: %d inodes\", self.current_ino)\n\n    def mount(self, mountpoint, mount_options, foreground=False, show_rc=False):\n        \"\"\"Mount filesystem on *mountpoint* with *mount_options*.\"\"\"\n\n        def pop_option(options, key, present, not_present, wanted_type, int_base=0):\n            assert isinstance(options, list)  # we mutate this\n            for idx, option in enumerate(options):\n                if option == key:\n                    options.pop(idx)\n                    return present\n                if option.startswith(key + \"=\"):\n                    options.pop(idx)\n                    value = option.split(\"=\", 1)[1]\n                    if wanted_type is bool:\n                        v = value.lower()\n                        if v in (\"y\", \"yes\", \"true\", \"1\"):\n                            return True\n                        if v in (\"n\", \"no\", \"false\", \"0\"):\n                            return False\n                        raise ValueError(\"unsupported value in option: %s\" % option)\n                    if wanted_type is int:\n                        try:\n                            return int(value, base=int_base)\n                        except ValueError:\n                            raise ValueError(\"unsupported value in option: %s\" % option) from None\n                    try:\n                        return wanted_type(value)\n                    except ValueError:\n                        raise ValueError(\"unsupported value in option: %s\" % option) from None\n            else:\n                return not_present\n\n        options = [\"fsname=borgfs\", \"ro\", \"default_permissions\"]\n        if mount_options:\n            options.extend(mount_options.split(\",\"))\n        if is_darwin:\n            volname = pop_option(options, \"volname\", \"\", \"\", str)\n            volname = volname or f\"{os.path.basename(mountpoint)} (borgfs)\"\n            options.append(f\"volname={volname}\")\n        ignore_permissions = pop_option(options, \"ignore_permissions\", True, False, bool)\n        if ignore_permissions:\n            pop_option(options, \"default_permissions\", True, False, bool)\n        self.allow_damaged_files = pop_option(options, \"allow_damaged_files\", True, False, bool)\n        self.versions = pop_option(options, \"versions\", True, False, bool)\n        self.uid_forced = pop_option(options, \"uid\", None, None, int)\n        self.gid_forced = pop_option(options, \"gid\", None, None, int)\n        self.umask = pop_option(options, \"umask\", 0, 0, int, int_base=8)\n        dir_uid = self.uid_forced if self.uid_forced is not None else self.default_uid\n        dir_gid = self.gid_forced if self.gid_forced is not None else self.default_gid\n        dir_user = uid2user(dir_uid)\n        dir_group = gid2group(dir_gid)\n        assert isinstance(dir_user, str)\n        assert isinstance(dir_group, str)\n        dir_mode = 0o40755 & ~self.umask\n        self.default_dir = Item(\n            mode=dir_mode, mtime=int(time.time() * 1e9), user=dir_user, group=dir_group, uid=dir_uid, gid=dir_gid\n        )\n        self._create_filesystem()\n\n        # hlfuse.FUSE will block if foreground=True, otherwise it returns immediately\n        if not foreground:\n            # Background mode: daemonize first, then start FUSE (blocking)\n            if isinstance(self.repository, RemoteRepository):\n                daemonize()\n            else:\n                with daemonizing(show_rc=show_rc) as (old_id, new_id):\n                    logger.debug(\"fuse: mount local repo, going to background: migrating lock.\")\n                    self.repository.migrate_lock(old_id, new_id)\n\n        # Run the FUSE main loop in foreground (we might be daemonized already or not)\n        with signal_handler(\"SIGUSR1\", self.sig_info_handler), signal_handler(\"SIGINFO\", self.sig_info_handler):\n            hlfuse.FUSE(self, mountpoint, options, foreground=True, use_ino=True)\n\n    def statfs(self, path):\n        debug_log(f\"statfs(path={path!r})\")\n        stat_ = {}\n        stat_[\"f_bsize\"] = BLOCK_SIZE\n        stat_[\"f_frsize\"] = BLOCK_SIZE\n        stat_[\"f_blocks\"] = 0\n        stat_[\"f_bfree\"] = 0\n        stat_[\"f_bavail\"] = 0\n        stat_[\"f_files\"] = 0\n        stat_[\"f_ffree\"] = 0\n        stat_[\"f_favail\"] = 0\n        stat_[\"f_namemax\"] = 255\n        debug_log(f\"statfs -> {stat_}\")\n        return stat_\n\n    def getattr(self, path, fh=None):\n        debug_log(f\"getattr(path={path!r}, fh={fh})\")\n        if fh is not None:\n            # use file handle if available to avoid path lookup\n            node = self._get_node_from_handle(fh)\n            if node is None:\n                raise hlfuse.FuseOSError(errno.EBADF)\n        else:\n            node = self._find_node(path)\n            if node is None:\n                raise hlfuse.FuseOSError(errno.ENOENT)\n        st = self._make_stat_dict(node)\n        debug_log(f\"getattr -> {st}\")\n        return st\n\n    def listxattr(self, path):\n        debug_log(f\"listxattr(path={path!r})\")\n        node = self._find_node(path)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.ENOENT)\n        item = self.get_inode(node.ino)\n        result = [k.decode(\"utf-8\", \"surrogateescape\") for k in item.get(\"xattrs\", {}).keys()]\n        debug_log(f\"listxattr -> {result}\")\n        return result\n\n    def getxattr(self, path, name, position=0):\n        debug_log(f\"getxattr(path={path!r}, name={name!r}, position={position})\")\n        node = self._find_node(path)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.ENOENT)\n        item = self.get_inode(node.ino)\n        try:\n            if isinstance(name, str):\n                name = name.encode(\"utf-8\", \"surrogateescape\")\n            result = item.get(\"xattrs\", {})[name] or b\"\"\n            debug_log(f\"getxattr -> {len(result)} bytes\")\n            return result\n        except KeyError:\n            debug_log(\"getxattr -> ENOATTR\")\n            raise hlfuse.FuseOSError(ENOATTR) from None\n\n    def open(self, path, fi):\n        debug_log(f\"open(path={path!r}, fi={fi})\")\n        node = self._find_node(path)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.ENOENT)\n        fh = self._get_handle(node)\n        fi.fh = fh\n        debug_log(f\"open -> fh={fh}\")\n        return 0\n\n    def release(self, path, fi):\n        debug_log(f\"release(path={path!r}, fh={fi.fh})\")\n        self.handles.pop(fi.fh, None)\n        self._last_pos.pop(fi.fh, None)\n        return 0\n\n    def create(self, path, mode, fi=None):\n        debug_log(f\"create(path={path!r}, mode={mode}, fi={fi}) -> EROFS\")\n        raise hlfuse.FuseOSError(errno.EROFS)\n\n    def read(self, path, size, offset, fi):\n        fh = fi.fh\n        debug_log(f\"read(path={path!r}, size={size}, offset={offset}, fh={fh})\")\n        node = self._get_node_from_handle(fh)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.EBADF)\n\n        item = self.get_inode(node.ino)\n        parts = []\n\n        # optimize for linear reads:\n        chunk_no, chunk_offset = self._last_pos.get(fh, (0, 0))\n        if chunk_offset > offset:\n            chunk_no, chunk_offset = (0, 0)\n\n        offset -= chunk_offset\n        chunks = item.chunks\n\n        for idx in range(chunk_no, len(chunks)):\n            id, s = chunks[idx]\n            if s < offset:\n                offset -= s\n                chunk_offset += s\n                chunk_no += 1\n                continue\n            n = min(size, s - offset)\n            if id in self.data_cache:\n                data = self.data_cache[id]\n                if offset + n == len(data):\n                    del self.data_cache[id]\n            else:\n                try:\n                    # Direct repository access\n                    cdata = self.repository.get(id)\n                except Repository.ObjectNotFound:\n                    if self.allow_damaged_files:\n                        data = zeros[:s]\n                        assert len(data) == s\n                    else:\n                        raise hlfuse.FuseOSError(errno.EIO) from None\n                else:\n                    _, data = self.repo_objs.parse(id, cdata, ro_type=ROBJ_FILE_STREAM)\n                if offset + n < len(data):\n                    self.data_cache[id] = data\n            parts.append(data[offset : offset + n])\n            offset = 0\n            size -= n\n            if not size:\n                if fh in self._last_pos:\n                    self._last_pos.replace(fh, (chunk_no, chunk_offset))\n                else:\n                    self._last_pos[fh] = (chunk_no, chunk_offset)\n                break\n        result = b\"\".join(parts)\n        debug_log(f\"read -> {len(result)} bytes\")\n        return result\n\n    def readdir(self, path, fh=None):\n        debug_log(f\"readdir(path={path!r}, fh={fh})\")\n        node = self._find_node(path)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.ENOENT)\n\n        offset = 0\n        offset += 0  # += 1\n        debug_log(f\"readdir yielding . {offset}\")\n        yield (\".\", self._make_stat_dict(node), offset)\n        offset += 0  # += 1\n        debug_log(f\"readdir yielding .. {offset}\")\n        parent = node.parent if node.parent else node\n        yield (\"..\", self._make_stat_dict(parent), offset)\n\n        for name, child_node in node.iter_children():\n            name_str = name.decode(\"utf-8\", \"surrogateescape\")\n            st = self._make_stat_dict(child_node)\n            offset += 0  # += 1\n            debug_log(f\"readdir yielding {name_str} {offset} {st}\")\n            yield (name_str, st, offset)\n\n    def readlink(self, path):\n        debug_log(f\"readlink(path={path!r})\")\n        node = self._find_node(path)\n        if node is None:\n            raise hlfuse.FuseOSError(errno.ENOENT)\n        item = self.get_inode(node.ino)\n        result = item.target\n        debug_log(f\"readlink -> {result!r}\")\n        return result\n"
  },
  {
    "path": "src/borg/item.pyi",
    "content": "from typing import Set, NamedTuple, Tuple, Mapping, Dict, List, Iterator, Callable, Any\n\nfrom .helpers import StableDict\n\ndef want_bytes(v: Any, *, errors: str = ...) -> bytes: ...\ndef chunks_contents_equal(chunks1: Iterator, chunks2: Iterator) -> bool: ...\n\nclass PropDict:\n    VALID_KEYS: Set[str] = ...\n    def __init__(self, data_dict: dict = None, internal_dict: dict = None, **kw) -> None: ...\n    def as_dict(self) -> StableDict: ...\n    def get(self, key: str, default: Any = None) -> Any: ...\n    def update(self, d: dict) -> None: ...\n    def update_internal(self, d: dict) -> None: ...\n    def __contains__(self, key: str) -> bool: ...\n    def __eq__(self, other: object) -> bool: ...\n\nclass ArchiveItem(PropDict):\n    @property\n    def version(self) -> int: ...\n    @version.setter\n    def version(self, val: int) -> None: ...\n    @property\n    def name(self) -> str: ...\n    @name.setter\n    def name(self, val: str) -> None: ...\n    @property\n    def start(self) -> str: ...\n    @start.setter\n    def start(self, val: str) -> None: ...\n    @property\n    def end(self) -> str: ...\n    @end.setter\n    def end(self, val: str) -> None: ...\n    @property\n    def time(self) -> str: ...\n    @time.setter\n    def time(self, val: str) -> None: ...\n    @property\n    def time_end(self) -> str: ...\n    @time_end.setter\n    def time_end(self, val: str) -> None: ...\n    @property\n    def username(self) -> str: ...\n    @username.setter\n    def username(self, val: str) -> None: ...\n    @property\n    def hostname(self) -> str: ...\n    @hostname.setter\n    def hostname(self, val: str) -> None: ...\n    @property\n    def comment(self) -> str: ...\n    @comment.setter\n    def comment(self, val: str) -> None: ...\n    @property\n    def tags(self) -> List[str]: ...\n    @tags.setter\n    def tags(self, val: List[str]) -> None: ...\n    @property\n    def chunker_params(self) -> Tuple: ...\n    @chunker_params.setter\n    def chunker_params(self, val: Tuple) -> None: ...\n    @property\n    def cmdline(self) -> List[str]: ...\n    @cmdline.setter\n    def cmdline(self, val: List[str]) -> None: ...\n    @property\n    def recreate_cmdline(self) -> List[str]: ...\n    @recreate_cmdline.setter\n    def recreate_cmdline(self, val: List[str]) -> None: ...\n    @property\n    def recreate_args(self) -> Any: ...\n    @recreate_args.setter\n    def recreate_args(self, val: Any) -> None: ...\n    @property\n    def recreate_partial_chunks(self) -> Any: ...\n    @recreate_partial_chunks.setter\n    def recreate_partial_chunks(self, val: Any) -> None: ...\n    @property\n    def recreate_source_id(self) -> Any: ...\n    @recreate_source_id.setter\n    def recreate_source_id(self, val: Any) -> None: ...\n    @property\n    def nfiles(self) -> int: ...\n    @nfiles.setter\n    def nfiles(self, val: int) -> None: ...\n    @property\n    def nfiles_parts(self) -> int: ...\n    @nfiles_parts.setter\n    def nfiles_parts(self, val: int) -> None: ...\n    @property\n    def size(self) -> int: ...\n    @size.setter\n    def size(self, val: int) -> None: ...\n    @property\n    def size_parts(self) -> int: ...\n    @size_parts.setter\n    def size_parts(self, val: int) -> None: ...\n    @property\n    def csize(self) -> int: ...\n    @csize.setter\n    def csize(self, val: int) -> None: ...\n    @property\n    def items(self) -> List: ...\n    @items.setter\n    def items(self, val: List) -> None: ...\n    @property\n    def item_ptrs(self) -> List: ...\n    @item_ptrs.setter\n    def item_ptrs(self, val: List) -> None: ...\n\nclass ChunkListEntry(NamedTuple):\n    id: bytes\n    size: int\n\nclass Item(PropDict):\n    @property\n    def path(self) -> str: ...\n    @path.setter\n    def path(self, val: str) -> None: ...\n    @property\n    def source(self) -> str: ...\n    @source.setter\n    def source(self, val: str) -> None: ...\n    def is_dir(self) -> bool: ...\n    def is_link(self) -> bool: ...\n    def _is_type(self, typetest: Callable) -> bool: ...\n    @classmethod\n    def create_deleted(self, path) -> Item: ...\n    @classmethod\n    def from_optr(self, optr: Any) -> Item: ...\n    def to_optr(self) -> Any: ...\n    @property\n    def atime(self) -> int: ...\n    @atime.setter\n    def atime(self, val: int) -> None: ...\n    @property\n    def ctime(self) -> int: ...\n    @ctime.setter\n    def ctime(self, val: int) -> None: ...\n    @property\n    def mtime(self) -> int: ...\n    @mtime.setter\n    def mtime(self, val: int) -> None: ...\n    @property\n    def birthtime(self) -> int: ...\n    @birthtime.setter\n    def birthtime(self, val: int) -> None: ...\n    @property\n    def xattrs(self) -> StableDict: ...\n    @xattrs.setter\n    def xattrs(self, val: StableDict) -> None: ...\n    @property\n    def acl_access(self) -> bytes: ...\n    @acl_access.setter\n    def acl_access(self, val: bytes) -> None: ...\n    @property\n    def acl_default(self) -> bytes: ...\n    @acl_default.setter\n    def acl_default(self, val: bytes) -> None: ...\n    @property\n    def acl_extended(self) -> bytes: ...\n    @acl_extended.setter\n    def acl_extended(self, val: bytes) -> None: ...\n    @property\n    def acl_nfs4(self) -> bytes: ...\n    @acl_nfs4.setter\n    def acl_nfs4(self, val: bytes) -> None: ...\n    @property\n    def bsdflags(self) -> int: ...\n    @bsdflags.setter\n    def bsdflags(self, val: int) -> None: ...\n    @property\n    def chunks(self) -> List: ...\n    @chunks.setter\n    def chunks(self, val: List) -> None: ...\n    @property\n    def chunks_healthy(self) -> List: ...\n    @chunks_healthy.setter\n    def chunks_healthy(self, val: List) -> None: ...\n    @property\n    def deleted(self) -> bool: ...\n    @deleted.setter\n    def deleted(self, val: bool) -> None: ...\n    @property\n    def hlid(self) -> bytes: ...\n    @hlid.setter\n    def hlid(self, val: bytes) -> None: ...\n    @property\n    def hardlink_master(self) -> bool: ...\n    @hardlink_master.setter\n    def hardlink_master(self, val: bool) -> None: ...\n    @property\n    def uid(self) -> int: ...\n    @uid.setter\n    def uid(self, val: int) -> None: ...\n    @property\n    def gid(self) -> int: ...\n    @gid.setter\n    def gid(self, val: int) -> None: ...\n    @property\n    def user(self) -> str: ...\n    @user.setter\n    def user(self, val: str) -> None: ...\n    @property\n    def group(self) -> str: ...\n    @group.setter\n    def group(self, val: str) -> None: ...\n    @property\n    def mode(self) -> int: ...\n    @mode.setter\n    def mode(self, val: int) -> None: ...\n    @property\n    def rdev(self) -> int: ...\n    @rdev.setter\n    def rdev(self, val: int) -> None: ...\n    @property\n    def nlink(self) -> int: ...\n    @nlink.setter\n    def nlink(self, val: int) -> None: ...\n    @property\n    def inode(self) -> int: ...\n    @inode.setter\n    def inode(self, val: int) -> None: ...\n    @property\n    def size(self) -> int: ...\n    @size.setter\n    def size(self, val: int) -> None: ...\n    def get_size(\n        self,\n        hardlink_masters=...,\n        memorize: bool = ...,\n        compressed: bool = ...,\n        from_chunks: bool = ...,\n        consider_ids: List[bytes] = ...,\n    ) -> int: ...\n    @property\n    def part(self) -> int: ...\n    @part.setter\n    def part(self, val: int) -> None: ...\n\nclass ManifestItem(PropDict):\n    @property\n    def version(self) -> int: ...\n    @version.setter\n    def version(self, val: int) -> None: ...\n    @property\n    def timestamp(self) -> str: ...\n    @timestamp.setter\n    def timestamp(self, val: str) -> None: ...\n    @property\n    def archives(self) -> Mapping[bytes, dict]: ...\n    @archives.setter\n    def archives(self, val: Mapping[bytes, dict]) -> None: ...\n    @property\n    def config(self) -> Dict: ...\n    @config.setter\n    def config(self, val: Dict) -> None: ...\n    @property\n    def item_keys(self) -> Tuple: ...\n    @item_keys.setter\n    def item_keys(self, val: Tuple) -> None: ...\n\nclass DiffChange:\n    diff_type: str\n    diff_data: Dict[str, Any]\n    def __init__(self, diff_type: str, diff_data: Dict[str, Any] | None = ...) -> None: ...\n    def to_dict(self) -> Dict[str, Any]: ...\n\nclass ItemDiff:\n    path: str\n    _item1: Item\n    _item2: Item\n    _chunk_1: Iterator\n    _chunk_2: Iterator\n    _numeric_ids: bool\n    _can_compare_chunk_ids: bool\n    def __init__(\n        self,\n        path: str,\n        item1: Item,\n        item2: Item,\n        chunk_1: Iterator,\n        chunk_2: Iterator,\n        numeric_ids: bool = ...,\n        can_compare_chunk_ids: bool = ...,\n    ) -> None: ...\n    def changes(self) -> Dict[str, DiffChange]: ...\n    def equal(self, content_only: bool = ...) -> bool: ...\n    def content(self) -> DiffChange | None: ...\n    def ctime(self) -> DiffChange | None: ...\n    def mtime(self) -> DiffChange | None: ...\n    def mode(self) -> DiffChange | None: ...\n    def type(self) -> DiffChange | None: ...\n    def owner(self) -> DiffChange | None: ...\n    def user(self) -> DiffChange | None: ...\n    def group(self) -> DiffChange | None: ...\n\ndef chunk_content_equal(chunks_a: Iterator, chunks_b: Iterator) -> bool: ...\n\nclass Key(PropDict):\n    @property\n    def version(self) -> int: ...\n    @version.setter\n    def version(self, val: int) -> None: ...\n    @property\n    def chunk_seed(self) -> int: ...\n    @chunk_seed.setter\n    def chunk_seed(self, val: int) -> None: ...\n    @property\n    def tam_required(self) -> bool: ...\n    @tam_required.setter\n    def tam_required(self, val: bool) -> None: ...\n    @property\n    def enc_hmac_key(self) -> bytes: ...\n    @enc_hmac_key.setter\n    def enc_hmac_key(self, val: bytes) -> None: ...\n    @property\n    def enc_key(self) -> bytes: ...\n    @enc_key.setter\n    def enc_key(self, val: bytes) -> None: ...\n    @property\n    def id_key(self) -> bytes: ...\n    @id_key.setter\n    def id_key(self, val: bytes) -> None: ...\n    @property\n    def repository_id(self) -> bytes: ...\n    @repository_id.setter\n    def repository_id(self, val: bytes) -> None: ...\n\nclass EncryptedKey(PropDict):\n    @property\n    def version(self) -> int: ...\n    @version.setter\n    def version(self, val: int) -> None: ...\n    @property\n    def algorithm(self) -> str: ...\n    @algorithm.setter\n    def algorithm(self, val: str) -> None: ...\n    @property\n    def salt(self) -> bytes: ...\n    @salt.setter\n    def salt(self, val: bytes) -> None: ...\n    @property\n    def iterations(self) -> int: ...\n    @iterations.setter\n    def iterations(self, val: int) -> None: ...\n    @property\n    def data(self) -> bytes: ...\n    @data.setter\n    def data(self, val: bytes) -> None: ...\n    @property\n    def hash(self) -> bytes: ...\n    @hash.setter\n    def hash(self, val: bytes) -> None: ...\n"
  },
  {
    "path": "src/borg/item.pyx",
    "content": "import stat\nfrom collections import namedtuple\n\nfrom libc.string cimport memcmp\nfrom cpython.bytes cimport PyBytes_AsStringAndSize\n\nfrom .constants import ITEM_KEYS, ARCHIVE_KEYS\nfrom .helpers import StableDict\nfrom .helpers import format_file_size\nfrom .helpers.fs import assert_sanitized_path, to_sanitized_path, map_chars, slashify\nfrom .helpers.msgpack import timestamp_to_int, int_to_timestamp, Timestamp\nfrom .helpers.time import OutputTimestamp, safe_timestamp\n\n\ncdef extern from \"_item.c\":\n    object _object_to_optr(object obj)\n    object _optr_to_object(object bytes)\n\n\n\n\n\ndef fix_key(data, key, *, errors='strict'):\n    \"\"\"If the key is bytes-typed, migrate the key/value to a str-typed key in the data dict.\"\"\"\n    if isinstance(key, bytes):\n        value = data.pop(key)\n        key = key.decode('utf-8', errors=errors)\n        data[key] = value\n    assert isinstance(key, str)\n    return key\n\n\ndef fix_str_value(data, key, errors='surrogateescape'):\n    \"\"\"Ensure that data[key] is a str (decode if it is bytes).\"\"\"\n    assert isinstance(key, str)  # fix_key must be called first\n    value = data[key]\n    value = want_str(value, errors=errors)\n    data[key] = value\n    return value\n\n\ndef fix_bytes_value(data, key):\n    \"\"\"Ensure that data[key] is bytes (encode if it is str).\"\"\"\n    assert isinstance(key, str)  # fix_key must be called first\n    value = data[key]\n    value = want_bytes(value)\n    data[key] = value\n    return value\n\n\ndef fix_list_of_str(v):\n    \"\"\"Ensure that we have a list of str.\"\"\"\n    assert isinstance(v, (tuple, list))\n    return [want_str(e) for e in v]\n\n\ndef fix_list_of_bytes(v):\n    \"\"\"Ensure that we have a list of bytes.\"\"\"\n    assert isinstance(v, (tuple, list))\n    return [want_bytes(e) for e in v]\n\n\ndef fix_list_of_chunkentries(v):\n    \"\"\"Ensure that we have a list of valid chunk entries.\"\"\"\n    assert isinstance(v, (tuple, list))\n    chunks = []\n    for ce in v:\n        assert isinstance(ce, (tuple, list))\n        assert len(ce) in (2, 3)  # id, size[, csize]\n        assert isinstance(ce[1], int)\n        assert len(ce) == 2 or isinstance(ce[2], int)\n        ce_fixed = [want_bytes(ce[0]), ce[1]]  # list! id, size only, drop csize\n        chunks.append(ce_fixed)  # create a list of lists\n    return chunks\n\n\ndef fix_tuple_of_str(v):\n    \"\"\"Ensure that we have a tuple of str.\"\"\"\n    assert isinstance(v, (tuple, list))\n    return tuple(want_str(e) for e in v)\n\n\ndef fix_tuple_of_str_and_int(v):\n    \"\"\"Ensure that we have a tuple of str or int.\"\"\"\n    assert isinstance(v, (tuple, list))\n    t = tuple(e.decode() if isinstance(e, bytes) else e for e in v)\n    assert all(isinstance(e, (str, int)) for e in t), repr(t)\n    return t\n\n\ndef fix_timestamp(v):\n    \"\"\"Ensure that v is a Timestamp.\"\"\"\n    if isinstance(v, Timestamp):\n        return v\n    # legacy support\n    if isinstance(v, bytes):  # was: bigint_to_int()\n        v = int.from_bytes(v, 'little', signed=True)\n    assert isinstance(v, int)\n    return int_to_timestamp(v)\n\n\ndef want_bytes(v, *, errors='surrogateescape'):\n    \"\"\"Ensure that the value is bytes (encode if necessary).\"\"\"\n    # legacy support: it being str can be caused by msgpack unpack decoding old data that was packed with use_bin_type=False\n    if isinstance(v, str):\n        v = v.encode('utf-8', errors=errors)\n    assert isinstance(v, bytes), f'not a bytes object, but {v!r}'\n    return v\n\n\ndef want_str(v, *, errors='surrogateescape'):\n    \"\"\"Ensure that the value is str (decode if necessary).\"\"\"\n    if isinstance(v, bytes):\n        v = v.decode('utf-8', errors=errors)\n    assert isinstance(v, str), f'not a str object, but {v!r}'\n    return v\n\n\ncdef class PropDict:\n    \"\"\"\n    Manage a dictionary via properties.\n\n    - initialization by giving a dict or kw args\n    - on initialization, normalize dict keys to be str type\n    - access dict via properties, like: x.key_name\n    - membership check via: 'key_name' in x\n    - optionally, encode when setting a value\n    - optionally, decode when getting a value\n    - be safe against typos in key names: check against VALID_KEYS\n    - when setting a value: check type of value\n\n    When \"packing\" a dict, i.e. you have a dict with some data and want to convert it into an instance,\n    then use e.g. Item({'a': 1, ...}). This way all keys in your dictionary are validated.\n\n    When \"unpacking\", that is you've read a dictionary with some data from somewhere (e.g. msgpack),\n    then use e.g. Item(internal_dict={...}). This does not validate the keys, therefore unknown keys\n    are ignored instead of causing an error.\n    \"\"\"\n    VALID_KEYS = frozenset()  # override with <set of str> in child class\n\n    cdef object _dict\n\n    def __cinit__(self, data_dict=None, internal_dict=None, **kw):\n        self._dict = {}\n        if internal_dict is None:\n            pass  # nothing to do\n        elif isinstance(internal_dict, dict):\n            self.update_internal(internal_dict)\n        else:\n            raise TypeError(\"internal_dict must be a dict\")\n        if data_dict is None:\n            data = kw\n        elif isinstance(data_dict, dict):\n            data = data_dict\n        else:\n            raise TypeError(\"data_dict must be a dict\")\n        if data:\n            self.update(data)\n\n    def update(self, d):\n        for k, v in d.items():\n            if isinstance(k, bytes):\n                k = k.decode()\n            setattr(self, self._check_key(k), v)\n\n    def update_internal(self, d):\n        for k, v in d.items():\n            if isinstance(k, bytes):\n                k = k.decode()\n            self._dict[k] = v\n\n    def __eq__(self, other):\n        return self.as_dict() == other.as_dict()\n\n    def __repr__(self):\n        return '%s(internal_dict=%r)' % (self.__class__.__name__, self._dict)\n\n    def as_dict(self):\n        \"\"\"return the internal dictionary\"\"\"\n        return StableDict(self._dict)\n\n    def _check_key(self, key):\n        \"\"\"make sure key is of type str and known\"\"\"\n        if not isinstance(key, str):\n            raise TypeError(\"key must be str\")\n        if key not in self.VALID_KEYS:\n            raise ValueError(\"key '%s' is not a valid key\" % key)\n        return key\n\n    def __contains__(self, key):\n        \"\"\"do we have this key?\"\"\"\n        return self._check_key(key) in self._dict\n\n    def get(self, key, default=None):\n        \"\"\"get value for key, return default if key does not exist\"\"\"\n        return getattr(self, self._check_key(key), default)\n\n\ncdef class PropDictProperty:\n    \"\"\"return a property that deals with self._dict[key] of PropDict\"\"\"\n    cdef readonly str key\n    cdef readonly object value_type\n    cdef str value_type_name\n    cdef readonly str __doc__\n    cdef object encode\n    cdef object decode\n    cdef str type_error_msg\n    cdef str attr_error_msg\n\n    def __cinit__(self, value_type, value_type_name=None, encode=None, decode=None):\n       self.key = None\n       self.value_type = value_type\n       self.value_type_name = value_type_name if value_type_name is not None else value_type.__name__\n       self.encode = encode\n       self.decode = decode\n\n    def __get__(self, PropDict instance, owner):\n        try:\n            value = instance._dict[self.key]\n        except KeyError:\n            raise AttributeError(self.attr_error_msg) from None\n        if self.decode is not None:\n            value = self.decode(value)\n        if not isinstance(value, self.value_type):\n            raise TypeError(self.type_error_msg)\n        return value\n\n    def __set__(self, PropDict instance, value):\n        if not isinstance(value, self.value_type):\n            raise TypeError(self.type_error_msg)\n        if self.encode is not None:\n            value = self.encode(value)\n        instance._dict[self.key] = value\n\n    def __delete__(self, PropDict instance):\n        try:\n            del instance._dict[self.key]\n        except KeyError:\n            raise AttributeError(self.attr_error_msg) from None\n\n    cpdef __set_name__(self, owner, name):\n       self.key = name\n       self.__doc__ = \"%s (%s)\" % (name, self.value_type_name)\n       self.type_error_msg = \"%s value must be %s\" % (name, self.value_type_name)\n       self.attr_error_msg = \"attribute %s not found\" % name\n\n\nChunkListEntry = namedtuple('ChunkListEntry', 'id size')\n\ncdef class Item(PropDict):\n    \"\"\"\n    Item abstraction that deals with validation and the low-level details internally:\n\n    Items are created either from msgpack unpacker output, from another dict, from kwargs or\n    built step-by-step by setting attributes.\n\n    msgpack unpacker gives us a dict, just give it to Item(internal_dict=d) and use item.key_name later.\n\n    If an Item shall be serialized, give as_dict() method output to msgpack packer.\n    \"\"\"\n\n    VALID_KEYS = ITEM_KEYS | {'deleted', 'nlink', }\n\n    # properties statically defined, so that IDEs can know their names:\n\n    path = PropDictProperty(str, 'surrogate-escaped str', encode=assert_sanitized_path, decode=to_sanitized_path)\n    source = PropDictProperty(str, 'surrogate-escaped str')  # legacy borg 1.x. borg 2: see .target\n    target = PropDictProperty(str, 'surrogate-escaped str', encode=slashify, decode=map_chars)\n    user = PropDictProperty(str, 'surrogate-escaped str')\n    group = PropDictProperty(str, 'surrogate-escaped str')\n\n    acl_access = PropDictProperty(bytes)\n    acl_default = PropDictProperty(bytes)\n    acl_extended = PropDictProperty(bytes)\n    acl_nfs4 = PropDictProperty(bytes)\n\n    mode = PropDictProperty(int)\n    uid = PropDictProperty(int)\n    gid = PropDictProperty(int)\n    rdev = PropDictProperty(int)\n    bsdflags = PropDictProperty(int)\n\n    atime = PropDictProperty(int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int)\n    ctime = PropDictProperty(int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int)\n    mtime = PropDictProperty(int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int)\n    birthtime = PropDictProperty(int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int)\n\n    # size is only present for items with a chunk list and then it is sum(chunk_sizes)\n    size = PropDictProperty(int)\n\n    inode = PropDictProperty(int)\n\n    hlid = PropDictProperty(bytes)  # hard link id: same value means same hard link.\n    hardlink_master = PropDictProperty(bool)  # legacy\n\n    chunks = PropDictProperty(list, 'list')\n    chunks_healthy = PropDictProperty(list, 'list')\n\n    xattrs = PropDictProperty(StableDict)\n\n    deleted = PropDictProperty(bool)\n    nlink = PropDictProperty(int)\n\n    part = PropDictProperty(int)  # legacy only\n\n    def get_size(self, *, memorize=False, from_chunks=False, consider_ids=None):\n        \"\"\"\n        Determine the uncompressed size of this item.\n\n        :param memorize: Whether the computed size value will be stored into the item.\n        :param from_chunks: If true, size is computed from chunks even if a precomputed value is available.\n        :param consider_ids: Returns the size of the given ids only.\n        \"\"\"\n        attr = 'size'\n        assert not (consider_ids is not None and memorize), \"Can't store size when considering only certain ids\"\n        try:\n            if from_chunks or consider_ids is not None:\n                raise AttributeError\n            size = getattr(self, attr)\n        except AttributeError:\n            if stat.S_ISLNK(self.mode):\n                # get out of here quickly. symlinks have no own chunks, their fs size is the length of the target name.\n                if 'source' in self:  # legacy borg 1.x archives\n                    return len(self.source)\n                return len(self.target)\n            # no precomputed (c)size value available, compute it:\n            try:\n                chunks = getattr(self, 'chunks')\n            except AttributeError:\n                return 0\n            if consider_ids is not None:\n                size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks if chunk.id in consider_ids)\n            else:\n                size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks)\n            # if requested, memorize the precomputed (c)size for items that have an own chunks list:\n            if memorize:\n                setattr(self, attr, size)\n        return size\n\n    def to_optr(self):\n        \"\"\"\n        Return an \"object pointer\" (optr), an opaque bag of bytes.\n        The return value is effectively a reference to this object\n        that can be passed exactly once to Item.from_optr to get this\n        object back.\n\n        to_optr/from_optr must be used symmetrically,\n        don't call from_optr multiple times.\n\n        This object can't be deallocated after a call to to_optr()\n        until from_optr() is called.\n        \"\"\"\n        return _object_to_optr(self)\n\n    @classmethod\n    def from_optr(self, optr):\n        return _optr_to_object(optr)\n\n    @classmethod\n    def create_deleted(cls, path):\n        return cls(deleted=True, chunks=[], mode=0, path=path)\n\n    def is_link(self):\n        return self._is_type(stat.S_ISLNK)\n\n    def is_dir(self):\n        return self._is_type(stat.S_ISDIR)\n\n    def is_fifo(self):\n        return self._is_type(stat.S_ISFIFO)\n\n    def is_blk(self):\n        return self._is_type(stat.S_ISBLK)\n\n    def is_chr(self):\n        return self._is_type(stat.S_ISCHR)\n\n    def _is_type(self, typetest):\n        try:\n            return typetest(self.mode)\n        except AttributeError:\n            return False\n\n    def update_internal(self, d):\n        # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str),\n        # also need to fix old timestamp data types.\n        for k, v in list(d.items()):\n            k = fix_key(d, k)\n            if k in ('user', 'group') and d[k] is None:\n                # borg 1 stored some \"not known\" values with a None value.\n                # borg 2 policy for such cases is to just not have the key/value pair.\n                continue\n            if k in ('path', 'source', 'target', 'user', 'group'):\n                v = fix_str_value(d, k)\n            if k in ('chunks', 'chunks_healthy'):\n                v = fix_list_of_chunkentries(v)\n            if k in ('atime', 'ctime', 'mtime', 'birthtime'):\n                v = fix_timestamp(v)\n            if k in ('acl_access', 'acl_default', 'acl_extended', 'acl_nfs4'):\n                v = fix_bytes_value(d, k)\n            if k == 'xattrs':\n                if not isinstance(v, StableDict):\n                    v = StableDict(v)\n                v_new = StableDict()\n                for xk, xv in list(v.items()):\n                    xk = want_bytes(xk)\n                    # old borg used to store None instead of a b'' value\n                    xv = b'' if xv is None else want_bytes(xv)\n                    v_new[xk] = xv\n                v = v_new  # xattrs is a StableDict(bytes keys -> bytes values)\n            self._dict[k] = v\n\n\ncdef class EncryptedKey(PropDict):\n    \"\"\"\n    EncryptedKey abstraction that deals with validation and the low-level details internally:\n\n    A EncryptedKey is created either from msgpack unpacker output, from another dict, from kwargs or\n    built step-by-step by setting attributes.\n\n    msgpack unpacker gives us a dict, just give it to EncryptedKey(d) and use enc_key.xxx later.\n\n    If a EncryptedKey shall be serialized, give as_dict() method output to msgpack packer.\n    \"\"\"\n\n    VALID_KEYS = {'version', 'algorithm', 'iterations', 'salt', 'hash', 'data',\n                  'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type'}\n\n    version = PropDictProperty(int)\n    algorithm = PropDictProperty(str)\n    iterations = PropDictProperty(int)\n    salt = PropDictProperty(bytes)\n    hash = PropDictProperty(bytes)\n    data = PropDictProperty(bytes)\n    argon2_time_cost = PropDictProperty(int)\n    argon2_memory_cost = PropDictProperty(int)\n    argon2_parallelism = PropDictProperty(int)\n    argon2_type = PropDictProperty(str)\n\n    def update_internal(self, d):\n        # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str)\n        for k, v in list(d.items()):\n            k = fix_key(d, k)\n            if k == 'version':\n                assert isinstance(v, int)\n            if k in ('algorithm', 'argon2_type'):\n                v = fix_str_value(d, k)\n            if k in ('salt', 'hash', 'data'):\n                v = fix_bytes_value(d, k)\n            self._dict[k] = v\n\n\ncdef class Key(PropDict):\n    \"\"\"\n    Key abstraction that deals with validation and the low-level details internally:\n\n    A Key is created either from msgpack unpacker output, from another dict, from kwargs or\n    built step-by-step by setting attributes.\n\n    msgpack unpacker gives us a dict, just give it to Key(d) and use key.xxx later.\n\n    If a Key shall be serialized, give as_dict() method output to msgpack packer.\n    \"\"\"\n\n    VALID_KEYS = {'version', 'repository_id', 'crypt_key', 'id_key', 'chunk_seed', 'tam_required'}\n\n    version = PropDictProperty(int)\n    repository_id = PropDictProperty(bytes)\n    crypt_key = PropDictProperty(bytes)\n    id_key = PropDictProperty(bytes)\n    chunk_seed = PropDictProperty(int)\n    tam_required = PropDictProperty(bool)  # legacy. borg now implicitly always requires TAM.\n\n    def update_internal(self, d):\n        # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str)\n        for k, v in list(d.items()):\n            k = fix_key(d, k)\n            if k == 'version':\n                assert isinstance(v, int)\n            if k in ('repository_id', 'crypt_key', 'id_key'):\n                v = fix_bytes_value(d, k)\n            self._dict[k] = v\n        if 'crypt_key' not in self._dict:  # legacy, we're loading an old v1 key\n            k = fix_bytes_value(d, 'enc_key') + fix_bytes_value(d, 'enc_hmac_key')\n            assert isinstance(k, bytes), \"k == %r\" % k\n            assert len(k) in (32 + 32, 32 + 128)  # 256+256 or 256+1024 bits\n            self._dict['crypt_key'] = k\n\ncdef class ArchiveItem(PropDict):\n    \"\"\"\n    ArchiveItem abstraction that deals with validation and the low-level details internally:\n\n    An ArchiveItem is created either from msgpack unpacker output, from another dict, from kwargs or\n    built step-by-step by setting attributes.\n\n    msgpack unpacker gives us a dict, just give it to ArchiveItem(d) and use arch.xxx later.\n\n    If a ArchiveItem shall be serialized, give as_dict() method output to msgpack packer.\n    \"\"\"\n\n    VALID_KEYS = ARCHIVE_KEYS\n\n    version = PropDictProperty(int)\n    name = PropDictProperty(str, 'surrogate-escaped str')\n    items = PropDictProperty(list)  # list of chunk ids of item metadata stream (only in memory)\n    item_ptrs = PropDictProperty(list)  # list of blocks with list of chunk ids of ims, arch v2\n    cmdline = PropDictProperty(list)  # legacy, list of s-e-str\n    command_line = PropDictProperty(str, 'surrogate-escaped str')\n    hostname = PropDictProperty(str, 'surrogate-escaped str')\n    username = PropDictProperty(str, 'surrogate-escaped str')\n    start = PropDictProperty(str)  # new in borg2 (was: time)\n    end = PropDictProperty(str)  # new in borg2 (was: time_end)\n    time = PropDictProperty(str)  # borg2: nominal archive time, borg 1.x: same + start time\n    time_end = PropDictProperty(str)  # legacy borg 1.x (now: end)\n    comment = PropDictProperty(str, 'surrogate-escaped str')\n    tags = PropDictProperty(list)  # list of s-e-str\n    chunker_params = PropDictProperty(tuple)\n    recreate_cmdline = PropDictProperty(list)  # legacy, list of s-e-str\n    recreate_command_line = PropDictProperty(str, 'surrogate-escaped str')\n    # recreate_source_id, recreate_args, recreate_partial_chunks were used in 1.1.0b1 .. b2\n    recreate_source_id = PropDictProperty(bytes)\n    recreate_args = PropDictProperty(list)  # list of s-e-str\n    recreate_partial_chunks = PropDictProperty(list)  # list of tuples\n    size = PropDictProperty(int)\n    nfiles = PropDictProperty(int)\n    size_parts = PropDictProperty(int)  # legacy only\n    nfiles_parts = PropDictProperty(int)  # legacy only\n    cwd = PropDictProperty(str, 'surrogate-escaped str')\n\n    def update_internal(self, d):\n        # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str)\n        for k, v in list(d.items()):\n            k = fix_key(d, k)\n            if k == 'version':\n                assert isinstance(v, int)\n            if k in ('name', 'hostname', 'username', 'comment', 'cwd'):\n                v = fix_str_value(d, k)\n            if k in ('start', 'end', 'time', 'time_end'):\n                v = fix_str_value(d, k, 'replace')\n            if k == 'chunker_params':\n                v = fix_tuple_of_str_and_int(v)\n            if k in ('command_line', 'recreate_command_line'):\n                v = fix_str_value(d, k)\n            if k in ('cmdline', 'recreate_cmdline'):  # legacy\n                v = fix_list_of_str(v)\n            if k == 'items':  # legacy\n                v = fix_list_of_bytes(v)\n            if k == 'item_ptrs':\n                v = fix_list_of_bytes(v)\n            self._dict[k] = v\n\n\ncdef class ManifestItem(PropDict):\n    \"\"\"\n    ManifestItem abstraction that deals with validation and the low-level details internally:\n\n    A ManifestItem is created either from msgpack unpacker output, from another dict, from kwargs or\n    built step-by-step by setting attributes.\n\n    msgpack unpacker gives us a dict, just give it to ManifestItem(d) and use manifest.xxx later.\n\n    If a ManifestItem shall be serialized, give as_dict() method output to msgpack packer.\n    \"\"\"\n\n    VALID_KEYS = {'version', 'archives', 'timestamp', 'config', 'item_keys', }\n\n    version = PropDictProperty(int)\n    archives = PropDictProperty(dict, 'dict of str -> dict')  # name -> dict\n    timestamp = PropDictProperty(str)\n    config = PropDictProperty(dict)\n    item_keys = PropDictProperty(tuple, 'tuple of str')  # legacy. new location is inside config.\n\n    def update_internal(self, d):\n        # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str)\n        for k, v in list(d.items()):\n            k = fix_key(d, k)\n            if k == 'version':\n                assert isinstance(v, int)\n            if k == 'archives':\n                ad = v\n                assert isinstance(ad, dict)\n                for ak, av in list(ad.items()):\n                    ak = fix_key(ad, ak, errors='surrogateescape')\n                    assert isinstance(av, dict)\n                    for ik, iv in list(av.items()):\n                        ik = fix_key(av, ik)\n                        if ik == 'id':\n                            fix_bytes_value(av, 'id')\n                        if ik == 'time':\n                            fix_str_value(av, 'time')\n                    assert set(av) == {'id', 'time'}\n            if k == 'timestamp':\n                v = fix_str_value(d, k, 'replace')\n            if k == 'config':\n                cd = v\n                assert isinstance(cd, dict)\n                for ck, cv in list(cd.items()):\n                    ck = fix_key(cd, ck)\n                    if ck == 'tam_required':\n                        assert isinstance(cv, bool)\n                    if ck == 'feature_flags':\n                        assert isinstance(cv, dict)\n                        ops = {'read', 'check', 'write', 'delete'}\n                        for op, specs in list(cv.items()):\n                            op = fix_key(cv, op)\n                            assert op in ops\n                            for speck, specv in list(specs.items()):\n                                speck = fix_key(specs, speck)\n                                if speck == 'mandatory':\n                                    specs[speck] = fix_tuple_of_str(specv)\n                        assert set(cv).issubset(ops)\n            if k == 'item_keys':\n                v = fix_tuple_of_str(v)\n            self._dict[k] = v\n\n\ncpdef _init_names():\n    \"\"\"\n    re-implements python __set_name__ for Cython<3.1\n    \"\"\"\n    for cls in PropDict.__subclasses__():\n        for name, value in vars(cls).items():\n            if isinstance(value, PropDictProperty):\n                value.__set_name__(cls, name)\n\n_init_names()\n\n\nclass DiffChange:\n    \"\"\"\n    Stores a change in a diff.\n\n    The diff_type denotes the type of change, e.g. \"added\", \"removed\", \"modified\".\n    The diff_data contains additional information about the change, e.g. the old and new mode.\n    \"\"\"\n    def __init__(self, diff_type, diff_data=None):\n        self.diff_type = diff_type\n        self.diff_data = diff_data or {}\n\n    def to_dict(self):\n        return {\"type\": self.diff_type, **self.diff_data}\n\n\nclass ItemDiff:\n    \"\"\"\n    Comparison of two items from different archives.\n\n    The items may have different paths and still be considered equal (e.g. for renames).\n    \"\"\"\n\n    def __init__(self, path, item1, item2, chunk_1, chunk_2, numeric_ids=False, can_compare_chunk_ids=False):\n        self.path = path\n        self._item1 = item1\n        self._item2 = item2\n        self._numeric_ids = numeric_ids\n        self._can_compare_chunk_ids = can_compare_chunk_ids\n        self._chunk_1 = chunk_1\n        self._chunk_2 = chunk_2\n        self._changes = {}\n\n        if self._item1.is_link() or self._item2.is_link():\n            self._link_diff()\n\n        if 'chunks' in self._item1 and 'chunks' in self._item2:\n            self._content_diff()\n\n        if self._item1.is_dir() or self._item2.is_dir():\n            self._presence_diff('directory')\n\n        if self._item1.is_blk() or self._item2.is_blk():\n            self._presence_diff('blkdev')\n\n        if self._item1.is_chr() or self._item2.is_chr():\n            self._presence_diff('chrdev')\n\n        if self._item1.is_fifo() or self._item2.is_fifo():\n            self._presence_diff('fifo')\n\n        if not (self._item1.get('deleted') or self._item2.get('deleted')):\n            self._owner_diff()\n            self._mode_diff()\n            self._time_diffs()\n\n\n    def changes(self):\n        return self._changes\n\n    def __repr__(self):\n        return (' '.join(self._changes.keys())) or 'equal'\n\n    def equal(self, content_only=False):\n        # if both are deleted, there is nothing at path regardless of what was deleted\n        if self._item1.get('deleted') and self._item2.get('deleted'):\n            return True\n\n        attr_list = ['deleted', 'target']\n        if not content_only:\n            attr_list += ['mode', 'ctime', 'mtime']\n            attr_list += ['uid', 'gid'] if self._numeric_ids else ['user', 'group']\n\n        for attr in attr_list:\n            if self._item1.get(attr) != self._item2.get(attr):\n                return False\n\n        if 'mode' in self._item1:     # mode of item1 and item2 is equal\n            if (self._item1.is_link() and 'target' in self._item1 and 'target' in self._item2\n                and self._item1.target != self._item2.target):\n                return False\n\n        if 'chunks' in self._item1 and 'chunks' in self._item2:\n            return self._content_equal()\n\n        return True\n\n    def _presence_diff(self, item_type):\n        if not self._item1.get('deleted') and self._item2.get('deleted'):\n            self._changes[item_type] = DiffChange(f\"removed {item_type}\")\n            return True\n        if self._item1.get('deleted') and not self._item2.get('deleted'):\n            self._changes[item_type] = DiffChange(f\"added {item_type}\")\n            return True\n\n    def _link_diff(self):\n        if self._presence_diff('link'):\n            return True\n        if 'target' in self._item1 and 'target' in self._item2 and self._item1.target != self._item2.target:\n            self._changes['link'] = DiffChange('changed link')\n            return True\n\n    def _content_diff(self):\n        if self._item1.get('deleted'):\n            sz = self._item2.get_size()\n            self._changes['content'] = DiffChange(\"added\", {\"added\": sz, \"removed\": 0})\n            return True\n        if self._item2.get('deleted'):\n            sz = self._item1.get_size()\n            self._changes['content'] = DiffChange(\"removed\", {\"added\": 0, \"removed\": sz})\n            return True\n        if not self._can_compare_chunk_ids:\n            self._changes['content'] = DiffChange(\"modified\")\n            return True\n        chunk_ids1 = {c.id for c in self._item1.chunks}\n        chunk_ids2 = {c.id for c in self._item2.chunks}\n        added_ids = chunk_ids2 - chunk_ids1\n        removed_ids = chunk_ids1 - chunk_ids2\n        added = self._item2.get_size(consider_ids=added_ids)\n        removed = self._item1.get_size(consider_ids=removed_ids)\n        self._changes['content'] = DiffChange(\"modified\", {\"added\": added, \"removed\": removed})\n        return True\n\n\n    def _owner_diff(self):\n        u_attr, g_attr = ('uid', 'gid') if self._numeric_ids else ('user', 'group')\n        u1, g1 = self._item1.get(u_attr), self._item1.get(g_attr)\n        u2, g2 = self._item2.get(u_attr), self._item2.get(g_attr)\n        if (u1, g1) == (u2, g2):\n            return False\n        self._changes['owner'] = DiffChange(\"changed owner\", {\"item1\": (u1, g1), \"item2\": (u2, g2)})\n        if u1 != u2:\n            self._changes['user'] = DiffChange(\"changed user\", {\"item1\": u1, \"item2\": u2})\n        if g1 != g2:\n            self._changes['group'] = DiffChange(\"changed group\", {\"item1\": g1, \"item2\": g2})\n        return True\n\n    def _mode_diff(self):\n        if 'mode' in self._item1 and 'mode' in self._item2 and self._item1.mode != self._item2.mode:\n            mode1 = stat.filemode(self._item1.mode)\n            mode2 = stat.filemode(self._item2.mode)\n            self._changes['mode'] = DiffChange(\"changed mode\", {\"item1\": mode1, \"item2\": mode2})\n            if mode1[0] != mode2[0]:\n                self._changes['type'] = DiffChange(\"changed type\", {\"item1\": mode1[0], \"item2\": mode2[0]})\n\n    def _time_diffs(self):\n        attrs = [\"ctime\", \"mtime\"]\n        for attr in attrs:\n            if attr in self._item1 and attr in self._item2 and self._item1.get(attr) != self._item2.get(attr):\n                ts1 = OutputTimestamp(safe_timestamp(self._item1.get(attr)))\n                ts2 = OutputTimestamp(safe_timestamp(self._item2.get(attr)))\n                self._changes[attr] = DiffChange(attr, {\"item1\": ts1, \"item2\": ts2},)\n        return True\n\n    def content(self):\n        return self._changes.get('content')\n\n    def ctime(self):\n        return self._changes.get('ctime')\n\n    def mtime(self):\n        return self._changes.get('mtime')\n\n    def mode(self):\n        return self._changes.get('mode')\n\n    def type(self):\n        return self._changes.get('type')\n\n    def owner(self):\n        return self._changes.get('owner')\n\n    def user(self):\n        return self._changes.get('user')\n\n    def group(self):\n        return self._changes.get('group')\n\n    def _content_equal(self):\n        if self._can_compare_chunk_ids:\n            return self._item1.chunks == self._item2.chunks\n        if self._item1.get_size() != self._item2.get_size():\n            return False\n        return chunks_contents_equal(self._chunk_1, self._chunk_2)\n\n\ndef chunks_contents_equal(chunks_a, chunks_b):\n    \"\"\"\n    Compare chunk content and return True if they are identical.\n\n    The chunks must be given as chunk iterators (like returned by :meth:`.DownloadPipeline.fetch_many`).\n    \"\"\"\n    cdef:\n        bytes a, b\n        char * ap\n        char * bp\n        Py_ssize_t slicelen = 0\n        Py_ssize_t alen = 0\n        Py_ssize_t blen = 0\n\n    while True:\n        if not alen:\n            a = next(chunks_a, None)\n            if a is None:\n                return not blen and next(chunks_b, None) is None\n            PyBytes_AsStringAndSize(a, &ap, &alen)\n        if not blen:\n            b = next(chunks_b, None)\n            if b is None:\n                return not alen and next(chunks_a, None) is None\n            PyBytes_AsStringAndSize(b, &bp, &blen)\n        slicelen = min(alen, blen)\n        if memcmp(ap, bp, slicelen) != 0:\n            return False\n        ap += slicelen\n        bp += slicelen\n        alen -= slicelen\n        blen -= slicelen\n"
  },
  {
    "path": "src/borg/legacyremote.py",
    "content": "import errno\nimport functools\nimport inspect\nimport logging\nimport os\nimport select\nimport shlex\nimport shutil\nimport socket\nimport struct\nimport sys\nimport tempfile\nimport textwrap\nimport time\nfrom subprocess import Popen, PIPE\n\nfrom xxhash import xxh64\n\nfrom . import __version__\nfrom .compress import Compressor\nfrom .constants import *  # NOQA\nfrom .helpers import Error, ErrorWithTraceback, IntegrityError\nfrom .helpers import bin_to_hex\nfrom .helpers import get_limited_unpacker\nfrom .helpers import replace_placeholders\nfrom .helpers import format_file_size\nfrom .helpers import safe_unlink\nfrom .helpers import prepare_subprocess_env, ignore_sigint\nfrom .helpers import get_socket_filename\nfrom .fslocking import LockTimeout, NotLocked, NotMyLock, LockFailed\nfrom .logger import create_logger\nfrom .helpers import msgpack\nfrom .legacyrepository import LegacyRepository\nfrom .version import parse_version, format_version\nfrom .helpers.datastruct import EfficientCollectionQueue\nfrom .platform import is_win32\n\nlogger = create_logger(__name__)\n\nBORG_VERSION = parse_version(__version__)\nMSGID, MSG, ARGS, RESULT, LOG = \"i\", \"m\", \"a\", \"r\", \"l\"\n\nMAX_INFLIGHT = 100\n\nRATELIMIT_PERIOD = 0.1\n\n\nclass ConnectionClosed(Error):\n    \"\"\"Connection closed by remote host.\"\"\"\n\n    exit_mcode = 80\n\n\nclass ConnectionClosedWithHint(ConnectionClosed):\n    \"\"\"Connection closed by remote host. {}\"\"\"\n\n    exit_mcode = 81\n\n\nclass PathNotAllowed(Error):\n    \"\"\"Repository path not allowed: {}.\"\"\"\n\n    exit_mcode = 83\n\n\nclass InvalidRPCMethod(Error):\n    \"\"\"RPC method {} is not valid.\"\"\"\n\n    exit_mcode = 82\n\n\nclass UnexpectedRPCDataFormatFromClient(Error):\n    \"\"\"Borg {}: Got unexpected RPC data format from client.\"\"\"\n\n    exit_mcode = 85\n\n\nclass UnexpectedRPCDataFormatFromServer(Error):\n    \"\"\"Got unexpected RPC data format from server:\\n{}\"\"\"\n\n    exit_mcode = 86\n\n    def __init__(self, data):\n        try:\n            data = data.decode()[:128]\n        except UnicodeDecodeError:\n            data = data[:128]\n            data = [\"%02X\" % byte for byte in data]\n            data = textwrap.fill(\" \".join(data), 16 * 3)\n        super().__init__(data)\n\n\nclass ConnectionBrokenWithHint(Error):\n    \"\"\"Connection to remote host is broken. {}\"\"\"\n\n    exit_mcode = 87\n\n\n# Protocol compatibility:\n# In general the server is responsible for rejecting too old clients and the client is responsible for rejecting\n# too old servers. This ensures that the knowledge what is compatible is always held by the newer component.\n#\n# For the client the return of the negotiate method is a dict which includes the server version.\n#\n# All method calls on the remote repository object must be allowlisted in RepositoryServer.rpc_methods and have api\n# stubs in LegacyRemoteRepository. The @api decorator on these stubs is used to set server version requirements.\n#\n# Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server.\n# If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs\n# to be added.\n# When parameters are removed, they need to be preserved as defaulted parameters on the client stubs so that older\n# servers still get compatible input.\n\n\nclass SleepingBandwidthLimiter:\n    def __init__(self, limit):\n        if limit:\n            self.ratelimit = int(limit * RATELIMIT_PERIOD)\n            self.ratelimit_last = time.monotonic()\n            self.ratelimit_quota = self.ratelimit\n        else:\n            self.ratelimit = None\n\n    def write(self, fd, to_send):\n        if self.ratelimit:\n            now = time.monotonic()\n            if self.ratelimit_last + RATELIMIT_PERIOD <= now:\n                self.ratelimit_quota += self.ratelimit\n                if self.ratelimit_quota > 2 * self.ratelimit:\n                    self.ratelimit_quota = 2 * self.ratelimit\n                self.ratelimit_last = now\n            if self.ratelimit_quota == 0:\n                tosleep = self.ratelimit_last + RATELIMIT_PERIOD - now\n                time.sleep(tosleep)\n                self.ratelimit_quota += self.ratelimit\n                self.ratelimit_last = time.monotonic()\n            if len(to_send) > self.ratelimit_quota:\n                to_send = to_send[: self.ratelimit_quota]\n        try:\n            written = os.write(fd, to_send)\n        except BrokenPipeError:\n            raise ConnectionBrokenWithHint(\"Broken Pipe\") from None\n        if self.ratelimit:\n            self.ratelimit_quota -= written\n        return written\n\n\ndef api(*, since, **kwargs_decorator):\n    \"\"\"Check version requirements and use self.call to do the remote method call.\n\n    <since> specifies the version in which borg introduced this method.\n    Calling this method when connected to an older version will fail without transmitting anything to the server.\n\n    Further kwargs can be used to encode version specific restrictions:\n\n    <previously> is the value resulting in the behaviour before introducing the new parameter.\n    If a previous hardcoded behaviour is parameterized in a version, this allows calls that use the previously\n    hardcoded behaviour to pass through and generates an error if another behaviour is requested by the client.\n    E.g. when 'append_only' was introduced in 1.0.7 the previous behaviour was what now is append_only=False.\n    Thus @api(..., append_only={'since': parse_version('1.0.7'), 'previously': False}) allows calls\n    with append_only=False for all version but rejects calls using append_only=True on versions older than 1.0.7.\n\n    <dontcare> is a flag to set the behaviour if an old version is called the new way.\n    If set to True, the method is called without the (not yet supported) parameter (this should be done if that is the\n    more desirable behaviour). If False, an exception is generated.\n    E.g. before 'threshold' was introduced in 1.2.0a8, a hardcoded threshold of 0.1 was used in commit().\n    \"\"\"\n\n    def decorator(f):\n        @functools.wraps(f)\n        def do_rpc(self, *args, **kwargs):\n            sig = inspect.signature(f)\n            bound_args = sig.bind(self, *args, **kwargs)\n            named = {}  # Arguments for the remote process\n            extra = {}  # Arguments for the local process\n            for name, param in sig.parameters.items():\n                if name == \"self\":\n                    continue\n                if name in bound_args.arguments:\n                    if name == \"wait\":\n                        extra[name] = bound_args.arguments[name]\n                    else:\n                        named[name] = bound_args.arguments[name]\n                else:\n                    if param.default is not param.empty:\n                        named[name] = param.default\n\n            if self.server_version < since:\n                raise self.RPCServerOutdated(f.__name__, format_version(since))\n\n            for name, restriction in kwargs_decorator.items():\n                if restriction[\"since\"] <= self.server_version:\n                    continue\n                if \"previously\" in restriction and named[name] == restriction[\"previously\"]:\n                    continue\n                if restriction.get(\"dontcare\", False):\n                    continue\n\n                raise self.RPCServerOutdated(\n                    f\"{f.__name__} {name}={named[name]!s}\", format_version(restriction[\"since\"])\n                )\n\n            return self.call(f.__name__, named, **extra)\n\n        return do_rpc\n\n    return decorator\n\n\nclass LegacyRemoteRepository:\n    extra_test_args = []  # type: ignore\n\n    class RPCError(Exception):\n        def __init__(self, unpacked):\n            # unpacked has keys: 'exception_args', 'exception_full', 'exception_short', 'sysinfo'\n            self.unpacked = unpacked\n\n        def get_message(self):\n            return \"\\n\".join(self.unpacked[\"exception_short\"])\n\n        @property\n        def traceback(self):\n            return self.unpacked.get(\"exception_trace\", True)\n\n        @property\n        def exception_class(self):\n            return self.unpacked[\"exception_class\"]\n\n        @property\n        def exception_full(self):\n            return \"\\n\".join(self.unpacked[\"exception_full\"])\n\n        @property\n        def sysinfo(self):\n            return self.unpacked[\"sysinfo\"]\n\n    class RPCServerOutdated(Error):\n        \"\"\"Borg server is too old for {}. Required version {}\"\"\"\n\n        exit_mcode = 84\n\n        @property\n        def method(self):\n            return self.args[0]\n\n        @property\n        def required_version(self):\n            return self.args[1]\n\n    def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, args=None):\n        self.location = self._location = location\n        self.preload_ids = []\n        self.msgid = 0\n        self.rx_bytes = 0\n        self.tx_bytes = 0\n        self.to_send = EfficientCollectionQueue(1024 * 1024, bytes)\n        self.stdin_fd = self.stdout_fd = self.stderr_fd = None\n        self.stderr_received = b\"\"  # incomplete stderr line bytes received (no \\n yet)\n        self.chunkid_to_msgids = {}\n        self.ignore_responses = set()\n        self.responses = {}\n        self.async_responses = {}\n        self.shutdown_time = None\n        self.ratelimit = SleepingBandwidthLimiter(args.upload_ratelimit * 1024 if args and args.upload_ratelimit else 0)\n        self.upload_buffer_size_limit = args.upload_buffer * 1024 * 1024 if args and args.upload_buffer else 0\n        self.unpacker = get_limited_unpacker(\"client\")\n        self.server_version = None  # we update this after server sends its version\n        self.p = self.sock = None\n        self._args = args\n        if self.location.proto == \"ssh\":\n            testing = location.host == \"__testsuite__\"\n            # when testing, we invoke and talk to a borg process directly (no ssh).\n            # when not testing, we invoke the system-installed ssh binary to talk to a remote borg.\n            env = prepare_subprocess_env(system=not testing)\n            borg_cmd = self.borg_cmd(args, testing)\n            if not testing:\n                borg_cmd = self.ssh_cmd(location) + borg_cmd\n            logger.debug(\"SSH command line: %s\", borg_cmd)\n            # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg.\n            self.p = Popen(\n                borg_cmd,\n                bufsize=0,\n                stdin=PIPE,\n                stdout=PIPE,\n                stderr=PIPE,\n                env=env,\n                preexec_fn=None if is_win32 else ignore_sigint,\n            )  # nosec B603\n            self.stdin_fd = self.p.stdin.fileno()\n            self.stdout_fd = self.p.stdout.fileno()\n            self.stderr_fd = self.p.stderr.fileno()\n            self.r_fds = [self.stdout_fd, self.stderr_fd]\n            self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd]\n        elif self.location.proto == \"socket\":\n            if args.use_socket is False or args.use_socket is True:  # nothing or --socket\n                socket_path = get_socket_filename()\n            else:  # --socket=/some/path\n                socket_path = args.use_socket\n            self.sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)\n            try:\n                self.sock.connect(socket_path)  # note: socket_path length is rather limited.\n            except FileNotFoundError:\n                self.sock = None\n                raise Error(f\"The socket file {socket_path} does not exist.\")\n            except ConnectionRefusedError:\n                self.sock = None\n                raise Error(f\"There is no borg serve running for the socket file {socket_path}.\")\n            self.stdin_fd = self.sock.makefile(\"wb\").fileno()\n            self.stdout_fd = self.sock.makefile(\"rb\").fileno()\n            self.stderr_fd = None\n            self.r_fds = [self.stdout_fd]\n            self.x_fds = [self.stdin_fd, self.stdout_fd]\n        else:\n            raise Error(f\"Unsupported protocol {location.proto}\")\n\n        os.set_blocking(self.stdin_fd, False)\n        assert not os.get_blocking(self.stdin_fd)\n        os.set_blocking(self.stdout_fd, False)\n        assert not os.get_blocking(self.stdout_fd)\n        if self.stderr_fd is not None:\n            os.set_blocking(self.stderr_fd, False)\n            assert not os.get_blocking(self.stderr_fd)\n\n        try:\n            try:\n                version = self.call(\"negotiate\", {\"client_data\": {\"client_version\": BORG_VERSION}})\n            except ConnectionClosed:\n                raise ConnectionClosedWithHint(\"Is borg working on the server?\") from None\n            if isinstance(version, dict):\n                self.server_version = version[\"server_version\"]\n            else:\n                raise Exception(\"Server insisted on using unsupported protocol version %s\" % version)\n\n            self.id = self.open(\n                path=self.location.path,\n                create=create,\n                lock_wait=lock_wait,\n                lock=lock,\n                exclusive=exclusive,\n                v1_or_v2=True,  # make remote use LegacyRepository\n            )\n            info = self.info()\n            self.version = info[\"version\"]\n\n        except Exception:\n            self.close()\n            raise\n\n    def __del__(self):\n        if len(self.responses):\n            logging.debug(\"still %d cached responses left in LegacyRemoteRepository\" % (len(self.responses),))\n        if self.p or self.sock:\n            self.close()\n            assert False, \"cleanup happened in LegacyRemoteRepository.__del__\"\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {self.location.canonical_path()}>\"\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        try:\n            if exc_type is not None:\n                self.shutdown_time = time.monotonic() + 30\n                self.rollback()\n        finally:\n            # in any case, we want to close the repo cleanly, even if the\n            # rollback can not succeed (e.g. because the connection was\n            # already closed) and raised another exception:\n            logger.debug(\n                \"LegacyRemoteRepository: %s bytes sent, %s bytes received, %d messages sent\",\n                format_file_size(self.tx_bytes),\n                format_file_size(self.rx_bytes),\n                self.msgid,\n            )\n            self.close()\n\n    @property\n    def id_str(self):\n        return bin_to_hex(self.id)\n\n    def borg_cmd(self, args, testing):\n        \"\"\"return a borg serve command line\"\"\"\n        # give some args/options to 'borg serve' process as they were given to us\n        opts = []\n        if args is not None:\n            root_logger = logging.getLogger()\n            if root_logger.isEnabledFor(logging.DEBUG):\n                opts.append(\"--debug\")\n            elif root_logger.isEnabledFor(logging.INFO):\n                opts.append(\"--info\")\n            elif root_logger.isEnabledFor(logging.WARNING):\n                pass  # warning is default\n            elif root_logger.isEnabledFor(logging.ERROR):\n                opts.append(\"--error\")\n            elif root_logger.isEnabledFor(logging.CRITICAL):\n                opts.append(\"--critical\")\n            else:\n                raise ValueError(\"log level missing, fix this code\")\n\n            # Tell the remote server about debug topics it may need to consider.\n            # Note that debug topics are usable for \"spew\" or \"trace\" logs which would\n            # be too plentiful to transfer for normal use, so the server doesn't send\n            # them unless explicitly enabled.\n            #\n            # Needless to say, if you do --debug-topic=repository.compaction, for example,\n            # with a 1.0.x server it won't work, because the server does not recognize the\n            # option.\n            #\n            # This is not considered a problem, since this is a debugging feature that\n            # should not be used for regular use.\n            for topic in args.debug_topics:\n                if \".\" not in topic:\n                    topic = \"borg.debug.\" + topic\n                if \"repository\" in topic:\n                    opts.append(\"--debug-topic=%s\" % topic)\n        env_vars = []\n        if testing:\n            return env_vars + [sys.executable, \"-m\", \"borg\", \"serve\"] + opts + self.extra_test_args\n        else:  # pragma: no cover\n            remote_path = args.remote_path or os.environ.get(\"BORG_REMOTE_PATH\", \"borg\")\n            remote_path = replace_placeholders(remote_path)\n            return env_vars + [remote_path, \"serve\"] + opts\n\n    def ssh_cmd(self, location):\n        \"\"\"return a ssh command line that can be prefixed to a borg command line\"\"\"\n        rsh = self._args.rsh or os.environ.get(\"BORG_RSH\", \"ssh\")\n        args = shlex.split(rsh)\n        if location.port:\n            args += [\"-p\", str(location.port)]\n        if location.user:\n            args.append(f\"{location.user}@{location.host}\")\n        else:\n            args.append(\"%s\" % location.host)\n        return args\n\n    def call(self, cmd, args, **kw):\n        for resp in self.call_many(cmd, [args], **kw):\n            return resp\n\n    def call_many(self, cmd, calls, wait=True, is_preloaded=False, async_wait=True):\n        if not calls and cmd != \"async_responses\":\n            return\n\n        def send_buffer():\n            if self.to_send:\n                try:\n                    written = self.ratelimit.write(self.stdin_fd, self.to_send.peek_front())\n                    self.tx_bytes += written\n                    self.to_send.pop_front(written)\n                except OSError as e:\n                    # io.write might raise EAGAIN even though select indicates\n                    # that the fd should be writable.\n                    # EWOULDBLOCK is added for defensive programming sake.\n                    if e.errno not in [errno.EAGAIN, errno.EWOULDBLOCK]:\n                        raise\n\n        def pop_preload_msgid(chunkid):\n            msgid = self.chunkid_to_msgids[chunkid].pop(0)\n            if not self.chunkid_to_msgids[chunkid]:\n                del self.chunkid_to_msgids[chunkid]\n            return msgid\n\n        def handle_error(unpacked):\n            if \"exception_class\" not in unpacked:\n                return\n\n            error = unpacked[\"exception_class\"]\n            args = unpacked[\"exception_args\"]\n\n            if error == \"Error\":\n                raise Error(args[0])\n            elif error == \"ErrorWithTraceback\":\n                raise ErrorWithTraceback(args[0])\n            elif error == \"DoesNotExist\":\n                raise LegacyRepository.DoesNotExist(self.location.processed)\n            elif error == \"AlreadyExists\":\n                raise LegacyRepository.AlreadyExists(self.location.processed)\n            elif error == \"CheckNeeded\":\n                raise LegacyRepository.CheckNeeded(self.location.processed)\n            elif error == \"IntegrityError\":\n                raise IntegrityError(args[0])\n            elif error == \"PathNotAllowed\":\n                raise PathNotAllowed(args[0])\n            elif error == \"PathPermissionDenied\":\n                raise LegacyRepository.PathPermissionDenied(args[0])\n            elif error == \"ParentPathDoesNotExist\":\n                raise LegacyRepository.ParentPathDoesNotExist(args[0])\n            elif error == \"ObjectNotFound\":\n                raise LegacyRepository.ObjectNotFound(args[0], self.location.processed)\n            elif error == \"InvalidRPCMethod\":\n                raise InvalidRPCMethod(args[0])\n            elif error == \"LockTimeout\":\n                raise LockTimeout(args[0])\n            elif error == \"LockFailed\":\n                raise LockFailed(args[0], args[1])\n            elif error == \"NotLocked\":\n                raise NotLocked(args[0])\n            elif error == \"NotMyLock\":\n                raise NotMyLock(args[0])\n            else:\n                raise self.RPCError(unpacked)\n\n        calls = list(calls)\n        waiting_for = []\n        maximum_to_send = 0 if wait else self.upload_buffer_size_limit\n        send_buffer()  # Try to send data, as some cases (async_response) will never try to send data otherwise.\n        while wait or calls:\n            if self.shutdown_time and time.monotonic() > self.shutdown_time:\n                # we are shutting this LegacyRemoteRepository down already, make sure we do not waste\n                # a lot of time in case a lot of async stuff is coming in or remote is gone or slow.\n                logger.debug(\n                    \"shutdown_time reached, shutting down with %d waiting_for and %d async_responses.\",\n                    len(waiting_for),\n                    len(self.async_responses),\n                )\n                return\n            while waiting_for:\n                try:\n                    unpacked = self.responses.pop(waiting_for[0])\n                    waiting_for.pop(0)\n                    handle_error(unpacked)\n                    yield unpacked[RESULT]\n                    if not waiting_for and not calls:\n                        return\n                except KeyError:\n                    break\n            if cmd == \"async_responses\":\n                while True:\n                    try:\n                        msgid, unpacked = self.async_responses.popitem()\n                    except KeyError:\n                        # there is nothing left what we already have received\n                        if async_wait and self.ignore_responses:\n                            # but do not return if we shall wait and there is something left to wait for:\n                            break\n                        else:\n                            return\n                    else:\n                        handle_error(unpacked)\n                        yield unpacked[RESULT]\n            if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT):\n                w_fds = [self.stdin_fd]\n            else:\n                w_fds = []\n            r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1)\n            if x:\n                raise Exception(\"FD exception occurred\")\n            for fd in r:\n                if fd is self.stdout_fd:\n                    data = os.read(fd, BUFSIZE)\n                    if not data:\n                        raise ConnectionClosed()\n                    self.rx_bytes += len(data)\n                    self.unpacker.feed(data)\n                    for unpacked in self.unpacker:\n                        if not isinstance(unpacked, dict):\n                            raise UnexpectedRPCDataFormatFromServer(data)\n\n                        lr_dict = unpacked.get(LOG)\n                        if lr_dict is not None:\n                            # Re-emit remote log messages locally.\n                            _logger = logging.getLogger(lr_dict[\"name\"])\n                            if _logger.isEnabledFor(lr_dict[\"level\"]):\n                                _logger.handle(logging.LogRecord(**lr_dict))\n                            continue\n\n                        msgid = unpacked[MSGID]\n                        if msgid in self.ignore_responses:\n                            self.ignore_responses.remove(msgid)\n                            # async methods never return values, but may raise exceptions.\n                            if \"exception_class\" in unpacked:\n                                self.async_responses[msgid] = unpacked\n                            else:\n                                # we currently do not have async result values except \"None\",\n                                # so we do not add them into async_responses.\n                                if unpacked[RESULT] is not None:\n                                    self.async_responses[msgid] = unpacked\n                        else:\n                            self.responses[msgid] = unpacked\n                elif fd is self.stderr_fd:\n                    data = os.read(fd, 32768)\n                    if not data:\n                        raise ConnectionClosed()\n                    self.rx_bytes += len(data)\n                    # deal with incomplete lines (may appear due to block buffering)\n                    if self.stderr_received:\n                        data = self.stderr_received + data\n                        self.stderr_received = b\"\"\n                    lines = data.splitlines(keepends=True)\n                    if lines and not lines[-1].endswith((b\"\\r\", b\"\\n\")):\n                        self.stderr_received = lines.pop()\n                    # now we have complete lines in <lines> and any partial line in self.stderr_received.\n                    _logger = logging.getLogger()\n                    for line in lines:\n                        # borg serve (remote/server side) should not emit stuff on stderr,\n                        # but e.g. the ssh process (local/client side) might output errors there.\n                        assert line.endswith((b\"\\r\", b\"\\n\"))\n                        # something came in on stderr, log it to not lose it.\n                        # decode late, avoid partial utf-8 sequences.\n                        _logger.warning(\"stderr: \" + line.decode().strip())\n            if w:\n                while (\n                    (len(self.to_send) <= maximum_to_send)\n                    and (calls or self.preload_ids)\n                    and len(waiting_for) < MAX_INFLIGHT\n                ):\n                    if calls:\n                        if is_preloaded:\n                            assert cmd == \"get\", \"is_preload is only supported for 'get'\"\n                            if calls[0][\"id\"] in self.chunkid_to_msgids:\n                                waiting_for.append(pop_preload_msgid(calls.pop(0)[\"id\"]))\n                        else:\n                            args = calls.pop(0)\n                            if cmd == \"get\" and args[\"id\"] in self.chunkid_to_msgids:\n                                waiting_for.append(pop_preload_msgid(args[\"id\"]))\n                            else:\n                                self.msgid += 1\n                                waiting_for.append(self.msgid)\n                                self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: cmd, ARGS: args}))\n                    if not self.to_send and self.preload_ids:\n                        chunk_id = self.preload_ids.pop(0)\n                        args = {\"id\": chunk_id}\n                        self.msgid += 1\n                        self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid)\n                        self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: \"get\", ARGS: args}))\n\n                send_buffer()\n        self.ignore_responses |= set(waiting_for)  # we lose order here\n\n    @api(since=parse_version(\"1.0.0\"), v1_or_v2={\"since\": parse_version(\"2.0.0b10\"), \"previously\": True})\n    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, v1_or_v2=False):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0a3\"))\n    def info(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"), max_duration={\"since\": parse_version(\"1.2.0a4\"), \"previously\": 0})\n    def check(self, repair=False, max_duration=0):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(\n        since=parse_version(\"1.0.0\"),\n        compact={\"since\": parse_version(\"1.2.0a0\"), \"previously\": True, \"dontcare\": True},\n        threshold={\"since\": parse_version(\"1.2.0a8\"), \"previously\": 0.1, \"dontcare\": True},\n    )\n    def commit(self, compact=True, threshold=0.1):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def rollback(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def destroy(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def __len__(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def list(self, limit=None, marker=None):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    def get(self, id, read_data=True, raise_missing=True):\n        for resp in self.get_many([id], read_data=read_data, raise_missing=raise_missing):\n            return resp\n\n    def get_many(self, ids, read_data=True, is_preloaded=False, raise_missing=True):\n        # note: legacy remote protocol does not support raise_missing parameter, so we ignore it here\n        yield from self.call_many(\"get\", [{\"id\": id, \"read_data\": read_data} for id in ids], is_preloaded=is_preloaded)\n\n    @api(since=parse_version(\"1.0.0\"))\n    def put(self, id, data, wait=True):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def delete(self, id, wait=True):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def save_key(self, keydata):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def load_key(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def break_lock(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    def close(self):\n        if self.p or self.sock:\n            self.call(\"close\", {}, wait=True)\n        if self.p:\n            self.p.stdin.close()\n            self.p.stdout.close()\n            self.p.wait()\n            self.p = None\n        if self.sock:\n            try:\n                self.sock.shutdown(socket.SHUT_RDWR)\n            except OSError as e:\n                if e.errno != errno.ENOTCONN:\n                    raise\n            self.sock.close()\n            self.sock = None\n\n    def async_response(self, wait=True):\n        for resp in self.call_many(\"async_responses\", calls=[], wait=True, async_wait=wait):\n            return resp\n\n    def preload(self, ids):\n        self.preload_ids += ids\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def get_manifest(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def put_manifest(self, data):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n\nclass RepositoryNoCache:\n    \"\"\"A not caching Repository wrapper, passes through to repository.\n\n    Just to have same API (including the context manager) as RepositoryCache.\n\n    *transform* is a callable taking two arguments, key and raw repository data.\n    The return value is returned from get()/get_many(). By default, the raw\n    repository data is returned.\n    \"\"\"\n\n    def __init__(self, repository, transform=None):\n        self.repository = repository\n        self.transform = transform or (lambda key, data: data)\n\n    def close(self):\n        pass\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def get(self, key, read_data=True, raise_missing=True):\n        return next(self.get_many([key], read_data=read_data, raise_missing=raise_missing, cache=False))\n\n    def get_many(self, keys, read_data=True, cache=True, raise_missing=True):\n        for key, data in zip(keys, self.repository.get_many(keys, read_data=read_data, raise_missing=raise_missing)):\n            yield self.transform(key, data)\n\n    def log_instrumentation(self):\n        pass\n\n\nclass RepositoryCache(RepositoryNoCache):\n    \"\"\"\n    A caching Repository wrapper.\n\n    Caches Repository GET operations locally.\n\n    *pack* and *unpack* complement *transform* of the base class.\n    *pack* receives the output of *transform* and should return bytes,\n    which are stored in the cache. *unpack* receives these bytes and\n    should return the initial data (as returned by *transform*).\n    \"\"\"\n\n    def __init__(self, repository, pack=None, unpack=None, transform=None):\n        super().__init__(repository, transform)\n        self.pack = pack or (lambda data: data)\n        self.unpack = unpack or (lambda data: data)\n        self.cache = set()\n        self.basedir = tempfile.mkdtemp(prefix=\"borg-cache-\")\n        self.query_size_limit()\n        self.size = 0\n        # Instrumentation\n        self.hits = 0\n        self.misses = 0\n        self.slow_misses = 0\n        self.slow_lat = 0.0\n        self.evictions = 0\n        self.enospc = 0\n\n    def query_size_limit(self):\n        available_space = shutil.disk_usage(self.basedir).free\n        self.size_limit = int(min(available_space * 0.25, 2**31))\n\n    def prefixed_key(self, key, complete):\n        # just prefix another byte telling whether this key refers to a complete chunk\n        # or a without-data-metadata-only chunk (see also read_data param).\n        prefix = b\"\\x01\" if complete else b\"\\x00\"\n        return prefix + key\n\n    def key_filename(self, key):\n        return os.path.join(self.basedir, bin_to_hex(key))\n\n    def backoff(self):\n        self.query_size_limit()\n        target_size = int(0.9 * self.size_limit)\n        while self.size > target_size and self.cache:\n            key = self.cache.pop()\n            file = self.key_filename(key)\n            self.size -= os.stat(file).st_size\n            os.unlink(file)\n            self.evictions += 1\n\n    def add_entry(self, key, data, cache, complete):\n        transformed = self.transform(key, data)\n        if not cache:\n            return transformed\n        packed = self.pack(transformed)\n        pkey = self.prefixed_key(key, complete=complete)\n        file = self.key_filename(pkey)\n        try:\n            with open(file, \"wb\") as fd:\n                fd.write(packed)\n        except OSError as os_error:\n            try:\n                safe_unlink(file)\n            except FileNotFoundError:\n                pass  # open() could have failed as well\n            if os_error.errno == errno.ENOSPC:\n                self.enospc += 1\n                self.backoff()\n            else:\n                raise\n        else:\n            self.size += len(packed)\n            self.cache.add(pkey)\n            if self.size > self.size_limit:\n                self.backoff()\n        return transformed\n\n    def log_instrumentation(self):\n        logger.debug(\n            \"RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), \"\n            \"%d evictions, %d ENOSPC hit\",\n            len(self.cache),\n            format_file_size(self.size),\n            format_file_size(self.size_limit),\n            self.hits,\n            self.misses,\n            self.slow_misses,\n            self.slow_lat,\n            self.evictions,\n            self.enospc,\n        )\n\n    def close(self):\n        self.log_instrumentation()\n        self.cache.clear()\n        shutil.rmtree(self.basedir)\n\n    def get_many(self, keys, read_data=True, cache=True, raise_missing=True):\n        # It could use different cache keys depending on read_data and cache full vs. meta-only chunks.\n        unknown_keys = [key for key in keys if self.prefixed_key(key, complete=read_data) not in self.cache]\n        repository_iterator = zip(\n            unknown_keys, self.repository.get_many(unknown_keys, read_data=read_data, raise_missing=raise_missing)\n        )\n        for key in keys:\n            pkey = self.prefixed_key(key, complete=read_data)\n            if pkey in self.cache:\n                file = self.key_filename(pkey)\n                with open(file, \"rb\") as fd:\n                    self.hits += 1\n                    yield self.unpack(fd.read())\n            else:\n                for key_, data in repository_iterator:\n                    if key_ == key:\n                        transformed = self.add_entry(key, data, cache, complete=read_data)\n                        self.misses += 1\n                        yield transformed\n                        break\n                else:\n                    # slow path: eviction during this get_many removed this key from the cache\n                    t0 = time.perf_counter()\n                    data = self.repository.get(key, read_data=read_data, raise_missing=raise_missing)\n                    self.slow_lat += time.perf_counter() - t0\n                    transformed = self.add_entry(key, data, cache, complete=read_data)\n                    self.slow_misses += 1\n                    yield transformed\n        # Consume any pending requests\n        for _ in repository_iterator:\n            pass\n\n\ndef cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None, force_cache=False):\n    \"\"\"\n    Return a Repository(No)Cache for *repository*.\n\n    If *decrypted_cache* is a repo_objs object, then get and get_many will return a tuple\n    (csize, plaintext) instead of the actual data in the repository. The cache will\n    store decrypted data, which increases CPU efficiency (by avoiding repeatedly decrypting\n    and more importantly MAC and ID checking cached objects).\n    Internally, objects are compressed with LZ4.\n    \"\"\"\n    if decrypted_cache and (pack or unpack or transform):\n        raise ValueError(\"decrypted_cache and pack/unpack/transform are incompatible\")\n    elif decrypted_cache:\n        repo_objs = decrypted_cache\n        # 32 bit csize, 64 bit (8 byte) xxh64, 1 byte ctype, 1 byte clevel\n        cache_struct = struct.Struct(\"=I8sBB\")\n        compressor = Compressor(\"lz4\")\n\n        def pack(data):\n            csize, decrypted = data\n            meta, compressed = compressor.compress({}, decrypted)\n            return cache_struct.pack(csize, xxh64(compressed).digest(), meta[\"ctype\"], meta[\"clevel\"]) + compressed\n\n        def unpack(data):\n            data = memoryview(data)\n            csize, checksum, ctype, clevel = cache_struct.unpack(data[: cache_struct.size])\n            compressed = data[cache_struct.size :]\n            if checksum != xxh64(compressed).digest():\n                raise IntegrityError(\"detected corrupted data in metadata cache\")\n            meta = dict(ctype=ctype, clevel=clevel, csize=len(compressed))\n            _, decrypted = compressor.decompress(meta, compressed)\n            return csize, decrypted\n\n        def transform(id_, data):\n            meta, decrypted = repo_objs.parse(id_, data, ro_type=ROBJ_DONTCARE)\n            csize = meta.get(\"csize\", len(data))\n            return csize, decrypted\n\n    if isinstance(repository, LegacyRemoteRepository) or force_cache:\n        return RepositoryCache(repository, pack, unpack, transform)\n    else:\n        return RepositoryNoCache(repository, transform)\n"
  },
  {
    "path": "src/borg/legacyrepository.py",
    "content": "import errno\nimport mmap\nimport os\nimport shutil\nimport stat\nimport struct\nimport time\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom configparser import ConfigParser\nfrom functools import partial\nfrom itertools import islice\nfrom collections.abc import Callable\n\nimport xxhash\n\nfrom .constants import *  # NOQA\nfrom .hashindex import NSIndex1Entry, NSIndex1\nfrom .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size\nfrom .helpers import Location\nfrom .helpers import ProgressIndicatorPercent\nfrom .helpers import bin_to_hex, hex_to_bin\nfrom .helpers import secure_erase, safe_unlink\nfrom .helpers import msgpack\nfrom .helpers.lrucache import LRUCache\nfrom .fslocking import Lock, LockError, LockErrorT\nfrom .logger import create_logger\nfrom .manifest import Manifest, NoManifestError\nfrom .platform import SaveFile, SyncFile, sync_dir, safe_fadvise\nfrom .repoobj import RepoObj\nfrom .checksums import crc32\nfrom .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError\n\nlogger = create_logger(__name__)\n\nMAGIC = b\"BORG_SEG\"\nMAGIC_LEN = len(MAGIC)\n\nTAG_PUT = 0\nTAG_DELETE = 1\nTAG_COMMIT = 2\nTAG_PUT2 = 3\n\n# Highest ID usable as TAG_* value\n#\n# Code may expect not to find any tags exceeding this value. In particular,\n# in order to speed up `borg check --repair`, any tag greater than MAX_TAG_ID\n# is assumed to be corrupted. When increasing this value, in order to add more\n# tags, keep in mind that old versions of Borg accessing a new repository\n# may not be able to handle the new tags.\nMAX_TAG_ID = 15\n\nFreeSpace: Callable[[], defaultdict] = partial(defaultdict, int)\n\n\ndef header_size(tag):\n    if tag == TAG_PUT2:\n        size = LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE\n    elif tag == TAG_PUT or tag == TAG_DELETE:\n        size = LoggedIO.HEADER_ID_SIZE\n    elif tag == TAG_COMMIT:\n        size = LoggedIO.header_fmt.size\n    else:\n        raise ValueError(f\"unsupported tag: {tag!r}\")\n    return size\n\n\nclass LegacyRepository:\n    \"\"\"\n    Filesystem-based transactional key-value store.\n\n    Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files\n    called segments. Each segment is a series of log entries. The segment number together with the offset of each\n    entry relative to its segment start establishes an ordering of the log entries. This is the \"definition\" of\n    time for the purposes of the log.\n\n    Log entries are either PUT, DELETE, or COMMIT.\n\n    A COMMIT is always the final log entry in a segment and marks all data from the beginning of the log until the\n    segment ending with the COMMIT as committed and consistent. The segment number of a segment ending with a COMMIT\n    is called the transaction ID of that commit, and a segment ending with a COMMIT is called committed.\n\n    When reading from a repository it is first checked whether the last segment is committed. If it is not, then\n    all segments after the last committed segment are deleted; they contain log entries whose consistency is not\n    established by a COMMIT.\n\n    Note that the COMMIT cannot establish consistency by itself, but only manages to do so with proper support from\n    the platform (including the hardware). See platform.base.SyncFile for details.\n\n    A PUT inserts a key-value pair. The value is stored in the log entry, hence the repository implements\n    full data logging, meaning that all data is consistent, not just metadata (which is common in filesystems).\n\n    A DELETE marks a key as deleted.\n\n    For a given key only the last entry regarding the key, which is called current (all other entries are called\n    superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist.\n    Otherwise the last PUT defines the value of the key.\n\n    By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing\n    such obsolete entries is called sparse, while a segment containing no such entries is called compact.\n\n    Sparse segments can be compacted and thereby disk space freed. This destroys the transaction for which the\n    superseded entries were current.\n\n    On-disk layout:\n\n    dir/README\n    dir/config\n    dir/data/<X // SEGMENTS_PER_DIR>/<X>\n    dir/index.X\n    dir/hints.X\n\n    Filesystem interaction\n    ----------------------\n\n    LoggedIO generally tries to rely on common behaviours across transactional file systems.\n\n    Segments that are deleted are truncated first, which avoids problems if the FS needs to\n    allocate space to delete the dirent of the segment. This mostly affects CoW file systems,\n    traditional journaling file systems have a fairly good grip on this problem.\n\n    Note that deletion, i.e. unlink(2), is atomic on every file system that uses inode reference\n    counts, which includes pretty much all of them. To remove a dirent the inodes refcount has\n    to be decreased, but you can't decrease the refcount before removing the dirent nor can you\n    decrease the refcount after removing the dirent. File systems solve this with a lock,\n    and by ensuring it all stays within the same FS transaction.\n\n    Truncation is generally not atomic in itself, and combining truncate(2) and unlink(2) is of\n    course never guaranteed to be atomic. Truncation in a classic extent-based FS is done in\n    roughly two phases, first the extents are removed then the inode is updated. (In practice\n    this is of course way more complex).\n\n    LoggedIO gracefully handles truncate/unlink splits as long as the truncate resulted in\n    a zero length file. Zero length segments are considered not to exist, while LoggedIO.cleanup()\n    will still get rid of them.\n    \"\"\"\n\n    class AlreadyExists(Error):\n        \"\"\"A repository already exists at {}.\"\"\"\n\n        exit_mcode = 10\n\n    class CheckNeeded(ErrorWithTraceback):\n        \"\"\"Inconsistency detected. Please run \"borg check {}\".\"\"\"\n\n        exit_mcode = 12\n\n    class DoesNotExist(Error):\n        \"\"\"Repository {} does not exist.\"\"\"\n\n        exit_mcode = 13\n\n    class InsufficientFreeSpaceError(Error):\n        \"\"\"Insufficient free space to complete transaction (required: {}, available: {}).\"\"\"\n\n        exit_mcode = 14\n\n    class InvalidRepository(Error):\n        \"\"\"{} is not a valid repository. Check repo config.\"\"\"\n\n        exit_mcode = 15\n\n    class InvalidRepositoryConfig(Error):\n        \"\"\"{} does not have a valid configuration. Check repo config [{}].\"\"\"\n\n        exit_mcode = 16\n\n    class ObjectNotFound(ErrorWithTraceback):\n        \"\"\"Object with key {} not found in repository {}.\"\"\"\n\n        exit_mcode = 17\n\n        def __init__(self, id, repo):\n            if isinstance(id, bytes):\n                id = bin_to_hex(id)\n            super().__init__(id, repo)\n\n    class ParentPathDoesNotExist(Error):\n        \"\"\"The parent path of the repo directory [{}] does not exist.\"\"\"\n\n        exit_mcode = 18\n\n    class PathAlreadyExists(Error):\n        \"\"\"There is already something at {}.\"\"\"\n\n        exit_mcode = 19\n\n    # StorageQuotaExceeded was exit_mcode = 20\n\n    class PathPermissionDenied(Error):\n        \"\"\"Permission denied to {}.\"\"\"\n\n        exit_mcode = 21\n\n    def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, send_log_cb=None):\n        p = Path(path).absolute()\n        self.path = str(p)\n        self._location = Location(p.as_uri())\n        self.version = None\n        # long-running repository methods which emit log or progress output are responsible for calling\n        # the ._send_log method periodically to get log and progress output transferred to the borg client\n        # in a timely manner, in case we have a LegacyRemoteRepository.\n        # for local repositories ._send_log can be called also (it will just do nothing in that case).\n        self._send_log = send_log_cb or (lambda: None)\n        self.io = None  # type: LoggedIO\n        self.lock = None\n        self.index = None\n        # This is an index of shadowed log entries during this transaction. Consider the following sequence:\n        # segment_n PUT A, segment_x DELETE A\n        # After the \"DELETE A\" in segment_x the shadow index will contain \"A -> [n]\".\n        # .delete() is updating this index, it is persisted into \"hints\" file and is later used by .compact_segments().\n        # We need the entries in the shadow_index to not accidentally drop the \"DELETE A\" when we compact segment_x\n        # only (and we do not compact segment_n), because DELETE A is still needed then because PUT A will be still\n        # there. Otherwise chunk A would reappear although it was previously deleted.\n        self.shadow_index = {}\n        self._active_txn = False\n        self.lock_wait = lock_wait\n        self.do_lock = lock\n        self.do_create = create\n        self.created = False\n        self.exclusive = exclusive\n        self.transaction_doomed = None\n        # v2 is the default repo version for borg 2.0\n        # v1 repos must only be used in a read-only way, e.g. for\n        # --other-repo=V1_REPO with borg init and borg transfer!\n        self.acceptable_repo_versions = (1, 2)\n\n    def __del__(self):\n        if self.lock:\n            self.close()\n            assert False, \"cleanup happened in Repository.__del__\"\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {self.path}>\"\n\n    def __enter__(self):\n        if self.do_create:\n            self.do_create = False\n            self.create(self.path)\n            self.created = True\n        self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if exc_type is not None:\n            no_space_left_on_device = exc_type is OSError and exc_val.errno == errno.ENOSPC\n            # The ENOSPC could have originated somewhere else besides the Repository. The cleanup is always safe, unless\n            # EIO or FS corruption ensues, which is why we specifically check for ENOSPC.\n            if self._active_txn and no_space_left_on_device:\n                logger.warning(\"No space left on device, cleaning up partial transaction to free space.\")\n                cleanup = True\n            else:\n                cleanup = False\n            self._rollback(cleanup=cleanup)\n        self.close()\n\n    @property\n    def id_str(self):\n        return bin_to_hex(self.id)\n\n    @staticmethod\n    def is_repository(path):\n        \"\"\"Check whether there is already a Borg repository at *path*.\"\"\"\n        try:\n            # Use binary mode to avoid troubles if a README contains some stuff not in our locale\n            with open(os.path.join(path, \"README\"), \"rb\") as fd:\n                # Read only the first ~100 bytes (if any), in case some README file we stumble upon is large.\n                readme_head = fd.read(100)\n                # The first comparison captures our current variant (REPOSITORY_README), the second comparison\n                # is an older variant of the README file (used by 1.0.x).\n                return b\"Borg Backup repository\" in readme_head or b\"Borg repository\" in readme_head\n        except OSError:\n            # Ignore FileNotFound, PermissionError, ...\n            return False\n\n    def check_can_create_repository(self, path):\n        \"\"\"\n        Raise an exception if a repository already exists at *path* or any parent directory.\n\n        Checking parent directories is done because it's just a weird thing to do, and usually not intended.\n        A Borg using the \"parent\" repository may be confused, or we may accidentally put stuff into the \"data/\" or\n        \"data/<n>/\" directories.\n        \"\"\"\n        try:\n            st = os.stat(path)\n        except FileNotFoundError:\n            pass  # nothing there!\n        except PermissionError:\n            raise self.PathPermissionDenied(path) from None\n        else:\n            # there is something already there!\n            if self.is_repository(path):\n                raise self.AlreadyExists(path)\n            if not stat.S_ISDIR(st.st_mode):\n                raise self.PathAlreadyExists(path)\n            try:\n                files = os.listdir(path)\n            except PermissionError:\n                raise self.PathPermissionDenied(path) from None\n            else:\n                if files:  # a dir, but not empty\n                    raise self.PathAlreadyExists(path)\n                else:  # an empty directory is acceptable for us.\n                    pass\n\n        while True:\n            # Check all parent directories for Borg's repository README\n            previous_path = path\n            # Thus, path = previous_path/..\n            path = os.path.abspath(os.path.join(previous_path, os.pardir))\n            if path == previous_path:\n                # We reached the root of the directory hierarchy (/.. = / and C:\\.. = C:\\).\n                break\n            if self.is_repository(path):\n                raise self.AlreadyExists(path)\n\n    def create(self, path):\n        \"\"\"Create a new empty repository at `path`\"\"\"\n        self.check_can_create_repository(path)\n        os.makedirs(path, exist_ok=True)\n        with open(os.path.join(path, \"README\"), \"w\") as fd:\n            fd.write(REPOSITORY_README)\n        os.mkdir(os.path.join(path, \"data\"))\n        config = ConfigParser(interpolation=None)\n        config.add_section(\"repository\")\n        self.version = 2\n        config.set(\"repository\", \"version\", str(self.version))\n        config.set(\"repository\", \"segments_per_dir\", str(DEFAULT_SEGMENTS_PER_DIR))\n        config.set(\"repository\", \"max_segment_size\", str(DEFAULT_MAX_SEGMENT_SIZE))\n        config.set(\"repository\", \"additional_free_space\", \"0\")\n        config.set(\"repository\", \"id\", bin_to_hex(os.urandom(32)))\n        self.save_config(path, config)\n\n    def save_config(self, path, config):\n        config_path = os.path.join(path, \"config\")\n        old_config_path = os.path.join(path, \"config.old\")\n\n        if os.path.isfile(old_config_path):\n            logger.warning(\"Old config file not securely erased on previous config update\")\n            secure_erase(old_config_path, avoid_collateral_damage=True)\n\n        if os.path.isfile(config_path):\n            link_error_msg = (\n                \"Failed to erase old repository config file securely (hard links not supported). \"\n                \"Old repokey data, if any, might persist on physical storage.\"\n            )\n            try:\n                os.link(config_path, old_config_path)\n            except OSError as e:\n                if e.errno in (errno.EMLINK, errno.ENOSYS, errno.EPERM, errno.EACCES, errno.ENOTSUP, errno.EIO):\n                    logger.warning(link_error_msg)\n                else:\n                    raise\n            except AttributeError:\n                # some python ports have no os.link, see #4901\n                logger.warning(link_error_msg)\n\n        try:\n            with SaveFile(config_path) as fd:\n                config.write(fd)\n        except PermissionError as e:\n            # error is only a problem if we even had a lock\n            if self.do_lock:\n                raise\n            logger.warning(\n                \"%s: Failed writing to '%s'. This is expected when working on \"\n                \"read-only repositories.\" % (e.strerror, e.filename)\n            )\n\n        if os.path.isfile(old_config_path):\n            secure_erase(old_config_path, avoid_collateral_damage=True)\n\n    def save_key(self, keydata):\n        assert self.config\n        keydata = keydata.decode(\"utf-8\")  # remote repo: msgpack issue #99, getting bytes\n        # note: saving an empty key means that there is no repokey any more\n        self.config.set(\"repository\", \"key\", keydata)\n        self.save_config(self.path, self.config)\n\n    def load_key(self):\n        keydata = self.config.get(\"repository\", \"key\", fallback=\"\").strip()\n        # note: if we return an empty string, it means there is no repo key\n        return keydata.encode(\"utf-8\")  # remote repo: msgpack issue #99, returning bytes\n\n    def destroy(self):\n        \"\"\"Destroy the repository at `self.path`\"\"\"\n        self.close()\n        os.remove(os.path.join(self.path, \"config\"))  # kill config first\n        shutil.rmtree(self.path)\n\n    def get_index_transaction_id(self):\n        indices = sorted(\n            int(fn[6:])\n            for fn in os.listdir(self.path)\n            if fn.startswith(\"index.\") and fn[6:].isdigit() and os.stat(os.path.join(self.path, fn)).st_size != 0\n        )\n        if indices:\n            return indices[-1]\n        else:\n            return None\n\n    def check_transaction(self):\n        index_transaction_id = self.get_index_transaction_id()\n        segments_transaction_id = self.io.get_segments_transaction_id()\n        if index_transaction_id is not None and segments_transaction_id is None:\n            # we have a transaction id from the index, but we did not find *any*\n            # commit in the segment files (thus no segments transaction id).\n            # this can happen if a lot of segment files are lost, e.g. due to a\n            # filesystem or hardware malfunction. it means we have no identifiable\n            # valid (committed) state of the repo which we could use.\n            msg = '%s\" - although likely this is \"beyond repair' % self.path  # dirty hack\n            raise self.CheckNeeded(msg)\n        # Attempt to rebuild index automatically if we crashed between commit\n        # tag write and index save.\n        if index_transaction_id != segments_transaction_id:\n            if index_transaction_id is not None and index_transaction_id > segments_transaction_id:\n                replay_from = None\n            else:\n                replay_from = index_transaction_id\n            self.replay_segments(replay_from, segments_transaction_id)\n\n    def get_transaction_id(self):\n        self.check_transaction()\n        return self.get_index_transaction_id()\n\n    def break_lock(self):\n        Lock(os.path.join(self.path, \"lock\")).break_lock()\n\n    def migrate_lock(self, old_id, new_id):\n        # note: only needed for local repos\n        if self.lock is not None:\n            self.lock.migrate_lock(old_id, new_id)\n\n    def open(self, path, exclusive, lock_wait=None, lock=True):\n        self.path = path\n        try:\n            st = os.stat(path)\n        except FileNotFoundError:\n            raise self.DoesNotExist(path)\n        if not stat.S_ISDIR(st.st_mode):\n            raise self.InvalidRepository(path)\n        if lock:\n            self.lock = Lock(os.path.join(path, \"lock\"), exclusive, timeout=lock_wait).acquire()\n        else:\n            self.lock = None\n        self.config = ConfigParser(interpolation=None)\n        try:\n            with open(os.path.join(self.path, \"config\")) as fd:\n                self.config.read_file(fd)\n        except FileNotFoundError:\n            self.close()\n            raise self.InvalidRepository(self.path)\n        if \"repository\" not in self.config.sections():\n            self.close()\n            raise self.InvalidRepositoryConfig(path, \"no repository section found\")\n        self.version = self.config.getint(\"repository\", \"version\")\n        if self.version not in self.acceptable_repo_versions:\n            self.close()\n            raise self.InvalidRepositoryConfig(\n                path, \"repository version %d is not supported by this borg version\" % self.version\n            )\n        self.max_segment_size = parse_file_size(self.config.get(\"repository\", \"max_segment_size\"))\n        if self.max_segment_size >= MAX_SEGMENT_SIZE_LIMIT:\n            self.close()\n            raise self.InvalidRepositoryConfig(path, \"max_segment_size >= %d\" % MAX_SEGMENT_SIZE_LIMIT)  # issue 3592\n        self.segments_per_dir = self.config.getint(\"repository\", \"segments_per_dir\")\n        self.additional_free_space = parse_file_size(self.config.get(\"repository\", \"additional_free_space\", fallback=0))\n        self.id = hex_to_bin(self.config.get(\"repository\", \"id\").strip(), length=32)\n        self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir)\n\n    def _load_hints(self):\n        if (transaction_id := self.get_transaction_id()) is None:\n            # self is a fresh repo, so transaction_id is None and there is no hints file\n            return\n        hints = self._unpack_hints(transaction_id)\n        self.version = hints[\"version\"]\n        self.shadow_index = hints[\"shadow_index\"]\n\n    def info(self):\n        \"\"\"return some infos about the repo (must be opened first)\"\"\"\n        info = dict(id=self.id, version=self.version)\n        self._load_hints()\n        return info\n\n    def close(self):\n        if self.lock:\n            if self.io:\n                self.io.close()\n            self.io = None\n            self.lock.release()\n            self.lock = None\n\n    def commit(self, compact=True, threshold=0.1):\n        \"\"\"Commit transaction\"\"\"\n        if self.transaction_doomed:\n            exception = self.transaction_doomed\n            self.rollback()\n            raise exception\n        self.check_free_space()\n        segment = self.io.write_commit()\n        self.segments.setdefault(segment, 0)\n        self.compact[segment] += LoggedIO.header_fmt.size\n        if compact:\n            self.compact_segments(threshold)\n        self.write_index()\n        self.rollback()\n\n    def _read_integrity(self, transaction_id, key):\n        integrity_file = \"integrity.%d\" % transaction_id\n        integrity_path = os.path.join(self.path, integrity_file)\n        try:\n            with open(integrity_path, \"rb\") as fd:\n                integrity = msgpack.unpack(fd)\n        except FileNotFoundError:\n            return\n        if integrity.get(\"version\") != 2:\n            logger.warning(\"Unknown integrity data version %r in %s\", integrity.get(\"version\"), integrity_file)\n            return\n        return integrity[key]\n\n    def open_index(self, transaction_id):\n        if transaction_id is None:\n            return NSIndex1()\n        index_path = os.path.join(self.path, \"index.%d\" % transaction_id)\n        integrity_data = self._read_integrity(transaction_id, \"index\")\n        with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd:\n            return NSIndex1.read(fd)\n\n    def _unpack_hints(self, transaction_id):\n        hints_path = os.path.join(self.path, \"hints.%d\" % transaction_id)\n        integrity_data = self._read_integrity(transaction_id, \"hints\")\n        with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd:\n            return msgpack.unpack(fd)\n\n    def prepare_txn(self, transaction_id, do_cleanup=True):\n        self._active_txn = True\n        if self.do_lock and not self.lock.got_exclusive_lock():\n            if self.exclusive is not None:\n                # self.exclusive is either True or False, thus a new client is active here.\n                # if it is False and we get here, the caller did not use exclusive=True although\n                # it is needed for a write operation. if it is True and we get here, something else\n                # went very wrong, because we should have an exclusive lock, but we don't.\n                raise AssertionError(\"bug in code, exclusive lock should exist here\")\n            # if we are here, this is an old client talking to a new server (expecting lock upgrade).\n            # or we are replaying segments and might need a lock upgrade for that.\n            try:\n                self.lock.upgrade()\n            except (LockError, LockErrorT):\n                # if upgrading the lock to exclusive fails, we do not have an\n                # active transaction. this is important for \"serve\" mode, where\n                # the repository instance lives on - even if exceptions happened.\n                self._active_txn = False\n                raise\n        if not self.index or transaction_id is None:\n            try:\n                self.index = self.open_index(transaction_id)\n            except (ValueError, OSError, FileIntegrityError) as exc:\n                logger.warning(\"Checking repository transaction due to previous error: %s\", exc)\n                self.check_transaction()\n                self.index = self.open_index(transaction_id)\n        if transaction_id is None:\n            self.segments = {}  # XXX bad name: usage_count_of_segment_x = self.segments[x]\n            self.compact = FreeSpace()  # XXX bad name: freeable_space_of_segment_x = self.compact[x]\n            self.shadow_index.clear()\n        else:\n            if do_cleanup:\n                self.io.cleanup(transaction_id)\n            hints_path = os.path.join(self.path, \"hints.%d\" % transaction_id)\n            index_path = os.path.join(self.path, \"index.%d\" % transaction_id)\n            try:\n                hints = self._unpack_hints(transaction_id)\n            except (msgpack.UnpackException, FileNotFoundError, FileIntegrityError) as e:\n                logger.warning(\"Repository hints file missing or corrupted, trying to recover: %s\", e)\n                if not isinstance(e, FileNotFoundError):\n                    os.unlink(hints_path)\n                # index must exist at this point\n                os.unlink(index_path)\n                self.check_transaction()\n                self.prepare_txn(transaction_id)\n                return\n            if hints[\"version\"] == 1:\n                logger.debug(\"Upgrading from v1 hints.%d\", transaction_id)\n                self.segments = hints[\"segments\"]\n                self.compact = FreeSpace()\n                self.shadow_index = {}\n                for segment in sorted(hints[\"compact\"]):\n                    logger.debug(\"Rebuilding sparse info for segment %d\", segment)\n                    self._rebuild_sparse(segment)\n                logger.debug(\"Upgrade to v2 hints complete\")\n            elif hints[\"version\"] != 2:\n                raise ValueError(\"Unknown hints file version: %d\" % hints[\"version\"])\n            else:\n                self.segments = hints[\"segments\"]\n                self.compact = FreeSpace(hints[\"compact\"])\n                self.shadow_index = hints.get(\"shadow_index\", {})\n            # Drop uncommitted segments in the shadow index\n            for key, shadowed_segments in self.shadow_index.items():\n                for segment in list(shadowed_segments):\n                    if segment > transaction_id:\n                        shadowed_segments.remove(segment)\n\n    def write_index(self):\n        def flush_and_sync(fd):\n            fd.flush()\n            os.fsync(fd.fileno())\n\n        def rename_tmp(file):\n            os.replace(file + \".tmp\", file)\n\n        hints = {\"version\": 2, \"segments\": self.segments, \"compact\": self.compact, \"shadow_index\": self.shadow_index}\n        integrity = {\n            # Integrity version started at 2, the current hints version.\n            # Thus, integrity version == hints version, for now.\n            \"version\": 2\n        }\n        transaction_id = self.io.get_segments_transaction_id()\n        assert transaction_id is not None\n\n        # Write hints file\n        hints_name = \"hints.%d\" % transaction_id\n        hints_file = os.path.join(self.path, hints_name)\n        with IntegrityCheckedFile(hints_file + \".tmp\", filename=hints_name, write=True) as fd:\n            msgpack.pack(hints, fd)\n            flush_and_sync(fd)\n        integrity[\"hints\"] = fd.integrity_data\n\n        # Write repository index\n        index_name = \"index.%d\" % transaction_id\n        index_file = os.path.join(self.path, index_name)\n        with IntegrityCheckedFile(index_file + \".tmp\", filename=index_name, write=True) as fd:\n            # XXX: Consider using SyncFile for index write-outs.\n            self.index.write(fd)\n            flush_and_sync(fd)\n        integrity[\"index\"] = fd.integrity_data\n\n        # Write integrity file, containing checksums of the hints and index files\n        integrity_name = \"integrity.%d\" % transaction_id\n        integrity_file = os.path.join(self.path, integrity_name)\n        with open(integrity_file + \".tmp\", \"wb\") as fd:\n            msgpack.pack(integrity, fd)\n            flush_and_sync(fd)\n\n        # Rename the integrity file first\n        rename_tmp(integrity_file)\n        sync_dir(self.path)\n        # Rename the others after the integrity file is hypothetically on disk\n        rename_tmp(hints_file)\n        rename_tmp(index_file)\n        sync_dir(self.path)\n\n        # Remove old auxiliary files\n        current = \".%d\" % transaction_id\n        for name in os.listdir(self.path):\n            if not name.startswith((\"index.\", \"hints.\", \"integrity.\")):\n                continue\n            if name.endswith(current):\n                continue\n            os.unlink(os.path.join(self.path, name))\n        self.index = None\n\n    def check_free_space(self):\n        \"\"\"Pre-commit check for sufficient free space necessary to perform the commit.\"\"\"\n        # As a baseline we take four times the current (on-disk) index size.\n        # At this point the index may only be updated by compaction, which won't resize it.\n        # We still apply a factor of four so that a later, separate invocation can free space\n        # (journaling all deletes for all chunks is one index size) or still make minor additions\n        # (which may grow the index up to twice its current size).\n        # Note that in a subsequent operation the committed index is still on-disk, therefore we\n        # arrive at index_size * (1 + 2 + 1).\n        # In that order: journaled deletes (1), hashtable growth (2), persisted index (1).\n        required_free_space = self.index.size() * 4\n\n        # Conservatively estimate hints file size:\n        # 10 bytes for each segment-refcount pair, 10 bytes for each segment-space pair\n        # Assume maximum of 5 bytes per integer. Segment numbers will usually be packed more densely (1-3 bytes),\n        # as will refcounts and free space integers. For 5 MiB segments this estimate is good to ~20 PB repo size.\n        # Add a generous 4K to account for constant format overhead.\n        hints_size = len(self.segments) * 10 + len(self.compact) * 10 + 4096\n        required_free_space += hints_size\n\n        required_free_space += self.additional_free_space\n        if True:\n            full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE\n            if len(self.compact) < 10:\n                # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying\n                # if TMP is a small-ish tmpfs.\n                compact_working_space = 0\n                for segment, free in self.compact.items():\n                    try:\n                        compact_working_space += self.io.segment_size(segment) - free\n                    except FileNotFoundError:\n                        # looks like self.compact is referring to a nonexistent segment file, ignore it.\n                        pass\n                logger.debug(\"check_free_space: Few segments, not requiring a full free segment\")\n                compact_working_space = min(compact_working_space, full_segment_size)\n                logger.debug(\n                    \"check_free_space: Calculated working space for compact as %d bytes\", compact_working_space\n                )\n                required_free_space += compact_working_space\n            else:\n                # Keep one full worst-case segment free.\n                required_free_space += full_segment_size\n\n        try:\n            free_space = shutil.disk_usage(self.path).free\n        except OSError as os_error:\n            logger.warning(\"Failed to check free space before committing: \" + str(os_error))\n            return\n        logger.debug(f\"check_free_space: Required bytes {required_free_space}, free bytes {free_space}\")\n        if free_space < required_free_space:\n            if self.created:\n                logger.error(\"Not enough free space to initialize repository at this location.\")\n                self.destroy()\n            else:\n                self._rollback(cleanup=True)\n            formatted_required = format_file_size(required_free_space)\n            formatted_free = format_file_size(free_space)\n            raise self.InsufficientFreeSpaceError(formatted_required, formatted_free)\n\n    def compact_segments(self, threshold):\n        \"\"\"Compact sparse segments by copying data into new segments\"\"\"\n        if not self.compact:\n            logger.debug(\"Nothing to do: compact empty\")\n            return\n        index_transaction_id = self.get_index_transaction_id()\n        segments = self.segments\n        unused = []  # list of segments, that are not used anymore\n\n        def complete_xfer(intermediate=True):\n            # complete the current transfer (when some target segment is full)\n            nonlocal unused\n            # commit the new, compact, used segments\n            segment = self.io.write_commit(intermediate=intermediate)\n            self.segments.setdefault(segment, 0)\n            self.compact[segment] += LoggedIO.header_fmt.size\n            logger.debug(\n                \"complete_xfer: Wrote %scommit at segment %d\", \"intermediate \" if intermediate else \"\", segment\n            )\n            # get rid of the old, sparse, unused segments. free space.\n            for segment in unused:\n                logger.debug(\"complete_xfer: Deleting unused segment %d\", segment)\n                count = self.segments.pop(segment)\n                if count != 0:\n                    logger.warning(\n                        \"Corrupted segment reference count %d (expected 0) for segment %d - corrupted index or hints\",\n                        count,\n                        segment,\n                    )\n                self.io.delete_segment(segment)\n                del self.compact[segment]\n            unused = []\n\n        logger.debug(\"Compaction started (threshold is %i%%).\", threshold * 100)\n        pi = ProgressIndicatorPercent(\n            total=len(self.compact), msg=\"Compacting segments %3.0f%%\", step=1, msgid=\"repository.compact_segments\"\n        )\n        for segment, freeable_space in sorted(self.compact.items()):\n            if not self.io.segment_exists(segment):\n                logger.warning(\"Segment %d not found, but listed in compaction data\", segment)\n                self.compact.pop(segment, None)\n                self.segments.pop(segment, None)\n                pi.show()\n                self._send_log()\n                continue\n            segment_size = self.io.segment_size(segment)\n            freeable_ratio = 1.0 * freeable_space / segment_size\n            # we want to compact if:\n            # - we can free a considerable relative amount of space (freeable_ratio over some threshold)\n            if not (freeable_ratio > threshold):\n                logger.debug(\n                    \"Not compacting segment %d (maybe freeable: %2.2f%% [%d bytes])\",\n                    segment,\n                    freeable_ratio * 100.0,\n                    freeable_space,\n                )\n                pi.show()\n                self._send_log()\n                continue\n            segments.setdefault(segment, 0)\n            logger.debug(\n                \"Compacting segment %d with usage count %d (maybe freeable: %2.2f%% [%d bytes])\",\n                segment,\n                segments[segment],\n                freeable_ratio * 100.0,\n                freeable_space,\n            )\n            for tag, key, offset, _, data in self.io.iter_objects(segment):\n                if tag == TAG_COMMIT:\n                    continue\n                in_index = self.index.get(key)\n                is_index_object = in_index and (in_index.segment, in_index.offset) == (segment, offset)\n                if tag in (TAG_PUT2, TAG_PUT) and is_index_object:\n                    try:\n                        new_segment, offset = self.io.write_put(key, data, raise_full=True)\n                    except LoggedIO.SegmentFull:\n                        complete_xfer()\n                        new_segment, offset = self.io.write_put(key, data)\n                    self.index[key] = NSIndex1Entry(new_segment, offset)\n                    segments.setdefault(new_segment, 0)\n                    segments[new_segment] += 1\n                    segments[segment] -= 1\n                elif tag in (TAG_PUT2, TAG_PUT) and not is_index_object:\n                    # If this is a PUT shadowed by a later tag, then it will be gone when this segment is deleted after\n                    # this loop. Therefore it is removed from the shadow index.\n                    try:\n                        self.shadow_index[key].remove(segment)\n                    except (KeyError, ValueError):\n                        # do not remove entry with empty shadowed_segments list here,\n                        # it is needed for shadowed_put_exists code (see below)!\n                        pass\n                elif tag == TAG_DELETE and not in_index:\n                    # If the shadow index doesn't contain this key, then we can't say if there's a shadowed older tag,\n                    # therefore we do not drop the delete, but write it to a current segment.\n                    key_not_in_shadow_index = key not in self.shadow_index\n                    # If the key is in the shadow index and there is any segment with an older PUT of this\n                    # key, we have a shadowed put.\n                    shadowed_put_exists = key_not_in_shadow_index or any(\n                        shadowed < segment for shadowed in self.shadow_index[key]\n                    )\n                    delete_is_not_stable = index_transaction_id is None or segment > index_transaction_id\n\n                    if shadowed_put_exists or delete_is_not_stable:\n                        # (introduced in 6425d16aa84be1eaaf88)\n                        # This is needed to avoid object un-deletion if we crash between the commit and the deletion\n                        # of old segments in complete_xfer().\n                        #\n                        # However, this only happens if the crash also affects the FS to the effect that file deletions\n                        # did not materialize consistently after journal recovery. If they always materialize in-order\n                        # then this is not a problem, because the old segment containing a deleted object would be\n                        # deleted before the segment containing the delete.\n                        #\n                        # Consider the following series of operations if we would not do this, i.e. this entire if:\n                        # would be removed.\n                        # Columns are segments, lines are different keys (line 1 = some key, line 2 = some other key)\n                        # Legend: P=TAG_PUT/TAG_PUT2, D=TAG_DELETE, c=commit, i=index is written for latest commit\n                        #\n                        # Segment | 1     | 2   | 3\n                        # --------+-------+-----+------\n                        # Key 1   | P     | D   |\n                        # Key 2   | P     |     | P\n                        # commits |   c i |   c |   c i\n                        # --------+-------+-----+------\n                        #                       ^- compact_segments starts\n                        #                           ^- complete_xfer commits, after that complete_xfer deletes\n                        #                              segments 1 and 2 (and then the index would be written).\n                        #\n                        # Now we crash. But only segment 2 gets deleted, while segment 1 is still around. Now key 1\n                        # is suddenly undeleted (because the delete in segment 2 is now missing).\n                        # Again, note the requirement here. We delete these in the correct order that this doesn't\n                        # happen, and only if the FS materialization of these deletes is reordered or parts dropped\n                        # this can happen.\n                        # In this case it doesn't cause outright corruption, 'just' an index count mismatch, which\n                        # will be fixed by borg-check --repair.\n                        #\n                        # Note that in this check the index state is the proxy for a \"most definitely settled\"\n                        # repository state, i.e. the assumption is that *all* operations on segments <= index state\n                        # are completed and stable.\n                        try:\n                            new_segment, size = self.io.write_delete(key, raise_full=True)\n                        except LoggedIO.SegmentFull:\n                            complete_xfer()\n                            new_segment, size = self.io.write_delete(key)\n                        self.compact[new_segment] += size\n                        segments.setdefault(new_segment, 0)\n                    else:\n                        logger.debug(\n                            \"Dropping DEL for id %s - seg %d, iti %r, knisi %r, spe %r, dins %r, si %r\",\n                            bin_to_hex(key),\n                            segment,\n                            index_transaction_id,\n                            key_not_in_shadow_index,\n                            shadowed_put_exists,\n                            delete_is_not_stable,\n                            self.shadow_index.get(key),\n                        )\n                        # we did not keep the delete tag for key (see if-branch)\n                        if not self.shadow_index[key]:\n                            # shadowed segments list is empty -> remove it\n                            del self.shadow_index[key]\n            if segments[segment] != 0:\n                logger.warning(\n                    \"Corrupted segment reference count %d (expected 0) for segment %d - corrupted index or hints\",\n                    segments[segment],\n                    segment,\n                )\n            unused.append(segment)\n            pi.show()\n            self._send_log()\n        pi.finish()\n        self._send_log()\n        complete_xfer(intermediate=False)\n        self.io.clear_empty_dirs()\n        logger.debug(\"Compaction completed.\")\n\n    def replay_segments(self, index_transaction_id, segments_transaction_id):\n        # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock:\n        remember_exclusive = self.exclusive\n        self.exclusive = None\n        self.prepare_txn(index_transaction_id, do_cleanup=False)\n        try:\n            segment_count = sum(1 for _ in self.io.segment_iterator())\n            pi = ProgressIndicatorPercent(\n                total=segment_count, msg=\"Replaying segments %3.0f%%\", msgid=\"repository.replay_segments\"\n            )\n            for i, (segment, filename) in enumerate(self.io.segment_iterator()):\n                pi.show(i)\n                self._send_log()\n                if index_transaction_id is not None and segment <= index_transaction_id:\n                    continue\n                if segment > segments_transaction_id:\n                    break\n                objects = self.io.iter_objects(segment)\n                self._update_index(segment, objects)\n            pi.finish()\n            self._send_log()\n            self.write_index()\n        finally:\n            self.exclusive = remember_exclusive\n            self.rollback()\n\n    def _update_index(self, segment, objects, report=None):\n        \"\"\"some code shared between replay_segments and check\"\"\"\n        self.segments[segment] = 0\n        for tag, key, offset, size, _ in objects:\n            if tag in (TAG_PUT2, TAG_PUT):\n                try:\n                    # If this PUT supersedes an older PUT, mark the old segment for compaction and count the free space\n                    in_index = self.index[key]\n                    self.compact[in_index.segment] += header_size(tag) + size\n                    self.segments[in_index.segment] -= 1\n                    self.shadow_index.setdefault(key, []).append(in_index.segment)\n                except KeyError:\n                    pass\n                self.index[key] = NSIndex1Entry(segment, offset)\n                self.segments[segment] += 1\n            elif tag == TAG_DELETE:\n                try:\n                    # if the deleted PUT is not in the index, there is nothing to clean up\n                    in_index = self.index.pop(key)\n                except KeyError:\n                    pass\n                else:\n                    if self.io.segment_exists(in_index.segment):\n                        # the old index is not necessarily valid for this transaction (e.g. compaction); if the segment\n                        # is already gone, then it was already compacted.\n                        self.segments[in_index.segment] -= 1\n                        self.compact[in_index.segment] += header_size(tag) + 0\n                        self.shadow_index.setdefault(key, []).append(in_index.segment)\n            elif tag == TAG_COMMIT:\n                continue\n            else:\n                msg = f\"Unexpected tag {tag} in segment {segment}\"\n                if report is None:\n                    raise self.CheckNeeded(msg)\n                else:\n                    report(msg)\n        if self.segments[segment] == 0:\n            self.compact[segment] = self.io.segment_size(segment)\n\n    def _rebuild_sparse(self, segment):\n        \"\"\"Rebuild sparse bytes count for a single segment relative to the current index.\"\"\"\n        try:\n            segment_size = self.io.segment_size(segment)\n        except FileNotFoundError:\n            # segment does not exist any more, remove it from the mappings.\n            # note: no need to self.compact.pop(segment), as we start from empty mapping.\n            self.segments.pop(segment)\n            return\n\n        if self.segments[segment] == 0:\n            self.compact[segment] = segment_size\n            return\n\n        self.compact[segment] = 0\n        for tag, key, offset, size, _ in self.io.iter_objects(segment, read_data=False):\n            if tag in (TAG_PUT2, TAG_PUT):\n                in_index = self.index.get(key)\n                if not in_index or (in_index.segment, in_index.offset) != (segment, offset):\n                    # This PUT is superseded later.\n                    self.compact[segment] += header_size(tag) + size\n            elif tag == TAG_DELETE:\n                # The outcome of the DELETE has been recorded in the PUT branch already.\n                self.compact[segment] += header_size(tag) + size\n\n    def check(self, repair=False, max_duration=0):\n        \"\"\"Check repository consistency\n\n        This method verifies all segment checksums and makes sure\n        the index is consistent with the data stored in the segments.\n        \"\"\"\n        error_found = False\n\n        def report_error(msg, *args):\n            nonlocal error_found\n            error_found = True\n            logger.error(msg, *args)\n\n        logger.info(\"Starting repository check\")\n        assert not self._active_txn\n        try:\n            transaction_id = self.get_transaction_id()\n            current_index = self.open_index(transaction_id)\n            logger.debug(\"Read committed index of transaction %d\", transaction_id)\n        except Exception as exc:\n            transaction_id = self.io.get_segments_transaction_id()\n            current_index = None\n            logger.debug(\"Failed to read committed index (%s)\", exc)\n        if transaction_id is None:\n            logger.debug(\"No segments transaction found\")\n            transaction_id = self.get_index_transaction_id()\n        if transaction_id is None:\n            logger.debug(\"No index transaction found, trying latest segment\")\n            transaction_id = self.io.get_latest_segment()\n        if transaction_id is None:\n            report_error(\"This repository contains no valid data.\")\n            return False\n        if repair:\n            self.io.cleanup(transaction_id)\n        segments_transaction_id = self.io.get_segments_transaction_id()\n        logger.debug(\"Segment transaction is    %s\", segments_transaction_id)\n        logger.debug(\"Determined transaction is %s\", transaction_id)\n        self.prepare_txn(None)  # self.index, self.compact, self.segments, self.shadow_index all empty now!\n        segment_count = sum(1 for _ in self.io.segment_iterator())\n        logger.debug(\"Found %d segments\", segment_count)\n\n        partial = bool(max_duration)\n        assert not (repair and partial)\n        mode = \"partial\" if partial else \"full\"\n        if partial:\n            # continue a past partial check (if any) or start one from beginning\n            last_segment_checked = self.config.getint(\"repository\", \"last_segment_checked\", fallback=-1)\n            logger.info(\"Skipping to segments >= %d\", last_segment_checked + 1)\n        else:\n            # start from the beginning and also forget about any potential past partial checks\n            last_segment_checked = -1\n            self.config.remove_option(\"repository\", \"last_segment_checked\")\n            self.save_config(self.path, self.config)\n        t_start = time.monotonic()\n        pi = ProgressIndicatorPercent(\n            total=segment_count, msg=\"Checking segments %3.1f%%\", step=0.1, msgid=\"repository.check\"\n        )\n        segment = -1  # avoid uninitialized variable if there are no segment files at all\n        for i, (segment, filename) in enumerate(self.io.segment_iterator()):\n            pi.show(i)\n            self._send_log()\n            if segment <= last_segment_checked:\n                continue\n            if segment > transaction_id:\n                continue\n            logger.debug(\"Checking segment file %s...\", filename)\n            try:\n                objects = list(self.io.iter_objects(segment))\n            except IntegrityError as err:\n                report_error(str(err))\n                objects = []\n                if repair:\n                    self.io.recover_segment(segment, filename)\n                    objects = list(self.io.iter_objects(segment))\n            if not partial:\n                self._update_index(segment, objects, report_error)\n            if partial and time.monotonic() > t_start + max_duration:\n                logger.info(\"Finished partial segment check, last segment checked is %d\", segment)\n                self.config.set(\"repository\", \"last_segment_checked\", str(segment))\n                self.save_config(self.path, self.config)\n                break\n        else:\n            logger.info(\"Finished segment check at segment %d\", segment)\n            self.config.remove_option(\"repository\", \"last_segment_checked\")\n            self.save_config(self.path, self.config)\n\n        pi.finish()\n        self._send_log()\n        # self.index, self.segments, self.compact now reflect the state of the segment files up to <transaction_id>.\n        # We might need to add a commit tag if no committed segment is found.\n        if repair and segments_transaction_id is None:\n            report_error(f\"Adding commit tag to segment {transaction_id}\")\n            self.io.segment = transaction_id + 1\n            self.io.write_commit()\n        if not partial:\n            logger.info(\"Starting repository index check\")\n            if current_index and not repair:\n                # current_index = \"as found on disk\"\n                # self.index = \"as rebuilt in-memory from segments\"\n                if len(current_index) != len(self.index):\n                    report_error(\"Index object count mismatch.\")\n                    report_error(\"committed index: %d objects\", len(current_index))\n                    report_error(\"rebuilt index:   %d objects\", len(self.index))\n                else:\n                    logger.info(\"Index object count match.\")\n                line_format = \"ID: %-64s rebuilt index: %-16s committed index: %-16s\"\n                not_found = \"<not found>\"\n                for key, value in self.index.iteritems():\n                    current_value = current_index.get(key, not_found)\n                    if current_value != value:\n                        report_error(line_format, bin_to_hex(key), value, current_value)\n                self._send_log()\n                for key, current_value in current_index.iteritems():\n                    if key in self.index:\n                        continue\n                    value = self.index.get(key, not_found)\n                    if current_value != value:\n                        report_error(line_format, bin_to_hex(key), value, current_value)\n                self._send_log()\n            if repair:\n                self.write_index()\n        self.rollback()\n        if error_found:\n            if repair:\n                logger.info(\"Finished %s repository check, errors found and repaired.\", mode)\n            else:\n                logger.error(\"Finished %s repository check, errors found.\", mode)\n        else:\n            logger.info(\"Finished %s repository check, no problems found.\", mode)\n        return not error_found or repair\n\n    def _rollback(self, *, cleanup):\n        if cleanup:\n            self.io.cleanup(self.io.get_segments_transaction_id())\n        self.index = None\n        self._active_txn = False\n        self.transaction_doomed = None\n\n    def rollback(self):\n        # note: when used in remote mode, this is time limited, see LegacyRemoteRepository.shutdown_time.\n        self._rollback(cleanup=False)\n\n    def __len__(self):\n        if not self.index:\n            self.index = self.open_index(self.get_transaction_id())\n        return len(self.index)\n\n    def __contains__(self, id):\n        if not self.index:\n            self.index = self.open_index(self.get_transaction_id())\n        return id in self.index\n\n    def list(self, limit=None, marker=None):\n        \"\"\"\n        list <limit> IDs starting from after id <marker> - in index (pseudo-random) order.\n        \"\"\"\n        if not self.index:\n            self.index = self.open_index(self.get_transaction_id())\n        return [id_ for id_, _ in islice(self.index.iteritems(marker=marker), limit)]\n\n    def get(self, id, read_data=True, raise_missing=True):\n        if not self.index:\n            self.index = self.open_index(self.get_transaction_id())\n        try:\n            in_index = NSIndex1Entry(*(self.index[id][:2]))  # legacy: index entries have no size element\n            return self.io.read(in_index.segment, in_index.offset, id, read_data=read_data)\n        except KeyError:\n            if raise_missing:\n                raise self.ObjectNotFound(id, self.path) from None\n            else:\n                return None\n\n    def get_many(self, ids, read_data=True, is_preloaded=False, raise_missing=True):\n        for id_ in ids:\n            yield self.get(id_, read_data=read_data, raise_missing=raise_missing)\n\n    def put(self, id, data, wait=True):\n        \"\"\"put a repo object\n\n        Note: when doing calls with wait=False this gets async and caller must\n              deal with async results / exceptions later.\n        \"\"\"\n        if not self._active_txn:\n            self.prepare_txn(self.get_transaction_id())\n        try:\n            in_index = self.index[id]\n        except KeyError:\n            pass\n        else:\n            # this put call supersedes a previous put to same id.\n            # it is essential to do a delete first to get a correctly updated shadow_index,\n            # so that the compaction code does not wrongly resurrect an old PUT by\n            # dropping a DEL that is still needed.\n            self._delete(id, in_index.segment, in_index.offset, 0)\n        segment, offset = self.io.write_put(id, data)\n        self.segments.setdefault(segment, 0)\n        self.segments[segment] += 1\n        self.index[id] = NSIndex1Entry(segment, offset)\n\n    def delete(self, id, wait=True):\n        \"\"\"delete a repo object\n\n        Note: when doing calls with wait=False this gets async and caller must\n              deal with async results / exceptions later.\n        \"\"\"\n        if not self._active_txn:\n            self.prepare_txn(self.get_transaction_id())\n        try:\n            in_index = self.index.pop(id)\n        except KeyError:\n            raise self.ObjectNotFound(id, self.path) from None\n        self._delete(id, in_index.segment, in_index.offset, 0)\n\n    def _delete(self, id, segment, offset, size):\n        # common code used by put and delete\n        # because we'll write a DEL tag to the repository, we must update the shadow index.\n        # this is always true, no matter whether we are called from put() or delete().\n        # the compaction code needs this to not drop DEL tags if they are still required\n        # to keep a PUT in an earlier segment in the \"effectively deleted\" state.\n        self.shadow_index.setdefault(id, []).append(segment)\n        self.segments[segment] -= 1\n        self.compact[segment] += header_size(TAG_PUT2) + size\n        segment, size = self.io.write_delete(id)\n        self.compact[segment] += size\n        self.segments.setdefault(segment, 0)\n\n    def async_response(self, wait=True):\n        \"\"\"Get one async result (only applies to remote repositories).\n\n        async commands (== calls with wait=False, e.g. delete and put) have no results,\n        but may raise exceptions. These async exceptions must get collected later via\n        async_response() calls. Repeat the call until it returns None.\n        The previous calls might either return one (non-None) result or raise an exception.\n        If wait=True is given and there are outstanding responses, it will wait for them\n        to arrive. With wait=False, it will only return already received responses.\n        \"\"\"\n\n    def preload(self, ids):\n        \"\"\"Preload objects (only applies to remote repositories)\"\"\"\n\n    def get_manifest(self):\n        try:\n            return self.get(Manifest.MANIFEST_ID)\n        except self.ObjectNotFound:\n            raise NoManifestError\n\n    def put_manifest(self, data):\n        return self.put(Manifest.MANIFEST_ID, data)\n\n\nclass LoggedIO:\n    class SegmentFull(Exception):\n        \"\"\"raised when a segment is full, before opening next\"\"\"\n\n    header_fmt = struct.Struct(\"<IIB\")\n    assert header_fmt.size == 9\n    header_no_crc_fmt = struct.Struct(\"<IB\")\n    assert header_no_crc_fmt.size == 5\n    crc_fmt = struct.Struct(\"<I\")\n    assert crc_fmt.size == 4\n\n    _commit = header_no_crc_fmt.pack(9, TAG_COMMIT)\n    COMMIT = crc_fmt.pack(crc32(_commit)) + _commit\n\n    HEADER_ID_SIZE = header_fmt.size + 32\n    ENTRY_HASH_SIZE = 8\n\n    def __init__(self, path, limit, segments_per_dir, capacity=90):\n        self.path = path\n        self.fds = LRUCache(capacity, dispose=self._close_fd)\n        self.segment = 0\n        self.limit = limit\n        self.segments_per_dir = segments_per_dir\n        self.offset = 0\n        self._write_fd = None\n        self._fds_cleaned = 0\n\n    def close(self):\n        self.close_segment()\n        self.fds.clear()\n        self.fds = None  # Just to make sure we're disabled\n\n    def _close_fd(self, ts_fd):\n        ts, fd = ts_fd\n        safe_fadvise(fd.fileno(), 0, 0, \"DONTNEED\")\n        fd.close()\n\n    def get_segment_dirs(self, data_dir, start_index=MIN_SEGMENT_DIR_INDEX, end_index=MAX_SEGMENT_DIR_INDEX):\n        \"\"\"Returns generator yielding required segment dirs in data_dir as `os.DirEntry` objects.\n        Start and end are inclusive.\n        \"\"\"\n        segment_dirs = (\n            f\n            for f in os.scandir(data_dir)\n            if f.is_dir() and f.name.isdigit() and start_index <= int(f.name) <= end_index\n        )\n        return segment_dirs\n\n    def get_segment_files(self, segment_dir, start_index=MIN_SEGMENT_INDEX, end_index=MAX_SEGMENT_INDEX):\n        \"\"\"Returns generator yielding required segment files in segment_dir as `os.DirEntry` objects.\n        Start and end are inclusive.\n        \"\"\"\n        segment_files = (\n            f\n            for f in os.scandir(segment_dir)\n            if f.is_file() and f.name.isdigit() and start_index <= int(f.name) <= end_index\n        )\n        return segment_files\n\n    def segment_iterator(self, start_segment=None, end_segment=None, reverse=False):\n        if start_segment is None:\n            start_segment = MIN_SEGMENT_INDEX if not reverse else MAX_SEGMENT_INDEX\n        if end_segment is None:\n            end_segment = MAX_SEGMENT_INDEX if not reverse else MIN_SEGMENT_INDEX\n        data_path = os.path.join(self.path, \"data\")\n        start_segment_dir = start_segment // self.segments_per_dir\n        end_segment_dir = end_segment // self.segments_per_dir\n        if not reverse:\n            dirs = self.get_segment_dirs(data_path, start_index=start_segment_dir, end_index=end_segment_dir)\n        else:\n            dirs = self.get_segment_dirs(data_path, start_index=end_segment_dir, end_index=start_segment_dir)\n        dirs = sorted(dirs, key=lambda dir: int(dir.name), reverse=reverse)\n        for dir in dirs:\n            if not reverse:\n                files = self.get_segment_files(dir, start_index=start_segment, end_index=end_segment)\n            else:\n                files = self.get_segment_files(dir, start_index=end_segment, end_index=start_segment)\n            files = sorted(files, key=lambda file: int(file.name), reverse=reverse)\n            for file in files:\n                # Note: Do not filter out logically deleted segments  (see \"File system interaction\" above),\n                # since this is used by cleanup and txn state detection as well.\n                yield int(file.name), file.path\n\n    def get_latest_segment(self):\n        for segment, filename in self.segment_iterator(reverse=True):\n            return segment\n        return None\n\n    def get_segments_transaction_id(self):\n        \"\"\"Return the last committed segment.\"\"\"\n        for segment, filename in self.segment_iterator(reverse=True):\n            if self.is_committed_segment(segment):\n                return segment\n        return None\n\n    def cleanup(self, transaction_id):\n        \"\"\"Delete segment files left by aborted transactions\"\"\"\n        self.close_segment()\n        self.segment = transaction_id + 1\n        count = 0\n        for segment, filename in self.segment_iterator(reverse=True):\n            if segment > transaction_id:\n                self.delete_segment(segment)\n                count += 1\n            else:\n                break\n        logger.debug(\"Cleaned up %d uncommitted segment files (== everything after segment %d).\", count, transaction_id)\n\n    def is_committed_segment(self, segment):\n        \"\"\"Check if segment ends with a COMMIT_TAG tag\"\"\"\n        try:\n            iterator = self.iter_objects(segment)\n        except IntegrityError:\n            return False\n        with open(self.segment_filename(segment), \"rb\") as fd:\n            try:\n                fd.seek(-self.header_fmt.size, os.SEEK_END)\n            except OSError as e:\n                # return False if segment file is empty or too small\n                if e.errno == errno.EINVAL:\n                    return False\n                raise e\n            if fd.read(self.header_fmt.size) != self.COMMIT:\n                return False\n        seen_commit = False\n        while True:\n            try:\n                tag, key, offset, _, _ = next(iterator)\n            except IntegrityError:\n                return False\n            except StopIteration:\n                break\n            if tag == TAG_COMMIT:\n                seen_commit = True\n                continue\n            if seen_commit:\n                return False\n        return seen_commit\n\n    def segment_filename(self, segment):\n        return os.path.join(self.path, \"data\", str(segment // self.segments_per_dir), str(segment))\n\n    def get_write_fd(self, no_new=False, want_new=False, raise_full=False):\n        if not no_new and (want_new or self.offset and self.offset > self.limit):\n            if raise_full:\n                raise self.SegmentFull\n            self.close_segment()\n        if not self._write_fd:\n            if self.segment % self.segments_per_dir == 0:\n                dirname = os.path.join(self.path, \"data\", str(self.segment // self.segments_per_dir))\n                if not os.path.exists(dirname):\n                    os.mkdir(dirname)\n                    sync_dir(os.path.join(self.path, \"data\"))\n            self._write_fd = SyncFile(self.segment_filename(self.segment), binary=True)\n            self._write_fd.write(MAGIC)\n            self.offset = MAGIC_LEN\n            if self.segment in self.fds:\n                # we may have a cached fd for a segment file we already deleted and\n                # we are writing now a new segment file to same file name. get rid of\n                # the cached fd that still refers to the old file, so it will later\n                # get repopulated (on demand) with a fd that refers to the new file.\n                del self.fds[self.segment]\n        return self._write_fd\n\n    def get_fd(self, segment):\n        # note: get_fd() returns a fd with undefined file pointer position,\n        # so callers must always seek() to desired position afterwards.\n        now = time.monotonic()\n\n        def open_fd():\n            fd = open(self.segment_filename(segment), \"rb\")\n            self.fds[segment] = (now, fd)\n            return fd\n\n        def clean_old():\n            # we regularly get rid of all old FDs here:\n            if now - self._fds_cleaned > FD_MAX_AGE // 8:\n                self._fds_cleaned = now\n                for k, ts_fd in list(self.fds.items()):\n                    ts, fd = ts_fd\n                    if now - ts > FD_MAX_AGE:\n                        # we do not want to touch long-unused file handles to\n                        # avoid ESTALE issues (e.g. on network filesystems).\n                        del self.fds[k]\n\n        clean_old()\n        if self._write_fd is not None:\n            # without this, we have a test failure now\n            self._write_fd.sync()\n        try:\n            ts, fd = self.fds[segment]\n        except KeyError:\n            fd = open_fd()\n        else:\n            # we only have fresh enough stuff here.\n            # update the timestamp of the lru cache entry.\n            self.fds.replace(segment, (now, fd))\n        return fd\n\n    def close_segment(self):\n        # set self._write_fd to None early to guard against reentry from error handling code paths:\n        fd, self._write_fd = self._write_fd, None\n        if fd is not None:\n            self.segment += 1\n            self.offset = 0\n            fd.close()\n\n    def delete_segment(self, segment):\n        if segment in self.fds:\n            del self.fds[segment]\n        try:\n            safe_unlink(self.segment_filename(segment))\n        except FileNotFoundError:\n            pass\n\n    def clear_empty_dirs(self):\n        \"\"\"Delete empty segment dirs, i.e those with no segment files.\"\"\"\n        data_dir = os.path.join(self.path, \"data\")\n        segment_dirs = self.get_segment_dirs(data_dir)\n        for segment_dir in segment_dirs:\n            try:\n                # os.rmdir will only delete the directory if it is empty\n                # so we don't need to explicitly check for emptiness first.\n                os.rmdir(segment_dir)\n            except OSError:\n                # OSError is raised by os.rmdir if directory is not empty. This is expected.\n                # Its subclass FileNotFoundError may be raised if the directory already does not exist. Ignorable.\n                pass\n        sync_dir(data_dir)\n\n    def segment_exists(self, segment):\n        filename = self.segment_filename(segment)\n        # When deleting segments, they are first truncated. If truncate(2) and unlink(2) are split\n        # across FS transactions, then logically deleted segments will show up as truncated.\n        return os.path.exists(filename) and os.path.getsize(filename)\n\n    def segment_size(self, segment):\n        return os.path.getsize(self.segment_filename(segment))\n\n    def get_segment_magic(self, segment):\n        fd = self.get_fd(segment)\n        fd.seek(0)\n        return fd.read(MAGIC_LEN)\n\n    def iter_objects(self, segment, read_data=True):\n        \"\"\"\n        Return object iterator for *segment*.\n\n        See the _read() docstring about confidence in the returned data.\n\n        The iterator returns five-tuples of (tag, key, offset, size, data).\n        \"\"\"\n        fd = self.get_fd(segment)\n        offset = 0\n        fd.seek(offset)\n        if fd.read(MAGIC_LEN) != MAGIC:\n            raise IntegrityError(f\"Invalid segment magic [segment {segment}, offset {offset}]\")\n        offset = MAGIC_LEN\n        header = fd.read(self.header_fmt.size)\n        while header:\n            size, tag, key, data = self._read(\n                fd, header, segment, offset, (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT), read_data=read_data\n            )\n            # tuple[3]: corresponds to len(data) == length of the full chunk payload (meta_len+enc_meta+enc_data)\n            # tuple[4]: data will be None if read_data is False.\n            yield tag, key, offset, size - header_size(tag), data\n            assert size >= 0\n            offset += size\n            # we must get the fd via get_fd() here again as we yielded to our caller and it might\n            # have triggered closing of the fd we had before (e.g. by calling io.read() for\n            # different segment(s)).\n            # by calling get_fd() here again we also make our fd \"recently used\" so it likely\n            # does not get kicked out of self.fds LRUcache.\n            fd = self.get_fd(segment)\n            fd.seek(offset)\n            header = fd.read(self.header_fmt.size)\n\n    def recover_segment(self, segment, filename):\n        logger.info(\"Attempting to recover \" + filename)\n        if segment in self.fds:\n            del self.fds[segment]\n        if os.path.getsize(filename) < MAGIC_LEN + self.header_fmt.size:\n            # this is either a zero-byte file (which would crash mmap() below) or otherwise\n            # just too small to be a valid non-empty segment file, so do a shortcut here:\n            with SaveFile(filename, binary=True) as fd:\n                fd.write(MAGIC)\n            return\n        with SaveFile(filename, binary=True) as dst_fd:\n            with open(filename, \"rb\") as src_fd:\n                # note: file must not be 0 size or mmap() will crash.\n                with mmap.mmap(src_fd.fileno(), 0, access=mmap.ACCESS_READ) as mm:\n                    # memoryview context manager is problematic, see https://bugs.python.org/issue35686\n                    data = memoryview(mm)\n                    d = data\n                    try:\n                        dst_fd.write(MAGIC)\n                        while len(d) >= self.header_fmt.size:\n                            crc, size, tag = self.header_fmt.unpack(d[: self.header_fmt.size])\n                            size_invalid = size > MAX_OBJECT_SIZE or size < self.header_fmt.size or size > len(d)\n                            if size_invalid or tag > MAX_TAG_ID:\n                                d = d[1:]\n                                continue\n                            if tag == TAG_PUT2:\n                                c_offset = self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE\n                                # skip if header is invalid\n                                if crc32(d[4:c_offset]) & 0xFFFFFFFF != crc:\n                                    d = d[1:]\n                                    continue\n                                # skip if content is invalid\n                                if (\n                                    self.entry_hash(d[4 : self.HEADER_ID_SIZE], d[c_offset:size])\n                                    != d[self.HEADER_ID_SIZE : c_offset]\n                                ):\n                                    d = d[1:]\n                                    continue\n                            elif tag in (TAG_DELETE, TAG_COMMIT, TAG_PUT):\n                                if crc32(d[4:size]) & 0xFFFFFFFF != crc:\n                                    d = d[1:]\n                                    continue\n                            else:  # tag unknown\n                                d = d[1:]\n                                continue\n                            dst_fd.write(d[:size])\n                            d = d[size:]\n                    finally:\n                        del d\n                        data.release()\n\n    def entry_hash(self, *data):\n        h = xxhash.xxh64()\n        for d in data:\n            h.update(d)\n        return h.digest()\n\n    def read(self, segment, offset, id, *, read_data=True, expected_size=None):\n        \"\"\"\n        Read entry from *segment* at *offset* with *id*.\n\n        See the _read() docstring about confidence in the returned data.\n        \"\"\"\n        if segment == self.segment and self._write_fd:\n            self._write_fd.sync()\n        fd = self.get_fd(segment)\n        fd.seek(offset)\n        header = fd.read(self.header_fmt.size)\n        size, tag, key, data = self._read(fd, header, segment, offset, (TAG_PUT2, TAG_PUT), read_data=read_data)\n        if id != key:\n            raise IntegrityError(\n                f\"Invalid segment entry header, is not for wanted id [segment {segment}, offset {offset}]\"\n            )\n        data_size_from_header = size - header_size(tag)\n        if expected_size is not None and expected_size != data_size_from_header:\n            raise IntegrityError(\n                f\"size from repository index: {expected_size} != \" f\"size from entry header: {data_size_from_header}\"\n            )\n        return data\n\n    def _read(self, fd, header, segment, offset, acceptable_tags, read_data=True):\n        \"\"\"\n        Code shared by read() and iter_objects().\n\n        Confidence in returned data:\n        PUT2 tags, read_data == True: crc32 check (header) plus digest check (header+data)\n        PUT2 tags, read_data == False: crc32 check (header)\n        PUT tags, read_data == True: crc32 check (header+data)\n        PUT tags, read_data == False: crc32 check can not be done, all data obtained must be considered informational\n\n        read_data == False behaviour:\n        PUT2 tags: return enough of the chunk so that the client is able to decrypt the metadata,\n                   do not read, but just seek over the data.\n        PUT tags:  return None and just seek over the data.\n        \"\"\"\n\n        def check_crc32(wanted, header, *data):\n            result = crc32(memoryview(header)[4:])  # skip first 32 bits of the header, they contain the crc.\n            for d in data:\n                result = crc32(d, result)\n            if result & 0xFFFFFFFF != wanted:\n                raise IntegrityError(f\"Segment entry header checksum mismatch [segment {segment}, offset {offset}]\")\n\n        # See comment on MAX_TAG_ID for details\n        assert max(acceptable_tags) <= MAX_TAG_ID, \"Exceeding MAX_TAG_ID will break backwards compatibility\"\n        key = data = None\n        fmt = self.header_fmt\n        try:\n            hdr_tuple = fmt.unpack(header)\n        except struct.error as err:\n            raise IntegrityError(f\"Invalid segment entry header [segment {segment}, offset {offset}]: {err}\") from None\n        crc, size, tag = hdr_tuple\n        length = size - fmt.size  # we already read the header\n        if size > MAX_OBJECT_SIZE:\n            # if you get this on an archive made with borg < 1.0.7 and millions of files and\n            # you need to restore it, you can disable this check by using \"if False:\" above.\n            raise IntegrityError(f\"Invalid segment entry size {size} - too big [segment {segment}, offset {offset}]\")\n        if size < fmt.size:\n            raise IntegrityError(f\"Invalid segment entry size {size} - too small [segment {segment}, offset {offset}]\")\n        if tag not in (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT):\n            raise IntegrityError(\n                f\"Invalid segment entry header, did not get a known tag \" f\"[segment {segment}, offset {offset}]\"\n            )\n        if tag not in acceptable_tags:\n            raise IntegrityError(\n                f\"Invalid segment entry header, did not get acceptable tag \" f\"[segment {segment}, offset {offset}]\"\n            )\n        if tag == TAG_COMMIT:\n            check_crc32(crc, header)\n            # that's all for COMMITs.\n        else:\n            # all other tags (TAG_PUT2, TAG_DELETE, TAG_PUT) have a key\n            key = fd.read(32)\n            length -= 32\n            if len(key) != 32:\n                raise IntegrityError(\n                    f\"Segment entry key short read [segment {segment}, offset {offset}]: \"\n                    f\"expected {32}, got {len(key)} bytes\"\n                )\n            if tag == TAG_DELETE:\n                check_crc32(crc, header, key)\n                # that's all for DELETEs.\n            else:\n                # TAG_PUT: we can not do a crc32 header check here, because the crc32 is computed over header+data!\n                #          for the check, see code below when read_data is True.\n                if tag == TAG_PUT2:\n                    entry_hash = fd.read(self.ENTRY_HASH_SIZE)\n                    length -= self.ENTRY_HASH_SIZE\n                    if len(entry_hash) != self.ENTRY_HASH_SIZE:\n                        raise IntegrityError(\n                            f\"Segment entry hash short read [segment {segment}, offset {offset}]: \"\n                            f\"expected {self.ENTRY_HASH_SIZE}, got {len(entry_hash)} bytes\"\n                        )\n                    check_crc32(crc, header, key, entry_hash)\n                if not read_data:\n                    if tag == TAG_PUT2:\n                        # PUT2 is only used in new repos and they also have different RepoObj layout,\n                        # supporting separately encrypted metadata and data.\n                        # In this case, we return enough bytes so the client can decrypt the metadata\n                        # and seek over the rest (over the encrypted data).\n                        hdr_size = RepoObj.obj_header.size\n                        hdr = fd.read(hdr_size)\n                        length -= hdr_size\n                        if len(hdr) != hdr_size:\n                            raise IntegrityError(\n                                f\"Segment entry meta length short read [segment {segment}, offset {offset}]: \"\n                                f\"expected {hdr_size}, got {len(hdr)} bytes\"\n                            )\n                        meta_size = RepoObj.obj_header.unpack(hdr)[0]\n                        meta = fd.read(meta_size)\n                        length -= meta_size\n                        if len(meta) != meta_size:\n                            raise IntegrityError(\n                                f\"Segment entry meta short read [segment {segment}, offset {offset}]: \"\n                                f\"expected {meta_size}, got {len(meta)} bytes\"\n                            )\n                        data = hdr + meta  # shortened chunk - enough so the client can decrypt the metadata\n                    # in any case, we seek over the remainder of the chunk\n                    oldpos = fd.tell()\n                    seeked = fd.seek(length, os.SEEK_CUR) - oldpos\n                    if seeked != length:\n                        raise IntegrityError(\n                            f\"Segment entry data short seek [segment {segment}, offset {offset}]: \"\n                            f\"expected {length}, got {seeked} bytes\"\n                        )\n                else:  # read data!\n                    data = fd.read(length)\n                    if len(data) != length:\n                        raise IntegrityError(\n                            f\"Segment entry data short read [segment {segment}, offset {offset}]: \"\n                            f\"expected {length}, got {len(data)} bytes\"\n                        )\n                    if tag == TAG_PUT2:\n                        if self.entry_hash(memoryview(header)[4:], key, data) != entry_hash:\n                            raise IntegrityError(f\"Segment entry hash mismatch [segment {segment}, offset {offset}]\")\n                    elif tag == TAG_PUT:\n                        check_crc32(crc, header, key, data)\n        return size, tag, key, data\n\n    def write_put(self, id, data, raise_full=False):\n        data_size = len(data)\n        if data_size > MAX_DATA_SIZE:\n            # this would push the segment entry size beyond MAX_OBJECT_SIZE.\n            raise IntegrityError(f\"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]\")\n        fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full)\n        size = data_size + self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE\n        offset = self.offset\n        header = self.header_no_crc_fmt.pack(size, TAG_PUT2)\n        entry_hash = self.entry_hash(header, id, data)\n        crc = self.crc_fmt.pack(crc32(entry_hash, crc32(id, crc32(header))) & 0xFFFFFFFF)\n        fd.write(b\"\".join((crc, header, id, entry_hash)))\n        fd.write(data)\n        self.offset += size\n        return self.segment, offset\n\n    def write_delete(self, id, raise_full=False):\n        fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full)\n        header = self.header_no_crc_fmt.pack(self.HEADER_ID_SIZE, TAG_DELETE)\n        crc = self.crc_fmt.pack(crc32(id, crc32(header)) & 0xFFFFFFFF)\n        fd.write(b\"\".join((crc, header, id)))\n        self.offset += self.HEADER_ID_SIZE\n        return self.segment, self.HEADER_ID_SIZE\n\n    def write_commit(self, intermediate=False):\n        # Intermediate commits go directly into the current segment - this makes checking their validity more\n        # expensive, but is faster and reduces clobber. Final commits go into a new segment.\n        fd = self.get_write_fd(want_new=not intermediate, no_new=intermediate)\n        if intermediate:\n            fd.sync()\n        header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT)\n        crc = self.crc_fmt.pack(crc32(header) & 0xFFFFFFFF)\n        fd.write(b\"\".join((crc, header)))\n        self.close_segment()\n        return self.segment - 1  # close_segment() increments it\n\n\nassert LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE == 41 + 8  # see constants.MAX_OBJECT_SIZE\n"
  },
  {
    "path": "src/borg/logger.py",
    "content": "\"\"\"Logging facilities.\n\nUsage:\n\n- Each module declares its own logger using:\n\n      from .logger import create_logger\n      logger = create_logger()\n\n- Then each module uses logger.info/warning/debug/etc. according to the\n  appropriate level:\n\n      logger.debug('debugging info for developers or power users')\n      logger.info('normal, informational output')\n      logger.warning('warn about a non-fatal error or something else')\n      logger.error('a fatal error')\n\n  See the `logging documentation\n  <https://docs.python.org/3/howto/logging.html#when-to-use-logging>`_\n  for more information.\n\n- Console interaction happens on stderr; that includes interactive\n  reporting functions like `help`, `info`, and `list`.\n\n- ``input()`` is special because we cannot control the stream it uses.\n  We assume it will not clutter stdout, because interaction would be broken\n  otherwise.\n\n- What is output at INFO level is additionally controlled by command-line\n  flags.\n\nLogging setup in Borg needs to work under various conditions:\n- Purely local, not client/server (easy).\n- Client/server: RemoteRepository (\"borg serve\" process) writes log records into a global\n  queue, which is then sent to the client side by the main serve loop (via the RPC protocol),\n  either over SSH stdout, more directly via process stdout without SSH (used in tests),\n  or via a socket. On the client side, the log records are fed into the client-side logging\n  system. When remote_repo.close() is called, the server side must send all queued log records\n  via the RPC channel before returning the close() call's return value (as the client will\n  then shut down the connection).\n- Progress output is always given as JSON to the logger (including the plain text inside\n  the JSON), but then formatted by the logging system's formatter as either plain text or\n  JSON depending on the CLI args given (--log-json?).\n- Tests: potentially running in parallel via pytest-xdist, capturing Borg output into a\n  given stream.\n- Logging might be short-lived (e.g., when invoking a single Borg command via the CLI)\n  or long-lived (e.g., borg serve --socket or when running the tests).\n- Logging is global and exists only once per process.\n\"\"\"\n\nimport inspect\nimport json\nimport logging\nimport logging.config\nimport logging.handlers  # needed for handlers defined there being configurable in logging.conf file\nimport os\nimport queue\nimport sys\nimport time\nimport warnings\nfrom pathlib import Path\n\nlogging_debugging_path: Path | None = None  # if set, write borg.logger debugging log to that_path/borg-*.log\n\nconfigured = False\nborg_serve_log_queue: queue.SimpleQueue = queue.SimpleQueue()\n\n\nclass BorgQueueHandler(logging.handlers.QueueHandler):\n    \"\"\"Borg serve writes log record dicts to a borg_serve_log_queue.\"\"\"\n\n    def prepare(self, record: logging.LogRecord) -> dict:\n        return dict(\n            # kwargs needed for LogRecord constructor:\n            name=record.name,\n            level=record.levelno,\n            pathname=record.pathname,\n            lineno=record.lineno,\n            msg=record.msg,\n            args=record.args,\n            exc_info=record.exc_info,\n            func=record.funcName,\n            sinfo=record.stack_info,\n        )\n\n\nclass StderrHandler(logging.StreamHandler):\n    \"\"\"\n    This class is like a StreamHandler using sys.stderr, but always uses\n    whatever sys.stderr is currently set to rather than the value of\n    sys.stderr at handler construction time.\n    \"\"\"\n\n    def __init__(self, stream=None):\n        logging.Handler.__init__(self)\n\n    @property\n    def stream(self):\n        return sys.stderr\n\n\nclass TextProgressFormatter(logging.Formatter):\n    def format(self, record: logging.LogRecord) -> str:\n        # record.msg contains json (because we always do json for progress log)\n        j = json.loads(record.msg)\n        # inside the json, the text log line can be found under \"message\"\n        return f\"{j['message']}\"\n\n\nclass JSONProgressFormatter(logging.Formatter):\n    def format(self, record: logging.LogRecord) -> str:\n        # record.msg contains json (because we always do json for progress log)\n        return f\"{record.msg}\"\n\n\n# use something like this to ignore warnings:\n# warnings.filterwarnings('ignore', r'... regex for warning message to ignore ...')\n\n\n# we do not want that urllib spoils test output with LibreSSL related warnings on OpenBSD.\n# NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+,\n#                    currently the 'ssl' module is compiled with 'LibreSSL 3.8.2'.\nwarnings.filterwarnings(\"ignore\", message=r\".*urllib3 v2 only supports OpenSSL.*\")\n\n\ndef _log_warning(message, category, filename, lineno, file=None, line=None):\n    # for warnings, we just want to use the logging system, not stderr or other files\n    msg = f\"{filename}:{lineno}: {category.__name__}: {message}\"\n    logger = create_logger(__name__)\n    # Note: the warning will look like coming from here,\n    # but msg contains info about where it really comes from\n    logger.warning(msg)\n\n\ndef remove_handlers(logger):\n    for handler in logger.handlers[:]:\n        handler.flush()\n        handler.close()\n        logger.removeHandler(handler)\n\n\ndef flush_logging():\n    # make sure all log output is flushed,\n    # this is especially important for the \"borg serve\" RemoteRepository logging:\n    # all log output needs to be sent via the ssh / socket connection before closing it.\n    for logger_name in \"borg.output.progress\", \"\":\n        logger = logging.getLogger(logger_name)\n        for handler in logger.handlers:\n            handler.flush()\n\n\ndef setup_logging(\n    stream=None, conf_fname=None, env_var=\"BORG_LOGGING_CONF\", level=\"info\", is_serve=False, log_json=False, func=None\n):\n    \"\"\"setup logging module according to the arguments provided\n\n    if conf_fname is given (or the config file name can be determined via\n    the env_var, if given): load this logging configuration.\n\n    otherwise, set up a stream handler logger on stderr (by default, if no\n    stream is provided).\n\n    is_serve: are we setting up the logging for \"borg serve\"?\n    \"\"\"\n    global configured\n    err_msg = None\n    if env_var:\n        conf_fname = os.environ.get(env_var, conf_fname)\n    if conf_fname:\n        try:\n            conf_path = Path(conf_fname).absolute()\n            # we open the conf file here to be able to give a reasonable\n            # error message in case of failure (if we give the filename to\n            # fileConfig(), it silently ignores unreadable files and gives\n            # unhelpful error msgs like \"No section: 'formatters'\"):\n            with conf_path.open() as f:\n                logging.config.fileConfig(f)\n            configured = True\n            logger = logging.getLogger(__name__)\n            logger.debug(f'using logging configuration read from \"{conf_fname}\"')\n            warnings.showwarning = _log_warning\n            return None\n        except Exception as err:  # XXX be more precise\n            err_msg = str(err)\n\n    # if we did not / not successfully load a logging configuration, fallback to this:\n    level = level.upper()\n    fmt = \"%(message)s\"\n    formatter = JsonFormatter(fmt) if log_json else logging.Formatter(fmt)\n    SHandler = StderrHandler if stream is None else logging.StreamHandler\n    handler = BorgQueueHandler(borg_serve_log_queue) if is_serve else SHandler(stream)\n    handler.setFormatter(formatter)\n    logger = logging.getLogger()\n    remove_handlers(logger)\n    logger.setLevel(level)\n\n    if logging_debugging_path is not None:\n        # add an addtl. root handler for debugging purposes\n        log_path = logging_debugging_path / (f\"borg-{'serve' if is_serve else 'client'}-root.log\")\n        handler2 = logging.StreamHandler(log_path.open(\"a\"))\n        handler2.setFormatter(formatter)\n        logger.addHandler(handler2)\n        logger.warning(f\"--- {func} ---\")  # only handler2 shall get this\n\n    logger.addHandler(handler)  # do this late, so handler is not added while debug handler is set up\n\n    bop_formatter = JSONProgressFormatter() if log_json else TextProgressFormatter()\n    bop_handler = BorgQueueHandler(borg_serve_log_queue) if is_serve else SHandler(stream)\n    bop_handler.setFormatter(bop_formatter)\n    bop_logger = logging.getLogger(\"borg.output.progress\")\n    remove_handlers(bop_logger)\n    bop_logger.setLevel(\"INFO\")\n    bop_logger.propagate = False\n\n    if logging_debugging_path is not None:\n        # add an addtl. progress handler for debugging purposes\n        log_path = logging_debugging_path / (f\"borg-{'serve' if is_serve else 'client'}-progress.log\")\n        bop_handler2 = logging.StreamHandler(log_path.open(\"a\"))\n        bop_handler2.setFormatter(bop_formatter)\n        bop_logger.addHandler(bop_handler2)\n        json_dict = dict(\n            message=f\"--- {func} ---\", operation=0, msgid=\"\", type=\"progress_message\", finished=False, time=time.time()\n        )\n        bop_logger.warning(json.dumps(json_dict))  # only bop_handler2 shall get this\n\n    bop_logger.addHandler(bop_handler)  # do this late, so bop_handler is not added while debug handler is set up\n\n    configured = True\n\n    logger = logging.getLogger(__name__)\n    if err_msg:\n        logger.warning(f'setup_logging for \"{conf_fname}\" failed with \"{err_msg}\".')\n    logger.debug(\"using builtin fallback logging configuration\")\n    warnings.showwarning = _log_warning\n    return handler\n\n\ndef find_parent_module():\n    \"\"\"find the name of the first module calling this module\n\n    if we cannot find it, we return the current module's name\n    (__name__) instead.\n    \"\"\"\n    try:\n        frame = inspect.currentframe().f_back\n        module = inspect.getmodule(frame)\n        while module is None or module.__name__ == __name__:\n            frame = frame.f_back\n            module = inspect.getmodule(frame)\n        return module.__name__\n    except AttributeError:\n        # somehow we failed to find our module\n        # return the logger module name by default\n        return __name__\n\n\nclass LazyLogger:\n    def __init__(self, name=None):\n        self.__name = name or find_parent_module()\n        self.__real_logger = None\n\n    @property\n    def __logger(self):\n        if self.__real_logger is None:\n            if not configured:\n                raise Exception(\"tried to call a logger before setup_logging() was called\")\n            self.__real_logger = logging.getLogger(self.__name)\n            if self.__name.startswith(\"borg.debug.\") and self.__real_logger.level == logging.NOTSET:\n                self.__real_logger.setLevel(\"WARNING\")\n        return self.__real_logger\n\n    def getChild(self, suffix):\n        return LazyLogger(self.__name + \".\" + suffix)\n\n    def setLevel(self, level):\n        return self.__logger.setLevel(level)\n\n    def log(self, level, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.log(level, msg, *args, **kwargs)\n\n    def exception(self, msg, *args, exc_info=True, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.exception(msg, *args, exc_info=exc_info, **kwargs)\n\n    def debug(self, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.debug(msg, *args, **kwargs)\n\n    def info(self, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.info(msg, *args, **kwargs)\n\n    def warning(self, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.warning(msg, *args, **kwargs)\n\n    def error(self, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.error(msg, *args, **kwargs)\n\n    def critical(self, msg, *args, **kwargs):\n        if \"msgid\" in kwargs:\n            kwargs.setdefault(\"extra\", {})[\"msgid\"] = kwargs.pop(\"msgid\")\n        return self.__logger.critical(msg, *args, **kwargs)\n\n\ndef create_logger(name: str = None) -> LazyLogger:\n    \"\"\"lazily create a Logger object with the proper path, which is returned by\n    find_parent_module() by default, or is provided via the commandline\n\n    this is really a shortcut for:\n\n        logger = logging.getLogger(__name__)\n\n    we use it to avoid errors and provide a more standard API.\n\n    We must create the logger lazily, because this is usually called from\n    module level (and thus executed at import time - BEFORE setup_logging()\n    was called). By doing it lazily we can do the setup first, we just have to\n    be careful not to call any logger methods before the setup_logging() call.\n    If you try, you'll get an exception.\n    \"\"\"\n\n    return LazyLogger(name)\n\n\nclass JsonFormatter(logging.Formatter):\n    RECORD_ATTRIBUTES = (\n        \"levelname\",\n        \"name\",\n        \"message\",\n        # msgid is an attribute we made up in Borg to expose a non-changing handle for log messages\n        \"msgid\",\n    )\n\n    # Other attributes that are not very useful but do exist:\n    # processName, process, relativeCreated, stack_info, thread, threadName\n    # msg == message\n    # *args* are the unformatted arguments passed to the logger function, not useful now,\n    # become useful if sanitized properly (must be JSON serializable) in the code +\n    # fixed message IDs are assigned.\n    # exc_info, exc_text are generally uninteresting because the message will have that\n\n    def format(self, record):\n        super().format(record)\n        data = {\"type\": \"log_message\", \"time\": record.created, \"message\": \"\", \"levelname\": \"CRITICAL\"}\n        for attr in self.RECORD_ATTRIBUTES:\n            value = getattr(record, attr, None)\n            if value:\n                data[attr] = value\n        return json.dumps(data)\n"
  },
  {
    "path": "src/borg/manifest.py",
    "content": "import enum\nimport re\nfrom collections import namedtuple\nfrom datetime import datetime, timedelta, timezone\nfrom operator import attrgetter\nfrom collections.abc import Sequence\n\nfrom borgstore.store import ObjectNotFound, ItemInfo\n\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfrom .constants import *  # NOQA\nfrom .helpers.datastruct import StableDict\nfrom .helpers.parseformat import bin_to_hex, hex_to_bin\nfrom .helpers.time import parse_timestamp, calculate_relative_offset, archive_ts_now\nfrom .helpers.errors import Error, CommandError\nfrom .item import ArchiveItem\nfrom .patterns import get_regex_from_pattern\nfrom .repoobj import RepoObj\n\n\nclass MandatoryFeatureUnsupported(Error):\n    \"\"\"Unsupported repository feature(s) {}. A newer version of Borg is required to access this repository.\"\"\"\n\n    exit_mcode = 25\n\n\nclass NoManifestError(Error):\n    \"\"\"Repository has no manifest.\"\"\"\n\n    exit_mcode = 26\n\n\nArchiveInfo = namedtuple(\"ArchiveInfo\", \"name id ts tags host user\", defaults=[(), None, None])\n\n# timestamp is a replacement for ts, archive is an alias for name (see SortBySpec)\nAI_HUMAN_SORT_KEYS = [\"timestamp\", \"archive\"] + list(ArchiveInfo._fields)\nAI_HUMAN_SORT_KEYS.remove(\"ts\")\n\n\ndef filter_archives_by_date(archives, older=None, newer=None, oldest=None, newest=None):\n    def get_first_and_last_archive_ts(archives_list):\n        timestamps = [x.ts for x in archives_list]\n        return min(timestamps), max(timestamps)\n\n    if not archives:\n        return archives\n\n    now = archive_ts_now()\n    earliest_ts, latest_ts = get_first_and_last_archive_ts(archives)\n\n    until_ts = calculate_relative_offset(older, now, earlier=True) if older is not None else latest_ts\n    from_ts = calculate_relative_offset(newer, now, earlier=True) if newer is not None else earliest_ts\n    archives = [x for x in archives if from_ts <= x.ts <= until_ts]\n\n    if not archives:\n        return archives\n\n    earliest_ts, latest_ts = get_first_and_last_archive_ts(archives)\n    if oldest:\n        until_ts = calculate_relative_offset(oldest, earliest_ts, earlier=False)\n        archives = [x for x in archives if x.ts <= until_ts]\n    if newest:\n        from_ts = calculate_relative_offset(newest, latest_ts, earlier=True)\n        archives = [x for x in archives if x.ts >= from_ts]\n\n    return archives\n\n\nclass Archives:\n    \"\"\"\n    Manage the list of archives.\n\n    We still need to support the borg 1.x manifest-with-list-of-archives,\n    so borg transfer can work.\n    borg2 has separate items archives/* in the borgstore.\n    \"\"\"\n\n    def __init__(self, repository, manifest):\n        from .repository import Repository\n        from .remote import RemoteRepository\n\n        self.repository = repository\n        self.legacy = not isinstance(repository, (Repository, RemoteRepository))\n        # key: str archive name, value: dict('id': bytes_id, 'time': str_iso_ts)\n        self._archives = {}\n        self.manifest = manifest\n\n    def prepare(self, manifest, m):\n        if not self.legacy:\n            pass\n        else:\n            self._set_raw_dict(m.archives)\n\n    def finish(self, manifest):\n        if not self.legacy:\n            manifest_archives = {}\n        else:\n            manifest_archives = StableDict(self._get_raw_dict())\n        return manifest_archives\n\n    def ids(self, *, deleted=False):\n        # yield the binary IDs of all archives\n        if not self.legacy:\n            try:\n                infos = list(self.repository.store_list(\"archives\", deleted=deleted))\n            except ObjectNotFound:\n                infos = []\n            for info in infos:\n                info = ItemInfo(*info)  # RPC does not give us a NamedTuple\n                yield hex_to_bin(info.name)\n        else:\n            for archive_info in self._archives.values():\n                yield archive_info[\"id\"]\n\n    def _get_archive_meta(self, id: bytes) -> dict:\n        # get all metadata directly from the ArchiveItem in the repo.\n        from .legacyrepository import LegacyRepository\n        from .repository import Repository\n\n        try:\n            cdata = self.repository.get(id)\n        except (Repository.ObjectNotFound, LegacyRepository.ObjectNotFound):\n            metadata = dict(\n                id=id,\n                name=\"archive-does-not-exist\",\n                time=\"1970-01-01T00:00:00.000000\",\n                # new:\n                exists=False,  # we have the pointer, but the repo does not have an archive item\n                username=\"\",\n                hostname=\"\",\n                tags=(),\n            )\n        else:\n            _, data = self.manifest.repo_objs.parse(id, cdata, ro_type=ROBJ_ARCHIVE_META)\n            archive_dict = self.manifest.key.unpack_archive(data)\n            archive_item = ArchiveItem(internal_dict=archive_dict)\n            if archive_item.version not in (1, 2):  # legacy: still need to read v1 archives\n                raise Exception(\"Unknown archive metadata version\")\n            # callers expect a dict with dict[\"key\"] access, not ArchiveItem.key access.\n            # also, we need to put the id in there.\n            metadata = dict(\n                id=id,\n                name=archive_item.name,\n                time=archive_item.time,\n                # new:\n                exists=True,  # repo has a valid archive item\n                username=archive_item.username,\n                hostname=archive_item.hostname,\n                size=archive_item.get(\"size\", 0),\n                nfiles=archive_item.get(\"nfiles\", 0),\n                comment=archive_item.get(\"comment\", \"\"),\n                tags=tuple(sorted(getattr(archive_item, \"tags\", []))),  # must be hashable\n            )\n        return metadata\n\n    def _infos(self, *, deleted=False):\n        # yield the infos of all archives\n        for id in self.ids(deleted=deleted):\n            yield self._get_archive_meta(id)\n\n    def _info_tuples(self, *, deleted=False):\n        for info in self._infos(deleted=deleted):\n            yield ArchiveInfo(\n                name=info[\"name\"],\n                id=info[\"id\"],\n                ts=parse_timestamp(info[\"time\"]),\n                tags=info[\"tags\"],\n                user=info[\"username\"],\n                host=info[\"hostname\"],\n            )\n\n    def _matching_info_tuples(self, match_patterns, match_end, *, deleted=False):\n        archive_infos = list(self._info_tuples(deleted=deleted))\n        if match_patterns:\n            assert isinstance(match_patterns, list), f\"match_pattern is a {type(match_patterns)}\"\n            for match in match_patterns:\n                if match.startswith(\"aid:\"):  # do a match on the archive ID (prefix)\n                    wanted_id = match.removeprefix(\"aid:\")\n                    archive_infos = [x for x in archive_infos if bin_to_hex(x.id).startswith(wanted_id)]\n                    if len(archive_infos) != 1:\n                        raise CommandError(\"archive ID based match needs to match precisely one archive ID\")\n                elif match.startswith(\"tags:\"):\n                    wanted_tags = match.removeprefix(\"tags:\")\n                    wanted_tags = [tag for tag in wanted_tags.split(\",\") if tag]  # remove empty tags\n                    archive_infos = [x for x in archive_infos if set(x.tags) >= set(wanted_tags)]\n                elif match.startswith(\"user:\"):\n                    wanted_user = match.removeprefix(\"user:\")\n                    archive_infos = [x for x in archive_infos if x.user == wanted_user]\n                elif match.startswith(\"host:\"):\n                    wanted_host = match.removeprefix(\"host:\")\n                    archive_infos = [x for x in archive_infos if x.host == wanted_host]\n                else:  #  do a match on the name\n                    match = match.removeprefix(\"name:\")  # accept optional name: prefix\n                    regex = get_regex_from_pattern(match)\n                    regex = re.compile(regex + match_end)\n                    archive_infos = [x for x in archive_infos if regex.match(x.name) is not None]\n        return archive_infos\n\n    def count(self):\n        # return the count of archives in the repo\n        return len(list(self.ids()))\n\n    def names(self):\n        # yield the names of all archives\n        for archive_info in self._infos():\n            yield archive_info[\"name\"]\n\n    def exists(self, name):\n        # check if an archive with this name exists\n        assert isinstance(name, str)\n        if not self.legacy:\n            return name in self.names()\n        else:\n            return name in self._archives\n\n    def exists_id(self, id, *, deleted=False):\n        # check if an archive with this id exists\n        assert isinstance(id, bytes)\n        if not self.legacy:\n            return id in self.ids(deleted=deleted)\n        else:\n            raise NotImplementedError\n\n    def exists_name_and_id(self, name, id):\n        # check if an archive with this name AND id exists\n        assert isinstance(name, str)\n        assert isinstance(id, bytes)\n        if not self.legacy:\n            for archive_info in self._infos():\n                if archive_info[\"name\"] == name and archive_info[\"id\"] == id:\n                    return True\n            else:\n                return False\n        else:\n            raise NotImplementedError\n\n    def exists_name_and_ts(self, name, ts):\n        # check if an archive with this name AND timestamp exists\n        assert isinstance(name, str)\n        assert isinstance(ts, datetime)\n        if not self.legacy:\n            for archive_info in self._info_tuples():\n                if archive_info.name == name and archive_info.ts == ts:\n                    return True\n            else:\n                return False\n        else:\n            raise NotImplementedError\n\n    def _lookup_name(self, name, raw=False):\n        assert isinstance(name, str)\n        assert not self.legacy\n        for archive_info in self._infos():\n            if archive_info[\"exists\"] and archive_info[\"name\"] == name:\n                if not raw:\n                    ts = parse_timestamp(archive_info[\"time\"])\n                    return ArchiveInfo(\n                        name=archive_info[\"name\"],\n                        id=archive_info[\"id\"],\n                        ts=ts,\n                        tags=archive_info[\"tags\"],\n                        user=archive_info[\"username\"],\n                        host=archive_info[\"hostname\"],\n                    )\n                else:\n                    return archive_info\n        else:\n            raise KeyError(name)\n\n    def get(self, name, raw=False):\n        assert isinstance(name, str)\n        if not self.legacy:\n            try:\n                return self._lookup_name(name, raw=raw)\n            except KeyError:\n                return None\n        else:\n            values = self._archives.get(name)\n            if values is None:\n                return None\n            if not raw:\n                ts = parse_timestamp(values[\"time\"])\n                return ArchiveInfo(name=name, id=values[\"id\"], ts=ts)\n            else:\n                return dict(name=name, id=values[\"id\"], time=values[\"time\"])\n\n    def get_by_id(self, id, raw=False, *, deleted=False):\n        assert isinstance(id, bytes)\n        if not self.legacy:\n            if id in self.ids(deleted=deleted):  # check directory\n                # looks like this archive id is in the archives directory, thus it is NOT deleted.\n                # OR we have explicitly requested a soft-deleted archive via deleted=True.\n                archive_info = self._get_archive_meta(id)\n                if archive_info[\"exists\"]:  # True means we have found Archive metadata in the repo.\n                    if not raw:\n                        ts = parse_timestamp(archive_info[\"time\"])\n                        archive_info = ArchiveInfo(\n                            name=archive_info[\"name\"],\n                            id=archive_info[\"id\"],\n                            ts=ts,\n                            tags=archive_info[\"tags\"],\n                            user=archive_info[\"username\"],\n                            host=archive_info[\"hostname\"],\n                        )\n                    return archive_info\n        else:\n            for name, values in self._archives.items():\n                if id == values[\"id\"]:\n                    break\n            else:\n                return None\n            if not raw:\n                ts = parse_timestamp(values[\"time\"])\n                return ArchiveInfo(name=name, id=values[\"id\"], ts=ts)\n            else:\n                return dict(name=name, id=values[\"id\"], time=values[\"time\"])\n\n    def create(self, name, id, ts, *, overwrite=False):\n        assert isinstance(name, str)\n        assert isinstance(id, bytes)\n        if isinstance(ts, datetime):\n            ts = ts.isoformat(timespec=\"microseconds\")\n        assert isinstance(ts, str)\n        if not self.legacy:\n            # we only create a directory entry, its name points to the archive item:\n            self.repository.store_store(f\"archives/{bin_to_hex(id)}\", b\"\")\n        else:\n            if self.exists(name) and not overwrite:\n                raise KeyError(\"archive already exists\")\n            self._archives[name] = {\"id\": id, \"time\": ts}\n\n    def delete_by_id(self, id):\n        # soft-delete an archive\n        assert isinstance(id, bytes)\n        assert not self.legacy\n        self.repository.store_move(f\"archives/{bin_to_hex(id)}\", delete=True)  # soft-delete\n\n    def undelete_by_id(self, id):\n        # undelete an archive\n        assert isinstance(id, bytes)\n        assert not self.legacy\n        self.repository.store_move(f\"archives/{bin_to_hex(id)}\", undelete=True)\n\n    def nuke_by_id(self, id):\n        # really delete an already soft-deleted archive\n        assert isinstance(id, bytes)\n        assert not self.legacy\n        self.repository.store_delete(f\"archives/{bin_to_hex(id)}\", deleted=True)\n\n    def list(\n        self,\n        *,\n        match=None,\n        match_end=r\"\\Z\",\n        sort_by=(),\n        reverse=False,\n        first=None,\n        last=None,\n        older=None,\n        newer=None,\n        oldest=None,\n        newest=None,\n        deleted=False,\n    ):\n        \"\"\"\n        Return list of ArchiveInfo instances according to the parameters.\n\n        First match *match* (considering *match_end*), then filter by timestamp considering *older* and *newer*.\n        Second, follow with a filter considering *oldest* and *newest*, then sort by the given *sort_by* argument.\n\n        Apply *first* and *last* filters, and then possibly *reverse* the list.\n\n        *sort_by* is a list of sort keys applied in reverse order.\n        *newer* and *older* are relative time markers that indicate offset from now.\n        *newest* and *oldest* are relative time markers that indicate offset from newest/oldest archive's timestamp.\n\n\n        Note: for better robustness, all filtering / limiting parameters must default to\n              \"not limit / not filter\", so a FULL archive list is produced by a simple .list().\n              some callers EXPECT to iterate over all archives in a repo for correct operation.\n        \"\"\"\n        if isinstance(sort_by, (str, bytes)):\n            raise TypeError(\"sort_by must be a sequence of str\")\n\n        archive_infos = self._matching_info_tuples(match, match_end, deleted=deleted)\n\n        if any([oldest, newest, older, newer]):\n            archive_infos = filter_archives_by_date(\n                archive_infos, oldest=oldest, newest=newest, newer=newer, older=older\n            )\n        for sortkey in reversed(sort_by):\n            archive_infos.sort(key=attrgetter(sortkey))\n        if first:\n            archive_infos = archive_infos[:first]\n        elif last:\n            archive_infos = archive_infos[max(len(archive_infos) - last, 0) :]\n        if reverse:\n            archive_infos.reverse()\n        return archive_infos\n\n    def list_considering(self, args):\n        \"\"\"\n        get a list of archives, considering --first/last/prefix/match-archives/sort cmdline args\n        \"\"\"\n        name = getattr(args, \"name\", None)\n        if name is not None:\n            raise Error(\n                \"Giving a specific name is incompatible with options --first, --last \" \"and -a / --match-archives.\"\n            )\n        return self.list(\n            sort_by=args.sort_by.split(\",\"),\n            match=args.match_archives,\n            first=getattr(args, \"first\", None),\n            last=getattr(args, \"last\", None),\n            older=getattr(args, \"older\", None),\n            newer=getattr(args, \"newer\", None),\n            oldest=getattr(args, \"oldest\", None),\n            newest=getattr(args, \"newest\", None),\n            deleted=getattr(args, \"deleted\", False),\n        )\n\n    def get_one(self, match, *, match_end=r\"\\Z\", deleted=False):\n        \"\"\"get exactly one archive matching <match>\"\"\"\n        assert match is not None\n        archive_infos = self._matching_info_tuples(match, match_end, deleted=deleted)\n        if len(archive_infos) != 1:\n            raise CommandError(f\"{match} needed to match precisely one archive, but matched {len(archive_infos)}.\")\n        return archive_infos[0]\n\n    def _set_raw_dict(self, d):\n        \"\"\"set the dict we get from the msgpack unpacker\"\"\"\n        for k, v in d.items():\n            assert isinstance(k, str)\n            assert isinstance(v, dict) and \"id\" in v and \"time\" in v\n            self._archives[k] = v\n\n    def _get_raw_dict(self):\n        \"\"\"get the dict we can give to the msgpack packer\"\"\"\n        return self._archives\n\n\nclass Manifest:\n    @enum.unique\n    class Operation(enum.Enum):\n        # The comments here only roughly describe the scope of each feature. In the end, additions need to be\n        # based on potential problems older clients could produce when accessing newer repositories and the\n        # trade-offs of locking version out or still allowing access. As all older versions and their exact\n        # behaviours are known when introducing new features sometimes this might not match the general descriptions\n        # below.\n\n        # The READ operation describes which features are needed to list and extract the archives safely in the\n        # repository.\n        READ = \"read\"\n        # The CHECK operation is for all operations that need either to understand every detail\n        # of the repository (for consistency checks and repairs) or are seldom used functions that just\n        # should use the most restrictive feature set because more fine grained compatibility tracking is\n        # not needed.\n        CHECK = \"check\"\n        # The WRITE operation is for adding archives. Features here ensure that older clients don't add archives\n        # in an old format, or is used to lock out clients that for other reasons can no longer safely add new\n        # archives.\n        WRITE = \"write\"\n        # The DELETE operation is for all operations (like archive deletion) that need a 100% correct reference\n        # count and the need to be able to find all (directly and indirectly) referenced chunks of a given archive.\n        DELETE = \"delete\"\n\n    NO_OPERATION_CHECK: Sequence[Operation] = tuple()\n\n    SUPPORTED_REPO_FEATURES: frozenset[str] = frozenset([])\n\n    MANIFEST_ID = b\"\\0\" * 32\n\n    def __init__(self, key, repository, item_keys=None, ro_cls=RepoObj):\n        self.archives = Archives(repository, self)\n        self.config = {}\n        self.key = key\n        self.repo_objs = ro_cls(key)\n        self.repository = repository\n        self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS\n        self.timestamp = None\n\n    @property\n    def id_str(self):\n        return bin_to_hex(self.id)\n\n    @property\n    def last_timestamp(self):\n        return parse_timestamp(self.timestamp)\n\n    @classmethod\n    def load(cls, repository, operations, key=None, *, other=False, ro_cls=RepoObj):\n        from .item import ManifestItem\n        from .crypto.key import key_factory\n\n        cdata = repository.get_manifest()\n        if not key:\n            key = key_factory(repository, cdata, other=other, ro_cls=ro_cls)\n        manifest = cls(key, repository, ro_cls=ro_cls)\n        _, data = manifest.repo_objs.parse(cls.MANIFEST_ID, cdata, ro_type=ROBJ_MANIFEST)\n        manifest_dict = key.unpack_manifest(data)\n        m = ManifestItem(internal_dict=manifest_dict)\n        manifest.id = manifest.repo_objs.id_hash(data)\n        if m.get(\"version\") not in (1, 2):\n            raise ValueError(\"Invalid manifest version\")\n        manifest.archives.prepare(manifest, m)\n        manifest.timestamp = m.get(\"timestamp\")\n        manifest.config = m.config\n        # valid item keys are whatever is known in the repo or every key we know\n        manifest.item_keys = ITEM_KEYS\n        manifest.item_keys |= frozenset(m.config.get(\"item_keys\", []))  # new location of item_keys since borg2\n        manifest.item_keys |= frozenset(m.get(\"item_keys\", []))  # legacy: borg 1.x: item_keys not in config yet\n        manifest.check_repository_compatibility(operations)\n        return manifest\n\n    def check_repository_compatibility(self, operations):\n        for operation in operations:\n            assert isinstance(operation, self.Operation)\n            feature_flags = self.config.get(\"feature_flags\", None)\n            if feature_flags is None:\n                return\n            if operation.value not in feature_flags:\n                continue\n            requirements = feature_flags[operation.value]\n            if \"mandatory\" in requirements:\n                unsupported = set(requirements[\"mandatory\"]) - self.SUPPORTED_REPO_FEATURES\n                if unsupported:\n                    raise MandatoryFeatureUnsupported(list(unsupported))\n\n    def get_all_mandatory_features(self):\n        result = {}\n        feature_flags = self.config.get(\"feature_flags\", None)\n        if feature_flags is None:\n            return result\n\n        for operation, requirements in feature_flags.items():\n            if \"mandatory\" in requirements:\n                result[operation] = set(requirements[\"mandatory\"])\n        return result\n\n    def write(self):\n        from .item import ManifestItem\n\n        # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly\n        if self.timestamp is None:\n            self.timestamp = datetime.now(tz=timezone.utc).isoformat(timespec=\"microseconds\")\n        else:\n            incremented_ts = self.last_timestamp + timedelta(microseconds=1)\n            now_ts = datetime.now(tz=timezone.utc)\n            max_ts = max(incremented_ts, now_ts)\n            self.timestamp = max_ts.isoformat(timespec=\"microseconds\")\n        # include checks for limits as enforced by limited unpacker (used by load())\n        assert self.archives.count() <= MAX_ARCHIVES\n        assert len(self.item_keys) <= 100\n        self.config[\"item_keys\"] = tuple(sorted(self.item_keys))\n        manifest_archives = self.archives.finish(self)\n        manifest = ManifestItem(\n            version=2, archives=manifest_archives, timestamp=self.timestamp, config=StableDict(self.config)\n        )\n        data = self.key.pack_metadata(manifest.as_dict())\n        self.id = self.repo_objs.id_hash(data)\n        robj = self.repo_objs.format(self.MANIFEST_ID, {}, data, ro_type=ROBJ_MANIFEST)\n        self.repository.put_manifest(robj)\n"
  },
  {
    "path": "src/borg/paperkey.html",
    "content": "<!doctype html>\n<html moznomarginboxes mozdisallowselectionprint>\n<!--\n    Notes:\n        This may never cause external network connections. Everything needs to be included in this file.\n        No minified libraries. Everything needs to be auditable and in the preferred form for modification.\n\n        This file includes two libraries inline:\n            Kazuhiko Arase's qrcode-generator library (unpatched)\n            Chris Veness's sha256 implementation (locally modified to utf8Encode)\n        Both are MIT licensed.\n        As this script doesn’t interact with any untrusted parties or components, it should be safe to\n        use locally embedded copies of these libraries.\n-->\n\n\n<head>\n<title>BorgBackup Printable Key Template</title>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n<script>\n// https://github.com/kazuhikoarase/qrcode-generator/blob/master/js/qrcode.js\n//---------------------------------------------------------------------\n//\n// QR Code Generator for JavaScript\n//\n// Copyright (c) 2009 Kazuhiko Arase\n//\n// URL: http://www.d-project.com/\n//\n// Licensed under the MIT license:\n//  http://www.opensource.org/licenses/mit-license.php\n//\n// The word 'QR Code' is a registered trademark of\n// DENSO WAVE INCORPORATED\n//  https://www.denso-wave.com/qrcode/faqpatent-e.html\n//\n//---------------------------------------------------------------------\n\nvar qrcode = function() {\n\n  //---------------------------------------------------------------------\n  // qrcode\n  //---------------------------------------------------------------------\n\n  /**\n   * qrcode\n   * @param typeNumber 1 to 40\n   * @param errorCorrectionLevel 'L','M','Q','H'\n   */\n  var qrcode = function(typeNumber, errorCorrectionLevel) {\n\n    var PAD0 = 0xEC;\n    var PAD1 = 0x11;\n\n    var _typeNumber = typeNumber;\n    var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];\n    var _modules = null;\n    var _moduleCount = 0;\n    var _dataCache = null;\n    var _dataList = new Array();\n\n    var _this = {};\n\n    var makeImpl = function(test, maskPattern) {\n\n      _moduleCount = _typeNumber * 4 + 17;\n      _modules = function(moduleCount) {\n        var modules = new Array(moduleCount);\n        for (var row = 0; row < moduleCount; row += 1) {\n          modules[row] = new Array(moduleCount);\n          for (var col = 0; col < moduleCount; col += 1) {\n            modules[row][col] = null;\n          }\n        }\n        return modules;\n      }(_moduleCount);\n\n      setupPositionProbePattern(0, 0);\n      setupPositionProbePattern(_moduleCount - 7, 0);\n      setupPositionProbePattern(0, _moduleCount - 7);\n      setupPositionAdjustPattern();\n      setupTimingPattern();\n      setupTypeInfo(test, maskPattern);\n\n      if (_typeNumber >= 7) {\n        setupTypeNumber(test);\n      }\n\n      if (_dataCache == null) {\n        _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);\n      }\n\n      mapData(_dataCache, maskPattern);\n    };\n\n    var setupPositionProbePattern = function(row, col) {\n\n      for (var r = -1; r <= 7; r += 1) {\n\n        if (row + r <= -1 || _moduleCount <= row + r) continue;\n\n        for (var c = -1; c <= 7; c += 1) {\n\n          if (col + c <= -1 || _moduleCount <= col + c) continue;\n\n          if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )\n              || (0 <= c && c <= 6 && (r == 0 || r == 6) )\n              || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {\n            _modules[row + r][col + c] = true;\n          } else {\n            _modules[row + r][col + c] = false;\n          }\n        }\n      }\n    };\n\n    var getBestMaskPattern = function() {\n\n      var minLostPoint = 0;\n      var pattern = 0;\n\n      for (var i = 0; i < 8; i += 1) {\n\n        makeImpl(true, i);\n\n        var lostPoint = QRUtil.getLostPoint(_this);\n\n        if (i == 0 || minLostPoint > lostPoint) {\n          minLostPoint = lostPoint;\n          pattern = i;\n        }\n      }\n\n      return pattern;\n    };\n\n    var setupTimingPattern = function() {\n\n      for (var r = 8; r < _moduleCount - 8; r += 1) {\n        if (_modules[r][6] != null) {\n          continue;\n        }\n        _modules[r][6] = (r % 2 == 0);\n      }\n\n      for (var c = 8; c < _moduleCount - 8; c += 1) {\n        if (_modules[6][c] != null) {\n          continue;\n        }\n        _modules[6][c] = (c % 2 == 0);\n      }\n    };\n\n    var setupPositionAdjustPattern = function() {\n\n      var pos = QRUtil.getPatternPosition(_typeNumber);\n\n      for (var i = 0; i < pos.length; i += 1) {\n\n        for (var j = 0; j < pos.length; j += 1) {\n\n          var row = pos[i];\n          var col = pos[j];\n\n          if (_modules[row][col] != null) {\n            continue;\n          }\n\n          for (var r = -2; r <= 2; r += 1) {\n\n            for (var c = -2; c <= 2; c += 1) {\n\n              if (r == -2 || r == 2 || c == -2 || c == 2\n                  || (r == 0 && c == 0) ) {\n                _modules[row + r][col + c] = true;\n              } else {\n                _modules[row + r][col + c] = false;\n              }\n            }\n          }\n        }\n      }\n    };\n\n    var setupTypeNumber = function(test) {\n\n      var bits = QRUtil.getBCHTypeNumber(_typeNumber);\n\n      for (var i = 0; i < 18; i += 1) {\n        var mod = (!test && ( (bits >> i) & 1) == 1);\n        _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;\n      }\n\n      for (var i = 0; i < 18; i += 1) {\n        var mod = (!test && ( (bits >> i) & 1) == 1);\n        _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;\n      }\n    };\n\n    var setupTypeInfo = function(test, maskPattern) {\n\n      var data = (_errorCorrectionLevel << 3) | maskPattern;\n      var bits = QRUtil.getBCHTypeInfo(data);\n\n      // vertical\n      for (var i = 0; i < 15; i += 1) {\n\n        var mod = (!test && ( (bits >> i) & 1) == 1);\n\n        if (i < 6) {\n          _modules[i][8] = mod;\n        } else if (i < 8) {\n          _modules[i + 1][8] = mod;\n        } else {\n          _modules[_moduleCount - 15 + i][8] = mod;\n        }\n      }\n\n      // horizontal\n      for (var i = 0; i < 15; i += 1) {\n\n        var mod = (!test && ( (bits >> i) & 1) == 1);\n\n        if (i < 8) {\n          _modules[8][_moduleCount - i - 1] = mod;\n        } else if (i < 9) {\n          _modules[8][15 - i - 1 + 1] = mod;\n        } else {\n          _modules[8][15 - i - 1] = mod;\n        }\n      }\n\n      // fixed module\n      _modules[_moduleCount - 8][8] = (!test);\n    };\n\n    var mapData = function(data, maskPattern) {\n\n      var inc = -1;\n      var row = _moduleCount - 1;\n      var bitIndex = 7;\n      var byteIndex = 0;\n      var maskFunc = QRUtil.getMaskFunction(maskPattern);\n\n      for (var col = _moduleCount - 1; col > 0; col -= 2) {\n\n        if (col == 6) col -= 1;\n\n        while (true) {\n\n          for (var c = 0; c < 2; c += 1) {\n\n            if (_modules[row][col - c] == null) {\n\n              var dark = false;\n\n              if (byteIndex < data.length) {\n                dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);\n              }\n\n              var mask = maskFunc(row, col - c);\n\n              if (mask) {\n                dark = !dark;\n              }\n\n              _modules[row][col - c] = dark;\n              bitIndex -= 1;\n\n              if (bitIndex == -1) {\n                byteIndex += 1;\n                bitIndex = 7;\n              }\n            }\n          }\n\n          row += inc;\n\n          if (row < 0 || _moduleCount <= row) {\n            row -= inc;\n            inc = -inc;\n            break;\n          }\n        }\n      }\n    };\n\n    var createBytes = function(buffer, rsBlocks) {\n\n      var offset = 0;\n\n      var maxDcCount = 0;\n      var maxEcCount = 0;\n\n      var dcdata = new Array(rsBlocks.length);\n      var ecdata = new Array(rsBlocks.length);\n\n      for (var r = 0; r < rsBlocks.length; r += 1) {\n\n        var dcCount = rsBlocks[r].dataCount;\n        var ecCount = rsBlocks[r].totalCount - dcCount;\n\n        maxDcCount = Math.max(maxDcCount, dcCount);\n        maxEcCount = Math.max(maxEcCount, ecCount);\n\n        dcdata[r] = new Array(dcCount);\n\n        for (var i = 0; i < dcdata[r].length; i += 1) {\n          dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];\n        }\n        offset += dcCount;\n\n        var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);\n        var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);\n\n        var modPoly = rawPoly.mod(rsPoly);\n        ecdata[r] = new Array(rsPoly.getLength() - 1);\n        for (var i = 0; i < ecdata[r].length; i += 1) {\n          var modIndex = i + modPoly.getLength() - ecdata[r].length;\n          ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;\n        }\n      }\n\n      var totalCodeCount = 0;\n      for (var i = 0; i < rsBlocks.length; i += 1) {\n        totalCodeCount += rsBlocks[i].totalCount;\n      }\n\n      var data = new Array(totalCodeCount);\n      var index = 0;\n\n      for (var i = 0; i < maxDcCount; i += 1) {\n        for (var r = 0; r < rsBlocks.length; r += 1) {\n          if (i < dcdata[r].length) {\n            data[index] = dcdata[r][i];\n            index += 1;\n          }\n        }\n      }\n\n      for (var i = 0; i < maxEcCount; i += 1) {\n        for (var r = 0; r < rsBlocks.length; r += 1) {\n          if (i < ecdata[r].length) {\n            data[index] = ecdata[r][i];\n            index += 1;\n          }\n        }\n      }\n\n      return data;\n    };\n\n    var createData = function(typeNumber, errorCorrectionLevel, dataList) {\n\n      var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);\n\n      var buffer = qrBitBuffer();\n\n      for (var i = 0; i < dataList.length; i += 1) {\n        var data = dataList[i];\n        buffer.put(data.getMode(), 4);\n        buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );\n        data.write(buffer);\n      }\n\n      // calc num max data.\n      var totalDataCount = 0;\n      for (var i = 0; i < rsBlocks.length; i += 1) {\n        totalDataCount += rsBlocks[i].dataCount;\n      }\n\n      if (buffer.getLengthInBits() > totalDataCount * 8) {\n        throw new Error('code length overflow. ('\n          + buffer.getLengthInBits()\n          + '>'\n          + totalDataCount * 8\n          + ')');\n      }\n\n      // end code\n      if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {\n        buffer.put(0, 4);\n      }\n\n      // padding\n      while (buffer.getLengthInBits() % 8 != 0) {\n        buffer.putBit(false);\n      }\n\n      // padding\n      while (true) {\n\n        if (buffer.getLengthInBits() >= totalDataCount * 8) {\n          break;\n        }\n        buffer.put(PAD0, 8);\n\n        if (buffer.getLengthInBits() >= totalDataCount * 8) {\n          break;\n        }\n        buffer.put(PAD1, 8);\n      }\n\n      return createBytes(buffer, rsBlocks);\n    };\n\n    _this.addData = function(data) {\n      var newData = qr8BitByte(data);\n      _dataList.push(newData);\n      _dataCache = null;\n    };\n\n    _this.isDark = function(row, col) {\n      if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {\n        throw new Error(row + ',' + col);\n      }\n      return _modules[row][col];\n    };\n\n    _this.getModuleCount = function() {\n      return _moduleCount;\n    };\n\n    _this.make = function() {\n      makeImpl(false, getBestMaskPattern() );\n    };\n\n    _this.createTableTag = function(cellSize, margin) {\n\n      cellSize = cellSize || 2;\n      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;\n\n      var qrHtml = '';\n\n      qrHtml += '<table style=\"';\n      qrHtml += ' border-width: 0px; border-style: none;';\n      qrHtml += ' border-collapse: collapse;';\n      qrHtml += ' padding: 0px; margin: ' + margin + 'px;';\n      qrHtml += '\">';\n      qrHtml += '<tbody>';\n\n      for (var r = 0; r < _this.getModuleCount(); r += 1) {\n\n        qrHtml += '<tr>';\n\n        for (var c = 0; c < _this.getModuleCount(); c += 1) {\n          qrHtml += '<td style=\"';\n          qrHtml += ' border-width: 0px; border-style: none;';\n          qrHtml += ' border-collapse: collapse;';\n          qrHtml += ' padding: 0px; margin: 0px;';\n          qrHtml += ' width: ' + cellSize + 'px;';\n          qrHtml += ' height: ' + cellSize + 'px;';\n          qrHtml += ' background-color: ';\n          qrHtml += _this.isDark(r, c)? '#000000' : '#ffffff';\n          qrHtml += ';';\n          qrHtml += '\"/>';\n        }\n\n        qrHtml += '</tr>';\n      }\n\n      qrHtml += '</tbody>';\n      qrHtml += '</table>';\n\n      return qrHtml;\n    };\n\n    _this.createSvgTag = function(cellSize, margin) {\n\n      cellSize = cellSize || 2;\n      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;\n      var size = _this.getModuleCount() * cellSize + margin * 2;\n      var c, mc, r, mr, qrSvg='', rect;\n\n      rect = 'l' + cellSize + ',0 0,' + cellSize +\n        ' -' + cellSize + ',0 0,-' + cellSize + 'z ';\n\n      qrSvg += '<svg';\n      qrSvg += ' width=\"' + size + 'px\"';\n      qrSvg += ' height=\"' + size + 'px\"';\n      qrSvg += ' xmlns=\"http://www.w3.org/2000/svg\"';\n      qrSvg += '>';\n      qrSvg += '<path d=\"';\n\n      for (r = 0; r < _this.getModuleCount(); r += 1) {\n        mr = r * cellSize + margin;\n        for (c = 0; c < _this.getModuleCount(); c += 1) {\n          if (_this.isDark(r, c) ) {\n            mc = c*cellSize+margin;\n            qrSvg += 'M' + mc + ',' + mr + rect;\n          }\n        }\n      }\n\n      qrSvg += '\" stroke=\"transparent\" fill=\"black\"/>';\n      qrSvg += '</svg>';\n\n      return qrSvg;\n    };\n\n    _this.createImgTag = function(cellSize, margin) {\n\n      cellSize = cellSize || 2;\n      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;\n\n      var size = _this.getModuleCount() * cellSize + margin * 2;\n      var min = margin;\n      var max = size - margin;\n\n      return createImgTag(size, size, function(x, y) {\n        if (min <= x && x < max && min <= y && y < max) {\n          var c = Math.floor( (x - min) / cellSize);\n          var r = Math.floor( (y - min) / cellSize);\n          return _this.isDark(r, c)? 0 : 1;\n        } else {\n          return 1;\n        }\n      } );\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // qrcode.stringToBytes\n  //---------------------------------------------------------------------\n\n  qrcode.stringToBytes = function(s) {\n    var bytes = new Array();\n    for (var i = 0; i < s.length; i += 1) {\n      var c = s.charCodeAt(i);\n      bytes.push(c & 0xff);\n    }\n    return bytes;\n  };\n\n  //---------------------------------------------------------------------\n  // qrcode.createStringToBytes\n  //---------------------------------------------------------------------\n\n  /**\n   * @param unicodeData base64 string of byte array.\n   * [16bit Unicode],[16bit Bytes], ...\n   * @param numChars\n   */\n  qrcode.createStringToBytes = function(unicodeData, numChars) {\n\n    // create conversion map.\n\n    var unicodeMap = function() {\n\n      var bin = base64DecodeInputStream(unicodeData);\n      var read = function() {\n        var b = bin.read();\n        if (b == -1) throw new Error();\n        return b;\n      };\n\n      var count = 0;\n      var unicodeMap = {};\n      while (true) {\n        var b0 = bin.read();\n        if (b0 == -1) break;\n        var b1 = read();\n        var b2 = read();\n        var b3 = read();\n        var k = String.fromCharCode( (b0 << 8) | b1);\n        var v = (b2 << 8) | b3;\n        unicodeMap[k] = v;\n        count += 1;\n      }\n      if (count != numChars) {\n        throw new Error(count + ' != ' + numChars);\n      }\n\n      return unicodeMap;\n    }();\n\n    var unknownChar = '?'.charCodeAt(0);\n\n    return function(s) {\n      var bytes = new Array();\n      for (var i = 0; i < s.length; i += 1) {\n        var c = s.charCodeAt(i);\n        if (c < 128) {\n          bytes.push(c);\n        } else {\n          var b = unicodeMap[s.charAt(i)];\n          if (typeof b == 'number') {\n            if ( (b & 0xff) == b) {\n              // 1byte\n              bytes.push(b);\n            } else {\n              // 2bytes\n              bytes.push(b >>> 8);\n              bytes.push(b & 0xff);\n            }\n          } else {\n            bytes.push(unknownChar);\n          }\n        }\n      }\n      return bytes;\n    };\n  };\n\n  //---------------------------------------------------------------------\n  // QRMode\n  //---------------------------------------------------------------------\n\n  var QRMode = {\n    MODE_NUMBER :    1 << 0,\n    MODE_ALPHA_NUM : 1 << 1,\n    MODE_8BIT_BYTE : 1 << 2,\n    MODE_KANJI :     1 << 3\n  };\n\n  //---------------------------------------------------------------------\n  // QRErrorCorrectionLevel\n  //---------------------------------------------------------------------\n\n  var QRErrorCorrectionLevel = {\n    L : 1,\n    M : 0,\n    Q : 3,\n    H : 2\n  };\n\n  //---------------------------------------------------------------------\n  // QRMaskPattern\n  //---------------------------------------------------------------------\n\n  var QRMaskPattern = {\n    PATTERN000 : 0,\n    PATTERN001 : 1,\n    PATTERN010 : 2,\n    PATTERN011 : 3,\n    PATTERN100 : 4,\n    PATTERN101 : 5,\n    PATTERN110 : 6,\n    PATTERN111 : 7\n  };\n\n  //---------------------------------------------------------------------\n  // QRUtil\n  //---------------------------------------------------------------------\n\n  var QRUtil = function() {\n\n    var PATTERN_POSITION_TABLE = [\n      [],\n      [6, 18],\n      [6, 22],\n      [6, 26],\n      [6, 30],\n      [6, 34],\n      [6, 22, 38],\n      [6, 24, 42],\n      [6, 26, 46],\n      [6, 28, 50],\n      [6, 30, 54],\n      [6, 32, 58],\n      [6, 34, 62],\n      [6, 26, 46, 66],\n      [6, 26, 48, 70],\n      [6, 26, 50, 74],\n      [6, 30, 54, 78],\n      [6, 30, 56, 82],\n      [6, 30, 58, 86],\n      [6, 34, 62, 90],\n      [6, 28, 50, 72, 94],\n      [6, 26, 50, 74, 98],\n      [6, 30, 54, 78, 102],\n      [6, 28, 54, 80, 106],\n      [6, 32, 58, 84, 110],\n      [6, 30, 58, 86, 114],\n      [6, 34, 62, 90, 118],\n      [6, 26, 50, 74, 98, 122],\n      [6, 30, 54, 78, 102, 126],\n      [6, 26, 52, 78, 104, 130],\n      [6, 30, 56, 82, 108, 134],\n      [6, 34, 60, 86, 112, 138],\n      [6, 30, 58, 86, 114, 142],\n      [6, 34, 62, 90, 118, 146],\n      [6, 30, 54, 78, 102, 126, 150],\n      [6, 24, 50, 76, 102, 128, 154],\n      [6, 28, 54, 80, 106, 132, 158],\n      [6, 32, 58, 84, 110, 136, 162],\n      [6, 26, 54, 82, 110, 138, 166],\n      [6, 30, 58, 86, 114, 142, 170]\n    ];\n    var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);\n    var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);\n    var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);\n\n    var _this = {};\n\n    var getBCHDigit = function(data) {\n      var digit = 0;\n      while (data != 0) {\n        digit += 1;\n        data >>>= 1;\n      }\n      return digit;\n    };\n\n    _this.getBCHTypeInfo = function(data) {\n      var d = data << 10;\n      while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {\n        d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );\n      }\n      return ( (data << 10) | d) ^ G15_MASK;\n    };\n\n    _this.getBCHTypeNumber = function(data) {\n      var d = data << 12;\n      while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {\n        d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );\n      }\n      return (data << 12) | d;\n    };\n\n    _this.getPatternPosition = function(typeNumber) {\n      return PATTERN_POSITION_TABLE[typeNumber - 1];\n    };\n\n    _this.getMaskFunction = function(maskPattern) {\n\n      switch (maskPattern) {\n\n      case QRMaskPattern.PATTERN000 :\n        return function(i, j) { return (i + j) % 2 == 0; };\n      case QRMaskPattern.PATTERN001 :\n        return function(i, j) { return i % 2 == 0; };\n      case QRMaskPattern.PATTERN010 :\n        return function(i, j) { return j % 3 == 0; };\n      case QRMaskPattern.PATTERN011 :\n        return function(i, j) { return (i + j) % 3 == 0; };\n      case QRMaskPattern.PATTERN100 :\n        return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };\n      case QRMaskPattern.PATTERN101 :\n        return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };\n      case QRMaskPattern.PATTERN110 :\n        return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };\n      case QRMaskPattern.PATTERN111 :\n        return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };\n\n      default :\n        throw new Error('bad maskPattern:' + maskPattern);\n      }\n    };\n\n    _this.getErrorCorrectPolynomial = function(errorCorrectLength) {\n      var a = qrPolynomial([1], 0);\n      for (var i = 0; i < errorCorrectLength; i += 1) {\n        a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );\n      }\n      return a;\n    };\n\n    _this.getLengthInBits = function(mode, type) {\n\n      if (1 <= type && type < 10) {\n\n        // 1 - 9\n\n        switch(mode) {\n        case QRMode.MODE_NUMBER    : return 10;\n        case QRMode.MODE_ALPHA_NUM : return 9;\n        case QRMode.MODE_8BIT_BYTE : return 8;\n        case QRMode.MODE_KANJI     : return 8;\n        default :\n          throw new Error('mode:' + mode);\n        }\n\n      } else if (type < 27) {\n\n        // 10 - 26\n\n        switch(mode) {\n        case QRMode.MODE_NUMBER    : return 12;\n        case QRMode.MODE_ALPHA_NUM : return 11;\n        case QRMode.MODE_8BIT_BYTE : return 16;\n        case QRMode.MODE_KANJI     : return 10;\n        default :\n          throw new Error('mode:' + mode);\n        }\n\n      } else if (type < 41) {\n\n        // 27 - 40\n\n        switch(mode) {\n        case QRMode.MODE_NUMBER    : return 14;\n        case QRMode.MODE_ALPHA_NUM : return 13;\n        case QRMode.MODE_8BIT_BYTE : return 16;\n        case QRMode.MODE_KANJI     : return 12;\n        default :\n          throw new Error('mode:' + mode);\n        }\n\n      } else {\n        throw new Error('type:' + type);\n      }\n    };\n\n    _this.getLostPoint = function(qrcode) {\n\n      var moduleCount = qrcode.getModuleCount();\n\n      var lostPoint = 0;\n\n      // LEVEL1\n\n      for (var row = 0; row < moduleCount; row += 1) {\n        for (var col = 0; col < moduleCount; col += 1) {\n\n          var sameCount = 0;\n          var dark = qrcode.isDark(row, col);\n\n          for (var r = -1; r <= 1; r += 1) {\n\n            if (row + r < 0 || moduleCount <= row + r) {\n              continue;\n            }\n\n            for (var c = -1; c <= 1; c += 1) {\n\n              if (col + c < 0 || moduleCount <= col + c) {\n                continue;\n              }\n\n              if (r == 0 && c == 0) {\n                continue;\n              }\n\n              if (dark == qrcode.isDark(row + r, col + c) ) {\n                sameCount += 1;\n              }\n            }\n          }\n\n          if (sameCount > 5) {\n            lostPoint += (3 + sameCount - 5);\n          }\n        }\n      };\n\n      // LEVEL2\n\n      for (var row = 0; row < moduleCount - 1; row += 1) {\n        for (var col = 0; col < moduleCount - 1; col += 1) {\n          var count = 0;\n          if (qrcode.isDark(row, col) ) count += 1;\n          if (qrcode.isDark(row + 1, col) ) count += 1;\n          if (qrcode.isDark(row, col + 1) ) count += 1;\n          if (qrcode.isDark(row + 1, col + 1) ) count += 1;\n          if (count == 0 || count == 4) {\n            lostPoint += 3;\n          }\n        }\n      }\n\n      // LEVEL3\n\n      for (var row = 0; row < moduleCount; row += 1) {\n        for (var col = 0; col < moduleCount - 6; col += 1) {\n          if (qrcode.isDark(row, col)\n              && !qrcode.isDark(row, col + 1)\n              &&  qrcode.isDark(row, col + 2)\n              &&  qrcode.isDark(row, col + 3)\n              &&  qrcode.isDark(row, col + 4)\n              && !qrcode.isDark(row, col + 5)\n              &&  qrcode.isDark(row, col + 6) ) {\n            lostPoint += 40;\n          }\n        }\n      }\n\n      for (var col = 0; col < moduleCount; col += 1) {\n        for (var row = 0; row < moduleCount - 6; row += 1) {\n          if (qrcode.isDark(row, col)\n              && !qrcode.isDark(row + 1, col)\n              &&  qrcode.isDark(row + 2, col)\n              &&  qrcode.isDark(row + 3, col)\n              &&  qrcode.isDark(row + 4, col)\n              && !qrcode.isDark(row + 5, col)\n              &&  qrcode.isDark(row + 6, col) ) {\n            lostPoint += 40;\n          }\n        }\n      }\n\n      // LEVEL4\n\n      var darkCount = 0;\n\n      for (var col = 0; col < moduleCount; col += 1) {\n        for (var row = 0; row < moduleCount; row += 1) {\n          if (qrcode.isDark(row, col) ) {\n            darkCount += 1;\n          }\n        }\n      }\n\n      var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;\n      lostPoint += ratio * 10;\n\n      return lostPoint;\n    };\n\n    return _this;\n  }();\n\n  //---------------------------------------------------------------------\n  // QRMath\n  //---------------------------------------------------------------------\n\n  var QRMath = function() {\n\n    var EXP_TABLE = new Array(256);\n    var LOG_TABLE = new Array(256);\n\n    // initialize tables\n    for (var i = 0; i < 8; i += 1) {\n      EXP_TABLE[i] = 1 << i;\n    }\n    for (var i = 8; i < 256; i += 1) {\n      EXP_TABLE[i] = EXP_TABLE[i - 4]\n        ^ EXP_TABLE[i - 5]\n        ^ EXP_TABLE[i - 6]\n        ^ EXP_TABLE[i - 8];\n    }\n    for (var i = 0; i < 255; i += 1) {\n      LOG_TABLE[EXP_TABLE[i] ] = i;\n    }\n\n    var _this = {};\n\n    _this.glog = function(n) {\n\n      if (n < 1) {\n        throw new Error('glog(' + n + ')');\n      }\n\n      return LOG_TABLE[n];\n    };\n\n    _this.gexp = function(n) {\n\n      while (n < 0) {\n        n += 255;\n      }\n\n      while (n >= 256) {\n        n -= 255;\n      }\n\n      return EXP_TABLE[n];\n    };\n\n    return _this;\n  }();\n\n  //---------------------------------------------------------------------\n  // qrPolynomial\n  //---------------------------------------------------------------------\n\n  function qrPolynomial(num, shift) {\n\n    if (typeof num.length == 'undefined') {\n      throw new Error(num.length + '/' + shift);\n    }\n\n    var _num = function() {\n      var offset = 0;\n      while (offset < num.length && num[offset] == 0) {\n        offset += 1;\n      }\n      var _num = new Array(num.length - offset + shift);\n      for (var i = 0; i < num.length - offset; i += 1) {\n        _num[i] = num[i + offset];\n      }\n      return _num;\n    }();\n\n    var _this = {};\n\n    _this.getAt = function(index) {\n      return _num[index];\n    };\n\n    _this.getLength = function() {\n      return _num.length;\n    };\n\n    _this.multiply = function(e) {\n\n      var num = new Array(_this.getLength() + e.getLength() - 1);\n\n      for (var i = 0; i < _this.getLength(); i += 1) {\n        for (var j = 0; j < e.getLength(); j += 1) {\n          num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) );\n        }\n      }\n\n      return qrPolynomial(num, 0);\n    };\n\n    _this.mod = function(e) {\n\n      if (_this.getLength() - e.getLength() < 0) {\n        return _this;\n      }\n\n      var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) );\n\n      var num = new Array(_this.getLength() );\n      for (var i = 0; i < _this.getLength(); i += 1) {\n        num[i] = _this.getAt(i);\n      }\n\n      for (var i = 0; i < e.getLength(); i += 1) {\n        num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio);\n      }\n\n      // recursive call\n      return qrPolynomial(num, 0).mod(e);\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // QRRSBlock\n  //---------------------------------------------------------------------\n\n  var QRRSBlock = function() {\n\n    var RS_BLOCK_TABLE = [\n\n      // L\n      // M\n      // Q\n      // H\n\n      // 1\n      [1, 26, 19],\n      [1, 26, 16],\n      [1, 26, 13],\n      [1, 26, 9],\n\n      // 2\n      [1, 44, 34],\n      [1, 44, 28],\n      [1, 44, 22],\n      [1, 44, 16],\n\n      // 3\n      [1, 70, 55],\n      [1, 70, 44],\n      [2, 35, 17],\n      [2, 35, 13],\n\n      // 4\n      [1, 100, 80],\n      [2, 50, 32],\n      [2, 50, 24],\n      [4, 25, 9],\n\n      // 5\n      [1, 134, 108],\n      [2, 67, 43],\n      [2, 33, 15, 2, 34, 16],\n      [2, 33, 11, 2, 34, 12],\n\n      // 6\n      [2, 86, 68],\n      [4, 43, 27],\n      [4, 43, 19],\n      [4, 43, 15],\n\n      // 7\n      [2, 98, 78],\n      [4, 49, 31],\n      [2, 32, 14, 4, 33, 15],\n      [4, 39, 13, 1, 40, 14],\n\n      // 8\n      [2, 121, 97],\n      [2, 60, 38, 2, 61, 39],\n      [4, 40, 18, 2, 41, 19],\n      [4, 40, 14, 2, 41, 15],\n\n      // 9\n      [2, 146, 116],\n      [3, 58, 36, 2, 59, 37],\n      [4, 36, 16, 4, 37, 17],\n      [4, 36, 12, 4, 37, 13],\n\n      // 10\n      [2, 86, 68, 2, 87, 69],\n      [4, 69, 43, 1, 70, 44],\n      [6, 43, 19, 2, 44, 20],\n      [6, 43, 15, 2, 44, 16],\n\n      // 11\n      [4, 101, 81],\n      [1, 80, 50, 4, 81, 51],\n      [4, 50, 22, 4, 51, 23],\n      [3, 36, 12, 8, 37, 13],\n\n      // 12\n      [2, 116, 92, 2, 117, 93],\n      [6, 58, 36, 2, 59, 37],\n      [4, 46, 20, 6, 47, 21],\n      [7, 42, 14, 4, 43, 15],\n\n      // 13\n      [4, 133, 107],\n      [8, 59, 37, 1, 60, 38],\n      [8, 44, 20, 4, 45, 21],\n      [12, 33, 11, 4, 34, 12],\n\n      // 14\n      [3, 145, 115, 1, 146, 116],\n      [4, 64, 40, 5, 65, 41],\n      [11, 36, 16, 5, 37, 17],\n      [11, 36, 12, 5, 37, 13],\n\n      // 15\n      [5, 109, 87, 1, 110, 88],\n      [5, 65, 41, 5, 66, 42],\n      [5, 54, 24, 7, 55, 25],\n      [11, 36, 12, 7, 37, 13],\n\n      // 16\n      [5, 122, 98, 1, 123, 99],\n      [7, 73, 45, 3, 74, 46],\n      [15, 43, 19, 2, 44, 20],\n      [3, 45, 15, 13, 46, 16],\n\n      // 17\n      [1, 135, 107, 5, 136, 108],\n      [10, 74, 46, 1, 75, 47],\n      [1, 50, 22, 15, 51, 23],\n      [2, 42, 14, 17, 43, 15],\n\n      // 18\n      [5, 150, 120, 1, 151, 121],\n      [9, 69, 43, 4, 70, 44],\n      [17, 50, 22, 1, 51, 23],\n      [2, 42, 14, 19, 43, 15],\n\n      // 19\n      [3, 141, 113, 4, 142, 114],\n      [3, 70, 44, 11, 71, 45],\n      [17, 47, 21, 4, 48, 22],\n      [9, 39, 13, 16, 40, 14],\n\n      // 20\n      [3, 135, 107, 5, 136, 108],\n      [3, 67, 41, 13, 68, 42],\n      [15, 54, 24, 5, 55, 25],\n      [15, 43, 15, 10, 44, 16],\n\n      // 21\n      [4, 144, 116, 4, 145, 117],\n      [17, 68, 42],\n      [17, 50, 22, 6, 51, 23],\n      [19, 46, 16, 6, 47, 17],\n\n      // 22\n      [2, 139, 111, 7, 140, 112],\n      [17, 74, 46],\n      [7, 54, 24, 16, 55, 25],\n      [34, 37, 13],\n\n      // 23\n      [4, 151, 121, 5, 152, 122],\n      [4, 75, 47, 14, 76, 48],\n      [11, 54, 24, 14, 55, 25],\n      [16, 45, 15, 14, 46, 16],\n\n      // 24\n      [6, 147, 117, 4, 148, 118],\n      [6, 73, 45, 14, 74, 46],\n      [11, 54, 24, 16, 55, 25],\n      [30, 46, 16, 2, 47, 17],\n\n      // 25\n      [8, 132, 106, 4, 133, 107],\n      [8, 75, 47, 13, 76, 48],\n      [7, 54, 24, 22, 55, 25],\n      [22, 45, 15, 13, 46, 16],\n\n      // 26\n      [10, 142, 114, 2, 143, 115],\n      [19, 74, 46, 4, 75, 47],\n      [28, 50, 22, 6, 51, 23],\n      [33, 46, 16, 4, 47, 17],\n\n      // 27\n      [8, 152, 122, 4, 153, 123],\n      [22, 73, 45, 3, 74, 46],\n      [8, 53, 23, 26, 54, 24],\n      [12, 45, 15, 28, 46, 16],\n\n      // 28\n      [3, 147, 117, 10, 148, 118],\n      [3, 73, 45, 23, 74, 46],\n      [4, 54, 24, 31, 55, 25],\n      [11, 45, 15, 31, 46, 16],\n\n      // 29\n      [7, 146, 116, 7, 147, 117],\n      [21, 73, 45, 7, 74, 46],\n      [1, 53, 23, 37, 54, 24],\n      [19, 45, 15, 26, 46, 16],\n\n      // 30\n      [5, 145, 115, 10, 146, 116],\n      [19, 75, 47, 10, 76, 48],\n      [15, 54, 24, 25, 55, 25],\n      [23, 45, 15, 25, 46, 16],\n\n      // 31\n      [13, 145, 115, 3, 146, 116],\n      [2, 74, 46, 29, 75, 47],\n      [42, 54, 24, 1, 55, 25],\n      [23, 45, 15, 28, 46, 16],\n\n      // 32\n      [17, 145, 115],\n      [10, 74, 46, 23, 75, 47],\n      [10, 54, 24, 35, 55, 25],\n      [19, 45, 15, 35, 46, 16],\n\n      // 33\n      [17, 145, 115, 1, 146, 116],\n      [14, 74, 46, 21, 75, 47],\n      [29, 54, 24, 19, 55, 25],\n      [11, 45, 15, 46, 46, 16],\n\n      // 34\n      [13, 145, 115, 6, 146, 116],\n      [14, 74, 46, 23, 75, 47],\n      [44, 54, 24, 7, 55, 25],\n      [59, 46, 16, 1, 47, 17],\n\n      // 35\n      [12, 151, 121, 7, 152, 122],\n      [12, 75, 47, 26, 76, 48],\n      [39, 54, 24, 14, 55, 25],\n      [22, 45, 15, 41, 46, 16],\n\n      // 36\n      [6, 151, 121, 14, 152, 122],\n      [6, 75, 47, 34, 76, 48],\n      [46, 54, 24, 10, 55, 25],\n      [2, 45, 15, 64, 46, 16],\n\n      // 37\n      [17, 152, 122, 4, 153, 123],\n      [29, 74, 46, 14, 75, 47],\n      [49, 54, 24, 10, 55, 25],\n      [24, 45, 15, 46, 46, 16],\n\n      // 38\n      [4, 152, 122, 18, 153, 123],\n      [13, 74, 46, 32, 75, 47],\n      [48, 54, 24, 14, 55, 25],\n      [42, 45, 15, 32, 46, 16],\n\n      // 39\n      [20, 147, 117, 4, 148, 118],\n      [40, 75, 47, 7, 76, 48],\n      [43, 54, 24, 22, 55, 25],\n      [10, 45, 15, 67, 46, 16],\n\n      // 40\n      [19, 148, 118, 6, 149, 119],\n      [18, 75, 47, 31, 76, 48],\n      [34, 54, 24, 34, 55, 25],\n      [20, 45, 15, 61, 46, 16]\n    ];\n\n    var qrRSBlock = function(totalCount, dataCount) {\n      var _this = {};\n      _this.totalCount = totalCount;\n      _this.dataCount = dataCount;\n      return _this;\n    };\n\n    var _this = {};\n\n    var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {\n\n      switch(errorCorrectionLevel) {\n      case QRErrorCorrectionLevel.L :\n        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];\n      case QRErrorCorrectionLevel.M :\n        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];\n      case QRErrorCorrectionLevel.Q :\n        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];\n      case QRErrorCorrectionLevel.H :\n        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];\n      default :\n        return undefined;\n      }\n    };\n\n    _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {\n\n      var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);\n\n      if (typeof rsBlock == 'undefined') {\n        throw new Error('bad rs block @ typeNumber:' + typeNumber +\n            '/errorCorrectionLevel:' + errorCorrectionLevel);\n      }\n\n      var length = rsBlock.length / 3;\n\n      var list = new Array();\n\n      for (var i = 0; i < length; i += 1) {\n\n        var count = rsBlock[i * 3 + 0];\n        var totalCount = rsBlock[i * 3 + 1];\n        var dataCount = rsBlock[i * 3 + 2];\n\n        for (var j = 0; j < count; j += 1) {\n          list.push(qrRSBlock(totalCount, dataCount) );\n        }\n      }\n\n      return list;\n    };\n\n    return _this;\n  }();\n\n  //---------------------------------------------------------------------\n  // qrBitBuffer\n  //---------------------------------------------------------------------\n\n  var qrBitBuffer = function() {\n\n    var _buffer = new Array();\n    var _length = 0;\n\n    var _this = {};\n\n    _this.getBuffer = function() {\n      return _buffer;\n    };\n\n    _this.getAt = function(index) {\n      var bufIndex = Math.floor(index / 8);\n      return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;\n    };\n\n    _this.put = function(num, length) {\n      for (var i = 0; i < length; i += 1) {\n        _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);\n      }\n    };\n\n    _this.getLengthInBits = function() {\n      return _length;\n    };\n\n    _this.putBit = function(bit) {\n\n      var bufIndex = Math.floor(_length / 8);\n      if (_buffer.length <= bufIndex) {\n        _buffer.push(0);\n      }\n\n      if (bit) {\n        _buffer[bufIndex] |= (0x80 >>> (_length % 8) );\n      }\n\n      _length += 1;\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // qr8BitByte\n  //---------------------------------------------------------------------\n\n  var qr8BitByte = function(data) {\n\n    var _mode = QRMode.MODE_8BIT_BYTE;\n    var _data = data;\n    var _bytes = qrcode.stringToBytes(data);\n\n    var _this = {};\n\n    _this.getMode = function() {\n      return _mode;\n    };\n\n    _this.getLength = function(buffer) {\n      return _bytes.length;\n    };\n\n    _this.write = function(buffer) {\n      for (var i = 0; i < _bytes.length; i += 1) {\n        buffer.put(_bytes[i], 8);\n      }\n    };\n\n    return _this;\n  };\n\n  //=====================================================================\n  // GIF Support etc.\n  //\n\n  //---------------------------------------------------------------------\n  // byteArrayOutputStream\n  //---------------------------------------------------------------------\n\n  var byteArrayOutputStream = function() {\n\n    var _bytes = new Array();\n\n    var _this = {};\n\n    _this.writeByte = function(b) {\n      _bytes.push(b & 0xff);\n    };\n\n    _this.writeShort = function(i) {\n      _this.writeByte(i);\n      _this.writeByte(i >>> 8);\n    };\n\n    _this.writeBytes = function(b, off, len) {\n      off = off || 0;\n      len = len || b.length;\n      for (var i = 0; i < len; i += 1) {\n        _this.writeByte(b[i + off]);\n      }\n    };\n\n    _this.writeString = function(s) {\n      for (var i = 0; i < s.length; i += 1) {\n        _this.writeByte(s.charCodeAt(i) );\n      }\n    };\n\n    _this.toByteArray = function() {\n      return _bytes;\n    };\n\n    _this.toString = function() {\n      var s = '';\n      s += '[';\n      for (var i = 0; i < _bytes.length; i += 1) {\n        if (i > 0) {\n          s += ',';\n        }\n        s += _bytes[i];\n      }\n      s += ']';\n      return s;\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // base64EncodeOutputStream\n  //---------------------------------------------------------------------\n\n  var base64EncodeOutputStream = function() {\n\n    var _buffer = 0;\n    var _buflen = 0;\n    var _length = 0;\n    var _base64 = '';\n\n    var _this = {};\n\n    var writeEncoded = function(b) {\n      _base64 += String.fromCharCode(encode(b & 0x3f) );\n    };\n\n    var encode = function(n) {\n      if (n < 0) {\n        // error.\n      } else if (n < 26) {\n        return 0x41 + n;\n      } else if (n < 52) {\n        return 0x61 + (n - 26);\n      } else if (n < 62) {\n        return 0x30 + (n - 52);\n      } else if (n == 62) {\n        return 0x2b;\n      } else if (n == 63) {\n        return 0x2f;\n      }\n      throw new Error('n:' + n);\n    };\n\n    _this.writeByte = function(n) {\n\n      _buffer = (_buffer << 8) | (n & 0xff);\n      _buflen += 8;\n      _length += 1;\n\n      while (_buflen >= 6) {\n        writeEncoded(_buffer >>> (_buflen - 6) );\n        _buflen -= 6;\n      }\n    };\n\n    _this.flush = function() {\n\n      if (_buflen > 0) {\n        writeEncoded(_buffer << (6 - _buflen) );\n        _buffer = 0;\n        _buflen = 0;\n      }\n\n      if (_length % 3 != 0) {\n        // padding\n        var padlen = 3 - _length % 3;\n        for (var i = 0; i < padlen; i += 1) {\n          _base64 += '=';\n        }\n      }\n    };\n\n    _this.toString = function() {\n      return _base64;\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // base64DecodeInputStream\n  //---------------------------------------------------------------------\n\n  var base64DecodeInputStream = function(str) {\n\n    var _str = str;\n    var _pos = 0;\n    var _buffer = 0;\n    var _buflen = 0;\n\n    var _this = {};\n\n    _this.read = function() {\n\n      while (_buflen < 8) {\n\n        if (_pos >= _str.length) {\n          if (_buflen == 0) {\n            return -1;\n          }\n          throw new Error('unexpected end of file./' + _buflen);\n        }\n\n        var c = _str.charAt(_pos);\n        _pos += 1;\n\n        if (c == '=') {\n          _buflen = 0;\n          return -1;\n        } else if (c.match(/^\\s$/) ) {\n          // ignore if whitespace.\n          continue;\n        }\n\n        _buffer = (_buffer << 6) | decode(c.charCodeAt(0) );\n        _buflen += 6;\n      }\n\n      var n = (_buffer >>> (_buflen - 8) ) & 0xff;\n      _buflen -= 8;\n      return n;\n    };\n\n    var decode = function(c) {\n      if (0x41 <= c && c <= 0x5a) {\n        return c - 0x41;\n      } else if (0x61 <= c && c <= 0x7a) {\n        return c - 0x61 + 26;\n      } else if (0x30 <= c && c <= 0x39) {\n        return c - 0x30 + 52;\n      } else if (c == 0x2b) {\n        return 62;\n      } else if (c == 0x2f) {\n        return 63;\n      } else {\n        throw new Error('c:' + c);\n      }\n    };\n\n    return _this;\n  };\n\n  //---------------------------------------------------------------------\n  // gifImage (B/W)\n  //---------------------------------------------------------------------\n\n  var gifImage = function(width, height) {\n\n    var _width = width;\n    var _height = height;\n    var _data = new Array(width * height);\n\n    var _this = {};\n\n    _this.setPixel = function(x, y, pixel) {\n      _data[y * _width + x] = pixel;\n    };\n\n    _this.write = function(out) {\n\n      //---------------------------------\n      // GIF Signature\n\n      out.writeString('GIF87a');\n\n      //---------------------------------\n      // Screen Descriptor\n\n      out.writeShort(_width);\n      out.writeShort(_height);\n\n      out.writeByte(0x80); // 2bit\n      out.writeByte(0);\n      out.writeByte(0);\n\n      //---------------------------------\n      // Global Color Map\n\n      // black\n      out.writeByte(0x00);\n      out.writeByte(0x00);\n      out.writeByte(0x00);\n\n      // white\n      out.writeByte(0xff);\n      out.writeByte(0xff);\n      out.writeByte(0xff);\n\n      //---------------------------------\n      // Image Descriptor\n\n      out.writeString(',');\n      out.writeShort(0);\n      out.writeShort(0);\n      out.writeShort(_width);\n      out.writeShort(_height);\n      out.writeByte(0);\n\n      //---------------------------------\n      // Local Color Map\n\n      //---------------------------------\n      // Raster Data\n\n      var lzwMinCodeSize = 2;\n      var raster = getLZWRaster(lzwMinCodeSize);\n\n      out.writeByte(lzwMinCodeSize);\n\n      var offset = 0;\n\n      while (raster.length - offset > 255) {\n        out.writeByte(255);\n        out.writeBytes(raster, offset, 255);\n        offset += 255;\n      }\n\n      out.writeByte(raster.length - offset);\n      out.writeBytes(raster, offset, raster.length - offset);\n      out.writeByte(0x00);\n\n      //---------------------------------\n      // GIF Terminator\n      out.writeString(';');\n    };\n\n    var bitOutputStream = function(out) {\n\n      var _out = out;\n      var _bitLength = 0;\n      var _bitBuffer = 0;\n\n      var _this = {};\n\n      _this.write = function(data, length) {\n\n        if ( (data >>> length) != 0) {\n          throw new Error('length over');\n        }\n\n        while (_bitLength + length >= 8) {\n          _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) );\n          length -= (8 - _bitLength);\n          data >>>= (8 - _bitLength);\n          _bitBuffer = 0;\n          _bitLength = 0;\n        }\n\n        _bitBuffer = (data << _bitLength) | _bitBuffer;\n        _bitLength = _bitLength + length;\n      };\n\n      _this.flush = function() {\n        if (_bitLength > 0) {\n          _out.writeByte(_bitBuffer);\n        }\n      };\n\n      return _this;\n    };\n\n    var getLZWRaster = function(lzwMinCodeSize) {\n\n      var clearCode = 1 << lzwMinCodeSize;\n      var endCode = (1 << lzwMinCodeSize) + 1;\n      var bitLength = lzwMinCodeSize + 1;\n\n      // Setup LZWTable\n      var table = lzwTable();\n\n      for (var i = 0; i < clearCode; i += 1) {\n        table.add(String.fromCharCode(i) );\n      }\n      table.add(String.fromCharCode(clearCode) );\n      table.add(String.fromCharCode(endCode) );\n\n      var byteOut = byteArrayOutputStream();\n      var bitOut = bitOutputStream(byteOut);\n\n      // clear code\n      bitOut.write(clearCode, bitLength);\n\n      var dataIndex = 0;\n\n      var s = String.fromCharCode(_data[dataIndex]);\n      dataIndex += 1;\n\n      while (dataIndex < _data.length) {\n\n        var c = String.fromCharCode(_data[dataIndex]);\n        dataIndex += 1;\n\n        if (table.contains(s + c) ) {\n\n          s = s + c;\n\n        } else {\n\n          bitOut.write(table.indexOf(s), bitLength);\n\n          if (table.size() < 0xfff) {\n\n            if (table.size() == (1 << bitLength) ) {\n              bitLength += 1;\n            }\n\n            table.add(s + c);\n          }\n\n          s = c;\n        }\n      }\n\n      bitOut.write(table.indexOf(s), bitLength);\n\n      // end code\n      bitOut.write(endCode, bitLength);\n\n      bitOut.flush();\n\n      return byteOut.toByteArray();\n    };\n\n    var lzwTable = function() {\n\n      var _map = {};\n      var _size = 0;\n\n      var _this = {};\n\n      _this.add = function(key) {\n        if (_this.contains(key) ) {\n          throw new Error('dup key:' + key);\n        }\n        _map[key] = _size;\n        _size += 1;\n      };\n\n      _this.size = function() {\n        return _size;\n      };\n\n      _this.indexOf = function(key) {\n        return _map[key];\n      };\n\n      _this.contains = function(key) {\n        return typeof _map[key] != 'undefined';\n      };\n\n      return _this;\n    };\n\n    return _this;\n  };\n\n  var createImgTag = function(width, height, getPixel, alt) {\n\n    var gif = gifImage(width, height);\n    for (var y = 0; y < height; y += 1) {\n      for (var x = 0; x < width; x += 1) {\n        gif.setPixel(x, y, getPixel(x, y) );\n      }\n    }\n\n    var b = byteArrayOutputStream();\n    gif.write(b);\n\n    var base64 = base64EncodeOutputStream();\n    var bytes = b.toByteArray();\n    for (var i = 0; i < bytes.length; i += 1) {\n      base64.writeByte(bytes[i]);\n    }\n    base64.flush();\n\n    var img = '';\n    img += '<img';\n    img += '\\u0020src=\"';\n    img += 'data:image/gif;base64,';\n    img += base64;\n    img += '\"';\n    img += '\\u0020width=\"';\n    img += width;\n    img += '\"';\n    img += '\\u0020height=\"';\n    img += height;\n    img += '\"';\n    if (alt) {\n      img += '\\u0020alt=\"';\n      img += alt;\n      img += '\"';\n    }\n    img += '/>';\n\n    return img;\n  };\n\n  //---------------------------------------------------------------------\n  // returns qrcode function.\n\n  return qrcode;\n}();\n\n(function (factory) {\n  if (typeof define === 'function' && define.amd) {\n      define([], factory);\n  } else if (typeof exports === 'object') {\n      module.exports = factory();\n  }\n}(function () {\n    return qrcode;\n}));\n</script>\n<script>\n// http://www.movable-type.co.uk/scripts/sha256.html\n// local modifications: removed msg = msg.utf8Encode(); from start of Sha256.hash\n/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */\n/*  SHA-256 implementation in JavaScript                (c) Chris Veness 2002-2014 / MIT Licence  */\n/*                                                                                                */\n/*  - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html                              */\n/*        http://csrc.nist.gov/groups/ST/toolkit/examples.html                                    */\n/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */\n\n/* jshint node:true *//* global define, escape, unescape */\n'use strict';\n\n\n/**\n * SHA-256 hash function reference implementation.\n *\n * @namespace\n */\nvar Sha256 = {};\n\n\n/**\n * Generates SHA-256 hash of string.\n *\n * @param   {string} msg - String to be hashed\n * @returns {string} Hash of msg as hex character string\n */\nSha256.hash = function(msg) {\n    // convert string to UTF-8, as SHA only deals with byte-streams\n    //msg = msg.utf8Encode();\n\n    // constants [§4.2.2]\n    var K = [\n        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ];\n    // initial hash value [§5.3.1]\n    var H = [\n        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];\n\n    // PREPROCESSING\n\n    msg += String.fromCharCode(0x80);  // add trailing '1' bit (+ 0's padding) to string [§5.1.1]\n\n    // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]\n    var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length\n    var N = Math.ceil(l/16);  // number of 16-integer-blocks required to hold 'l' ints\n    var M = new Array(N);\n\n    for (var i=0; i<N; i++) {\n        M[i] = new Array(16);\n        for (var j=0; j<16; j++) {  // encode 4 chars per integer, big-endian encoding\n            M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |\n                      (msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));\n        } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0\n    }\n    // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]\n    // note: most significant word would be (len-1)*8 >>> 32, but since JS converts\n    // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators\n    M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]);\n    M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;\n\n\n    // HASH COMPUTATION [§6.1.2]\n\n    var W = new Array(64); var a, b, c, d, e, f, g, h;\n    for (var i=0; i<N; i++) {\n\n        // 1 - prepare message schedule 'W'\n        for (var t=0;  t<16; t++) W[t] = M[i][t];\n        for (var t=16; t<64; t++) W[t] = (Sha256.σ1(W[t-2]) + W[t-7] + Sha256.σ0(W[t-15]) + W[t-16]) & 0xffffffff;\n\n        // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value\n        a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7];\n\n        // 3 - main loop (note 'addition modulo 2^32')\n        for (var t=0; t<64; t++) {\n            var T1 = h + Sha256.Σ1(e) + Sha256.Ch(e, f, g) + K[t] + W[t];\n            var T2 =     Sha256.Σ0(a) + Sha256.Maj(a, b, c);\n            h = g;\n            g = f;\n            f = e;\n            e = (d + T1) & 0xffffffff;\n            d = c;\n            c = b;\n            b = a;\n            a = (T1 + T2) & 0xffffffff;\n        }\n         // 4 - compute the new intermediate hash value (note 'addition modulo 2^32')\n        H[0] = (H[0]+a) & 0xffffffff;\n        H[1] = (H[1]+b) & 0xffffffff;\n        H[2] = (H[2]+c) & 0xffffffff;\n        H[3] = (H[3]+d) & 0xffffffff;\n        H[4] = (H[4]+e) & 0xffffffff;\n        H[5] = (H[5]+f) & 0xffffffff;\n        H[6] = (H[6]+g) & 0xffffffff;\n        H[7] = (H[7]+h) & 0xffffffff;\n    }\n\n    return Sha256.toHexStr(H[0]) + Sha256.toHexStr(H[1]) + Sha256.toHexStr(H[2]) + Sha256.toHexStr(H[3]) +\n           Sha256.toHexStr(H[4]) + Sha256.toHexStr(H[5]) + Sha256.toHexStr(H[6]) + Sha256.toHexStr(H[7]);\n};\n\n\n/**\n * Rotates right (circular right shift) value x by n positions [§3.2.4].\n * @private\n */\nSha256.ROTR = function(n, x) {\n    return (x >>> n) | (x << (32-n));\n};\n\n/**\n * Logical functions [§4.1.2].\n * @private\n */\nSha256.Σ0  = function(x) { return Sha256.ROTR(2,  x) ^ Sha256.ROTR(13, x) ^ Sha256.ROTR(22, x); };\nSha256.Σ1  = function(x) { return Sha256.ROTR(6,  x) ^ Sha256.ROTR(11, x) ^ Sha256.ROTR(25, x); };\nSha256.σ0  = function(x) { return Sha256.ROTR(7,  x) ^ Sha256.ROTR(18, x) ^ (x>>>3);  };\nSha256.σ1  = function(x) { return Sha256.ROTR(17, x) ^ Sha256.ROTR(19, x) ^ (x>>>10); };\nSha256.Ch  = function(x, y, z) { return (x & y) ^ (~x & z); };\nSha256.Maj = function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); };\n\n\n/**\n * Hexadecimal representation of a number.\n * @private\n */\nSha256.toHexStr = function(n) {\n    // note can't use toString(16) as it is implementation-dependent,\n    // and in IE returns signed numbers when used on full words\n    var s=\"\", v;\n    for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); }\n    return s;\n};\n\n\n/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */\n\n\n/** Extend String object with method to encode multi-byte string to utf8\n *  - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */\nif (typeof String.prototype.utf8Encode == 'undefined') {\n    String.prototype.utf8Encode = function() {\n        return unescape( encodeURIComponent( this ) );\n    };\n}\n\n/** Extend String object with method to decode utf8 string to multi-byte */\nif (typeof String.prototype.utf8Decode == 'undefined') {\n    String.prototype.utf8Decode = function() {\n        try {\n            return decodeURIComponent( escape( this ) );\n        } catch (e) {\n            return this; // invalid UTF-8? return as-is\n        }\n    };\n}\n\n\n/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */\nif (typeof module != 'undefined' && module.exports) module.exports = Sha256; // CommonJs export\nif (typeof define == 'function' && define.amd) define([], function() { return Sha256; }); // AMD\n</script>\n<style>\n\n    #typein {\n        position: absolute;\n        font-family: monospace;\n        font-size: 10mm;\n        transform-origin: 0 0;\n        white-space: pre;\n    }\n\n    .columns2 {\n        -moz-column-count: 2;\n        -webkit-column-count: 2;\n        column-count: 2;\n        -webkit-column-rule: 4mm outset #ddd;\n        -moz-column-rule: 4mm outset #ddd;\n        column-rule: 4mm outset #ddd;\n    }\n\n    #title {\n        font-size: 6mm;\n    }\n\n\n    .placeholder {\n        color: #ddd;\n    }\n\n    @media print {\n        @page {\n            margin: 0mm; /* disable url etc headers and footers (e.g. chrome, newer firefox) */\n        }\n\n        html {\n            margin: 15mm 20mm;\n            padding: 0px;\n        }\n        * {\n            -webkit-print-color-adjust: exact;\n        }\n\n        #settings {\n            display: none;\n        }\n\n        #printout, body {\n            margin: 0px;\n            padding: 0px;\n        }\n\n        #lowermargin {\n            display: none;\n        }\n\n    }\n\n    @media screen, projection {\n        *[contenteditable] {\n            cursor: pointer;\n            border: 1px dotted #A4DDED;\n        }\n        *[contenteditable]:hover,\n        *[contenteditable]:focus {\n            background: #DEF;\n            box-shadow: 0 0 1em 0.5em #DEF;\n        }\n\n        html {\n            background: #999;\n            padding: 0.5in;\n        }\n\n        #settings {\n            box-sizing: border-box;\n            margin: 0 auto;\n            background: #FFF;\n            border-radius: 1px;\n            box-shadow: 0 0 1in -0.25in rgba(0, 0, 0, 0.5);\n\n            margin-bottom: 2cm;\n            padding: 10px;\n        }\n\n        .block {\n            display: inline-block;\n            vertical-align: top;\n        }\n\n        .label {\n            display: inline-block;\n            min-width: 50mm;\n        }\n\n        #keyfile, #keyfile_expander {\n            min-width: 140mm;\n        }\n\n        .instructions {\n            max-width: 140mm;\n            padding-bottom: 1rem;\n        }\n\n        #fileinput {\n            display: none;\n        }\n\n        #printout {\n            box-sizing: border-box;\n            height: 279mm; /* smallest of US Letter */\n            width: 210mm; /* and DIN/ISO A4 */\n            /*height: 297mm; use this for real A4 */\n            margin: 0 auto;\n            overflow: hidden;\n            padding: 15mm 20mm 0mm 20mm;\n            background: #FFF;\n            border-radius: 1px;\n            box-shadow: 0 0 1in -0.25in rgba(0, 0, 0, 0.5);\n        }\n\n        #lowermargin {\n            height: 15mm;\n        }\n    }\n\n    /* center the QR code on the page */\n    #qr {\n      width: 100%;\n      text-align: center;\n    }\n</style>\n</head>\n<body>\n    <div id=\"settings\">\n        <div class=\"instructions\">\n           <p>To create a printable key, either paste the contents of your keyfile or a key export into the text field\n           below, or select a key export file.</p>\n           <p>To create a key export, use:</p>\n           <pre>borg key export /path/to/repository exportfile.txt</pre>\n           <p>If you are using keyfile mode, keyfiles are usually stored in $HOME/.config/borg/keys/.</p>\n           <p>You can edit the parts with a light blue border in the print preview below by clicking into them.</p>\n           <p>Key security: This print template will never send anything to remote servers. Keep in mind that printing\n              might involve computers that can store the printed image, such as cloud printing services or\n              networked printers.</p>\n        </div>\n        <div class=\"block\">\n            <div id=\"keyfile_expander\" style=\"display:none;\"><a href=\"#\" onclick=\"document.getElementById('keyfile').style.display='inline';document.getElementById('keyfile_expander').style.display='none';\">Show keyfile</a></div>\n            <textarea id=\"keyfile\" rows=\"10\" cols=\"73\"></textarea>\n        </div>\n        <div class=\"block\">\n            <span class=\"label\">QR error correction:</span>\n            <select id=\"errorCorrectionLevel\" name=\"e\">\n                <option value=\"L\">7% (L)</option>\n                <option value=\"M\">15% (M)</option>\n                <option value=\"Q\">25% (Q)</option>\n                <option value=\"H\" selected=\"selected\">30% (H)</option>\n            </select><br>\n            <span class=\"label\">QR code size</span><input type=\"range\" id=\"qrsize\" min=\"45\" max=\"157\" value=\"100\"><br>\n            <span class=\"label\">Text size</span><input type=\"range\" id=\"textsize\" min=\"1\" max=\"60\" value=\"27\"><br>\n            <span class=\"label\">Text columns</span>\n            <select id=\"cols\">\n                <option value=\"1\">1</option>\n                <option value=\"2\" selected=\"selected\">2</option>\n            </select><br>\n\n        </div>\n        <div>\n            <input type=\"file\" id=\"fileinput\"/>\n            <input type=\"button\" value=\"Update\" onclick=\"update()\"/>\n        </div>\n    </div>\n\n    <div id=\"printout\">\n        <div id=\"title\" contenteditable>BorgBackup Printable Key Backup</div>\n        <div contenteditable>To restore, either scan the QR code below, decode it, and import it using\n<pre>borg key import /path/to/repo scannedfile</pre>\n\nAlternatively, run\n<pre>borg key import --paper /path/to/repo</pre> and type the text below.<br><br></div>\n        <div id=\"typein\"></div>\n        <div id=\"spacer\"></div>\n        <div id=\"qr\"></div>\n        <div contenteditable>Notes:</div>\n        <div id=\"lowermargin\"></div>\n    </div>\n\n<script>\n\"use strict\";\n\nfunction placeholder_qrcode() {\n    return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 194 194\" width=\"170mm\" height=\"170mm\">' +\n                '<path d=\"m 186,8 0,14 -14,0 0,-14 m 12,12 0,-10 -10,0 0,10 m -162,-8 0,6 6,0 0,-6 m 164,6 -6,0 0,-6 6,0 z M 10,10 20,10 20,20 10,20 M 8,8 8,22 22,22 22,8 m 150,166 0,-2 2,0 0,2 z m 6,-6 -10,0 0,10 10,0 m -8,-8 6,0 0,6 -6,0 z m -158,12 0,-6 6,0 0,6 m 2,-8 0,10 -10,0 0,-10 m 0,0 10,0 m 2,-2 -14,0 0,14 14,0\" style=\"fill: #ddd;\"/>' +\n                '<text style=\"font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;\" x=\"47.321415\" y=\"99.851288\">QR Code goes here</text>' +\n            '</svg>';\n}\n\nfunction create_qrcode(text, errorCorrectionLevel) {\n    for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {\n        try {\n            var qr = qrcode(typeNumber, errorCorrectionLevel);\n            qr.addData(text);\n            qr.make();\n            return qr.createSvgTag();\n        } catch (e) {\n            if (!e.message || !e.message.startsWith(\"code length overflow\")) {\n                throw e;\n            }\n        }\n    }\n\n    return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 194 194\" width=\"194px\" height=\"194px\">' +\n                '<path d=\"m 186,8 0,14 -14,0 0,-14 m 12,12 0,-10 -10,0 0,10 m -162,-8 0,6 6,0 0,-6 m 164,6 -6,0 0,-6 6,0 z M 10,10 20,10 20,20 10,20 M 8,8 8,22 22,22 22,8 m 150,166 0,-2 2,0 0,2 z m 6,-6 -10,0 0,10 10,0 m -8,-8 6,0 0,6 -6,0 z m -158,12 0,-6 6,0 0,6 m 2,-8 0,10 -10,0 0,-10 m 0,0 10,0 m 2,-2 -14,0 0,14 14,0\" style=\"fill: #ddd;\"/>' +\n                '<text style=\"font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;\" x=\"30\" y=\"90\">Key too long for this</text>' +\n                '<text style=\"font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;\" x=\"30\" y=\"110\">error correction level</text>' +\n            '</svg>'\n}\n\nfunction placeholder_paperkey() {\n\n    var res = \"BORG PAPER KEY v1\\n\"\n    res += \"id: ?? / ?????? ?????? ?????? / ?????? ?????? - ??\\n\"\n    for (var i = 1; i <= 19; i++) {\n        res += (i < 10 ? ' ' : '') + i.toString();\n        res += \": ?????? ?????? ?????? ?????? ?????? ?????? - ??\\n\";\n    }\n\n    return res;\n}\n\nfunction create_paperkey(text) {\n    var grouped = function(s) {\n        var ret = '';\n        var i = 0\n        for (var ch of s) {\n            if (i && i % 6 == 0) {\n                ret += ' ';\n            }\n            ret += ch;\n            i += 1;\n        }\n        return ret;\n    };\n    var toHex = function(b) {\n        var ret = '';\n        for (var ch of b) {\n            var tmp = ch.charCodeAt(0).toString(16);\n            if (tmp.length == 0) {\n                ret += '00';\n            } else if (tmp.length == 1) {\n                ret += '0';\n            }\n            ret += tmp;\n        }\n        return ret;\n    }\n    var sha256_truncated = function(b, digits) {\n        return Sha256.hash(b).substr(0, digits);\n    };\n\n    var export_ = '';\n\n    var lines = text.split('\\n');\n    var first_line = lines.shift();\n\n    var binary = atob(lines.join('\\n'));\n    export_ += 'BORG PAPER KEY v1\\n';\n    lines = Math.ceil(binary.length / 18);\n    var repoid = first_line.substr(9, 18)\n    var complete_checksum = sha256_truncated(binary, 12)\n    export_ += 'id: ';\n    export_ += lines.toString();\n    var checksum = sha256_truncated(lines.toString() + '/' + repoid + '/' + complete_checksum, 2);\n    export_ += ' / ' + grouped(repoid) + ' / ' + grouped(complete_checksum) + ' - ' + checksum +  '\\n';\n\n    var idx = 0\n    while (binary.length) {\n        idx += 1\n        var binline = binary.substr(0, 18);\n        checksum = sha256_truncated(String.fromCharCode(idx >> 8) + String.fromCharCode(idx & 0xff) + binline, 2);\n        export_ += (idx < 10 ? ' ' : '') + idx.toString();\n        export_ += ': ' + grouped(toHex(binline)) + ' - ' + checksum + '\\n';\n        binary = binary.substr(18);\n    }\n\n    return export_;\n}\n\nfunction update() {\n    var text = document.getElementById('keyfile').value;\n    var target = document.getElementById('qr');\n    var typein = document.getElementById('typein');\n\n    if (!text) {\n        typein.innerText = placeholder_paperkey()\n        target.innerHTML = placeholder_qrcode();\n        var svg = target.children[0];\n\n        typein.classList.add(\"placeholder\");\n        svg.classList.add(\"placeholder\");\n    } else {\n        var e = document.getElementById('errorCorrectionLevel').value;\n        target.innerHTML = create_qrcode(text, e);\n        var svg = target.children[0];\n        svg.setAttribute(\"viewBox\", \"0 0 \" + svg.getAttribute(\"width\").replace(\"px\", \"\")\n                        + \" \" + svg.getAttribute(\"height\").replace(\"px\", \"\"));\n\n        typein.innerText = create_paperkey(text);\n\n        typein.classList.remove(\"placeholder\");\n        svg.classList.remove(\"placeholder\");\n    }\n\n    var printout = document.getElementById('printout');\n    var size = document.getElementById('qrsize').value;\n\n    typein.style.transform = \"scale(\" + document.getElementById('textsize').value / 100 + \")\";\n    if (document.getElementById('cols').value == \"2\") {\n        typein.classList.add(\"columns2\");\n    } else {\n        typein.classList.remove(\"columns2\");\n    }\n    document.getElementById('spacer').style.height = typein.getBoundingClientRect().height + \"px\";\n\n    while (true) {\n        svg.setAttribute(\"width\", size + \"mm\");\n        svg.setAttribute(\"height\", size + \"mm\");\n\n        if (printout.scrollHeight <= printout.clientHeight || size <= 50) {\n            break;\n        }\n        size -= 1;\n    }\n\n};\n\nfunction call_soon(func, wait) {\n    var timeout;\n    return function() {\n        clearTimeout(timeout);\n        timeout = setTimeout(function() {\n            timeout = null;\n            func();\n        }, wait);\n    };\n};\n\nfunction updateFromFile() {\n    var f = document.getElementById('fileinput').files[0];\n\n    if (f) {\n        var r = new FileReader();\n        r.onload = function(ev) {\n            var contents = ev.target.result;\n            document.getElementById('keyfile').value = contents;\n            document.getElementById('keyfile').style.display='none';\n            document.getElementById('keyfile_expander').style.display='block';\n            update();\n        };\n        r.readAsText(f);\n    }\n}\n\nif (window.File && window.FileReader && window.FileList && window.Blob) {\n    document.getElementById('fileinput').style.display='inline';\n    document.getElementById('fileinput').addEventListener('change', updateFromFile, false);\n\n    if (document.getElementById('fileinput').files.length) {\n        updateFromFile();\n    }\n}\n\nfunction loaded() {\n    document.getElementById('qrsize').addEventListener('change', update, false);\n    document.getElementById('qrsize').addEventListener('input', call_soon(update), false);\n    document.getElementById('textsize').addEventListener('change', update, false);\n    document.getElementById('textsize').addEventListener('input', call_soon(update), false);\n\n    document.getElementById('cols').addEventListener('change', update, false);\n    document.getElementById('errorCorrectionLevel').addEventListener('change', update, false);\n\n    var editables = document.querySelectorAll(\"*[contenteditable]\");\n    for (var i = 0; i < editables.length; ++i) {\n        editables[i].addEventListener('input', call_soon(update), false);\n    }\n\n    update();\n}\n\nwindow.addEventListener(\"load\", loaded, false);\n\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "src/borg/patterns.py",
    "content": "import fnmatch\nimport posixpath\nimport re\nimport sys\nimport unicodedata\nfrom collections import namedtuple\nfrom enum import Enum\n\nfrom .helpers import clean_lines, shellpattern\nfrom .helpers.argparsing import Action, ArgumentTypeError\nfrom .helpers.errors import Error\n\n\ndef parse_patternfile_line(line, roots, ie_commands, fallback):\n    \"\"\"Parse a pattern-file line and act depending on which command it represents.\"\"\"\n    ie_command = parse_inclexcl_command(line, fallback=fallback)\n    if ie_command.cmd is IECommand.RootPath:\n        roots.append(ie_command.val)\n    elif ie_command.cmd is IECommand.PatternStyle:\n        fallback = ie_command.val\n    else:\n        # it is some kind of include/exclude command\n        ie_commands.append(ie_command)\n    return fallback\n\n\ndef load_pattern_file(fileobj, roots, ie_commands, fallback=None):\n    if fallback is None:\n        fallback = ShellPattern  # ShellPattern is defined later in this module\n    for line in clean_lines(fileobj):\n        fallback = parse_patternfile_line(line, roots, ie_commands, fallback)\n\n\ndef load_exclude_file(fileobj, patterns):\n    for patternstr in clean_lines(fileobj):\n        patterns.append(parse_exclude_pattern(patternstr))\n\n\nclass ArgparsePatternAction(Action):\n    def __init__(self, nargs=1, **kw):\n        super().__init__(nargs=nargs, **kw)\n\n    def __call__(self, parser, args, values, option_string=None):\n        parse_patternfile_line(values[0], args.pattern_roots, args.patterns, ShellPattern)\n\n\nclass ArgparsePatternFileAction(Action):\n    def __init__(self, nargs=1, **kw):\n        super().__init__(nargs=nargs, **kw)\n\n    def __call__(self, parser, args, values, option_string=None):\n        \"\"\"Load and parse patterns from a file.\n        Lines empty or starting with '#' after stripping whitespace on both line ends are ignored.\n        \"\"\"\n        filename = values[0]\n        try:\n            with open(filename) as f:\n                self.parse(f, args)\n        except FileNotFoundError as e:\n            raise Error(str(e))\n\n    def parse(self, fobj, args):\n        load_pattern_file(fobj, args.pattern_roots, args.patterns)\n\n\nclass ArgparseExcludeFileAction(ArgparsePatternFileAction):\n    def parse(self, fobj, args):\n        load_exclude_file(fobj, args.patterns)\n\n\nclass PatternMatcher:\n    \"\"\"Represents a collection of pattern objects to match paths against.\n\n    *fallback* is a boolean value that *match()* returns if no matching patterns are found.\n\n    \"\"\"\n\n    def __init__(self, fallback=None):\n        self._items = []\n\n        # Value to return from match function when none of the patterns match.\n        self.fallback = fallback\n\n        # optimizations\n        self._path_full_patterns = {}  # full path -> return value\n\n        # indicates whether the last match() call ended on a pattern for which\n        # we should recurse into any matching folder.  Will be set to True or\n        # False when calling match().\n        self.recurse_dir = None\n\n        # whether to recurse into directories when no match is found\n        # TODO: allow modification as a config option?\n        self.recurse_dir_default = True\n\n        self.include_patterns = []\n\n        # TODO: move this info to parse_inclexcl_command and store in PatternBase subclass?\n        self.is_include_cmd = {IECommand.Exclude: False, IECommand.ExcludeNoRecurse: False, IECommand.Include: True}\n\n    def empty(self):\n        return not len(self._items) and not len(self._path_full_patterns)\n\n    def _add(self, pattern, cmd):\n        \"\"\"*cmd* is an IECommand value.\"\"\"\n        if isinstance(pattern, PathFullPattern):\n            key = pattern.pattern  # full, normalized path\n            self._path_full_patterns[key] = cmd\n        else:\n            self._items.append((pattern, cmd))\n\n    def add(self, patterns, cmd):\n        \"\"\"Add list of patterns to internal list. *cmd* indicates whether the\n        pattern is an include/exclude pattern, and whether recursion should be\n        done on excluded folders.\n        \"\"\"\n        for pattern in patterns:\n            self._add(pattern, cmd)\n\n    def add_includepaths(self, include_paths):\n        \"\"\"Used to add inclusion-paths from args.paths (from the command line).\"\"\"\n        include_patterns = [parse_pattern(p, PathPrefixPattern) for p in include_paths]\n        self.add(include_patterns, IECommand.Include)\n        self.fallback = not include_patterns\n        self.include_patterns = include_patterns\n\n    def get_unmatched_include_patterns(self):\n        \"\"\"Note that this only returns patterns added via *add_includepaths*, and it\n        won't return PathFullPattern patterns, as we do not maintain match_count for them.\n        \"\"\"\n        return [p for p in self.include_patterns if p.match_count == 0 and not isinstance(p, PathFullPattern)]\n\n    def add_inclexcl(self, patterns):\n        \"\"\"Add list of patterns (of type CmdTuple) to internal list.\"\"\"\n        for pattern, cmd in patterns:\n            self._add(pattern, cmd)\n\n    def match(self, path):\n        \"\"\"Return True or False depending on whether *path* is matched.\n\n        If no match is found among the patterns in this matcher, then the value\n        in self.fallback is returned (defaults to None).\n\n        \"\"\"\n        path = normalize_path(path).lstrip(\"/\")\n        # do a fast lookup for full path matches (note: we do not count such matches):\n        non_existent = object()\n        value = self._path_full_patterns.get(path, non_existent)\n\n        if value is not non_existent:\n            # we have a full path match!\n            self.recurse_dir = command_recurses_dir(value)\n            return self.is_include_cmd[value]\n\n        # this is the slow way, if we have many patterns in self._items:\n        for pattern, cmd in self._items:\n            if pattern.match(path, normalize=False):\n                self.recurse_dir = pattern.recurse_dir\n                return self.is_include_cmd[cmd]\n\n        # by default we will recurse if there is no match\n        self.recurse_dir = self.recurse_dir_default\n        return self.fallback\n\n\ndef normalize_path(path):\n    \"\"\"normalize paths for MacOS (but do nothing on other platforms)\"\"\"\n    # HFS+ converts paths to a canonical form, so users shouldn't be required to enter an exact match.\n    # Windows and Unix filesystems allow different forms, so users always have to enter an exact match.\n    return unicodedata.normalize(\"NFD\", path) if sys.platform == \"darwin\" else path\n\n\nclass PatternBase:\n    \"\"\"Shared logic for inclusion/exclusion patterns.\"\"\"\n\n    PREFIX: str = None\n\n    def __init__(self, pattern, recurse_dir=False):\n        self.pattern_orig = pattern\n        self.match_count = 0\n        pattern = normalize_path(pattern)\n        self._prepare(pattern)\n        self.recurse_dir = recurse_dir\n\n    def match(self, path, normalize=True):\n        \"\"\"Return a boolean indicating whether *path* is matched by this pattern.\n\n        If normalize is True (default), the path will get normalized using normalize_path(),\n        otherwise it is assumed that it already is normalized using that function.\n        \"\"\"\n        if normalize:\n            path = normalize_path(path)\n        matches = self._match(path)\n        if matches:\n            self.match_count += 1\n        return matches\n\n    def __repr__(self):\n        return f\"{type(self)}({self.pattern})\"\n\n    def __str__(self):\n        return self.pattern_orig\n\n    def _prepare(self, pattern):\n        \"Should set the value of self.pattern\"\n        raise NotImplementedError\n\n    def _match(self, path):\n        raise NotImplementedError\n\n\nclass PathFullPattern(PatternBase):\n    \"\"\"Full match of a path.\"\"\"\n\n    PREFIX = \"pf\"\n\n    def _prepare(self, pattern):\n        self.pattern = posixpath.normpath(pattern).lstrip(\"/\")  # / at beginning is removed\n\n    def _match(self, path):\n        return path == self.pattern\n\n\n# For PathPrefixPattern, FnmatchPattern and ShellPattern, we require that the pattern either match the whole path\n# or an initial segment of the path up to but not including a path separator. To unify the two cases, we add a path\n# separator to the end of the path before matching.\n\n\nclass PathPrefixPattern(PatternBase):\n    \"\"\"Literal files or directories listed on the command line\n    for some operations (e.g. extract, but not create).\n    If a directory is specified, all paths that start with that\n    path match as well.  A trailing slash makes no difference.\n    \"\"\"\n\n    PREFIX = \"pp\"\n\n    def _prepare(self, pattern):\n        self.pattern = (posixpath.normpath(pattern).rstrip(\"/\") + \"/\").lstrip(\"/\")  # / at beginning is removed\n\n    def _match(self, path):\n        return (path + \"/\").startswith(self.pattern)\n\n\nclass FnmatchPattern(PatternBase):\n    \"\"\"Shell glob patterns to exclude.  A trailing slash means to\n    exclude the contents of a directory, but not the directory itself.\n    \"\"\"\n\n    PREFIX = \"fm\"\n\n    def _prepare(self, pattern):\n        if pattern.endswith(\"/\"):\n            pattern = posixpath.normpath(pattern).rstrip(\"/\") + \"/*/\"\n        else:\n            pattern = posixpath.normpath(pattern) + \"/*\"\n\n        self.pattern = pattern.lstrip(\"/\")  # / at beginning is removed\n\n        # fnmatch and re.match both cache compiled regular expressions.\n        # Nevertheless, this is about 10 times faster.\n        self.regex = re.compile(fnmatch.translate(self.pattern))\n\n    def _match(self, path):\n        return self.regex.match(path + \"/\") is not None\n\n\nclass ShellPattern(PatternBase):\n    \"\"\"Shell glob patterns to exclude.  A trailing slash means to\n    exclude the contents of a directory, but not the directory itself.\n    \"\"\"\n\n    PREFIX = \"sh\"\n\n    def _prepare(self, pattern):\n        if pattern.endswith(\"/\"):\n            pattern = posixpath.normpath(pattern).rstrip(\"/\") + \"/**/*/\"\n        else:\n            pattern = posixpath.normpath(pattern) + \"/**/*\"\n\n        self.pattern = pattern.lstrip(\"/\")  # / at beginning is removed\n        self.regex = re.compile(shellpattern.translate(self.pattern))\n\n    def _match(self, path):\n        return self.regex.match(path + \"/\") is not None\n\n\nclass RegexPattern(PatternBase):\n    \"\"\"Regular expression to exclude.\"\"\"\n\n    PREFIX = \"re\"\n\n    def _prepare(self, pattern):\n        self.pattern = pattern  # / at beginning is NOT removed\n        self.regex = re.compile(pattern)\n\n    def _match(self, path):\n        return self.regex.search(path) is not None\n\n\n_PATTERN_CLASSES = {FnmatchPattern, PathFullPattern, PathPrefixPattern, RegexPattern, ShellPattern}\n\n_PATTERN_CLASS_BY_PREFIX = {i.PREFIX: i for i in _PATTERN_CLASSES}\n\nCmdTuple = namedtuple(\"CmdTuple\", \"val cmd\")\n\n\nclass IECommand(Enum):\n    \"\"\"A command that an InclExcl file line can represent.\"\"\"\n\n    RootPath = 1\n    PatternStyle = 2\n    Include = 3\n    Exclude = 4\n    ExcludeNoRecurse = 5\n\n\ndef command_recurses_dir(cmd):\n    # TODO?: raise error or return None if *cmd* is RootPath or PatternStyle\n    return cmd not in [IECommand.ExcludeNoRecurse]\n\n\ndef get_pattern_class(prefix):\n    try:\n        return _PATTERN_CLASS_BY_PREFIX[prefix]\n    except KeyError:\n        raise ValueError(f\"Unknown pattern style: {prefix}\") from None\n\n\ndef parse_pattern(pattern, fallback=FnmatchPattern, recurse_dir=True):\n    \"\"\"Read pattern from string and return an instance of the appropriate implementation class.\"\"\"\n    if len(pattern) > 2 and pattern[2] == \":\" and pattern[:2].isalnum():\n        (style, pattern) = (pattern[:2], pattern[3:])\n        cls = get_pattern_class(style)\n    else:\n        cls = fallback\n    return cls(pattern, recurse_dir)\n\n\ndef parse_exclude_pattern(pattern_str, fallback=FnmatchPattern):\n    \"\"\"Read pattern from string and return an instance of the appropriate implementation class.\"\"\"\n    epattern_obj = parse_pattern(pattern_str, fallback, recurse_dir=False)\n    return CmdTuple(epattern_obj, IECommand.ExcludeNoRecurse)\n\n\ndef parse_inclexcl_command(cmd_line_str, fallback=ShellPattern):\n    \"\"\"Read a --patterns-from command from string and return a CmdTuple object.\"\"\"\n\n    cmd_prefix_map = {\n        \"-\": IECommand.Exclude,\n        \"!\": IECommand.ExcludeNoRecurse,\n        \"+\": IECommand.Include,\n        \"R\": IECommand.RootPath,\n        \"r\": IECommand.RootPath,\n        \"P\": IECommand.PatternStyle,\n        \"p\": IECommand.PatternStyle,\n    }\n    if not cmd_line_str:\n        raise ArgumentTypeError(\"A pattern/command must not be empty.\")\n\n    cmd = cmd_prefix_map.get(cmd_line_str[0])\n    if cmd is None:\n        raise ArgumentTypeError(\"A pattern/command must start with any of: %s\" % \", \".join(cmd_prefix_map))\n\n    # remaining text on command-line following the command character\n    remainder_str = cmd_line_str[1:].lstrip()\n    if not remainder_str:\n        raise ArgumentTypeError(\"A pattern/command must have a value part.\")\n\n    if cmd is IECommand.RootPath:\n        # TODO: validate string?\n        val = remainder_str\n    elif cmd is IECommand.PatternStyle:\n        # then remainder_str is something like 're' or 'sh'\n        try:\n            val = get_pattern_class(remainder_str)\n        except ValueError:\n            raise ArgumentTypeError(f\"Invalid pattern style: {remainder_str}\")\n    else:\n        # determine recurse_dir based on command type\n        recurse_dir = command_recurses_dir(cmd)\n        val = parse_pattern(remainder_str, fallback, recurse_dir)\n\n    return CmdTuple(val, cmd)\n\n\ndef get_regex_from_pattern(pattern: str) -> str:\n    \"\"\"\n    return a regular expression string corresponding to the given pattern string.\n\n    the allowed pattern types are similar to the ones implemented by PatternBase subclasses,\n    but here we rather do generic string matching, not specialised filesystem paths matching.\n    \"\"\"\n    if len(pattern) > 2 and pattern[2] == \":\" and pattern[:2] in {\"sh\", \"re\", \"id\"}:\n        (style, pattern) = (pattern[:2], pattern[3:])\n    else:\n        (style, pattern) = (\"id\", pattern)  # \"identical\" match is the default\n    if style == \"sh\":\n        # (?ms) (meaning re.MULTILINE and re.DOTALL) are not desired here.\n        regex = shellpattern.translate(pattern, match_end=\"\").removeprefix(\"(?ms)\")\n    elif style == \"re\":\n        regex = pattern\n    elif style == \"id\":\n        regex = re.escape(pattern)\n    else:\n        raise NotImplementedError\n    return regex\n"
  },
  {
    "path": "src/borg/platform/__init__.py",
    "content": "\"\"\"\nPlatform-specific APIs.\n\nPublic APIs are documented in platform.base.\n\"\"\"\n\nfrom types import ModuleType\n\nfrom ..platformflags import is_win32, is_linux, is_freebsd, is_netbsd, is_darwin, is_cygwin, is_haiku\n\nfrom .base import ENOATTR\nfrom .base import SaveFile, sync_dir, fdatasync, safe_fadvise\nfrom .base import get_process_id, fqdn, hostname, hostid, swidth\n\n# work around pyinstaller \"forgetting\" to include the xattr module\nfrom . import xattr  # noqa: F401\n\nplatform_ug: ModuleType | None = None  # make mypy happy\n\nif is_linux:  # pragma: linux only\n    from .linux import listxattr, getxattr, setxattr\n    from .linux import acl_get, acl_set\n    from .linux import set_flags, get_flags\n    from .linux import SyncFile\n    from .posix import process_alive, local_pid_alive\n    from .posix import get_errno\n    from .posix import getosusername\n    from . import posix_ug as platform_ug\nelif is_freebsd:  # pragma: freebsd only\n    from .freebsd import listxattr, getxattr, setxattr\n    from .freebsd import acl_get, acl_set\n    from .freebsd import set_flags\n    from .base import get_flags\n    from .base import SyncFile\n    from .posix import process_alive, local_pid_alive\n    from .posix import get_errno\n    from .posix import getosusername\n    from . import posix_ug as platform_ug\nelif is_netbsd:  # pragma: netbsd only\n    from .netbsd import listxattr, getxattr, setxattr\n    from .base import acl_get, acl_set\n    from .base import set_flags, get_flags\n    from .base import SyncFile\n    from .posix import process_alive, local_pid_alive\n    from .posix import get_errno\n    from .posix import getosusername\n    from . import posix_ug as platform_ug\nelif is_darwin:  # pragma: darwin only\n    from .darwin import listxattr, getxattr, setxattr\n    from .darwin import acl_get, acl_set\n    from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns\n    from .darwin import set_flags\n    from .darwin import fdatasync, sync_dir  # type: ignore[no-redef]\n    from .base import get_flags\n    from .base import SyncFile\n    from .posix import process_alive, local_pid_alive\n    from .posix import get_errno\n    from .posix import getosusername\n    from . import posix_ug as platform_ug\nelif not is_win32:  # pragma: posix only\n    # Generic code for all other POSIX OSes\n    from .base import listxattr, getxattr, setxattr\n    from .base import acl_get, acl_set\n    from .base import set_flags, get_flags\n    from .base import SyncFile\n    from .posix import process_alive, local_pid_alive\n    from .posix import get_errno\n    from .posix import getosusername\n    from . import posix_ug as platform_ug\nelse:  # pragma: win32 only\n    # Win32-specific stuff\n    from .base import listxattr, getxattr, setxattr\n    from .base import acl_get, acl_set\n    from .base import set_flags, get_flags\n    from .windows import SyncFile\n    from .windows import process_alive, local_pid_alive\n    from .windows import getosusername\n    from . import windows_ug as platform_ug\n\n\ndef get_birthtime_ns(st, path, fd=None):\n    if hasattr(st, \"st_birthtime_ns\"):\n        # Added in Python 3.12, but not always available.\n        return st.st_birthtime_ns\n    elif is_darwin and is_darwin_feature_64_bit_inode:\n        return _get_birthtime_ns(fd or path, follow_symlinks=False)\n    elif hasattr(st, \"st_birthtime\"):\n        return int(st.st_birthtime * 10**9)\n    else:\n        return None\n\n\n# have some wrapper functions, so we can monkeypatch the functions in platform_ug.\n# for normal usage from outside the platform package, always import these:\ndef uid2user(uid, default=None):\n    return platform_ug._uid2user(uid, default)\n\n\ndef gid2group(gid, default=None):\n    return platform_ug._gid2group(gid, default)\n\n\ndef user2uid(user, default=None):\n    return platform_ug._user2uid(user, default)\n\n\ndef group2gid(group, default=None):\n    return platform_ug._group2gid(group, default)\n"
  },
  {
    "path": "src/borg/platform/base.py",
    "content": "import errno\nimport io\nimport os\nimport socket\nimport unicodedata\nimport uuid\nfrom pathlib import Path\n\nfrom ..helpers import safe_unlink\nfrom ..platformflags import is_win32\n\n\"\"\"\nplatform base module\n====================\n\nContains platform API implementations based on what Python itself provides. More specific\nAPIs are stubs in this module.\n\nWhen functions in this module use platform APIs themselves, they access the public\nplatform API; in this way, platform APIs provided by the platform-specific support module\nare correctly composed into the base functionality.\n\"\"\"\n\n\nfdatasync = getattr(os, \"fdatasync\", os.fsync)\nhas_posix_fadvise = hasattr(os, \"posix_fadvise\")\n\ntry:\n    ENOATTR = errno.ENOATTR  # type: ignore[attr-defined]\nexcept AttributeError:\n    # on some platforms, ENOATTR is missing, use ENODATA there\n    ENOATTR = errno.ENODATA  # type: ignore[attr-defined]\n\n\ndef listxattr(path, *, follow_symlinks=False):\n    \"\"\"\n    Return xattr names of a file (list of bytes objects).\n\n    *path* can either be a path (bytes) or an open file descriptor (int).\n    *follow_symlinks* indicates whether symlinks should be followed\n    and only applies when *path* is not an open file descriptor.\n    \"\"\"\n    return []\n\n\ndef getxattr(path, name, *, follow_symlinks=False):\n    \"\"\"\n    Read xattr and return its value (as bytes).\n\n    *path* can either be a path (bytes) or an open file descriptor (int).\n    *name* is the name of the xattr to read (bytes).\n    *follow_symlinks* indicates whether symlinks should be followed\n    and only applies when *path* is not an open file descriptor.\n    \"\"\"\n    # As this base dummy implementation returns [] from listxattr,\n    # it must raise here for any given name:\n    raise OSError(ENOATTR, os.strerror(ENOATTR), path)\n\n\ndef setxattr(path, name, value, *, follow_symlinks=False):\n    \"\"\"\n    Write an xattr on *path*.\n\n    *path* can either be a path (bytes) or an open file descriptor (int).\n    *name* is the name of the xattr to write (bytes).\n    *value* is the value to write (bytes).\n    *follow_symlinks* indicates whether symlinks should be followed\n    and only applies when *path* is not an open file descriptor.\n    \"\"\"\n\n\ndef acl_get(path, item, st, numeric_ids=False, fd=None):\n    \"\"\"\n    Save ACL entries.\n\n    If `numeric_ids` is True, the user/group field is not preserved; only uid/gid are stored.\n    \"\"\"\n\n\ndef acl_set(path, item, numeric_ids=False, fd=None):\n    \"\"\"\n    Restore ACL entries.\n\n    If `numeric_ids` is True, the stored uid/gid is used instead of the user/group names.\n    \"\"\"\n\n\ntry:\n    from os import lchflags  # type: ignore[attr-defined]\n\n    def set_flags(path, bsd_flags, fd=None):\n        lchflags(path, bsd_flags)\n\nexcept ImportError:\n\n    def set_flags(path, bsd_flags, fd=None):\n        pass\n\n\ndef get_flags(path, st, fd=None):\n    \"\"\"Return BSD-style file flags for path or stat without following symlinks.\"\"\"\n    return getattr(st, \"st_flags\", 0)\n\n\ndef sync_dir(path):\n    if is_win32:\n        # Opening directories is not supported on Windows.\n        # TODO: Do we need to handle this in some other way?\n        return\n    fd = os.open(str(path), os.O_RDONLY)\n    try:\n        os.fsync(fd)\n    except OSError as os_error:\n        # Some network filesystems don't support this and fail with EINVAL.\n        # Other error codes (e.g. EIO) shouldn't be silenced.\n        if os_error.errno != errno.EINVAL:\n            raise\n    finally:\n        os.close(fd)\n\n\ndef safe_fadvise(fd, offset, len, advice):\n    if has_posix_fadvise:\n        advice = getattr(os, \"POSIX_FADV_\" + advice)\n        try:\n            # UNIX only; and, in the case of block sizes that are not a multiple of the system's page size,\n            # this is better used with a bug-fixed Linux kernel > 4.6.0; see Borg issue #907.\n            os.posix_fadvise(fd, offset, len, advice)\n        except OSError:\n            # Usually, posix_fadvise does not fail for us, but there seem to be failures\n            # when running Borg under Docker on ARM, likely due to a bug outside of Borg.\n            # Also, there is a Python wrapper bug always giving errno = 0.\n            # https://github.com/borgbackup/borg/issues/2095\n            # As this call is not critical for correct function (it just helps optimize cache usage),\n            # we ignore these errors.\n            pass\n\n\nclass SyncFile:\n    \"\"\"\n    A file class that is supposed to enable write ordering (one way or another) and data durability after close().\n\n    The degree to which either is possible varies with operating system, filesystem, and hardware.\n\n    This fallback implements a naive and slow way of doing this. On some operating systems it can't actually\n    guarantee any of the above, since fsync() doesn't guarantee it. Furthermore it may not be possible at all\n    to satisfy the above guarantees on some hardware or operating systems. In these cases we hope that the thorough\n    checksumming implemented catches any corrupted data due to misordered, delayed or partial writes.\n\n    Note that POSIX doesn't specify anything about power failures (or similar failures). A system that\n    routinely loses files or corrupts files on power loss is POSIX-compliant.\n\n    Calling SyncFile(path) for an existing path will raise FileExistsError. See the comment in __init__.\n\n    See platform/windows.pyx for the Windows implementation using CreateFile with FILE_FLAG_WRITE_THROUGH.\n    \"\"\"\n\n    def __init__(self, path, *, fd=None, binary=False):\n        \"\"\"\n        Open a SyncFile.\n\n        :param path: full path/filename\n        :param fd: additionally to path, it is possible to give an already open OS-level fd\n               that corresponds to path (like from os.open(path, ...) or os.mkstemp(...))\n        :param binary: whether to open in binary mode, default is False.\n        \"\"\"\n        mode = \"x+b\" if binary else \"x+\"  # x -> raise FileExists exception in open() if file exists already\n        self.path = path\n        if fd is None:\n            self.f = open(str(path), mode=mode)  # Python file object\n        else:\n            self.f = os.fdopen(fd, mode=mode)\n        self.fd = self.f.fileno()  # OS-level fd\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def write(self, data):\n        self.f.write(data)\n\n    def read(self, *args, **kwargs):\n        return self.f.read(*args, **kwargs)\n\n    def seek(self, offset, whence=io.SEEK_SET):\n        return self.f.seek(offset, whence)\n\n    def tell(self):\n        return self.f.tell()\n\n    def sync(self):\n        \"\"\"\n        Synchronize file contents. Everything written prior to sync() must become durable before anything written\n        after sync().\n        \"\"\"\n        from .. import platform\n\n        self.f.flush()\n        platform.fdatasync(self.fd)\n        # tell the OS that it does not need to cache what we just wrote,\n        # avoids spoiling the cache for the OS and other processes.\n        safe_fadvise(self.fd, 0, 0, \"DONTNEED\")\n\n    def close(self):\n        \"\"\"sync() and close.\"\"\"\n        if self.f.closed:\n            return\n        from .. import platform\n\n        dirname = None\n        try:\n            dirname = Path(self.path).parent\n            self.sync()\n        finally:\n            self.f.close()\n            if dirname:\n                platform.sync_dir(dirname)\n\n\nclass SaveFile:\n    \"\"\"\n    Update file contents atomically.\n\n    Must be used as a context manager (defining the scope of the transaction).\n\n    On a journaling filesystem the file contents are always updated\n    atomically and won't become corrupted, even on power failures or\n    crashes (for caveats see SyncFile).\n\n    SaveFile can safely be used in parallel (e.g., by multiple processes) to write\n    to the same target path. Whatever writer finishes last (executes the os.replace\n    last) \"wins\" and has successfully written its content to the target path.\n    Internally used temporary files are created in the target directory and are\n    named <BASENAME>-<RANDOMCHARS>.tmp and cleaned up in normal and error conditions.\n    \"\"\"\n\n    def __init__(self, path, binary=False):\n        self.binary = binary\n        self.path = path\n        path_obj = Path(path)\n        self.dir = str(path_obj.parent)\n        self.tmp_prefix = path_obj.name + \"-\"\n        self.tmp_fd = None  # OS-level fd\n        self.tmp_fname = None  # full path/filename corresponding to self.tmp_fd\n        self.f = None  # Python file-like SyncFile\n\n    def __enter__(self):\n        from .. import platform\n        from ..helpers.fs import mkstemp_mode\n\n        self.tmp_fd, self.tmp_fname = mkstemp_mode(prefix=self.tmp_prefix, suffix=\".tmp\", dir=self.dir, mode=0o666)\n        self.f = platform.SyncFile(self.tmp_fname, fd=self.tmp_fd, binary=self.binary)\n        return self.f\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        from .. import platform\n\n        self.f.close()  # this indirectly also closes self.tmp_fd\n        self.tmp_fd = None\n        if exc_type is not None:\n            safe_unlink(self.tmp_fname)  # with-body has failed, clean up tmp file\n            return  # continue processing the exception normally\n\n        try:\n            os.replace(self.tmp_fname, self.path)  # POSIX: atomic rename\n        except OSError:\n            safe_unlink(self.tmp_fname)  # rename has failed, clean up tmp file\n            raise\n        finally:\n            platform.sync_dir(self.dir)\n\n\ndef swidth(s):\n    \"\"\"terminal output width of string <s>\n\n    For western scripts, this is just len(s), but for cjk glyphs, 2 cells are used.\n    \"\"\"\n    width = 0\n    for char in s:\n        # Get the East Asian Width property\n        ea_width = unicodedata.east_asian_width(char)\n\n        # Wide (W) and Fullwidth (F) characters take 2 cells\n        if ea_width in (\"W\", \"F\"):\n            width += 2\n        # Not a zero-width characters (combining marks, format characters)\n        elif unicodedata.category(char) not in (\"Mn\", \"Me\", \"Cf\"):\n            # Normal characters take 1 cell\n            width += 1\n\n    return width\n\n\n# patched socket.getfqdn() - see https://bugs.python.org/issue5004\ndef getfqdn(name=\"\"):\n    \"\"\"Get fully qualified domain name from name.\n\n    An empty argument is interpreted as meaning the local host.\n    \"\"\"\n    name = name.strip()\n    if not name or name == \"0.0.0.0\":  # nosec B104:hardcoded_bind_all_interfaces\n        name = socket.gethostname()\n    try:\n        addrs = socket.getaddrinfo(name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)\n    except OSError:\n        pass\n    else:\n        for addr in addrs:\n            if addr[3]:\n                name = addr[3]\n                break\n    return name\n\n\n# for performance reasons, only determine hostname / fqdn / hostid once.\n# XXX this sometimes requires live internet access for issuing a DNS query in the background.\nhostname = socket.gethostname()\nfqdn = getfqdn(hostname)\n# some people put the fqdn into /etc/hostname (which is wrong, should be the short hostname)\n# fix this (do the same as \"hostname --short\" cli command does internally):\nhostname = hostname.split(\".\")[0]\n\n# uuid.getnode() is problematic in some environments (e.g. OpenVZ, see #3968) where the virtual MAC address\n# is all-zero. uuid.getnode falls back to returning a random value in that case, which is not what we want.\n# thus, we offer BORG_HOST_ID where a user can set an own, unique id for each of his hosts.\nhostid = os.environ.get(\"BORG_HOST_ID\")\nif not hostid:\n    hostid = f\"{fqdn}@{uuid.getnode()}\"\n\n\ndef get_process_id():\n    \"\"\"\n    Return identification tuple (hostname, pid, thread_id) for 'us'.\n    This always returns the current pid, which might be different from before, e.g. if daemonize() was used.\n\n    Note: Currently thread_id is *always* zero.\n    \"\"\"\n    thread_id = 0\n    pid = os.getpid()\n    return hostid, pid, thread_id\n"
  },
  {
    "path": "src/borg/platform/darwin.pyx",
    "content": "import os\n\nfrom libc.stdint cimport uint32_t\nfrom libc cimport errno\nfrom posix.time cimport timespec\n\nfrom . import posix_ug\nfrom ..helpers import safe_decode, safe_encode\nfrom .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0\n\n\n\ncdef extern from *:\n    \"\"\"\n    #ifdef _DARWIN_FEATURE_64_BIT_INODE\n    #define DARWIN_FEATURE_64_BIT_INODE_DEFINED 1\n    #else\n    #define DARWIN_FEATURE_64_BIT_INODE_DEFINED 0\n    #endif\n    \"\"\"\n    int DARWIN_FEATURE_64_BIT_INODE_DEFINED\n\nis_darwin_feature_64_bit_inode = DARWIN_FEATURE_64_BIT_INODE_DEFINED != 0\n\ncdef extern from \"sys/xattr.h\":\n    ssize_t c_listxattr \"listxattr\" (const char *path, char *list, size_t size, int flags)\n    ssize_t c_flistxattr \"flistxattr\" (int filedes, char *list, size_t size, int flags)\n\n    ssize_t c_getxattr \"getxattr\" (const char *path, const char *name, void *value, size_t size, uint32_t pos, int flags)\n    ssize_t c_fgetxattr \"fgetxattr\" (int filedes, const char *name, void *value, size_t size, uint32_t pos, int flags)\n\n    int c_setxattr \"setxattr\" (const char *path, const char *name, const void *value, size_t size, uint32_t pos, int flags)\n    int c_fsetxattr \"fsetxattr\" (int filedes, const char *name, const void *value, size_t size, uint32_t pos, int flags)\n\n    int XATTR_NOFOLLOW\n\ncdef int XATTR_NOFLAGS = 0x0000\n\ncdef extern from \"sys/acl.h\":\n    ctypedef struct _acl_t:\n        pass\n    ctypedef _acl_t *acl_t\n\n    int acl_free(void *obj)\n    acl_t acl_get_link_np(const char *path, int type)\n    acl_t acl_get_fd_np(int fd, int type)\n    int acl_set_link_np(const char *path, int type, acl_t acl)\n    int acl_set_fd_np(int fd, acl_t acl, int type)\n    acl_t acl_from_text(const char *buf)\n    char *acl_to_text(acl_t acl, ssize_t *len_p)\n    int ACL_TYPE_EXTENDED\n\ncdef extern from \"sys/stat.h\":\n    cdef struct stat:\n        timespec st_birthtimespec\n\n    int c_stat \"stat\" (const char *path, stat *buf)\n    int c_lstat \"lstat\" (const char *path, stat *buf)\n    int c_fstat \"fstat\" (int filedes, stat *buf)\n\n\ndef listxattr(path, *, follow_symlinks=False):\n    def func(path, buf, size):\n        if isinstance(path, int):\n            return c_flistxattr(path, <char *> buf, size, XATTR_NOFLAGS)\n        else:\n            if follow_symlinks:\n                return c_listxattr(path, <char *> buf, size, XATTR_NOFLAGS)\n            else:\n                return c_listxattr(path, <char *> buf, size, XATTR_NOFOLLOW)\n\n    n, buf = _listxattr_inner(func, path)\n    return [name for name in split_string0(buf[:n]) if name]\n\n\ndef getxattr(path, name, *, follow_symlinks=False):\n    def func(path, name, buf, size):\n        if isinstance(path, int):\n            return c_fgetxattr(path, name, <char *> buf, size, 0, XATTR_NOFLAGS)\n        else:\n            if follow_symlinks:\n                return c_getxattr(path, name, <char *> buf, size, 0, XATTR_NOFLAGS)\n            else:\n                return c_getxattr(path, name, <char *> buf, size, 0, XATTR_NOFOLLOW)\n\n    n, buf = _getxattr_inner(func, path, name)\n    return bytes(buf[:n])\n\n\ndef setxattr(path, name, value, *, follow_symlinks=False):\n    def func(path, name, value, size):\n        if isinstance(path, int):\n            return c_fsetxattr(path, name, <char *> value, size, 0, XATTR_NOFLAGS)\n        else:\n            if follow_symlinks:\n                return c_setxattr(path, name, <char *> value, size, 0, XATTR_NOFLAGS)\n            else:\n                return c_setxattr(path, name, <char *> value, size, 0, XATTR_NOFOLLOW)\n\n    _setxattr_inner(func, path, name, value)\n\n\ndef _remove_numeric_id_if_possible(acl):\n    \"\"\"Replace the user/group field with the local uid/gid, if possible.\"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in safe_decode(acl).split('\\n'):\n        if entry:\n            fields = entry.split(':')\n            if fields[0] == 'user':\n                if posix_ug._user2uid(fields[2]) is not None:\n                    fields[1] = fields[3] = ''\n            elif fields[0] == 'group':\n                if posix_ug._group2gid(fields[2]) is not None:\n                    fields[1] = fields[3] = ''\n            entries.append(':'.join(fields))\n    return safe_encode('\\n'.join(entries))\n\n\ndef _remove_non_numeric_identifier(acl):\n    \"\"\"Remove user and group names from the acl\n    \"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in safe_decode(acl).split('\\n'):\n        if entry:\n            fields = entry.split(':')\n            if fields[0] in ('user', 'group'):\n                fields[2] = ''\n                entries.append(':'.join(fields))\n            else:\n                entries.append(entry)\n    return safe_encode('\\n'.join(entries))\n\n\ndef acl_get(path, item, st, numeric_ids=False, fd=None):\n    cdef acl_t acl = NULL\n    cdef char *text = NULL\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    try:\n        if fd is not None:\n            acl = acl_get_fd_np(fd, ACL_TYPE_EXTENDED)\n        else:\n            acl = acl_get_link_np(path, ACL_TYPE_EXTENDED)\n        if acl == NULL:\n            if errno.errno == errno.ENOENT:\n                # macOS weirdness: if a file has no ACLs, it sets errno to ENOENT. :-(\n                return\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        text = acl_to_text(acl, NULL)\n        if text == NULL:\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        if numeric_ids:\n            item['acl_extended'] = _remove_non_numeric_identifier(text)\n        else:\n            item['acl_extended'] = text\n    finally:\n        acl_free(text)\n        acl_free(acl)\n\n\ndef acl_set(path, item, numeric_ids=False, fd=None):\n    cdef acl_t acl = NULL\n    acl_text = item.get('acl_extended')\n    if acl_text is not None:\n        try:\n            if isinstance(path, str):\n                path = os.fsencode(path)\n            if numeric_ids:\n                acl = acl_from_text(acl_text)\n            else:\n                acl = acl_from_text(<bytes>_remove_numeric_id_if_possible(acl_text))\n            if acl == NULL:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            if fd is not None:\n                if acl_set_fd_np(fd, acl, ACL_TYPE_EXTENDED) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            else:\n                if acl_set_link_np(path, ACL_TYPE_EXTENDED, acl) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        finally:\n            acl_free(acl)\n\n\ndef _get_birthtime_ns(path, follow_symlinks=False):\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    cdef stat stat_info\n    cdef int result\n    if isinstance(path, int):\n        result = c_fstat(path, &stat_info)\n    else:\n        if follow_symlinks:\n            result = c_stat(path, &stat_info)\n        else:\n            result = c_lstat(path, &stat_info)\n    if result != 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    return stat_info.st_birthtimespec.tv_sec * 1_000_000_000 + stat_info.st_birthtimespec.tv_nsec\n\n\n# macOS flags handling: only modify flags documented as settable; preserve all others, see #9090.\n# The man page states UF_COMPRESSED and SF_DATALESS are internal flags and must not be modified\n# from user space. We therefore only modify flags that are documented to be settable by owner or\n# super-user and preserve everything else (including unknown or future flags).\n\ncdef extern from \"sys/stat.h\":\n    int chflags(const char *path, uint32_t flags)\n    int lchflags(const char *path, uint32_t flags)\n    int fchflags(int fd, uint32_t flags)\n\n\n# Known-good settable flags from macOS chflags(2). We intentionally do NOT include\n# internal flags like UF_COMPRESSED and SF_DATALESS. Resolved once at import time.\n# getattr(..., 0) keeps this importable on non-Darwin platforms or Python versions\n# missing some constants.\nimport stat as stat_mod\n\nSETTABLE_FLAG_NAMES = (\n    # Owner-settable (UF_*)\n    'UF_NODUMP',\n    'UF_IMMUTABLE',\n    'UF_APPEND',\n    'UF_OPAQUE',\n    'UF_NOUNLINK',\n    'UF_HIDDEN',\n    # Super-user-settable (SF_*)\n    'SF_ARCHIVED',\n    'SF_IMMUTABLE',\n    'SF_APPEND',\n    # SF_NOUNLINK exists on some BSDs; include defensively\n    'SF_NOUNLINK',\n)\n\ncdef uint32_t SETTABLE_FLAGS_MASK = 0\nfor _name in SETTABLE_FLAG_NAMES:\n    SETTABLE_FLAGS_MASK |= <uint32_t> getattr(stat_mod, _name, 0)\n\n\ndef set_flags(path, bsd_flags, fd=None):\n    \"\"\"Set BSD-style flags on macOS, preserving system-managed read-only flags.\"\"\"\n    # Determine current flags.\n    try:\n        if fd is not None:\n            st = os.fstat(fd)\n        else:\n            st = os.lstat(path)\n        current = st.st_flags\n    except (OSError, AttributeError):\n        # We can't determine the current flags, so better give up than corrupting anything.\n        return\n\n    new_flags = (current & ~SETTABLE_FLAGS_MASK) | (bsd_flags & SETTABLE_FLAGS_MASK)\n\n    # Apply flags.\n    cdef uint32_t c_flags = <uint32_t> new_flags\n    if fd is not None:\n        if fchflags(fd, c_flags) == -1:\n            raise OSError(errno.errno, os.strerror(errno.errno), path)\n    else:\n        path_bytes = os.fsencode(path)\n        if lchflags(path_bytes, c_flags) == -1:\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))\n\n\nimport errno as errno_mod\nimport fcntl as fcntl_mod\n\n\ndef fdatasync(fd):\n    \"\"\"macOS fdatasync using F_FULLFSYNC for true data durability.\n\n    On macOS, os.fsync() only flushes to the drive's write cache.\n    fcntl F_FULLFSYNC flushes to persistent storage.\n    Falls back to os.fsync() if F_FULLFSYNC is not supported.\n    \"\"\"\n    try:\n        fcntl_mod.fcntl(fd, fcntl_mod.F_FULLFSYNC)\n    except OSError:\n        # F_FULLFSYNC not supported (e.g. network filesystem), fall back\n        os.fsync(fd)\n\n\ndef sync_dir(path):\n    \"\"\"Sync a directory to persistent storage on macOS using F_FULLFSYNC.\"\"\"\n    fd = os.open(str(path), os.O_RDONLY)\n    try:\n        fdatasync(fd)\n    except OSError as os_error:\n        if os_error.errno != errno_mod.EINVAL:\n            raise\n    finally:\n        os.close(fd)\n"
  },
  {
    "path": "src/borg/platform/freebsd.pyx",
    "content": "import os\nimport stat\n\nfrom libc cimport errno\n\nfrom .posix import posix_acl_use_stored_uid_gid\nfrom ..helpers import safe_encode, safe_decode\nfrom .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lstring\n\n\n\ncdef extern from \"sys/extattr.h\":\n    ssize_t c_extattr_list_file \"extattr_list_file\" (const char *path, int attrnamespace, void *data, size_t nbytes)\n    ssize_t c_extattr_list_link \"extattr_list_link\" (const char *path, int attrnamespace, void *data, size_t nbytes)\n    ssize_t c_extattr_list_fd \"extattr_list_fd\" (int fd, int attrnamespace, void *data, size_t nbytes)\n\n    ssize_t c_extattr_get_file \"extattr_get_file\" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n    ssize_t c_extattr_get_link \"extattr_get_link\" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n    ssize_t c_extattr_get_fd \"extattr_get_fd\" (int fd, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n\n    int c_extattr_set_file \"extattr_set_file\" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n    int c_extattr_set_link \"extattr_set_link\" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n    int c_extattr_set_fd \"extattr_set_fd\" (int fd, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n\n    int EXTATTR_NAMESPACE_USER\n\ncdef extern from \"sys/types.h\":\n    int ACL_TYPE_ACCESS\n    int ACL_TYPE_DEFAULT\n    int ACL_TYPE_NFS4\n\ncdef extern from \"sys/acl.h\":\n    ctypedef struct _acl_t:\n        pass\n    ctypedef _acl_t *acl_t\n\n    int acl_free(void *obj)\n    acl_t acl_get_link_np(const char *path, int type)\n    acl_t acl_get_fd_np(int fd, int type)\n    int acl_set_link_np(const char *path, int type, acl_t acl)\n    int acl_set_fd_np(int fd, acl_t acl, int type)\n    acl_t acl_from_text(const char *buf)\n    char *acl_to_text_np(acl_t acl, ssize_t *len, int flags)\n    int ACL_TEXT_NUMERIC_IDS\n    int ACL_TEXT_APPEND_ID\n    int acl_extended_link_np(const char * path)  # check also: acl_is_trivial_np\n\ncdef extern from \"unistd.h\":\n    long lpathconf(const char *path, int name)\n    int _PC_ACL_NFS4\n    int _PC_ACL_EXTENDED\n\n\n# On FreeBSD, Borg currently only deals with the USER namespace, as it is unclear\n# whether (and, if so, how exactly) it should deal with the SYSTEM namespace.\nNS_ID_MAP = {b\"user\": EXTATTR_NAMESPACE_USER, }\n\n\ndef split_ns(ns_name, default_ns):\n    # Split ns_name (which is in the form b\"namespace.name\") into namespace and name.\n    # If there is no namespace given in ns_name, default to default_ns.\n    # Note:\n    # Borg < 1.1.10 on FreeBSD did not prefix the namespace to the names; see #3952.\n    # We also need to deal with \"unexpected\" namespaces here — they could come\n    # from Borg archives made on other operating systems.\n    ns_name_tuple = ns_name.split(b\".\", 1)\n    if len(ns_name_tuple) == 2:\n        # We have a namespace prefix in the given name.\n        ns, name = ns_name_tuple\n    else:\n        # No namespace given in ns_name (no dot found); maybe data from an old Borg archive.\n        ns, name = default_ns, ns_name\n    return ns, name\n\n\ndef listxattr(path, *, follow_symlinks=False):\n    def func(path, buf, size):\n        if isinstance(path, int):\n            return c_extattr_list_fd(path, ns_id, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_list_file(path, ns_id, <char *> buf, size)\n            else:\n                return c_extattr_list_link(path, ns_id, <char *> buf, size)\n\n    ns = b\"user\"\n    ns_id = NS_ID_MAP[ns]\n    n, buf = _listxattr_inner(func, path)\n    return [ns + b\".\" + name for name in split_lstring(buf[:n]) if name]\n\n\ndef getxattr(path, name, *, follow_symlinks=False):\n    def func(path, name, buf, size):\n        if isinstance(path, int):\n            return c_extattr_get_fd(path, ns_id, name, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_get_file(path, ns_id, name, <char *> buf, size)\n            else:\n                return c_extattr_get_link(path, ns_id, name, <char *> buf, size)\n\n    ns, name = split_ns(name, b\"user\")\n    ns_id = NS_ID_MAP[ns]  # this will raise a KeyError it the namespace is unsupported\n    n, buf = _getxattr_inner(func, path, name)\n    return bytes(buf[:n])\n\n\ndef setxattr(path, name, value, *, follow_symlinks=False):\n    def func(path, name, value, size):\n        if isinstance(path, int):\n            return c_extattr_set_fd(path, ns_id, name, <char *> value, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_set_file(path, ns_id, name, <char *> value, size)\n            else:\n                return c_extattr_set_link(path, ns_id, name, <char *> value, size)\n\n    ns, name = split_ns(name, b\"user\")\n    try:\n        ns_id = NS_ID_MAP[ns]  # this will raise a KeyError it the namespace is unsupported\n    except KeyError:\n        pass\n    else:\n        _setxattr_inner(func, path, name, value)\n\n\ncdef _get_acl(p, type, item, attribute, flags, fd=None):\n    cdef acl_t acl\n    cdef char *text\n    if fd is not None:\n        acl = acl_get_fd_np(fd, type)\n    else:\n        acl = acl_get_link_np(p, type)\n    if acl == NULL:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(p))\n    text = acl_to_text_np(acl, NULL, flags)\n    if text == NULL:\n        acl_free(acl)\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(p))\n    item[attribute] = text\n    acl_free(text)\n    acl_free(acl)\n\ndef acl_get(path, item, st, numeric_ids=False, fd=None):\n    \"\"\"Saves ACL Entries\n\n    If `numeric_ids` is True the user/group field is not preserved only uid/gid\n    \"\"\"\n    cdef int flags = ACL_TEXT_APPEND_ID\n    # Note: likely this could be faster if we always used ACL_TEXT_NUMERIC_IDS,\n    # and then used uid2user() and gid2group() to translate the numeric ids to names\n    # inside borg (borg has a LRUcache for these lookups).\n    # See how the Linux implementation does it.\n    flags |= ACL_TEXT_NUMERIC_IDS if numeric_ids else 0\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    ret = acl_extended_link_np(path)\n    if ret < 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    if ret == 0:\n        # there is no ACL defining permissions other than those defined by the traditional file permission bits.\n        return\n    ret = lpathconf(path, _PC_ACL_NFS4)\n    if ret < 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    nfs4_acl = ret == 1\n    if nfs4_acl:\n        _get_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', flags, fd=fd)\n    else:\n        _get_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', flags, fd=fd)\n        if stat.S_ISDIR(st.st_mode):\n            _get_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', flags, fd=fd)\n\n\ncdef _set_acl(path, type, item, attribute, numeric_ids=False, fd=None):\n    cdef acl_t acl = NULL\n    text = item.get(attribute)\n    if text:\n        if numeric_ids:\n            if type == ACL_TYPE_NFS4:\n                text = _nfs4_use_stored_uid_gid(text)\n            elif type in (ACL_TYPE_ACCESS, ACL_TYPE_DEFAULT):\n                text = posix_acl_use_stored_uid_gid(text)\n        acl = acl_from_text(<bytes>text)\n        if acl == NULL:\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        try:\n            if fd is not None:\n                if acl_set_fd_np(fd, acl, type) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            else:\n                if acl_set_link_np(path, type, acl) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        finally:\n            acl_free(acl)\n\n\ncdef _nfs4_use_stored_uid_gid(acl):\n    \"\"\"Replace the user/group field with the stored uid/gid\n    \"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in safe_decode(acl).split('\\n'):\n        if entry:\n            if entry.startswith('user:') or entry.startswith('group:'):\n                fields = entry.split(':')\n                entries.append(':'.join([fields[0], fields[5]] + fields[2:-1]))\n            else:\n                entries.append(entry)\n    return safe_encode('\\n'.join(entries))\n\n\ndef acl_set(path, item, numeric_ids=False, fd=None):\n    \"\"\"Restore ACL Entries\n\n    If `numeric_ids` is True the stored uid/gid is used instead\n    of the user/group names\n    \"\"\"\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    ret = lpathconf(path, _PC_ACL_NFS4)\n    if ret < 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    if ret == 1:\n        _set_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', numeric_ids, fd=fd)\n    ret = lpathconf(path, _PC_ACL_EXTENDED)\n    if ret < 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    if ret == 1:\n        _set_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', numeric_ids, fd=fd)\n        _set_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', numeric_ids, fd=fd)\n\n\ncdef extern from \"sys/stat.h\":\n    int chflags(const char *path, unsigned long flags)\n    int lchflags(const char *path, unsigned long flags)\n    int fchflags(int fd, unsigned long flags)\n\n# ----------------------------\n# BSD file flags (FreeBSD)\n# ----------------------------\n# Only influence flags that are known to be settable and leave system-managed/read-only flags untouched.\n# We express the mask in terms of names to avoid hard failures if a constant does\n# not exist on a given FreeBSD version; missing names simply contribute 0.\nimport stat as stat_mod\n\nSETTABLE_FLAG_NAMES = (\n    # Owner-settable (UF_*)\n    'UF_NODUMP',\n    'UF_IMMUTABLE',\n    'UF_APPEND',\n    'UF_OPAQUE',\n    'UF_NOUNLINK',\n    'UF_HIDDEN',\n    # Super-user-only (SF_*)\n    'SF_ARCHIVED',\n    'SF_IMMUTABLE',\n    'SF_APPEND',\n    'SF_NOUNLINK',\n)\n\ncdef unsigned long SETTABLE_FLAGS_MASK = 0\nfor _name in SETTABLE_FLAG_NAMES:\n    # getattr(..., 0) keeps this importable when flags are missing on some FreeBSD versions\n    SETTABLE_FLAGS_MASK |= <unsigned long> getattr(stat_mod, _name, 0)\n\n\ndef set_flags(path, bsd_flags, fd=None):\n    \"\"\"Set a subset of BSD file flags on FreeBSD without disturbing other bits.\n\n    Only flags that are known to be settable are influenced. All other flag bits are preserved as-is.\n    \"\"\"\n    # Determine current flags.\n    try:\n        if fd is not None:\n            st = os.fstat(fd)\n        else:\n            st = os.lstat(path)\n        current = st.st_flags\n    except (OSError, AttributeError):\n        # We can't determine the current flags, so better give up than corrupting anything.\n        return\n\n    new_flags = (current & ~SETTABLE_FLAGS_MASK) | (bsd_flags & SETTABLE_FLAGS_MASK)\n\n    cdef unsigned long c_flags = <unsigned long> new_flags\n    if fd is not None:\n        if fchflags(fd, c_flags) == -1:\n            err = errno.errno\n            # Some filesystems may not support flags; ignore EOPNOTSUPP quietly.\n            if err != errno.EOPNOTSUPP:\n                # Keep error signature consistent with other platforms; st may not exist here.\n                raise OSError(err, os.strerror(err), path)\n    else:\n        path_bytes = os.fsencode(path)\n        if lchflags(path_bytes, c_flags) == -1:\n            err = errno.errno\n            if err != errno.EOPNOTSUPP:\n                raise OSError(err, os.strerror(err), os.fsdecode(path_bytes))\n"
  },
  {
    "path": "src/borg/platform/linux.pyx",
    "content": "import os\nimport re\nimport stat\n\nfrom .posix import posix_acl_use_stored_uid_gid\nfrom . import posix_ug\nfrom ..helpers import workarounds\nfrom ..helpers import safe_decode, safe_encode\nfrom .base import SyncFile as BaseSyncFile\nfrom .base import safe_fadvise\nfrom .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0\ntry:\n    from .syncfilerange import sync_file_range, SYNC_FILE_RANGE_WRITE, SYNC_FILE_RANGE_WAIT_BEFORE, SYNC_FILE_RANGE_WAIT_AFTER\n    SYNC_FILE_RANGE_LOADED = True\nexcept ImportError:\n    SYNC_FILE_RANGE_LOADED = False\n\nfrom libc cimport errno\n\n\n\ncdef extern from \"sys/xattr.h\":\n    ssize_t c_listxattr \"listxattr\" (const char *path, char *list, size_t size)\n    ssize_t c_llistxattr \"llistxattr\" (const char *path, char *list, size_t size)\n    ssize_t c_flistxattr \"flistxattr\" (int filedes, char *list, size_t size)\n\n    ssize_t c_getxattr \"getxattr\" (const char *path, const char *name, void *value, size_t size)\n    ssize_t c_lgetxattr \"lgetxattr\" (const char *path, const char *name, void *value, size_t size)\n    ssize_t c_fgetxattr \"fgetxattr\" (int filedes, const char *name, void *value, size_t size)\n\n    int c_setxattr \"setxattr\" (const char *path, const char *name, const void *value, size_t size, int flags)\n    int c_lsetxattr \"lsetxattr\" (const char *path, const char *name, const void *value, size_t size, int flags)\n    int c_fsetxattr \"fsetxattr\" (int filedes, const char *name, const void *value, size_t size, int flags)\n\ncdef extern from \"sys/types.h\":\n    int ACL_TYPE_ACCESS\n    int ACL_TYPE_DEFAULT\n\ncdef extern from \"sys/acl.h\":\n    ctypedef struct _acl_t:\n        pass\n    ctypedef _acl_t *acl_t\n\n    int acl_free(void *obj)\n    acl_t acl_get_file(const char *path, int type)\n    acl_t acl_get_fd(int fd)\n    int acl_set_file(const char *path, int type, acl_t acl)\n    int acl_set_fd(int fd, acl_t acl)\n    acl_t acl_from_text(const char *buf)\n\ncdef extern from \"acl/libacl.h\":\n    int acl_extended_file_nofollow(const char *path)\n    int acl_extended_fd(int fd)\n    char *acl_to_any_text(acl_t acl, const char *prefix, char separator, int options)\n    int TEXT_NUMERIC_IDS\n\ncdef extern from \"linux/fs.h\":\n    # ioctls\n    int FS_IOC_SETFLAGS\n    int FS_IOC_GETFLAGS\n\n    # inode flags\n    int FS_NODUMP_FL\n    int FS_IMMUTABLE_FL\n    int FS_APPEND_FL\n    int FS_COMPR_FL\n\ncdef extern from \"sys/ioctl.h\":\n    int ioctl(int fildes, int request, ...)\n\ncdef extern from \"unistd.h\":\n    int _SC_PAGESIZE\n    long sysconf(int name)\n\ncdef extern from \"string.h\":\n    char *strerror(int errnum)\n\n_comment_re = re.compile(' *#.*', re.M)\n\n\ndef listxattr(path, *, follow_symlinks=False):\n    def func(path, buf, size):\n        if isinstance(path, int):\n            return c_flistxattr(path, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_listxattr(path, <char *> buf, size)\n            else:\n                return c_llistxattr(path, <char *> buf, size)\n\n    n, buf = _listxattr_inner(func, path)\n    return [name for name in split_string0(buf[:n])\n            if name and not name.startswith(b'system.posix_acl_')]\n\n\ndef getxattr(path, name, *, follow_symlinks=False):\n    def func(path, name, buf, size):\n        if isinstance(path, int):\n            return c_fgetxattr(path, name, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_getxattr(path, name, <char *> buf, size)\n            else:\n                return c_lgetxattr(path, name, <char *> buf, size)\n\n    n, buf = _getxattr_inner(func, path, name)\n    return bytes(buf[:n])\n\n\ndef setxattr(path, name, value, *, follow_symlinks=False):\n    def func(path, name, value, size):\n        flags = 0\n        if isinstance(path, int):\n            return c_fsetxattr(path, name, <char *> value, size, flags)\n        else:\n            if follow_symlinks:\n                return c_setxattr(path, name, <char *> value, size, flags)\n            else:\n                return c_lsetxattr(path, name, <char *> value, size, flags)\n\n    _setxattr_inner(func, path, name, value)\n\n\nBSD_TO_LINUX_FLAGS = {\n    stat.UF_NODUMP: FS_NODUMP_FL,\n    stat.UF_IMMUTABLE: FS_IMMUTABLE_FL,\n    stat.UF_APPEND: FS_APPEND_FL,\n}\n# must be a bitwise OR of all values in BSD_TO_LINUX_FLAGS.\nLINUX_MASK = FS_NODUMP_FL | FS_IMMUTABLE_FL | FS_APPEND_FL\n\n\ndef set_flags(path, bsd_flags, fd=None):\n    if fd is None:\n        st = os.stat(path, follow_symlinks=False)\n        if stat.S_ISBLK(st.st_mode) or stat.S_ISCHR(st.st_mode) or stat.S_ISLNK(st.st_mode):\n            # see comment in get_flags()\n            return\n    cdef int flags\n    cdef int mask = LINUX_MASK  # 1 at positions we want to influence\n    cdef int new_flags = 0\n    for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():\n        if bsd_flags & bsd_flag:\n            new_flags |= linux_flag\n\n    open_fd = fd is None\n    if open_fd:\n        fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)\n    try:\n        # Get current flags.\n        if ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1:\n            # If this fails, give up because it is either not supported by the fs\n            # or maybe not permitted? If we can't determine the current flags,\n            # we better not risk corrupting them by setflags, see the comment below.\n            return  # give up silently\n\n        # Replace only the bits we actually want to influence, keep others.\n        # We can't just set all flags to the archived value, because we might\n        # reset flags that are not controllable from userspace, see #9039.\n        flags = (flags & ~mask) | (new_flags & mask)\n\n        if ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1:\n            error_number = errno.errno\n            # Usually we would only catch EOPNOTSUPP here, but Linux Kernel 6.17\n            # has a bug where it returns ENOTTY instead of EOPNOTSUPP.\n            if error_number not in (errno.EOPNOTSUPP, errno.ENOTTY):\n                raise OSError(error_number, strerror(error_number).decode(), path)\n    finally:\n        if open_fd:\n            os.close(fd)\n\n\ndef get_flags(path, st, fd=None):\n    if stat.S_ISBLK(st.st_mode) or stat.S_ISCHR(st.st_mode) or stat.S_ISLNK(st.st_mode):\n        # Avoid opening device files — trying to open non-present devices can be rather slow.\n        # Avoid opening symlinks; O_NOFOLLOW would make the open() fail anyway.\n        return 0\n    cdef int linux_flags\n    open_fd = fd is None\n    if open_fd:\n        try:\n            fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)\n        except OSError:\n            return 0\n    try:\n        if ioctl(fd, FS_IOC_GETFLAGS, &linux_flags) == -1:\n            return 0\n    finally:\n        if open_fd:\n            os.close(fd)\n    bsd_flags = 0\n    for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():\n        if linux_flags & linux_flag:\n            bsd_flags |= bsd_flag\n    return bsd_flags\n\n\ndef acl_use_local_uid_gid(acl):\n    \"\"\"Replace the user/group field with the local uid/gid if possible\n    \"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in safe_decode(acl).split('\\n'):\n        if entry:\n            fields = entry.split(':')\n            if fields[0] == 'user' and fields[1]:\n                fields[1] = str(posix_ug._user2uid(fields[1], fields[3]))\n            elif fields[0] == 'group' and fields[1]:\n                fields[1] = str(posix_ug._group2gid(fields[1], fields[3]))\n            entries.append(':'.join(fields[:3]))\n    return safe_encode('\\n'.join(entries))\n\n\ndef _acl_from_numeric_to_named_with_id(acl):\n    \"\"\"Convert numeric-id ACL entries to name entries and append numeric id as 4th field.\n\n    Input format (Linux libacl): lines like 'user:1000:rwx' or 'group:100:r-x' or 'user::rwx'.\n    Output format: for entries with a name/id field, become 'user:uname:rwx:uid' or 'group:gname:r-x:gid'.\n    \"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in _comment_re.sub('', safe_decode(acl)).split('\\n'):\n        if not entry:\n            continue\n        fields = entry.split(':')\n        # Expected 3 fields: type, ugid_or_empty, perms\n        if len(fields) >= 3:\n            typ, ugid_str, perm = fields[0], fields[1], fields[2]\n            if ugid_str and typ == 'user':\n                try:\n                    uid = int(ugid_str)\n                except ValueError:\n                    uid = None\n                uname = posix_ug._uid2user(uid, ugid_str) if uid is not None else ugid_str\n                entries.append(':'.join([typ, uname, perm, str(uid if uid is not None else ugid_str)]))\n            elif ugid_str and typ == 'group':\n                try:\n                    gid = int(ugid_str)\n                except ValueError:\n                    gid = None\n                gname = posix_ug._gid2group(gid, ugid_str) if gid is not None else ugid_str\n                entries.append(':'.join([typ, gname, perm, str(gid if gid is not None else ugid_str)]))\n            else:\n                # owner, group_obj, mask, other (empty ugid_str field) stay as-is\n                entries.append(':'.join([typ, '', perm]))\n        else:\n            entries.append(entry)\n    return safe_encode('\\n'.join(entries))\n\n\ndef _acl_from_numeric_to_numeric_with_id(acl):\n    \"\"\"Keep numeric ids in name field and append the same id as 4th field where applicable.\"\"\"\n    assert isinstance(acl, bytes)\n    entries = []\n    for entry in _comment_re.sub('', safe_decode(acl)).split('\\n'):\n        if not entry:\n            continue\n        fields = entry.split(':')\n        if len(fields) >= 3:\n            typ, ugid, perm = fields[0], fields[1], fields[2]\n            if ugid and (typ == 'user' or typ == 'group'):\n                entries.append(':'.join([typ, ugid, perm, ugid]))\n            else:\n                entries.append(':'.join([typ, '', perm]))\n        else:\n            entries.append(entry)\n    return safe_encode('\\n'.join(entries))\n\n\ndef acl_get(path, item, st, numeric_ids=False, fd=None):\n    cdef acl_t default_acl = NULL\n    cdef acl_t access_acl = NULL\n    cdef char *default_text = NULL\n    cdef char *access_text = NULL\n    cdef int ret = 0\n\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    if fd is not None:\n        ret = acl_extended_fd(fd)\n    else:\n        ret = acl_extended_file_nofollow(path)\n    if ret < 0:\n        raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n    if ret == 0:\n        # there is no ACL defining permissions other than those defined by the traditional file permission bits.\n        # note: this should also be the case for symlink fs objects, as they can not have ACLs.\n        return\n    if numeric_ids:\n        converter = _acl_from_numeric_to_numeric_with_id\n    else:\n        converter = _acl_from_numeric_to_named_with_id\n    try:\n        if fd is not None:\n            access_acl = acl_get_fd(fd)\n        else:\n            access_acl = acl_get_file(path, ACL_TYPE_ACCESS)\n        if access_acl == NULL:\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        access_text = acl_to_any_text(access_acl, NULL, '\\n', TEXT_NUMERIC_IDS)\n        if access_text == NULL:\n            raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        item['acl_access'] = converter(access_text)\n    finally:\n        acl_free(access_text)\n        acl_free(access_acl)\n    if stat.S_ISDIR(st.st_mode):\n        # only directories can have a default ACL. there is no fd-based api to get it.\n        try:\n            default_acl = acl_get_file(path, ACL_TYPE_DEFAULT)\n            if default_acl == NULL:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            default_text = acl_to_any_text(default_acl, NULL, '\\n', TEXT_NUMERIC_IDS)\n            if default_text == NULL:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            item['acl_default'] = converter(default_text)\n        finally:\n            acl_free(default_text)\n            acl_free(default_acl)\n\n\ndef acl_set(path, item, numeric_ids=False, fd=None):\n    cdef acl_t access_acl = NULL\n    cdef acl_t default_acl = NULL\n\n    if stat.S_ISLNK(item.get('mode', 0)):\n        # Linux does not support setting ACLs on symlinks\n        return\n\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    if numeric_ids:\n        converter = posix_acl_use_stored_uid_gid\n    else:\n        converter = acl_use_local_uid_gid\n    access_text = item.get('acl_access')\n    if access_text is not None:\n        try:\n            access_acl = acl_from_text(<bytes>converter(access_text))\n            if access_acl == NULL:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            if fd is not None:\n                if acl_set_fd(fd, access_acl) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            else:\n                if acl_set_file(path, ACL_TYPE_ACCESS, access_acl) == -1:\n                    raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        finally:\n            acl_free(access_acl)\n    default_text = item.get('acl_default')\n    if default_text is not None:\n        try:\n            default_acl = acl_from_text(<bytes>converter(default_text))\n            if default_acl == NULL:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n            # only directories can get a default ACL. there is no fd-based api to set it.\n            if acl_set_file(path, ACL_TYPE_DEFAULT, default_acl) == -1:\n                raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))\n        finally:\n            acl_free(default_acl)\n\n\ncdef _sync_file_range(fd, offset, length, flags):\n    assert offset & PAGE_MASK == 0, \"offset %d not page-aligned\" % offset\n    assert length & PAGE_MASK == 0, \"length %d not page-aligned\" % length\n    if sync_file_range(fd, offset, length, flags) != 0:\n        raise OSError(errno.errno, os.strerror(errno.errno))\n    safe_fadvise(fd, offset, length, 'DONTNEED')\n\n\ncdef unsigned PAGE_MASK = sysconf(_SC_PAGESIZE) - 1\n\n\nif 'basesyncfile' in workarounds or not SYNC_FILE_RANGE_LOADED:\n    class SyncFile(BaseSyncFile):\n        # if we are on platforms with a broken or not implemented sync_file_range,\n        # use the more generic BaseSyncFile to avoid issues.\n        # see basesyncfile description in our docs for details.\n        pass\nelse:\n    # a real Linux, so we can do better. :)\n    class SyncFile(BaseSyncFile):\n        \"\"\"\n        Implemented using sync_file_range for asynchronous write-out and fdatasync for actual durability.\n\n        \"write-out\" means that dirty pages (= data that was written) are submitted to an I/O queue and will be send to\n        disk in the immediate future.\n        \"\"\"\n\n        def __init__(self, path, *, fd=None, binary=False):\n            super().__init__(path, fd=fd, binary=binary)\n            self.offset = 0\n            self.write_window = (16 * 1024 ** 2) & ~PAGE_MASK\n            self.last_sync = 0\n            self.pending_sync = None\n\n        def write(self, data):\n            self.offset += self.f.write(data)\n            offset = self.offset & ~PAGE_MASK\n            if offset >= self.last_sync + self.write_window:\n                self.f.flush()\n                _sync_file_range(self.fd, self.last_sync, offset - self.last_sync, SYNC_FILE_RANGE_WRITE)\n                if self.pending_sync is not None:\n                    _sync_file_range(self.fd, self.pending_sync, self.last_sync - self.pending_sync,\n                                     SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WAIT_AFTER)\n                self.pending_sync = self.last_sync\n                self.last_sync = offset\n\n        def sync(self):\n            self.f.flush()\n            os.fdatasync(self.fd)\n            # tell the OS that it does not need to cache what we just wrote,\n            # avoids spoiling the cache for the OS and other processes.\n            safe_fadvise(self.fd, 0, 0, 'DONTNEED')\n"
  },
  {
    "path": "src/borg/platform/netbsd.pyx",
    "content": "from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lstring\n\n\n\ncdef extern from \"sys/extattr.h\":\n    ssize_t c_extattr_list_file \"extattr_list_file\" (const char *path, int attrnamespace, void *data, size_t nbytes)\n    ssize_t c_extattr_list_link \"extattr_list_link\" (const char *path, int attrnamespace, void *data, size_t nbytes)\n    ssize_t c_extattr_list_fd \"extattr_list_fd\" (int fd, int attrnamespace, void *data, size_t nbytes)\n\n    ssize_t c_extattr_get_file \"extattr_get_file\" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n    ssize_t c_extattr_get_link \"extattr_get_link\" (const char *path, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n    ssize_t c_extattr_get_fd \"extattr_get_fd\" (int fd, int attrnamespace, const char *attrname, void *data, size_t nbytes)\n\n    int c_extattr_set_file \"extattr_set_file\" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n    int c_extattr_set_link \"extattr_set_link\" (const char *path, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n    int c_extattr_set_fd \"extattr_set_fd\" (int fd, int attrnamespace, const char *attrname, const void *data, size_t nbytes)\n\n    int EXTATTR_NAMESPACE_USER\n\n\n# On NetBSD, Borg currently only deals with the USER namespace, as it is unclear\n# whether (and, if so, how exactly) it should deal with the SYSTEM namespace.\nNS_ID_MAP = {b\"user\": EXTATTR_NAMESPACE_USER, }\n\n\ndef split_ns(ns_name, default_ns):\n    # Split ns_name (which is in the form b\"namespace.name\") into namespace and name.\n    # If there is no namespace given in ns_name, default to default_ns.\n    # We also need to deal with \"unexpected\" namespaces here — they could come\n    # from Borg archives made on other operating systems.\n    ns_name_tuple = ns_name.split(b\".\", 1)\n    if len(ns_name_tuple) == 2:\n        # We have a namespace prefix in the given name.\n        ns, name = ns_name_tuple\n    else:\n        # No namespace given in ns_name (no dot found); maybe data from an old Borg archive.\n        ns, name = default_ns, ns_name\n    return ns, name\n\n\ndef listxattr(path, *, follow_symlinks=False):\n    def func(path, buf, size):\n        if isinstance(path, int):\n            return c_extattr_list_fd(path, ns_id, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_list_file(path, ns_id, <char *> buf, size)\n            else:\n                return c_extattr_list_link(path, ns_id, <char *> buf, size)\n\n    ns = b\"user\"\n    ns_id = NS_ID_MAP[ns]\n    n, buf = _listxattr_inner(func, path)\n    return [ns + b\".\" + name for name in split_lstring(buf[:n]) if name]\n\n\ndef getxattr(path, name, *, follow_symlinks=False):\n    def func(path, name, buf, size):\n        if isinstance(path, int):\n            return c_extattr_get_fd(path, ns_id, name, <char *> buf, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_get_file(path, ns_id, name, <char *> buf, size)\n            else:\n                return c_extattr_get_link(path, ns_id, name, <char *> buf, size)\n\n    ns, name = split_ns(name, b\"user\")\n    ns_id = NS_ID_MAP[ns]  # this will raise a KeyError it the namespace is unsupported\n    n, buf = _getxattr_inner(func, path, name)\n    return bytes(buf[:n])\n\n\ndef setxattr(path, name, value, *, follow_symlinks=False):\n    def func(path, name, value, size):\n        if isinstance(path, int):\n            return c_extattr_set_fd(path, ns_id, name, <char *> value, size)\n        else:\n            if follow_symlinks:\n                return c_extattr_set_file(path, ns_id, name, <char *> value, size)\n            else:\n                return c_extattr_set_link(path, ns_id, name, <char *> value, size)\n\n    ns, name = split_ns(name, b\"user\")\n    try:\n        ns_id = NS_ID_MAP[ns]  # this will raise a KeyError it the namespace is unsupported\n    except KeyError:\n        pass\n    else:\n        _setxattr_inner(func, path, name, value)\n"
  },
  {
    "path": "src/borg/platform/posix.pyx",
    "content": "import errno\nimport os\n\nfrom . import posix_ug\n\nfrom libc.errno cimport errno as c_errno\n\n\ndef get_errno():\n    return c_errno\n\n\ndef process_alive(host, pid, thread):\n    \"\"\"\n    Check whether the (host, pid, thread_id) combination corresponds to a process potentially alive.\n\n    If the process is local, then this will be accurate. If the process is not local, then this\n    always returns True, since there is no real way to check.\n    \"\"\"\n    from . import local_pid_alive\n    from . import hostid\n\n    assert isinstance(host, str)\n    assert isinstance(hostid, str)\n    assert isinstance(pid, int)\n    assert isinstance(thread, int)\n\n    if host != hostid:\n        return True\n\n    if thread != 0:\n        # Currently, thread is always 0; if we ever decide to set this to a non-zero value,\n        # this code needs to be revisited to do a sensible thing.\n        return True\n\n    return local_pid_alive(pid)\n\n\ndef local_pid_alive(pid):\n    \"\"\"Return whether *pid* is alive.\"\"\"\n    try:\n        # This doesn't work on Windows.\n        # This does not kill anything, 0 means \"see if we can send a signal to this process or not\".\n        # Possible errors: No such process (== stale lock) or permission denied (not a stale lock).\n        # If the exception is not raised that means such a pid is valid and we can send a signal to it.\n        os.kill(pid, 0)\n        return True\n    except OSError as err:\n        if err.errno == errno.ESRCH:\n            # ESRCH = no such process\n            return False\n        # Any other error (e.g., permissions) means that the process ID refers to a live process.\n        return True\n\n\ndef posix_acl_use_stored_uid_gid(acl):\n    \"\"\"Replace the user/group field with the stored uid/gid.\"\"\"\n    assert isinstance(acl, bytes)\n    from ..helpers import safe_decode, safe_encode\n    entries = []\n    for entry in safe_decode(acl).split('\\n'):\n        if entry:\n            fields = entry.split(':')\n            if len(fields) == 4:\n                entries.append(':'.join([fields[0], fields[3], fields[2]]))\n            else:\n                entries.append(entry)\n    return safe_encode('\\n'.join(entries))\n\n\ndef getosusername():\n    \"\"\"Return the OS username.\"\"\"\n    uid = os.getuid()\n    return posix_ug._uid2user(uid, uid)\n"
  },
  {
    "path": "src/borg/platform/posix_ug.py",
    "content": "import grp\nimport pwd\nfrom functools import lru_cache\n\n\n@lru_cache(maxsize=None)\ndef _uid2user(uid, default=None):\n    try:\n        return pwd.getpwuid(uid).pw_name\n    except KeyError:\n        return default\n\n\n@lru_cache(maxsize=None)\ndef _user2uid(user, default=None):\n    if not user:\n        return default\n    try:\n        return pwd.getpwnam(user).pw_uid\n    except KeyError:\n        return default\n\n\n@lru_cache(maxsize=None)\ndef _gid2group(gid, default=None):\n    try:\n        return grp.getgrgid(gid).gr_name\n    except KeyError:\n        return default\n\n\n@lru_cache(maxsize=None)\ndef _group2gid(group, default=None):\n    if not group:\n        return default\n    try:\n        return grp.getgrnam(group).gr_gid\n    except KeyError:\n        return default\n"
  },
  {
    "path": "src/borg/platform/syncfilerange.pyx",
    "content": "from libc.stdint cimport int64_t\n\n\n# Some Linux systems (like Termux on Android 7 or earlier) do not have access\n# to sync_file_range. By isolating the access to sync_file_range in this\n# separate extension, it can be imported dynamically from linux.pyx only when\n# available, and systems without support can otherwise use the rest of\n# linux.pyx.\ncdef extern from \"fcntl.h\":\n    int sync_file_range(int fd, int64_t offset, int64_t nbytes, unsigned int flags)\n    unsigned int SYNC_FILE_RANGE_WRITE\n    unsigned int SYNC_FILE_RANGE_WAIT_BEFORE\n    unsigned int SYNC_FILE_RANGE_WAIT_AFTER\n"
  },
  {
    "path": "src/borg/platform/windows.pyx",
    "content": "import ctypes\nimport ctypes.wintypes\nimport errno as errno_mod\nimport msvcrt\nimport os\nimport platform\n\nfrom .base import SyncFile as BaseSyncFile\n\n\ncdef extern from 'windows.h':\n    ctypedef void* HANDLE\n    ctypedef int BOOL\n    ctypedef unsigned long DWORD\n\n    BOOL CloseHandle(HANDLE hObject)\n    HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dbProcessId)\n\n    cdef extern int PROCESS_QUERY_INFORMATION\n\n\n# Win32 API constants for CreateFileW\nGENERIC_READ = 0x80000000\nGENERIC_WRITE = 0x40000000\nFILE_SHARE_READ = 0x00000001\nCREATE_NEW = 1\nFILE_ATTRIBUTE_NORMAL = 0x80\nFILE_FLAG_WRITE_THROUGH = 0x80000000\nERROR_FILE_EXISTS = 80\n\n_kernel32 = ctypes.WinDLL(\"kernel32\", use_last_error=True)\n_CreateFileW = _kernel32.CreateFileW\n_CreateFileW.restype = ctypes.wintypes.HANDLE\n_CreateFileW.argtypes = [\n    ctypes.wintypes.LPCWSTR,\n    ctypes.wintypes.DWORD,\n    ctypes.wintypes.DWORD,\n    ctypes.c_void_p,\n    ctypes.wintypes.DWORD,\n    ctypes.wintypes.DWORD,\n    ctypes.wintypes.HANDLE,\n]\n_CloseHandle = _kernel32.CloseHandle\nINVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value\n\n\nclass SyncFile(BaseSyncFile):\n    \"\"\"\n    Windows SyncFile using FILE_FLAG_WRITE_THROUGH for data durability.\n\n    FILE_FLAG_WRITE_THROUGH instructs Windows to write through any intermediate\n    cache and go directly to disk, providing data durability guarantees similar\n    to fdatasync/F_FULLFSYNC on POSIX/macOS systems.\n\n    When an already-open fd is provided, falls back to base implementation.\n    \"\"\"\n\n    def __init__(self, path, *, fd=None, binary=False):\n        if fd is not None:\n            # An already-opened fd was provided (e.g., from SaveFile via mkstemp).\n            # We cannot change its flags, so fall back to the base implementation.\n            super().__init__(path, fd=fd, binary=binary)\n            return\n\n        self.path = path\n        handle = _CreateFileW(\n            str(path),\n            GENERIC_READ | GENERIC_WRITE,\n            FILE_SHARE_READ,\n            None,\n            CREATE_NEW,  # fail if file exists, matching Python's 'x' mode\n            FILE_FLAG_WRITE_THROUGH | FILE_ATTRIBUTE_NORMAL,\n            None,\n        )\n        if handle == INVALID_HANDLE_VALUE:\n            error = ctypes.get_last_error()\n            if error == ERROR_FILE_EXISTS:\n                raise FileExistsError(errno_mod.EEXIST, os.strerror(errno_mod.EEXIST), str(path))\n            raise ctypes.WinError(error)\n\n        try:\n            oflags = os.O_BINARY if binary else os.O_TEXT\n            c_fd = msvcrt.open_osfhandle(handle, oflags)\n        except Exception:\n            _CloseHandle(handle)\n            raise\n\n        try:\n            mode = \"r+b\" if binary else \"r+\"\n            self.f = os.fdopen(c_fd, mode=mode)\n        except Exception:\n            os.close(c_fd)  # Also closes the underlying Windows handle\n            raise\n        self.fd = self.f.fileno()\n\n    def sync(self):\n        \"\"\"Flush and sync to persistent storage.\n\n        With FILE_FLAG_WRITE_THROUGH, writes already go to stable storage.\n        We still call os.fsync (FlushFileBuffers) for belt-and-suspenders safety.\n        \"\"\"\n        self.f.flush()\n        os.fsync(self.fd)\n\n\ndef getosusername():\n    \"\"\"Return the OS username.\"\"\"\n    return os.getlogin()\n\n\ndef process_alive(host, pid, thread):\n    \"\"\"\n    Check whether the (host, pid, thread_id) combination corresponds to a process potentially alive.\n    \"\"\"\n    if host.split('@')[0].lower() != platform.node().lower():\n        # If not running on the same node, assume the process is running.\n        return True\n\n    # If the process can be opened, the process is alive.\n    handle = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)\n    if handle != NULL:\n        CloseHandle(handle)\n        return True\n    return False\n\n\ndef local_pid_alive(pid):\n    \"\"\"Return whether *pid* is alive.\"\"\"\n    raise NotImplementedError\n"
  },
  {
    "path": "src/borg/platform/windows_ug.py",
    "content": "from functools import lru_cache\n\n\n@lru_cache(maxsize=None)\ndef _uid2user(uid, default=None):\n    # On Windows, Borg uses a simplified mapping for ownership fields.\n    # Return a stable placeholder name.\n    return \"root\"\n\n\n@lru_cache(maxsize=None)\ndef _user2uid(user, default=None):\n    if not user:\n        # user is either None or the empty string\n        return default\n    # Use 0 as the canonical uid placeholder on Windows.\n    return 0\n\n\n@lru_cache(maxsize=None)\ndef _gid2group(gid, default=None):\n    # On Windows, Borg uses a simplified mapping for ownership fields.\n    # Return a stable placeholder name.\n    return \"root\"\n\n\n@lru_cache(maxsize=None)\ndef _group2gid(group, default=None):\n    if not group:\n        # group is either None or the empty string\n        return default\n    # Use 0 as the canonical gid placeholder on Windows.\n    return 0\n"
  },
  {
    "path": "src/borg/platform/xattr.py",
    "content": "import errno\nimport os\n\nfrom ..helpers import Buffer\n\n\nbuffer = Buffer(bytearray, limit=2**24)\n\n\ndef split_string0(buf):\n    \"\"\"Split a list of zero-terminated byte strings into Python bytes (without zero termination).\"\"\"\n    if isinstance(buf, bytearray):\n        buf = bytes(buf)  # Use a bytes object so we return a list of bytes objects.\n    return buf.split(b\"\\0\")[:-1]\n\n\ndef split_lstring(buf):\n    \"\"\"Split a list of length-prefixed strings into Python bytes (without length prefixes).\"\"\"\n    result = []\n    mv = memoryview(buf)\n    while mv:\n        length = mv[0]\n        result.append(bytes(mv[1 : 1 + length]))\n        mv = mv[1 + length :]\n    return result\n\n\nclass BufferTooSmallError(Exception):\n    \"\"\"The buffer given to an xattr function was too small for the result.\"\"\"\n\n\ndef _check(rv, path=None, detect_buffer_too_small=False):\n    from . import get_errno\n\n    if rv < 0:\n        e = get_errno()\n        if detect_buffer_too_small and e == errno.ERANGE:\n            # listxattr and getxattr signal with ERANGE that they need a bigger result buffer.\n            # setxattr signals this when, e.g., an xattr key name is too long or unacceptable.\n            raise BufferTooSmallError\n        else:\n            try:\n                msg = os.strerror(e)\n            except ValueError:\n                msg = \"\"\n            if isinstance(path, int):\n                path = \"<FD %d>\" % path\n            raise OSError(e, msg, path)\n    if detect_buffer_too_small and rv >= len(buffer):\n        # FreeBSD does not error with ERANGE if the buffer is too small;\n        # it just fills the buffer, truncates, and returns.\n        # So, we play it safe and assume the result is truncated if\n        # it happens to be a full buffer.\n        raise BufferTooSmallError\n    return rv\n\n\ndef _listxattr_inner(func, path):\n    assert isinstance(path, (bytes, int))\n    size = len(buffer)\n    while True:\n        buf = buffer.get(size)\n        try:\n            n = _check(func(path, buf, size), path, detect_buffer_too_small=True)\n        except BufferTooSmallError:\n            size *= 2\n        else:\n            return n, buf\n\n\ndef _getxattr_inner(func, path, name):\n    assert isinstance(path, (bytes, int))\n    assert isinstance(name, bytes)\n    size = len(buffer)\n    while True:\n        buf = buffer.get(size)\n        try:\n            n = _check(func(path, name, buf, size), path, detect_buffer_too_small=True)\n        except BufferTooSmallError:\n            size *= 2\n        else:\n            return n, buf\n\n\ndef _setxattr_inner(func, path, name, value):\n    assert isinstance(path, (bytes, int))\n    assert isinstance(name, bytes)\n    assert isinstance(value, bytes)\n    _check(func(path, name, value, len(value)), path, detect_buffer_too_small=False)\n"
  },
  {
    "path": "src/borg/platformflags.py",
    "content": "\"\"\"\nFlags for platform-specific APIs.\n\nUse these flags instead of sys.platform.startswith('<os>') or try/except.\n\"\"\"\n\nimport os\nimport sys\n\nis_win32 = sys.platform.startswith(\"win32\")\nis_cygwin = sys.platform.startswith(\"cygwin\")\n\nis_linux = sys.platform.startswith(\"linux\")\nis_freebsd = sys.platform.startswith(\"freebsd\")\nis_netbsd = sys.platform.startswith(\"netbsd\")\nis_openbsd = sys.platform.startswith(\"openbsd\")\nis_darwin = sys.platform.startswith(\"darwin\")\nis_haiku = sys.platform.startswith(\"haiku\")\n\n# MSYS2 (on Windows)\nis_msystem = is_win32 and \"MSYSTEM\" in os.environ\n"
  },
  {
    "path": "src/borg/remote.py",
    "content": "import atexit\nimport errno\nimport functools\nimport inspect\nimport logging\nimport os\nimport queue\nimport select\nimport shlex\nimport shutil\nimport socket\nimport struct\nimport sys\nimport tempfile\nimport textwrap\nimport time\nimport traceback\nfrom subprocess import Popen, PIPE\n\nfrom xxhash import xxh64\n\nimport borg.logger\nfrom . import __version__\nfrom .compress import Compressor\nfrom .constants import *  # NOQA\nfrom .helpers import Error, ErrorWithTraceback, IntegrityError\nfrom .helpers import bin_to_hex\nfrom .helpers import get_limited_unpacker\nfrom .helpers import replace_placeholders\nfrom .helpers import sysinfo\nfrom .helpers import format_file_size\nfrom .helpers import safe_unlink\nfrom .helpers import prepare_subprocess_env, ignore_sigint\nfrom .helpers import get_socket_filename\nfrom .fslocking import LockTimeout, NotLocked, NotMyLock, LockFailed\nfrom .logger import create_logger, borg_serve_log_queue\nfrom .manifest import NoManifestError\nfrom .helpers import msgpack\nfrom .legacyrepository import LegacyRepository\nfrom .repository import Repository, StoreObjectNotFound\nfrom .version import parse_version, format_version\nfrom .helpers.datastruct import EfficientCollectionQueue\nfrom .platform import is_win32\n\nlogger = create_logger(__name__)\n\nBORG_VERSION = parse_version(__version__)\nMSGID, MSG, ARGS, RESULT, LOG = \"i\", \"m\", \"a\", \"r\", \"l\"\n\nMAX_INFLIGHT = 100\n\nRATELIMIT_PERIOD = 0.1\n\n\nclass ConnectionClosed(Error):\n    \"\"\"Connection closed by remote host\"\"\"\n\n    exit_mcode = 80\n\n\nclass ConnectionClosedWithHint(ConnectionClosed):\n    \"\"\"Connection closed by remote host. {}\"\"\"\n\n    exit_mcode = 81\n\n\nclass PathNotAllowed(Error):\n    \"\"\"Repository path not allowed: {}\"\"\"\n\n    exit_mcode = 83\n\n\nclass InvalidRPCMethod(Error):\n    \"\"\"RPC method {} is not valid\"\"\"\n\n    exit_mcode = 82\n\n\nclass UnexpectedRPCDataFormatFromClient(Error):\n    \"\"\"Borg {}: Got unexpected RPC data format from client.\"\"\"\n\n    exit_mcode = 85\n\n\nclass UnexpectedRPCDataFormatFromServer(Error):\n    \"\"\"Got unexpected RPC data format from server:\\n{}\"\"\"\n\n    exit_mcode = 86\n\n    def __init__(self, data):\n        try:\n            data = data.decode()[:128]\n        except UnicodeDecodeError:\n            data = data[:128]\n            data = [\"%02X\" % byte for byte in data]\n            data = textwrap.fill(\" \".join(data), 16 * 3)\n        super().__init__(data)\n\n\nclass ConnectionBrokenWithHint(Error):\n    \"\"\"Connection to remote host is broken. {}\"\"\"\n\n    exit_mcode = 87\n\n\n# Protocol compatibility:\n# In general the server is responsible for rejecting too old clients and the client it responsible for rejecting\n# too old servers. This ensures that the knowledge what is compatible is always held by the newer component.\n#\n# For the client the return of the negotiate method is a dict which includes the server version.\n#\n# All method calls on the remote repository object must be allowlisted in RepositoryServer.rpc_methods and have api\n# stubs in RemoteRepository*. The @api decorator on these stubs is used to set server version requirements.\n#\n# Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server.\n# If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs\n# to be added.\n# When parameters are removed, they need to be preserved as defaulted parameters on the client stubs so that older\n# servers still get compatible input.\n\n\nclass RepositoryServer:  # pragma: no cover\n    _legacy_rpc_methods = (  # LegacyRepository\n        \"__len__\",\n        \"check\",\n        \"commit\",\n        \"delete\",\n        \"destroy\",\n        \"get\",\n        \"list\",\n        \"negotiate\",\n        \"open\",\n        \"close\",\n        \"info\",\n        \"put\",\n        \"rollback\",\n        \"save_key\",\n        \"load_key\",\n        \"break_lock\",\n        \"inject_exception\",\n        \"get_manifest\",  # borg2 LegacyRepository has this\n    )\n\n    _rpc_methods = (  # Repository\n        \"__len__\",\n        \"check\",\n        \"delete\",\n        \"destroy\",\n        \"get\",\n        \"list\",\n        \"negotiate\",\n        \"open\",\n        \"close\",\n        \"info\",\n        \"put\",\n        \"save_key\",\n        \"load_key\",\n        \"break_lock\",\n        \"inject_exception\",\n        \"get_manifest\",\n        \"put_manifest\",\n        \"store_list\",\n        \"store_load\",\n        \"store_store\",\n        \"store_delete\",\n        \"store_move\",\n    )\n\n    def __init__(self, restrict_to_paths, restrict_to_repositories, use_socket, permissions=None):\n        self.repository = None\n        self.RepoCls = None\n        self.rpc_methods = (\"open\", \"close\", \"negotiate\")\n        self.restrict_to_paths = restrict_to_paths\n        self.restrict_to_repositories = restrict_to_repositories\n        self.permissions = permissions\n        # This flag is parsed from the serve command line via Archiver.do_serve,\n        # i.e. it reflects local system policy and generally ranks higher than\n        # whatever the client wants, except when initializing a new repository\n        # (see RepositoryServer.open below).\n        self.client_version = None  # we update this after client sends version information\n        if use_socket is False:\n            self.socket_path = None\n        elif use_socket is True:  # --socket\n            self.socket_path = get_socket_filename()\n        else:  # --socket=/some/path\n            self.socket_path = use_socket\n\n    def filter_args(self, f, kwargs):\n        \"\"\"Remove unknown named parameters from call, because client did (implicitly) say it's ok.\"\"\"\n        known = set(inspect.signature(f).parameters)\n        return {name: kwargs[name] for name in kwargs if name in known}\n\n    def send_queued_log(self):\n        while True:\n            try:\n                # lr_dict contents see BorgQueueHandler\n                lr_dict = borg_serve_log_queue.get_nowait()\n            except queue.Empty:\n                break\n            else:\n                msg = msgpack.packb({LOG: lr_dict})\n                os.write(self.stdout_fd, msg)\n\n    def serve(self):\n        def inner_serve():\n            os.set_blocking(self.stdin_fd, False)\n            assert not os.get_blocking(self.stdin_fd)\n            os.set_blocking(self.stdout_fd, True)\n            assert os.get_blocking(self.stdout_fd)\n\n            unpacker = get_limited_unpacker(\"server\")\n            shutdown_serve = False\n            while True:\n                # before processing any new RPCs, send out all pending log output\n                self.send_queued_log()\n\n                if shutdown_serve:\n                    # shutdown wanted! get out of here after sending all log output.\n                    assert self.repository is None\n                    return\n\n                # process new RPCs\n                r, w, es = select.select([self.stdin_fd], [], [], 10)\n                if r:\n                    data = os.read(self.stdin_fd, BUFSIZE)\n                    if not data:\n                        shutdown_serve = True\n                        continue\n                    unpacker.feed(data)\n                    for unpacked in unpacker:\n                        if isinstance(unpacked, dict):\n                            msgid = unpacked[MSGID]\n                            method = unpacked[MSG]\n                            args = unpacked[ARGS]\n                        else:\n                            if self.repository is not None:\n                                self.repository.close()\n                            raise UnexpectedRPCDataFormatFromClient(__version__)\n                        try:\n                            # logger.debug(f\"{type(self)} method: {type(self.repository)}.{method}\")\n                            if method not in self.rpc_methods:\n                                raise InvalidRPCMethod(method)\n                            try:\n                                f = getattr(self, method)\n                            except AttributeError:\n                                f = getattr(self.repository, method)\n                            args = self.filter_args(f, args)\n                            res = f(**args)\n                        except BaseException as e:\n                            # These exceptions are reconstructed on the client end in RemoteRepository.call_many(),\n                            # and will be handled just like locally raised exceptions. Suppress the remote traceback\n                            # for these, except ErrorWithTraceback, which should always display a traceback.\n                            reconstructed_exceptions = (\n                                Repository.InvalidRepository,\n                                Repository.InvalidRepositoryConfig,\n                                Repository.DoesNotExist,\n                                Repository.AlreadyExists,\n                                Repository.PathAlreadyExists,\n                                PathNotAllowed,\n                                Repository.InsufficientFreeSpaceError,\n                            )\n                            # logger.exception(e)\n                            ex_short = traceback.format_exception_only(e.__class__, e)\n                            ex_full = traceback.format_exception(*sys.exc_info())\n                            ex_trace = True\n                            if isinstance(e, Error):\n                                ex_short = [e.get_message()]\n                                ex_trace = e.traceback\n                            if not isinstance(e, reconstructed_exceptions):\n                                logging.debug(\"\\n\".join(ex_full))\n\n                            sys_info = sysinfo()\n                            # StoreObjectNotFound and Repository.ObjectNotFound both have\n                            # __name__ == \"ObjectNotFound\", so we need to distinguish them\n                            # explicitly for correct client-side reconstruction.\n                            exc_cls_name = (\n                                \"StoreObjectNotFound\" if isinstance(e, StoreObjectNotFound) else e.__class__.__name__\n                            )\n                            try:\n                                msg = msgpack.packb(\n                                    {\n                                        MSGID: msgid,\n                                        \"exception_class\": exc_cls_name,\n                                        \"exception_args\": e.args,\n                                        \"exception_full\": ex_full,\n                                        \"exception_short\": ex_short,\n                                        \"exception_trace\": ex_trace,\n                                        \"sysinfo\": sys_info,\n                                    }\n                                )\n                            except TypeError:\n                                msg = msgpack.packb(\n                                    {\n                                        MSGID: msgid,\n                                        \"exception_class\": exc_cls_name,\n                                        \"exception_args\": [\n                                            x if isinstance(x, (str, bytes, int)) else None for x in e.args\n                                        ],\n                                        \"exception_full\": ex_full,\n                                        \"exception_short\": ex_short,\n                                        \"exception_trace\": ex_trace,\n                                        \"sysinfo\": sys_info,\n                                    }\n                                )\n                            os.write(self.stdout_fd, msg)\n                        else:\n                            os.write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res}))\n                if es:\n                    shutdown_serve = True\n                    continue\n\n        if self.socket_path:  # server for socket:// connections\n            try:\n                # remove any left-over socket file\n                os.unlink(self.socket_path)\n            except OSError:\n                if os.path.exists(self.socket_path):\n                    raise\n            sock_dir = os.path.dirname(self.socket_path)\n            os.makedirs(sock_dir, exist_ok=True)\n            pid_file = self.socket_path.removesuffix(\".sock\") + \".pid\"\n            pid = os.getpid()\n            with open(pid_file, \"w\") as f:\n                f.write(str(pid))\n            atexit.register(functools.partial(os.remove, pid_file))\n            atexit.register(functools.partial(os.remove, self.socket_path))\n            sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)\n            sock.bind(self.socket_path)  # this creates the socket file in the fs\n            sock.listen(0)  # no backlog\n            os.chmod(self.socket_path, mode=0o0770)  # group members may use the socket, too.\n            print(f\"borg serve: PID {pid}, listening on socket {self.socket_path} ...\", file=sys.stderr)\n\n            while True:\n                connection, client_address = sock.accept()\n                print(f\"Accepted a connection on socket {self.socket_path} ...\", file=sys.stderr)\n                self.stdin_fd = connection.makefile(\"rb\").fileno()\n                self.stdout_fd = connection.makefile(\"wb\").fileno()\n                inner_serve()\n                print(f\"Finished with connection on socket {self.socket_path} .\", file=sys.stderr)\n        else:  # server for one ssh:// connection\n            self.stdin_fd = sys.stdin.fileno()\n            self.stdout_fd = sys.stdout.fileno()\n            inner_serve()\n\n    def negotiate(self, client_data):\n        if isinstance(client_data, dict):\n            self.client_version = client_data[\"client_version\"]\n        else:\n            self.client_version = BORG_VERSION  # seems to be newer than current version (no known old format)\n\n        # not a known old format, send newest negotiate this version knows\n        return {\"server_version\": BORG_VERSION}\n\n    def _resolve_path(self, path):\n        if isinstance(path, bytes):\n            path = os.fsdecode(path)\n        path = os.path.realpath(path)\n        return path\n\n    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, v1_or_v2=False):\n        self.RepoCls = LegacyRepository if v1_or_v2 else Repository\n        self.rpc_methods = self._legacy_rpc_methods if v1_or_v2 else self._rpc_methods\n        logging.debug(\"Resolving repository path %r\", path)\n        path = self._resolve_path(path)\n        logging.debug(\"Resolved repository path to %r\", path)\n        path_with_sep = os.path.join(path, \"\")  # make sure there is a trailing slash (os.sep)\n        if self.restrict_to_paths:\n            # if --restrict-to-path P is given, we make sure that we only operate in/below path P.\n            # for the prefix check, it is important that the compared paths both have trailing slashes,\n            # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option.\n            for restrict_to_path in self.restrict_to_paths:\n                restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), \"\")  # trailing slash\n                if path_with_sep.startswith(restrict_to_path_with_sep):\n                    break\n            else:\n                raise PathNotAllowed(path)\n        if self.restrict_to_repositories:\n            for restrict_to_repository in self.restrict_to_repositories:\n                restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), \"\")\n                if restrict_to_repository_with_sep == path_with_sep:\n                    break\n            else:\n                raise PathNotAllowed(path)\n        kwargs = dict(lock_wait=lock_wait, lock=lock, exclusive=exclusive, send_log_cb=self.send_queued_log)\n        if not v1_or_v2:\n            kwargs[\"permissions\"] = self.permissions\n        self.repository = self.RepoCls(path, create, **kwargs)\n        self.repository.__enter__()  # clean exit handled by serve() method\n        return self.repository.id\n\n    def close(self):\n        if self.repository is not None:\n            self.repository.__exit__(None, None, None)\n            self.repository = None\n        borg.logger.flush_logging()\n        self.send_queued_log()\n\n    def inject_exception(self, kind):\n        s1 = \"test string\"\n        s2 = \"test string2\"\n        if kind == \"DoesNotExist\":\n            raise self.RepoCls.DoesNotExist(s1)\n        elif kind == \"AlreadyExists\":\n            raise self.RepoCls.AlreadyExists(s1)\n        elif kind == \"CheckNeeded\":\n            raise self.RepoCls.CheckNeeded(s1)\n        elif kind == \"IntegrityError\":\n            raise IntegrityError(s1)\n        elif kind == \"PathNotAllowed\":\n            raise PathNotAllowed(\"foo\")\n        elif kind == \"ObjectNotFound\":\n            raise self.RepoCls.ObjectNotFound(s1, s2)\n        elif kind == \"StoreObjectNotFound\":\n            raise StoreObjectNotFound(s1)\n        elif kind == \"InvalidRPCMethod\":\n            raise InvalidRPCMethod(s1)\n        elif kind == \"divide\":\n            0 // 0\n\n\nclass SleepingBandwidthLimiter:\n    def __init__(self, limit):\n        if limit:\n            self.ratelimit = int(limit * RATELIMIT_PERIOD)\n            self.ratelimit_last = time.monotonic()\n            self.ratelimit_quota = self.ratelimit\n        else:\n            self.ratelimit = None\n\n    def write(self, fd, to_send):\n        if self.ratelimit:\n            now = time.monotonic()\n            if self.ratelimit_last + RATELIMIT_PERIOD <= now:\n                self.ratelimit_quota += self.ratelimit\n                if self.ratelimit_quota > 2 * self.ratelimit:\n                    self.ratelimit_quota = 2 * self.ratelimit\n                self.ratelimit_last = now\n            if self.ratelimit_quota == 0:\n                tosleep = self.ratelimit_last + RATELIMIT_PERIOD - now\n                time.sleep(tosleep)\n                self.ratelimit_quota += self.ratelimit\n                self.ratelimit_last = time.monotonic()\n            if len(to_send) > self.ratelimit_quota:\n                to_send = to_send[: self.ratelimit_quota]\n        try:\n            written = os.write(fd, to_send)\n        except BrokenPipeError:\n            raise ConnectionBrokenWithHint(\"Broken Pipe\") from None\n        if self.ratelimit:\n            self.ratelimit_quota -= written\n        return written\n\n\ndef api(*, since, **kwargs_decorator):\n    \"\"\"Check version requirements and use self.call to do the remote method call.\n\n    <since> specifies the version in which borg introduced this method.\n    Calling this method when connected to an older version will fail without transmitting anything to the server.\n\n    Further kwargs can be used to encode version specific restrictions:\n\n    <previously> is the value resulting in the behaviour before introducing the new parameter.\n    If a previous hardcoded behaviour is parameterized in a version, this allows calls that use the previously\n    hardcoded behaviour to pass through and generates an error if another behaviour is requested by the client.\n    E.g. when 'append_only' was introduced in 1.0.7 the previous behaviour was what now is append_only=False.\n    Thus @api(..., append_only={'since': parse_version('1.0.7'), 'previously': False}) allows calls\n    with append_only=False for all version but rejects calls using append_only=True on versions older than 1.0.7.\n\n    <dontcare> is a flag to set the behaviour if an old version is called the new way.\n    If set to True, the method is called without the (not yet supported) parameter (this should be done if that is the\n    more desirable behaviour). If False, an exception is generated.\n    E.g. before 'threshold' was introduced in 1.2.0a8, a hardcoded threshold of 0.1 was used in commit().\n    \"\"\"\n\n    def decorator(f):\n        @functools.wraps(f)\n        def do_rpc(self, *args, **kwargs):\n            sig = inspect.signature(f)\n            bound_args = sig.bind(self, *args, **kwargs)\n            named = {}  # Arguments for the remote process\n            extra = {}  # Arguments for the local process\n            for name, param in sig.parameters.items():\n                if name == \"self\":\n                    continue\n                if name in bound_args.arguments:\n                    if name == \"wait\":\n                        extra[name] = bound_args.arguments[name]\n                    else:\n                        named[name] = bound_args.arguments[name]\n                else:\n                    if param.default is not param.empty:\n                        named[name] = param.default\n\n            if self.server_version < since:\n                raise self.RPCServerOutdated(f.__name__, format_version(since))\n\n            for name, restriction in kwargs_decorator.items():\n                if restriction[\"since\"] <= self.server_version:\n                    continue\n                if \"previously\" in restriction and named[name] == restriction[\"previously\"]:\n                    continue\n                if restriction.get(\"dontcare\", False):\n                    continue\n\n                raise self.RPCServerOutdated(\n                    f\"{f.__name__} {name}={named[name]!s}\", format_version(restriction[\"since\"])\n                )\n\n            return self.call(f.__name__, named, **extra)\n\n        return do_rpc\n\n    return decorator\n\n\nclass RemoteRepository:\n    extra_test_args = []  # type: ignore\n\n    class RPCError(Exception):\n        def __init__(self, unpacked):\n            # unpacked has keys: 'exception_args', 'exception_full', 'exception_short', 'sysinfo'\n            self.unpacked = unpacked\n\n        def get_message(self):\n            return \"\\n\".join(self.unpacked[\"exception_short\"])\n\n        @property\n        def traceback(self):\n            return self.unpacked.get(\"exception_trace\", True)\n\n        @property\n        def exception_class(self):\n            return self.unpacked[\"exception_class\"]\n\n        @property\n        def exception_full(self):\n            return \"\\n\".join(self.unpacked[\"exception_full\"])\n\n        @property\n        def sysinfo(self):\n            return self.unpacked[\"sysinfo\"]\n\n    class RPCServerOutdated(Error):\n        \"\"\"Borg server is too old for {}. Required version {}\"\"\"\n\n        exit_mcode = 84\n\n        @property\n        def method(self):\n            return self.args[0]\n\n        @property\n        def required_version(self):\n            return self.args[1]\n\n    def __init__(self, location, create=False, exclusive=False, lock_wait=1.0, lock=True, args=None):\n        self.location = self._location = location\n        self.preload_ids = []\n        self.msgid = 0\n        self.rx_bytes = 0\n        self.tx_bytes = 0\n        self.to_send = EfficientCollectionQueue(1024 * 1024, bytes)\n        self.stdin_fd = self.stdout_fd = self.stderr_fd = None\n        self.stderr_received = b\"\"  # incomplete stderr line bytes received (no \\n yet)\n        self.chunkid_to_msgids = {}\n        self.ignore_responses = set()\n        self.responses = {}\n        self.async_responses = {}\n        self.shutdown_time = None\n        self.ratelimit = SleepingBandwidthLimiter(args.upload_ratelimit * 1024 if args and args.upload_ratelimit else 0)\n        self.upload_buffer_size_limit = args.upload_buffer * 1024 * 1024 if args and args.upload_buffer else 0\n        self.unpacker = get_limited_unpacker(\"client\")\n        self.server_version = None  # we update this after server sends its version\n        self.p = self.sock = None\n        self._args = args\n        if self.location.proto == \"ssh\":\n            testing = location.host == \"__testsuite__\"\n            # when testing, we invoke and talk to a borg process directly (no ssh).\n            # when not testing, we invoke the system-installed ssh binary to talk to a remote borg.\n            env = prepare_subprocess_env(system=not testing)\n            borg_cmd = self.borg_cmd(args, testing)\n            if not testing:\n                borg_cmd = self.ssh_cmd(location) + borg_cmd\n            logger.debug(\"SSH command line: %s\", borg_cmd)\n            # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg.\n            self.p = Popen(\n                borg_cmd,\n                bufsize=0,\n                stdin=PIPE,\n                stdout=PIPE,\n                stderr=PIPE,\n                env=env,\n                preexec_fn=None if is_win32 else ignore_sigint,\n            )  # nosec B603\n            self.stdin_fd = self.p.stdin.fileno()\n            self.stdout_fd = self.p.stdout.fileno()\n            self.stderr_fd = self.p.stderr.fileno()\n            self.r_fds = [self.stdout_fd, self.stderr_fd]\n            self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd]\n        elif self.location.proto == \"socket\":\n            if args.use_socket is False or args.use_socket is True:  # nothing or --socket\n                socket_path = get_socket_filename()\n            else:  # --socket=/some/path\n                socket_path = args.use_socket\n            self.sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)\n            try:\n                self.sock.connect(socket_path)  # note: socket_path length is rather limited.\n            except FileNotFoundError:\n                self.sock = None\n                raise Error(f\"The socket file {socket_path} does not exist.\")\n            except ConnectionRefusedError:\n                self.sock = None\n                raise Error(f\"There is no borg serve running for the socket file {socket_path}.\")\n            self.stdin_fd = self.sock.makefile(\"wb\").fileno()\n            self.stdout_fd = self.sock.makefile(\"rb\").fileno()\n            self.stderr_fd = None\n            self.r_fds = [self.stdout_fd]\n            self.x_fds = [self.stdin_fd, self.stdout_fd]\n        else:\n            raise Error(f\"Unsupported protocol {location.proto}\")\n\n        os.set_blocking(self.stdin_fd, False)\n        assert not os.get_blocking(self.stdin_fd)\n        os.set_blocking(self.stdout_fd, False)\n        assert not os.get_blocking(self.stdout_fd)\n        if self.stderr_fd is not None:\n            os.set_blocking(self.stderr_fd, False)\n            assert not os.get_blocking(self.stderr_fd)\n\n        try:\n            try:\n                version = self.call(\"negotiate\", {\"client_data\": {\"client_version\": BORG_VERSION}})\n            except ConnectionClosed:\n                raise ConnectionClosedWithHint(\"Is borg working on the server?\") from None\n            if isinstance(version, dict):\n                self.server_version = version[\"server_version\"]\n            else:\n                raise Exception(\"Server insisted on using unsupported protocol version %s\" % version)\n\n            self.id = self.open(\n                path=self.location.path, create=create, lock_wait=lock_wait, lock=lock, exclusive=exclusive\n            )\n            info = self.info()\n            self.version = info[\"version\"]\n\n        except Exception:\n            self.close()\n            raise\n\n    def __del__(self):\n        if len(self.responses):\n            logging.debug(\"still %d cached responses left in RemoteRepository\" % (len(self.responses),))\n        if self.p or self.sock:\n            self.close()\n            assert False, \"cleanup happened in RemoteRepository.__del__\"\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {self.location.canonical_path()}>\"\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        try:\n            if exc_type is not None:\n                self.shutdown_time = time.monotonic() + 30\n        finally:\n            # in any case, we want to close the repo cleanly.\n            logger.debug(\n                \"RemoteRepository: %s bytes sent, %s bytes received, %d messages sent\",\n                format_file_size(self.tx_bytes),\n                format_file_size(self.rx_bytes),\n                self.msgid,\n            )\n            self.close()\n\n    @property\n    def id_str(self):\n        return bin_to_hex(self.id)\n\n    def borg_cmd(self, args, testing):\n        \"\"\"return a borg serve command line\"\"\"\n        # give some args/options to 'borg serve' process as they were given to us\n        opts = []\n        if args is not None:\n            root_logger = logging.getLogger()\n            if root_logger.isEnabledFor(logging.DEBUG):\n                opts.append(\"--debug\")\n            elif root_logger.isEnabledFor(logging.INFO):\n                opts.append(\"--info\")\n            elif root_logger.isEnabledFor(logging.WARNING):\n                pass  # warning is default\n            elif root_logger.isEnabledFor(logging.ERROR):\n                opts.append(\"--error\")\n            elif root_logger.isEnabledFor(logging.CRITICAL):\n                opts.append(\"--critical\")\n            else:\n                raise ValueError(\"log level missing, fix this code\")\n\n            # Tell the remote server about debug topics it may need to consider.\n            # Note that debug topics are usable for \"spew\" or \"trace\" logs which would\n            # be too plentiful to transfer for normal use, so the server doesn't send\n            # them unless explicitly enabled.\n            #\n            # Needless to say, if you do --debug-topic=repository.compaction, for example,\n            # with a 1.0.x server it won't work, because the server does not recognize the\n            # option.\n            #\n            # This is not considered a problem, since this is a debugging feature that\n            # should not be used for regular use.\n            for topic in args.debug_topics:\n                if \".\" not in topic:\n                    topic = \"borg.debug.\" + topic\n                if \"repository\" in topic:\n                    opts.append(\"--debug-topic=%s\" % topic)\n        env_vars = []\n        if testing:\n            return env_vars + [sys.executable, \"-m\", \"borg\", \"serve\"] + opts + self.extra_test_args\n        else:  # pragma: no cover\n            remote_path = args.remote_path or os.environ.get(\"BORG_REMOTE_PATH\", \"borg\")\n            remote_path = replace_placeholders(remote_path)\n            return env_vars + [remote_path, \"serve\"] + opts\n\n    def ssh_cmd(self, location):\n        \"\"\"return a ssh command line that can be prefixed to a borg command line\"\"\"\n        rsh = self._args.rsh or os.environ.get(\"BORG_RSH\", \"ssh\")\n        args = shlex.split(rsh)\n        if location.port:\n            args += [\"-p\", str(location.port)]\n        if location.user:\n            args.append(f\"{location.user}@{location.host}\")\n        else:\n            args.append(\"%s\" % location.host)\n        return args\n\n    def call(self, cmd, args, **kw):\n        for resp in self.call_many(cmd, [args], **kw):\n            return resp\n\n    def call_many(self, cmd, calls, wait=True, is_preloaded=False, async_wait=True):\n        if not calls and cmd != \"async_responses\":\n            return\n\n        assert not is_preloaded or cmd == \"get\", \"is_preloaded is only supported for 'get'\"\n\n        def send_buffer():\n            if self.to_send:\n                try:\n                    written = self.ratelimit.write(self.stdin_fd, self.to_send.peek_front())\n                    self.tx_bytes += written\n                    self.to_send.pop_front(written)\n                except OSError as e:\n                    # io.write might raise EAGAIN even though select indicates\n                    # that the fd should be writable.\n                    # EWOULDBLOCK is added for defensive programming sake.\n                    if e.errno not in [errno.EAGAIN, errno.EWOULDBLOCK]:\n                        raise\n\n        def pop_preload_msgid(chunkid):\n            msgid = self.chunkid_to_msgids[chunkid].pop(0)\n            if not self.chunkid_to_msgids[chunkid]:\n                del self.chunkid_to_msgids[chunkid]\n            return msgid\n\n        def handle_error(unpacked):\n            if \"exception_class\" not in unpacked:\n                return\n\n            error = unpacked[\"exception_class\"]\n            args = unpacked[\"exception_args\"]\n\n            if error == \"Error\":\n                raise Error(args[0])\n            elif error == \"ErrorWithTraceback\":\n                raise ErrorWithTraceback(args[0])\n            elif error == \"InvalidRepository\":\n                raise Repository.InvalidRepository(self.location.processed)\n            elif error == \"DoesNotExist\":\n                raise Repository.DoesNotExist(self.location.processed)\n            elif error == \"AlreadyExists\":\n                raise Repository.AlreadyExists(self.location.processed)\n            elif error == \"CheckNeeded\":\n                raise Repository.CheckNeeded(self.location.processed)\n            elif error == \"IntegrityError\":\n                raise IntegrityError(args[0])\n            elif error == \"PathNotAllowed\":\n                raise PathNotAllowed(args[0])\n            elif error == \"PathPermissionDenied\":\n                raise Repository.PathPermissionDenied(args[0])\n            elif error == \"PathAlreadyExists\":\n                raise Repository.PathAlreadyExists(args[0])\n            elif error == \"ParentPathDoesNotExist\":\n                raise Repository.ParentPathDoesNotExist(args[0])\n            elif error == \"ObjectNotFound\":\n                raise Repository.ObjectNotFound(args[0], self.location.processed)\n            elif error == \"StoreObjectNotFound\":\n                raise StoreObjectNotFound(args[0])\n            elif error == \"InvalidRPCMethod\":\n                raise InvalidRPCMethod(args[0])\n            elif error == \"LockTimeout\":\n                raise LockTimeout(args[0])\n            elif error == \"LockFailed\":\n                raise LockFailed(args[0], args[1])\n            elif error == \"NotLocked\":\n                raise NotLocked(args[0])\n            elif error == \"NotMyLock\":\n                raise NotMyLock(args[0])\n            elif error == \"NoManifestError\":\n                raise NoManifestError\n            elif error == \"InsufficientFreeSpaceError\":\n                raise Repository.InsufficientFreeSpaceError(args[0], args[1])\n            elif error == \"InvalidRepositoryConfig\":\n                raise Repository.InvalidRepositoryConfig(self.location.processed, args[1])\n            else:\n                raise self.RPCError(unpacked)\n\n        calls = list(calls)\n        waiting_for = []\n        maximum_to_send = 0 if wait else self.upload_buffer_size_limit\n        send_buffer()  # Try to send data, as some cases (async_response) will never try to send data otherwise.\n        while wait or calls:\n            logger.debug(\n                f\"call_many: calls: {len(calls)} waiting_for: {len(waiting_for)} responses: {len(self.responses)}\"\n            )\n            if self.shutdown_time and time.monotonic() > self.shutdown_time:\n                # we are shutting this RemoteRepository down already, make sure we do not waste\n                # a lot of time in case a lot of async stuff is coming in or remote is gone or slow.\n                logger.debug(\n                    \"shutdown_time reached, shutting down with %d waiting_for and %d async_responses.\",\n                    len(waiting_for),\n                    len(self.async_responses),\n                )\n                return\n            while waiting_for:\n                try:\n                    unpacked = self.responses.pop(waiting_for[0])\n                    waiting_for.pop(0)\n                    handle_error(unpacked)\n                    yield unpacked[RESULT]\n                    if not waiting_for and not calls:\n                        return\n                except KeyError:\n                    break\n            if cmd == \"async_responses\":\n                while True:\n                    try:\n                        msgid, unpacked = self.async_responses.popitem()\n                    except KeyError:\n                        # there is nothing left what we already have received\n                        if async_wait and self.ignore_responses:\n                            # but do not return if we shall wait and there is something left to wait for:\n                            break\n                        else:\n                            return\n                    else:\n                        handle_error(unpacked)\n                        yield unpacked[RESULT]\n            if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT):\n                w_fds = [self.stdin_fd]\n            else:\n                w_fds = []\n            r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1)\n            if x:\n                raise Exception(\"FD exception occurred\")\n            for fd in r:\n                if fd is self.stdout_fd:\n                    data = os.read(fd, BUFSIZE)\n                    if not data:\n                        raise ConnectionClosed()\n                    self.rx_bytes += len(data)\n                    self.unpacker.feed(data)\n                    for unpacked in self.unpacker:\n                        if not isinstance(unpacked, dict):\n                            raise UnexpectedRPCDataFormatFromServer(data)\n\n                        lr_dict = unpacked.get(LOG)\n                        if lr_dict is not None:\n                            # Re-emit remote log messages locally.\n                            _logger = logging.getLogger(lr_dict[\"name\"])\n                            if _logger.isEnabledFor(lr_dict[\"level\"]):\n                                _logger.handle(logging.LogRecord(**lr_dict))\n                            continue\n\n                        msgid = unpacked[MSGID]\n                        if msgid in self.ignore_responses:\n                            self.ignore_responses.remove(msgid)\n                            # async methods never return values, but may raise exceptions.\n                            if \"exception_class\" in unpacked:\n                                self.async_responses[msgid] = unpacked\n                            else:\n                                # we currently do not have async result values except \"None\",\n                                # so we do not add them into async_responses.\n                                if unpacked[RESULT] is not None:\n                                    self.async_responses[msgid] = unpacked\n                        else:\n                            self.responses[msgid] = unpacked\n                elif fd is self.stderr_fd:\n                    data = os.read(fd, 32768)\n                    if not data:\n                        raise ConnectionClosed()\n                    self.rx_bytes += len(data)\n                    # deal with incomplete lines (may appear due to block buffering)\n                    if self.stderr_received:\n                        data = self.stderr_received + data\n                        self.stderr_received = b\"\"\n                    lines = data.splitlines(keepends=True)\n                    if lines and not lines[-1].endswith((b\"\\r\", b\"\\n\")):\n                        self.stderr_received = lines.pop()\n                    # now we have complete lines in <lines> and any partial line in self.stderr_received.\n                    _logger = logging.getLogger()\n                    for line in lines:\n                        # borg serve (remote/server side) should not emit stuff on stderr,\n                        # but e.g. the ssh process (local/client side) might output errors there.\n                        assert line.endswith((b\"\\r\", b\"\\n\"))\n                        # something came in on stderr, log it to not lose it.\n                        # decode late, avoid partial utf-8 sequences.\n                        _logger.warning(\"stderr: \" + line.decode().strip())\n            if w:\n                while (\n                    (len(self.to_send) <= maximum_to_send)\n                    and (calls or self.preload_ids)\n                    and len(waiting_for) < MAX_INFLIGHT\n                ):\n                    if calls:\n                        args = calls[0]\n                        if cmd == \"get\" and args[\"id\"] in self.chunkid_to_msgids:\n                            # we have a get command and have already sent a request for this chunkid when\n                            # doing preloading, so we know the msgid of the response we are waiting for:\n                            waiting_for.append(pop_preload_msgid(args[\"id\"]))\n                            del calls[0]\n                        elif not is_preloaded:\n                            # make and send a request (already done if we are using preloading)\n                            self.msgid += 1\n                            waiting_for.append(self.msgid)\n                            del calls[0]\n                            self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: cmd, ARGS: args}))\n                    if not self.to_send and self.preload_ids:\n                        chunk_id = self.preload_ids.pop(0)\n                        # for preloading chunks, the raise_missing behaviour is defined HERE,\n                        # not in the get_many / fetch_many call that later fetches the preloaded chunks.\n                        args = {\"id\": chunk_id, \"raise_missing\": False}\n                        self.msgid += 1\n                        self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid)\n                        self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: \"get\", ARGS: args}))\n\n                send_buffer()\n        self.ignore_responses |= set(waiting_for)  # we lose order here\n\n    @api(since=parse_version(\"1.0.0\"), v1_or_v2={\"since\": parse_version(\"2.0.0b9\"), \"previously\": True})\n    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, v1_or_v2=False):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0a3\"))\n    def info(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"), max_duration={\"since\": parse_version(\"1.2.0a4\"), \"previously\": 0})\n    def check(self, repair=False, max_duration=0):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(\n        since=parse_version(\"1.0.0\"),\n        compact={\"since\": parse_version(\"1.2.0a0\"), \"previously\": True, \"dontcare\": True},\n        threshold={\"since\": parse_version(\"1.2.0a8\"), \"previously\": 0.1, \"dontcare\": True},\n    )\n    def commit(self, compact=True, threshold=0.1):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def rollback(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def destroy(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def __len__(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def list(self, limit=None, marker=None):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    def get(self, id, read_data=True, raise_missing=True):\n        for resp in self.get_many([id], read_data=read_data, raise_missing=raise_missing):\n            return resp\n\n    def get_many(self, ids, read_data=True, is_preloaded=False, raise_missing=True):\n        yield from self.call_many(\n            \"get\",\n            [{\"id\": id, \"read_data\": read_data, \"raise_missing\": raise_missing} for id in ids],\n            is_preloaded=is_preloaded,\n        )\n\n    @api(since=parse_version(\"1.0.0\"))\n    def put(self, id, data, wait=True):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def delete(self, id, wait=True):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def save_key(self, keydata):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def load_key(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"1.0.0\"))\n    def break_lock(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    def close(self):\n        if self.p or self.sock:\n            self.call(\"close\", {}, wait=True)\n        if self.p:\n            self.p.stdin.close()\n            self.p.stdout.close()\n            self.p.wait()\n            self.p = None\n        if self.sock:\n            try:\n                self.sock.shutdown(socket.SHUT_RDWR)\n            except OSError as e:\n                if e.errno != errno.ENOTCONN:\n                    raise\n            self.sock.close()\n            self.sock = None\n\n    def async_response(self, wait=True):\n        for resp in self.call_many(\"async_responses\", calls=[], wait=True, async_wait=wait):\n            return resp\n\n    def preload(self, ids):\n        self.preload_ids += ids\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def get_manifest(self):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def put_manifest(self, data):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"), deleted={\"since\": parse_version(\"2.0.0b14\"), \"previously\": False})\n    def store_list(self, name, *, deleted=False):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def store_load(self, name):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"))\n    def store_store(self, name, value):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b8\"), deleted={\"since\": parse_version(\"2.0.0b14\"), \"previously\": False})\n    def store_delete(self, name, *, deleted=False):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n    @api(since=parse_version(\"2.0.0b14\"))\n    def store_move(self, name, new_name=None, *, delete=False, undelete=False, deleted=False):\n        \"\"\"actual remoting is done via self.call in the @api decorator\"\"\"\n\n\nclass RepositoryNoCache:\n    \"\"\"A not caching Repository wrapper, passes through to repository.\n\n    Just to have same API (including the context manager) as RepositoryCache.\n\n    *transform* is a callable taking two arguments, key and raw repository data.\n    The return value is returned from get()/get_many(). By default, the raw\n    repository data is returned.\n    \"\"\"\n\n    def __init__(self, repository, transform=None):\n        self.repository = repository\n        self.transform = transform or (lambda key, data: data)\n\n    def close(self):\n        pass\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def get(self, key, read_data=True, raise_missing=True):\n        return next(self.get_many([key], read_data=read_data, raise_missing=raise_missing, cache=False))\n\n    def get_many(self, keys, read_data=True, raise_missing=True, cache=True):\n        for key, data in zip(keys, self.repository.get_many(keys, read_data=read_data, raise_missing=raise_missing)):\n            yield self.transform(key, data)\n\n    def log_instrumentation(self):\n        pass\n\n\nclass RepositoryCache(RepositoryNoCache):\n    \"\"\"\n    A caching Repository wrapper.\n\n    Caches Repository GET operations locally.\n\n    *pack* and *unpack* complement *transform* of the base class.\n    *pack* receives the output of *transform* and should return bytes,\n    which are stored in the cache. *unpack* receives these bytes and\n    should return the initial data (as returned by *transform*).\n    \"\"\"\n\n    def __init__(self, repository, pack=None, unpack=None, transform=None):\n        super().__init__(repository, transform)\n        self.pack = pack or (lambda data: data)\n        self.unpack = unpack or (lambda data: data)\n        self.cache = set()\n        self.basedir = tempfile.mkdtemp(prefix=\"borg-cache-\")\n        self.query_size_limit()\n        self.size = 0\n        # Instrumentation\n        self.hits = 0\n        self.misses = 0\n        self.slow_misses = 0\n        self.slow_lat = 0.0\n        self.evictions = 0\n        self.enospc = 0\n\n    def query_size_limit(self):\n        available_space = shutil.disk_usage(self.basedir).free\n        self.size_limit = int(min(available_space * 0.25, 2**31))\n\n    def prefixed_key(self, key, complete):\n        # just prefix another byte telling whether this key refers to a complete chunk\n        # or a without-data-metadata-only chunk (see also read_data param).\n        prefix = b\"\\x01\" if complete else b\"\\x00\"\n        return prefix + key\n\n    def key_filename(self, key):\n        return os.path.join(self.basedir, bin_to_hex(key))\n\n    def backoff(self):\n        self.query_size_limit()\n        target_size = int(0.9 * self.size_limit)\n        while self.size > target_size and self.cache:\n            key = self.cache.pop()\n            file = self.key_filename(key)\n            self.size -= os.stat(file).st_size\n            os.unlink(file)\n            self.evictions += 1\n\n    def add_entry(self, key, data, cache, complete):\n        transformed = self.transform(key, data)\n        if not cache:\n            return transformed\n        packed = self.pack(transformed)\n        pkey = self.prefixed_key(key, complete=complete)\n        file = self.key_filename(pkey)\n        try:\n            with open(file, \"wb\") as fd:\n                fd.write(packed)\n        except OSError as os_error:\n            try:\n                safe_unlink(file)\n            except FileNotFoundError:\n                pass  # open() could have failed as well\n            if os_error.errno == errno.ENOSPC:\n                self.enospc += 1\n                self.backoff()\n            else:\n                raise\n        else:\n            self.size += len(packed)\n            self.cache.add(pkey)\n            if self.size > self.size_limit:\n                self.backoff()\n        return transformed\n\n    def log_instrumentation(self):\n        logger.debug(\n            \"RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), \"\n            \"%d evictions, %d ENOSPC hit\",\n            len(self.cache),\n            format_file_size(self.size),\n            format_file_size(self.size_limit),\n            self.hits,\n            self.misses,\n            self.slow_misses,\n            self.slow_lat,\n            self.evictions,\n            self.enospc,\n        )\n\n    def close(self):\n        self.log_instrumentation()\n        self.cache.clear()\n        shutil.rmtree(self.basedir)\n\n    def get_many(self, keys, read_data=True, raise_missing=True, cache=True):\n        # It could use different cache keys depending on read_data and cache full vs. meta-only chunks.\n        unknown_keys = [key for key in keys if self.prefixed_key(key, complete=read_data) not in self.cache]\n        repository_iterator = zip(\n            unknown_keys, self.repository.get_many(unknown_keys, read_data=read_data, raise_missing=raise_missing)\n        )\n        for key in keys:\n            pkey = self.prefixed_key(key, complete=read_data)\n            if pkey in self.cache:\n                file = self.key_filename(pkey)\n                with open(file, \"rb\") as fd:\n                    self.hits += 1\n                    yield self.unpack(fd.read())\n            else:\n                for key_, data in repository_iterator:\n                    if key_ == key:\n                        transformed = self.add_entry(key, data, cache, complete=read_data)\n                        self.misses += 1\n                        yield transformed\n                        break\n                else:\n                    # slow path: eviction during this get_many removed this key from the cache\n                    t0 = time.perf_counter()\n                    data = self.repository.get(key, read_data=read_data, raise_missing=raise_missing)\n                    self.slow_lat += time.perf_counter() - t0\n                    transformed = self.add_entry(key, data, cache, complete=read_data)\n                    self.slow_misses += 1\n                    yield transformed\n        # Consume any pending requests\n        for _ in repository_iterator:\n            pass\n\n\ndef cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None, force_cache=False):\n    \"\"\"\n    Return a Repository(No)Cache for *repository*.\n\n    If *decrypted_cache* is a repo_objs object, then get and get_many will return a tuple\n    (csize, plaintext) instead of the actual data in the repository. The cache will\n    store decrypted data, which increases CPU efficiency (by avoiding repeatedly decrypting\n    and more importantly MAC and ID checking cached objects).\n    Internally, objects are compressed with LZ4.\n    \"\"\"\n    if decrypted_cache and (pack or unpack or transform):\n        raise ValueError(\"decrypted_cache and pack/unpack/transform are incompatible\")\n    elif decrypted_cache:\n        repo_objs = decrypted_cache\n        # 32 bit csize, 64 bit (8 byte) xxh64, 1 byte ctype, 1 byte clevel\n        cache_struct = struct.Struct(\"=I8sBB\")\n        compressor = Compressor(\"lz4\")\n\n        def pack(data):\n            csize, decrypted = data\n            meta, compressed = compressor.compress({}, decrypted)\n            return cache_struct.pack(csize, xxh64(compressed).digest(), meta[\"ctype\"], meta[\"clevel\"]) + compressed\n\n        def unpack(data):\n            data = memoryview(data)\n            csize, checksum, ctype, clevel = cache_struct.unpack(data[: cache_struct.size])\n            compressed = data[cache_struct.size :]\n            if checksum != xxh64(compressed).digest():\n                raise IntegrityError(\"detected corrupted data in metadata cache\")\n            meta = dict(ctype=ctype, clevel=clevel, csize=len(compressed))\n            _, decrypted = compressor.decompress(meta, compressed)\n            return csize, decrypted\n\n        def transform(id_, data):\n            meta, decrypted = repo_objs.parse(id_, data, ro_type=ROBJ_DONTCARE)\n            csize = meta.get(\"csize\", len(data))\n            return csize, decrypted\n\n    if isinstance(repository, RemoteRepository) or force_cache:\n        return RepositoryCache(repository, pack, unpack, transform)\n    else:\n        return RepositoryNoCache(repository, transform)\n"
  },
  {
    "path": "src/borg/repoobj.py",
    "content": "from collections import namedtuple\nfrom struct import Struct\n\nfrom xxhash import xxh64\n\nfrom .constants import *  # NOQA\nfrom .helpers import msgpack, workarounds\nfrom .helpers.errors import IntegrityError\nfrom .compress import Compressor, LZ4_COMPRESSOR, get_compressor\n\n# Workaround for lost passphrase or key in \"authenticated\" or \"authenticated-blake2\" mode\nAUTHENTICATED_NO_KEY = \"authenticated_no_key\" in workarounds\n\n\nclass RepoObj:\n    # Object header format includes size information for parsing the object into meta and data,\n    # as well as hashes to enable checking consistency without having the borg key.\n    obj_header = Struct(\"<II8s8s\")  # meta size (32b), data size (32b), meta hash (64b), data hash (64b)\n    ObjHeader = namedtuple(\"ObjHeader\", \"meta_size data_size meta_hash data_hash\")\n\n    @classmethod\n    def extract_crypted_data(cls, data: bytes) -> bytes:\n        # used for crypto type detection\n        hdr_size = cls.obj_header.size\n        hdr = cls.ObjHeader(*cls.obj_header.unpack(data[:hdr_size]))\n        return data[hdr_size + hdr.meta_size :]\n\n    def __init__(self, key):\n        self.key = key\n        # Some commands write new chunks (e.g. rename) but don't take a --compression argument. This duplicates\n        # the default used by those commands who do take a --compression argument.\n        self.compressor = LZ4_COMPRESSOR\n\n    def id_hash(self, data: bytes) -> bytes:\n        return self.key.id_hash(data)\n\n    def format(\n        self,\n        id: bytes,\n        meta: dict,\n        data: bytes,\n        compress: bool = True,\n        size: int = None,\n        ctype: int = None,\n        clevel: int = None,\n        ro_type: str = None,\n    ) -> bytes:\n        assert isinstance(ro_type, str)\n        assert ro_type != ROBJ_DONTCARE\n        meta[\"type\"] = ro_type\n        assert isinstance(id, bytes)\n        assert isinstance(meta, dict)\n        assert isinstance(data, (bytes, memoryview))\n        assert compress or size is not None and ctype is not None and clevel is not None\n        if compress:\n            assert size is None or size == len(data)\n            meta, data_compressed = self.compressor.compress(meta, data)\n        else:\n            assert isinstance(size, int)\n            meta[\"size\"] = size\n            assert isinstance(ctype, int)\n            meta[\"ctype\"] = ctype\n            assert isinstance(clevel, int)\n            meta[\"clevel\"] = clevel\n            data_compressed = data  # is already compressed, is NOT prefixed by type/level bytes\n            meta[\"csize\"] = len(data_compressed)\n        data_encrypted = self.key.encrypt(id, data_compressed)\n        meta_packed = msgpack.packb(meta)\n        meta_encrypted = self.key.encrypt(id, meta_packed)\n        hdr = self.ObjHeader(\n            len(meta_encrypted), len(data_encrypted), xxh64(meta_encrypted).digest(), xxh64(data_encrypted).digest()\n        )\n        hdr_packed = self.obj_header.pack(*hdr)\n        return hdr_packed + meta_encrypted + data_encrypted\n\n    def parse_meta(self, id: bytes, cdata: bytes, ro_type: str) -> dict:\n        # when calling parse_meta, enough cdata needs to be supplied to contain completely the\n        # meta_len_hdr and the encrypted, packed metadata. it is allowed to provide more cdata.\n        assert isinstance(id, bytes)\n        assert isinstance(cdata, bytes)\n        assert isinstance(ro_type, str)\n        obj = memoryview(cdata)\n        hdr_size = self.obj_header.size\n        hdr = self.ObjHeader(*self.obj_header.unpack(obj[:hdr_size]))\n        assert hdr_size + hdr.meta_size <= len(obj)\n        meta_encrypted = obj[hdr_size : hdr_size + hdr.meta_size]\n        meta_packed = self.key.decrypt(id, meta_encrypted)\n        meta = msgpack.unpackb(meta_packed)\n        if ro_type != ROBJ_DONTCARE and meta[\"type\"] != ro_type:\n            raise IntegrityError(f\"ro_type expected: {ro_type} got: {meta['type']}\")\n        return meta\n\n    def parse(\n        self, id: bytes, cdata: bytes, decompress: bool = True, want_compressed: bool = False, ro_type: str = None\n    ) -> tuple[dict, bytes]:\n        \"\"\"\n        Parse a repo object into metadata and data (decrypt it, maybe decompress, maybe verify if the chunk plaintext\n        corresponds to the chunk id via assert_id()).\n\n        Tweaking options (default is usually fine):\n        - decompress=True, want_compressed=False: slow, verifying. returns decompressed data (default).\n        - decompress=True, want_compressed=True: slow, verifying. returns compressed data (caller wants to reuse it).\n        - decompress=False, want_compressed=True: quick, not verifying. returns compressed data (caller wants to reuse).\n        - decompress=False, want_compressed=False: invalid\n        \"\"\"\n        assert isinstance(ro_type, str)\n        assert not (not decompress and not want_compressed), \"invalid parameter combination!\"\n        assert isinstance(id, bytes)\n        assert isinstance(cdata, bytes)\n        obj = memoryview(cdata)\n        hdr_size = self.obj_header.size\n        hdr = self.ObjHeader(*self.obj_header.unpack(obj[:hdr_size]))\n        assert hdr_size + hdr.meta_size <= len(obj)\n        meta_encrypted = obj[hdr_size : hdr_size + hdr.meta_size]\n        meta_packed = self.key.decrypt(id, meta_encrypted)\n        meta_compressed = msgpack.unpackb(meta_packed)  # means: before adding more metadata in decompress block\n        if ro_type != ROBJ_DONTCARE and meta_compressed[\"type\"] != ro_type:\n            raise IntegrityError(f\"ro_type expected: {ro_type} got: {meta_compressed['type']}\")\n        assert hdr_size + hdr.meta_size + hdr.data_size <= len(obj)\n        data_encrypted = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size]\n        data_compressed = self.key.decrypt(id, data_encrypted)  # does not include the type/level bytes\n        if decompress:\n            ctype = meta_compressed[\"ctype\"]\n            clevel = meta_compressed[\"clevel\"]\n            csize = meta_compressed[\"csize\"]  # always the overall size\n            assert csize == len(data_compressed)\n            psize = meta_compressed.get(\n                \"psize\", csize\n            )  # obfuscation: psize (payload size) is potentially less than csize.\n            assert psize <= csize\n            compr_hdr = bytes((ctype, clevel))\n            compressor_cls, compression_level = Compressor.detect(compr_hdr)\n            compressor = compressor_cls(level=compression_level)\n            meta, data = compressor.decompress(dict(meta_compressed), data_compressed[:psize])\n            if not AUTHENTICATED_NO_KEY:\n                self.key.assert_id(id, data)\n        else:\n            meta, data = None, None\n        return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data\n\n\nclass RepoObj1:  # legacy\n    @classmethod\n    def extract_crypted_data(cls, data: bytes) -> bytes:\n        # used for crypto type detection\n        return data\n\n    def __init__(self, key):\n        self.key = key\n        self.compressor = get_compressor(\"lz4\", legacy_mode=True)\n\n    def id_hash(self, data: bytes) -> bytes:\n        return self.key.id_hash(data)\n\n    def format(\n        self,\n        id: bytes,\n        meta: dict,\n        data: bytes,\n        compress: bool = True,\n        size: int = None,\n        ctype: int = None,\n        clevel: int = None,\n        ro_type: str = None,\n    ) -> bytes:\n        assert isinstance(id, bytes)\n        assert meta == {}\n        assert isinstance(data, (bytes, memoryview))\n        assert ro_type is not None\n        assert compress or size is not None and ctype is not None and clevel is not None\n        if compress:\n            assert size is None or size == len(data)\n            meta, data_compressed = self.compressor.compress(meta, data)\n        else:\n            assert isinstance(size, int)\n            data_compressed = data  # is already compressed, must include type/level bytes\n        data_encrypted = self.key.encrypt(id, data_compressed)\n        return data_encrypted\n\n    def parse_meta(self, id: bytes, cdata: bytes) -> dict:\n        raise NotImplementedError(\"parse_meta is not available for RepoObj1\")\n\n    def parse(\n        self, id: bytes, cdata: bytes, decompress: bool = True, want_compressed: bool = False, ro_type: str = None\n    ) -> tuple[dict, bytes]:\n        assert not (not decompress and not want_compressed), \"invalid parameter combination!\"\n        assert isinstance(id, bytes)\n        assert isinstance(cdata, bytes)\n        assert ro_type is not None\n        data_compressed = self.key.decrypt(id, cdata)\n        compressor_cls, compression_level = Compressor.detect(data_compressed[:2])\n        compressor = compressor_cls(level=compression_level, legacy_mode=True)\n        meta_compressed = {}\n        meta_compressed[\"ctype\"] = compressor.ID\n        meta_compressed[\"clevel\"] = compressor.level\n        meta_compressed[\"csize\"] = len(data_compressed)\n        if decompress:\n            meta, data = compressor.decompress(None, data_compressed)\n            if not AUTHENTICATED_NO_KEY:\n                self.key.assert_id(id, data)\n        else:\n            meta, data = None, None\n        return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data\n"
  },
  {
    "path": "src/borg/repository.py",
    "content": "import os\nimport time\nfrom pathlib import Path\n\nfrom xxhash import xxh64\n\nfrom borgstore.store import Store\nfrom borgstore.store import ObjectNotFound as StoreObjectNotFound\nfrom borgstore.backends.errors import BackendError as StoreBackendError\nfrom borgstore.backends.errors import BackendDoesNotExist as StoreBackendDoesNotExist\nfrom borgstore.backends.errors import BackendAlreadyExists as StoreBackendAlreadyExists\n\nfrom .constants import *  # NOQA\nfrom .hashindex import ChunkIndex, ChunkIndexEntry\nfrom .helpers import Error, ErrorWithTraceback, IntegrityError\nfrom .helpers import Location\nfrom .helpers import bin_to_hex, hex_to_bin\nfrom .storelocking import Lock\nfrom .logger import create_logger\nfrom .manifest import NoManifestError\nfrom .repoobj import RepoObj\n\nlogger = create_logger(__name__)\n\n\ndef repo_lister(repository, *, limit=None):\n    marker = None\n    finished = False\n    while not finished:\n        result = repository.list(limit=limit, marker=marker)\n        finished = (len(result) < limit) if limit is not None else (len(result) == 0)\n        if not finished:\n            marker = result[-1][0]\n        yield from result\n\n\nclass Repository:\n    \"\"\"borgstore-based key/value store.\"\"\"\n\n    class AlreadyExists(Error):\n        \"\"\"A repository already exists at {}.\"\"\"\n\n        exit_mcode = 10\n\n    class CheckNeeded(ErrorWithTraceback):\n        \"\"\"Inconsistency detected. Please run \"borg check {}\".\"\"\"\n\n        exit_mcode = 12\n\n    class DoesNotExist(Error):\n        \"\"\"Repository {} does not exist.\"\"\"\n\n        exit_mcode = 13\n\n    class InsufficientFreeSpaceError(Error):\n        \"\"\"Insufficient free space to complete the transaction (required: {}, available: {}).\"\"\"\n\n        exit_mcode = 14\n\n    class InvalidRepository(Error):\n        \"\"\"{} is not a valid repository. Check the repository config.\"\"\"\n\n        exit_mcode = 15\n\n    class InvalidRepositoryConfig(Error):\n        \"\"\"{} does not have a valid config. Check the repository config [{}].\"\"\"\n\n        exit_mcode = 16\n\n    class ObjectNotFound(ErrorWithTraceback):\n        \"\"\"Object with key {} not found in repository {}.\"\"\"\n\n        exit_mcode = 17\n\n        def __init__(self, id, repo):\n            if isinstance(id, bytes):\n                id = bin_to_hex(id)\n            super().__init__(id, repo)\n\n    class ParentPathDoesNotExist(Error):\n        \"\"\"The parent path of the repository directory [{}] does not exist.\"\"\"\n\n        exit_mcode = 18\n\n    class PathAlreadyExists(Error):\n        \"\"\"There is already something at {}.\"\"\"\n\n        exit_mcode = 19\n\n    # StorageQuotaExceeded was exit_mcode = 20\n\n    class PathPermissionDenied(Error):\n        \"\"\"Permission denied to {}.\"\"\"\n\n        exit_mcode = 21\n\n    def __init__(\n        self,\n        path_or_location,\n        create=False,\n        exclusive=False,\n        lock_wait=1.0,\n        lock=True,\n        send_log_cb=None,\n        permissions=None,\n    ):\n        if isinstance(path_or_location, Location):\n            location = path_or_location\n            if location.proto == \"file\":\n                url = Path(location.path).as_uri()\n            else:\n                url = location.processed  # location as given by user, processed placeholders\n        else:\n            url = Path(path_or_location).absolute().as_uri()\n            location = Location(url)\n        self._location = location\n        self.url = url\n        # lots of stuff in data: use 2 levels by default (data/00/00/ .. data/ff/ff/ dirs)!\n        data_levels = int(os.environ.get(\"BORG_STORE_DATA_LEVELS\", \"2\"))\n        levels_config = {\n            \"archives/\": [0],\n            \"cache/\": [0],\n            \"config/\": [0],\n            \"data/\": [data_levels],\n            \"keys/\": [0],\n            \"locks/\": [0],\n        }\n        # Get permissions from parameter or environment variable\n        permissions = permissions if permissions is not None else os.environ.get(\"BORG_REPO_PERMISSIONS\", \"all\")\n\n        if permissions == \"all\":\n            permissions = None  # permissions system will not be used\n        elif permissions == \"no-delete\":  # mostly no delete, no overwrite\n            permissions = {\n                \"\": \"lr\",\n                \"archives\": \"lrw\",\n                \"cache\": \"lrwWD\",  # WD for chunks.<HASH>, last-key-checked, ...\n                \"config\": \"lrW\",  # W for manifest\n                \"data\": \"lrw\",\n                \"keys\": \"lr\",\n                \"locks\": \"lrwD\",  # borg needs to create/delete a shared lock here\n            }\n        elif permissions == \"write-only\":  # mostly no reading\n            permissions = {\n                \"\": \"l\",\n                \"archives\": \"lw\",\n                \"cache\": \"lrwWD\",  # read allowed, e.g. for chunks.<HASH> cache\n                \"config\": \"lrW\",  # W for manifest\n                \"data\": \"lw\",  # no r!\n                \"keys\": \"lr\",\n                \"locks\": \"lrwD\",  # borg needs to create/delete a shared lock here\n            }\n        elif permissions == \"read-only\":  # mostly r/o\n            permissions = {\"\": \"lr\", \"locks\": \"lrwD\"}\n        else:\n            raise Error(\n                f\"Invalid BORG_REPO_PERMISSIONS value: {permissions}, should be one of: \"\n                f\"all, no-delete, write-only, read-only.\"\n            )\n\n        try:\n            self.store = Store(url, levels=levels_config, permissions=permissions)\n        except StoreBackendError as e:\n            raise Error(str(e))\n        self.store_opened = False\n        self.version = None\n        # long-running repository methods which emit log or progress output are responsible for calling\n        # the ._send_log method periodically to get log and progress output transferred to the borg client\n        # in a timely manner, in case we have a RemoteRepository.\n        # for local repositories ._send_log can be called also (it will just do nothing in that case).\n        self._send_log = send_log_cb or (lambda: None)\n        self.do_create = create\n        self.created = False\n        self.acceptable_repo_versions = (3,)\n        self.opened = False\n        self.lock = None\n        self.do_lock = lock\n        self.lock_wait = lock_wait\n        self.exclusive = exclusive\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {self._location}>\"\n\n    def __enter__(self):\n        if self.do_create:\n            self.do_create = False\n            self.create()\n            self.created = True\n        try:\n            self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n        except Exception:\n            self.close()\n            raise\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    @property\n    def id_str(self):\n        return bin_to_hex(self.id)\n\n    def create(self):\n        \"\"\"Create a new empty repository\"\"\"\n        try:\n            self.store.create()\n        except StoreBackendAlreadyExists:\n            raise self.AlreadyExists(self.url)\n        self.store.open()\n        try:\n            self.store.store(\"config/readme\", REPOSITORY_README.encode())\n            self.version = 3\n            self.store.store(\"config/version\", str(self.version).encode())\n            self.store.store(\"config/id\", bin_to_hex(os.urandom(32)).encode())\n            # we know repo/data/ still does not have any chunks stored in it,\n            # but for some stores, there might be a lot of empty directories and\n            # listing them all might be rather slow, so we better cache an empty\n            # ChunkIndex from here so that the first repo operation does not have\n            # to build the ChunkIndex the slow way by listing all the directories.\n            from borg.cache import write_chunkindex_to_repo_cache\n\n            write_chunkindex_to_repo_cache(self, ChunkIndex(), clear=True, force_write=True)\n        finally:\n            self.store.close()\n\n    def _set_id(self, id):\n        # for testing: change the id of an existing repository\n        assert self.opened\n        assert isinstance(id, bytes) and len(id) == 32\n        self.id = id\n        self.store.store(\"config/id\", bin_to_hex(id).encode())\n\n    def _lock_refresh(self):\n        if self.lock is not None:\n            self.lock.refresh()\n\n    def save_key(self, keydata):\n        # note: saving an empty key means that there is no repokey anymore\n        self.store.store(\"keys/repokey\", keydata)\n\n    def load_key(self):\n        keydata = self.store.load(\"keys/repokey\")\n        # note: if we return an empty string, it means there is no repo key\n        return keydata\n\n    def destroy(self):\n        \"\"\"Destroy the repository\"\"\"\n        self.close()\n        self.store.destroy()\n\n    def open(self, *, exclusive, lock_wait=None, lock=True):\n        assert lock_wait is not None\n        try:\n            self.store.open()\n        except StoreBackendDoesNotExist:\n            raise self.DoesNotExist(str(self._location)) from None\n        else:\n            self.store_opened = True\n        try:\n            readme = self.store.load(\"config/readme\").decode()\n        except StoreObjectNotFound:\n            raise self.DoesNotExist(str(self._location)) from None\n        if readme != REPOSITORY_README:\n            raise self.InvalidRepository(str(self._location))\n        self.version = int(self.store.load(\"config/version\").decode())\n        if self.version not in self.acceptable_repo_versions:\n            self.close()\n            raise self.InvalidRepositoryConfig(\n                str(self._location), \"repository version %d is not supported by this borg version\" % self.version\n            )\n        self.id = hex_to_bin(self.store.load(\"config/id\").decode(), length=32)\n        # important: lock *after* making sure that there actually is an existing, supported repository.\n        if lock:\n            self.lock = Lock(self.store, exclusive, timeout=lock_wait).acquire()\n        self.opened = True\n\n    def close(self):\n        if self.lock:\n            self.lock.release()\n            self.lock = None\n        if self.store_opened:\n            self.store.close()\n            self.store_opened = False\n        self.opened = False\n\n    def info(self):\n        \"\"\"return some infos about the repo (must be opened first)\"\"\"\n        # note: don't do anything expensive here or separate the lock refresh into a separate method.\n        self._lock_refresh()  # do not remove, see do_with_lock()\n        info = dict(id=self.id, version=self.version)\n        return info\n\n    def check(self, repair=False, max_duration=0):\n        \"\"\"Check repository consistency\"\"\"\n\n        def log_error(msg):\n            nonlocal obj_corrupted\n            obj_corrupted = True\n            logger.error(f\"Repo object {info.name} is corrupted: {msg}\")\n\n        def check_object(obj):\n            \"\"\"Check if obj looks valid.\"\"\"\n            hdr_size = RepoObj.obj_header.size\n            obj_size = len(obj)\n            if obj_size >= hdr_size:\n                hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size]))\n                meta = obj[hdr_size : hdr_size + hdr.meta_size]\n                if hdr.meta_size != len(meta):\n                    log_error(\"metadata size incorrect.\")\n                elif hdr.meta_hash != xxh64(meta).digest():\n                    log_error(\"metadata does not match checksum.\")\n                data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size]\n                if hdr.data_size != len(data):\n                    log_error(\"data size incorrect.\")\n                elif hdr.data_hash != xxh64(data).digest():\n                    log_error(\"data does not match checksum.\")\n            else:\n                log_error(\"too small.\")\n\n        # TODO: progress indicator, ...\n        partial = bool(max_duration)\n        assert not (repair and partial)\n        mode = \"partial\" if partial else \"full\"\n        LAST_KEY_CHECKED = \"cache/last-key-checked\"\n        logger.info(f\"Starting {mode} repository check\")\n        if partial:\n            # continue a past partial check (if any) or from a checkpoint or start one from beginning\n            try:\n                last_key_checked = self.store.load(LAST_KEY_CHECKED).decode()\n            except StoreObjectNotFound:\n                last_key_checked = \"\"\n        else:\n            # start from the beginning and also forget about any potential past partial checks\n            last_key_checked = \"\"\n            try:\n                self.store.delete(LAST_KEY_CHECKED)\n            except StoreObjectNotFound:\n                pass\n        if last_key_checked:\n            logger.info(f\"Skipping to keys after {last_key_checked}.\")\n        else:\n            logger.info(\"Starting from beginning.\")\n        t_start = time.monotonic()\n        t_last_checkpoint = t_start\n        objs_checked = objs_errors = 0\n        chunks = ChunkIndex()\n        # we don't do refcounting anymore, neither we can know here whether any archive\n        # is using this object, but we assume that this is the case.\n        # As we don't do garbage collection here, this is not a problem.\n        # We also don't know the plaintext size, so we set it to 0.\n        init_entry = ChunkIndexEntry(flags=ChunkIndex.F_USED, size=0)\n        infos = self.store.list(\"data\")\n        try:\n            for info in infos:\n                self._lock_refresh()\n                key = \"data/%s\" % info.name\n                if key <= last_key_checked:  # needs sorted keys\n                    continue\n                try:\n                    obj = self.store.load(key)\n                except StoreObjectNotFound:\n                    # looks like object vanished since store.list(), ignore that.\n                    continue\n                obj_corrupted = False\n                check_object(obj)\n                objs_checked += 1\n                if obj_corrupted:\n                    objs_errors += 1\n                    if repair:\n                        # if it is corrupted, we can't do much except getting rid of it.\n                        # but let's just retry loading it, in case the error goes away.\n                        try:\n                            obj = self.store.load(key)\n                        except StoreObjectNotFound:\n                            log_error(\"existing object vanished.\")\n                        else:\n                            obj_corrupted = False\n                            check_object(obj)\n                            if obj_corrupted:\n                                log_error(\"reloading did not help, deleting it!\")\n                                self.store.delete(key)\n                            else:\n                                log_error(\"reloading did help, inconsistent behaviour detected!\")\n                if not (obj_corrupted and repair):\n                    # add all existing objects to the index.\n                    # borg check: the index may have corrupted objects (we did not delete them)\n                    # borg check --repair: the index will only have non-corrupted objects.\n                    id = hex_to_bin(info.name)\n                    chunks[id] = init_entry\n                now = time.monotonic()\n                if now > t_last_checkpoint + 300:  # checkpoint every 5 mins\n                    t_last_checkpoint = now\n                    logger.info(f\"Checkpointing at key {key}.\")\n                    self.store.store(LAST_KEY_CHECKED, key.encode())\n                if partial and now > t_start + max_duration:\n                    logger.info(f\"Finished partial repository check, last key checked is {key}.\")\n                    self.store.store(LAST_KEY_CHECKED, key.encode())\n                    break\n            else:\n                logger.info(\"Finished repository check.\")\n                try:\n                    self.store.delete(LAST_KEY_CHECKED)\n                except StoreObjectNotFound:\n                    pass\n                if not partial:\n                    # if we did a full pass in one go, we built a complete, up-to-date ChunkIndex, cache it!\n                    from .cache import write_chunkindex_to_repo_cache\n\n                    write_chunkindex_to_repo_cache(\n                        self, chunks, incremental=False, clear=True, force_write=True, delete_other=True\n                    )\n        except StoreObjectNotFound:\n            # it can be that there is no \"data/\" at all, then it crashes when iterating infos.\n            pass\n        logger.info(f\"Checked {objs_checked} repository objects, {objs_errors} errors.\")\n        if objs_errors == 0:\n            logger.info(f\"Finished {mode} repository check, no problems found.\")\n        else:\n            if repair:\n                logger.info(f\"Finished {mode} repository check, errors found and repaired.\")\n            else:\n                logger.error(f\"Finished {mode} repository check, errors found.\")\n        return objs_errors == 0 or repair\n\n    def list(self, limit=None, marker=None):\n        \"\"\"\n        list <limit> infos starting from after id <marker>.\n        each info is a tuple (id, storage_size).\n        \"\"\"\n        collect = True if marker is None else False\n        result = []\n        infos = self.store.list(\"data\")  # generator yielding ItemInfos\n        while True:\n            self._lock_refresh()\n            try:\n                info = next(infos)\n            except StoreObjectNotFound:\n                break  # can happen e.g. if \"data\" does not exist, pointless to continue in that case\n            except StopIteration:\n                break\n            else:\n                id = hex_to_bin(info.name)\n                if collect:\n                    result.append((id, info.size))\n                    if len(result) == limit:\n                        break\n                elif id == marker:\n                    collect = True\n                    # note: do not collect the marker id\n        return result\n\n    def get(self, id, read_data=True, raise_missing=True):\n        self._lock_refresh()\n        id_hex = bin_to_hex(id)\n        key = \"data/\" + id_hex\n        try:\n            if read_data:\n                # read everything\n                return self.store.load(key)\n            else:\n                # RepoObj layout supports separately encrypted metadata and data.\n                # We return enough bytes so the client can decrypt the metadata.\n                hdr_size = RepoObj.obj_header.size\n                extra_size = 1024 - hdr_size  # load a bit more, 1024b, reduces round trips\n                obj = self.store.load(key, size=hdr_size + extra_size)\n                hdr = obj[0:hdr_size]\n                if len(hdr) != hdr_size:\n                    raise IntegrityError(f\"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes\")\n                meta_size = RepoObj.obj_header.unpack(hdr)[0]\n                if meta_size > extra_size:\n                    # we did not get enough, need to load more, but not all.\n                    # this should be rare, as chunk metadata is rather small usually.\n                    obj = self.store.load(key, size=hdr_size + meta_size)\n                meta = obj[hdr_size : hdr_size + meta_size]\n                if len(meta) != meta_size:\n                    raise IntegrityError(f\"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes\")\n                return hdr + meta\n        except StoreObjectNotFound:\n            if raise_missing:\n                raise self.ObjectNotFound(id, str(self._location)) from None\n            else:\n                return None\n\n    def get_many(self, ids, read_data=True, is_preloaded=False, raise_missing=True):\n        for id_ in ids:\n            yield self.get(id_, read_data=read_data, raise_missing=raise_missing)\n\n    def put(self, id, data, wait=True):\n        \"\"\"put a repo object\n\n        Note: when doing calls with wait=False this gets async and caller must\n              deal with async results / exceptions later.\n        \"\"\"\n        self._lock_refresh()\n        data_size = len(data)\n        if data_size > MAX_DATA_SIZE:\n            raise IntegrityError(f\"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]\")\n\n        key = \"data/\" + bin_to_hex(id)\n        self.store.store(key, data)\n\n    def delete(self, id, wait=True):\n        \"\"\"delete a repo object\n\n        Note: when doing calls with wait=False this gets async and caller must\n              deal with async results / exceptions later.\n        \"\"\"\n        self._lock_refresh()\n        key = \"data/\" + bin_to_hex(id)\n        try:\n            self.store.delete(key)\n        except StoreObjectNotFound:\n            raise self.ObjectNotFound(id, str(self._location)) from None\n\n    def async_response(self, wait=True):\n        \"\"\"Get one async result (only applies to remote repositories).\n\n        async commands (== calls with wait=False, e.g. delete and put) have no results,\n        but may raise exceptions. These async exceptions must get collected later via\n        async_response() calls. Repeat the call until it returns None.\n        The previous calls might either return one (non-None) result or raise an exception.\n        If wait=True is given and there are outstanding responses, it will wait for them\n        to arrive. With wait=False, it will only return already received responses.\n        \"\"\"\n\n    def preload(self, ids):\n        \"\"\"Preload objects (only applies to remote repositories)\"\"\"\n\n    def break_lock(self):\n        Lock(self.store).break_lock()\n\n    def migrate_lock(self, old_id, new_id):\n        # note: only needed for local repos\n        if self.lock is not None:\n            self.lock.migrate_lock(old_id, new_id)\n\n    def get_manifest(self):\n        self._lock_refresh()\n        try:\n            return self.store.load(\"config/manifest\")\n        except StoreObjectNotFound:\n            raise NoManifestError\n\n    def put_manifest(self, data):\n        self._lock_refresh()\n        return self.store.store(\"config/manifest\", data)\n\n    def store_list(self, name, *, deleted=False):\n        self._lock_refresh()\n        try:\n            return list(self.store.list(name, deleted=deleted))\n        except StoreObjectNotFound:\n            return []\n\n    def store_load(self, name):\n        self._lock_refresh()\n        return self.store.load(name)\n\n    def store_store(self, name, value):\n        self._lock_refresh()\n        return self.store.store(name, value)\n\n    def store_delete(self, name, *, deleted=False):\n        self._lock_refresh()\n        return self.store.delete(name, deleted=deleted)\n\n    def store_move(self, name, new_name=None, *, delete=False, undelete=False, deleted=False):\n        self._lock_refresh()\n        return self.store.move(name, new_name, delete=delete, undelete=undelete, deleted=deleted)\n"
  },
  {
    "path": "src/borg/selftest.py",
    "content": "# Note: these tests are part of the self test, do not use or import pytest functionality here.\n#       See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT\n\n\"\"\"\nSelf-testing module\n===================\n\nThe selftest() function runs a small test suite of relatively fast tests that are meant to discover issues\nwith how Borg was compiled or packaged, as well as bugs in Borg itself.\n\nThese tests are a subset of borg/testsuite and are run with Python's built-in unittest; therefore none of\nthese tests can or should be ported to pytest at this time.\n\nTo ensure that self-test discovery works correctly, the expected number of tests is stored in\nSELFTEST_COUNT. Update SELFTEST_COUNT whenever tests used here are added or removed.\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom unittest import TestResult, TestSuite, defaultTestLoader\n\nfrom .testsuite.crypto.crypto_test import CryptoTestCase\nfrom .testsuite.chunkers.buzhash_self_test import ChunkerTestCase\nfrom .testsuite.chunkers.fixed_self_test import ChunkerFixedTestCase\n\nSELFTEST_CASES = [CryptoTestCase, ChunkerTestCase, ChunkerFixedTestCase]\n\nSELFTEST_COUNT = 17\n\n\nclass SelfTestResult(TestResult):\n    def __init__(self):\n        super().__init__()\n        self.successes = []\n\n    def addSuccess(self, test):\n        super().addSuccess(test)\n        self.successes.append(test)\n\n    def test_name(self, test):\n        return test.shortDescription() or str(test)\n\n    def log_results(self, logger):\n        for test, failure in self.errors + self.failures + self.unexpectedSuccesses:\n            logger.error(\"self-test %s FAILED:\\n%s\", self.test_name(test), failure)\n        for test, reason in self.skipped:\n            logger.warning(\"self-test %s skipped: %s\", self.test_name(test), reason)\n\n    def successful_test_count(self):\n        return len(self.successes)\n\n\ndef selftest(logger):\n    if os.environ.get(\"BORG_SELFTEST\") == \"disabled\":\n        logger.debug(\"Borg self-test disabled via BORG_SELFTEST environment variable\")\n        return\n    selftest_started = time.perf_counter()\n    result = SelfTestResult()\n    test_suite = TestSuite()\n    for test_case in SELFTEST_CASES:\n        module = sys.modules[test_case.__module__]\n        # a normal borg user does not have pytest installed, we must not require it in the test modules used here.\n        # note: this only detects the usual toplevel import\n        assert \"pytest\" not in dir(module), \"pytest must not be imported in %s\" % module.__name__\n        test_suite.addTest(defaultTestLoader.loadTestsFromTestCase(test_case))\n    test_suite.run(result)\n    result.log_results(logger)\n    successful_tests = result.successful_test_count()\n    count_mismatch = successful_tests != SELFTEST_COUNT\n    if result.wasSuccessful() and count_mismatch:\n        # only print this if all tests succeeded\n        logger.error(\n            \"Self-test count (%d != %d) mismatch; either test discovery is broken, or a test was added \"\n            \"without updating borg.selftest\",\n            successful_tests,\n            SELFTEST_COUNT,\n        )\n    if not result.wasSuccessful() or count_mismatch:\n        logger.error(\n            \"Self-test failed.\\n\"\n            \"This could be a bug in Borg, the package/distribution you use, your OS, or your hardware.\"\n        )\n        sys.exit(2)\n        assert False, \"sanity assertion failed: ran beyond sys.exit()\"\n    selftest_elapsed = time.perf_counter() - selftest_started\n    logger.debug(\"%d self-tests completed in %.2f seconds\", successful_tests, selftest_elapsed)\n"
  },
  {
    "path": "src/borg/storelocking.py",
    "content": "import datetime\nimport json\nimport random\nimport time\n\nfrom xxhash import xxh64\n\nfrom borgstore.store import ObjectNotFound\n\nfrom . import platform\nfrom .helpers import Error, ErrorWithTraceback\nfrom .logger import create_logger\n\nlogger = create_logger(__name__)\n\n\nclass LockError(Error):\n    \"\"\"Failed to acquire the lock {}.\"\"\"\n\n    exit_mcode = 70\n\n\nclass LockErrorT(ErrorWithTraceback):\n    \"\"\"Failed to acquire the lock {}.\"\"\"\n\n    exit_mcode = 71\n\n\nclass LockFailed(LockErrorT):\n    \"\"\"Failed to create/acquire the lock {} ({}).\"\"\"\n\n    exit_mcode = 72\n\n\nclass LockTimeout(LockError):\n    \"\"\"Failed to create/acquire the lock {} (timeout).\"\"\"\n\n    exit_mcode = 73\n\n\nclass NotLocked(LockErrorT):\n    \"\"\"Failed to release the lock {} (was not locked).\"\"\"\n\n    exit_mcode = 74\n\n\nclass NotMyLock(LockErrorT):\n    \"\"\"Failed to release the lock {} (was/is locked, but not by me).\"\"\"\n\n    exit_mcode = 75\n\n\nclass Lock:\n    \"\"\"\n    A lock for a resource that can be accessed in a shared or exclusive way.\n\n    Typically, write access to a resource needs an exclusive lock (one writer,\n    no readers allowed), and read access to a resource needs a shared lock\n    (multiple readers are allowed).\n\n    If possible, use the context manager form::\n\n        with Lock(...) as lock:\n            ...\n\n    This ensures the lock is released when the block is exited, no matter how\n    (e.g., if an exception occurs).\n    \"\"\"\n\n    def __init__(self, store, exclusive=False, sleep=None, timeout=1.0, stale=30 * 60, id=None):\n        self.store = store\n        self.is_exclusive = exclusive\n        self.sleep = sleep\n        self.timeout = timeout\n        self.race_recheck_delay = 0.01  # local: 0.01, network/slow remote: >= 1.0\n        self.other_locks_go_away_delay = 0.1  # local: 0.1, network/slow remote: >= 1.0\n        self.retry_delay_min = 1.0\n        self.retry_delay_max = 5.0\n        self.stale_td = datetime.timedelta(seconds=stale)  # ignore/delete it if older\n        self.refresh_td = datetime.timedelta(seconds=stale // 2)  # don't refresh it if younger\n        self.last_refresh_dt = None\n        self.id = id or platform.get_process_id()\n        assert len(self.id) == 3\n        logger.debug(f\"LOCK-INIT: initializing. store: {store}, stale: {stale}s, refresh: {stale // 2}s.\")\n\n    def __enter__(self):\n        return self.acquire()\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        ignore_not_found = exc_type is not None\n        # if there was an exception, try to release the lock,\n        # but don't raise another exception while trying if it was not there.\n        self.release(ignore_not_found=ignore_not_found)\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__}: {self.id!r}>\"\n\n    def _create_lock(self, *, exclusive=None, update_last_refresh=False):\n        assert exclusive is not None\n        now = datetime.datetime.now(datetime.timezone.utc)\n        timestamp = now.isoformat(timespec=\"milliseconds\")\n        lock = dict(exclusive=exclusive, hostid=self.id[0], processid=self.id[1], threadid=self.id[2], time=timestamp)\n        value = json.dumps(lock).encode(\"utf-8\")\n        key = xxh64(value).hexdigest()\n        logger.debug(f\"LOCK-CREATE: creating lock in store. key: {key}, lock: {lock}.\")\n        self.store.store(f\"locks/{key}\", value)\n        if update_last_refresh:\n            # we parse the timestamp string to get *precisely* the datetime in the lock:\n            self.last_refresh_dt = datetime.datetime.fromisoformat(timestamp)\n        return key\n\n    def _delete_lock(self, key, *, ignore_not_found=False, update_last_refresh=False):\n        logger.debug(f\"LOCK-DELETE: deleting lock from store. key: {key}.\")\n        try:\n            self.store.delete(f\"locks/{key}\")\n        except ObjectNotFound:\n            if not ignore_not_found:\n                raise\n        finally:\n            if update_last_refresh:\n                self.last_refresh_dt = None\n\n    def _is_our_lock(self, lock):\n        return self.id == (lock[\"hostid\"], lock[\"processid\"], lock[\"threadid\"])\n\n    def _is_stale_lock(self, lock):\n        now = datetime.datetime.now(datetime.timezone.utc)\n        if now > lock[\"dt\"] + self.stale_td:\n            logger.debug(f\"LOCK-STALE: lock is too old, it was not refreshed. lock: {lock}.\")\n            return True\n        if not platform.process_alive(lock[\"hostid\"], lock[\"processid\"], lock[\"threadid\"]):\n            logger.debug(f\"LOCK-STALE: we KNOW that the lock-owning process is dead. lock: {lock}.\")\n            return True\n        return False\n\n    def _get_locks(self):\n        locks = {}\n        try:\n            infos = list(self.store.list(\"locks\"))\n        except ObjectNotFound:\n            return {}\n        for info in infos:\n            key = info.name\n            content = self.store.load(f\"locks/{key}\")\n            lock = json.loads(content.decode(\"utf-8\"))\n            lock[\"key\"] = key\n            lock[\"dt\"] = datetime.datetime.fromisoformat(lock[\"time\"])\n            if self._is_stale_lock(lock):\n                # ignore it and delete it (even if it is not from us)\n                self._delete_lock(key, ignore_not_found=True, update_last_refresh=self._is_our_lock(lock))\n            else:\n                locks[key] = lock\n        return locks\n\n    def _find_locks(self, *, only_exclusive=False, only_mine=False):\n        locks = self._get_locks()\n        found_locks = []\n        for key in locks:\n            lock = locks[key]\n            if (not only_exclusive or lock[\"exclusive\"]) and (\n                not only_mine or (lock[\"hostid\"], lock[\"processid\"], lock[\"threadid\"]) == self.id\n            ):\n                found_locks.append(lock)\n        return found_locks\n\n    def acquire(self):\n        # goal\n        # for exclusive lock: there must be only 1 exclusive lock and no other (exclusive or non-exclusive) locks.\n        # for non-exclusive lock: there can be multiple n-e locks, but there must not exist an exclusive lock.\n        logger.debug(f\"LOCK-ACQUIRE: trying to acquire a lock. exclusive: {self.is_exclusive}.\")\n        started = time.monotonic()\n        while time.monotonic() - started < self.timeout:\n            exclusive_locks = self._find_locks(only_exclusive=True)\n            if len(exclusive_locks) == 0:\n                # looks like there are no exclusive locks, create our lock.\n                key = self._create_lock(exclusive=self.is_exclusive, update_last_refresh=True)\n                # obviously we have a race condition here: other client(s) might have created exclusive\n                # lock(s) at the same time in parallel. thus we have to check again.\n                time.sleep(\n                    self.race_recheck_delay\n                )  # give other clients time to notice our exclusive lock, stop creating theirs\n                exclusive_locks = self._find_locks(only_exclusive=True)\n                if self.is_exclusive:\n                    if len(exclusive_locks) == 1 and exclusive_locks[0][\"key\"] == key:\n                        logger.debug(\"LOCK-ACQUIRE: we are the only exclusive lock!\")\n                        while time.monotonic() - started < self.timeout:\n                            locks = self._find_locks(only_exclusive=False)\n                            if len(locks) == 1 and locks[0][\"key\"] == key:\n                                logger.debug(\"LOCK-ACQUIRE: success! no non-exclusive locks are left!\")\n                                return self\n                            time.sleep(self.other_locks_go_away_delay)\n                        logger.debug(\"LOCK-ACQUIRE: timeout while waiting for non-exclusive locks to go away.\")\n                        break  # timeout\n                    else:\n                        logger.debug(\"LOCK-ACQUIRE: someone else also created an exclusive lock, deleting ours.\")\n                        self._delete_lock(key, ignore_not_found=True, update_last_refresh=True)\n                else:  # not is_exclusive\n                    if len(exclusive_locks) == 0:\n                        logger.debug(\"LOCK-ACQUIRE: success! no exclusive locks detected.\")\n                        # We don't care for other non-exclusive locks.\n                        return self\n                    else:\n                        logger.debug(\"LOCK-ACQUIRE: exclusive locks detected, deleting our shared lock.\")\n                        self._delete_lock(key, ignore_not_found=True, update_last_refresh=True)\n            # wait a random bit before retrying\n            time.sleep(\n                self.retry_delay_min + (self.retry_delay_max - self.retry_delay_min) * random.random()  # nosec B311\n            )\n        logger.debug(\"LOCK-ACQUIRE: timeout while trying to acquire a lock.\")\n        raise LockTimeout(str(self.store))\n\n    def release(self, *, ignore_not_found=False):\n        self.last_refresh_dt = None\n        locks = self._find_locks(only_mine=True)\n        if not locks:\n            if ignore_not_found:\n                logger.debug(\"LOCK-RELEASE: trying to release the lock, but none was found.\")\n                return\n            else:\n                raise NotLocked(str(self.store))\n        assert len(locks) == 1\n        lock = locks[0]\n        logger.debug(f\"LOCK-RELEASE: releasing lock: {lock}.\")\n        self._delete_lock(lock[\"key\"], ignore_not_found=True, update_last_refresh=True)\n\n    def got_exclusive_lock(self):\n        locks = self._find_locks(only_mine=True, only_exclusive=True)\n        return len(locks) == 1\n\n    def break_lock(self):\n        \"\"\"Breaks all locks (not just ours).\"\"\"\n        logger.debug(\"LOCK-BREAK: break_lock() was called - deleting ALL locks!\")\n        locks = self._get_locks()\n        for key in locks:\n            self._delete_lock(key, ignore_not_found=True)\n        self.last_refresh_dt = None\n\n    def migrate_lock(self, old_id, new_id):\n        \"\"\"Migrates the lock ownership from old_id to new_id.\"\"\"\n        logger.debug(f\"LOCK-MIGRATE: {old_id} -> {new_id}.\")\n        assert self.id == old_id\n        assert len(new_id) == 3\n        old_locks = self._find_locks(only_mine=True)\n        assert len(old_locks) == 1\n        self.id = new_id\n        self._create_lock(exclusive=old_locks[0][\"exclusive\"], update_last_refresh=True)\n        self._delete_lock(old_locks[0][\"key\"], update_last_refresh=False)\n\n    def refresh(self):\n        \"\"\"Refreshes the lock; call this frequently, but not later than every <stale> seconds.\"\"\"\n        now = datetime.datetime.now(datetime.timezone.utc)\n        if self.last_refresh_dt is not None and now > self.last_refresh_dt + self.refresh_td:\n            old_locks = self._find_locks(only_mine=True)\n            if len(old_locks) == 0:\n                # crap, my lock has been removed. :-(\n                # this can happen e.g. if my machine has been suspended while doing a backup, so that the\n                # lock will auto-expire. a borg client on another machine might then kill that lock.\n                # if my machine then wakes up again, the lock will have vanished and we get here.\n                # in this case, we need to abort the operation, because the other borg might have removed\n                # repo objects we have written, but the referential tree was not yet full present, e.g.\n                # no archive has been added yet to the manifest, thus all objects looked unused/orphaned.\n                # another scenario when this can happen is a careless user running break-lock on another\n                # machine without making sure there is no borg activity in that repo.\n                logger.debug(\"LOCK-REFRESH: our lock was killed, there is no safe way to continue.\")\n                raise LockTimeout(str(self.store))\n            assert len(old_locks) == 1  # there shouldn't be more than 1\n            old_lock = old_locks[0]\n            if now > old_lock[\"dt\"] + self.refresh_td:\n                logger.debug(f\"LOCK-REFRESH: lock needs a refresh. lock: {old_lock}.\")\n                self._create_lock(exclusive=old_lock[\"exclusive\"], update_last_refresh=True)\n                self._delete_lock(old_lock[\"key\"], update_last_refresh=False)\n"
  },
  {
    "path": "src/borg/testsuite/__init__.py",
    "content": "from contextlib import contextmanager\nimport functools\nimport os\n\ntry:\n    import posix\nexcept ImportError:\n    posix = None\n\nimport stat\nimport sys\nimport sysconfig\nimport tempfile\nimport time\nimport unittest\n\n# Note: this is used by borg.selftest, do not *require* pytest functionality here.\ntry:\n    from pytest import raises\nexcept:  # noqa\n    raises = None\n\nfrom ..fuse_impl import llfuse, has_any_fuse, has_llfuse, has_pyfuse3, has_mfusepy, ENOATTR  # NOQA\nfrom .. import platform\nfrom ..platformflags import is_win32, is_darwin\n\n# Does this version of llfuse support ns precision?\nhave_fuse_mtime_ns = hasattr(llfuse.EntryAttributes, \"st_mtime_ns\") if llfuse else False\n\nhas_mknod = hasattr(os, \"mknod\")\n\nhas_lchflags = hasattr(os, \"lchflags\") or sys.platform.startswith(\"linux\")\ntry:\n    with tempfile.NamedTemporaryFile() as file:\n        platform.set_flags(file.name, stat.UF_NODUMP)\nexcept OSError:\n    has_lchflags = False\n\n# The mtime get/set precision varies on different OS and Python versions\nif posix and \"HAVE_FUTIMENS\" in getattr(posix, \"_have_functions\", []):\n    st_mtime_ns_round = 0  # 1ns resolution\nelif \"HAVE_UTIMES\" in sysconfig.get_config_vars():\n    st_mtime_ns_round = -3  # 1us resolution\nelse:\n    st_mtime_ns_round = -9  # 1s resolution\n\nif sys.platform.startswith(\"netbsd\"):\n    st_mtime_ns_round = -4  # 10us - strange: only >1 microsecond resolution here?\n\n\ndef same_ts_ns(ts_ns1, ts_ns2):\n    \"\"\"Compare two timestamps (both in nanoseconds) to determine whether they are (roughly) equal.\"\"\"\n    diff_ts = int(abs(ts_ns1 - ts_ns2))\n    diff_max = 10 ** (-st_mtime_ns_round)\n    return diff_ts <= diff_max\n\n\ndef granularity_sleep(*, ctime_quirk=False):\n    \"\"\"Sleep long enough to overcome filesystem timestamp granularity and related platform quirks.\n\n    Purpose\n    - Ensure that successive file operations land on different timestamp \"ticks\" across filesystems\n      and operating systems, so tests that compare mtime/ctime are reliable.\n\n    Default rationale (ctime_quirk=False)\n    - macOS: Some volumes may still be HFS+ (1 s timestamp granularity). To be safe across APFS and HFS+,\n      sleep 1.0 s on Darwin.\n    - Windows/NTFS: Although NTFS stores timestamps with 100 ns units, actual updates can be delayed by\n      scheduling/metadata behavior. Sleep a short but noticeable amount (0.2 s).\n    - Linux/BSD and others: Modern filesystems (ext4, XFS, Btrfs, ZFS, UFS2, etc.) typically have\n      sub-second granularity; a small delay (0.02 s) is sufficient in practice.\n\n    Windows ctime quirk (ctime_quirk=True)\n    - On Windows, ``stat().st_ctime`` is the file creation time, not \"metadata change time\" as on Unix.\n    - NTFS implements a feature called \"file system tunneling\" that preserves certain metadata — including\n      creation time — for short intervals when a file is deleted and a new file with the same name is\n      created in the same directory. The default tunneling window is about 15 seconds.\n    - Consequence: If a test deletes a file and quickly recreates it with the same name, the creation time\n      (st_ctime) may remain unchanged for up to ~15 s, causing flakiness when tests expect a changed ctime.\n    - When ``ctime_quirk=True`` this helper sleeps long enough on Windows (15.0 s) to exceed the tunneling\n      window so the new file receives a fresh creation time. On non-Windows platforms this flag has no\n      special effect beyond the normal, short sleep.\n\n    Parameters\n    - ctime_quirk: bool (default False)\n      If True, apply the Windows NTFS tunneling workaround (15 s sleep on Windows). Ignored elsewhere.\n    \"\"\"\n    if is_darwin:\n        duration = 1.0\n    elif is_win32:\n        duration = 0.2 if not ctime_quirk else 15.0\n    else:\n        # Default for Linux/BSD and others with fine-grained timestamps\n        duration = 0.02\n    time.sleep(duration)\n\n\nrejected_dotdot_paths = (\n    \"..\",\n    \"../\",\n    \"../etc/shadow\",\n    \"/..\",\n    \"/../\",\n    \"/../etc\",\n    \"/../etc/\",\n    \"etc/..\",\n    \"/etc/..\",\n    \"/etc/../etc/shadow\",\n    \"//etc/..\",\n    \"etc//..\",\n    \"etc/..//\",\n    \"foo/../bar\",\n)\n\n\n@contextmanager\ndef unopened_tempfile():\n    with tempfile.TemporaryDirectory() as tempdir:\n        yield os.path.join(tempdir, \"file\")\n\n\n@contextmanager\ndef changedir(dir):\n    cwd = os.getcwd()\n    os.chdir(dir)\n    yield\n    os.chdir(cwd)\n\n\ndef is_root():\n    \"\"\"Return True if running with high privileges (e.g., as root).\"\"\"\n    if is_win32:\n        return False  # TODO\n    else:\n        return os.getuid() == 0\n\n\n@functools.lru_cache\ndef are_symlinks_supported():\n    with unopened_tempfile() as filepath:\n        try:\n            os.symlink(\"somewhere\", filepath)\n            if os.stat(filepath, follow_symlinks=False) and os.readlink(filepath) == \"somewhere\":\n                return True\n        except OSError:\n            pass\n    return False\n\n\n@functools.lru_cache\ndef are_hardlinks_supported():\n    if not hasattr(os, \"link\"):\n        # some pythons do not have os.link\n        return False\n\n    with unopened_tempfile() as file1path, unopened_tempfile() as file2path:\n        open(file1path, \"w\").close()\n        try:\n            os.link(file1path, file2path)\n            stat1 = os.stat(file1path)\n            stat2 = os.stat(file2path)\n            if stat1.st_nlink == stat2.st_nlink == 2 and stat1.st_ino == stat2.st_ino:\n                return True\n        except OSError:\n            pass\n    return False\n\n\n@functools.lru_cache\ndef are_fifos_supported():\n    with unopened_tempfile() as filepath:\n        try:\n            os.mkfifo(filepath)\n            return True\n        except OSError:\n            pass\n        except NotImplementedError:\n            pass\n        except AttributeError:\n            pass\n        return False\n\n\n@functools.lru_cache\ndef is_utime_fully_supported():\n    with unopened_tempfile() as filepath:\n        # Some filesystems (such as SSHFS) don't support utime on symlinks\n        if are_symlinks_supported():\n            os.symlink(\"something\", filepath)\n        else:\n            open(filepath, \"w\").close()\n        try:\n            os.utime(filepath, (1000, 2000), follow_symlinks=False)\n            new_stats = os.stat(filepath, follow_symlinks=False)\n            if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:\n                return True\n        except OSError:\n            pass\n        except NotImplementedError:\n            pass\n        return False\n\n\n@functools.lru_cache\ndef is_birthtime_fully_supported():\n    if not hasattr(os.stat_result, \"st_birthtime\"):\n        return False\n    with unopened_tempfile() as filepath:\n        # Some filesystems (such as SSHFS) don't support utime on symlinks\n        if are_symlinks_supported():\n            os.symlink(\"something\", filepath)\n        else:\n            open(filepath, \"w\").close()\n        try:\n            birthtime, mtime, atime = 946598400, 946684800, 946771200\n            os.utime(filepath, (atime, birthtime), follow_symlinks=False)\n            os.utime(filepath, (atime, mtime), follow_symlinks=False)\n            new_stats = os.stat(filepath, follow_symlinks=False)\n            if new_stats.st_birthtime == birthtime and new_stats.st_mtime == mtime and new_stats.st_atime == atime:\n                return True\n        except OSError:\n            pass\n        except NotImplementedError:\n            pass\n        return False\n\n\ndef filter_xattrs(x):\n    # selinux and com.apple.provenance fail our FUSE tests, thus ignore them\n    UNWANTED_KEYS = {b\"security.selinux\", b\"com.apple.provenance\"}\n    if isinstance(x, dict):\n        return {k: v for k, v in x.items() if k not in UNWANTED_KEYS}\n    if isinstance(x, list):\n        return [k for k in x if k not in UNWANTED_KEYS]\n    raise ValueError(\"Unsupported type: %s\" % type(x))\n\n\nclass BaseTestCase(unittest.TestCase):\n    assert_in = unittest.TestCase.assertIn\n    assert_not_in = unittest.TestCase.assertNotIn\n    assert_equal = unittest.TestCase.assertEqual\n    assert_not_equal = unittest.TestCase.assertNotEqual\n    assert_raises = staticmethod(raises) if raises else unittest.TestCase.assertRaises  # type: ignore\n\n\nclass FakeInputs:\n    \"\"\"Simulate multiple user inputs, can be used as input() replacement\"\"\"\n\n    def __init__(self, inputs):\n        self.inputs = inputs\n\n    def __call__(self, prompt=None):\n        if prompt is not None:\n            print(prompt, end=\"\")\n        try:\n            return self.inputs.pop(0)\n        except IndexError:\n            raise EOFError from None\n"
  },
  {
    "path": "src/borg/testsuite/archive_test.py",
    "content": "import json\nimport os\nfrom collections import OrderedDict\nfrom datetime import datetime, timezone\nfrom io import StringIO\nfrom unittest.mock import Mock\n\nimport pytest\n\nfrom . import rejected_dotdot_paths\nfrom ..crypto.key import PlaintextKey\nfrom ..archive import Archive, CacheChunkBuffer, RobustUnpacker, valid_msgpacked_dict, ITEM_KEYS, Statistics\nfrom ..archive import BackupOSError, backup_io, backup_io_iter, get_item_uid_gid\nfrom ..helpers import msgpack\nfrom ..item import Item, ArchiveItem\nfrom ..manifest import Manifest\nfrom ..platform import uid2user, gid2group, is_win32\n\n\n@pytest.fixture()\ndef stats():\n    stats = Statistics()\n    stats.update(20, unique=True)\n    stats.nfiles = 1\n    return stats\n\n\ndef test_stats_basic(stats):\n    assert stats.osize == 20\n    assert stats.usize == 20\n    stats.update(20, unique=False)\n    assert stats.osize == 40\n    assert stats.usize == 20\n\n\ndef test_stats_progress_tty(stats, monkeypatch, columns=80):\n    class TTYStringIO(StringIO):\n        def isatty(self):\n            return True\n\n    monkeypatch.setenv(\"COLUMNS\", str(columns))\n    out = TTYStringIO()\n    stats.show_progress(stream=out)\n    s = \"20 B O 20 B U 1 N \"\n    buf = \" \" * (columns - len(s))\n    assert out.getvalue() == s + buf + \"\\r\"\n\n    out = TTYStringIO()\n    stats.update(10**3, unique=False)\n    stats.show_progress(item=Item(path=\"foo\"), final=False, stream=out)\n    s = \"1.02 kB O 20 B U 1 N foo\"\n    buf = \" \" * (columns - len(s))\n    assert out.getvalue() == s + buf + \"\\r\"\n\n    out = TTYStringIO()\n    stats.show_progress(item=Item(path=\"foo\" * 40), final=False, stream=out)\n    s = \"1.02 kB O 20 B U 1 N foofoofoofoofoofoofoofoofo...foofoofoofoofoofoofoofoofoofoo\"\n    buf = \" \" * (columns - len(s))\n    assert out.getvalue() == s + buf + \"\\r\"\n\n\ndef test_stats_progress_file(stats, monkeypatch):\n    out = StringIO()\n    stats.show_progress(stream=out)\n    s = \"20 B O 20 B U 1 N \"\n    assert out.getvalue() == s + \"\\n\"\n\n    out = StringIO()\n    stats.update(10**3, unique=False)\n    path = \"foo\"\n    stats.show_progress(item=Item(path=path), final=False, stream=out)\n    s = f\"1.02 kB O 20 B U 1 N {path}\"\n    assert out.getvalue() == s + \"\\n\"\n\n    out = StringIO()\n    path = \"foo\" * 40\n    stats.show_progress(item=Item(path=path), final=False, stream=out)\n    s = f\"1.02 kB O 20 B U 1 N {path}\"\n    assert out.getvalue() == s + \"\\n\"\n\n\ndef test_stats_format(stats):\n    assert (\n        str(stats)\n        == \"\"\"\\\nNumber of files: 1\nOriginal size: 20 B\nDeduplicated size: 20 B\nTime spent in hashing: 0.000 seconds\nTime spent in chunking: 0.000 seconds\nAdded files: 0\nUnchanged files: 0\nModified files: 0\nError files: 0\nFiles changed while reading: 0\nBytes read from remote: 0\nBytes sent to remote: 0\n\"\"\"\n    )\n    s = f\"{stats.osize_fmt}\"\n    assert s == \"20 B\"\n    # kind of redundant, but id is variable so we can't match reliably\n    assert repr(stats) == f\"<Statistics object at {id(stats):#x} (20, 20)>\"\n\n\ndef test_stats_progress_json(stats):\n    stats.output_json = True\n\n    out = StringIO()\n    stats.show_progress(item=Item(path=\"foo\"), stream=out)\n    result = json.loads(out.getvalue())\n    assert result[\"type\"] == \"archive_progress\"\n    assert isinstance(result[\"time\"], float)\n    assert result[\"finished\"] is False\n    assert result[\"path\"] == \"foo\"\n    assert result[\"original_size\"] == 20\n    assert result[\"nfiles\"] == 1\n\n    out = StringIO()\n    stats.show_progress(stream=out, final=True)\n    result = json.loads(out.getvalue())\n    assert result[\"type\"] == \"archive_progress\"\n    assert isinstance(result[\"time\"], float)\n    assert result[\"finished\"] is True  # see #6570\n    assert \"path\" not in result\n    assert \"original_size\" not in result\n    assert \"nfiles\" not in result\n\n\n@pytest.mark.parametrize(\n    \"isoformat, expected\",\n    [\n        (\"1970-01-01T00:00:01.000001\", datetime(1970, 1, 1, 0, 0, 1, 1, timezone.utc)),  # test with microseconds\n        (\"1970-01-01T00:00:01\", datetime(1970, 1, 1, 0, 0, 1, 0, timezone.utc)),  # test without microseconds\n    ],\n)\ndef test_timestamp_parsing(monkeypatch, isoformat, expected):\n    repository = Mock()\n    key = PlaintextKey(repository)\n    manifest = Manifest(key, repository)\n    a = Archive(manifest, \"test\", create=True)\n    a.metadata = ArchiveItem(time=isoformat)\n    assert a.ts == expected\n\n\nclass MockCache:\n    class MockRepo:\n        def async_response(self, wait=True):\n            pass\n\n    def __init__(self):\n        self.objects = {}\n        self.repository = self.MockRepo()\n\n    def add_chunk(self, id, meta, data, stats=None, wait=True, ro_type=None):\n        assert ro_type is not None\n        self.objects[id] = data\n        return id, len(data)\n\n\ndef test_cache_chunk_buffer():\n    data = [Item(path=\"p1\"), Item(path=\"p2\")]\n    cache = MockCache()\n    key = PlaintextKey(None)\n    chunks = CacheChunkBuffer(cache, key, None)\n    for d in data:\n        chunks.add(d)\n        chunks.flush()\n    chunks.flush(flush=True)\n    assert len(chunks.chunks) == 2\n    unpacker = msgpack.Unpacker()\n    for id in chunks.chunks:\n        unpacker.feed(cache.objects[id])\n    assert data == [Item(internal_dict=d) for d in unpacker]\n\n\ndef test_partial_cache_chunk_buffer():\n    big = \"0123456789abcdefghijklmnopqrstuvwxyz\" * 25000\n    data = [Item(path=\"full\", target=big), Item(path=\"partial\", target=big)]\n    cache = MockCache()\n    key = PlaintextKey(None)\n    chunks = CacheChunkBuffer(cache, key, None)\n    for d in data:\n        chunks.add(d)\n    chunks.flush(flush=False)\n    # the code is expected to leave the last partial chunk in the buffer\n    assert len(chunks.chunks) == 3\n    assert chunks.buffer.tell() > 0\n    # now really flush\n    chunks.flush(flush=True)\n    assert len(chunks.chunks) == 4\n    assert chunks.buffer.tell() == 0\n    unpacker = msgpack.Unpacker()\n    for id in chunks.chunks:\n        unpacker.feed(cache.objects[id])\n    assert data == [Item(internal_dict=d) for d in unpacker]\n\n\ndef make_chunks(items):\n    return b\"\".join(msgpack.packb({\"path\": item}) for item in items)\n\n\ndef _validator(value):\n    return isinstance(value, dict) and value.get(\"path\") in (\"foo\", \"bar\", \"boo\", \"baz\")\n\n\ndef process(input):\n    unpacker = RobustUnpacker(validator=_validator, item_keys=ITEM_KEYS)\n    result = []\n    for should_sync, chunks in input:\n        if should_sync:\n            unpacker.resync()\n        for data in chunks:\n            unpacker.feed(data)\n            for item in unpacker:\n                result.append(item)\n    return result\n\n\ndef test_extra_garbage_no_sync():\n    chunks = [(False, [make_chunks([\"foo\", \"bar\"])]), (False, [b\"garbage\"] + [make_chunks([\"boo\", \"baz\"])])]\n    res = process(chunks)\n    assert res == [{\"path\": \"foo\"}, {\"path\": \"bar\"}, 103, 97, 114, 98, 97, 103, 101, {\"path\": \"boo\"}, {\"path\": \"baz\"}]\n\n\ndef split(left, length):\n    parts = []\n    while left:\n        parts.append(left[:length])\n        left = left[length:]\n    return parts\n\n\ndef test_correct_stream():\n    chunks = split(make_chunks([\"foo\", \"bar\", \"boo\", \"baz\"]), 2)\n    input = [(False, chunks)]\n    result = process(input)\n    assert result == [{\"path\": \"foo\"}, {\"path\": \"bar\"}, {\"path\": \"boo\"}, {\"path\": \"baz\"}]\n\n\ndef test_missing_chunk():\n    chunks = split(make_chunks([\"foo\", \"bar\", \"boo\", \"baz\"]), 4)\n    input = [(False, chunks[:3]), (True, chunks[4:])]\n    result = process(input)\n    assert result == [{\"path\": \"foo\"}, {\"path\": \"boo\"}, {\"path\": \"baz\"}]\n\n\ndef test_corrupt_chunk():\n    chunks = split(make_chunks([\"foo\", \"bar\", \"boo\", \"baz\"]), 4)\n    input = [(False, chunks[:3]), (True, [b\"gar\", b\"bage\"] + chunks[3:])]\n    result = process(input)\n    assert result == [{\"path\": \"foo\"}, {\"path\": \"boo\"}, {\"path\": \"baz\"}]\n\n\n@pytest.fixture\ndef item_keys_serialized():\n    return [msgpack.packb(name) for name in ITEM_KEYS]\n\n\n@pytest.mark.parametrize(\n    \"packed\",\n    [b\"\", b\"x\", b\"foobar\"]\n    + [\n        msgpack.packb(o)\n        for o in (\n            [None, 0, 0.0, False, \"\", {}, [], ()]\n            + [42, 23.42, True, b\"foobar\", {b\"foo\": b\"bar\"}, [b\"foo\", b\"bar\"], (b\"foo\", b\"bar\")]\n        )\n    ],\n)\ndef test_invalid_msgpacked_item(packed, item_keys_serialized):\n    assert not valid_msgpacked_dict(packed, item_keys_serialized)\n\n\n# pytest-xdist always requires the same order for the keys and dicts:\nIK = sorted(list(ITEM_KEYS))\n\n\n@pytest.mark.parametrize(\n    \"packed\",\n    [\n        msgpack.packb(o)\n        for o in [\n            {\"path\": b\"/a/b/c\"},  # small (different msgpack mapping type!)\n            OrderedDict((k, b\"\") for k in IK),  # as big (key count) as it gets\n            OrderedDict((k, b\"x\" * 1000) for k in IK),  # as big (key count and volume) as it gets\n        ]\n    ],\n    ids=[\"minimal\", \"empty-values\", \"long-values\"],\n)\ndef test_valid_msgpacked_items(packed, item_keys_serialized):\n    assert valid_msgpacked_dict(packed, item_keys_serialized)\n\n\ndef test_key_length_msgpacked_items():\n    key = \"x\" * 32  # 31 bytes is the limit for fixstr msgpack type\n    data = {key: b\"\"}\n    item_keys_serialized = [msgpack.packb(key)]\n    assert valid_msgpacked_dict(msgpack.packb(data), item_keys_serialized)\n\n\ndef test_backup_io():\n    with pytest.raises(BackupOSError):\n        with backup_io:\n            raise OSError(123)\n\n\ndef test_backup_io_iter():\n    class Iterator:\n        def __init__(self, exc):\n            self.exc = exc\n\n        def __next__(self):\n            raise self.exc()\n\n    oserror_iterator = Iterator(OSError)\n    with pytest.raises(BackupOSError):\n        for _ in backup_io_iter(oserror_iterator):\n            pass\n\n    normal_iterator = Iterator(StopIteration)\n    for _ in backup_io_iter(normal_iterator):\n        assert False, \"StopIteration handled incorrectly\"\n\n\ndef test_get_item_uid_gid():\n    # test requires that:\n    # - a user/group name for the current process' real uid/gid exists.\n    # - a system user/group udoesnotexist:gdoesnotexist does NOT exist.\n\n    try:\n        puid, pgid = os.getuid(), os.getgid()  # UNIX only\n    except AttributeError:\n        puid, pgid = 0, 0\n    puser, pgroup = uid2user(puid), gid2group(pgid)\n\n    # This is intentionally a \"strange\" item, with non-matching IDs/names.\n    item = Item(path=\"filename\", uid=1, gid=2, user=puser, group=pgroup)\n\n    uid, gid = get_item_uid_gid(item, numeric=False)\n    # these are found via a name-to-id lookup\n    assert uid == puid\n    assert gid == pgid\n\n    uid, gid = get_item_uid_gid(item, numeric=True)\n    # these are directly taken from the item.uid and .gid\n    assert uid == 1\n    assert gid == 2\n\n    uid, gid = get_item_uid_gid(item, numeric=False, uid_forced=3, gid_forced=4)\n    # these are enforced (not from item metadata)\n    assert uid == 3\n    assert gid == 4\n\n    # item metadata broken, has negative ids.\n    item = Item(path=\"filename\", uid=-1, gid=-2, user=puser, group=pgroup)\n\n    uid, gid = get_item_uid_gid(item, numeric=True)\n    # use the uid/gid defaults (which both default to 0).\n    assert uid == 0\n    assert gid == 0\n\n    uid, gid = get_item_uid_gid(item, numeric=True, uid_default=5, gid_default=6)\n    # use the uid/gid defaults (as given).\n    assert uid == 5\n    assert gid == 6\n\n    # item metadata broken, has negative ids and non-existing user/group names.\n    item = Item(path=\"filename\", uid=-3, gid=-4, user=\"udoesnotexist\", group=\"gdoesnotexist\")\n\n    uid, gid = get_item_uid_gid(item, numeric=False)\n    # use the uid/gid defaults (which both default to 0).\n    assert uid == 0\n    assert gid == 0\n\n    uid, gid = get_item_uid_gid(item, numeric=True, uid_default=7, gid_default=8)\n    # use the uid/gid defaults (as given).\n    assert uid == 7\n    assert gid == 8\n\n    if not is_win32:\n        # Due to the hack in borg.platform.windows_ug, user2uid/group2gid always return 0\n        # (no matter which username we ask for), and they never raise a KeyError (e.g., for\n        # a non-existing user/group name). Thus, these tests can currently not succeed on win32.\n\n        # item metadata has valid uid/gid, but non-existing user/group names.\n        item = Item(path=\"filename\", uid=9, gid=10, user=\"udoesnotexist\", group=\"gdoesnotexist\")\n\n        uid, gid = get_item_uid_gid(item, numeric=False)\n        # because user/group name does not exist here, use valid numeric ids from item metadata.\n        assert uid == 9\n        assert gid == 10\n\n        uid, gid = get_item_uid_gid(item, numeric=False, uid_default=11, gid_default=12)\n        # because item uid/gid seems valid, do not use the given uid/gid defaults\n        assert uid == 9\n        assert gid == 10\n\n    # item metadata only has uid/gid, but no user/group.\n    item = Item(path=\"filename\", uid=13, gid=14)\n\n    uid, gid = get_item_uid_gid(item, numeric=False)\n    # It will check user/group first, but as there is nothing in the item, it falls back to uid/gid.\n    assert uid == 13\n    assert gid == 14\n\n    uid, gid = get_item_uid_gid(item, numeric=True)\n    # does not check user/group, directly returns uid/gid.\n    assert uid == 13\n    assert gid == 14\n\n    # item metadata has no uid/gid/user/group.\n    item = Item(path=\"filename\")\n\n    uid, gid = get_item_uid_gid(item, numeric=False, uid_default=15)\n    # As there is nothing, it will fall back to uid_default/gid_default.\n    assert uid == 15\n    assert gid == 0\n\n    uid, gid = get_item_uid_gid(item, numeric=True, gid_default=16)\n    # As there is nothing, it will fall back to uid_default/gid_default.\n    assert uid == 0\n    assert gid == 16\n\n\ndef test_reject_non_sanitized_item():\n    for path in rejected_dotdot_paths:\n        with pytest.raises(ValueError, match=\"unexpected '..' element in path\"):\n            Item(path=path, user=\"root\", group=\"root\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/__init__.py",
    "content": "import errno\nimport filecmp\nimport io\nimport os\nimport re\nimport stat\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom contextlib import contextmanager\nfrom datetime import datetime\nfrom io import BytesIO, StringIO\n\nimport pytest\n\nfrom ... import xattr, platform\nfrom ...archive import Archive\nfrom ...archiver import Archiver, PURE_PYTHON_MSGPACK_WARNING\nfrom ...constants import *  # NOQA\nfrom ...helpers import Location, umount\nfrom ...helpers import EXIT_SUCCESS\nfrom ...helpers import init_ec_warnings\nfrom ...logger import flush_logging\nfrom ...manifest import Manifest\nfrom ...platform import get_flags\nfrom ...remote import RemoteRepository\nfrom ...repository import Repository\nfrom .. import has_lchflags, has_mknod, is_utime_fully_supported, have_fuse_mtime_ns, st_mtime_ns_round, filter_xattrs\nfrom .. import changedir, ENOATTR  # NOQA\nfrom .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, granularity_sleep\nfrom ..platform.platform_test import is_win32\nfrom ...xattr import get_all\n\nRK_ENCRYPTION = \"--encryption=repokey-aes-ocb\"\nKF_ENCRYPTION = \"--encryption=keyfile-chacha20-poly1305\"\n\n# This points to the ``src/borg/archiver`` directory (small, with only a few files).\n# There are quite a lot of files in there, because there is a __pycache__ subdirectory.\n# Consider rather using the backup_files fixtures, which only has a few small files/dirs.\nsrc_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"..\", \"archiver\"))\nsrc_file = \"archiver/__init__.py\"  # relative path of one file in src_dir\n\nrequires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hard links not supported\")\n\n\ndef exec_cmd(*args, archiver=None, fork=False, exe=None, input=b\"\", binary_output=False, **kw):\n    if fork:\n        try:\n            if exe is None:\n                borg = (sys.executable, \"-m\", \"borg\")\n            elif isinstance(exe, str):\n                borg = (exe,)\n            elif not isinstance(exe, tuple):\n                raise ValueError(\"exe must be None, a tuple or a str\")\n            output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT, input=input)\n            ret = 0\n        except subprocess.CalledProcessError as e:\n            output = e.output\n            ret = e.returncode\n        except SystemExit as e:  # possibly raised by argparse\n            output = \"\"\n            ret = e.code\n        if binary_output:\n            return ret, output\n        else:\n            return ret, os.fsdecode(output)\n    else:\n        stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr\n        try:\n            sys.stdin = StringIO(input.decode())\n            sys.stdin.buffer = BytesIO(input)\n            output = BytesIO()\n            # Always use utf-8 here, to .decode() below\n            output_text = sys.stdout = sys.stderr = io.TextIOWrapper(output, encoding=\"utf-8\")\n            if archiver is None:\n                archiver = Archiver()\n            archiver.prerun_checks = lambda *args: None\n            init_ec_warnings()\n            try:\n                args = archiver.parse_args(list(args))\n                # argparse may raise SystemExit when the command line is bad or\n                # actions that abort early (e.g., --help) were given. Catch this and return\n                # the error code as if we invoked a Borg binary.\n            except SystemExit as e:\n                output_text.flush()\n                return e.code, output.getvalue() if binary_output else output.getvalue().decode()\n            try:\n                ret = archiver.run(args)  # calls setup_logging internally\n            finally:\n                flush_logging()  # usually done via atexit, but we do not exit here\n            output_text.flush()\n            return ret, output.getvalue() if binary_output else output.getvalue().decode()\n        finally:\n            sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr\n\n\n# Check whether the binary \"borg.exe\" is available (for local testing, a symlink to virtualenv/bin/borg will do).\ntry:\n    exec_cmd(\"help\", exe=\"borg.exe\", fork=True)\n    BORG_EXES = [\"python\", \"binary\"]\nexcept FileNotFoundError:\n    BORG_EXES = [\"python\"]\n\n\n@pytest.fixture(params=BORG_EXES)\ndef cmd_fixture(request):\n    if request.param == \"python\":\n        exe = None\n    elif request.param == \"binary\":\n        exe = \"borg.exe\"\n    else:\n        raise ValueError(\"param must be 'python' or 'binary'.\")\n\n    def exec_fn(*args, **kw):\n        return exec_cmd(*args, exe=exe, fork=True, **kw)\n\n    return exec_fn\n\n\ndef generate_archiver_tests(metafunc, kinds: str):\n    # Generate tests for different scenarios: local repository, remote repository, and using the borg binary.\n    archivers = []\n    for kind in kinds.split(\",\"):\n        if kind == \"local\":\n            archivers.append(\"archiver\")\n        elif kind == \"remote\":\n            archivers.append(\"remote_archiver\")\n        elif kind == \"binary\":\n            archivers.append(\"binary_archiver\")\n        else:\n            raise ValueError(f\"Invalid archiver: Expected local, remote, or binary, received {kind}.\")\n\n    if \"archivers\" in metafunc.fixturenames:\n        metafunc.parametrize(\"archivers\", archivers)\n\n\ndef checkts(ts):\n    # Check whether the timestamp is in the expected format\n    assert datetime.strptime(ts, ISO_FORMAT + \"%z\")  # must not raise\n\n\ndef cmd(archiver, *args, **kw):\n    exit_code = kw.pop(\"exit_code\", 0)\n    fork = kw.pop(\"fork\", None)\n    binary_output = kw.get(\"binary_output\", False)\n    if fork is None:\n        fork = archiver.FORK_DEFAULT\n    ret, output = exec_cmd(\n        f\"--repo={archiver.repository_location}\", *args, archiver=archiver.archiver, fork=fork, exe=archiver.EXE, **kw\n    )\n    if ret != exit_code:\n        print(output)\n    assert ret == exit_code\n    # if tests are run with the pure-python msgpack, there will be warnings about\n    # this in the output, which would make a lot of tests fail.\n    pp_msg = PURE_PYTHON_MSGPACK_WARNING.encode() if binary_output else PURE_PYTHON_MSGPACK_WARNING\n    empty = b\"\" if binary_output else \"\"\n    output = empty.join(line for line in output.splitlines(keepends=True) if pp_msg not in line)\n    return output\n\n\ndef create_src_archive(archiver, name, ts=None):\n    if ts:\n        cmd(archiver, \"create\", \"--compression=lz4\", f\"--timestamp={ts}\", name, src_dir)\n    else:\n        cmd(archiver, \"create\", \"--compression=lz4\", name, src_dir)\n\n\ndef open_archive(repo_path, name):\n    repository = Repository(repo_path, exclusive=True)\n    with repository:\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        archive_info = manifest.archives.get_one([name])\n        archive = Archive(manifest, archive_info.id)\n    return archive, repository\n\n\ndef open_repository(archiver):\n    if archiver.get_kind() == \"remote\":\n        return RemoteRepository(Location(archiver.repository_location))\n    else:\n        return Repository(archiver.repository_path, exclusive=True)\n\n\ndef create_regular_file(input_path, name, size=0, contents=None):\n    assert not (size != 0 and contents and len(contents) != size), \"size and contents do not match\"\n    filename = os.path.join(input_path, name)\n    if not os.path.exists(os.path.dirname(filename)):\n        os.makedirs(os.path.dirname(filename))\n    with open(filename, \"wb\") as fd:\n        if contents is None:\n            contents = b\"X\" * size\n        fd.write(contents)\n\n\ndef create_test_files(input_path, create_hardlinks=True):\n    \"\"\"Create a minimal test case including all supported file types\"\"\"\n    # File\n    create_regular_file(input_path, \"file1\", size=1024 * 80)\n    create_regular_file(input_path, \"flagfile\", size=1024)\n    # Directory\n    create_regular_file(input_path, \"dir2/file2\", size=1024 * 80)\n    # File mode\n    os.chmod(\"input/file1\", 0o4755)\n    # Hard link\n    if are_hardlinks_supported() and create_hardlinks:\n        os.link(os.path.join(input_path, \"file1\"), os.path.join(input_path, \"hardlink\"))\n    # Symlink\n    if are_symlinks_supported():\n        os.symlink(\"somewhere\", os.path.join(input_path, \"link1\"))\n    create_regular_file(input_path, \"fusexattr\", size=1)\n    if not xattr.XATTR_FAKEROOT and xattr.is_enabled(input_path):\n        fn = os.fsencode(os.path.join(input_path, \"fusexattr\"))\n        # Ironically, due to how fakeroot works, comparing FUSE file xattrs to original file xattrs\n        # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False.\n        # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file\n        # into \"fakeroot space\". Because the xattrs exposed by borgfs are these of an underlying file\n        # (from fakeroot's point of view) they are invisible to the test process inside fakeroot.\n        xattr.setxattr(fn, b\"user.foo\", b\"bar\")\n        xattr.setxattr(fn, b\"user.empty\", b\"\")\n        # XXX this always fails for me\n        # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot\n        # same for newer ubuntu and centos.\n        # If this is supported only on specific platforms, the platform should be checked first,\n        # so that the test setup for all tests using it does not always fail for others.\n    # FIFO node\n    if are_fifos_supported():\n        os.mkfifo(os.path.join(input_path, \"fifo1\"))\n    if has_lchflags:\n        platform.set_flags(os.path.join(input_path, \"flagfile\"), stat.UF_NODUMP)\n\n    if is_win32:\n        have_root = False\n    else:\n        try:\n            if has_mknod:\n                # Block device\n                os.mknod(\"input/bdev\", 0o600 | stat.S_IFBLK, os.makedev(10, 20))\n                # Char device\n                os.mknod(\"input/cdev\", 0o600 | stat.S_IFCHR, os.makedev(30, 40))\n            # File owner\n            os.chown(\"input/file1\", 100, 200)  # raises OSError invalid argument on cygwin\n            # File mode\n            os.chmod(\"input/dir2\", 0o555)  # if we take away write perms, we need root to remove contents\n            have_root = True  # we have (fake)root\n        except PermissionError:\n            have_root = False\n        except OSError as e:\n            # Note: ENOSYS \"Function not implemented\" happens as non-root on Win 10 Linux Subsystem.\n            if e.errno not in (errno.EINVAL, errno.ENOSYS):\n                raise\n            have_root = False\n    granularity_sleep()  # \"empty\" must have newer timestamp than other files\n    create_regular_file(input_path, \"empty\", size=0)\n    return have_root\n\n\ndef _extract_repository_id(repo_path):\n    with Repository(repo_path) as repository:\n        return repository.id\n\n\ndef _set_repository_id(repo_path, id):\n    with Repository(repo_path) as repository:\n        repository._set_id(id)\n        return repository.id\n\n\ndef _extract_hardlinks_setup(archiver):\n    input_path = archiver.input_path\n    os.mkdir(os.path.join(input_path, \"dir1\"))\n    os.mkdir(os.path.join(input_path, \"dir1/subdir\"))\n\n    create_regular_file(input_path, \"source\", contents=b\"123456\")\n    os.link(os.path.join(input_path, \"source\"), os.path.join(input_path, \"abba\"))\n    os.link(os.path.join(input_path, \"source\"), os.path.join(input_path, \"dir1/hardlink\"))\n    os.link(os.path.join(input_path, \"source\"), os.path.join(input_path, \"dir1/subdir/hardlink\"))\n\n    create_regular_file(input_path, \"dir1/source2\")\n    os.link(os.path.join(input_path, \"dir1/source2\"), os.path.join(input_path, \"dir1/aaaa\"))\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n\ndef _create_test_caches(archiver):\n    input_path = archiver.input_path\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(input_path, \"file1\", size=1024 * 80)\n    create_regular_file(input_path, \"cache1/%s\" % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b\" extra stuff\")\n    create_regular_file(input_path, \"cache2/%s\" % CACHE_TAG_NAME, contents=b\"invalid signature\")\n    os.mkdir(\"input/cache3\")\n    if are_hardlinks_supported():\n        os.link(\"input/cache1/%s\" % CACHE_TAG_NAME, \"input/cache3/%s\" % CACHE_TAG_NAME)\n    else:\n        create_regular_file(input_path, \"cache3/%s\" % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b\" extra stuff\")\n\n\ndef _assert_test_caches(archiver):\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert sorted(os.listdir(\"output/input\")) == [\"cache2\", \"file1\"]\n    assert sorted(os.listdir(\"output/input/cache2\")) == [CACHE_TAG_NAME]\n\n\ndef _create_test_tagged(archiver):\n    input_path = archiver.input_path\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(input_path, \"file1\", size=1024 * 80)\n    create_regular_file(input_path, \"tagged1/.NOBACKUP\")\n    create_regular_file(input_path, \"tagged2/00-NOBACKUP\")\n    create_regular_file(input_path, \"tagged3/.NOBACKUP/file2\", size=1024)\n\n\ndef _assert_test_tagged(archiver):\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\"]\n\n\ndef _create_test_keep_tagged(archiver):\n    input_path = archiver.input_path\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(input_path, \"file0\", size=1024)\n    create_regular_file(input_path, \"tagged1/.NOBACKUP1\")\n    create_regular_file(input_path, \"tagged1/file1\", size=1024)\n    create_regular_file(input_path, \"tagged2/.NOBACKUP2/subfile1\", size=1024)\n    create_regular_file(input_path, \"tagged2/file2\", size=1024)\n    create_regular_file(input_path, \"tagged3/%s\" % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b\" extra stuff\")\n    create_regular_file(input_path, \"tagged3/file3\", size=1024)\n    create_regular_file(input_path, \"taggedall/.NOBACKUP1\")\n    create_regular_file(input_path, \"taggedall/.NOBACKUP2/subfile1\", size=1024)\n    create_regular_file(input_path, \"taggedall/%s\" % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b\" extra stuff\")\n    create_regular_file(input_path, \"taggedall/file4\", size=1024)\n\n\ndef _assert_test_keep_tagged(archiver):\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert sorted(os.listdir(\"output/input\")), [\"file0\", \"tagged1\", \"tagged2\", \"tagged3\", \"taggedall\"]\n    assert os.listdir(\"output/input/tagged1\"), [\".NOBACKUP1\"]\n    assert os.listdir(\"output/input/tagged2\"), [\".NOBACKUP2\"]\n    assert os.listdir(\"output/input/tagged3\"), [CACHE_TAG_NAME]\n    assert sorted(os.listdir(\"output/input/taggedall\")), [\".NOBACKUP1\", \".NOBACKUP2\", CACHE_TAG_NAME]\n\n\n@contextmanager\ndef assert_creates_file(path):\n    assert not os.path.exists(path), f\"{path} should not exist\"\n    yield\n    assert os.path.exists(path), f\"{path} should exist\"\n\n\ndef assert_dirs_equal(dir1, dir2, **kwargs):\n    diff = filecmp.dircmp(dir1, dir2)\n    _assert_dirs_equal_cmp(diff, **kwargs)\n\n\ndef assert_line_exists(lines, expected_regexpr):\n    assert any(re.search(expected_regexpr, line) for line in lines), f\"no match for {expected_regexpr} in {lines}\"\n\n\ndef assert_line_not_exists(lines, expected_regexpr):\n    assert not any(\n        re.search(expected_regexpr, line) for line in lines\n    ), f\"unexpected match for {expected_regexpr} in {lines}\"\n\n\ndef _assert_dirs_equal_cmp(diff, ignore_flags=False, ignore_xattrs=False, ignore_ns=False):\n    assert diff.left_only == []\n    assert diff.right_only == []\n    assert diff.diff_files == []\n    assert diff.funny_files == []\n    for filename in diff.common:\n        path1 = os.path.join(diff.left, filename)\n        path2 = os.path.join(diff.right, filename)\n        s1 = os.stat(path1, follow_symlinks=False)\n        s2 = os.stat(path2, follow_symlinks=False)\n        # Assume path2 is on FUSE if st_dev is different\n        fuse = s1.st_dev != s2.st_dev\n        attrs = [\"st_uid\", \"st_gid\", \"st_rdev\"]\n        if not fuse or not os.path.isdir(path1):\n            # dir nlink is always 1 on our FUSE filesystem\n            attrs.append(\"st_nlink\")\n        d1 = [filename] + [getattr(s1, a) for a in attrs]\n        d2 = [filename] + [getattr(s2, a) for a in attrs]\n        d1.insert(1, oct(s1.st_mode))\n        d2.insert(1, oct(s2.st_mode))\n        if not ignore_flags:\n            d1.append(get_flags(path1, s1))\n            d2.append(get_flags(path2, s2))\n        # ignore st_rdev if file is not a block/char device, fixes #203\n        if not stat.S_ISCHR(s1.st_mode) and not stat.S_ISBLK(s1.st_mode):\n            d1[4] = None\n        if not stat.S_ISCHR(s2.st_mode) and not stat.S_ISBLK(s2.st_mode):\n            d2[4] = None\n        # If utime is not fully supported, Borg cannot set mtime.\n        # Therefore, we should not test it in that case.\n        if is_utime_fully_supported():\n            # Older versions of llfuse do not support ns precision properly\n            if ignore_ns:\n                d1.append(int(s1.st_mtime_ns / 1e9))\n                d2.append(int(s2.st_mtime_ns / 1e9))\n            elif fuse and not have_fuse_mtime_ns:\n                d1.append(round(s1.st_mtime_ns, -4))\n                d2.append(round(s2.st_mtime_ns, -4))\n            else:\n                d1.append(round(s1.st_mtime_ns, st_mtime_ns_round))\n                d2.append(round(s2.st_mtime_ns, st_mtime_ns_round))\n        if not ignore_xattrs:\n            d1.append(filter_xattrs(get_all(path1, follow_symlinks=False)))\n            d2.append(filter_xattrs(get_all(path2, follow_symlinks=False)))\n        assert d1 == d2\n    for sub_diff in diff.subdirs.values():\n        _assert_dirs_equal_cmp(sub_diff, ignore_flags=ignore_flags, ignore_xattrs=ignore_xattrs, ignore_ns=ignore_ns)\n\n\n@contextmanager\ndef read_only(path):\n    \"\"\"Some paths need to be made read-only for testing\n\n    If the tests are executed inside a fakeroot environment, the\n    changes from chmod won't affect the real permissions of that\n    folder. This issue is circumvented by temporarily disabling\n    fakeroot with `LD_PRELOAD=`.\n\n    Using chmod to remove write permissions is not enough if the\n    tests are running with root privileges. Instead, the folder is\n    rendered immutable with chattr or chflags, respectively.\n    \"\"\"\n    if sys.platform.startswith(\"linux\"):\n        cmd_immutable = 'chattr +i \"%s\"' % path\n        cmd_mutable = 'chattr -i \"%s\"' % path\n    elif sys.platform.startswith((\"darwin\", \"freebsd\", \"netbsd\", \"openbsd\")):\n        cmd_immutable = 'chflags uchg \"%s\"' % path\n        cmd_mutable = 'chflags nouchg \"%s\"' % path\n    elif sys.platform.startswith(\"sunos\"):  # openindiana\n        cmd_immutable = 'chmod S+vimmutable \"%s\"' % path\n        cmd_mutable = 'chmod S-vimmutable \"%s\"' % path\n    else:\n        message = \"Testing read-only repos is not supported on platform %s\" % sys.platform\n        pytest.skip(message)\n    try:\n        os.system('LD_PRELOAD= chmod -R ugo-w \"%s\"' % path)\n        rc = os.system(cmd_immutable)\n        if rc != 0:\n            # If we cannot make the path immutable (e.g., missing CAP_LINUX_IMMUTABLE\n            # in containers or restricted environments), the read-only tests would\n            # not be meaningful. Skip them instead of failing.\n            pytest.skip(f\"Unable to make path immutable with: {cmd_immutable} (rc={rc})\")\n        yield\n    finally:\n        # Restore permissions to ensure clean-up doesn't fail\n        os.system(cmd_mutable)\n        os.system('LD_PRELOAD= chmod -R ugo+w \"%s\"' % path)\n\n\ndef wait_for_mountstate(mountpoint, *, mounted, timeout=5):\n    \"\"\"Wait until a path meets specified mount point status\"\"\"\n    timeout += time.time()\n    while timeout > time.time():\n        if os.path.ismount(mountpoint) == mounted:\n            return\n        time.sleep(0.1)\n    message = \"Waiting for {} of {}\".format(\"mount\" if mounted else \"umount\", mountpoint)\n    raise TimeoutError(message)\n\n\n@contextmanager\ndef fuse_mount(archiver, mountpoint=None, *options, fork=True, os_fork=False, **kwargs):\n    # For a successful mount, `fork = True` is required for\n    # the borg mount daemon to work properly or the tests\n    # will just freeze. Therefore, if argument `fork` is not\n    # specified, the default value is `True`, regardless of\n    # `FORK_DEFAULT`. However, leaving the possibility to run\n    # the command with `fork = False` is still necessary for\n    # testing for mount failures, for example attempting to\n    # mount a read-only repository.\n    #    `os_fork = True` is needed for testing (the absence of)\n    # a race condition of the Lock during lock migration when\n    # borg mount (local repo) is daemonizing (#4953). This is another\n    # example where we need `fork = False`, because the test case\n    # needs an OS fork, not a spawning of the fuse mount.\n    # `fork = False` is implied if `os_fork = True`.\n    if mountpoint is None:\n        mountpoint = tempfile.mkdtemp()\n    else:\n        os.mkdir(mountpoint)\n    args = [\"mount\", mountpoint] + list(options)\n    if os_fork:\n        # Do not spawn, but actually (OS) fork.\n        if os.fork() == 0:\n            # The child process.\n            # Decouple from parent and fork again.\n            # Otherwise, it becomes a zombie and pretends to be alive.\n            os.setsid()\n            if os.fork() > 0:\n                os._exit(0)\n            # The grandchild process.\n            try:\n                cmd(archiver, *args, fork=False, **kwargs)  # borg mount not spawning.\n            finally:\n                # This should never be reached, since it daemonizes,\n                # and the grandchild process exits before cmd() returns.\n                # However, just in case...\n                print(\"Fatal: borg mount did not daemonize properly. Force exiting.\", file=sys.stderr, flush=True)\n                os._exit(0)\n    else:\n        cmd(archiver, *args, fork=fork, **kwargs)\n        if kwargs.get(\"exit_code\", EXIT_SUCCESS) == EXIT_ERROR:\n            # If argument `exit_code = EXIT_ERROR`, then this call\n            # is testing the behavior of an unsuccessful mount, and\n            # we must not continue, as there is no mount to work\n            # with. The test itself has already failed or succeeded\n            # with the call to `cmd`, above.\n            yield\n            return\n    try:\n        wait_for_mountstate(mountpoint, mounted=True)\n        yield\n    finally:\n        umount(mountpoint)\n        wait_for_mountstate(mountpoint, mounted=False)\n        os.rmdir(mountpoint)\n    # Give the daemon some time to exit\n    time.sleep(0.2)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/analyze_cmd_test.py",
    "content": "import pathlib\n\nfrom ...constants import *  # NOQA\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local\")  # NOQA\n\n\ndef test_analyze(archivers, request):\n    def create_archive():\n        cmd(archiver, \"create\", \"archive\", archiver.input_path)\n\n    def analyze_archives():\n        return cmd(archiver, \"analyze\", \"-a\", \"archive\")\n\n    archiver = request.getfixturevalue(archivers)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    input_path = pathlib.Path(archiver.input_path)\n\n    # 1st archive\n    (input_path / \"file1\").write_text(\"1\")\n    create_archive()\n\n    # 2nd archive\n    (input_path / \"file2\").write_text(\"22\")\n    create_archive()\n\n    assert \"/input: 2\" in analyze_archives()  # 2nd archive added 1 chunk for input path\n\n    # 3rd archive\n    (input_path / \"file3\").write_text(\"333\")\n    create_archive()\n\n    assert \"/input: 5\" in analyze_archives()  # 2nd/3rd archives added 2 chunks for input path\n\n    # 4th archive\n    (input_path / \"file2\").unlink()\n    create_archive()\n\n    assert \"/input: 7\" in analyze_archives()  # 2nd/3rd archives added 2, 4th archive removed 1\n"
  },
  {
    "path": "src/borg/testsuite/archiver/argparsing_test.py",
    "content": "import pytest\n\nfrom . import Archiver, RK_ENCRYPTION, cmd\nfrom ...helpers.argparsing import ArgumentParser, flatten_namespace\n\n\ndef test_bad_filters(archiver):\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"delete\", \"--first\", \"1\", \"--last\", \"1\", fork=True, exit_code=2)\n\n\ndef test_highlander(archiver):\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--comment\", \"comment 1\", \"test-1\", __file__)\n    error_msg = \"There can be only one\"\n    # Default umask value is 0077\n    # Test that it works with a one-time specified default or custom value\n    output_default = cmd(archiver, \"--umask\", \"0077\", \"repo-list\")\n    assert error_msg not in output_default\n    output_custom = cmd(archiver, \"--umask\", \"0007\", \"repo-list\")\n    assert error_msg not in output_custom\n    # Test that all combinations of custom and default values fail\n    for first, second in [(\"0007\", \"0007\"), (\"0007\", \"0077\"), (\"0077\", \"0007\"), (\"0077\", \"0077\")]:\n        output_custom = cmd(archiver, \"--umask\", first, \"--umask\", second, \"repo-list\", exit_code=2)\n        assert error_msg in output_custom\n\n\ndef test_get_args():\n    archiver = Archiver()\n    # everything normal:\n    # first param is argv as produced by ssh forced command,\n    # second param is like from SSH_ORIGINAL_COMMAND env variable\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--umask=0027\", \"--restrict-to-path=/p1\", \"--restrict-to-path=/p2\"], \"borg serve --info\"\n    )\n    assert args.func == archiver.do_serve\n    assert args.restrict_to_paths == [\"/p1\", \"/p2\"]\n    assert args.umask == 0o027\n    assert args.log_level == \"info\"\n    # similar, but with --restrict-to-repository\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--restrict-to-repository=/r1\", \"--restrict-to-repository=/r2\"],\n        \"borg serve --info --umask=0027\",\n    )\n    assert args.restrict_to_repositories == [\"/r1\", \"/r2\"]\n    # trying to cheat - break out of path restriction\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--restrict-to-path=/p1\", \"--restrict-to-path=/p2\"], \"borg serve --restrict-to-path=/\"\n    )\n    assert args.restrict_to_paths == [\"/p1\", \"/p2\"]\n    # trying to cheat - break out of repository restriction\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--restrict-to-repository=/r1\", \"--restrict-to-repository=/r2\"],\n        \"borg serve --restrict-to-repository=/\",\n    )\n    assert args.restrict_to_repositories == [\"/r1\", \"/r2\"]\n    # trying to cheat - break below repository restriction\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--restrict-to-repository=/r1\", \"--restrict-to-repository=/r2\"],\n        \"borg serve --restrict-to-repository=/r1/below\",\n    )\n    assert args.restrict_to_repositories == [\"/r1\", \"/r2\"]\n    # trying to cheat - try to execute different subcommand\n    args = archiver.get_args(\n        [\"borg\", \"serve\", \"--restrict-to-path=/p1\", \"--restrict-to-path=/p2\"],\n        f\"borg --repo=/ repo-create {RK_ENCRYPTION}\",\n    )\n    assert args.func == archiver.do_serve\n\n    # Check that environment variables in the forced command don't cause issues. If the command\n    # were not forced, environment variables would be interpreted by the shell, but this does not\n    # happen for forced commands - we get the verbatim command line and need to deal with env vars.\n    args = archiver.get_args([\"borg\", \"serve\"], \"BORG_FOO=bar borg serve --info\")\n    assert args.func == archiver.do_serve\n\n\nclass TestCommonOptions:\n    @staticmethod\n    def define_common_options(add_common_option):\n        add_common_option(\"-h\", \"--help\", action=\"help\", help=\"show this help message and exit\")\n        add_common_option(\n            \"--critical\", dest=\"log_level\", help=\"foo\", action=\"store_const\", const=\"critical\", default=\"warning\"\n        )\n        add_common_option(\n            \"--error\", dest=\"log_level\", help=\"foo\", action=\"store_const\", const=\"error\", default=\"warning\"\n        )\n        add_common_option(\"--append\", dest=\"append\", help=\"foo\", action=\"append\", metavar=\"TOPIC\", default=[])\n        add_common_option(\"-p\", \"--progress\", dest=\"progress\", action=\"store_true\", help=\"foo\")\n        add_common_option(\n            \"--lock-wait\", dest=\"lock_wait\", type=int, metavar=\"N\", default=1, help=\"(default: %(default)d).\"\n        )\n\n    @pytest.fixture\n    def basic_parser(self):\n        parser = ArgumentParser(prog=\"test\", description=\"test parser\")\n        parser.common_options = Archiver.CommonOptions(self.define_common_options)\n        return parser\n\n    @pytest.fixture\n    def subcommands(self, basic_parser):\n        return basic_parser.add_subcommands(required=False, title=\"required arguments\", metavar=\"<command>\")\n\n    @pytest.fixture\n    def parser(self, basic_parser):\n        basic_parser.common_options.add_common_group(basic_parser, provide_defaults=True)\n        return basic_parser\n\n    @pytest.fixture\n    def common_parser(self, parser):\n        common_parser = ArgumentParser(prog=\"test\")\n        parser.common_options.add_common_group(common_parser)\n        return common_parser\n\n    @pytest.fixture\n    def parse_vars_from_line(self, parser, subcommands, common_parser):\n        subparser = ArgumentParser(parents=[common_parser], description=\"foo\", epilog=\"bar\")\n        subparser.add_argument(\"--foo-bar\", dest=\"foo_bar\", action=\"store_true\")\n        subcommands.add_subcommand(\"subcmd\", subparser, help=\"baz\")\n\n        def parse_vars_from_line(*line):\n            print(line)\n            args = parser.parse_args(line)\n            args = flatten_namespace(args)\n            return vars(args)\n\n        return parse_vars_from_line\n\n    def test_simple(self, parse_vars_from_line):\n        assert parse_vars_from_line(\"--error\") == {\n            \"append\": [],\n            \"lock_wait\": 1,\n            \"log_level\": \"error\",\n            \"progress\": False,\n        }\n\n        assert parse_vars_from_line(\"--error\", \"subcmd\", \"--critical\") == {\n            \"append\": [],\n            \"lock_wait\": 1,\n            \"log_level\": \"critical\",\n            \"progress\": False,\n            \"foo_bar\": False,\n            \"subcommand\": \"subcmd\",\n        }\n\n        with pytest.raises(SystemExit):\n            parse_vars_from_line(\"--foo-bar\", \"subcmd\")\n\n        assert parse_vars_from_line(\"--append=foo\", \"--append\", \"bar\", \"subcmd\", \"--append\", \"baz\") == {\n            \"append\": [\"foo\", \"bar\", \"baz\"],\n            \"lock_wait\": 1,\n            \"log_level\": \"warning\",\n            \"progress\": False,\n            \"foo_bar\": False,\n            \"subcommand\": \"subcmd\",\n        }\n\n    @pytest.mark.parametrize(\"position\", (\"before\", \"after\", \"both\"))\n    @pytest.mark.parametrize(\"flag,args_key,args_value\", ((\"-p\", \"progress\", True), (\"--lock-wait=3\", \"lock_wait\", 3)))\n    def test_flag_position_independence(self, parse_vars_from_line, position, flag, args_key, args_value):\n        line = []\n        if position in (\"before\", \"both\"):\n            line.append(flag)\n        line.append(\"subcmd\")\n        if position in (\"after\", \"both\"):\n            line.append(flag)\n\n        result = {\n            \"append\": [],\n            \"lock_wait\": 1,\n            \"log_level\": \"warning\",\n            \"progress\": False,\n            \"foo_bar\": False,\n            \"subcommand\": \"subcmd\",\n            args_key: args_value,\n        }\n\n        assert parse_vars_from_line(*line) == result\n"
  },
  {
    "path": "src/borg/testsuite/archiver/benchmark_cmd_test.py",
    "content": "import json\n\nfrom ...constants import *  # NOQA\nfrom . import cmd, RK_ENCRYPTION\n\n\ndef test_benchmark_crud(archiver, monkeypatch):\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    monkeypatch.setenv(\"_BORG_BENCHMARK_CRUD_TEST\", \"YES\")\n    output = cmd(archiver, \"benchmark\", \"crud\", archiver.input_path)\n    # Verify human-readable output contains expected C/R/U/D lines with MB/s\n    for prefix in (\"C-Z-TEST\", \"R-Z-TEST\", \"U-Z-TEST\", \"D-Z-TEST\", \"C-R-TEST\", \"R-R-TEST\", \"U-R-TEST\", \"D-R-TEST\"):\n        assert prefix in output\n    assert \"MB/s\" in output\n\n\ndef test_benchmark_crud_json_lines(archiver, monkeypatch):\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    monkeypatch.setenv(\"_BORG_BENCHMARK_CRUD_TEST\", \"YES\")\n    output = cmd(archiver, \"benchmark\", \"crud\", \"--json-lines\", archiver.input_path)\n    # Filter for JSON lines only; the test harness merges stdout and stderr,\n    # so non-JSON messages (e.g. \"Done. Run borg compact...\") from inner\n    # commands may appear in the captured output.\n    lines = [line for line in output.splitlines() if line.strip().startswith(\"{\")]\n    # 2 test samples (Z-TEST, R-TEST) x 4 operations (C, R, U, D) = 8 lines\n    assert len(lines) == 8\n    entries = [json.loads(line) for line in lines]\n    # Verify all expected id values are present\n    expected_ids = {\"C-Z-TEST\", \"R-Z-TEST\", \"U-Z-TEST\", \"D-Z-TEST\", \"C-R-TEST\", \"R-R-TEST\", \"U-R-TEST\", \"D-R-TEST\"}\n    actual_ids = {e[\"id\"] for e in entries}\n    assert actual_ids == expected_ids\n    for entry in entries:\n        assert isinstance(entry[\"id\"], str)\n        assert entry[\"command\"] in (\"create1\", \"extract\", \"create2\", \"delete\")\n        assert isinstance(entry[\"sample\"], str)\n        assert entry[\"sample\"] in (\"Z-TEST\", \"R-TEST\")\n        assert isinstance(entry[\"sample_count\"], int)\n        assert entry[\"sample_count\"] == 1\n        assert isinstance(entry[\"sample_size\"], int)\n        assert entry[\"sample_size\"] == 1\n        assert isinstance(entry[\"sample_random\"], bool)\n        assert isinstance(entry[\"time\"], float)\n        assert entry[\"time\"] > 0\n        assert isinstance(entry[\"io\"], int)\n        assert entry[\"io\"] > 0\n\n\ndef test_benchmark_cpu(archiver, monkeypatch):\n    monkeypatch.setenv(\"_BORG_BENCHMARK_CPU_TEST\", \"YES\")\n    output = cmd(archiver, \"benchmark\", \"cpu\")\n    # verify all section headers appear in the plain-text output\n    assert \"Chunkers\" in output\n    assert \"Non-cryptographic checksums / hashes\" in output\n    assert \"Cryptographic hashes / MACs\" in output\n    assert \"Encryption\" in output\n    assert \"KDFs\" in output\n    assert \"Compression\" in output\n    assert \"msgpack\" in output\n\n\ndef test_benchmark_cpu_json(archiver, monkeypatch):\n    monkeypatch.setenv(\"_BORG_BENCHMARK_CPU_TEST\", \"YES\")\n    output = cmd(archiver, \"benchmark\", \"cpu\", \"--json\")\n    result = json.loads(output)\n    assert isinstance(result, dict)\n    # categories with \"size\" field (bytes)\n    for category in [\"chunkers\", \"checksums\", \"hashes\", \"encryption\"]:\n        assert isinstance(result[category], list)\n        assert len(result[category]) > 0\n        for entry in result[category]:\n            assert isinstance(entry[\"algo\"], str)\n            assert isinstance(entry[\"size\"], int)\n            assert isinstance(entry[\"time\"], float)\n    # chunkers and compression also have algo_params\n    for category in [\"chunkers\", \"compression\"]:\n        for entry in result[category]:\n            assert \"algo_params\" in entry\n    # categories with \"count\" field\n    for category in [\"kdf\", \"msgpack\"]:\n        assert isinstance(result[category], list)\n        assert len(result[category]) > 0\n        for entry in result[category]:\n            assert isinstance(entry[\"algo\"], str)\n            assert isinstance(entry[\"count\"], int)\n            assert isinstance(entry[\"time\"], float)\n    # compression has size field too\n    for entry in result[\"compression\"]:\n        assert isinstance(entry[\"size\"], int)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/check_cmd_test.py",
    "content": "from datetime import datetime, timezone, timedelta\nfrom pathlib import Path\nimport shutil\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom ...archive import ChunkBuffer\nfrom ...constants import *  # NOQA\nfrom ...helpers import bin_to_hex, msgpack\nfrom ...manifest import Manifest\nfrom ...remote import RemoteRepository\nfrom ...repository import Repository\nfrom ..repository_test import fchunk\nfrom . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef check_cmd_setup(archiver):\n    with patch.object(ChunkBuffer, \"BUFFER_SIZE\", 10):\n        cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n        create_src_archive(archiver, \"archive1\")\n        create_src_archive(archiver, \"archive2\")\n\n\ndef test_check_usage(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n\n    output = cmd(archiver, \"check\", \"-v\", \"--progress\", exit_code=0)\n    assert \"Starting full repository check\" in output\n    assert \"Starting archive consistency check\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--repository-only\", exit_code=0)\n    assert \"Starting full repository check\" in output\n    assert \"Starting archive consistency check\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", exit_code=0)\n    assert \"Starting full repository check\" not in output\n    assert \"Starting archive consistency check\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--match-archives=archive2\", exit_code=0)\n    assert \"archive1\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--first=1\", exit_code=0)\n    assert \"archive1\" in output\n    assert \"archive2\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--last=1\", exit_code=0)\n    assert \"archive1\" not in output\n    assert \"archive2\" in output\n\n\ndef test_date_matching(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive-2022-11-20\", ts=\"2022-11-20T23:59:59\")\n    create_src_archive(archiver, \"archive-2022-12-18\", ts=\"2022-12-18T23:59:59\")\n    create_src_archive(archiver, \"archive-now\")\n    cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--oldest=23e\", exit_code=2)\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--oldest=1y\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newest=1y\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--oldest=1m\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newest=1m\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--oldest=4w\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newest=4w\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newer=1d\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--older=1d\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newer=24H\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--older=24H\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newer=1440M\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--older=1440M\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--newer=86400S\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--older=86400S\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    # Check for output when a time span older than the earliest archive is given. Issue #1711\n    output = cmd(archiver, \"check\", \"-v\", \"--archives-only\", \"--older=9999m\", exit_code=0)\n    for archive in (\"archive1\", \"archive2\", \"archive3\"):\n        assert archive not in output\n\n\ndef test_missing_file_chunk(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n\n    with repository:\n        for item in archive.iter_items():\n            if item.path.endswith(src_file):\n                valid_chunks = item.chunks\n                killed_chunk = valid_chunks[-1]\n                repository.delete(killed_chunk.id)\n                break\n        else:\n            pytest.fail(\"should not happen\")  # convert 'fail'\n\n    output = cmd(archiver, \"check\", exit_code=1)\n    assert \"Missing file chunk detected\" in output\n    output = cmd(archiver, \"check\", \"--repair\", exit_code=0)\n    assert \"Missing file chunk detected\" in output  # repair is not changing anything, just reporting.\n\n    # check does not modify the chunks list.\n    for archive_name in (\"archive1\", \"archive2\"):\n        archive, repository = open_archive(archiver.repository_path, archive_name)\n        with repository:\n            for item in archive.iter_items():\n                if item.path.endswith(src_file):\n                    assert len(valid_chunks) == len(item.chunks)\n                    assert valid_chunks == item.chunks\n                    break\n            else:\n                pytest.fail(\"should not happen\")  # convert 'fail'\n\n    # do a fresh backup (that will include the killed chunk)\n    with patch.object(ChunkBuffer, \"BUFFER_SIZE\", 10):\n        create_src_archive(archiver, \"archive3\")\n\n    # check should not complain anymore about missing chunks:\n    output = cmd(archiver, \"check\", \"-v\", \"--repair\", exit_code=0)\n    assert \"Missing file chunk detected\" not in output\n\n\ndef test_missing_archive_item_chunk(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        repository.delete(archive.metadata.items[0])\n    cmd(archiver, \"check\", exit_code=1)\n    cmd(archiver, \"check\", \"--repair\", exit_code=0)\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_missing_archive_metadata(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        repository.delete(archive.id)\n    cmd(archiver, \"check\", exit_code=1)\n    cmd(archiver, \"check\", \"--repair\", exit_code=0)\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_missing_manifest(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        if isinstance(repository, (Repository, RemoteRepository)):\n            repository.store_delete(\"config/manifest\")\n        else:\n            repository.delete(Manifest.MANIFEST_ID)\n    cmd(archiver, \"check\", exit_code=1)\n    output = cmd(archiver, \"check\", \"-v\", \"--repair\", exit_code=0)\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_corrupted_manifest(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        manifest = repository.get_manifest()\n        corrupted_manifest = manifest[:123] + b\"corrupted!\" + manifest[123:]\n        repository.put_manifest(corrupted_manifest)\n    cmd(archiver, \"check\", exit_code=1)\n    output = cmd(archiver, \"check\", \"-v\", \"--repair\", exit_code=0)\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_spoofed_manifest(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        cdata = manifest.repo_objs.format(\n            Manifest.MANIFEST_ID,\n            {},\n            msgpack.packb(\n                {\n                    \"version\": 1,\n                    \"archives\": {},\n                    \"config\": {},\n                    \"timestamp\": (datetime.now(tz=timezone.utc) + timedelta(days=1)).isoformat(timespec=\"microseconds\"),\n                }\n            ),\n            # we assume that an attacker can put a file into backup src files that contains a fake manifest.\n            # but, the attacker can not influence the ro_type borg will use to store user file data:\n            ro_type=ROBJ_FILE_STREAM,  # a real manifest is stored with ROBJ_MANIFEST\n        )\n        # maybe a repo-side attacker could manage to move the fake manifest file chunk over to the manifest ID.\n        # we simulate this here by directly writing the fake manifest data to the manifest ID.\n        repository.put_manifest(cdata)\n    # borg should notice that the manifest has the wrong ro_type.\n    cmd(archiver, \"check\", exit_code=1)\n    # borg check --repair should remove the corrupted manifest and rebuild a new one.\n    output = cmd(archiver, \"check\", \"-v\", \"--repair\", exit_code=0)\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_manifest_rebuild_corrupted_chunk(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        manifest = repository.get_manifest()\n        corrupted_manifest = manifest[:123] + b\"corrupted!\" + manifest[123:]\n        repository.put_manifest(corrupted_manifest)\n        chunk = repository.get(archive.id)\n        corrupted_chunk = chunk + b\"corrupted!\"\n        repository.put(archive.id, corrupted_chunk)\n    cmd(archiver, \"check\", exit_code=1)\n    output = cmd(archiver, \"check\", \"-v\", \"--repair\", exit_code=0)\n    assert \"archive2\" in output\n    cmd(archiver, \"check\", exit_code=0)\n\n\ndef test_check_undelete_archives(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)  # creates archive1 and archive2\n    existing_archive_ids = set(cmd(archiver, \"repo-list\", \"--short\").splitlines())\n    create_src_archive(archiver, \"archive3\")\n    archive_ids = set(cmd(archiver, \"repo-list\", \"--short\").splitlines())\n    new_archive_id_hex = (archive_ids - existing_archive_ids).pop()\n    (Path(archiver.repository_path) / \"archives\" / new_archive_id_hex).unlink()  # lose the entry for archive3\n    output = cmd(archiver, \"repo-list\")\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    assert \"archive3\" not in output\n    # borg check will re-discover archive3 and create a new archives directory entry.\n    cmd(archiver, \"check\", \"--repair\", \"--find-lost-archives\", exit_code=0)\n    output = cmd(archiver, \"repo-list\")\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    assert \"archive3\" in output\n\n\ndef test_spoofed_archive(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    repo_objs = archive.repo_objs\n    with repository:\n        # attacker would corrupt or delete the manifest to trigger a rebuild of it:\n        manifest = repository.get_manifest()\n        corrupted_manifest = manifest[:123] + b\"corrupted!\" + manifest[123:]\n        repository.put_manifest(corrupted_manifest)\n        archive_dict = {\n            \"command_line\": \"\",\n            \"item_ptrs\": [],\n            \"hostname\": \"foo\",\n            \"username\": \"bar\",\n            \"name\": \"archive_spoofed\",\n            \"time\": \"2016-12-15T18:49:51.849711\",\n            \"version\": 2,\n        }\n        archive = repo_objs.key.pack_metadata(archive_dict)\n        archive_id = repo_objs.id_hash(archive)\n        repository.put(\n            archive_id,\n            repo_objs.format(\n                archive_id,\n                {},\n                archive,\n                # we assume that an attacker can put a file into backup src files that contains a fake archive.\n                # but, the attacker can not influence the ro_type borg will use to store user file data:\n                ro_type=ROBJ_FILE_STREAM,  # a real archive is stored with ROBJ_ARCHIVE_META\n            ),\n        )\n    cmd(archiver, \"check\", exit_code=1)\n    cmd(archiver, \"check\", \"--repair\", \"--debug\", exit_code=0)\n    output = cmd(archiver, \"repo-list\")\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n    assert \"archive_spoofed\" not in output\n\n\ndef test_extra_chunks(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() == \"remote\":\n        pytest.skip(\"only works locally\")\n    check_cmd_setup(archiver)\n    cmd(archiver, \"check\", exit_code=0)\n    with Repository(archiver.repository_location, exclusive=True) as repository:\n        chunk = fchunk(b\"xxxx\")\n        repository.put(b\"01234567890123456789012345678901\", chunk)\n    cmd(archiver, \"check\", \"-v\", exit_code=0)  # check does not deal with orphans anymore\n\n\n@pytest.mark.parametrize(\"init_args\", [[\"--encryption=repokey-aes-ocb\"], [\"--encryption\", \"none\"]])\ndef test_verify_data(archivers, request, init_args):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() != \"local\":\n        pytest.skip(\"only works locally, patches objects\")\n\n    # it's tricky to test the cryptographic data verification, because usually already the\n    # repository-level xxh64 hash fails to verify. So we use a fake one that doesn't.\n    # note: it only works like tested here for a highly engineered data corruption attack,\n    # because with accidental corruption, usually already the xxh64 low-level check fails.\n    def fake_xxh64(data, seed=0):\n        # xxhash.xxh64.digest() returns -> bytes\n        class FakeDigest:\n            def digest(self):\n                return b\"fakefake\"\n\n        return FakeDigest()\n\n    import borg.repoobj\n    import borg.repository\n\n    with patch.object(borg.repoobj, \"xxh64\", fake_xxh64), patch.object(borg.repository, \"xxh64\", fake_xxh64):\n        check_cmd_setup(archiver)\n        shutil.rmtree(archiver.repository_path)\n        cmd(archiver, \"repo-create\", *init_args)\n        create_src_archive(archiver, \"archive1\")\n        archive, repository = open_archive(archiver.repository_path, \"archive1\")\n        with repository:\n            for item in archive.iter_items():\n                if item.path.endswith(src_file):\n                    chunk = item.chunks[-1]\n                    data = repository.get(chunk.id)\n                    data = data[0:123] + b\"x\" + data[123:]\n                    repository.put(chunk.id, data)\n                    break\n\n        # the normal archives check does not read file content data.\n        cmd(archiver, \"check\", \"--archives-only\", exit_code=0)\n        # but with --verify-data, it does and notices the issue.\n        output = cmd(archiver, \"check\", \"--archives-only\", \"--verify-data\", exit_code=1)\n        assert f\"{bin_to_hex(chunk.id)}, integrity error\" in output\n\n        # repair will find the defect chunk and remove it\n        output = cmd(archiver, \"check\", \"--repair\", \"--verify-data\", exit_code=0)\n        assert f\"{bin_to_hex(chunk.id)}, integrity error\" in output\n        assert f\"{src_file}: Missing file chunk detected\" in output\n\n        # run with --verify-data again, it will notice the missing chunk.\n        output = cmd(archiver, \"check\", \"--archives-only\", \"--verify-data\", exit_code=1)\n        assert f\"{src_file}: Missing file chunk detected\" in output\n\n\n@pytest.mark.parametrize(\"init_args\", [[\"--encryption=repokey-aes-ocb\"], [\"--encryption\", \"none\"]])\ndef test_corrupted_file_chunk(archivers, request, init_args):\n    ## similar to test_verify_data, but here we let the low level repository-only checks discover the issue.\n\n    archiver = request.getfixturevalue(archivers)\n    check_cmd_setup(archiver)\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", *init_args)\n    create_src_archive(archiver, \"archive1\")\n    archive, repository = open_archive(archiver.repository_path, \"archive1\")\n    with repository:\n        for item in archive.iter_items():\n            if item.path.endswith(src_file):\n                chunk = item.chunks[-1]\n                data = repository.get(chunk.id)\n                data = data[0:123] + b\"x\" + data[123:]\n                repository.put(chunk.id, data)\n                break\n\n    # the normal check checks all repository objects and the xxh64 checksum fails.\n    output = cmd(archiver, \"check\", \"--repository-only\", exit_code=1)\n    assert f\"{bin_to_hex(chunk.id)} is corrupted: data does not match checksum.\" in output\n\n    # repair: the defect chunk will be removed by repair.\n    output = cmd(archiver, \"check\", \"--repair\", exit_code=0)\n    assert f\"{bin_to_hex(chunk.id)} is corrupted: data does not match checksum.\" in output\n    assert f\"{src_file}: Missing file chunk detected\" in output\n\n    # run normal check again\n    cmd(archiver, \"check\", \"--repository-only\", exit_code=0)\n    output = cmd(archiver, \"check\", \"--archives-only\", exit_code=1)\n    assert f\"{src_file}: Missing file chunk detected\" in output\n\n\ndef test_empty_repository(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() == \"remote\":\n        pytest.skip(\"only works locally\")\n    check_cmd_setup(archiver)\n    with Repository(archiver.repository_location, exclusive=True) as repository:\n        for id, _ in repository.list():\n            repository.delete(id)\n    cmd(archiver, \"check\", exit_code=1)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/checks_test.py",
    "content": "import os\nimport shutil\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom ...cache import Cache\nfrom ...constants import *  # NOQA\nfrom ...helpers import Location, get_security_dir, bin_to_hex\nfrom ...helpers import EXIT_ERROR\nfrom ...manifest import Manifest, MandatoryFeatureUnsupported\nfrom ...remote import RemoteRepository, PathNotAllowed\nfrom ...repository import Repository\nfrom .. import llfuse\nfrom .. import changedir\nfrom . import cmd, _extract_repository_id, create_test_files\nfrom . import _set_repository_id, create_regular_file, assert_creates_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote\")  # NOQA\n\n\ndef get_security_directory(repo_path):\n    repository_id = bin_to_hex(_extract_repository_id(repo_path))\n    return get_security_dir(repository_id)\n\n\ndef add_unknown_feature(repo_path, operation):\n    with Repository(repo_path, exclusive=True) as repository:\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        manifest.config[\"feature_flags\"] = {operation.value: {\"mandatory\": [\"unknown-feature\"]}}\n        manifest.write()\n\n\ndef cmd_raises_unknown_feature(archiver, args):\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, *args, exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(MandatoryFeatureUnsupported) as excinfo:\n            cmd(archiver, *args)\n        assert excinfo.value.args == ([\"unknown-feature\"],)\n\n\ndef test_repository_swap_detection(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.environ[\"BORG_PASSPHRASE\"] = \"passphrase\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    repository_id = _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    _set_repository_id(archiver.repository_path, repository_id)\n    assert repository_id == _extract_repository_id(archiver.repository_path)\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test.2\", \"input\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.EncryptionMethodMismatch):\n            cmd(archiver, \"create\", \"test.2\", \"input\")\n\n\ndef test_repository_swap_detection2(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    original_location = archiver.repository_location\n    archiver.repository_location = original_location + \"_unencrypted\"\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    os.environ[\"BORG_PASSPHRASE\"] = \"passphrase\"\n    archiver.repository_location = original_location + \"_encrypted\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    shutil.rmtree(archiver.repository_path + \"_encrypted\")\n    os.replace(archiver.repository_path + \"_unencrypted\", archiver.repository_path + \"_encrypted\")\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test.2\", \"input\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.RepositoryAccessAborted):\n            cmd(archiver, \"create\", \"test.2\", \"input\")\n\n\ndef test_repository_swap_detection_no_cache(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.environ[\"BORG_PASSPHRASE\"] = \"passphrase\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    repository_id = _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    _set_repository_id(archiver.repository_path, repository_id)\n    assert repository_id == _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"repo-delete\", \"--cache-only\")\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test.2\", \"input\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.EncryptionMethodMismatch):\n            cmd(archiver, \"create\", \"test.2\", \"input\")\n\n\ndef test_repository_swap_detection2_no_cache(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    original_location = archiver.repository_location\n    create_test_files(archiver.input_path)\n    archiver.repository_location = original_location + \"_unencrypted\"\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    os.environ[\"BORG_PASSPHRASE\"] = \"passphrase\"\n    archiver.repository_location = original_location + \"_encrypted\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    archiver.repository_location = original_location + \"_unencrypted\"\n    cmd(archiver, \"repo-delete\", \"--cache-only\")\n    archiver.repository_location = original_location + \"_encrypted\"\n    cmd(archiver, \"repo-delete\", \"--cache-only\")\n    shutil.rmtree(archiver.repository_path + \"_encrypted\")\n    os.replace(archiver.repository_path + \"_unencrypted\", archiver.repository_path + \"_encrypted\")\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test.2\", \"input\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.RepositoryAccessAborted):\n            cmd(archiver, \"create\", \"test.2\", \"input\")\n\n\ndef test_repository_swap_detection_repokey_blank_passphrase(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    # Check that a repokey repo with a blank passphrase is considered like a plaintext repo.\n    create_test_files(archiver.input_path)\n    # User initializes her repository with her passphrase\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    # Attacker replaces it with her own repository, which is encrypted but has no passphrase set\n    shutil.rmtree(archiver.repository_path)\n\n    monkeypatch.setenv(\"BORG_PASSPHRASE\", \"\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Delete cache & security database, AKA switch to user perspective\n    cmd(archiver, \"repo-delete\", \"--cache-only\")\n    shutil.rmtree(get_security_directory(archiver.repository_path))\n\n    monkeypatch.delenv(\"BORG_PASSPHRASE\")\n    # This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE\n    # is set, while it isn't. Previously this raised no warning,\n    # since the repository is, technically, encrypted.\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test.2\", \"input\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.CacheInitAbortedError):\n            cmd(archiver, \"create\", \"test.2\", \"input\")\n\n\ndef test_repository_move(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    security_dir = get_security_directory(archiver.repository_path)\n    os.replace(archiver.repository_path, archiver.repository_path + \"_new\")\n    archiver.repository_location += \"_new\"\n    # borg should notice that the repository location changed and abort.\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"repo-info\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.RepositoryAccessAborted):\n            cmd(archiver, \"repo-info\")\n    # if we explicitly allow relocated repos, it should work fine.\n    monkeypatch.setenv(\"BORG_RELOCATED_REPO_ACCESS_IS_OK\", \"yes\")\n    cmd(archiver, \"repo-info\")\n    monkeypatch.delenv(\"BORG_RELOCATED_REPO_ACCESS_IS_OK\")\n    with open(os.path.join(security_dir, \"location\")) as fd:\n        location = fd.read()\n        assert location == Location(archiver.repository_location).canonical_path()\n    # after new repo location was confirmed once, it needs no further confirmation anymore.\n    cmd(archiver, \"repo-info\")\n    shutil.rmtree(security_dir)\n    # it also needs no confirmation if we have no knowledge about the previous location.\n    cmd(archiver, \"repo-info\")\n    # it will re-create security-related infos in the security dir:\n    for file in (\"location\", \"key-type\", \"manifest-timestamp\"):\n        assert os.path.exists(os.path.join(security_dir, file))\n\n\ndef test_unknown_unencrypted(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    # Ok: repository is known\n    cmd(archiver, \"repo-info\")\n\n    # Ok: repository is still known (through security_dir)\n    shutil.rmtree(archiver.cache_path)\n    cmd(archiver, \"repo-info\")\n\n    # Needs confirmation: cache and security dir both gone (e.g. another host or rm -rf ~)\n    shutil.rmtree(get_security_directory(archiver.repository_path))\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"repo-info\", exit_code=EXIT_ERROR)\n    else:\n        with pytest.raises(Cache.CacheInitAbortedError):\n            cmd(archiver, \"repo-info\")\n    monkeypatch.setenv(\"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK\", \"yes\")\n    cmd(archiver, \"repo-info\")\n\n\ndef test_unknown_feature_on_create(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.WRITE)\n    cmd_raises_unknown_feature(archiver, [\"create\", \"test\", \"input\"])\n\n\ndef test_unknown_feature_on_change_passphrase(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.CHECK)\n    cmd_raises_unknown_feature(archiver, [\"key\", \"change-passphrase\"])\n\n\ndef test_unknown_feature_on_read(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n    cmd(archiver, \"create\", \"test\", \"input\")\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.READ)\n    with changedir(\"output\"):\n        cmd_raises_unknown_feature(archiver, [\"extract\", \"test\"])\n    cmd_raises_unknown_feature(archiver, [\"repo-list\"])\n    cmd_raises_unknown_feature(archiver, [\"info\", \"-a\", \"test\"])\n\n\ndef test_unknown_feature_on_rename(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n    cmd(archiver, \"create\", \"test\", \"input\")\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.CHECK)\n    cmd_raises_unknown_feature(archiver, [\"rename\", \"test\", \"other\"])\n\n\ndef test_unknown_feature_on_delete(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n    cmd(archiver, \"create\", \"test\", \"input\")\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.DELETE)\n    # delete of an archive raises\n    cmd_raises_unknown_feature(archiver, [\"delete\", \"-a\", \"test\"])\n    cmd_raises_unknown_feature(archiver, [\"prune\", \"--keep-daily=3\"])\n    # delete of the whole repository ignores features\n    cmd(archiver, \"repo-delete\")\n\n\n@pytest.mark.skipif(not llfuse, reason=\"llfuse not installed\")\ndef test_unknown_feature_on_mount(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    add_unknown_feature(archiver.repository_path, Manifest.Operation.READ)\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    os.mkdir(mountpoint)\n    # XXX this might hang if it doesn't raise an error\n    cmd_raises_unknown_feature(archiver, [\"mount\", mountpoint])\n\n\ndef test_unknown_mandatory_feature_in_cache(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    remote_repo = archiver.get_kind() == \"remote\"\n    print(cmd(archiver, \"repo-create\", RK_ENCRYPTION))\n\n    with Repository(archiver.repository_path, exclusive=True) as repository:\n        if remote_repo:\n            repository._location = Location(archiver.repository_location)\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        with Cache(repository, manifest) as cache:\n            cache.cache_config.mandatory_features = {\"unknown-feature\"}\n\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, \"create\", \"test\", \"input\")\n\n    with Repository(archiver.repository_path, exclusive=True) as repository:\n        if remote_repo:\n            repository._location = Location(archiver.repository_location)\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        with Cache(repository, manifest) as cache:\n            assert cache.cache_config.mandatory_features == set()\n\n\n# Begin Remote Tests\ndef test_remote_repo_restrict_to_path(remote_archiver):\n    original_location, repo_path = remote_archiver.repository_location, remote_archiver.repository_path\n    # restricted to repo directory itself:\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-path\", repo_path]):\n        cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    # restricted to repo directory itself, fail for other directories with same prefix:\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-path\", repo_path]):\n        with pytest.raises(PathNotAllowed):\n            remote_archiver.repository_location = original_location + \"_0\"\n            cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    # restricted to a completely different path:\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-path\", \"/foo\"]):\n        with pytest.raises(PathNotAllowed):\n            remote_archiver.repository_location = original_location + \"_1\"\n            cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    path_prefix = os.path.dirname(repo_path)\n    # restrict to repo directory's parent directory:\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-path\", path_prefix]):\n        remote_archiver.repository_location = original_location + \"_2\"\n        cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    # restrict to repo directory's parent directory and another directory:\n    with patch.object(\n        RemoteRepository, \"extra_test_args\", [\"--restrict-to-path\", \"/foo\", \"--restrict-to-path\", path_prefix]\n    ):\n        remote_archiver.repository_location = original_location + \"_3\"\n        cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n\n\ndef test_remote_repo_restrict_to_repository(remote_archiver):\n    repo_path = remote_archiver.repository_path\n    # restricted to repo directory itself:\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-repository\", repo_path]):\n        cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    parent_path = os.path.join(repo_path, \"..\")\n    with patch.object(RemoteRepository, \"extra_test_args\", [\"--restrict-to-repository\", parent_path]):\n        with pytest.raises(PathNotAllowed):\n            cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n\n\ndef test_remote_repo_strip_components_doesnt_leak(remote_archiver):\n    cmd(remote_archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(remote_archiver.input_path, \"dir/file\", contents=b\"test file contents 1\")\n    create_regular_file(remote_archiver.input_path, \"dir/file2\", contents=b\"test file contents 2\")\n    create_regular_file(remote_archiver.input_path, \"skipped-file1\", contents=b\"test file contents 3\")\n    create_regular_file(remote_archiver.input_path, \"skipped-file2\", contents=b\"test file contents 4\")\n    create_regular_file(remote_archiver.input_path, \"skipped-file3\", contents=b\"test file contents 5\")\n    cmd(remote_archiver, \"create\", \"test\", \"input\")\n    marker = \"cached responses left in RemoteRepository\"\n    with changedir(\"output\"):\n        res = cmd(remote_archiver, \"extract\", \"test\", \"--debug\", \"--strip-components\", \"3\")\n        assert marker not in res\n        with assert_creates_file(\"file\"):\n            res = cmd(remote_archiver, \"extract\", \"test\", \"--debug\", \"--strip-components\", \"2\")\n            assert marker not in res\n        with assert_creates_file(\"dir/file\"):\n            res = cmd(remote_archiver, \"extract\", \"test\", \"--debug\", \"--strip-components\", \"1\")\n            assert marker not in res\n        with assert_creates_file(\"input/dir/file\"):\n            res = cmd(remote_archiver, \"extract\", \"test\", \"--debug\", \"--strip-components\", \"0\")\n            assert marker not in res\n"
  },
  {
    "path": "src/borg/testsuite/archiver/compact_cmd_test.py",
    "content": "from pathlib import Path\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import get_cache_dir\nfrom ...cache import files_cache_name, discover_files_cache_names\nfrom . import cmd, create_src_archive, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\n@pytest.mark.parametrize(\"stats\", (True, False))\ndef test_compact_empty_repository(archivers, request, stats):\n    archiver = request.getfixturevalue(archivers)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    args = (\"-v\", \"--stats\") if stats else (\"-v\",)\n    output = cmd(archiver, \"compact\", *args, exit_code=0)\n    assert \"Starting compaction\" in output\n    if stats:\n        assert \"Repository size is 0 B in 0 objects.\" in output\n    else:\n        assert \"Repository has data stored in 0 objects.\" in output\n    assert \"Finished compaction\" in output\n\n\n@pytest.mark.parametrize(\"stats\", (True, False))\ndef test_compact_after_deleting_all_archives(archivers, request, stats):\n    archiver = request.getfixturevalue(archivers)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive\")\n    cmd(archiver, \"delete\", \"-a\", \"archive\", exit_code=0)\n\n    args = (\"-v\", \"--stats\") if stats else (\"-v\",)\n    output = cmd(archiver, \"compact\", *args, exit_code=0)\n    assert \"Starting compaction\" in output\n    assert \"Deleting \" in output\n    if stats:\n        assert \"Repository size is 0 B in 0 objects.\" in output\n    else:\n        assert \"Repository has data stored in 0 objects.\" in output\n    assert \"Finished compaction\" in output\n\n\n@pytest.mark.parametrize(\"stats\", (True, False))\ndef test_compact_after_deleting_some_archives(archivers, request, stats):\n    archiver = request.getfixturevalue(archivers)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive1\")\n    create_src_archive(archiver, \"archive2\")\n    cmd(archiver, \"delete\", \"-a\", \"archive1\", exit_code=0)\n\n    args = (\"-v\", \"--stats\") if stats else (\"-v\",)\n    output = cmd(archiver, \"compact\", *args, exit_code=0)\n    assert \"Starting compaction\" in output\n    assert \"Deleting \" in output\n    if stats:\n        assert \"Repository size is 0 B in 0 objects.\" not in output\n    else:\n        assert \"Repository has data stored in 0 objects.\" not in output\n    assert \"Finished compaction\" in output\n\n\ndef test_compact_index_corruption(archivers, request):\n    # see issue #8813 (borg did not write a complete index)\n    archiver = request.getfixturevalue(archivers)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive1\")\n\n    output = cmd(archiver, \"compact\", \"-v\", \"--stats\", exit_code=0)\n    assert \"missing objects\" not in output\n\n    output = cmd(archiver, \"compact\", \"-v\", exit_code=0)\n    assert \"missing objects\" not in output\n\n    output = cmd(archiver, \"compact\", \"-v\", exit_code=0)\n    assert \"missing objects\" not in output\n\n    output = cmd(archiver, \"compact\", \"-v\", \"--stats\", exit_code=0)\n    assert \"missing objects\" not in output\n\n\ndef test_compact_files_cache_cleanup(archivers, request):\n    \"\"\"Test that files cache files for deleted archives are removed during compact.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create repository and archives\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive1\")\n    create_src_archive(archiver, \"archive2\")\n    create_src_archive(archiver, \"archive3\")\n\n    # Get repository ID\n    output = cmd(archiver, \"repo-info\")\n    for line in output.splitlines():\n        if \"Repository ID:\" in line:\n            repo_id = line.split(\":\", 1)[1].strip()\n            break\n    else:\n        pytest.fail(\"Could not find repository ID in info output\")\n\n    # Check cache directory for files cache files\n    cache_dir = Path(get_cache_dir()) / repo_id\n    if not cache_dir.exists():\n        pytest.skip(\"Cache directory does not exist, skipping test\")\n\n    # Get initial files cache files\n    try:\n        initial_cache_files = set(discover_files_cache_names(cache_dir))\n    except (FileNotFoundError, PermissionError):\n        pytest.skip(\"Could not access cache directory, skipping test\")\n\n    # Get expected cache files for remaining archives\n    expected_cache_files = {files_cache_name(name) for name in [\"archive1\", \"archive2\", \"archive3\"]}\n    assert expected_cache_files == initial_cache_files, \"Unexpected cache files found\"\n\n    # Delete one archive\n    cmd(archiver, \"delete\", \"-a\", \"archive2\")\n\n    # Run compact\n    output = cmd(archiver, \"compact\", \"-v\")\n    assert \"Cleaning up files cache\" in output\n\n    # Check that files cache for deleted archive is removed\n    try:\n        remaining_cache_files = set(discover_files_cache_names(cache_dir))\n    except (FileNotFoundError, PermissionError):\n        pytest.fail(\"Could not access cache directory after compact\")\n\n    # Get expected cache files for remaining archives\n    expected_cache_files = {files_cache_name(name) for name in [\"archive1\", \"archive3\"]}\n    assert expected_cache_files == remaining_cache_files, \"Unexpected cache files found\"\n"
  },
  {
    "path": "src/borg/testsuite/archiver/completion_cmd_test.py",
    "content": "import functools\nimport os\nimport subprocess\nimport tempfile\n\nimport pytest\n\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local\")  # NOQA\n\n\n@functools.lru_cache\ndef cmd_available(cmd):\n    \"\"\"Check if a shell command is available.\"\"\"\n    try:\n        subprocess.run(cmd.split(), capture_output=True, check=True)\n        return True\n    except (subprocess.SubprocessError, FileNotFoundError):\n        return False\n\n\nneeds_bash = pytest.mark.skipif(not cmd_available(\"bash --version\"), reason=\"Bash not available\")\nneeds_zsh = pytest.mark.skipif(not cmd_available(\"zsh --version\"), reason=\"Zsh not available\")\n\n\ndef _run_bash_completion_fn(completion_script, setup_code):\n    \"\"\"Source the completion script in bash and run setup_code, return subprocess result.\"\"\"\n    with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".bash\", delete=False) as f:\n        f.write(completion_script)\n        script_path = f.name\n    try:\n        result = subprocess.run(\n            [\"bash\", \"-c\", f\"source {script_path}\\n{setup_code}\"], capture_output=True, text=True, timeout=120\n        )\n    finally:\n        os.unlink(script_path)\n    return result\n\n\n# -- output sanity checks -----------------------------------------------------\n\n\ndef test_bash_completion_nontrivial(archivers, request):\n    \"\"\"Verify the generated Bash completion is non-trivially sized.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    output = cmd(archiver, \"completion\", \"bash\")\n    assert len(output) > 5000, f\"Bash completion suspiciously small: {len(output)} chars\"\n    assert output.count(\"\\n\") > 100, f\"Bash completion suspiciously few lines: {output.count(chr(10))}\"\n\n\ndef test_zsh_completion_nontrivial(archivers, request):\n    \"\"\"Verify the generated Zsh completion is non-trivially sized.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    output = cmd(archiver, \"completion\", \"zsh\")\n    assert len(output) > 5000, f\"Zsh completion suspiciously small: {len(output)} chars\"\n    assert output.count(\"\\n\") > 100, f\"Zsh completion suspiciously few lines: {output.count(chr(10))}\"\n\n\n# -- syntax validation --------------------------------------------------------\n\n\ndef _check_shell_syntax(script_content, shell, suffix):\n    \"\"\"Write script_content to a temp file and verify syntax with ``shell -n``.\"\"\"\n    with tempfile.NamedTemporaryFile(mode=\"w\", suffix=suffix, delete=False) as f:\n        f.write(script_content)\n        script_path = f.name\n    try:\n        result = subprocess.run([shell, \"-n\", script_path], capture_output=True)\n    finally:\n        os.unlink(script_path)\n    return result\n\n\n@needs_bash\ndef test_bash_completion_syntax(archivers, request):\n    \"\"\"Verify the generated Bash completion script has valid syntax.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    output = cmd(archiver, \"completion\", \"bash\")\n    result = _check_shell_syntax(output, \"bash\", \".bash\")\n    assert result.returncode == 0, f\"Generated Bash completion has syntax errors: {result.stderr.decode()}\"\n\n\n@needs_zsh\ndef test_zsh_completion_syntax(archivers, request):\n    \"\"\"Verify the generated Zsh completion script has valid syntax.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    output = cmd(archiver, \"completion\", \"zsh\")\n    result = _check_shell_syntax(output, \"zsh\", \".zsh\")\n    assert result.returncode == 0, f\"Generated Zsh completion has syntax errors: {result.stderr.decode()}\"\n\n\n# -- borg-specific preamble function behavior (bash) --------------------------\n\n\n@needs_bash\ndef test_bash_sortby_dedup(archivers, request):\n    \"\"\"_borg_complete_sortby should not re-offer already-selected sort keys.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    script = cmd(archiver, \"completion\", \"bash\")\n\n    # Simulate: user typed \"borg repo-list --sort-by timestamp,\"\n    # The function should offer remaining keys but NOT \"timestamp\" again.\n    result = _run_bash_completion_fn(\n        script, 'COMP_WORDS=(borg repo-list --sort-by \"timestamp,\")\\n' \"COMP_CWORD=3\\n\" \"_borg_complete_sortby\\n\"\n    )\n    assert result.returncode == 0, f\"stderr: {result.stderr}\"\n    lines = [line for line in result.stdout.strip().splitlines() if line.strip()]\n    # \"timestamp\" must not appear as a standalone completion candidate\n    bare_keys = [line.rsplit(\",\", 1)[-1] for line in lines]\n    assert \"timestamp\" not in bare_keys, f\"timestamp was re-offered: {lines}\"\n    # Other keys like \"archive\" should be offered\n    assert any(\"archive\" in line for line in lines), f\"expected 'archive' in completions: {lines}\"\n\n\n@needs_bash\ndef test_bash_filescachemode_exclusivity(archivers, request):\n    \"\"\"_borg_complete_filescachemode should enforce ctime/mtime and disabled mutual exclusion.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    script = cmd(archiver, \"completion\", \"bash\")\n\n    # After selecting \"ctime,\", mtime should not be offered\n    result = _run_bash_completion_fn(\n        script, 'COMP_WORDS=(borg create --files-cache \"ctime,\")\\n' \"COMP_CWORD=3\\n\" \"_borg_complete_filescachemode\\n\"\n    )\n    assert result.returncode == 0, f\"stderr: {result.stderr}\"\n    bare_keys = [line.rsplit(\",\", 1)[-1] for line in result.stdout.strip().splitlines() if line.strip()]\n    assert \"mtime\" not in bare_keys, f\"mtime offered after ctime: {bare_keys}\"\n    assert \"disabled\" not in bare_keys, f\"disabled offered after ctime: {bare_keys}\"\n\n    # After selecting \"disabled,\", nothing should be offered\n    result2 = _run_bash_completion_fn(\n        script,\n        'COMP_WORDS=(borg create --files-cache \"disabled,\")\\n' \"COMP_CWORD=3\\n\" \"_borg_complete_filescachemode\\n\",\n    )\n    assert result2.returncode == 0\n    assert result2.stdout.strip() == \"\", f\"completions offered after disabled: {result2.stdout}\"\n\n    # After selecting \"size,\", disabled should not be offered\n    result3 = _run_bash_completion_fn(\n        script, 'COMP_WORDS=(borg create --files-cache \"size,\")\\n' \"COMP_CWORD=3\\n\" \"_borg_complete_filescachemode\\n\"\n    )\n    assert result3.returncode == 0\n    bare_keys3 = [line.rsplit(\",\", 1)[-1] for line in result3.stdout.strip().splitlines() if line.strip()]\n    assert \"disabled\" not in bare_keys3, f\"disabled offered after size: {bare_keys3}\"\n\n\n@needs_bash\ndef test_bash_archive_name_completion(archivers, request):\n    \"\"\"_borg_complete_archive should complete archive names from a real repo.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"mybackup-2024\", archiver.input_path)\n    cmd(archiver, \"create\", \"mybackup-2025\", archiver.input_path)\n\n    script = cmd(archiver, \"completion\", \"bash\")\n    repo = archiver.repository_path\n\n    result = _run_bash_completion_fn(\n        script, f'COMP_WORDS=(borg delete --repo \"{repo}\" \"mybackup\")\\n' f\"COMP_CWORD=4\\n\" f\"_borg_complete_archive\\n\"\n    )\n    assert result.returncode == 0, f\"stderr: {result.stderr}\"\n    assert \"mybackup-2024\" in result.stdout, f\"archive name missing: {result.stdout}\"\n    assert \"mybackup-2025\" in result.stdout, f\"archive name missing: {result.stdout}\"\n\n\n@needs_bash\ndef test_bash_archive_aid_completion(archivers, request):\n    \"\"\"_borg_complete_archive should complete aid: prefixed archive IDs.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"testarchive\", archiver.input_path)\n\n    script = cmd(archiver, \"completion\", \"bash\")\n    repo = archiver.repository_path\n\n    result = _run_bash_completion_fn(\n        script, f'COMP_WORDS=(borg info --repo \"{repo}\" \"aid:\")\\n' f\"COMP_CWORD=4\\n\" f\"_borg_complete_archive\\n\"\n    )\n    assert result.returncode == 0, f\"stderr: {result.stderr}\"\n    lines = [line for line in result.stdout.strip().splitlines() if line.strip()]\n    assert len(lines) >= 1, \"Expected at least one archive ID completion\"\n    for line in lines:\n        assert line.startswith(\"aid:\"), f\"Expected aid: prefix, got: {line}\"\n"
  },
  {
    "path": "src/borg/testsuite/archiver/corruption_test.py",
    "content": "import json\nimport os\nfrom configparser import ConfigParser\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import bin_to_hex\nfrom . import cmd, create_test_files, RK_ENCRYPTION\n\n\ndef corrupt_archiver(archiver):\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    archiver.cache_path = json.loads(cmd(archiver, \"repo-info\", \"--json\"))[\"cache\"].get(\"path\")\n\n\ndef test_old_version_interfered(archiver):\n    corrupt_archiver(archiver)\n    if archiver.cache_path is None:\n        pytest.skip(\"No cache path for this kind of cache implementation.\")\n\n    # Modify the main manifest ID without touching the manifest ID in the integrity section.\n    # This happens if a version without integrity checking modifies the cache.\n    config_path = os.path.join(archiver.cache_path, \"config\")\n    config = ConfigParser(interpolation=None)\n    config.read(config_path)\n    config.set(\"cache\", \"manifest\", bin_to_hex(bytes(32)))\n    with open(config_path, \"w\") as fd:\n        config.write(fd)\n    out = cmd(archiver, \"repo-info\")\n    assert \"Cache integrity data not available: old Borg version modified the cache.\" in out\n"
  },
  {
    "path": "src/borg/testsuite/archiver/create_cmd_test.py",
    "content": "import errno\nimport json\nimport os\nimport tempfile\nimport shutil\nimport socket\nimport stat\nimport subprocess\n\nimport pytest\n\nfrom ... import platform\nfrom ...constants import *  # NOQA\nfrom ...constants import zeros\nfrom ...manifest import Manifest\nfrom ...platform import is_win32\nfrom ...platformflags import is_msystem\nfrom ...repository import Repository\nfrom ...helpers import CommandError, BackupPermissionError\nfrom .. import has_lchflags, has_mknod\nfrom .. import changedir\nfrom .. import (\n    are_symlinks_supported,\n    are_hardlinks_supported,\n    are_fifos_supported,\n    is_utime_fully_supported,\n    is_birthtime_fully_supported,\n    same_ts_ns,\n    is_root,\n    granularity_sleep,\n)\nfrom . import (\n    cmd,\n    generate_archiver_tests,\n    create_test_files,\n    assert_dirs_equal,\n    create_regular_file,\n    requires_hardlinks,\n    _create_test_caches,\n    _create_test_tagged,\n    _create_test_keep_tagged,\n    _assert_test_caches,\n    _assert_test_tagged,\n    _assert_test_keep_tagged,\n    RK_ENCRYPTION,\n)\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_basic_functionality(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"test_basic_functionality seems incompatible with fakeroot and/or the binary.\")\n    have_root = create_test_files(archiver.input_path)\n    # Fork required to test --show-rc output.\n    output = cmd(archiver, \"repo-create\", RK_ENCRYPTION, \"--show-version\", \"--show-rc\", fork=True)\n    assert \"borgbackup version\" in output\n    assert \"terminating with success status, rc 0\" in output\n\n    cmd(archiver, \"create\", \"test\", \"input\")\n    output = cmd(archiver, \"create\", \"--stats\", \"test.2\", \"input\")\n    assert \"Archive name: test.2\" in output\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n\n    list_output = cmd(archiver, \"repo-list\")\n    assert \"test\" in list_output\n    assert \"test.2\" in list_output\n\n    expected = [\n        \"input\",\n        \"input/bdev\",\n        \"input/cdev\",\n        \"input/dir2\",\n        \"input/dir2/file2\",  # 1\n        \"input/empty\",  # 2\n        \"input/file1\",  # 3\n        \"input/flagfile\",  # 4\n        \"input/fusexattr\",  # 5\n    ]\n    item_count = 5  # we only count regular files\n    if are_fifos_supported():\n        expected.append(\"input/fifo1\")\n    if are_symlinks_supported():\n        expected.append(\"input/link1\")\n    if are_hardlinks_supported():\n        expected.append(\"input/hardlink\")\n        item_count += 1\n    if not have_root or not has_mknod:\n        # We could not create these device files without (fake)root.\n        expected.remove(\"input/bdev\")\n        expected.remove(\"input/cdev\")\n    if has_lchflags:\n        # remove the file we did not back up, so input and output become equal\n        expected.remove(\"input/flagfile\")  # this file is UF_NODUMP\n        os.remove(os.path.join(\"input\", \"flagfile\"))\n        item_count -= 1\n    list_output = cmd(archiver, \"list\", \"test\", \"--short\")\n    for name in expected:\n        assert name in list_output\n    assert_dirs_equal(\"input\", \"output/input\")\n\n    info_output = cmd(archiver, \"info\", \"-a\", \"test\")\n    print(\"archive contents:\\n%s\" % list_output)\n    assert \"Number of files: %d\" % item_count in info_output\n    shutil.rmtree(archiver.cache_path)\n    info_output2 = cmd(archiver, \"info\", \"-a\", \"test\")\n\n    def filter(output):\n        # Filter for interesting 'info' output; ignore cache-rebuilding related messages.\n        prefixes = [\"Name:\", \"Fingerprint:\", \"Number of files:\", \"This archive:\", \"All archives:\", \"Chunk index:\"]\n        result = []\n        for line in output.splitlines():\n            for prefix in prefixes:\n                if line.startswith(prefix):\n                    result.append(line)\n        return \"\\n\".join(result)\n\n    # The interesting parts of info_output2 and info_output should be the same.\n    assert filter(info_output) == filter(info_output2)\n\n\ndef test_archived_paths(archivers, request):\n    # As Borg comes from the POSIX (Linux/UNIX) world, much assumes path separators\n    # to be slashes \"/\", e.g., in archived items or for pattern matching.\n    # To make our lives easier and to support cross-platform extraction, we always use slashes.\n    # Similarly, archived paths are expected to be full but relative (have no leading slash).\n    archiver = request.getfixturevalue(archivers)\n    full_path = os.path.abspath(os.path.join(archiver.input_path, \"test\"))\n    # remove windows drive letter, if any:\n    posix_path = full_path[2:] if full_path[1] == \":\" else full_path\n    # only needed on Windows in case there are backslashes:\n    posix_path = posix_path.replace(\"\\\\\", \"/\")\n    # no leading slash in borg archives:\n    archived_path = posix_path.lstrip(\"/\")\n    create_regular_file(archiver.input_path, \"test\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"test\", \"input\", posix_path)\n    # \"input\" directory is recursed into, \"input/test\" is discovered and joined by borg's recursion.\n    # posix_path was directly given as a cli argument and should end up as archive_path in the borg archive.\n    expected_paths = sorted([\"input\", \"input/test\", archived_path])\n\n    # check path in archived items:\n    archive_list = cmd(archiver, \"list\", \"test\", \"--short\")\n    assert expected_paths == sorted([path for path in archive_list.splitlines() if path])\n\n    # check path in archived items (json):\n    archive_list = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    assert expected_paths == sorted([json.loads(line)[\"path\"] for line in archive_list.splitlines() if line])\n\n\n@pytest.mark.skipif(not is_msystem, reason=\"only for msystem\")\ndef test_create_msys2_path_translation_warning(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"test\")\n\n    # When MSYS2 path translation is active (variables NOT set), a warning should be emitted.\n    monkeypatch.delenv(\"MSYS2_ARG_CONV_EXCL\", raising=False)\n    monkeypatch.delenv(\"MSYS2_ENV_CONV_EXCL\", raising=False)\n    output = cmd(archiver, \"create\", \"test1\", \"input\", fork=True)\n    assert \"MSYS2 path translation is active.\" in output\n\n    # When the variables ARE set, the warning should not be emitted,\n    # and /tmp should be archived properly without being translated to msys64/tmp.\n    monkeypatch.setenv(\"MSYS2_ARG_CONV_EXCL\", \"*\")\n    monkeypatch.setenv(\"MSYS2_ENV_CONV_EXCL\", \"*\")\n\n    # We must create a real /tmp directory to avoid file not found errors,\n    # since we will pass '/tmp' directly to Borg\n    tmp_path = os.path.abspath(\"/tmp\")\n    os.makedirs(tmp_path, exist_ok=True)\n    test_filepath = os.path.join(tmp_path, \"borg_msys2_test_file\")\n    with open(test_filepath, \"w\") as f:\n        f.write(\"test\")\n\n    try:\n        output2 = cmd(archiver, \"create\", \"test2\", \"/tmp\", fork=True)\n        assert \"MSYS2 path translation is active.\" not in output2\n\n        archive_list = cmd(archiver, \"list\", \"test2\", \"--json-lines\")\n        paths = [json.loads(line)[\"path\"] for line in archive_list.splitlines() if line]\n\n        # Verify that msys64 is not present and paths start with tmp/\n        assert not any(\"msys64\" in p for p in paths)\n        assert any(p.startswith(\"tmp/borg_msys2_test_file\") for p in paths)\n    finally:\n        os.unlink(test_filepath)\n\n\n@requires_hardlinks\ndef test_create_duplicate_root(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # setup for #5603\n    path_a = os.path.join(archiver.input_path, \"a\")\n    path_b = os.path.join(archiver.input_path, \"b\")\n    os.mkdir(path_a)\n    os.mkdir(path_b)\n    hl_a = os.path.join(path_a, \"hardlink\")\n    hl_b = os.path.join(path_b, \"hardlink\")\n    create_regular_file(archiver.input_path, hl_a, contents=b\"123456\")\n    os.link(hl_a, hl_b)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"test\", \"input\", \"input\")  # give input twice!\n    # test if created archive has 'input' contents twice:\n    archive_list = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    paths = [json.loads(line)[\"path\"] for line in archive_list.split(\"\\n\") if line]\n    # we have all fs items exactly once!\n    assert sorted(paths) == [\"input\", \"input/a\", \"input/a/hardlink\", \"input/b\", \"input/b/hardlink\"]\n\n\ndef test_create_unreadable_parent(archiver):\n    parent_dir = os.path.join(archiver.input_path, \"parent\")\n    root_dir = os.path.join(archiver.input_path, \"parent\", \"root\")\n    os.mkdir(parent_dir)\n    os.mkdir(root_dir)\n    os.chmod(parent_dir, 0o111)  # --x--x--x == parent dir traversable, but not readable\n    try:\n        cmd(archiver, \"repo-create\", \"--encryption=none\")\n        # issue #7746: we *can* read root_dir and we *can* traverse parent_dir, so this should work:\n        cmd(archiver, \"create\", \"test\", root_dir)\n    finally:\n        os.chmod(parent_dir, 0o771)  # otherwise cleanup after this test fails\n\n\n@pytest.mark.skipif(is_win32, reason=\"unix sockets not available on windows\")\ndef test_unix_socket(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    try:\n        with tempfile.TemporaryDirectory() as temp_dir:\n            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            sock.bind(os.path.join(temp_dir, \"unix-socket\"))\n    except PermissionError as err:\n        if err.errno == errno.EPERM:\n            pytest.skip(\"unix sockets disabled or not supported\")\n        elif err.errno == errno.EACCES:\n            pytest.skip(\"permission denied to create unix sockets\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    sock.close()\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        print(f\"{temp_dir}/unix-socket\")\n        assert not os.path.exists(f\"{temp_dir}/unix-socket\")\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot setup and execute test without utime\")\n@pytest.mark.skipif(not is_birthtime_fully_supported(), reason=\"cannot setup and execute test without birth time\")\ndef test_nobirthtime(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    birthtime, mtime, atime = 946598400, 946684800, 946771200\n    os.utime(\"input/file1\", (atime, birthtime))\n    os.utime(\"input/file1\", (atime, mtime))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--nobirthtime\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    sti = os.stat(\"input/file1\")\n    sto = os.stat(\"output/input/file1\")\n    assert same_ts_ns(sti.st_birthtime * 1e9, birthtime * 1e9)\n    assert same_ts_ns(sto.st_birthtime * 1e9, mtime * 1e9)\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n    assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9)\n\n\ndef test_create_stdin(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    input_data = b\"\\x00foo\\n\\nbar\\n   \\n\"\n    cmd(archiver, \"create\", \"test\", \"-\", input=input_data)\n    item = json.loads(cmd(archiver, \"list\", \"test\", \"--json-lines\"))\n    assert item[\"size\"] == len(input_data)\n    assert item[\"path\"] == \"stdin\"\n    extracted_data = cmd(archiver, \"extract\", \"test\", \"--stdout\", binary_output=True)\n    assert extracted_data == input_data\n\n\ndef test_create_erroneous_file(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    chunk_size = 1000  # fixed chunker with this size\n    create_regular_file(archiver.input_path, os.path.join(archiver.input_path, \"file1\"), size=chunk_size * 2)\n    create_regular_file(archiver.input_path, os.path.join(archiver.input_path, \"file2\"), size=chunk_size * 2)\n    create_regular_file(archiver.input_path, os.path.join(archiver.input_path, \"file3\"), size=chunk_size * 2)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    flist = \"\".join(f\"input/file{n}\\n\" for n in range(1, 4))\n    out = cmd(\n        archiver,\n        \"create\",\n        f\"--chunker-params=fail,{chunk_size},rrrEEErrrr\",\n        \"--paths-from-stdin\",\n        \"--list\",\n        \"test\",\n        input=flist.encode(),\n        exit_code=0,\n    )\n    assert \"retry: 3 of \" in out\n    assert \"E input/file2\" not in out  # we managed to read it in the 3rd retry (after 3 failed reads)\n    # repo looking good overall? checks for rc == 0.\n    cmd(archiver, \"check\", \"--debug\")\n    # check files in created archive\n    out = cmd(archiver, \"list\", \"test\")\n    assert \"input/file1\" in out\n    assert \"input/file2\" in out\n    assert \"input/file3\" in out\n\n\n@pytest.mark.skipif(is_root(), reason=\"test must not be run as (fake)root\")\ndef test_create_no_permission_file(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    file_path = os.path.join(archiver.input_path, \"file\")\n    create_regular_file(archiver.input_path, file_path + \"1\", size=1000)\n    create_regular_file(archiver.input_path, file_path + \"2\", size=1000)\n    create_regular_file(archiver.input_path, file_path + \"3\", size=1000)\n    # revoke read permissions on file2 for everybody, including us:\n    if is_win32:\n        subprocess.run([\"icacls.exe\", file_path + \"2\", \"/deny\", \"everyone:(R)\"])\n    else:\n        # note: this will NOT take away read permissions for root\n        os.chmod(file_path + \"2\", 0o000)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    flist = \"\".join(f\"input/file{n}\\n\" for n in range(1, 4))\n    expected_ec = BackupPermissionError(\"open\", OSError(13, \"permission denied\")).exit_code\n    if expected_ec == EXIT_ERROR:  # workaround, TODO: fix it\n        expected_ec = EXIT_WARNING\n    out = cmd(\n        archiver,\n        \"create\",\n        \"--paths-from-stdin\",\n        \"--list\",\n        \"test\",\n        input=flist.encode(),\n        exit_code=expected_ec,  # WARNING status: could not back up file2.\n    )\n    assert \"retry: 1 of \" not in out  # retries were NOT attempted!\n    assert \"E input/file2\" in out  # no permissions!\n    # repo looking good overall? checks for rc == 0.\n    cmd(archiver, \"check\", \"--debug\")\n    # check files in created archive\n    out = cmd(archiver, \"list\", \"test\")\n    assert \"input/file1\" in out\n    assert \"input/file2\" not in out  # it skipped file2\n    assert \"input/file3\" in out\n\n\ndef test_sanitized_stdin_name(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--stdin-name\", \"./a//path\", \"test\", \"-\", input=b\"\")\n    item = json.loads(cmd(archiver, \"list\", \"test\", \"--json-lines\"))\n    assert item[\"path\"] == \"a/path\"\n\n\ndef test_dotdot_stdin_name(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"--stdin-name\", \"foo/../bar\", \"test\", \"-\", input=b\"\", exit_code=2)\n    assert output.endswith(\"'..' element in path 'foo/../bar'\" + os.linesep)\n\n\ndef test_dot_stdin_name(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"--stdin-name\", \"./\", \"test\", \"-\", input=b\"\", exit_code=2)\n    assert output.endswith(\"'./' is not a valid file name\" + os.linesep)\n\n\ndef test_create_content_from_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    input_data = \"some test content\"\n    name = \"a/b/c\"\n    cmd(archiver, \"create\", \"--stdin-name\", name, \"--content-from-command\", \"test\", \"--\", \"echo\", input_data)\n    item = json.loads(cmd(archiver, \"list\", \"test\", \"--json-lines\"))\n    assert item[\"size\"] == len(input_data) + 1  # `echo` adds newline\n    assert item[\"path\"] == name\n    extracted_data = cmd(archiver, \"extract\", \"test\", \"--stdout\")\n    assert extracted_data == input_data + \"\\n\"\n\n\ndef test_create_content_from_command_with_failed_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        expected_ec = CommandError().exit_code\n        output = cmd(\n            archiver, \"create\", \"--content-from-command\", \"test\", \"--\", \"sh\", \"-c\", \"exit 73;\", exit_code=expected_ec\n        )\n        assert output.endswith(\"Command 'sh' exited with status 73\" + os.linesep)\n    else:\n        with pytest.raises(CommandError):\n            cmd(archiver, \"create\", \"--content-from-command\", \"test\", \"--\", \"sh\", \"-c\", \"exit 73;\")\n    archive_list = json.loads(cmd(archiver, \"repo-list\", \"--json\"))\n    assert archive_list[\"archives\"] == []\n\n\ndef test_create_content_from_command_missing_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"test\", \"--content-from-command\", exit_code=2)\n    assert output.endswith(\"No command given.\" + os.linesep)\n\n\ndef test_create_paths_from_stdin(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir1/file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir1/file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    input_data = b\"input/file1\\0input/dir1\\0input/file4\"\n    cmd(archiver, \"create\", \"test\", \"--paths-from-stdin\", \"--paths-delimiter\", \"\\\\0\", input=input_data)\n    archive_list = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    paths = [json.loads(line)[\"path\"] for line in archive_list.split(\"\\n\") if line]\n    assert paths == [\"input/file1\", \"input/dir1\", \"input/file4\"]\n\n\ndef test_create_paths_from_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    input_data = \"input/file1\\ninput/file2\\ninput/file3\"\n    if is_win32:\n        with open(\"filenames.cmd\", \"w\") as script:\n            for filename in input_data.splitlines():\n                script.write(f\"@echo {filename}\\n\")\n    cmd(archiver, \"create\", \"--paths-from-command\", \"test\", \"--\", \"filenames.cmd\" if is_win32 else \"echo\", input_data)\n    archive_list = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    paths = [json.loads(line)[\"path\"] for line in archive_list.split(\"\\n\") if line]\n    assert paths == [\"input/file1\", \"input/file2\", \"input/file3\"]\n\n\ndef test_create_paths_from_command_with_failed_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        expected_ec = CommandError().exit_code\n        output = cmd(\n            archiver, \"create\", \"--paths-from-command\", \"test\", \"--\", \"sh\", \"-c\", \"exit 73;\", exit_code=expected_ec\n        )\n        assert output.endswith(\"Command 'sh' exited with status 73\" + os.linesep)\n    else:\n        with pytest.raises(CommandError):\n            cmd(archiver, \"create\", \"--paths-from-command\", \"test\", \"--\", \"sh\", \"-c\", \"exit 73;\")\n    archive_list = json.loads(cmd(archiver, \"repo-list\", \"--json\"))\n    assert archive_list[\"archives\"] == []\n\n\ndef test_create_paths_from_command_missing_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"test\", \"--paths-from-command\", exit_code=2)\n    assert output.endswith(\"No command given.\" + os.linesep)\n\n\n@pytest.mark.skipif(is_win32, reason=\"shell patterns not supported on Windows\")\ndef test_create_paths_from_shell_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    input_data = \"input/file1\\ninput/file2\\ninput/file3\"\n    # Use a shell pipe to test that shell=True works correctly.\n    cmd(archiver, \"create\", \"--paths-from-shell-command\", \"test\", \"--\", f\"echo '{input_data}' | head -n 2\")\n    archive_list = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    paths = [json.loads(line)[\"path\"] for line in archive_list.split(\"\\n\") if line]\n    assert paths == [\"input/file1\", \"input/file2\"]\n\n\ndef test_create_without_root(archivers, request):\n    \"\"\"test create without a root\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", exit_code=2)\n\n\ndef test_create_pattern_root(archivers, request):\n    \"\"\"test create with only a root pattern\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    output = cmd(archiver, \"create\", \"test\", \"-v\", \"--list\", \"--pattern=R input\")\n    assert \"A input/file1\" in output\n    assert \"A input/file2\" in output\n\n\ndef test_create_pattern(archivers, request):\n    \"\"\"test file patterns during create\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file_important\", size=1024 * 80)\n    output = cmd(\n        archiver, \"create\", \"-v\", \"--list\", \"--pattern=+input/file_important\", \"--pattern=-input/file*\", \"test\", \"input\"\n    )\n    assert \"A input/file_important\" in output\n    assert \"- input/file1\" in output\n    assert \"- input/file2\" in output\n\n\ndef test_create_pattern_file(archivers, request):\n    \"\"\"test file patterns during create\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"otherfile\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file_important\", size=1024 * 80)\n    output = cmd(\n        archiver,\n        \"create\",\n        \"-v\",\n        \"--list\",\n        \"--pattern=-input/otherfile\",\n        \"--patterns-from=\" + archiver.patterns_file_path,\n        \"test\",\n        \"input\",\n    )\n    assert \"A input/file_important\" in output\n    assert \"- input/file1\" in output\n    assert \"- input/file2\" in output\n    assert \"- input/otherfile\" in output\n\n\ndef test_create_pattern_exclude_folder_but_recurse(archivers, request):\n    \"\"\"test when patterns exclude a parent folder, but include a child\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    patterns_file_path2 = os.path.join(archiver.tmpdir, \"patterns2\")\n    with open(patterns_file_path2, \"wb\") as fd:\n        fd.write(b\"+ input/x/b\\n- input/x*\\n\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"x/a/foo_a\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"x/b/foo_b\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"y/foo_y\", size=1024 * 80)\n    output = cmd(archiver, \"create\", \"-v\", \"--list\", \"--patterns-from=\" + patterns_file_path2, \"test\", \"input\")\n    assert \"- input/x/a/foo_a\" in output\n    assert \"A input/x/b/foo_b\" in output\n    assert \"A input/y/foo_y\" in output\n\n\ndef test_create_pattern_exclude_folder_no_recurse(archivers, request):\n    \"\"\"test when patterns exclude a parent folder, but include a child\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    patterns_file_path2 = os.path.join(archiver.tmpdir, \"patterns2\")\n    with open(patterns_file_path2, \"wb\") as fd:\n        fd.write(b\"+ input/x/b\\n! input/x*\\n\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"x/a/foo_a\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"x/b/foo_b\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"y/foo_y\", size=1024 * 80)\n    output = cmd(archiver, \"create\", \"-v\", \"--list\", \"--patterns-from=\" + patterns_file_path2, \"test\", \"input\")\n    assert \"input/x/a/foo_a\" not in output\n    assert \"input/x/a\" not in output\n    assert \"A input/y/foo_y\" in output\n\n\ndef test_create_pattern_intermediate_folders_first(archivers, request):\n    \"\"\"test that intermediate folders appear first when patterns exclude a parent folder but include a child\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    patterns_file_path2 = os.path.join(archiver.tmpdir, \"patterns2\")\n    with open(patterns_file_path2, \"wb\") as fd:\n        fd.write(b\"+ input/x/a\\n+ input/x/b\\n- input/x*\\n\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"x/a/foo_a\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"x/b/foo_b\", size=1024 * 80)\n    with changedir(\"input\"):\n        cmd(archiver, \"create\", \"--patterns-from=\" + patterns_file_path2, \"test\", \".\")\n    # list the archive and verify that the \"intermediate\" folders appear before\n    # their contents\n    out = cmd(archiver, \"list\", \"test\", \"--format\", \"{type} {path}{NL}\")\n    out_list = out.splitlines()\n    assert \"d x/a\" in out_list\n    assert \"d x/b\" in out_list\n    assert out_list.index(\"d x/a\") < out_list.index(\"- x/a/foo_a\")\n    assert out_list.index(\"d x/b\") < out_list.index(\"- x/b/foo_b\")\n\n\ndef test_create_archivename_with_placeholder(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    ts = \"1999-12-31T23:59:59\"\n    name_given = \"test-{now}\"  # placeholder in archive name gets replaced by borg\n    name_expected = f\"test-{ts}\"  # placeholder in f-string gets replaced by python\n    cmd(archiver, \"create\", f\"--timestamp={ts}\", name_given, \"input\")\n    list_output = cmd(archiver, \"repo-list\")\n    assert name_expected in list_output\n\n\ndef test_exclude_caches(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_caches(archiver)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--exclude-caches\")\n    _assert_test_caches(archiver)\n\n\ndef test_exclude_tagged(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_tagged(archiver)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--exclude-if-present\", \".NOBACKUP\", \"--exclude-if-present\", \"00-NOBACKUP\")\n    _assert_test_tagged(archiver)\n\n\ndef test_exclude_keep_tagged(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_keep_tagged(archiver)\n    cmd(\n        archiver,\n        \"create\",\n        \"test\",\n        \"input\",\n        \"--exclude-if-present\",\n        \".NOBACKUP1\",\n        \"--exclude-if-present\",\n        \".NOBACKUP2\",\n        \"--exclude-caches\",\n        \"--keep-exclude-tags\",\n    )\n    _assert_test_keep_tagged(archiver)\n\n\ndef test_path_sanitation(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"dir1/dir2/file\", size=1024 * 80)\n    with changedir(\"input/dir1/dir2\"):\n        cmd(archiver, \"create\", \"test\", \"../../../input/dir1/../dir1/dir2/..\")\n    output = cmd(archiver, \"list\", \"test\")\n    assert \"..\" not in output\n    assert \" input/dir1/dir2/file\" in output\n\n\ndef test_exclude_sanitation(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    with changedir(\"input\"):\n        cmd(archiver, \"create\", \"test1\", \".\", \"--exclude=file1\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test1\")\n    assert sorted(os.listdir(\"output\")) == [\"file2\"]\n    with changedir(\"input\"):\n        cmd(archiver, \"create\", \"test2\", \".\", \"--exclude=./file1\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test2\")\n    assert sorted(os.listdir(\"output\")) == [\"file2\"]\n    cmd(archiver, \"create\", \"test3\", \"input\", \"--exclude=input/./file1\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test3\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file2\"]\n\n\ndef test_repeated_files(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\", \"input\")\n\n\n@pytest.mark.skipif(\"BORG_TESTS_IGNORE_MODES\" in os.environ, reason=\"modes unreliable\")\n@pytest.mark.skipif(is_win32, reason=\"modes unavailable on Windows\")\ndef test_umask(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    mode = os.stat(archiver.repository_path).st_mode\n    assert stat.S_IMODE(mode) == 0o700\n\n\ndef test_create_dry_run(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--dry-run\", \"test\", \"input\")\n    # Make sure no archive has been created\n    with Repository(archiver.repository_path) as repository:\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        assert manifest.archives.count() == 0\n\n\ndef test_progress_on(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"test4\", \"input\", \"--progress\")\n    assert \"0 B O 0 B U 0 N\" in output\n\n\ndef test_progress_off(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"test5\", \"input\")\n    assert \"0 B O 0 B U 0 N\" not in output\n\n\ndef test_file_status(archivers, request):\n    \"\"\"test that various file status show expected results\n    clearly incomplete: only tests for the weird \"unchanged\" status for now\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"--list\", \"test\", \"input\")\n    assert \"A input/file1\" in output\n    assert \"A input/file2\" in output\n    # should find first file as unmodified\n    output = cmd(archiver, \"create\", \"--list\", \"test\", \"input\")\n    assert \"U input/file1\" in output\n    # although surprising, this is expected. For why, see:\n    # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file\n    assert \"A input/file2\" in output\n\n\ndef test_create_tags(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--tags\", \"foo\", \"bar\", \"baz\", \"--\", \"test\", \"input\")\n    info = cmd(archiver, \"info\", \"--json\", \"test\")\n    info = json.loads(info)\n    assert sorted(info[\"archives\"][0][\"tags\"]) == [\"bar\", \"baz\", \"foo\"]\n\n\ndef test_create_invalid_tags(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"--tags\", \"@INVALID\", \"--\", \"test\", \"input\", exit_code=EXIT_ERROR)\n    assert \"Unknown special tags given\" in output\n\n\n@pytest.mark.skipif(\n    is_win32, reason=\"ctime attribute is file creation time on Windows\"\n)  # see https://docs.python.org/3/library/os.html#os.stat_result.st_ctime\ndef test_file_status_cs_cache_mode(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    \"\"\"test that a changed file with faked \"previous\" mtime still gets backed up in ctime,size cache_mode\"\"\"\n    create_regular_file(archiver.input_path, \"file1\", contents=b\"123\")\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=10)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--files-cache=ctime,size\")\n    # modify file1, but cheat with the mtime (and atime) and also keep same size:\n    st = os.stat(\"input/file1\")\n    create_regular_file(archiver.input_path, \"file1\", contents=b\"321\")\n    os.utime(\"input/file1\", ns=(st.st_atime_ns, st.st_mtime_ns))\n    # this mode uses ctime for change detection, so it should find file1 as modified\n    output = cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--files-cache=ctime,size\")\n    assert \"M input/file1\" in output\n\n\ndef test_files_changed_modes(archivers, request):\n    \"\"\"test that all --files-changed modes are accepted and work\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=10)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # test mtime mode (works on all platforms including Windows)\n    cmd(archiver, \"create\", \"test_mtime\", \"input\", \"--files-changed=mtime\")\n    # test disabled mode\n    cmd(archiver, \"create\", \"test_disabled\", \"input\", \"--files-changed=disabled\")\n    if not is_win32:\n        # test ctime mode (only meaningful on POSIX, where ctime = inode change time)\n        cmd(archiver, \"create\", \"test_ctime\", \"input\", \"--files-changed=ctime\")\n\n\ndef test_file_status_ms_cache_mode(archivers, request):\n    \"\"\"test that a chmod'ed file with no content changes does not get chunked again in mtime,size cache_mode\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=10)\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=10)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--list\", \"--files-cache=mtime,size\", \"test\", \"input\")\n    # change mode of file1, no content change:\n    st = os.stat(\"input/file1\")\n    os.chmod(\"input/file1\", st.st_mode ^ stat.S_IRWXO)  # this triggers a ctime change, but mtime is unchanged\n    # this mode uses mtime for change detection, so it should find file1 as unmodified\n    output = cmd(archiver, \"create\", \"--list\", \"--files-cache=mtime,size\", \"test\", \"input\")\n    assert \"U input/file1\" in output\n\n\ndef test_file_status_rc_cache_mode(archivers, request):\n    \"\"\"test that files get rechunked unconditionally in rechunk,ctime cache mode\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=10)\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=10)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--list\", \"--files-cache=rechunk,ctime\", \"test\", \"input\")\n    # no changes here, but this mode rechunks unconditionally\n    output = cmd(archiver, \"create\", \"--list\", \"--files-cache=rechunk,ctime\", \"test\", \"input\")\n    assert \"A input/file1\" in output\n\n\ndef test_file_status_excluded(archivers, request):\n    \"\"\"test that excluded paths are listed\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    if has_lchflags:\n        create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n        platform.set_flags(os.path.join(archiver.input_path, \"file3\"), stat.UF_NODUMP)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    output = cmd(archiver, \"create\", \"--list\", \"test\", \"input\")\n    assert \"A input/file1\" in output\n    assert \"A input/file2\" in output\n    if has_lchflags:\n        assert \"- input/file3\" in output\n    # should find second file as excluded\n    output = cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--exclude\", \"*/file2\")\n    assert \"U input/file1\" in output\n    assert \"- input/file2\" in output\n    if has_lchflags:\n        assert \"- input/file3\" in output\n\n\ndef test_file_status_counters(archivers, request):\n    \"\"\"Test file status counters in the stats of `borg create --stats`\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    def to_dict(borg_create_output):\n        borg_create_output = borg_create_output.strip().splitlines()\n        borg_create_output = [line.split(\":\", 1) for line in borg_create_output]\n        borg_create_output = {\n            key: int(value)\n            for key, value in borg_create_output\n            if key in (\"Added files\", \"Unchanged files\", \"Modified files\")\n        }\n        return borg_create_output\n\n    # Test case set up: create a repository\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Archive an empty dir\n    result = cmd(archiver, \"create\", \"--stats\", \"test_archive\", archiver.input_path)\n    result = to_dict(result)\n    assert result[\"Added files\"] == 0\n    assert result[\"Unchanged files\"] == 0\n    assert result[\"Modified files\"] == 0\n    # Archive a dir with two added files\n    create_regular_file(archiver.input_path, \"testfile1\", contents=b\"test1\")\n    granularity_sleep()  # testfile2 must have newer timestamps than testfile1\n    create_regular_file(archiver.input_path, \"testfile2\", contents=b\"test2\")\n    result = cmd(archiver, \"create\", \"--stats\", \"test_archive\", archiver.input_path)\n    result = to_dict(result)\n    assert result[\"Added files\"] == 2\n    assert result[\"Unchanged files\"] == 0\n    assert result[\"Modified files\"] == 0\n    # Archive a dir with 1 unmodified file and 1 modified\n    create_regular_file(archiver.input_path, \"testfile1\", contents=b\"new data\")\n    result = cmd(archiver, \"create\", \"--stats\", \"test_archive\", archiver.input_path)\n    result = to_dict(result)\n    # Should process testfile2 as added because of\n    # https://borgbackup.readthedocs.io/en/stable/faq.html#i-am-seeing-a-added-status-for-an-unchanged-file\n    assert result[\"Added files\"] == 1\n    assert result[\"Unchanged files\"] == 0\n    assert result[\"Modified files\"] == 1\n\n\ndef test_create_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_info = json.loads(cmd(archiver, \"create\", \"--json\", \"test\", \"input\"))\n    # The usual keys\n    assert \"encryption\" in create_info\n    assert \"repository\" in create_info\n    assert \"cache\" in create_info\n    assert \"last_modified\" in create_info[\"repository\"]\n\n    archive = create_info[\"archive\"]\n    assert archive[\"name\"] == \"test\"\n    assert isinstance(archive[\"command_line\"], str)\n    assert isinstance(archive[\"duration\"], float)\n    assert len(archive[\"id\"]) == 64\n    assert \"stats\" in archive\n\n\ndef test_explicit_hostname_and_username(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--hostname\", \"foo_host\", \"--username\", \"bar_user\", \"test\", \"input\")\n    info = json.loads(cmd(archiver, \"info\", \"--json\", \"test\"))\n    archive = info[\"archives\"][0]\n    assert archive[\"hostname\"] == \"foo_host\"\n    assert archive[\"username\"] == \"bar_user\"\n\n\ndef test_create_topical(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    granularity_sleep()  # file2 must have newer timestamps than file1\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # no listing by default\n    output = cmd(archiver, \"create\", \"test\", \"input\")\n    assert \"file1\" not in output\n    # shouldn't be listed even if unchanged\n    output = cmd(archiver, \"create\", \"test\", \"input\")\n    assert \"file1\" not in output\n    # should list the file as unchanged\n    output = cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--filter=U\")\n    assert \"file1\" in output\n    # should *not* list the file as changed\n    output = cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--filter=AM\")\n    assert \"file1\" not in output\n    # change the file\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 100)\n    # should list the file as changed\n    output = cmd(archiver, \"create\", \"test\", \"input\", \"--list\", \"--filter=AM\")\n    assert \"file1\" in output\n\n\n# @pytest.mark.skipif(not are_fifos_supported() or is_cygwin, reason=\"FIFOs not supported, hangs on cygwin\")\n@pytest.mark.skip(reason=\"This test is problematic and should be skipped\")\ndef test_create_read_special_symlink(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    from threading import Thread\n\n    def fifo_feeder(fifo_fn, data):\n        fd = os.open(fifo_fn, os.O_WRONLY)\n        try:\n            os.write(fd, data)\n        finally:\n            os.close(fd)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    data = b\"foobar\" * 1000\n\n    fifo_fn = os.path.join(archiver.input_path, \"fifo\")\n    link_fn = os.path.join(archiver.input_path, \"link_fifo\")\n    os.mkfifo(fifo_fn)\n    os.symlink(fifo_fn, link_fn)\n\n    t = Thread(target=fifo_feeder, args=(fifo_fn, data))\n    t.start()\n    try:\n        cmd(archiver, \"create\", \"--read-special\", \"test\", \"input/link_fifo\")\n    finally:\n        # In case `borg create` failed to open FIFO, read all data to avoid join() hanging.\n        fd = os.open(fifo_fn, os.O_RDONLY | os.O_NONBLOCK)\n        try:\n            os.read(fd, len(data))\n        except OSError:\n            # fails on FreeBSD 13 with BlockingIOError\n            pass\n        finally:\n            os.close(fd)\n        t.join()\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        fifo_fn = \"input/link_fifo\"\n        with open(fifo_fn, \"rb\") as f:\n            extracted_data = f.read()\n    assert extracted_data == data\n\n\n@pytest.mark.skipif(not are_symlinks_supported(), reason=\"symlinks not supported\")\ndef test_create_read_special_broken_symlink(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    os.symlink(\"somewhere does not exist\", os.path.join(archiver.input_path, \"link\"))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--read-special\", \"test\", \"input\")\n    output = cmd(archiver, \"list\", \"test\")\n    assert \"input/link -> somewhere does not exist\" in output\n\n\ndef test_create_dotslash_hack(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    os.makedirs(os.path.join(archiver.input_path, \"first\", \"secondA\", \"thirdA\"))\n    os.makedirs(os.path.join(archiver.input_path, \"first\", \"secondB\", \"thirdB\"))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input/first/./\")  # hack!\n    output = cmd(archiver, \"list\", \"test\")\n    # dir levels left of slashdot (= input, first) not in archive:\n    assert \"input\" not in output\n    assert \"input/first\" not in output\n    assert \"input/first/secondA\" not in output\n    assert \"input/first/secondA/thirdA\" not in output\n    assert \"input/first/secondB\" not in output\n    assert \"input/first/secondB/thirdB\" not in output\n    assert \"first\" not in output\n    assert \"first/secondA\" not in output\n    assert \"first/secondA/thirdA\" not in output\n    assert \"first/secondB\" not in output\n    assert \"first/secondB/thirdB\" not in output\n    # dir levels right of slashdot are in archive:\n    assert \"secondA\" in output\n    assert \"secondA/thirdA\" in output\n    assert \"secondB\" in output\n    assert \"secondB/thirdB\" in output\n\n\ndef test_log_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    log = cmd(archiver, \"create\", \"test\", \"input\", \"--log-json\", \"--list\", \"--debug\")\n    messages = {}  # type -> message, one of each kind\n    for line in log.splitlines():\n        msg = json.loads(line)\n        messages[msg[\"type\"]] = msg\n\n    file_status = messages[\"file_status\"]\n    assert \"status\" in file_status\n    assert file_status[\"path\"].startswith(\"input\")\n\n    log_message = messages[\"log_message\"]\n    assert isinstance(log_message[\"time\"], float)\n    assert log_message[\"levelname\"] == \"DEBUG\"  # there should only be DEBUG messages\n    assert isinstance(log_message[\"message\"], str)\n\n\ndef test_common_options(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    log = cmd(archiver, \"--debug\", \"create\", \"test\", \"input\")\n    assert \"security: read previous location\" in log\n\n\ndef test_create_big_zeros_files(archivers, request):\n    \"\"\"Test creating an archive from 10 files with 10MB zeros each.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    # Create 10 files with 10,000,000 bytes of zeros each\n    count, size = 10, 10 * 1000 * 1000\n    assert size <= len(zeros)\n    for i in range(count):\n        create_regular_file(archiver.input_path, f\"zeros_{i}\", contents=memoryview(zeros)[:size])\n    # Create repository and archive\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Extract the archive to verify contents\n    with tempfile.TemporaryDirectory() as extract_path:\n        with changedir(extract_path):\n            cmd(archiver, \"extract\", \"test\")\n\n            # Verify that the extracted files have the correct contents\n            for i in range(count):\n                extracted_file_path = os.path.join(extract_path, \"input\", f\"zeros_{i}\")\n                with open(extracted_file_path, \"rb\") as f:\n                    extracted_data = f.read()\n                    # Verify the file contains only zeros and has the correct size\n                    assert extracted_data == bytes(size)\n                    assert len(extracted_data) == size\n\n            # Also verify the directory structure matches\n            assert_dirs_equal(archiver.input_path, os.path.join(extract_path, \"input\"))\n\n\ndef test_create_big_random_files(archivers, request):\n    \"\"\"Test creating an archive from 10 files with 10MB random data each.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    # Create 10 files with 10,000,000 bytes of random data each\n    count, size = 10, 10 * 1000 * 1000\n    random_data = {}\n    for i in range(count):\n        data = os.urandom(size)\n        random_data[i] = data\n        create_regular_file(archiver.input_path, f\"random_{i}\", contents=data)\n    # Create repository and archive\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Extract the archive to verify contents\n    with tempfile.TemporaryDirectory() as extract_path:\n        with changedir(extract_path):\n            cmd(archiver, \"extract\", \"test\")\n\n            # Verify that the extracted files have the correct contents\n            for i in range(count):\n                extracted_file_path = os.path.join(extract_path, \"input\", f\"random_{i}\")\n                with open(extracted_file_path, \"rb\") as f:\n                    extracted_data = f.read()\n                    # Verify the file contains the original random data and has the correct size\n                    assert extracted_data == random_data[i]\n                    assert len(extracted_data) == size\n\n            # Also verify the directory structure matches\n            assert_dirs_equal(archiver.input_path, os.path.join(extract_path, \"input\"))\n\n\ndef test_create_with_compression_algorithms(archivers, request):\n    \"\"\"Test creating archives with different compression algorithms.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create test files: 5 files with zeros (highly compressible) and 5 with random data (incompressible)\n    count, size = 5, 1 * 1000 * 1000  # 1MB per file\n    random_data = {}\n\n    # Create zeros files\n    for i in range(count):\n        create_regular_file(archiver.input_path, f\"zeros_{i}\", contents=memoryview(zeros)[:size])\n\n    # Create random files\n    for i in range(count):\n        data = os.urandom(size)\n        random_data[i] = data\n        create_regular_file(archiver.input_path, f\"random_{i}\", contents=data)\n\n    # Create repository\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Test different compression algorithms\n    algorithms = [\n        \"none\",  # No compression\n        \"lz4\",  # Fast compression\n        \"zlib,6\",  # Medium compression\n        \"zstd,3\",  # Good compression/speed balance\n        \"lzma,6\",  # High compression\n    ]\n\n    for algo in algorithms:\n        # Create archive with specific compression algorithm\n        archive_name = f\"test_{algo.replace(',', '_')}\"\n        cmd(archiver, \"create\", \"--compression\", algo, archive_name, \"input\")\n\n        # Extract the archive to verify contents\n        with tempfile.TemporaryDirectory() as extract_path:\n            with changedir(extract_path):\n                cmd(archiver, \"extract\", archive_name)\n\n                # Verify zeros files\n                for i in range(count):\n                    extracted_file_path = os.path.join(extract_path, \"input\", f\"zeros_{i}\")\n                    with open(extracted_file_path, \"rb\") as f:\n                        extracted_data = f.read()\n                        # Verify the file contains only zeros and has the correct size\n                        assert extracted_data == bytes(size)\n                        assert len(extracted_data) == size\n\n                # Verify random files\n                for i in range(count):\n                    extracted_file_path = os.path.join(extract_path, \"input\", f\"random_{i}\")\n                    with open(extracted_file_path, \"rb\") as f:\n                        extracted_data = f.read()\n                        # Verify the file contains the original random data and has the correct size\n                        assert extracted_data == random_data[i]\n                        assert len(extracted_data) == size\n\n                # Also verify the directory structure matches\n                assert_dirs_equal(archiver.input_path, os.path.join(extract_path, \"input\"))\n\n\ndef test_exclude_nodump_dir_with_file(archivers, request):\n    \"\"\"A directory flagged NODUMP and its contents must not be archived.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    if not has_lchflags:\n        pytest.skip(\"platform does not support setting UF_NODUMP\")\n\n    # Prepare input tree: input/nd directory (NODUMP) containing a file.\n    create_regular_file(archiver.input_path, \"nd/file_in_ndir\", contents=b\"hello\")\n    platform.set_flags(os.path.join(archiver.input_path, \"nd\"), stat.UF_NODUMP)\n\n    # Create repo and archive\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Verify: neither the directory nor its contained file are present in the archive\n    list_output = cmd(archiver, \"list\", \"test\", \"--short\")\n    assert \"input/nd\\n\" not in list_output\n    assert \"input/nd/file_in_ndir\\n\" not in list_output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/debug_cmds_test.py",
    "content": "import json\nimport os\nimport pstats\n\nfrom ...constants import *  # NOQA\nfrom .. import changedir\nfrom ..compress_test import Compressor\nfrom . import cmd, create_test_files, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_debug_profile(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--debug-profile=create.prof\")\n    cmd(archiver, \"debug\", \"convert-profile\", \"create.prof\", \"create.pyprof\")\n    stats = pstats.Stats(\"create.pyprof\")\n    stats.strip_dirs()\n    stats.sort_stats(\"cumtime\")\n    cmd(archiver, \"create\", \"test2\", \"input\", \"--debug-profile=create.pyprof\")\n    stats = pstats.Stats(\"create.pyprof\")  # Only do this on trusted data!\n    stats.strip_dirs()\n    stats.sort_stats(\"cumtime\")\n\n\ndef test_debug_dump_archive_items(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        output = cmd(archiver, \"debug\", \"dump-archive-items\", \"test\")\n    output_dir = sorted(os.listdir(\"output\"))\n    assert len(output_dir) > 0 and output_dir[0].startswith(\"000000_\")\n    assert \"Done.\" in output\n\n\ndef test_debug_dump_repo_objs(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        output = cmd(archiver, \"debug\", \"dump-repo-objs\")\n    output_dir = sorted(os.listdir(\"output\"))\n    assert len(output_dir) > 0\n    assert \"Done.\" in output\n\n\ndef test_debug_put_get_delete_obj(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    data = b\"some data\"\n    create_regular_file(archiver.input_path, \"file\", contents=data)\n\n    output = cmd(archiver, \"debug\", \"id-hash\", \"input/file\")\n    id_hash = output.strip()\n\n    output = cmd(archiver, \"debug\", \"put-obj\", id_hash, \"input/file\")\n    assert id_hash in output\n\n    output = cmd(archiver, \"debug\", \"get-obj\", id_hash, \"output/file\")\n    assert id_hash in output\n\n    with open(\"output/file\", \"rb\") as f:\n        data_read = f.read()\n    assert data == data_read\n\n    output = cmd(archiver, \"debug\", \"delete-obj\", id_hash)\n    assert \"deleted\" in output\n\n    output = cmd(archiver, \"debug\", \"delete-obj\", id_hash)\n    assert \"not found\" in output\n\n    output = cmd(archiver, \"debug\", \"delete-obj\", \"invalid\")\n    assert \"is invalid\" in output\n\n\ndef test_debug_id_hash_format_put_get_parse_obj(archivers, request):\n    \"\"\"Test format-obj and parse-obj commands.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    data = b\"some data\" * 100\n    meta_dict = {\"some\": \"property\"}\n    meta = json.dumps(meta_dict).encode()\n    create_regular_file(archiver.input_path, \"plain.bin\", contents=data)\n    create_regular_file(archiver.input_path, \"meta.json\", contents=meta)\n    output = cmd(archiver, \"debug\", \"id-hash\", \"input/plain.bin\")\n    id_hash = output.strip()\n    cmd(\n        archiver,\n        \"debug\",\n        \"format-obj\",\n        id_hash,\n        \"input/plain.bin\",\n        \"input/meta.json\",\n        \"output/data.bin\",\n        \"--compression=zstd,2\",\n    )\n    output = cmd(archiver, \"debug\", \"put-obj\", id_hash, \"output/data.bin\")\n    assert id_hash in output\n\n    output = cmd(archiver, \"debug\", \"get-obj\", id_hash, \"output/object.bin\")\n    assert id_hash in output\n\n    cmd(archiver, \"debug\", \"parse-obj\", id_hash, \"output/object.bin\", \"output/plain.bin\", \"output/meta.json\")\n    with open(\"output/plain.bin\", \"rb\") as f:\n        data_read = f.read()\n    assert data == data_read\n\n    with open(\"output/meta.json\") as f:\n        meta_read = json.load(f)\n    for key, value in meta_dict.items():\n        assert meta_read.get(key) == value\n    assert meta_read.get(\"size\") == len(data_read)\n\n    c = Compressor(name=\"zstd\", level=2)\n    _, data_compressed = c.compress(meta_dict, data=data)\n    assert meta_read.get(\"csize\") == len(data_compressed)\n    assert meta_read.get(\"ctype\") == c.compressor.ID\n    assert meta_read.get(\"clevel\") == c.compressor.level\n\n\ndef test_debug_format_obj_respects_type(archivers, request):\n    \"\"\"Test format-obj uses the type from metadata JSON, not just ROBJ_FILE_STREAM.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    data = b\"some data\" * 100\n    meta_dict = {\"some\": \"property\", \"type\": ROBJ_ARCHIVE_STREAM}\n    meta = json.dumps(meta_dict).encode()\n    create_regular_file(archiver.input_path, \"data.bin\", contents=data)\n    create_regular_file(archiver.input_path, \"meta.json\", contents=meta)\n    output = cmd(archiver, \"debug\", \"id-hash\", \"input/data.bin\")\n    id_hash = output.strip()\n    cmd(archiver, \"debug\", \"format-obj\", id_hash, \"input/data.bin\", \"input/meta.json\", \"input/repoobj.bin\")\n    output = cmd(archiver, \"debug\", \"put-obj\", id_hash, \"input/repoobj.bin\")\n    assert id_hash in output\n    output = cmd(archiver, \"debug\", \"get-obj\", id_hash, \"output/object.bin\")\n    assert id_hash in output\n    cmd(archiver, \"debug\", \"parse-obj\", id_hash, \"output/object.bin\", \"output/data.bin\", \"output/meta.json\")\n    with open(\"output/meta.json\") as f:\n        meta_read = json.load(f)\n    assert meta_read[\"type\"] == ROBJ_ARCHIVE_STREAM\n\n\ndef test_debug_dump_manifest(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    dump_file = archiver.output_path + \"/dump\"\n    output = cmd(archiver, \"debug\", \"dump-manifest\", dump_file)\n    assert output == \"\"\n    with open(dump_file) as f:\n        result = json.load(f)\n    assert \"archives\" in result\n    assert \"config\" in result\n    assert \"timestamp\" in result\n    assert \"version\" in result\n    assert \"item_keys\" in result[\"config\"]\n    assert frozenset(result[\"config\"][\"item_keys\"]) == ITEM_KEYS\n\n\ndef test_debug_dump_archive(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    dump_file = archiver.output_path + \"/dump\"\n    output = cmd(archiver, \"debug\", \"dump-archive\", \"test\", dump_file)\n    assert output == \"\"\n\n    with open(dump_file) as f:\n        result = json.load(f)\n    assert \"_name\" in result\n    assert \"_manifest_entry\" in result\n    assert \"_meta\" in result\n    assert \"_items\" in result\n\n\ndef test_debug_info(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    output = cmd(archiver, \"debug\", \"info\")\n    assert \"Python\" in output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/delete_cmd_test.py",
    "content": "from ...constants import *  # NOQA\nfrom . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_delete_options(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir2/file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"create\", \"test.2\", \"input\")\n    cmd(archiver, \"create\", \"test.3\", \"input\")\n    cmd(archiver, \"create\", \"another_test.1\", \"input\")\n    cmd(archiver, \"create\", \"another_test.2\", \"input\")\n    cmd(archiver, \"delete\", \"--match-archives\", \"sh:another_*\")\n    cmd(archiver, \"delete\", \"--last\", \"1\")  # test.3\n    cmd(archiver, \"delete\", \"-a\", \"test\")\n    cmd(archiver, \"extract\", \"test.2\", \"--dry-run\")  # still there?\n    cmd(archiver, \"delete\", \"-a\", \"test.2\")\n    output = cmd(archiver, \"repo-list\")\n    assert output == \"\"  # no archives left!\n\n\ndef test_delete_multiple(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", \"input\")\n    cmd(archiver, \"create\", \"test2\", \"input\")\n    cmd(archiver, \"delete\", \"-a\", \"test1\")\n    cmd(archiver, \"delete\", \"-a\", \"test2\")\n    assert not cmd(archiver, \"repo-list\")\n\n\ndef test_delete_ignore_protected(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", \"input\")\n    cmd(archiver, \"tag\", \"--add=@PROT\", \"test1\")\n    cmd(archiver, \"create\", \"test2\", \"input\")\n    cmd(archiver, \"delete\", \"-a\", \"test1\")\n    cmd(archiver, \"delete\", \"-a\", \"test2\")\n    cmd(archiver, \"delete\", \"-a\", \"sh:test*\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"@PROT\" in output\n    assert \"test1\" in output\n    assert \"test2\" not in output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/diff_cmd_test.py",
    "content": "import json\nimport os\nfrom pathlib import Path\nimport stat\nimport time\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom .. import are_symlinks_supported, are_hardlinks_supported, granularity_sleep\nfrom ...platformflags import is_win32, is_freebsd, is_netbsd\nfrom . import (\n    cmd,\n    create_regular_file,\n    RK_ENCRYPTION,\n    assert_line_exists,\n    generate_archiver_tests,\n    assert_line_not_exists,\n)\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_basic_functionality(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # Setup files for the first snapshot\n    create_regular_file(archiver.input_path, \"empty\", size=0)\n    create_regular_file(archiver.input_path, \"file_unchanged\", size=128)\n    create_regular_file(archiver.input_path, \"file_removed\", size=256)\n    create_regular_file(archiver.input_path, \"file_removed2\", size=512)\n    create_regular_file(archiver.input_path, \"file_replaced\", size=1024)\n    create_regular_file(archiver.input_path, \"file_touched\", size=128)\n    os.mkdir(\"input/dir_replaced_with_file\")\n    os.chmod(\"input/dir_replaced_with_file\", stat.S_IFDIR | 0o755)\n    os.mkdir(\"input/dir_removed\")\n    if are_symlinks_supported():\n        os.mkdir(\"input/dir_replaced_with_link\")\n        os.symlink(\"input/dir_replaced_with_file\", \"input/link_changed\")\n        os.symlink(\"input/file_unchanged\", \"input/link_removed\")\n        os.symlink(\"input/file_removed2\", \"input/link_target_removed\")\n        os.symlink(\"input/empty\", \"input/link_target_contents_changed\")\n        os.symlink(\"input/empty\", \"input/link_replaced_by_file\")\n    if are_hardlinks_supported():\n        os.link(\"input/file_replaced\", \"input/hardlink_target_replaced\")\n        os.link(\"input/empty\", \"input/hardlink_contents_changed\")\n        os.link(\"input/file_removed\", \"input/hardlink_removed\")\n        os.link(\"input/file_removed2\", \"input/hardlink_target_removed\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Create the first snapshot\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    # Setup files for the second snapshot\n    create_regular_file(archiver.input_path, \"file_added\", size=2048)\n    create_regular_file(archiver.input_path, \"file_empty_added\", size=0)\n    os.unlink(\"input/file_replaced\")\n    create_regular_file(archiver.input_path, \"file_replaced\", contents=b\"0\" * 4096)\n    os.unlink(\"input/file_removed\")\n    os.unlink(\"input/file_removed2\")\n    granularity_sleep()\n    Path(\"input/file_touched\").touch()\n    os.rmdir(\"input/dir_replaced_with_file\")\n    create_regular_file(archiver.input_path, \"dir_replaced_with_file\", size=8192)\n    os.chmod(\"input/dir_replaced_with_file\", stat.S_IFREG | 0o755)\n    os.mkdir(\"input/dir_added\")\n    os.rmdir(\"input/dir_removed\")\n    if are_symlinks_supported():\n        os.rmdir(\"input/dir_replaced_with_link\")\n        os.symlink(\"input/dir_added\", \"input/dir_replaced_with_link\")\n        os.unlink(\"input/link_changed\")\n        os.symlink(\"input/dir_added\", \"input/link_changed\")\n        os.symlink(\"input/dir_added\", \"input/link_added\")\n        os.unlink(\"input/link_replaced_by_file\")\n        create_regular_file(archiver.input_path, \"link_replaced_by_file\", size=16384)\n        os.unlink(\"input/link_removed\")\n    if are_hardlinks_supported():\n        os.unlink(\"input/hardlink_removed\")\n        os.link(\"input/file_added\", \"input/hardlink_added\")\n    with open(\"input/empty\", \"ab\") as fd:\n        fd.write(b\"appended_data\")\n    # Create the second snapshot\n    cmd(archiver, \"create\", \"test1a\", \"input\")\n    cmd(archiver, \"create\", \"test1b\", \"input\", \"--chunker-params\", \"16,18,17,4095\")\n\n    def do_asserts(output, can_compare_ids, content_only=False):\n        lines: list = output.splitlines()\n        assert \"file_replaced\" in output  # added to debug #3494\n        change = \"modified.*B\" if can_compare_ids else r\"modified:  \\(can't get size\\)\"\n        assert_line_exists(lines, f\"{change}.*input/file_replaced\")\n        # File unchanged\n        assert \"input/file_unchanged\" not in output\n\n        # Directory replaced with a regular file\n        if \"BORG_TESTS_IGNORE_MODES\" not in os.environ and not is_win32 and not content_only:\n            assert_line_exists(lines, \"[drwxr-xr-x -> -rwxr-xr-x].*input/dir_replaced_with_file\")\n\n        # Basic directory cases\n        assert \"added directory             input/dir_added\" in output\n        assert \"removed directory           input/dir_removed\" in output\n\n        if are_symlinks_supported():\n            # Basic symlink cases\n            assert_line_exists(lines, \"changed link.*input/link_changed\")\n            assert_line_exists(lines, \"added link.*input/link_added\")\n            assert_line_exists(lines, \"removed link.*input/link_removed\")\n\n            # Symlink replacing or being replaced\n            if not content_only:\n                assert \"input/dir_replaced_with_link\" in output\n                assert \"input/link_replaced_by_file\" in output\n\n            # Symlink target removed. Should not affect the symlink at all.\n            assert \"input/link_target_removed\" not in output\n\n        # The inode has two links and the file contents changed. Borg\n        # should notice the changes in both links. However, the symlink\n        # pointing to the file is not changed.\n        change = \"modified.*0 B\" if can_compare_ids else r\"modified:  \\(can't get size\\)\"\n        assert_line_exists(lines, f\"{change}.*input/empty\")\n\n        # Do not show a 0 byte change for a file whose contents weren't modified.\n        assert_line_not_exists(lines, \"0 B.*input/file_touched\")\n        if not content_only:\n            assert_line_exists(lines, \"[cm]time:.*input/file_touched\")\n        else:\n            # And if we're doing content-only, don't show the file at all.\n            assert \"input/file_touched\" not in output\n\n        if are_hardlinks_supported():\n            assert_line_exists(lines, f\"{change}.*input/hardlink_contents_changed\")\n        if are_symlinks_supported():\n            assert \"input/link_target_contents_changed\" not in output\n\n        # Added a new file and a hard link to it. Both links to the same\n        # inode should appear as separate files.\n        assert \"added:              2.05 kB input/file_added\" in output\n        if are_hardlinks_supported():\n            assert \"added:              2.05 kB input/hardlink_added\" in output\n\n        # check if a diff between nonexistent and empty new file is found\n        assert \"added:                  0 B input/file_empty_added\" in output\n\n        # The inode has two links and both of them are deleted. They should\n        # appear as two deleted files.\n        assert \"removed:              256 B input/file_removed\" in output\n        if are_hardlinks_supported():\n            assert \"removed:              256 B input/hardlink_removed\" in output\n\n        if are_hardlinks_supported() and content_only:\n            # Another link (marked previously as the source in borg) to the\n            # same inode was removed. This should only change the ctime since removing\n            # the link would result in the decrementation of the inode's hard-link count.\n            assert \"input/hardlink_target_removed\" not in output\n\n            # Another link (marked previously as the source in borg) to the\n            # same inode was replaced with a new regular file. This should only change\n            # its ctime. This should not be reflected in the output if content-only is set\n            assert \"input/hardlink_target_replaced\" not in output\n\n    def do_json_asserts(output, can_compare_ids, content_only=False):\n        def get_changes(filename, data):\n            chgsets = [j[\"changes\"] for j in data if j[\"path\"] == filename]\n            assert len(chgsets) < 2\n            # return a flattened list of changes for given filename\n            return sum(chgsets, [])\n\n        # convert output to list of dicts\n        joutput = [json.loads(line) for line in output.split(\"\\n\") if line]\n\n        # File contents changed (deleted and replaced with a new file)\n        expected = {\"type\": \"modified\", \"added\": 4096, \"removed\": 1024} if can_compare_ids else {\"type\": \"modified\"}\n        assert expected in get_changes(\"input/file_replaced\", joutput)\n\n        # File unchanged\n        assert not any(get_changes(\"input/file_unchanged\", joutput))\n\n        # Do not show a 0 byte change for a file whose contents weren't modified.\n        unexpected = {\"type\": \"modified\", \"added\": 0, \"removed\": 0}\n        assert unexpected not in get_changes(\"input/file_touched\", joutput)\n        if not content_only:\n            # on win32, ctime is the file creation time and does not change.\n            # not sure why netbsd only has mtime, but it does, #8703.\n            expected = {\"mtime\"} if (is_win32 or is_netbsd) else {\"mtime\", \"ctime\"}\n            assert expected.issubset({c[\"type\"] for c in get_changes(\"input/file_touched\", joutput)})\n        else:\n            # And if we're doing content-only, don't show the file at all.\n            assert not any(get_changes(\"input/file_touched\", joutput))\n\n        # Directory replaced with a regular file\n        if \"BORG_TESTS_IGNORE_MODES\" not in os.environ and not is_win32 and not content_only:\n            assert {\"type\": \"changed mode\", \"item1\": \"drwxr-xr-x\", \"item2\": \"-rwxr-xr-x\"} in get_changes(\n                \"input/dir_replaced_with_file\", joutput\n            )\n\n        # Basic directory cases\n        assert {\"type\": \"added directory\"} in get_changes(\"input/dir_added\", joutput)\n        assert {\"type\": \"removed directory\"} in get_changes(\"input/dir_removed\", joutput)\n\n        if are_symlinks_supported():\n            # Basic symlink cases\n            assert {\"type\": \"changed link\"} in get_changes(\"input/link_changed\", joutput)\n            assert {\"type\": \"added link\"} in get_changes(\"input/link_added\", joutput)\n            assert {\"type\": \"removed link\"} in get_changes(\"input/link_removed\", joutput)\n\n            # Symlink replacing or being replaced\n\n            if not content_only:\n                assert any(\n                    chg[\"type\"] == \"changed mode\" and chg[\"item1\"].startswith(\"d\") and chg[\"item2\"].startswith(\"l\")\n                    for chg in get_changes(\"input/dir_replaced_with_link\", joutput)\n                ), get_changes(\"input/dir_replaced_with_link\", joutput)\n                assert any(\n                    chg[\"type\"] == \"changed mode\" and chg[\"item1\"].startswith(\"l\") and chg[\"item2\"].startswith(\"-\")\n                    for chg in get_changes(\"input/link_replaced_by_file\", joutput)\n                ), get_changes(\"input/link_replaced_by_file\", joutput)\n\n            # Symlink target removed. Should not affect the symlink at all.\n            assert not any(get_changes(\"input/link_target_removed\", joutput))\n\n        # The inode has two links and the file contents changed. Borg\n        # should notice the changes in both links. However, the symlink\n        # pointing to the file is not changed.\n        expected = {\"type\": \"modified\", \"added\": 13, \"removed\": 0} if can_compare_ids else {\"type\": \"modified\"}\n        assert expected in get_changes(\"input/empty\", joutput)\n        if are_hardlinks_supported():\n            assert expected in get_changes(\"input/hardlink_contents_changed\", joutput)\n        if are_symlinks_supported():\n            assert not any(get_changes(\"input/link_target_contents_changed\", joutput))\n\n        # Added a new file and a hard link to it. Both links to the same\n        # inode should appear as separate files.\n        assert {\"added\": 2048, \"removed\": 0, \"type\": \"added\"} in get_changes(\"input/file_added\", joutput)\n        if are_hardlinks_supported():\n            assert {\"added\": 2048, \"removed\": 0, \"type\": \"added\"} in get_changes(\"input/hardlink_added\", joutput)\n\n        # check if a diff between nonexistent and empty new file is found\n        assert {\"added\": 0, \"removed\": 0, \"type\": \"added\"} in get_changes(\"input/file_empty_added\", joutput)\n\n        # The inode has two links and both of them are deleted. They should\n        # appear as two deleted files.\n        assert {\"added\": 0, \"removed\": 256, \"type\": \"removed\"} in get_changes(\"input/file_removed\", joutput)\n        if are_hardlinks_supported():\n            assert {\"added\": 0, \"removed\": 256, \"type\": \"removed\"} in get_changes(\"input/hardlink_removed\", joutput)\n\n        if are_hardlinks_supported() and content_only:\n            # Another link (marked previously as the source in borg) to the\n            # same inode was removed. This should only change the ctime since removing\n            # the link would result in the decrementation of the inode's hard-link count.\n            assert not any(get_changes(\"input/hardlink_target_removed\", joutput))\n\n            # Another link (marked previously as the source in borg) to the\n            # same inode was replaced with a new regular file. This should only change\n            # its ctime. This should not be reflected in the output if content-only is set\n            assert not any(get_changes(\"input/hardlink_target_replaced\", joutput))\n\n    output = cmd(archiver, \"diff\", \"test0\", \"test1a\")\n    do_asserts(output, True)\n\n    output = cmd(archiver, \"diff\", \"test0\", \"test1b\", \"--content-only\")\n    do_asserts(output, False, content_only=True)\n\n    output = cmd(archiver, \"diff\", \"test0\", \"test1a\", \"--json-lines\")\n    do_json_asserts(output, True)\n\n    output = cmd(archiver, \"diff\", \"test0\", \"test1a\", \"--json-lines\", \"--content-only\")\n    do_json_asserts(output, True, content_only=True)\n\n\ndef test_time_diffs(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"test_file\", size=10)\n    cmd(archiver, \"create\", \"archive1\", \"input\")\n    time.sleep(0.1)\n    os.unlink(\"input/test_file\")\n    granularity_sleep(ctime_quirk=True)\n    create_regular_file(archiver.input_path, \"test_file\", size=15)\n    cmd(archiver, \"create\", \"archive2\", \"input\")\n    output = cmd(archiver, \"diff\", \"archive1\", \"archive2\", \"--format\", \"'{mtime}{ctime} {path}{NL}'\")\n    assert \"mtime\" in output\n    assert \"ctime\" in output  # Should show up on Windows as well since it is a new file.\n\n    granularity_sleep()\n    os.chmod(\"input/test_file\", 0o777)\n    cmd(archiver, \"create\", \"archive3\", \"input\")\n    output = cmd(archiver, \"diff\", \"archive2\", \"archive3\", \"--format\", \"'{mtime}{ctime} {path}{NL}'\")\n    assert \"mtime\" not in output\n    # Checking platform because ctime should not be shown on Windows since it wasn't recreated.\n    if not is_win32:\n        assert \"ctime\" in output\n    else:\n        assert \"ctime\" not in output\n\n\ndef test_sort_by_option(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    create_regular_file(archiver.input_path, \"a_file_removed\", size=8)\n    create_regular_file(archiver.input_path, \"f_file_removed\", size=16)\n    create_regular_file(archiver.input_path, \"c_file_changed\", size=32)\n    create_regular_file(archiver.input_path, \"e_file_changed\", size=64)\n    cmd(archiver, \"create\", \"test0\", \"input\")\n\n    os.unlink(\"input/a_file_removed\")\n    os.unlink(\"input/f_file_removed\")\n    os.unlink(\"input/c_file_changed\")\n    os.unlink(\"input/e_file_changed\")\n    create_regular_file(archiver.input_path, \"c_file_changed\", size=512)\n    create_regular_file(archiver.input_path, \"e_file_changed\", size=1024)\n    create_regular_file(archiver.input_path, \"b_file_added\", size=128)\n    create_regular_file(archiver.input_path, \"d_file_added\", size=256)\n    cmd(archiver, \"create\", \"test1\", \"input\")\n\n    output = cmd(archiver, \"diff\", \"test0\", \"test1\", \"--sort-by=path\", \"--content-only\")\n    expected = [\"a_file_removed\", \"b_file_added\", \"c_file_changed\", \"d_file_added\", \"e_file_changed\", \"f_file_removed\"]\n    assert isinstance(output, str)\n    outputs = output.splitlines()\n    assert len(outputs) == len(expected)\n    assert all(x in line for x, line in zip(expected, outputs))\n\n\ndef test_sort_by_invalid_field_is_rejected(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    create_regular_file(archiver.input_path, \"file\", size=1)\n    cmd(archiver, \"create\", \"a1\", \"input\")\n    create_regular_file(archiver.input_path, \"file\", size=2)\n    cmd(archiver, \"create\", \"a2\", \"input\")\n\n    # Unsupported field should cause argument parsing error\n    cmd(archiver, \"diff\", \"a1\", \"a2\", \"--sort-by=not_a_field\", exit_code=EXIT_ERROR)\n\n\ndef test_sort_by_size_added_then_path(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Base archive with two files that will be removed later\n    create_regular_file(archiver.input_path, \"r_big_removed\", size=50)\n    create_regular_file(archiver.input_path, \"r_small_removed\", size=5)\n    cmd(archiver, \"create\", \"base\", \"input\")\n\n    # Second archive: remove both above and add two new files of different sizes\n    os.unlink(\"input/r_big_removed\")\n    os.unlink(\"input/r_small_removed\")\n    create_regular_file(archiver.input_path, \"a_small_added\", size=10)\n    create_regular_file(archiver.input_path, \"b_large_added\", size=30)\n    cmd(archiver, \"create\", \"next\", \"input\")\n\n    # Sort by size added (ascending), then path to break ties deterministically\n    output = cmd(archiver, \"diff\", \"base\", \"next\", \"--sort-by=size_added,path\", \"--content-only\")\n    lines = output.splitlines()\n    # Expect removed entries first (size_added=0), ordered by path, then added entries by increasing size\n    expected_order = [\n        \"removed:.*input/r_big_removed\",  # size_added=0\n        \"removed:.*input/r_small_removed\",  # size_added=0\n        \"added:.*10 B.*input/a_small_added\",\n        \"added:.*30 B.*input/b_large_added\",\n    ]\n    assert len(lines) == len(expected_order)\n    for pattern, line in zip(expected_order, lines):\n        assert_line_exists([line], pattern)\n\n\n@pytest.mark.parametrize(\n    \"sort_key\",\n    [\n        \"path\",\n        \"size\",\n        \"size_added\",\n        \"size_removed\",\n        \"size_diff\",\n        \"user\",\n        \"group\",\n        \"uid\",\n        \"gid\",\n        \"ctime\",\n        \"mtime\",\n        \"ctime_diff\",\n        \"mtime_diff\",\n    ],\n)\ndef test_sort_by_all_keys_with_directions(archivers, request, sort_key):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Prepare initial files\n    create_regular_file(archiver.input_path, \"a_removed\", size=11)\n    create_regular_file(archiver.input_path, \"f_removed\", size=22)\n    create_regular_file(archiver.input_path, \"c_changed\", size=33)\n    create_regular_file(archiver.input_path, \"e_changed\", size=44)\n    cmd(archiver, \"create\", \"s0\", \"input\")\n\n    # Ensure that subsequent modifications happen on a later timestamp tick than s0\n    granularity_sleep()\n\n    # Create differences for second archive\n    os.unlink(\"input/a_removed\")\n    os.unlink(\"input/f_removed\")\n    os.unlink(\"input/c_changed\")\n    os.unlink(\"input/e_changed\")\n    # Recreate changed files with different sizes\n    create_regular_file(archiver.input_path, \"c_changed\", size=333)\n    create_regular_file(archiver.input_path, \"e_changed\", size=444)\n    # Added files\n    create_regular_file(archiver.input_path, \"b_added\", size=55)\n    create_regular_file(archiver.input_path, \"d_added\", size=66)\n    cmd(archiver, \"create\", \"s1\", \"input\")\n\n    expected_paths = {\n        \"input/a_removed\",\n        \"input/b_added\",\n        \"input/c_changed\",\n        \"input/d_added\",\n        \"input/e_changed\",\n        \"input/f_removed\",\n    }\n\n    # Exercise both ascending and descending for each key.\n    for direction in (\"<\", \">\"):\n        sort_spec = f\"{direction}{sort_key},path\"\n        output = cmd(archiver, \"diff\", \"s0\", \"s1\", f\"--sort-by={sort_spec}\", \"--content-only\")\n        lines = output.splitlines()\n        assert len(lines) == len(expected_paths)\n        # Validate that we got exactly the expected items regardless of order.\n        # As we do not test the order, this is mostly for test coverage.\n        seen_paths = {line.split()[-1] for line in lines}\n        assert seen_paths == expected_paths\n\n\n@pytest.mark.skipif(\n    not are_hardlinks_supported() or is_freebsd or is_netbsd or is_win32,\n    reason=\"hardlinks not supported or test failing on freebsd, netbsd and windows\",\n)\ndef test_hard_link_deletion_and_replacement(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n\n    # repo-create changes umask, so create the repo first to avoid any\n    # unexpected permission changes.\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    path_a = os.path.join(archiver.input_path, \"a\")\n    path_b = os.path.join(archiver.input_path, \"b\")\n    os.mkdir(path_a)\n    os.mkdir(path_b)\n    hl_a = os.path.join(path_a, \"hardlink\")\n    hl_b = os.path.join(path_b, \"hardlink\")\n    create_regular_file(archiver.input_path, hl_a, contents=b\"123456\")\n    os.link(hl_a, hl_b)\n\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    os.unlink(hl_a)  # Don't duplicate warning message - one is enough.\n    cmd(archiver, \"create\", \"test1\", \"input\")\n\n    # Moral equivalent of test_multiple_link_exclusion in borg v1.x... see #8344\n    # Borg v2 doesn't have this issue comparing hard-links, so we'll defer to\n    # POSIX behavior:\n    # https://pubs.opengroup.org/onlinepubs/9799919799/functions/unlink.html\n    # Upon successful completion, unlink() shall mark for update the last data modification\n    # and last file status change timestamps of the parent directory. Also, if the\n    # file's link count is not 0, the last file status change timestamp of the\n    # file shall be marked for update.\n    output = cmd(\n        archiver, \"diff\", \"--pattern=+ fm:input/b\", \"--pattern=! **/\", \"test0\", \"test1\", exit_code=EXIT_SUCCESS\n    )\n    lines = output.splitlines()\n    # Directory was excluded.\n    assert_line_not_exists(lines, \"input/a$\")\n    # Remaining hardlink\n    assert_line_exists(lines, \"ctime:.*input/b/hardlink\")\n    assert_line_not_exists(lines, \".*mtime:.*input/b/hardlink\")\n    # Deleted hardlink was excluded\n    assert_line_not_exists(lines, \"input/a/hardlink$\")\n\n    # Now try again, except with no patterns!\n    output = cmd(archiver, \"diff\", \"test0\", \"test1\", exit_code=EXIT_SUCCESS)\n    lines = output.splitlines()\n    # Directory... preferably, let's not care about order differences are presented.\n    assert_line_exists(lines, \"[cm]time:.*[cm]time:.*input/a\")\n    # Remaining hardlink\n    assert_line_exists(lines, \"ctime:.*input/b/hardlink\")\n    assert_line_not_exists(lines, \".*mtime:.*input/b/hardlink\")\n    # Deleted hardlink\n    assert_line_exists(lines, \"removed:.*input/a/hardlink\")\n\n    # Now recreate the unlinked file as a different entity with identical\n    # contents.\n    create_regular_file(archiver.input_path, hl_a, contents=b\"123456\")\n    cmd(archiver, \"create\", \"test2\", \"input\")\n\n    # Compare test0 and test2.\n    output = cmd(archiver, \"diff\", \"test0\", \"test2\", exit_code=EXIT_SUCCESS)\n    lines = output.splitlines()\n    # Adding a file changes c/mtime.\n    assert_line_exists(lines, \"[cm]time:.*[cm]time:.*input/a$\")\n    # Different c/mtime but no apparent changes (i.e. perms) or content\n    # modifications should be a hint that something hard-link related is going on.\n    assert_line_exists(lines, \"[cm]time:.*[cm]time:.*input/a/hardlink\")\n    assert_line_not_exists(lines, \"modified.*B.*input/a/hardlink\")\n    assert_line_not_exists(lines, \"-[r-][w-][x-].*input/a/hardlink\")\n    # ctime changed because the hard-link count went down. But no mtime changes\n    # because file content isn't modified. No permissions changes either.\n    # This is another hint that something hard-link related changed.\n    assert_line_exists(lines, \"ctime:.*input/b/hardlink\")\n    assert_line_not_exists(lines, \".*mtime:.*input/b/hardlink\")\n    assert_line_not_exists(lines, \"modified.*B.*input/b/hardlink\")\n    assert_line_not_exists(lines, \"-[r-][w-][x-].*input/b/hardlink\")\n\n    # Finally, compare test1 and test2.\n    output = cmd(archiver, \"diff\", \"test1\", \"test2\", exit_code=EXIT_SUCCESS)\n    lines = output.splitlines()\n    # Same situation applies as previous diff for a.\n    assert_line_exists(lines, \"[cm]time:.*[cm]time:.*input/a$\")\n    # From test1 to test2's POV, the a/hardlink file is a fresh new file.\n    assert_line_exists(lines, \"added.*B.*input/a/hardlink\")\n    # But the b/hardlink file was not modified at all.\n    assert_line_not_exists(lines, \".*input/b/hardlink\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/disk_full_test.py",
    "content": "\"\"\"\ntest_disk_full is very slow and not recommended for daily test runs.\nFor this test, an empty, writable 700 MB filesystem mounted on DF_MOUNT is required.\nFor speed and other reasons, it is recommended that the underlying block device is\nin RAM, not a magnetic or flash disk.\n\nAssuming /dev/shm is a tmpfs (in-memory filesystem), one can use this:\n\n    dd if=/dev/zero of=/dev/shm/borg-disk bs=1M count=700\n    mkfs.ext4 /dev/shm/borg-disk\n    mkdir /tmp/borg-mount\n    sudo mount /dev/shm/borg-disk /tmp/borg-mount\n    sudo chown myuser /tmp/borg-mount/\n\nIf the directory does not exist, the test will be skipped.\n\"\"\"\n\nimport errno\nimport os\nimport random\nimport shutil\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom . import cmd_fixture  # NOQA\n\nDF_MOUNT = \"/tmp/borg-mount\"\n\n\ndef make_files(dir, count, size, rnd=True):\n    shutil.rmtree(dir, ignore_errors=True)\n    os.mkdir(dir)\n    if rnd:\n        count = random.randint(1, count)\n        if size > 1:\n            size = random.randint(1, size)\n    for i in range(count):\n        fn = os.path.join(dir, \"file%03d\" % i)\n        with open(fn, \"wb\") as f:\n            data = os.urandom(size)\n            f.write(data)\n\n\n@pytest.mark.skipif(not os.path.exists(DF_MOUNT), reason=\"needs a 700MB fs mounted on %s\" % DF_MOUNT)\n@pytest.mark.parametrize(\"test_pass\", range(10))\ndef test_disk_full(test_pass, cmd_fixture, monkeypatch):\n    monkeypatch.setenv(\"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\", \"YES\")\n    monkeypatch.setenv(\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\", \"YES\")\n    repo = os.path.join(DF_MOUNT, \"repo\")\n    input = os.path.join(DF_MOUNT, \"input\")\n    shutil.rmtree(repo, ignore_errors=True)\n    shutil.rmtree(input, ignore_errors=True)\n    rc, out = cmd_fixture(f\"--repo={repo}\", \"repo-create\", \"--encryption=none\")\n    if rc != EXIT_SUCCESS:\n        print(\"repo-create\", rc, out)\n    assert rc == EXIT_SUCCESS\n    try:\n        try:\n            success, i = True, 0\n            while success:\n                i += 1\n                try:\n                    # have some randomness here to produce different out of space conditions:\n                    make_files(input, 40, 1000000, rnd=True)\n                except OSError as err:\n                    if err.errno == errno.ENOSPC:\n                        # already out of space\n                        break\n                    raise\n                try:\n                    rc, out = cmd_fixture(\"--repo=%s\" % repo, \"create\", \"test%03d\" % i, input)\n                    success = rc == EXIT_SUCCESS\n                    if not success:\n                        print(\"create\", rc, out)\n                finally:\n                    # make sure repo is not locked\n                    shutil.rmtree(os.path.join(repo, \"lock.exclusive\"), ignore_errors=True)\n                    shutil.rmtree(os.path.join(repo, \"lock.roster\"), ignore_errors=True)\n        finally:\n            # now some error happened, likely we are out of disk space.\n            # free some space such that we can expect borg to be able to work normally:\n            shutil.rmtree(input, ignore_errors=True)\n        rc, out = cmd_fixture(f\"--repo={repo}\", \"repo-list\")\n        if rc != EXIT_SUCCESS:\n            print(\"repo-list\", rc, out)\n        rc, out = cmd_fixture(f\"--repo={repo}\", \"check\", \"--repair\")\n        if rc != EXIT_SUCCESS:\n            print(\"check\", rc, out)\n        assert rc == EXIT_SUCCESS\n    finally:\n        # try to free the space allocated for the repo\n        cmd_fixture(f\"--repo={repo}\", \"repo-delete\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/extract_cmd_test.py",
    "content": "import errno\nimport os\nfrom pathlib import Path\nimport shutil\nimport stat\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom ... import xattr\nfrom ... import platform\nfrom ...chunkers import has_seek_hole\nfrom ...constants import *  # NOQA\nfrom ...helpers import EXIT_WARNING, BackupPermissionError, bin_to_hex\nfrom ...helpers import flags_noatime, flags_normal\nfrom .. import changedir, same_ts_ns, granularity_sleep\nfrom .. import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported, is_birthtime_fully_supported\nfrom ...platform import get_birthtime_ns\nfrom ...platformflags import is_darwin, is_freebsd, is_win32\nfrom . import (\n    RK_ENCRYPTION,\n    requires_hardlinks,\n    cmd,\n    create_test_files,\n    create_regular_file,\n    assert_dirs_equal,\n    _extract_hardlinks_setup,\n    assert_creates_file,\n    generate_archiver_tests,\n    create_src_archive,\n    open_archive,\n    src_file,\n)\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\n@pytest.mark.skipif(not are_symlinks_supported(), reason=\"symlinks not supported\")\ndef test_symlink_extract(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        assert os.readlink(\"input/link1\") == \"somewhere\"\n\n\n@pytest.mark.skipif(\n    not are_symlinks_supported() or not are_hardlinks_supported() or is_darwin,\n    reason=\"symbolic links or hard links or hard-linked sym-links not supported\",\n)\ndef test_hardlinked_symlinks_extract(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"target\", size=1024)\n    with changedir(\"input\"):\n        os.symlink(\"target\", \"symlink1\")\n        os.link(\"symlink1\", \"symlink2\", follow_symlinks=False)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\")\n        print(output)\n        with changedir(\"input\"):\n            assert os.path.exists(\"target\")\n            assert os.readlink(\"symlink1\") == \"target\"\n            assert os.readlink(\"symlink2\") == \"target\"\n            st1 = os.stat(\"symlink1\", follow_symlinks=False)\n            st2 = os.stat(\"symlink2\", follow_symlinks=False)\n            assert st1.st_nlink == 2\n            assert st2.st_nlink == 2\n            assert st1.st_ino == st2.st_ino\n            assert st1.st_size == st2.st_size\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot properly setup and execute test without utime\")\ndef test_directory_timestamps1(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Default file archiving order (internal recursion)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    # Extracting a file inside a directory touches the directory mtime\n    assert os.path.exists(\"output/input/dir2/file2\")\n    # Make sure Borg fixes the directory mtime after touching it\n    sti = os.stat(\"input/dir2\")\n    sto = os.stat(\"output/input/dir2\")\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot properly setup and execute test without utime\")\ndef test_directory_timestamps2(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Given order, directory first, file second\n    flist_dir_first = b\"input/dir2\\ninput/dir2/file2\\n\"\n    cmd(archiver, \"create\", \"--paths-from-stdin\", \"test\", input=flist_dir_first)\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    # Extracting a file inside a directory touches the directory mtime\n    assert os.path.exists(\"output/input/dir2/file2\")\n    # Make sure Borg fixes the directory mtime after touching it\n    sti = os.stat(\"input/dir2\")\n    sto = os.stat(\"output/input/dir2\")\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot properly setup and execute test without utime\")\ndef test_directory_timestamps3(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Given order, file first, directory second\n    flist_file_first = b\"input/dir2/file2\\ninput/dir2\\n\"\n    cmd(archiver, \"create\", \"--paths-from-stdin\", \"test\", input=flist_file_first)\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    # Extracting a file inside a directory touches the directory mtime\n    assert os.path.exists(\"output/input/dir2/file2\")\n    # Make sure Borg fixes the directory mtime after touching it\n    sti = os.stat(\"input/dir2\")\n    sto = os.stat(\"output/input/dir2\")\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot properly setup and execute test without utime\")\ndef test_atime(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n\n    def has_noatime(some_file):\n        atime_before = os.stat(some_file).st_atime_ns\n        try:\n            with open(os.open(some_file, flags_noatime)) as file:\n                file.read()\n        except PermissionError:\n            return False\n        else:\n            atime_after = os.stat(some_file).st_atime_ns\n            noatime_used = flags_noatime != flags_normal\n            return noatime_used and atime_before == atime_after\n\n    create_test_files(archiver.input_path)\n    atime, mtime = 123456780, 234567890\n    have_noatime = has_noatime(\"input/file1\")\n    os.utime(\"input/file1\", (atime, mtime))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--atime\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    sti = os.stat(\"input/file1\")\n    sto = os.stat(\"output/input/file1\")\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n    assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9)\n    if have_noatime:\n        assert same_ts_ns(sti.st_atime_ns, sto.st_atime_ns)\n        assert same_ts_ns(sto.st_atime_ns, atime * 10**9)\n    else:\n        # it touched the input file's atime while backing it up\n        assert same_ts_ns(sto.st_atime_ns, atime * 10**9)\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot setup and execute test without utime\")\n@pytest.mark.skipif(not is_birthtime_fully_supported(), reason=\"cannot setup and execute test without birthtime\")\ndef test_birthtime(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    birthtime, mtime, atime = 946598400, 946684800, 946771200\n    os.utime(\"input/file1\", (atime, birthtime))\n    os.utime(\"input/file1\", (atime, mtime))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    sti = os.stat(\"input/file1\")\n    sto = os.stat(\"output/input/file1\")\n    assert same_ts_ns(sti.st_birthtime * 1e9, sto.st_birthtime * 1e9)\n    assert same_ts_ns(sto.st_birthtime * 1e9, birthtime * 1e9)\n    assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)\n    assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9)\n\n\ndef test_sparse_file(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n\n    def is_sparse(fn, total_size, hole_size):\n        st = os.stat(fn)\n        assert st.st_size == total_size\n        sparse = True\n        if sparse and hasattr(st, \"st_blocks\") and st.st_blocks * 512 >= st.st_size:\n            sparse = False\n        if sparse and has_seek_hole:\n            with open(fn, \"rb\") as fd:\n                # only check if the first hole is as expected, because the 2nd hole check\n                # is problematic on xfs due to its \"dynamic speculative EOF pre-allocation\n                try:\n                    if fd.seek(0, os.SEEK_HOLE) != 0:\n                        sparse = False\n                    if fd.seek(0, os.SEEK_DATA) != hole_size:\n                        sparse = False\n                except OSError:\n                    # OS/FS does not really support SEEK_HOLE/SEEK_DATA\n                    sparse = False\n        return sparse\n\n    filename_in = os.path.join(archiver.input_path, \"sparse\")\n    content = b\"foobar\"\n    hole_size = 5 * (1 << CHUNK_MAX_EXP)  # 5 full chunker buffers\n    total_size = hole_size + len(content) + hole_size\n    with open(filename_in, \"wb\") as fd:\n        # create a file that has a hole at the beginning and end (if the\n        # OS and filesystem supports sparse files)\n        fd.seek(hole_size, 1)\n        fd.write(content)\n        fd.seek(hole_size, 1)\n        pos = fd.tell()\n        fd.truncate(pos)\n    # we first check if we could create a sparse input file:\n    sparse_support = is_sparse(filename_in, total_size, hole_size)\n    if sparse_support:\n        # we could create a sparse input file, so creating a backup of it and\n        # extracting it again (as sparse) should also work:\n        cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n        cmd(archiver, \"create\", \"test\", \"input\")\n        with changedir(archiver.output_path):\n            cmd(archiver, \"extract\", \"test\", \"--sparse\")\n        assert_dirs_equal(\"input\", \"output/input\")\n        filename_out = os.path.join(archiver.output_path, \"input\", \"sparse\")\n        with open(filename_out, \"rb\") as fd:\n            # check if file contents are as expected\n            assert fd.read(hole_size) == b\"\\0\" * hole_size\n            assert fd.read(len(content)) == content\n            assert fd.read(hole_size) == b\"\\0\" * hole_size\n        assert is_sparse(filename_out, total_size, hole_size)\n        os.unlink(filename_out)  # save space on TMPDIR\n    os.unlink(filename_in)  # save space on TMPDIR\n\n\ndef test_unusual_filenames(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    filenames = [\"normal\", \"with some blanks\", \"(with_parens)\"]\n    for filename in filenames:\n        filename = os.path.join(archiver.input_path, filename)\n        with open(filename, \"wb\"):\n            pass\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    for filename in filenames:\n        with changedir(\"output\"):\n            cmd(archiver, \"extract\", \"test\", os.path.join(\"input\", filename))\n        assert os.path.exists(os.path.join(\"output\", \"input\", filename))\n\n\ndef test_strip_components(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"dir/file\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--strip-components\", \"3\")\n        assert not os.path.exists(\"file\")\n        with assert_creates_file(\"file\"):\n            cmd(archiver, \"extract\", \"test\", \"--strip-components\", \"2\")\n        with assert_creates_file(\"dir/file\"):\n            cmd(archiver, \"extract\", \"test\", \"--strip-components\", \"1\")\n        with assert_creates_file(\"input/dir/file\"):\n            cmd(archiver, \"extract\", \"test\", \"--strip-components\", \"0\")\n\n\n@requires_hardlinks\ndef test_extract_hardlinks1(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _extract_hardlinks_setup(archiver)\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        assert os.stat(\"input/source\").st_nlink == 4\n        assert os.stat(\"input/abba\").st_nlink == 4\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 4\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 4\n        assert open(\"input/dir1/subdir/hardlink\", \"rb\").read() == b\"123456\"\n\n\n@requires_hardlinks\ndef test_extract_hardlinks2(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _extract_hardlinks_setup(archiver)\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--strip-components\", \"2\")\n        assert os.stat(\"hardlink\").st_nlink == 2\n        assert os.stat(\"subdir/hardlink\").st_nlink == 2\n        assert open(\"subdir/hardlink\", \"rb\").read() == b\"123456\"\n        assert os.stat(\"aaaa\").st_nlink == 2\n        assert os.stat(\"source2\").st_nlink == 2\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"input/dir1\")\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 2\n        assert open(\"input/dir1/subdir/hardlink\", \"rb\").read() == b\"123456\"\n        assert os.stat(\"input/dir1/aaaa\").st_nlink == 2\n        assert os.stat(\"input/dir1/source2\").st_nlink == 2\n\n\n@requires_hardlinks\ndef test_extract_hardlinks_twice(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # setup for #5603\n    path_a = os.path.join(archiver.input_path, \"a\")\n    path_b = os.path.join(archiver.input_path, \"b\")\n    os.mkdir(path_a)\n    os.mkdir(path_b)\n    hl_a = os.path.join(path_a, \"hardlink\")\n    hl_b = os.path.join(path_b, \"hardlink\")\n    create_regular_file(archiver.input_path, hl_a, contents=b\"123456\")\n    os.link(hl_a, hl_b)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"test\", \"input\", \"input\")  # give input twice!\n    # now test extraction\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        # if issue #5603 happens, extraction gives rc == 1 (triggering AssertionError) and warnings like:\n        # input/a/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/a/hardlink'\n        # input/b/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/b/hardlink'\n        # otherwise, when fixed, the hard links should be there and have a link count of 2\n        assert os.stat(\"input/a/hardlink\").st_nlink == 2\n        assert os.stat(\"input/b/hardlink\").st_nlink == 2\n\n\ndef test_extract_include_exclude(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    cmd(archiver, \"create\", \"--exclude=input/file4\", \"test\", \"input\")\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"input/file1\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\"]\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude=input/file2\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file3\"]\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude-from=\" + archiver.exclude_file_path)\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file3\"]\n\n\ndef test_extract_include_exclude_regex(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file333\", size=1024 * 80)\n    # Create with regular expression exclusion for file4\n    cmd(archiver, \"create\", \"--exclude=re:input/file4$\", \"test\", \"input\")\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\", \"file3\", \"file333\"]\n    shutil.rmtree(\"output/input\")\n\n    # Extract with regular expression exclusion\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude=re:file3+\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\"]\n    shutil.rmtree(\"output/input\")\n\n    # Combine --exclude with fnmatch and regular expression\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude=input/file2\", \"--exclude=re:file[01]\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file3\", \"file333\"]\n    shutil.rmtree(\"output/input\")\n\n    # Combine --exclude-from and regular expression exclusion\n    with changedir(\"output\"):\n        cmd(\n            archiver,\n            \"extract\",\n            \"test\",\n            \"--exclude-from=\" + archiver.exclude_file_path,\n            \"--exclude=re:file1\",\n            \"--exclude=re:file(\\\\d)\\\\1\\\\1$\",\n        )\n    assert sorted(os.listdir(\"output/input\")) == [\"file3\"]\n\n\ndef test_extract_include_exclude_regex_from_file(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file333\", size=1024 * 80)\n    # Create while excluding using mixed pattern styles\n    with open(archiver.exclude_file_path, \"wb\") as fd:\n        fd.write(b\"re:input/file4$\\n\")\n        fd.write(b\"fm:*file3*\\n\")\n    cmd(archiver, \"create\", \"--exclude-from=\" + archiver.exclude_file_path, \"test\", \"input\")\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\"]\n    shutil.rmtree(\"output/input\")\n\n    # Exclude using regular expression\n    with open(archiver.exclude_file_path, \"wb\") as fd:\n        fd.write(b\"re:file3+\\n\")\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude-from=\" + archiver.exclude_file_path)\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\"]\n    shutil.rmtree(\"output/input\")\n\n    # Mixed exclude pattern styles\n    with open(archiver.exclude_file_path, \"wb\") as fd:\n        fd.write(b\"re:file(\\\\d)\\\\1\\\\1$\\n\")\n        fd.write(b\"fm:nothingwillmatchthis\\n\")\n        fd.write(b\"*/file1\\n\")\n        fd.write(b\"re:file2$\\n\")\n\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--exclude-from=\" + archiver.exclude_file_path)\n    assert sorted(os.listdir(\"output/input\")) == []\n\n\ndef test_extract_with_pattern(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file3\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file4\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file333\", size=1024 * 80)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Extract everything with regular expression\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"re:.*\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\", \"file3\", \"file333\", \"file4\"]\n    shutil.rmtree(\"output/input\")\n\n    # Extract with pattern while also excluding files\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"--exclude=re:file[34]$\", \"test\", r\"re:file\\d$\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\"]\n    shutil.rmtree(\"output/input\")\n\n    # Combine --exclude with pattern for extraction\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"--exclude=input/file1\", \"test\", \"re:file[12]$\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file2\"]\n    shutil.rmtree(\"output/input\")\n\n    # Multiple pattern\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"fm:input/file1\", \"fm:*file33*\", \"input/file2\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file1\", \"file2\", \"file333\"]\n\n\ndef test_extract_list_output(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file\", size=1024 * 80)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\")\n    assert \"input/file\" not in output\n    shutil.rmtree(\"output/input\")\n\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\", \"--info\")\n    assert \"input/file\" not in output\n    shutil.rmtree(\"output/input\")\n\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\", \"--list\")\n    assert \"input/file\" in output\n    shutil.rmtree(\"output/input\")\n\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\", \"--list\", \"--info\")\n    assert \"input/file\" in output\n\n\ndef test_extract_progress(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file\", size=1024 * 80)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    with changedir(\"output\"):\n        output = cmd(archiver, \"extract\", \"test\", \"--progress\")\n        assert \"Extracting:\" in output\n\n\ndef test_extract_pattern_opt(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file2\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"file_important\", size=1024 * 80)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", \"--pattern=+input/file_important\", \"--pattern=-input/file*\")\n    assert sorted(os.listdir(\"output/input\")) == [\"file_important\"]\n\n\n@pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason=\"Linux capabilities test, requires fakeroot >= 1.20.2\")\ndef test_extract_capabilities(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"Skipping binary test due to patch objects\")\n    fchown = os.fchown\n\n    # We need to patch chown manually to get the behaviour Linux has, since fakeroot does not\n    # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.\n    def patched_fchown(fd, uid, gid):\n        xattr.setxattr(fd, b\"security.capability\", b\"\", follow_symlinks=False)\n        fchown(fd, uid, gid)\n\n    # The capability descriptor used here is valid and taken from a /usr/bin/ping\n    capabilities = b\"\\x01\\x00\\x00\\x02\\x00 \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n    create_regular_file(archiver.input_path, \"file\")\n    xattr.setxattr(b\"input/file\", b\"security.capability\", capabilities)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        with patch.object(os, \"fchown\", patched_fchown):\n            cmd(archiver, \"extract\", \"test\")\n        assert xattr.getxattr(b\"input/file\", b\"security.capability\") == capabilities\n\n\n@pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason=\"xattr not supported on this system, or this version of fakeroot\")\ndef test_extract_xattrs_errors(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"Skipping binary test due to patch objects\")\n\n    def patched_setxattr_E2BIG(*args, **kwargs):\n        raise OSError(errno.E2BIG, \"E2BIG\")\n\n    def patched_setxattr_ENOTSUP(*args, **kwargs):\n        raise OSError(errno.ENOTSUP, \"ENOTSUP\")\n\n    def patched_setxattr_EACCES(*args, **kwargs):\n        raise OSError(errno.EACCES, \"EACCES\")\n\n    create_regular_file(archiver.input_path, \"file\")\n    xattr.setxattr(b\"input/file\", b\"user.attribute\", b\"value\")\n    cmd(archiver, \"repo-create\", \"-e\" \"none\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        input_abspath = os.path.abspath(\"input/file\")\n\n        with patch.object(xattr, \"setxattr\", patched_setxattr_E2BIG):\n            out = cmd(archiver, \"extract\", \"test\", exit_code=EXIT_WARNING)\n            assert \"too big for this filesystem\" in out\n            assert \"When setting extended attribute user.attribute\" in out\n        os.remove(input_abspath)\n\n        with patch.object(xattr, \"setxattr\", patched_setxattr_ENOTSUP):\n            out = cmd(archiver, \"extract\", \"test\", exit_code=EXIT_WARNING)\n            assert \"ENOTSUP\" in out\n            assert \"When setting extended attribute user.attribute\" in out\n        os.remove(input_abspath)\n\n        with patch.object(xattr, \"setxattr\", patched_setxattr_EACCES):\n            out = cmd(archiver, \"extract\", \"test\", exit_code=EXIT_WARNING)\n            assert \"EACCES\" in out\n            assert \"When setting extended attribute user.attribute\" in out\n        assert os.path.isfile(input_abspath)\n\n\n@pytest.mark.skipif(not is_darwin, reason=\"only for macOS\")\ndef test_extract_xattrs_resourcefork(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file\")\n    cmd(archiver, \"repo-create\", \"-e\" \"none\")\n    input_path = os.path.abspath(\"input/file\")\n    xa_key, xa_value = b\"com.apple.ResourceFork\", b\"whatshouldbehere\"  # issue #7234\n    xattr.setxattr(input_path.encode(), xa_key, xa_value)\n    birthtime_expected = get_birthtime_ns(os.stat(input_path), input_path)\n    mtime_expected = os.stat(input_path).st_mtime_ns\n    # atime_expected = os.stat(input_path).st_atime_ns\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        extracted_path = os.path.abspath(\"input/file\")\n        birthtime_extracted = get_birthtime_ns(os.stat(extracted_path), extracted_path)\n        mtime_extracted = os.stat(extracted_path).st_mtime_ns\n        # atime_extracted = os.stat(extracted_path).st_atime_ns\n        xa_value_extracted = xattr.getxattr(extracted_path.encode(), xa_key)\n    assert xa_value_extracted == xa_value\n    # cope with small birthtime deviations of less than 1000ns:\n    assert birthtime_extracted == birthtime_expected\n    assert mtime_extracted == mtime_expected\n    # assert atime_extracted == atime_expected  # still broken, but not really important.\n\n\n@pytest.mark.skipif(not (is_darwin or is_freebsd), reason=\"only for macOS or FreeBSD\")\ndef test_extract_restores_append_flag(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # create a file and set the append flag on it\n    create_regular_file(archiver.input_path, \"appendflag\", size=1)\n    src_path = os.path.abspath(\"input/appendflag\")\n    if not hasattr(stat, \"UF_APPEND\"):\n        pytest.skip(\"UF_APPEND not available on this platform\")\n    try:\n        platform.set_flags(src_path, stat.UF_APPEND)\n    except Exception:\n        pytest.skip(\"setting UF_APPEND not supported on this filesystem\")\n    # Verify the flag actually got set; otherwise skip (filesystem may not support it)\n    st = os.lstat(src_path)\n    if (platform.get_flags(src_path, st) & stat.UF_APPEND) == 0:\n        pytest.skip(\"UF_APPEND not settable on this filesystem\")\n    # archive and extract\n    cmd(archiver, \"repo-create\", \"-e\" \"none\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        out_path = os.path.abspath(\"input/appendflag\")\n        st2 = os.lstat(out_path)\n        flags = platform.get_flags(out_path, st2)\n        assert (flags & stat.UF_APPEND) == stat.UF_APPEND\n\n\ndef test_overwrite(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"Test_overwrite seems incompatible with fakeroot and/or the binary.\")\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir2/file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Overwriting regular files and directories should be supported\n    os.mkdir(\"output/input\")\n    os.mkdir(\"output/input/file1\")\n    os.mkdir(\"output/input/dir2\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    assert_dirs_equal(\"input\", \"output/input\")\n\n    # But non-empty dirs should fail\n    os.unlink(\"output/input/file1\")\n    os.mkdir(\"output/input/file1\")\n    os.mkdir(\"output/input/file1/dir\")\n    expected_ec = BackupPermissionError(\"open\", OSError(21, \"is a directory\")).exit_code  # WARNING code\n    if expected_ec == EXIT_ERROR:  # workaround, TODO: fix it\n        expected_ec = EXIT_WARNING\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\", exit_code=expected_ec)\n\n\n# derived from test_extract_xattrs_errors()\n@pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason=\"xattr not supported on this system, or this version of fakeroot\")\ndef test_do_not_fail_when_percent_is_in_xattr_name(archivers, request):\n    \"\"\"https://github.com/borgbackup/borg/issues/6063\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"Skipping binary test due to patch objects\")\n\n    def patched_setxattr_EACCES(*args, **kwargs):\n        raise OSError(errno.EACCES, \"EACCES\")\n\n    create_regular_file(archiver.input_path, \"file\")\n    xattr.setxattr(b\"input/file\", b\"user.attribute%p\", b\"value\")\n    cmd(archiver, \"repo-create\", \"-e\" \"none\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        with patch.object(xattr, \"setxattr\", patched_setxattr_EACCES):\n            cmd(archiver, \"extract\", \"test\", exit_code=EXIT_WARNING)\n\n\n# derived from test_extract_xattrs_errors()\n@pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason=\"xattr not supported on this system, or this version of fakeroot\")\ndef test_do_not_fail_when_percent_is_in_file_name(archivers, request):\n    \"\"\"https://github.com/borgbackup/borg/issues/6063\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"Skipping binary test due to patch objects\")\n\n    def patched_setxattr_EACCES(*args, **kwargs):\n        raise OSError(errno.EACCES, \"EACCES\")\n\n    os.makedirs(os.path.join(archiver.input_path, \"dir%p\"))\n    xattr.setxattr(b\"input/dir%p\", b\"user.attribute\", b\"value\")\n    cmd(archiver, \"repo-create\", \"-e\" \"none\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        with patch.object(xattr, \"setxattr\", patched_setxattr_EACCES):\n            cmd(archiver, \"extract\", \"test\", exit_code=EXIT_WARNING)\n\n\n@pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hardlinks not supported\")\ndef test_extract_continue(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    CONTENTS1, CONTENTS2, CONTENTS3 = b\"contents1\" * 100, b\"contents2\" * 200, b\"contents3\" * 300\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"dir1/file1\", contents=CONTENTS1)\n    create_regular_file(archiver.input_path, \"dir2/file2\", contents=CONTENTS2)\n    create_regular_file(archiver.input_path, \"dir3/file3\", contents=CONTENTS3)\n    cmd(archiver, \"create\", \"arch\", \"input\")\n\n    granularity_sleep()\n\n    with changedir(\"output\"):\n        # we simulate an interrupted/partial extraction:\n        cmd(archiver, \"extract\", \"arch\")\n        # do not modify dir1 and file1, they stand for a successfully extracted files\n        dir1_st = os.stat(\"input/dir1\")\n        file1_st = os.stat(\"input/dir1/file1\")\n        # simulate a partially extracted dir2 (wrong mtime)\n        # simulate a partially extracted file2 (smaller size, archived mtime not yet set)\n        dir2_st = os.stat(\"input/dir2\")\n        file2_st = os.stat(\"input/dir2/file2\")\n        # make a hard link, so it does not free the inode when unlinking input/file2\n        os.link(\"input/dir2/file2\", \"hardlink-to-keep-inode-f2\")\n        os.truncate(\"input/dir2/file2\", 123)  # -> incorrect size, incorrect mtime\n        Path(\"input/dir2\").touch()  # -> mtime \"incorrect\" (not as archived)\n        # simulate dir3 and file3 have not yet been extracted\n        dir3_st = os.stat(\"input/dir3\")\n        file3_st = os.stat(\"input/dir3/file3\")\n        # make a hard link, so it does not free the inode when unlinking input/file3\n        os.link(\"input/dir3/file3\", \"hardlink-to-keep-inode-f3\")\n        os.remove(\"input/dir3/file3\")\n        os.rmdir(\"input/dir3\")\n\n    granularity_sleep()\n\n    with changedir(\"output\"):\n        # now try to continue extracting, using the same archive, same output dir:\n        cmd(archiver, \"extract\", \"arch\", \"--continue\")\n        now_dir1_st = os.stat(\"input/dir1\")\n        now_file1_st = os.stat(\"input/dir1/file1\")\n        assert dir1_st.st_ino == now_dir1_st.st_ino  # dir1 was NOT extracted again\n        assert dir1_st.st_mtime_ns == now_dir1_st.st_mtime_ns  # dir1 has correct mtime\n        assert file1_st.st_ino == now_file1_st.st_ino  # file1 was NOT extracted again\n        assert file1_st.st_mtime_ns == now_file1_st.st_mtime_ns  # has correct mtime\n        now_dir2_st = os.stat(\"input/dir2\")\n        new_file2_st = os.stat(\"input/dir2/file2\")\n        assert dir2_st.st_ino == now_dir2_st.st_ino  # dir2 was not removed/recreated\n        assert dir2_st.st_mtime_ns == now_dir2_st.st_mtime_ns  # dir2 mtime was fixed\n        assert file2_st.st_ino != new_file2_st.st_ino  # file2 was extracted again\n        assert file2_st.st_mtime_ns == new_file2_st.st_mtime_ns  # has correct mtime\n        new_dir3_st = os.stat(\"input/dir3\")\n        new_file3_st = os.stat(\"input/dir3/file3\")\n        assert dir3_st.st_mtime_ns == new_dir3_st.st_mtime_ns  # dir3 was extracted again\n        assert file3_st.st_mtime_ns == new_file3_st.st_mtime_ns  # file3 was extracted again\n        # windows has a strange ctime behaviour when deleting and recreating a file\n        if not is_win32:\n            assert file1_st.st_ctime_ns == now_file1_st.st_ctime_ns  # file not extracted again\n            assert file2_st.st_ctime_ns != new_file2_st.st_ctime_ns  # file extracted again\n            assert file3_st.st_ctime_ns != new_file3_st.st_ctime_ns  # file extracted again\n        # check if all contents (and thus also file sizes) are correct:\n        with open(\"input/dir1/file1\", \"rb\") as f:\n            assert f.read() == CONTENTS1\n        with open(\"input/dir2/file2\", \"rb\") as f:\n            assert f.read() == CONTENTS2\n        with open(\"input/dir3/file3\", \"rb\") as f:\n            assert f.read() == CONTENTS3\n\n\ndef test_dry_run_extraction_flags(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", 0)\n    create_regular_file(archiver.input_path, \"file2\", 0)\n    create_regular_file(archiver.input_path, \"file3\", 0)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    output = cmd(archiver, \"extract\", \"--dry-run\", \"--list\", \"test\", \"-e\", \"input/file3\")\n\n    expected_output = [\"+ input/file1\", \"+ input/file2\", \"- input/file3\"]\n    output_lines = output.splitlines()\n    for expected in expected_output:\n        assert expected in output_lines, f\"Expected line not found: {expected}\"\n        print(output)\n\n    assert not os.listdir(\"output\"), \"Output directory should be empty after dry-run\"\n\n\ndef test_extract_file_with_missing_chunk(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive\")\n    # Get rid of a chunk\n    archive, repository = open_archive(archiver.repository_path, \"archive\")\n    with repository:\n        for item in archive.iter_items():\n            if item.path.endswith(src_file):\n                chunk = item.chunks[-1]\n                repository.delete(chunk.id)\n                break\n        else:\n            assert False  # missed the file\n    output = cmd(archiver, \"extract\", \"archive\")\n    # TODO: this is a bit dirty still: no warning/error rc, no filename output for the damaged file.\n    assert f\"repository object {bin_to_hex(chunk.id)} missing, returning {chunk.size} zero bytes.\" in output\n\n\ndef test_extract_existing_directory(archivers, request):\n    # if we extract a directory and there is already a directory at that location,\n    # we should just use the existing directory and not remove/recreate it.\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    os.mkdir(\"input/dir\")\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        # create pre-existing directory:\n        os.makedirs(\"input/dir\", exist_ok=True)\n        st1 = os.stat(\"input/dir\")\n        # extract\n        cmd(archiver, \"extract\", \"test\")\n        st2 = os.stat(\"input/dir\")\n    assert st1.st_ino == st2.st_ino\n\n\n@pytest.mark.skipif(not is_utime_fully_supported(), reason=\"cannot properly setup and execute test without utime\")\ndef test_extract_y2261(archivers, request):\n    # test if roundtripping of timestamps well beyond y2038 works\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file_y2261\", contents=b\"post y2038 test\")\n    # 2261-01-01 00:00:00 UTC as a Unix timestamp (seconds).\n    time_y2261 = 9183110400\n    os.utime(\"input/file_y2261\", (time_y2261, time_y2261))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n    sto = os.stat(\"output/input/file_y2261\")\n    assert same_ts_ns(sto.st_mtime_ns, time_y2261 * 10**9)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/help_cmd_test.py",
    "content": "import pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers.nanorst import RstToTextLazy, rst_to_terminal\nfrom . import Archiver, cmd\n\n\ndef get_all_parsers():\n    # Return dict mapping command to parser.\n    parser = Archiver(prog=\"borg\").build_parser()\n    borgfs_parser = Archiver(prog=\"borgfs\").build_parser()\n    parsers = {}\n\n    def discover_level(prefix, parser, Archiver, extra_choices=None):\n        choices = {}\n        for action in parser._actions:\n            if action.choices is not None and \"SubParsersAction\" in str(action.__class__):\n                for command, parser in action.choices.items():\n                    choices[prefix + command] = parser\n        if extra_choices is not None:\n            choices.update(extra_choices)\n        if prefix and not choices:\n            return\n\n        for command, parser in sorted(choices.items()):\n            discover_level(command + \" \", parser, Archiver)\n            parsers[command] = parser\n\n    discover_level(\"\", parser, Archiver, {\"borgfs\": borgfs_parser})\n    return parsers\n\n\ndef test_usage(archiver):\n    cmd(archiver)\n    cmd(archiver, \"-h\")\n\n\ndef test_help(archiver):\n    assert \"Borg\" in cmd(archiver, \"help\")\n    assert \"patterns\" in cmd(archiver, \"help\", \"patterns\")\n    assert \"creates a new, empty repository\" in cmd(archiver, \"help\", \"repo-create\")\n    assert \"positional arguments\" not in cmd(archiver, \"help\", \"repo-create\", \"--epilog-only\")\n    assert \"creates a new, empty repository\" not in cmd(archiver, \"help\", \"repo-create\", \"--usage-only\")\n\n\n@pytest.mark.parametrize(\"command, parser\", list(get_all_parsers().items()))\ndef test_help_formatting(command, parser):\n    if isinstance(parser.epilog, RstToTextLazy):\n        assert parser.epilog.rst\n\n\n@pytest.mark.parametrize(\"topic\", list(Archiver.helptext.keys()))\ndef test_help_formatting_helptexts(topic):\n    helptext = Archiver.helptext[topic]\n    assert str(rst_to_terminal(helptext))\n"
  },
  {
    "path": "src/borg/testsuite/archiver/info_cmd_test.py",
    "content": "import json\nimport os\n\nfrom ...constants import *  # NOQA\nfrom .. import changedir\nfrom . import cmd, checkts, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_info(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    info_archive = cmd(archiver, \"info\", \"-a\", \"test\")\n    assert \"Archive name: test\" + os.linesep in info_archive\n    info_archive = cmd(archiver, \"info\", \"--first\", \"1\")\n    assert \"Archive name: test\" + os.linesep in info_archive\n\n\ndef test_info_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    info_archive = json.loads(cmd(archiver, \"info\", \"-a\", \"test\", \"--json\"))\n    archives = info_archive[\"archives\"]\n    assert len(archives) == 1\n    archive = archives[0]\n    assert archive[\"name\"] == \"test\"\n    assert isinstance(archive[\"command_line\"], str)\n    assert isinstance(archive[\"duration\"], float)\n    assert len(archive[\"id\"]) == 64\n    assert archive[\"tags\"] == []\n    assert \"stats\" in archive\n    checkts(archive[\"start\"])\n    checkts(archive[\"end\"])\n\n\ndef test_info_json_of_empty_archive(archivers, request):\n    \"\"\"See https://github.com/borgbackup/borg/issues/6120.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    info_repo = json.loads(cmd(archiver, \"info\", \"--json\", \"--first=1\"))\n    assert info_repo[\"archives\"] == []\n    info_repo = json.loads(cmd(archiver, \"info\", \"--json\", \"--last=1\"))\n    assert info_repo[\"archives\"] == []\n\n\ndef test_info_working_directory(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # create a file in input and create the archive from inside the input directory\n    create_regular_file(archiver.input_path, \"file1\", size=1)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    expected_cwd = os.path.abspath(archiver.input_path)\n    with changedir(archiver.input_path):\n        cmd(archiver, \"create\", \"test\", \".\")\n    info_archive = cmd(archiver, \"info\", \"-a\", \"test\")\n    assert f\"Working Directory: {expected_cwd}\" in info_archive\n"
  },
  {
    "path": "src/borg/testsuite/archiver/key_cmds_test.py",
    "content": "import binascii\nimport os\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPOKeyfileKey, Passphrase\nfrom ...crypto.keymanager import RepoIdMismatch, NotABorgKeyFile\nfrom ...helpers import CommandError\nfrom ...helpers import bin_to_hex, hex_to_bin\nfrom ...helpers import msgpack\nfrom ...repository import Repository\nfrom ..crypto.key_test import TestKey\nfrom . import RK_ENCRYPTION, KF_ENCRYPTION, cmd, _extract_repository_id, _set_repository_id, generate_archiver_tests\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_change_passphrase(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    os.environ[\"BORG_NEW_PASSPHRASE\"] = \"newpassphrase\"\n    # Here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set:\n    cmd(archiver, \"key\", \"change-passphrase\")\n    os.environ[\"BORG_PASSPHRASE\"] = \"newpassphrase\"\n    cmd(archiver, \"repo-list\")\n\n\ndef test_change_location_to_keyfile(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    log = cmd(archiver, \"repo-info\")\n    assert \"(repokey\" in log\n    cmd(archiver, \"key\", \"change-location\", \"keyfile\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(key file\" in log\n\n\ndef test_change_location_to_b2keyfile(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", \"--encryption=repokey-blake2-aes-ocb\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(repokey BLAKE2b\" in log\n    cmd(archiver, \"key\", \"change-location\", \"keyfile\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(key file BLAKE2b\" in log\n\n\ndef test_change_location_to_repokey(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    log = cmd(archiver, \"repo-info\")\n    assert \"(key file\" in log\n    cmd(archiver, \"key\", \"change-location\", \"repokey\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(repokey\" in log\n\n\ndef test_change_location_to_b2repokey(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", \"--encryption=keyfile-blake2-aes-ocb\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(key file BLAKE2b\" in log\n    cmd(archiver, \"key\", \"change-location\", \"repokey\")\n    log = cmd(archiver, \"repo-info\")\n    assert \"(repokey BLAKE2b\" in log\n\n\ndef test_key_export_keyfile(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_file = archiver.output_path + \"/exported\"\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    repo_id = _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"key\", \"export\", export_file)\n\n    with open(export_file) as fd:\n        export_contents = fd.read()\n\n    assert export_contents.startswith(\"BORG_KEY \" + bin_to_hex(repo_id) + \"\\n\")\n\n    key_file = archiver.keys_path + \"/\" + os.listdir(archiver.keys_path)[0]\n\n    with open(key_file) as fd:\n        key_contents = fd.read()\n\n    assert key_contents == export_contents\n\n    os.unlink(key_file)\n\n    cmd(archiver, \"key\", \"import\", export_file)\n\n    with open(key_file) as fd:\n        key_contents2 = fd.read()\n\n    assert key_contents2 == key_contents\n\n\ndef test_key_import_keyfile_with_borg_key_file(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n\n    exported_key_file = os.path.join(archiver.output_path, \"exported\")\n    cmd(archiver, \"key\", \"export\", exported_key_file)\n\n    key_file = os.path.join(archiver.keys_path, os.listdir(archiver.keys_path)[0])\n    with open(key_file) as fd:\n        key_contents = fd.read()\n    os.unlink(key_file)\n\n    imported_key_file = os.path.join(archiver.output_path, \"imported\")\n    monkeypatch.setenv(\"BORG_KEY_FILE\", imported_key_file)\n    cmd(archiver, \"key\", \"import\", exported_key_file)\n    assert not os.path.isfile(key_file), '\"borg key import\" should respect BORG_KEY_FILE'\n\n    with open(imported_key_file) as fd:\n        imported_key_contents = fd.read()\n    assert imported_key_contents == key_contents\n\n\ndef test_key_export_repokey(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_file = archiver.output_path + \"/exported\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    repo_id = _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"key\", \"export\", export_file)\n\n    with open(export_file) as fd:\n        export_contents = fd.read()\n\n    assert export_contents.startswith(\"BORG_KEY \" + bin_to_hex(repo_id) + \"\\n\")\n\n    with Repository(archiver.repository_path) as repository:\n        repo_key = AESOCBRepoKey(repository)\n        repo_key.load(None, Passphrase.env_passphrase())\n\n    backup_key = AESOCBKeyfileKey(TestKey.MockRepository())\n    backup_key.load(export_file, Passphrase.env_passphrase())\n\n    assert repo_key.crypt_key == backup_key.crypt_key\n\n    with Repository(archiver.repository_path) as repository:\n        repository.save_key(b\"\")\n\n    cmd(archiver, \"key\", \"import\", export_file)\n\n    with Repository(archiver.repository_path) as repository:\n        repo_key2 = AESOCBRepoKey(repository)\n        repo_key2.load(None, Passphrase.env_passphrase())\n\n    assert repo_key2.crypt_key == repo_key2.crypt_key\n\n\ndef test_key_export_qr(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_file = archiver.output_path + \"/exported.html\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    repo_id = _extract_repository_id(archiver.repository_path)\n    cmd(archiver, \"key\", \"export\", \"--qr-html\", export_file)\n\n    with open(export_file, encoding=\"utf-8\") as fd:\n        export_contents = fd.read()\n\n    assert bin_to_hex(repo_id) in export_contents\n    assert export_contents.startswith(\"<!doctype html>\")\n    assert export_contents.endswith(\"</html>\\n\")\n\n\ndef test_key_export_directory(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_directory = archiver.output_path + \"/exported\"\n    os.mkdir(export_directory)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        expected_ec = CommandError().exit_code\n        cmd(archiver, \"key\", \"export\", export_directory, exit_code=expected_ec)\n    else:\n        with pytest.raises(CommandError):\n            cmd(archiver, \"key\", \"export\", export_directory)\n\n\ndef test_key_export_qr_directory(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_directory = archiver.output_path + \"/exported\"\n    os.mkdir(export_directory)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        expected_ec = CommandError().exit_code\n        cmd(archiver, \"key\", \"export\", \"--qr-html\", export_directory, exit_code=expected_ec)\n    else:\n        with pytest.raises(CommandError):\n            cmd(archiver, \"key\", \"export\", \"--qr-html\", export_directory)\n\n\ndef test_key_import_errors(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    export_file = archiver.output_path + \"/exported\"\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        expected_ec = CommandError().exit_code\n        cmd(archiver, \"key\", \"import\", export_file, exit_code=expected_ec)\n    else:\n        with pytest.raises(CommandError):\n            cmd(archiver, \"key\", \"import\", export_file)\n\n    with open(export_file, \"w\") as fd:\n        fd.write(\"something not a key\\n\")\n\n    if archiver.FORK_DEFAULT:\n        expected_ec = NotABorgKeyFile().exit_code\n        cmd(archiver, \"key\", \"import\", export_file, exit_code=expected_ec)\n    else:\n        with pytest.raises(NotABorgKeyFile):\n            cmd(archiver, \"key\", \"import\", export_file)\n\n    with open(export_file, \"w\") as fd:\n        fd.write(\"BORG_KEY a0a0a0\\n\")\n\n    if archiver.FORK_DEFAULT:\n        expected_ec = RepoIdMismatch().exit_code\n        cmd(archiver, \"key\", \"import\", export_file, exit_code=expected_ec)\n    else:\n        with pytest.raises(RepoIdMismatch):\n            cmd(archiver, \"key\", \"import\", export_file)\n\n\ndef test_key_export_paperkey(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    repo_id = \"e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239\"\n    export_file = archiver.output_path + \"/exported\"\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    _set_repository_id(archiver.repository_path, hex_to_bin(repo_id))\n    key_file = archiver.keys_path + \"/\" + os.listdir(archiver.keys_path)[0]\n\n    with open(key_file, \"w\") as fd:\n        fd.write(CHPOKeyfileKey.FILE_ID + \" \" + repo_id + \"\\n\")\n        fd.write(binascii.b2a_base64(b\"abcdefghijklmnopqrstu\").decode())\n\n    cmd(archiver, \"key\", \"export\", \"--paper\", export_file)\n\n    with open(export_file) as fd:\n        export_contents = fd.read()\n\n    assert (\n        export_contents\n        == \"\"\"To restore key use borg key import --paper /path/to/repo\n\nBORG PAPER KEY v1\nid: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\n 1: 616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d\n 2: 737475 - 88\n\"\"\"\n    )\n\n\ndef test_key_import_paperkey(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    repo_id = \"e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239\"\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    _set_repository_id(archiver.repository_path, hex_to_bin(repo_id))\n\n    key_file = archiver.keys_path + \"/\" + os.listdir(archiver.keys_path)[0]\n    with open(key_file, \"w\") as fd:\n        fd.write(AESOCBKeyfileKey.FILE_ID + \" \" + repo_id + \"\\n\")\n        fd.write(binascii.b2a_base64(b\"abcdefghijklmnopqrstu\").decode())\n\n    typed_input = (\n        b\"2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41  02\\n\"  # Forgot to type \"-\"\n        b\"2 / e29442 3506da 4e1ea7  25f62a 5a3d41 - 02\\n\"  # Forgot to type second \"/\"\n        b\"2 / e29442 3506da 4e1ea7 / 25f62a 5a3d42 - 02\\n\"  # Typo (..42 not ..41)\n        b\"2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\\n\"  # Correct! Congratulations\n        b\"616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d\\n\"\n        b\"\\n\\n\"  # Abort [yN] => N\n        b\"737475 88\\n\"  # missing \"-\"\n        b\"73747i - 88\\n\"  # typo\n        b\"73747 - 88\\n\"  # missing nibble\n        b\"73 74 75  -  89\\n\"  # line checksum mismatch\n        b\"00a1 - 88\\n\"  # line hash collision - overall hash mismatch, have to start over\n        b\"2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\\n\"\n        b\"616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d\\n\"\n        b\"73 74 75  -  88\\n\"\n    )\n\n    # In case that this has to change, here is a quick way to find a colliding line hash:\n    #\n    # from hashlib import sha256\n    # hash_fn = lambda x: sha256(b'\\x00\\x02' + x).hexdigest()[:2]\n    # for i in range(1000):\n    #     if hash_fn(i.to_bytes(2, byteorder='big')) == '88':  # 88 = line hash\n    #         print(i.to_bytes(2, 'big'))\n    #         break\n\n    cmd(archiver, \"key\", \"import\", \"--paper\", input=typed_input)\n\n    # Test abort paths\n    typed_input = b\"\\ny\\n\"\n    cmd(archiver, \"key\", \"import\", \"--paper\", input=typed_input)\n    typed_input = b\"2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\\n\\ny\\n\"\n    cmd(archiver, \"key\", \"import\", \"--paper\", input=typed_input)\n\n\ndef test_init_defaults_to_argon2(archivers, request):\n    \"\"\"https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    with Repository(archiver.repository_path) as repository:\n        key = msgpack.unpackb(binascii.a2b_base64(repository.load_key()))\n        assert key[\"algorithm\"] == \"argon2 chacha20-poly1305\"\n\n\ndef test_change_passphrase_does_not_change_algorithm_argon2(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    os.environ[\"BORG_NEW_PASSPHRASE\"] = \"newpassphrase\"\n    cmd(archiver, \"key\", \"change-passphrase\")\n\n    with Repository(archiver.repository_path) as repository:\n        key = msgpack.unpackb(binascii.a2b_base64(repository.load_key()))\n        assert key[\"algorithm\"] == \"argon2 chacha20-poly1305\"\n\n\ndef test_change_location_does_not_change_algorithm_argon2(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    cmd(archiver, \"key\", \"change-location\", \"repokey\")\n\n    with Repository(archiver.repository_path) as repository:\n        key = msgpack.unpackb(binascii.a2b_base64(repository.load_key()))\n        assert key[\"algorithm\"] == \"argon2 chacha20-poly1305\"\n"
  },
  {
    "path": "src/borg/testsuite/archiver/list_cmd_test.py",
    "content": "import json\nimport os\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION, requires_hardlinks\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_list_format(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", backup_files)\n    output_1 = cmd(archiver, \"list\", \"test\")\n    output_2 = cmd(\n        archiver, \"list\", \"test\", \"--format\", \"{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}\"\n    )\n    output_3 = cmd(archiver, \"list\", \"test\", \"--format\", \"{mtime:%s} {path}{NL}\")\n    assert output_1 == output_2\n    assert output_1 != output_3\n\n\ndef test_list_hash(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"empty_file\", size=0)\n    create_regular_file(archiver.input_path, \"amb\", contents=b\"a\" * 1000000)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    output = cmd(archiver, \"list\", \"test\", \"--format\", \"{sha256} {path}{NL}\")\n    assert \"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb\" in output\n    assert \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file\" in output\n\n\ndef test_list_chunk_counts(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"empty_file\", size=0)\n    create_regular_file(archiver.input_path, \"two_chunks\")\n    filename = os.path.join(archiver.input_path, \"two_chunks\")\n    with open(filename, \"wb\") as fd:\n        fd.write(b\"abba\" * 2000000)\n        fd.write(b\"baab\" * 2000000)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    os.unlink(filename)  # save space on TMPDIR\n    output = cmd(archiver, \"list\", \"test\", \"--format\", \"{num_chunks} {path}{NL}\")\n    assert \"0 input/empty_file\" in output\n    assert \"2 input/two_chunks\" in output\n\n\ndef test_list_size(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"compressible_file\", size=10000)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"-C\", \"lz4\", \"test\", \"input\")\n    output = cmd(archiver, \"list\", \"test\", \"--format\", \"{size} {path}{NL}\")\n    size, path = output.split(\"\\n\")[1].split(\" \")\n    assert int(size) == 10000\n\n\ndef test_list_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    list_archive = cmd(archiver, \"list\", \"test\", \"--json-lines\")\n    items = [json.loads(s) for s in list_archive.splitlines()]\n    assert len(items) == 2\n    file1 = items[1]\n    assert file1[\"path\"] == \"input/file1\"\n    assert file1[\"size\"] == 81920\n\n    list_archive = cmd(archiver, \"list\", \"test\", \"--json-lines\", \"--format={sha256}\")\n    items = [json.loads(s) for s in list_archive.splitlines()]\n    assert len(items) == 2\n    file1 = items[1]\n    assert file1[\"path\"] == \"input/file1\"\n    assert file1[\"sha256\"] == \"b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b\"\n\n\ndef test_list_json_lines_includes_archive_keys_in_format(archivers, request):\n    # Issue #9095 / PR #9096: archivename/archiveid should be available in JSON lines when\n    # requested via --format.\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Query archive info to obtain expected name and id\n    info_archive = json.loads(cmd(archiver, \"info\", \"--json\", \"-a\", \"test\"))\n    assert len(info_archive[\"archives\"]) == 1\n    archive_info = info_archive[\"archives\"][0]\n    expected_name = archive_info[\"name\"]\n    expected_id = archive_info[\"id\"]\n\n    out = cmd(archiver, \"list\", \"test\", \"--json-lines\", \"--format={archivename} {archiveid}\")\n    rows = [json.loads(s) for s in out.splitlines() if s]\n    assert len(rows) >= 2  # directory + file\n    row = rows[-1]\n    assert row[\"archivename\"] == expected_name\n    assert row[\"archiveid\"] == expected_id\n\n\ndef test_list_depth(archivers, request):\n    \"\"\"Test the --depth option for the list command.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create repository\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Create files at different directory depths\n    create_regular_file(archiver.input_path, \"file_at_depth_1.txt\", size=1)\n    create_regular_file(archiver.input_path, \"dir1/file_at_depth_2.txt\", size=1)\n    create_regular_file(archiver.input_path, \"dir1/dir2/file_at_depth_3.txt\", size=1)\n\n    # Create archive\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Test with depth=0 (only the root directory)\n    output_depth_0 = cmd(archiver, \"list\", \"test\", \"--depth=0\")\n    assert \"input\" in output_depth_0\n    assert \"input/file_at_depth_1.txt\" not in output_depth_0\n    assert \"input/dir1\" not in output_depth_0\n    assert \"input/dir1/file_at_depth_2.txt\" not in output_depth_0\n    assert \"input/dir1/dir2\" not in output_depth_0\n    assert \"input/dir1/dir2/file_at_depth_3.txt\" not in output_depth_0\n\n    # Test with depth=1 (only input directory and files directly in it)\n    output_depth_1 = cmd(archiver, \"list\", \"test\", \"--depth=1\")\n    assert \"input\" in output_depth_1\n    assert \"input/file_at_depth_1.txt\" in output_depth_1\n    assert \"input/dir1\" in output_depth_1\n    assert \"input/dir1/file_at_depth_2.txt\" not in output_depth_1\n    assert \"input/dir1/dir2\" not in output_depth_1\n    assert \"input/dir1/dir2/file_at_depth_3.txt\" not in output_depth_1\n\n    # Test with depth=2 (files up to one level inside input)\n    output_depth_2 = cmd(archiver, \"list\", \"test\", \"--depth=2\")\n    assert \"input\" in output_depth_2\n    assert \"input/file_at_depth_1.txt\" in output_depth_2\n    assert \"input/dir1\" in output_depth_2\n    assert \"input/dir1/file_at_depth_2.txt\" in output_depth_2\n    assert \"input/dir1/dir2\" in output_depth_2\n    assert \"input/dir1/dir2/file_at_depth_3.txt\" not in output_depth_2\n\n    # Test with depth=3 (files up to two levels inside input)\n    output_depth_3 = cmd(archiver, \"list\", \"test\", \"--depth=3\")\n    assert \"input\" in output_depth_3\n    assert \"input/file_at_depth_1.txt\" in output_depth_3\n    assert \"input/dir1\" in output_depth_3\n    assert \"input/dir1/file_at_depth_2.txt\" in output_depth_3\n    assert \"input/dir1/dir2\" in output_depth_3\n    assert \"input/dir1/dir2/file_at_depth_3.txt\" in output_depth_3\n\n    # Test without depth parameter (should show all files)\n    output_no_depth = cmd(archiver, \"list\", \"test\")\n    assert \"input\" in output_no_depth\n    assert \"input/file_at_depth_1.txt\" in output_no_depth\n    assert \"input/dir1\" in output_no_depth\n    assert \"input/dir1/file_at_depth_2.txt\" in output_no_depth\n    assert \"input/dir1/dir2\" in output_no_depth\n    assert \"input/dir1/dir2/file_at_depth_3.txt\" in output_no_depth\n\n\n@requires_hardlinks\ndef test_list_inode_hardlinks(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n\n    # Prepare repository and input files: two hardlinks to same file and one separate file\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"fileA\", contents=b\"DATA\")\n    os.link(os.path.join(archiver.input_path, \"fileA\"), os.path.join(archiver.input_path, \"fileB\"))\n    create_regular_file(archiver.input_path, \"fileC\", contents=b\"DATA\")\n\n    # Create archive\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    # Use ItemFormatter via list --format to output {inode}\n    output = cmd(archiver, \"list\", \"test\", \"--format\", \"{path} {inode}{NL}\")\n\n    # Parse output lines and collect inode numbers for our files\n    inodes = {}\n    for line in output.splitlines():\n        try:\n            path, inode_str = line.rsplit(\" \", 1)\n        except ValueError:\n            continue\n        if path in {\"input/fileA\", \"input/fileB\", \"input/fileC\"}:\n            # inode may be missing (None) on some platforms; convert to int if possible\n            inode = None if inode_str in (\"\", \"None\") else int(inode_str)\n            inodes[path] = inode\n\n    # Ensure we captured all three files\n    assert set(inodes) == {\"input/fileA\", \"input/fileB\", \"input/fileC\"}\n\n    # On platforms where inode is available, verify hardlinks share same inode\n    # If inode is None, the formatter still worked, but platform didn't provide an inode; skip in that case.\n    if inodes[\"input/fileA\"] is not None and inodes[\"input/fileB\"] is not None and inodes[\"input/fileC\"] is not None:\n        assert inodes[\"input/fileA\"] == inodes[\"input/fileB\"]\n        assert inodes[\"input/fileA\"] != inodes[\"input/fileC\"]\n    else:\n        pytest.skip(\"Platform does not provide inode numbers for items\")\n\n\ndef test_fingerprint(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", contents=b\"content\")\n    create_regular_file(archiver.input_path, \"file2\", contents=b\"other\")\n    cmd(archiver, \"create\", \"test1\", \"input\")\n\n    output = cmd(archiver, \"list\", \"test1\", \"--format={fingerprint} {path}{NL}\")\n    fingerprints1 = {}\n    for line in output.splitlines():\n        fp, path = line.split(\" \", 1)\n        fingerprints1[path] = fp\n\n    # Same content, same chunker params -> same fingerprint\n    cmd(archiver, \"create\", \"test2\", \"input\")\n    output = cmd(archiver, \"list\", \"test2\", \"--format={fingerprint} {path}{NL}\")\n    fingerprints2 = {}\n    for line in output.splitlines():\n        fp, path = line.split(\" \", 1)\n        fingerprints2[path] = fp\n    assert fingerprints1 == fingerprints2\n\n    # Modified content -> different fingerprint\n    create_regular_file(archiver.input_path, \"file1\", contents=b\"modification\")\n    cmd(archiver, \"create\", \"test3\", \"input\")\n    output = cmd(archiver, \"list\", \"test3\", \"--format={fingerprint} {path}{NL}\")\n    fingerprints3 = {}\n    for line in output.splitlines():\n        fp, path = line.split(\" \", 1)\n        fingerprints3[path] = fp\n    assert fingerprints1[\"input/file1\"] != fingerprints3[\"input/file1\"]\n    # Unmodified file should still match\n    assert fingerprints1[\"input/file2\"] == fingerprints3[\"input/file2\"]\n\n    # Different chunker params -> different fingerprint\n    # We can use the same repo but specify different chunker params for a new archive\n    cmd(archiver, \"create\", \"--chunker-params=fixed,4096\", \"test4\", \"input\")\n    output = cmd(archiver, \"list\", \"test4\", \"--format={fingerprint} {path}{NL}\")\n    fingerprints4 = {}\n    for line in output.splitlines():\n        fp, path = line.split(\" \", 1)\n        fingerprints4[path] = fp\n\n    # Even unmodified files should have different fingerprints because conditions_hash changed\n    assert fingerprints1[\"input/file2\"] != fingerprints4[\"input/file2\"]\n\n    # Also try with buzhash64\n    cmd(archiver, \"create\", \"--chunker-params=buzhash64,10,23,16,4095\", \"test5\", \"input\")\n    output = cmd(archiver, \"list\", \"test5\", \"--format={fingerprint} {path}{NL}\")\n    fingerprints5 = {}\n    for line in output.splitlines():\n        fp, path = line.split(\" \", 1)\n        fingerprints5[path] = fp\n\n    # Even unmodified files should have different fingerprints because conditions_hash changed\n    assert fingerprints1[\"input/file2\"] != fingerprints5[\"input/file2\"]\n"
  },
  {
    "path": "src/borg/testsuite/archiver/lock_cmds_test.py",
    "content": "import os\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION\nfrom ...helpers import CommandError\nfrom ...platformflags import is_haiku, is_win32\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_break_lock(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"break-lock\")\n\n\n@pytest.mark.skipif(is_haiku or is_win32, reason=\"does not find borg python module on Haiku OS and Windows\")\ndef test_with_lock(tmp_path):\n    repo_path = tmp_path / \"repo\"\n    env = os.environ.copy()\n    env[\"BORG_REPO\"] = Path(repo_path).as_uri()\n    # test debug output:\n    print(\"sys.path: %r\" % sys.path)\n    print(\"PYTHONPATH: %s\" % env.get(\"PYTHONPATH\", \"\"))\n    print(\"PATH: %s\" % env.get(\"PATH\", \"\"))\n    command0 = \"python3\", \"-m\", \"borg\", \"repo-create\", \"--encryption=none\"\n    # Timings must be adjusted so that command1 keeps running while command2 tries to get the lock,\n    # so that lock acquisition for command2 fails as the test expects it.\n    lock_wait = 2\n    command1 = (\"python3\", \"-c\", 'import sys; print(\"first command - acquires the lock\", flush=True); sys.stdin.read()')\n    command2 = \"python3\", \"-c\", 'print(\"second command - should never get executed\")'\n    borgwl = \"python3\", \"-m\", \"borg\", \"with-lock\", f\"--lock-wait={lock_wait}\"\n    popen_options = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)\n    subprocess.run(command0, env=env, check=True, text=True, capture_output=True)\n    assert repo_path.exists()\n\n    p1_options = popen_options.copy()\n    p1_options[\"stdin\"] = subprocess.PIPE\n    with subprocess.Popen([*borgwl, *command1], **p1_options) as p1:\n        assert \"first command\" in p1.stdout.readline()  # Wait until p1 is running and has acquired the lock\n        # Now try to acquire another lock on the same repository:\n        with subprocess.Popen([*borgwl, *command2], **popen_options) as p2:\n            out, err_out = p2.communicate()\n            assert \"second command\" not in out  # command2 is \"locked out\"\n            assert \"Failed to create/acquire the lock\" in err_out\n            assert p2.returncode == 73  # LockTimeout: could not acquire the lock, p1 already has it\n        out, err_out = p1.communicate(input=\"\")  # Unblock command1 and read output\n        assert not err_out\n        assert p1.returncode == 0\n\n\ndef test_with_lock_non_existent_command(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    command = [\"non_existent_command\"]\n    expected_ec = CommandError().exit_code\n    cmd(archiver, \"with-lock\", *command, fork=True, exit_code=expected_ec)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/mount_cmds_test.py",
    "content": "# This file tests the mount/umount commands.\n# The FUSE implementation used depends on the BORG_FUSE_IMPL environment variable:\n# - BORG_FUSE_IMPL=pyfuse3,llfuse: Tests run with llfuse/pyfuse3 (skipped if not available)\n# - BORG_FUSE_IMPL=mfusepy: Tests run with mfusepy (skipped if not available)\n# The tox configuration (pyproject.toml) runs these tests with different BORG_FUSE_IMPL settings.\n\nimport errno\nimport os\nimport stat\nimport sys\n\nimport pytest\n\nfrom ... import xattr, platform\nfrom ...constants import *  # NOQA\nfrom ...storelocking import Lock\nfrom ...helpers import flags_noatime, flags_normal\nfrom .. import has_lchflags, has_any_fuse, ENOATTR\nfrom .. import changedir, filter_xattrs, same_ts_ns\nfrom .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported\nfrom ..platform.platform_test import fakeroot_detected\nfrom . import RK_ENCRYPTION, cmd, assert_dirs_equal, create_regular_file, create_src_archive, open_archive, src_file\nfrom . import requires_hardlinks, _extract_hardlinks_setup, fuse_mount, create_test_files, generate_archiver_tests\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\n@requires_hardlinks\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse_mount_hardlinks(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _extract_hardlinks_setup(archiver)\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    # we need to get rid of permissions checking because fakeroot causes issues with it.\n    # On all platforms, borg defaults to \"default_permissions\" and we need to get rid of it via \"ignore_permissions\".\n    # On macOS (darwin), we additionally need \"defer_permissions\" to switch off the checks in osxfuse.\n    if sys.platform == \"darwin\":\n        ignore_perms = [\"-o\", \"ignore_permissions,defer_permissions\"]\n    else:\n        ignore_perms = [\"-o\", \"ignore_permissions\"]\n    with (\n        fuse_mount(archiver, mountpoint, \"-a\", \"test\", \"--strip-components=2\", *ignore_perms),\n        changedir(os.path.join(mountpoint, \"test\")),\n    ):\n        assert os.stat(\"hardlink\").st_nlink == 2\n        assert os.stat(\"subdir/hardlink\").st_nlink == 2\n        assert open(\"subdir/hardlink\", \"rb\").read() == b\"123456\"\n        assert os.stat(\"aaaa\").st_nlink == 2\n        assert os.stat(\"source2\").st_nlink == 2\n    with (\n        fuse_mount(archiver, mountpoint, \"input/dir1\", \"-a\", \"test\", *ignore_perms),\n        changedir(os.path.join(mountpoint, \"test\")),\n    ):\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 2\n        assert open(\"input/dir1/subdir/hardlink\", \"rb\").read() == b\"123456\"\n        assert os.stat(\"input/dir1/aaaa\").st_nlink == 2\n        assert os.stat(\"input/dir1/source2\").st_nlink == 2\n    with fuse_mount(archiver, mountpoint, \"-a\", \"test\", *ignore_perms), changedir(os.path.join(mountpoint, \"test\")):\n        assert os.stat(\"input/source\").st_nlink == 4\n        assert os.stat(\"input/abba\").st_nlink == 4\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 4\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 4\n        assert open(\"input/dir1/subdir/hardlink\", \"rb\").read() == b\"123456\"\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE and fakeroot_detected():\n        pytest.skip(\"test_fuse with the binary is not compatible with fakeroot\")\n\n    def has_noatime(some_file):\n        atime_before = os.stat(some_file).st_atime_ns\n        try:\n            os.close(os.open(some_file, flags_noatime))\n        except PermissionError:\n            return False\n        else:\n            atime_after = os.stat(some_file).st_atime_ns\n            noatime_used = flags_noatime != flags_normal\n            return noatime_used and atime_before == atime_after\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_test_files(archiver.input_path)\n    have_noatime = has_noatime(\"input/file1\")\n    cmd(archiver, \"create\", \"--atime\", \"archive\", \"input\")\n    cmd(archiver, \"create\", \"--atime\", \"archive2\", \"input\")\n    if has_lchflags:\n        # remove the file that we did not back up, so input and output become equal\n        os.remove(os.path.join(\"input\", \"flagfile\"))\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    # mount the whole repository, archive contents shall show up in archivename subdirectories of mountpoint:\n    with fuse_mount(archiver, mountpoint):\n        # flags are not supported by the FUSE mount\n        # we also ignore xattrs here, they are tested separately\n        assert_dirs_equal(\n            archiver.input_path, os.path.join(mountpoint, \"archive\", \"input\"), ignore_flags=True, ignore_xattrs=True\n        )\n        assert_dirs_equal(\n            archiver.input_path, os.path.join(mountpoint, \"archive2\", \"input\"), ignore_flags=True, ignore_xattrs=True\n        )\n    with fuse_mount(archiver, mountpoint, \"-a\", \"archive\"):\n        assert_dirs_equal(\n            archiver.input_path, os.path.join(mountpoint, \"archive\", \"input\"), ignore_flags=True, ignore_xattrs=True\n        )\n        # regular file\n        in_fn = \"input/file1\"\n        out_fn = os.path.join(mountpoint, \"archive\", \"input\", \"file1\")\n        # stat\n        sti1 = os.stat(in_fn)\n        sto1 = os.stat(out_fn)\n        assert sti1.st_mode == sto1.st_mode\n        assert sti1.st_uid == sto1.st_uid\n        assert sti1.st_gid == sto1.st_gid\n        assert sti1.st_size == sto1.st_size\n        if have_noatime:\n            assert same_ts_ns(sti1.st_atime * 1e9, sto1.st_atime * 1e9)\n        assert same_ts_ns(sti1.st_ctime * 1e9, sto1.st_ctime * 1e9)\n        assert same_ts_ns(sti1.st_mtime * 1e9, sto1.st_mtime * 1e9)\n        if are_hardlinks_supported():\n            # note: there is another hard link to this, see below\n            assert sti1.st_nlink == sto1.st_nlink == 2\n        # read\n        with open(in_fn, \"rb\") as in_f, open(out_fn, \"rb\") as out_f:\n            assert in_f.read() == out_f.read()\n        # hard link (to 'input/file1')\n        if are_hardlinks_supported():\n            in_fn = \"input/hardlink\"\n            out_fn = os.path.join(mountpoint, \"archive\", \"input\", \"hardlink\")\n            sti2 = os.stat(in_fn)\n            sto2 = os.stat(out_fn)\n            assert sti2.st_nlink == sto2.st_nlink == 2\n            assert sto1.st_ino == sto2.st_ino\n        # symlink\n        if are_symlinks_supported():\n            in_fn = \"input/link1\"\n            out_fn = os.path.join(mountpoint, \"archive\", \"input\", \"link1\")\n            sti = os.stat(in_fn, follow_symlinks=False)\n            sto = os.stat(out_fn, follow_symlinks=False)\n            assert sti.st_size == len(\"somewhere\")\n            assert sto.st_size == len(\"somewhere\")\n            assert stat.S_ISLNK(sti.st_mode)\n            assert stat.S_ISLNK(sto.st_mode)\n            assert os.readlink(in_fn) == os.readlink(out_fn)\n        # FIFO\n        if are_fifos_supported():\n            out_fn = os.path.join(mountpoint, \"archive\", \"input\", \"fifo1\")\n            sto = os.stat(out_fn)\n            assert stat.S_ISFIFO(sto.st_mode)\n        # list/read xattrs\n        try:\n            in_fn = \"input/fusexattr\"\n            out_fn = os.fsencode(os.path.join(mountpoint, \"archive\", \"input\", \"fusexattr\"))\n            if not xattr.XATTR_FAKEROOT and xattr.is_enabled(archiver.input_path):\n                assert sorted(filter_xattrs(xattr.listxattr(out_fn))) == [b\"user.empty\", b\"user.foo\"]\n                assert xattr.getxattr(out_fn, b\"user.foo\") == b\"bar\"\n                assert xattr.getxattr(out_fn, b\"user.empty\") == b\"\"\n            else:\n                assert filter_xattrs(xattr.listxattr(out_fn)) == []\n                try:\n                    xattr.getxattr(out_fn, b\"user.foo\")\n                except OSError as e:\n                    assert e.errno == ENOATTR\n                else:\n                    assert False, \"expected OSError(ENOATTR), but no error was raised\"\n        except OSError as err:\n            if sys.platform.startswith((\"nothing_here_now\",)) and err.errno == errno.ENOTSUP:\n                # some systems have no xattr support on FUSE\n                pass\n            else:\n                raise\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse_versions_view(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"test\", contents=b\"first\")\n    if are_hardlinks_supported():\n        create_regular_file(archiver.input_path, \"hardlink1\", contents=b\"123456\")\n        os.link(\"input/hardlink1\", \"input/hardlink2\")\n        os.link(\"input/hardlink1\", \"input/hardlink3\")\n    cmd(archiver, \"create\", \"archive1\", \"input\")\n    create_regular_file(archiver.input_path, \"test\", contents=b\"second\")\n    cmd(archiver, \"create\", \"archive2\", \"input\")\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    # mount the whole repository, archive contents shall show up in versioned view:\n    with fuse_mount(archiver, mountpoint, \"-o\", \"versions\"):\n        path = os.path.join(mountpoint, \"input\", \"test\")  # filename shows up as directory ...\n        files = os.listdir(path)\n        assert all(f.startswith(\"test.\") for f in files)  # ... with files test.xxxxx in there\n        assert {b\"first\", b\"second\"} == {open(os.path.join(path, f), \"rb\").read() for f in files}\n        if are_hardlinks_supported():\n            hl1 = os.path.join(mountpoint, \"input\", \"hardlink1\", \"hardlink1.00001\")\n            hl2 = os.path.join(mountpoint, \"input\", \"hardlink2\", \"hardlink2.00001\")\n            hl3 = os.path.join(mountpoint, \"input\", \"hardlink3\", \"hardlink3.00001\")\n            assert os.stat(hl1).st_ino == os.stat(hl2).st_ino == os.stat(hl3).st_ino\n            assert open(hl3, \"rb\").read() == b\"123456\"\n    # similar again, but exclude the 1st hard link:\n    with fuse_mount(archiver, mountpoint, \"-o\", \"versions\", \"-e\", \"input/hardlink1\"):\n        if are_hardlinks_supported():\n            hl2 = os.path.join(mountpoint, \"input\", \"hardlink2\", \"hardlink2.00001\")\n            hl3 = os.path.join(mountpoint, \"input\", \"hardlink3\", \"hardlink3.00001\")\n            assert os.stat(hl2).st_ino == os.stat(hl3).st_ino\n            assert open(hl3, \"rb\").read() == b\"123456\"\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse_duplicate_name(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"duplicate\", \"input\")\n    cmd(archiver, \"create\", \"duplicate\", \"input\")\n    cmd(archiver, \"create\", \"unique1\", \"input\")\n    cmd(archiver, \"create\", \"unique2\", \"input\")\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    # mount the whole repository, archives show up as toplevel directories:\n    with fuse_mount(archiver, mountpoint):\n        path = os.path.join(mountpoint)\n        dirs = os.listdir(path)\n        assert len(set(dirs)) == 4  # there must be 4 unique dir names for 4 archives\n        assert \"unique1\" in dirs  # if an archive has a unique name, do not append the archive id\n        assert \"unique2\" in dirs\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse_allow_damaged_files(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"archive\")\n    # Get rid of a chunk and repair it\n    archive, repository = open_archive(archiver.repository_path, \"archive\")\n    with repository:\n        for item in archive.iter_items():\n            if item.path.endswith(src_file):\n                repository.delete(item.chunks[-1].id)\n                path = item.path  # store full path for later\n                break\n        else:\n            assert False  # missed the file\n\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    with fuse_mount(archiver, mountpoint, \"-a\", \"archive\"):\n        with open(os.path.join(mountpoint, \"archive\", path), \"rb\") as f:\n            with pytest.raises(OSError) as excinfo:\n                f.read()\n            assert excinfo.value.errno == errno.EIO\n\n    with fuse_mount(archiver, mountpoint, \"-a\", \"archive\", \"-o\", \"allow_damaged_files\"):\n        with open(os.path.join(mountpoint, \"archive\", path), \"rb\") as f:\n            # no exception raised, missing data will be all-zero\n            data = f.read()\n        assert data.endswith(b\"\\0\\0\")\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_fuse_mount_options(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_src_archive(archiver, \"arch11\")\n    create_src_archive(archiver, \"arch12\")\n    create_src_archive(archiver, \"arch21\")\n    create_src_archive(archiver, \"arch22\")\n    mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n    with fuse_mount(archiver, mountpoint, \"--first=2\", \"--sort=name\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == [\"arch11\", \"arch12\"]\n    with fuse_mount(archiver, mountpoint, \"--last=2\", \"--sort=name\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == [\"arch21\", \"arch22\"]\n    with fuse_mount(archiver, mountpoint, \"--match-archives=sh:arch1*\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == [\"arch11\", \"arch12\"]\n    with fuse_mount(archiver, mountpoint, \"--match-archives=sh:arch2*\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == [\"arch21\", \"arch22\"]\n    with fuse_mount(archiver, mountpoint, \"--match-archives=sh:arch*\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == [\"arch11\", \"arch12\", \"arch21\", \"arch22\"]\n    with fuse_mount(archiver, mountpoint, \"--match-archives=nope\"):\n        assert sorted(os.listdir(os.path.join(mountpoint))) == []\n\n\n@pytest.mark.skipif(not has_any_fuse, reason=\"FUSE not available\")\ndef test_migrate_lock_alive(archivers, request):\n    \"\"\"Both old_id and new_id must not be stale during lock migration / daemonization.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() == \"remote\":\n        pytest.skip(\"only works locally\")\n    from functools import wraps\n    import pickle\n    import traceback\n\n    # Check results are communicated from the borg mount background process\n    # to the pytest process by means of a serialized dict object stored in this file.\n    assert_data_file = os.path.join(archiver.tmpdir, \"migrate_lock_assert_data.pickle\")\n\n    # Decorates Lock.migrate_lock() with process_alive() checks before and after.\n    # (We don't want to mix testing code into runtime.)\n    def write_assert_data(migrate_lock):\n        @wraps(migrate_lock)\n        def wrapper(self, old_id, new_id):\n            wrapper.num_calls += 1\n            assert_data = {\n                \"num_calls\": wrapper.num_calls,\n                \"old_id\": old_id,\n                \"new_id\": new_id,\n                \"before\": {\n                    \"old_id_alive\": platform.process_alive(*old_id),\n                    \"new_id_alive\": platform.process_alive(*new_id),\n                },\n                \"exception\": None,\n                \"exception.extr_tb\": None,\n                \"after\": {\"old_id_alive\": None, \"new_id_alive\": None},\n            }\n            try:\n                with open(assert_data_file, \"wb\") as _out:\n                    pickle.dump(assert_data, _out)\n            except:  # noqa\n                pass\n            try:\n                return migrate_lock(self, old_id, new_id)\n            except BaseException as e:\n                assert_data[\"exception\"] = e\n                assert_data[\"exception.extr_tb\"] = traceback.extract_tb(e.__traceback__)\n            finally:\n                assert_data[\"after\"].update(\n                    {\"old_id_alive\": platform.process_alive(*old_id), \"new_id_alive\": platform.process_alive(*new_id)}\n                )\n                try:\n                    with open(assert_data_file, \"wb\") as _out:\n                        pickle.dump(assert_data, _out)\n                except:  # noqa\n                    pass\n\n        wrapper.num_calls = 0\n        return wrapper\n\n    # Decorate\n    Lock.migrate_lock = write_assert_data(Lock.migrate_lock)\n    try:\n        cmd(archiver, \"repo-create\", \"--encryption=none\")\n        create_src_archive(archiver, \"arch\")\n        mountpoint = os.path.join(archiver.tmpdir, \"mountpoint\")\n        # In order that the decoration is kept for the borg mount process, we must not spawn, but actually fork;\n        # not to be confused with the forking in borg.helpers.daemonize() which is done as well.\n        with fuse_mount(archiver, mountpoint, os_fork=True):\n            pass\n        with open(assert_data_file, \"rb\") as _in:\n            assert_data = pickle.load(_in)\n        print(f\"\\nLock.migrate_lock(): assert_data = {assert_data!r}.\", file=sys.stderr, flush=True)\n        exception = assert_data[\"exception\"]\n        if exception is not None:\n            extracted_tb = assert_data[\"exception.extr_tb\"]\n            print(\n                \"Lock.migrate_lock() raised an exception:\\n\",\n                \"Traceback (most recent call last):\\n\",\n                *traceback.format_list(extracted_tb),\n                *traceback.format_exception(exception.__class__, exception, None),\n                sep=\"\",\n                end=\"\",\n                file=sys.stderr,\n                flush=True,\n            )\n\n        assert assert_data[\"num_calls\"] == 1, \"Lock.migrate_lock() must be called exactly once.\"\n        assert exception is None, \"Lock.migrate_lock() may not raise an exception.\"\n\n        assert_data_before = assert_data[\"before\"]\n        assert assert_data_before[\n            \"old_id_alive\"\n        ], \"old_id must be alive (=must not be stale) when calling Lock.migrate_lock().\"\n        assert assert_data_before[\n            \"new_id_alive\"\n        ], \"new_id must be alive (=must not be stale) when calling Lock.migrate_lock().\"\n\n        assert_data_after = assert_data[\"after\"]\n        assert assert_data_after[\n            \"old_id_alive\"\n        ], \"old_id must be alive (=must not be stale) when Lock.migrate_lock() has returned.\"\n        assert assert_data_after[\n            \"new_id_alive\"\n        ], \"new_id must be alive (=must not be stale) when Lock.migrate_lock() has returned.\"\n    finally:\n        # Undecorate\n        Lock.migrate_lock = Lock.migrate_lock.__wrapped__\n"
  },
  {
    "path": "src/borg/testsuite/archiver/patterns_test.py",
    "content": "from ...archiver._common import build_filter\nfrom ...constants import *  # NOQA\nfrom ...patterns import IECommand, PatternMatcher, parse_pattern\nfrom ...item import Item\n\n\ndef test_basic():\n    matcher = PatternMatcher()\n    matcher.add([parse_pattern(\"included\")], IECommand.Include)\n    filter = build_filter(matcher, 0)\n    assert filter(Item(path=\"included\"))\n    assert filter(Item(path=\"included/file\"))\n    assert not filter(Item(path=\"something else\"))\n\n\ndef test_empty():\n    matcher = PatternMatcher(fallback=True)\n    filter = build_filter(matcher, 0)\n    assert filter(Item(path=\"anything\"))\n\n\ndef test_strip_components():\n    matcher = PatternMatcher(fallback=True)\n    filter = build_filter(matcher, strip_components=1)\n    assert not filter(Item(path=\"shallow\"))\n    assert filter(Item(path=\"deep enough/file\"))\n    assert filter(Item(path=\"something/dir/file\"))\n"
  },
  {
    "path": "src/borg/testsuite/archiver/prune_cmd_test.py",
    "content": "import re\nfrom datetime import datetime, timezone, timedelta\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...archiver.prune_cmd import prune_split, prune_within\nfrom . import cmd, RK_ENCRYPTION, generate_archiver_tests\nfrom ...helpers import interval\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef _create_archive_ts(archiver, backup_files, name, y, m, d, H=0, M=0, S=0):\n    cmd(\n        archiver,\n        \"create\",\n        \"--timestamp\",\n        datetime(y, m, d, H, M, S, 0).strftime(ISO_FORMAT_NO_USECS),  # naive == local time / local tz\n        name,\n        backup_files,\n    )\n\n\ndef test_prune_repository(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", backup_files)\n    cmd(archiver, \"create\", \"test2\", backup_files)\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=1\")\n    assert re.search(r\"Would prune:\\s+test1\", output)\n    # Must keep the latest archive:\n    assert re.search(r\"Keeping archive \\(rule: daily #1\\):\\s+test2\", output)\n    output = cmd(archiver, \"repo-list\")\n    assert \"test1\" in output\n    assert \"test2\" in output\n    cmd(archiver, \"prune\", \"--keep-daily=1\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"test1\" not in output\n    # The latest archive must still be there:\n    assert \"test2\" in output\n\n\n# This test must match docs/misc/prune-example.txt\ndef test_prune_repository_example(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Archives that will be kept, per the example\n    # Oldest archive\n    _create_archive_ts(archiver, backup_files, \"test01\", 2015, 1, 1)\n    # 6 monthly archives\n    _create_archive_ts(archiver, backup_files, \"test02\", 2015, 6, 30)\n    _create_archive_ts(archiver, backup_files, \"test03\", 2015, 7, 31)\n    _create_archive_ts(archiver, backup_files, \"test04\", 2015, 8, 31)\n    _create_archive_ts(archiver, backup_files, \"test05\", 2015, 9, 30)\n    _create_archive_ts(archiver, backup_files, \"test06\", 2015, 10, 31)\n    _create_archive_ts(archiver, backup_files, \"test07\", 2015, 11, 30)\n    # 14 daily archives\n    _create_archive_ts(archiver, backup_files, \"test08\", 2015, 12, 17)\n    _create_archive_ts(archiver, backup_files, \"test09\", 2015, 12, 18)\n    _create_archive_ts(archiver, backup_files, \"test10\", 2015, 12, 20)\n    _create_archive_ts(archiver, backup_files, \"test11\", 2015, 12, 21)\n    _create_archive_ts(archiver, backup_files, \"test12\", 2015, 12, 22)\n    _create_archive_ts(archiver, backup_files, \"test13\", 2015, 12, 23)\n    _create_archive_ts(archiver, backup_files, \"test14\", 2015, 12, 24)\n    _create_archive_ts(archiver, backup_files, \"test15\", 2015, 12, 25)\n    _create_archive_ts(archiver, backup_files, \"test16\", 2015, 12, 26)\n    _create_archive_ts(archiver, backup_files, \"test17\", 2015, 12, 27)\n    _create_archive_ts(archiver, backup_files, \"test18\", 2015, 12, 28)\n    _create_archive_ts(archiver, backup_files, \"test19\", 2015, 12, 29)\n    _create_archive_ts(archiver, backup_files, \"test20\", 2015, 12, 30)\n    _create_archive_ts(archiver, backup_files, \"test21\", 2015, 12, 31)\n    # Additional archives that would be pruned\n    # The second backup of the year\n    _create_archive_ts(archiver, backup_files, \"test22\", 2015, 1, 2)\n    # The next older monthly backup\n    _create_archive_ts(archiver, backup_files, \"test23\", 2015, 5, 31)\n    # The next older daily backup\n    _create_archive_ts(archiver, backup_files, \"test24\", 2015, 12, 16)\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=14\", \"--keep-monthly=6\", \"--keep-yearly=1\")\n    # Prune second backup of the year\n    assert re.search(r\"Would prune:\\s+test22\", output)\n    # Prune next older monthly and daily backups\n    assert re.search(r\"Would prune:\\s+test23\", output)\n    assert re.search(r\"Would prune:\\s+test24\", output)\n    # Must keep the other 21 backups\n    # Yearly is kept as oldest archive\n    assert re.search(r\"Keeping archive \\(rule: yearly\\[oldest\\] #1\\):\\s+test01\", output)\n    for i in range(1, 7):\n        assert re.search(r\"Keeping archive \\(rule: monthly #\" + str(i) + r\"\\):\\s+test\" + (\"%02d\" % (8 - i)), output)\n    for i in range(1, 15):\n        assert re.search(r\"Keeping archive \\(rule: daily #\" + str(i) + r\"\\):\\s+test\" + (\"%02d\" % (22 - i)), output)\n    output = cmd(archiver, \"repo-list\")\n    # Nothing pruned after dry run\n    for i in range(1, 25):\n        assert \"test%02d\" % i in output\n    cmd(archiver, \"prune\", \"--keep-daily=14\", \"--keep-monthly=6\", \"--keep-yearly=1\")\n    output = cmd(archiver, \"repo-list\")\n    # All matching backups plus oldest kept\n    for i in range(1, 22):\n        assert \"test%02d\" % i in output\n    # Other backups have been pruned\n    for i in range(22, 25):\n        assert \"test%02d\" % i not in output\n\n\ndef test_prune_quarterly(archivers, request, backup_files):\n    # Example worked through by hand when developing the quarterly\n    # strategy, based on existing backups where the quarterly strategy\n    # is desired. Weekly/monthly backups that do not affect results were\n    # trimmed to speed up the test.\n    #\n    # The ISO week number is shown in a comment for each row in the list below.\n    # The year is also shown when it does not match the year given in the\n    # date tuple.\n    archiver = request.getfixturevalue(archivers)\n    test_dates = [\n        (2020, 12, 6),\n        (2021, 1, 3),  # 49, 2020-53\n        (2021, 3, 28),\n        (2021, 4, 25),  # 12, 16\n        (2021, 6, 27),\n        (2021, 7, 4),  # 25, 26\n        (2021, 9, 26),\n        (2021, 10, 3),  # 38, 39\n        (2021, 12, 26),\n        (2022, 1, 2),  # 51, 2021-52\n    ]\n\n    def mk_name(tup):\n        (y, m, d) = tup\n        suff = datetime(y, m, d).strftime(\"%Y-%m-%d\")\n        return f\"test-{suff}\"\n\n    # The kept repos are based on working on an example by hand,\n    # archives made on the following dates should be kept:\n    EXPECTED_KEPT = {\n        \"13weekly\": [(2020, 12, 6), (2021, 1, 3), (2021, 3, 28), (2021, 7, 4), (2021, 10, 3), (2022, 1, 2)],\n        \"3monthly\": [(2020, 12, 6), (2021, 3, 28), (2021, 6, 27), (2021, 9, 26), (2021, 12, 26), (2022, 1, 2)],\n    }\n\n    for strat, to_keep in EXPECTED_KEPT.items():\n        # Initialize our repo.\n        cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n        for a, (y, m, d) in zip(map(mk_name, test_dates), test_dates):\n            _create_archive_ts(archiver, backup_files, a, y, m, d)\n\n        to_prune = list(set(test_dates) - set(to_keep))\n\n        # Use 99 instead of -1 to test that oldest backup is kept.\n        output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", f\"--keep-{strat}=99\")\n        for a in map(mk_name, to_prune):\n            assert re.search(rf\"Would prune:\\s+{a}\", output)\n\n        oldest = r\"\\[oldest\\]\" if strat in (\"13weekly\") else \"\"\n        assert re.search(rf\"Keeping archive \\(rule: quarterly_{strat}{oldest} #\\d+\\):\\s+test-2020-12-06\", output)\n        for a in map(mk_name, to_keep[1:]):\n            assert re.search(rf\"Keeping archive \\(rule: quarterly_{strat} #\\d+\\):\\s+{a}\", output)\n\n        output = cmd(archiver, \"repo-list\")\n        # Nothing pruned after dry run\n        for a in map(mk_name, test_dates):\n            assert a in output\n\n        cmd(archiver, \"prune\", f\"--keep-{strat}=99\")\n        output = cmd(archiver, \"repo-list\")\n        # All matching backups plus oldest kept\n        for a in map(mk_name, to_keep):\n            assert a in output\n        # Other backups have been pruned\n        for a in map(mk_name, to_prune):\n            assert a not in output\n\n        # Delete repo and begin anew\n        cmd(archiver, \"repo-delete\")\n\n\n# With an initial and daily backup, prune daily until oldest is replaced by a monthly backup\ndef test_prune_retain_and_expire_oldest(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # Initial backup\n    _create_archive_ts(archiver, backup_files, \"original_archive\", 2020, 9, 1, 11, 15)\n    # Archive and prune daily for 30 days\n    for i in range(1, 31):\n        _create_archive_ts(archiver, backup_files, \"september%02d\" % i, 2020, 9, i, 12)\n        cmd(archiver, \"prune\", \"--keep-daily=7\", \"--keep-monthly=1\")\n    # Archive and prune 6 days into the next month\n    for i in range(1, 7):\n        _create_archive_ts(archiver, backup_files, \"october%02d\" % i, 2020, 10, i, 12)\n        cmd(archiver, \"prune\", \"--keep-daily=7\", \"--keep-monthly=1\")\n    # Oldest backup is still retained\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=7\", \"--keep-monthly=1\")\n    assert re.search(r\"Keeping archive \\(rule: monthly\\[oldest\\] #1\" + r\"\\):\\s+original_archive\", output)\n    # Archive one more day and prune.\n    _create_archive_ts(archiver, backup_files, \"october07\", 2020, 10, 7, 12)\n    cmd(archiver, \"prune\", \"--keep-daily=7\", \"--keep-monthly=1\")\n    # Last day of previous month is retained as monthly, and oldest is expired.\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=7\", \"--keep-monthly=1\")\n    assert re.search(r\"Keeping archive \\(rule: monthly #1\\):\\s+september30\", output)\n    assert \"original_archive\" not in output\n\n\ndef test_prune_repository_prefix(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"foo-2015-08-12-10:00\", backup_files)\n    cmd(archiver, \"create\", \"foo-2015-08-12-20:00\", backup_files)\n    cmd(archiver, \"create\", \"bar-2015-08-12-10:00\", backup_files)\n    cmd(archiver, \"create\", \"bar-2015-08-12-20:00\", backup_files)\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=1\", \"--match-archives=sh:foo-*\")\n    assert re.search(r\"Keeping archive \\(rule: daily #1\\):\\s+foo-2015-08-12-20:00\", output)\n    assert re.search(r\"Would prune:\\s+foo-2015-08-12-10:00\", output)\n    output = cmd(archiver, \"repo-list\")\n    assert \"foo-2015-08-12-10:00\" in output\n    assert \"foo-2015-08-12-20:00\" in output\n    assert \"bar-2015-08-12-10:00\" in output\n    assert \"bar-2015-08-12-20:00\" in output\n    cmd(archiver, \"prune\", \"--keep-daily=1\", \"--match-archives=sh:foo-*\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"foo-2015-08-12-10:00\" not in output\n    assert \"foo-2015-08-12-20:00\" in output\n    assert \"bar-2015-08-12-10:00\" in output\n    assert \"bar-2015-08-12-20:00\" in output\n\n\ndef test_prune_repository_glob(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"2015-08-12-10:00-foo\", backup_files)\n    cmd(archiver, \"create\", \"2015-08-12-20:00-foo\", backup_files)\n    cmd(archiver, \"create\", \"2015-08-12-10:00-bar\", backup_files)\n    cmd(archiver, \"create\", \"2015-08-12-20:00-bar\", backup_files)\n    output = cmd(archiver, \"prune\", \"--list\", \"--dry-run\", \"--keep-daily=1\", \"--match-archives=sh:2015-*-foo\")\n    assert re.search(r\"Keeping archive \\(rule: daily #1\\):\\s+2015-08-12-20:00-foo\", output)\n    assert re.search(r\"Would prune:\\s+2015-08-12-10:00-foo\", output)\n    output = cmd(archiver, \"repo-list\")\n    assert \"2015-08-12-10:00-foo\" in output\n    assert \"2015-08-12-20:00-foo\" in output\n    assert \"2015-08-12-10:00-bar\" in output\n    assert \"2015-08-12-20:00-bar\" in output\n    cmd(archiver, \"prune\", \"--keep-daily=1\", \"--match-archives=sh:2015-*-foo\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"2015-08-12-10:00-foo\" not in output\n    assert \"2015-08-12-20:00-foo\" in output\n    assert \"2015-08-12-10:00-bar\" in output\n    assert \"2015-08-12-20:00-bar\" in output\n\n\ndef test_prune_ignore_protected(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive1\", archiver.input_path)\n    cmd(archiver, \"tag\", \"--set=@PROT\", \"archive1\")  # do not delete archive1!\n    cmd(archiver, \"create\", \"archive2\", archiver.input_path)\n    cmd(archiver, \"create\", \"archive3\", archiver.input_path)\n    output = cmd(archiver, \"prune\", \"--list\", \"--keep-last=1\", \"--match-archives=sh:archive*\")\n    assert \"archive1\" not in output  # @PROT archives are completely ignored.\n    assert re.search(r\"Keeping archive \\(rule: secondly #1\\):\\s+archive3\", output)\n    assert re.search(r\"Pruning archive \\(.*?\\):\\s+archive2\", output)\n    output = cmd(archiver, \"repo-list\")\n    assert \"archive1\" in output  # @PROT protected archive1 from deletion\n    assert \"archive3\" in output  # last one\n\n\nclass MockArchive:\n    def __init__(self, ts, id):\n        self.ts = ts\n        self.id = id\n\n    def __repr__(self):\n        return f\"{self.id}: {self.ts.isoformat()}\"\n\n\n# This is the local timezone of the system running the tests.\n# We need this e.g. to construct archive timestamps for the prune tests,\n# because borg prune operates in the local timezone (it first converts the\n# archive timestamp to the local timezone). So, if we want the y/m/d/h/m/s\n# values which prune uses to be exactly the ones we give [and NOT shift them\n# by tzoffset], we need to give the timestamps in the same local timezone.\n# Please note that the timestamps in a real borg archive or manifest are\n# stored in UTC timezone.\nlocal_tz = datetime.now(tz=timezone.utc).astimezone(tz=None).tzinfo\n\n\ndef test_prune_within():\n    def subset(lst, indices):\n        return {lst[i] for i in indices}\n\n    def dotest(test_archives, within, indices):\n        for ta in test_archives, reversed(test_archives):\n            kept_because = {}\n            keep = prune_within(ta, interval(within), kept_because)\n            assert set(keep) == subset(test_archives, indices)\n            assert all(\"within\" == kept_because[a.id][0] for a in keep)\n\n    # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours\n    test_offsets = [60, 90 * 60, 150 * 60, 210 * 60, 25 * 60 * 60, 49 * 60 * 60]\n    now = datetime.now(timezone.utc)\n    test_dates = [now - timedelta(seconds=s) for s in test_offsets]\n    test_archives = [MockArchive(date, i) for i, date in enumerate(test_dates)]\n\n    dotest(test_archives, \"15S\", [])\n    dotest(test_archives, \"2M\", [0])\n    dotest(test_archives, \"1H\", [0])\n    dotest(test_archives, \"2H\", [0, 1])\n    dotest(test_archives, \"3H\", [0, 1, 2])\n    dotest(test_archives, \"24H\", [0, 1, 2, 3])\n    dotest(test_archives, \"26H\", [0, 1, 2, 3, 4])\n    dotest(test_archives, \"2d\", [0, 1, 2, 3, 4])\n    dotest(test_archives, \"50H\", [0, 1, 2, 3, 4, 5])\n    dotest(test_archives, \"3d\", [0, 1, 2, 3, 4, 5])\n    dotest(test_archives, \"1w\", [0, 1, 2, 3, 4, 5])\n    dotest(test_archives, \"1m\", [0, 1, 2, 3, 4, 5])\n    dotest(test_archives, \"1y\", [0, 1, 2, 3, 4, 5])\n\n\n@pytest.mark.parametrize(\n    \"rule,num_to_keep,expected_ids\",\n    [\n        (\"yearly\", 3, (13, 2, 1)),\n        (\"monthly\", 3, (13, 8, 4)),\n        (\"weekly\", 2, (13, 8)),\n        (\"daily\", 3, (13, 8, 7)),\n        (\"hourly\", 3, (13, 10, 8)),\n        (\"minutely\", 3, (13, 10, 9)),\n        (\"secondly\", 4, (13, 12, 11, 10)),\n        (\"daily\", 0, []),\n    ],\n)\ndef test_prune_split(rule, num_to_keep, expected_ids):\n    def subset(lst, ids):\n        return {i for i in lst if i.id in ids}\n\n    archives = [\n        # years apart\n        MockArchive(datetime(2015, 1, 1, 10, 0, 0, tzinfo=local_tz), 1),\n        MockArchive(datetime(2016, 1, 1, 10, 0, 0, tzinfo=local_tz), 2),\n        MockArchive(datetime(2017, 1, 1, 10, 0, 0, tzinfo=local_tz), 3),\n        # months apart\n        MockArchive(datetime(2017, 2, 1, 10, 0, 0, tzinfo=local_tz), 4),\n        MockArchive(datetime(2017, 3, 1, 10, 0, 0, tzinfo=local_tz), 5),\n        # days apart\n        MockArchive(datetime(2017, 3, 2, 10, 0, 0, tzinfo=local_tz), 6),\n        MockArchive(datetime(2017, 3, 3, 10, 0, 0, tzinfo=local_tz), 7),\n        MockArchive(datetime(2017, 3, 4, 10, 0, 0, tzinfo=local_tz), 8),\n        # minutes apart\n        MockArchive(datetime(2017, 10, 1, 9, 45, 0, tzinfo=local_tz), 9),\n        MockArchive(datetime(2017, 10, 1, 9, 55, 0, tzinfo=local_tz), 10),\n        # seconds apart\n        MockArchive(datetime(2017, 10, 1, 10, 0, 1, tzinfo=local_tz), 11),\n        MockArchive(datetime(2017, 10, 1, 10, 0, 3, tzinfo=local_tz), 12),\n        MockArchive(datetime(2017, 10, 1, 10, 0, 5, tzinfo=local_tz), 13),\n    ]\n    kept_because = {}\n    keep = prune_split(archives, rule, num_to_keep, kept_because)\n\n    assert set(keep) == subset(archives, expected_ids)\n    for item in keep:\n        assert kept_because[item.id][0] == rule\n\n\ndef test_prune_split_keep_oldest():\n    def subset(lst, ids):\n        return {i for i in lst if i.id in ids}\n\n    archives = [\n        # oldest backup, but not last in its year\n        MockArchive(datetime(2018, 1, 1, 10, 0, 0, tzinfo=local_tz), 1),\n        # an interim backup\n        MockArchive(datetime(2018, 12, 30, 10, 0, 0, tzinfo=local_tz), 2),\n        # year-end backups\n        MockArchive(datetime(2018, 12, 31, 10, 0, 0, tzinfo=local_tz), 3),\n        MockArchive(datetime(2019, 12, 31, 10, 0, 0, tzinfo=local_tz), 4),\n    ]\n\n    # Keep oldest when retention target can't otherwise be met\n    kept_because = {}\n    keep = prune_split(archives, \"yearly\", 3, kept_because)\n\n    assert set(keep) == subset(archives, [1, 3, 4])\n    assert kept_because[1][0] == \"yearly[oldest]\"\n    assert kept_because[3][0] == \"yearly\"\n    assert kept_because[4][0] == \"yearly\"\n\n    # Otherwise, prune it\n    kept_because = {}\n    keep = prune_split(archives, \"yearly\", 2, kept_because)\n\n    assert set(keep) == subset(archives, [3, 4])\n    assert kept_because[3][0] == \"yearly\"\n    assert kept_because[4][0] == \"yearly\"\n\n\ndef test_prune_split_no_archives():\n    archives = []\n\n    kept_because = {}\n    keep = prune_split(archives, \"yearly\", 3, kept_because)\n\n    assert keep == []\n    assert kept_because == {}\n\n\ndef test_prune_list_with_metadata_format(archivers, request, backup_files):\n    # Regression test for: prune --list with a format string that requires loading\n    # archive metadata (e.g. {hostname}) must not fail when archives are deleted.\n    # The bug was that format_item() was called after archive.delete(), causing\n    # Archive.DoesNotExist when the formatter tried to lazy-load the archive.\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", backup_files)\n    cmd(archiver, \"create\", \"test2\", backup_files)\n    # {hostname} is a \"call key\" that triggers lazy loading of the archive from the repo.\n    # With the buggy code this would raise Archive.DoesNotExist for the pruned archive.\n    output = cmd(archiver, \"prune\", \"--list\", \"--keep-daily=1\", \"--format={name} {hostname}{NL}\")\n    assert \"test1\" in output\n    assert \"test2\" in output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/recreate_cmd_test.py",
    "content": "import os\nimport re\nimport time\nfrom datetime import datetime\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom .. import changedir, are_hardlinks_supported\nfrom . import (\n    _create_test_caches,\n    _create_test_tagged,\n    _create_test_keep_tagged,\n    _assert_test_caches,\n    _assert_test_tagged,\n    _assert_test_keep_tagged,\n    _extract_hardlinks_setup,\n    generate_archiver_tests,\n    cmd,\n    create_regular_file,\n    create_test_files,\n    RK_ENCRYPTION,\n)\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_recreate_exclude_caches(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_caches(archiver)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"recreate\", \"-a\", \"test\", \"--exclude-caches\")\n    _assert_test_caches(archiver)\n\n\ndef test_recreate_exclude_tagged(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_tagged(archiver)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"recreate\", \"-a\", \"test\", \"--exclude-if-present\", \".NOBACKUP\", \"--exclude-if-present\", \"00-NOBACKUP\")\n    _assert_test_tagged(archiver)\n\n\ndef test_recreate_exclude_keep_tagged(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _create_test_keep_tagged(archiver)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(\n        archiver,\n        \"recreate\",\n        \"-a\",\n        \"test\",\n        \"--exclude-if-present\",\n        \".NOBACKUP1\",\n        \"--exclude-if-present\",\n        \".NOBACKUP2\",\n        \"--exclude-caches\",\n        \"--keep-exclude-tags\",\n    )\n    _assert_test_keep_tagged(archiver)\n\n\n@pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hard links not supported\")\ndef test_recreate_hardlinked_tags(archivers, request):  # test for issue #4911\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    create_regular_file(\n        archiver.input_path, \"file1\", contents=CACHE_TAG_CONTENTS\n    )  # \"wrong\" filename, but correct tag contents\n    os.mkdir(os.path.join(archiver.input_path, \"subdir\"))  # to make sure the tag is encountered *after* file1\n    os.link(\n        os.path.join(archiver.input_path, \"file1\"), os.path.join(archiver.input_path, \"subdir\", CACHE_TAG_NAME)\n    )  # correct tag name, hard link to file1\n    cmd(archiver, \"create\", \"test\", \"input\")\n    # in the \"test\" archive, we now have, in this order:\n    # - a regular file item for \"file1\"\n    # - a hard link item for \"CACHEDIR.TAG\" referring back to file1 for its contents\n    cmd(archiver, \"recreate\", \"test\", \"--exclude-caches\", \"--keep-exclude-tags\")\n    # if issue #4911 is present, the recreate will crash with a KeyError for \"input/file1\"\n\n\ndef test_recreate_target(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"check\")\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    cmd(archiver, \"check\")\n    original_archive = cmd(archiver, \"repo-list\")\n    cmd(archiver, \"recreate\", \"test0\", \"input/dir2\", \"-e\", \"input/dir2/file3\", \"--target=new-archive\")\n    cmd(archiver, \"check\")\n\n    archives = cmd(archiver, \"repo-list\")\n    assert original_archive in archives\n    assert \"new-archive\" in archives\n\n    listing = cmd(archiver, \"list\", \"new-archive\", \"--short\")\n    assert \"file1\" not in listing\n    assert \"dir2/file2\" in listing\n    assert \"dir2/file3\" not in listing\n\n\ndef test_recreate_basic(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    create_regular_file(archiver.input_path, \"dir2/file3\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    cmd(archiver, \"recreate\", \"test0\", \"input/dir2\", \"-e\", \"input/dir2/file3\")\n    cmd(archiver, \"check\")\n    listing = cmd(archiver, \"list\", \"test0\", \"--short\")\n    assert \"file1\" not in listing\n    assert \"dir2/file2\" in listing\n    assert \"dir2/file3\" not in listing\n\n\n@pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hard links not supported\")\ndef test_recreate_subtree_hardlinks(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    # This is essentially the same problem set as in test_extract_hardlinks\n    _extract_hardlinks_setup(archiver)\n    cmd(archiver, \"create\", \"test2\", \"input\")\n    cmd(archiver, \"recreate\", \"-a\", \"test\", \"input/dir1\")\n    cmd(archiver, \"check\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test\")\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/aaaa\").st_nlink == 2\n        assert os.stat(\"input/dir1/source2\").st_nlink == 2\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"test2\")\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 4\n\n\ndef test_recreate_rechunkify(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    with open(os.path.join(archiver.input_path, \"large_file\"), \"wb\") as fd:\n        fd.write(b\"a\" * 280)\n        fd.write(b\"b\" * 280)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", \"input\", \"--chunker-params\", \"7,9,8,127\")\n    cmd(archiver, \"create\", \"test2\", \"input\", \"--files-cache=disabled\")\n    num_chunks1 = int(cmd(archiver, \"list\", \"test1\", \"input/large_file\", \"--format\", \"{num_chunks}\"))\n    num_chunks2 = int(cmd(archiver, \"list\", \"test2\", \"input/large_file\", \"--format\", \"{num_chunks}\"))\n    # right now, the file is chunked differently\n    assert num_chunks1 != num_chunks2\n    cmd(archiver, \"recreate\", \"--chunker-params\", \"default\")\n    cmd(archiver, \"check\")\n    num_chunks1 = int(cmd(archiver, \"list\", \"test1\", \"input/large_file\", \"--format\", \"{num_chunks}\"))\n    num_chunks2 = int(cmd(archiver, \"list\", \"test2\", \"input/large_file\", \"--format\", \"{num_chunks}\"))\n    # now the files are chunked in the same way\n    # TODO: this is a rather weak test, it could be improved by comparing the IDs in the chunk lists,\n    # to make sure that everything is completely deduplicated now (both files have identical chunks).\n    assert num_chunks1 == num_chunks2\n\n\ndef test_recreate_fixed_rechunkify(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    with open(os.path.join(archiver.input_path, \"file\"), \"wb\") as fd:\n        fd.write(b\"a\" * 8192)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\", \"--chunker-params\", \"7,9,8,127\")\n    output = cmd(archiver, \"list\", \"test\", \"input/file\", \"--format\", \"{num_chunks}\")\n    num_chunks = int(output)\n    assert num_chunks > 2\n    cmd(archiver, \"recreate\", \"--chunker-params\", \"fixed,4096\")\n    output = cmd(archiver, \"list\", \"test\", \"input/file\", \"--format\", \"{num_chunks}\")\n    num_chunks = int(output)\n    assert num_chunks == 2\n\n\ndef test_recreate_no_rechunkify(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    with open(os.path.join(archiver.input_path, \"file\"), \"wb\") as fd:\n        fd.write(b\"a\" * 8192)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    # first create an archive with non-default chunker params:\n    cmd(archiver, \"create\", \"test\", \"input\", \"--chunker-params\", \"7,9,8,127\")\n    output = cmd(archiver, \"list\", \"test\", \"input/file\", \"--format\", \"{num_chunks}\")\n    num_chunks = int(output)\n    # now recreate the archive and do NOT specify chunker params:\n    output = cmd(archiver, \"recreate\", \"--debug\", \"--exclude\", \"filename_never_matches\", \"-a\", \"test\")\n    assert \"Rechunking\" not in output  # we did not give --chunker-params, so it must not rechunk!\n    output = cmd(archiver, \"list\", \"test\", \"input/file\", \"--format\", \"{num_chunks}\")\n    num_chunks_after_recreate = int(output)\n    assert num_chunks == num_chunks_after_recreate\n\n\ndef test_recreate_keep_original_timestamp(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    info_orig = cmd(archiver, \"info\", \"-a\", \"test0\").splitlines()\n    # this shall recreate the archive and keep the nominal timestamp\n    time.sleep(1)\n    cmd(archiver, \"recreate\", \"test0\", \"--comment\", \"test\")\n    info_recreated = cmd(archiver, \"info\", \"-a\", \"test0\").splitlines()\n    nominal_orig = next(item for item in info_orig if item.startswith(\"Time (nominal):\"))\n    nominal_recreated = next(item for item in info_recreated if item.startswith(\"Time (nominal):\"))\n    assert nominal_orig == nominal_recreated\n\n\ndef test_recreate_with_given_timestamp(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test0\", \"input\")\n    # this shall recreate the archive with a different nominal timestamp\n    cmd(archiver, \"recreate\", \"test0\", \"--timestamp\", \"1970-01-02T00:00:00\", \"--comment\", \"test\")\n    info = cmd(archiver, \"info\", \"-a\", \"test0\").splitlines()\n    dtime = datetime(1970, 1, 2, 0, 0, 0).astimezone()  # local time in local timezone\n    s_time = dtime.strftime(\"%Y-%m-%d %H:%M:.. %z\").replace(\"+\", r\"\\+\")\n    assert any([re.search(r\"Time \\(nominal\\).+ %s\" % s_time, item) for item in info])\n    # start/end time are just from the recreate operation\n    dtime = datetime.now().astimezone()  # current local time\n    s_time = dtime.strftime(\"%Y-%m-%d %H:..:.. %z\").replace(\"+\", r\"\\+\")\n    assert any([re.search(r\"Time \\(end\\).+ %s\" % s_time, item) for item in info])\n    assert any([re.search(r\"Time \\(start\\).+ %s\" % s_time, item) for item in info])\n\n\ndef test_recreate_dry_run(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"compressible\", size=10000)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    archives_before = cmd(archiver, \"list\", \"test\")\n    cmd(archiver, \"recreate\", \"-n\", \"-e\", \"input/compressible\")\n    cmd(archiver, \"check\")\n    archives_after = cmd(archiver, \"list\", \"test\")\n    assert archives_after == archives_before\n\n\ndef test_recreate_skips_nothing_to_do(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    info_before = cmd(archiver, \"info\", \"-a\", \"test\")\n    cmd(archiver, \"recreate\", \"--chunker-params\", \"default\")\n    cmd(archiver, \"check\")\n    info_after = cmd(archiver, \"info\", \"-a\", \"test\")\n    assert info_before == info_after  # includes archive ID\n\n\ndef test_recreate_list_output(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=0)\n    create_regular_file(archiver.input_path, \"file2\", size=0)\n    create_regular_file(archiver.input_path, \"file3\", size=0)\n    create_regular_file(archiver.input_path, \"file4\", size=0)\n    create_regular_file(archiver.input_path, \"file5\", size=0)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    output = cmd(archiver, \"recreate\", \"-a\", \"test\", \"--list\", \"--info\", \"-e\", \"input/file2\")\n    cmd(archiver, \"check\")\n    assert \"input/file1\" in output\n    assert \"- input/file2\" in output\n\n    output = cmd(archiver, \"recreate\", \"-a\", \"test\", \"--list\", \"-e\", \"input/file3\")\n    cmd(archiver, \"check\")\n    assert \"input/file1\" in output\n    assert \"- input/file3\" in output\n\n    output = cmd(archiver, \"recreate\", \"-a\", \"test\", \"-e\", \"input/file4\")\n    cmd(archiver, \"check\")\n    assert \"input/file1\" not in output\n    assert \"- input/file4\" not in output\n\n    output = cmd(archiver, \"recreate\", \"-a\", \"test\", \"--info\", \"-e\", \"input/file5\")\n    cmd(archiver, \"check\")\n    assert \"input/file1\" not in output\n    assert \"- input/file5\" not in output\n\n\ndef test_comment(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test1\", \"input\")\n    cmd(archiver, \"create\", \"test2\", \"input\", \"--comment\", \"this is the comment\")\n    cmd(archiver, \"create\", \"test3\", \"input\", \"--comment\", '\"deleted\" comment')\n    cmd(archiver, \"create\", \"test4\", \"input\", \"--comment\", \"preserved comment\")\n    assert \"Comment: \" + os.linesep in cmd(archiver, \"info\", \"-a\", \"test1\")\n    assert \"Comment: this is the comment\" in cmd(archiver, \"info\", \"-a\", \"test2\")\n\n    cmd(archiver, \"recreate\", \"-a\", \"test1\", \"--comment\", \"added comment\")\n    cmd(archiver, \"recreate\", \"-a\", \"test2\", \"--comment\", \"modified comment\")\n    cmd(archiver, \"recreate\", \"-a\", \"test3\", \"--comment\", \"\")\n    cmd(archiver, \"recreate\", \"-a\", \"test4\", \"12345\")\n    assert \"Comment: added comment\" in cmd(archiver, \"info\", \"-a\", \"test1\")\n    assert \"Comment: modified comment\" in cmd(archiver, \"info\", \"-a\", \"test2\")\n    assert \"Comment: \" + os.linesep in cmd(archiver, \"info\", \"-a\", \"test3\")\n    assert \"Comment: preserved comment\" in cmd(archiver, \"info\", \"-a\", \"test4\")\n\n\ndef test_recreate_ignore_protected(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    create_regular_file(archiver.input_path, \"file1\", size=1024)\n    create_regular_file(archiver.input_path, \"file2\", size=1024)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive\", \"input\")\n    cmd(archiver, \"tag\", \"--add=@PROT\", \"archive\")\n    cmd(archiver, \"recreate\", \"archive\", \"-e\", \"input\")  # this would normally remove all from archive\n    listing = cmd(archiver, \"list\", \"archive\", \"--short\")\n    # archive was protected, so recreate ignored it:\n    assert \"file1\" in listing\n    assert \"file2\" in listing\n"
  },
  {
    "path": "src/borg/testsuite/archiver/remote_repo_test.py",
    "content": "import json\nimport os\nimport shutil\nimport subprocess\n\nimport pytest\n\nfrom .. import changedir\nfrom . import cmd, create_regular_file, RK_ENCRYPTION, assert_dirs_equal\n\n\nSFTP_URL = os.environ.get(\"BORG_TEST_SFTP_REPO\")\nS3_URL = os.environ.get(\"BORG_TEST_S3_REPO\")\n\n\ndef have_rclone():\n    rclone_path = shutil.which(\"rclone\")\n    if not rclone_path:\n        return False  # not installed\n    try:\n        # rclone returns JSON for core/version, e.g. {\"decomposed\": [1,59,2], \"version\": \"v1.59.2\"}\n        out = subprocess.check_output([rclone_path, \"rc\", \"--loopback\", \"core/version\"])\n        info = json.loads(out.decode(\"utf-8\"))\n    except Exception:\n        return False\n    try:\n        if info.get(\"decomposed\", []) < [1, 57, 0]:\n            return False  # too old\n    except Exception:\n        return False\n    return True  # looks good\n\n\n@pytest.mark.skipif(not have_rclone(), reason=\"rclone must be installed for this test.\")\ndef test_rclone_repo_basics(archiver, tmp_path):\n    create_regular_file(archiver.input_path, \"file1\", size=100 * 1024)\n    create_regular_file(archiver.input_path, \"file2\", size=10 * 1024)\n    rclone_repo_dir = tmp_path / \"rclone-repo\"\n    os.makedirs(rclone_repo_dir, exist_ok=True)\n    archiver.repository_location = f\"rclone:{os.fspath(rclone_repo_dir)}\"\n    archive_name = \"test-archive\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", archive_name, \"input\")\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name in list_output\n    archive_list_output = cmd(archiver, \"list\", archive_name)\n    assert \"input/file1\" in archive_list_output\n    assert \"input/file2\" in archive_list_output\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", archive_name)\n    assert_dirs_equal(\n        archiver.input_path, os.path.join(archiver.output_path, \"input\"), ignore_flags=True, ignore_xattrs=True\n    )\n    cmd(archiver, \"delete\", \"-a\", archive_name)\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name not in list_output\n    cmd(archiver, \"repo-delete\")\n\n\n@pytest.mark.skipif(not SFTP_URL, reason=\"BORG_TEST_SFTP_REPO not set.\")\ndef test_sftp_repo_basics(archiver):\n    create_regular_file(archiver.input_path, \"file1\", size=100 * 1024)\n    create_regular_file(archiver.input_path, \"file2\", size=10 * 1024)\n    archiver.repository_location = SFTP_URL\n    archive_name = \"test-archive\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", archive_name, \"input\")\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name in list_output\n    archive_list_output = cmd(archiver, \"list\", archive_name)\n    assert \"input/file1\" in archive_list_output\n    assert \"input/file2\" in archive_list_output\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", archive_name)\n    assert_dirs_equal(\n        archiver.input_path, os.path.join(archiver.output_path, \"input\"), ignore_flags=True, ignore_xattrs=True\n    )\n    cmd(archiver, \"delete\", \"-a\", archive_name)\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name not in list_output\n    cmd(archiver, \"repo-delete\")\n\n\n@pytest.mark.skipif(not S3_URL, reason=\"BORG_TEST_S3_REPO not set.\")\ndef test_s3_repo_basics(archiver):\n    create_regular_file(archiver.input_path, \"file1\", size=100 * 1024)\n    create_regular_file(archiver.input_path, \"file2\", size=10 * 1024)\n    archiver.repository_location = S3_URL\n    archive_name = \"test-archive\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", archive_name, \"input\")\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name in list_output\n    archive_list_output = cmd(archiver, \"list\", archive_name)\n    assert \"input/file1\" in archive_list_output\n    assert \"input/file2\" in archive_list_output\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", archive_name)\n    assert_dirs_equal(\n        archiver.input_path, os.path.join(archiver.output_path, \"input\"), ignore_flags=True, ignore_xattrs=True\n    )\n    cmd(archiver, \"delete\", \"-a\", archive_name)\n    list_output = cmd(archiver, \"repo-list\")\n    assert archive_name not in list_output\n    cmd(archiver, \"repo-delete\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/rename_cmd_test.py",
    "content": "from ...constants import *  # NOQA\nfrom ...manifest import Manifest\nfrom ...repository import Repository\nfrom . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_rename(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir2/file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"create\", \"test.2\", \"input\")\n    cmd(archiver, \"extract\", \"test\", \"--dry-run\")\n    cmd(archiver, \"extract\", \"test.2\", \"--dry-run\")\n    cmd(archiver, \"rename\", \"test\", \"test.3\")\n    cmd(archiver, \"extract\", \"test.2\", \"--dry-run\")\n    cmd(archiver, \"rename\", \"test.2\", \"test.4\")\n    cmd(archiver, \"extract\", \"test.3\", \"--dry-run\")\n    cmd(archiver, \"extract\", \"test.4\", \"--dry-run\")\n    # Make sure both archives have been renamed\n    with Repository(archiver.repository_path) as repository:\n        manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n        assert manifest.archives.count() == 2\n        assert manifest.archives.exists(\"test.3\")\n        assert manifest.archives.exists(\"test.4\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_compress_cmd_test.py",
    "content": "import os\n\nfrom ...constants import *  # NOQA\nfrom ...repository import Repository, repo_lister\nfrom ...manifest import Manifest\nfrom ...compress import ZSTD, ZLIB, LZ4, CNONE\nfrom ...helpers import bin_to_hex\n\nfrom . import create_regular_file, cmd, RK_ENCRYPTION\n\n\ndef test_repo_compress(archiver):\n    def check_compression(ctype, clevel, olevel):\n        \"\"\"Check that all chunks in the repo are compressed/obfuscated as expected.\"\"\"\n        repository = Repository(archiver.repository_path, exclusive=True)\n        with repository:\n            manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)\n            for id, _ in repo_lister(repository, limit=LIST_SCAN_LIMIT):\n                chunk = repository.get(id, read_data=True)\n                meta, data = manifest.repo_objs.parse(\n                    id, chunk, ro_type=ROBJ_DONTCARE\n                )  # will also decompress according to metadata\n                m_olevel = meta.get(\"olevel\", -1)\n                m_psize = meta.get(\"psize\", -1)\n                print(bin_to_hex(id), meta[\"ctype\"], meta[\"clevel\"], meta[\"csize\"], meta[\"size\"], m_olevel, m_psize)\n                # this is not as easy as one thinks due to the DecidingCompressor choosing the smallest of\n                # (desired compressed, lz4 compressed, not compressed).\n                assert meta[\"ctype\"] in (ctype, LZ4.ID, CNONE.ID)\n                assert meta[\"clevel\"] in (clevel, 255)  # LZ4 and CNONE have level 255\n                if olevel != -1:  # we expect obfuscation\n                    assert \"psize\" in meta\n                    assert m_olevel == olevel\n                else:\n                    assert \"psize\" not in meta\n                    assert \"olevel\" not in meta\n\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 10)\n    create_regular_file(archiver.input_path, \"file2\", contents=os.urandom(1024 * 10))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    cname, ctype, clevel, olevel = ZLIB.name, ZLIB.ID, 3, -1\n    cmd(archiver, \"create\", \"test\", \"input\", \"-C\", f\"{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZSTD.name, ZSTD.ID, 1, -1  # change compressor (and level)\n    cmd(archiver, \"repo-compress\", \"-C\", f\"{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZSTD.name, ZSTD.ID, 3, -1  # only change level\n    cmd(archiver, \"repo-compress\", \"-C\", f\"{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZSTD.name, ZSTD.ID, 3, 110  # only change to obfuscated\n    cmd(archiver, \"repo-compress\", \"-C\", f\"obfuscate,{olevel},{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZSTD.name, ZSTD.ID, 3, 112  # only change obfuscation level\n    cmd(archiver, \"repo-compress\", \"-C\", f\"obfuscate,{olevel},{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZSTD.name, ZSTD.ID, 3, -1  # change to not obfuscated\n    cmd(archiver, \"repo-compress\", \"-C\", f\"{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZLIB.name, ZLIB.ID, 1, -1\n    cmd(archiver, \"repo-compress\", \"-C\", f\"auto,{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n    cname, ctype, clevel, olevel = ZLIB.name, ZLIB.ID, 2, 111\n    cmd(archiver, \"repo-compress\", \"-C\", f\"obfuscate,{olevel},auto,{cname},{clevel}\")\n    check_compression(ctype, clevel, olevel)\n\n\ndef test_repo_compress_stats(archiver):\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 10)\n    create_regular_file(archiver.input_path, \"file2\", contents=os.urandom(1024 * 10))\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    cname, clevel = ZLIB.name, 3\n    cmd(archiver, \"create\", \"test\", \"input\", \"-C\", f\"{cname},{clevel}\")\n\n    cname, clevel = ZSTD.name, 1  # change compressor (and level)\n    output = cmd(archiver, \"repo-compress\", \"-C\", f\"{cname},{clevel}\", \"--stats\")\n    assert \"Recompression stats:\" in output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_create_cmd_test.py",
    "content": "import os\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom ...helpers.errors import Error, CancelledByUser\nfrom ...constants import *  # NOQA\nfrom ...crypto.key import FlexiKey\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION, KF_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_repo_create_interrupt(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:\n        pytest.skip(\"patches object\")\n\n    def raise_eof(*args, **kwargs):\n        raise EOFError\n\n    with patch.object(FlexiKey, \"create\", raise_eof):\n        if archiver.FORK_DEFAULT:\n            cmd(archiver, \"repo-create\", RK_ENCRYPTION, exit_code=2)\n        else:\n            with pytest.raises(CancelledByUser):\n                cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    assert not os.path.exists(archiver.repository_location)\n\n\ndef test_repo_create_requires_encryption_option(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", exit_code=2)\n\n\ndef test_repo_create_refuse_to_overwrite_keyfile(archivers, request, monkeypatch):\n    #  BORG_KEY_FILE=something borg repo-create should quit if \"something\" already exists.\n    #  See: https://github.com/borgbackup/borg/pull/6046\n    archiver = request.getfixturevalue(archivers)\n    keyfile = os.path.join(archiver.tmpdir, \"keyfile\")\n    monkeypatch.setenv(\"BORG_KEY_FILE\", keyfile)\n    original_location = archiver.repository_location\n    archiver.repository_location = original_location + \"0\"\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    with open(keyfile) as file:\n        before = file.read()\n    archiver.repository_location = original_location + \"1\"\n    arg = (\"repo-create\", KF_ENCRYPTION)\n    if archiver.FORK_DEFAULT:\n        cmd(archiver, *arg, exit_code=2)\n    else:\n        with pytest.raises(Error):\n            cmd(archiver, *arg)\n    with open(keyfile) as file:\n        after = file.read()\n    assert before == after\n\n\ndef test_repo_create_keyfile_same_path_creates_new_keys(archivers, request):\n    \"\"\"Regression test for GH issue #6230.\n\n    When creating a new keyfile-encrypted repository at the same filesystem path\n    multiple times (e.g., after moving/unmounting the previous one), Borg must not\n    overwrite or reuse the existing key file. Instead, it should create a new key\n    file in the keys directory, appending a numeric suffix like .2, .3, ...\n    \"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # First creation at path A\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    keys = sorted(os.listdir(archiver.keys_path))\n    assert len(keys) == 1\n    base_key = keys[0]\n    base_path = os.path.join(archiver.keys_path, base_key)\n    with open(base_path, \"rb\") as f:\n        base_contents = f.read()\n\n    # Simulate moving/unmounting the repo by removing the path to allow re-create at the same path\n    import shutil\n\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    keys = sorted(os.listdir(archiver.keys_path))\n    assert len(keys) == 2\n    assert base_key in keys\n    # The new file should be base_key suffixed with .2\n    assert any(k == base_key + \".2\" for k in keys)\n    second_path = os.path.join(archiver.keys_path, base_key + \".2\")\n    with open(second_path, \"rb\") as f:\n        second_contents = f.read()\n    assert second_contents != base_contents\n\n    # Remove repo again and create a third time at same path\n    shutil.rmtree(archiver.repository_path)\n    cmd(archiver, \"repo-create\", KF_ENCRYPTION)\n    keys = sorted(os.listdir(archiver.keys_path))\n    assert len(keys) == 3\n    assert any(k == base_key + \".3\" for k in keys)\n    third_path = os.path.join(archiver.keys_path, base_key + \".3\")\n    with open(third_path, \"rb\") as f:\n        third_contents = f.read()\n    # Ensure all keys are distinct\n    assert third_contents != base_contents\n    assert third_contents != second_contents\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_delete_cmd_test.py",
    "content": "import os\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import CancelledByUser\nfrom . import create_regular_file, cmd, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_delete_repo(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    create_regular_file(archiver.input_path, \"dir2/file2\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"create\", \"test.2\", \"input\")\n    os.environ[\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\"] = \"no\"\n    if archiver.FORK_DEFAULT:\n        expected_ec = CancelledByUser().exit_code\n        cmd(archiver, \"repo-delete\", exit_code=expected_ec)\n    else:\n        with pytest.raises(CancelledByUser):\n            cmd(archiver, \"repo-delete\")\n    assert os.path.exists(archiver.repository_path)\n    os.environ[\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\"] = \"YES\"\n    cmd(archiver, \"repo-delete\")\n    # Make sure the repository is gone\n    assert not os.path.exists(archiver.repository_path)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_info_cmd_test.py",
    "content": "import json\n\nfrom ...constants import *  # NOQA\nfrom . import checkts, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_info(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    info_repo = cmd(archiver, \"repo-info\")\n    assert \"Repository ID:\" in info_repo\n\n\ndef test_info_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n\n    info_repo = json.loads(cmd(archiver, \"repo-info\", \"--json\"))\n    repository = info_repo[\"repository\"]\n    assert len(repository[\"id\"]) == 64\n    assert \"last_modified\" in repository\n\n    checkts(repository[\"last_modified\"])\n    assert info_repo[\"encryption\"][\"mode\"] == RK_ENCRYPTION[13:]\n    assert \"keyfile\" not in info_repo[\"encryption\"]\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_list_cmd_test.py",
    "content": "import json\nimport os\n\nfrom ...constants import *  # NOQA\nfrom . import cmd, checkts, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\nfrom .prune_cmd_test import _create_archive_ts\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_repo_list_glob(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test-1\", backup_files)\n    cmd(archiver, \"create\", \"something-else-than-test-1\", backup_files)\n    cmd(archiver, \"create\", \"test-2\", backup_files)\n    output = cmd(archiver, \"repo-list\", \"--match-archives=sh:test-*\")\n    assert \"test-1\" in output\n    assert \"test-2\" in output\n    assert \"something-else\" not in output\n\n\ndef test_archives_format(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"--comment\", \"comment 1\", \"test-1\", backup_files)\n    cmd(archiver, \"create\", \"--comment\", \"comment 2\", \"test-2\", backup_files)\n    output_1 = cmd(archiver, \"repo-list\")\n    output_2 = cmd(\n        archiver,\n        \"repo-list\",\n        \"--format\",\n        \"{id:.8}  {time}  {archive:<15}  {tags:<10}  {username:<10}  {hostname:<10}  {comment:.40}{NL}\",\n    )\n    assert output_1 == output_2\n    output = cmd(archiver, \"repo-list\", \"--short\")\n    assert len(output) == 2 * 64 + 2 * len(os.linesep)\n    output = cmd(archiver, \"repo-list\", \"--format\", \"{name} {comment}{NL}\")\n    assert \"test-1 comment 1\" + os.linesep in output\n    assert \"test-2 comment 2\" + os.linesep in output\n\n\ndef test_size_nfiles(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    create_regular_file(archiver.input_path, \"file1\", size=123000)\n    create_regular_file(archiver.input_path, \"file2\", size=456)\n    cmd(archiver, \"create\", \"test\", \"input/file1\", \"input/file2\")\n    output = cmd(archiver, \"list\", \"test\")\n    print(output)\n    output = cmd(archiver, \"repo-list\", \"--format\", \"{name} {nfiles} {size}\")\n    o_t = output.split()\n    assert o_t[0] == \"test\"\n    assert int(o_t[1]) == 2\n    assert 123456 <= int(o_t[2]) < 123999  # There is some metadata overhead\n\n\ndef test_date_matching(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    _create_archive_ts(archiver, backup_files, \"archive-2022-11-20\", 2022, 11, 20, 23, 59, 59)\n    _create_archive_ts(archiver, backup_files, \"archive-2022-12-18\", 2022, 12, 18, 23, 59, 59)\n    cmd(archiver, \"create\", \"archive-now\", backup_files)\n\n    cmd(archiver, \"check\", \"-v\", \"--oldest=23e\", exit_code=2)\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--oldest=1y\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newest=1y\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--oldest=1m\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newest=1m\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--oldest=4w\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newest=4w\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newer=1d\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--older=1d\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newer=24H\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--older=24H\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newer=1440M\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--older=1440M\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--newer=86400S\", exit_code=0)\n    assert \"archive-2022-11-20\" not in output\n    assert \"archive-2022-12-18\" not in output\n    assert \"archive-now\" in output\n\n    output = cmd(archiver, \"repo-list\", \"-v\", \"--older=86400S\", exit_code=0)\n    assert \"archive-2022-11-20\" in output\n    assert \"archive-2022-12-18\" in output\n    assert \"archive-now\" not in output\n\n\ndef test_repo_list_json(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    list_repo = json.loads(cmd(archiver, \"repo-list\", \"--json\"))\n    repository = list_repo[\"repository\"]\n    assert len(repository[\"id\"]) == 64\n    checkts(repository[\"last_modified\"])\n    assert list_repo[\"encryption\"][\"mode\"] == RK_ENCRYPTION[13:]\n    assert \"keyfile\" not in list_repo[\"encryption\"]\n    archive0 = list_repo[\"archives\"][0]\n    checkts(archive0[\"time\"])\n\n\ndef test_repo_list_deleted(archivers, request, backup_files):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"normal1\", backup_files)\n    cmd(archiver, \"create\", \"deleted1\", backup_files)\n    cmd(archiver, \"create\", \"normal2\", backup_files)\n    cmd(archiver, \"create\", \"deleted2\", backup_files)\n    cmd(archiver, \"delete\", \"-a\", \"sh:deleted*\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"normal1\" in output\n    assert \"normal2\" in output\n    assert \"deleted1\" not in output\n    assert \"deleted2\" not in output\n    output = cmd(archiver, \"repo-list\", \"--deleted\")\n    assert \"normal1\" not in output\n    assert \"normal2\" not in output\n    assert \"deleted1\" in output\n    assert \"deleted2\" in output\n"
  },
  {
    "path": "src/borg/testsuite/archiver/repo_space_cmd_test.py",
    "content": "from ...constants import *  # NOQA\n\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote\")  # NOQA\n\n\ndef test_repo_space_basics(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Initially, no space should be reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 0 B reserved space in this repository.\" in output\n\n    # Test reserving some space.\n    output = cmd(archiver, \"repo-space\", \"--reserve\", \"100M\")\n    # The actual size will be rounded up to a multiple of 64 MiB blocks.\n    # For 100 MB, it should be 128 MiB (2 blocks) == 134.22 MB.\n    assert \"There is 134.22 MB reserved space in this repository now.\" in output\n\n    # Check that space is reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 134.22 MB reserved space in this repository.\" in output\n\n    # Test freeing the space.\n    output = cmd(archiver, \"repo-space\", \"--free\")\n    assert \"Freed 134.22 MB in the repository.\" in output\n\n    # Check that no space is reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 0 B reserved space in this repository.\" in output\n\n\ndef test_repo_space_modify_reservation(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Reserve some space.\n    output = cmd(archiver, \"repo-space\", \"--reserve\", \"50M\")\n    assert \"There is 67.11 MB reserved space in this repository now.\" in output\n\n    # Check that space is reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 67.11 MB reserved space in this repository.\" in output\n\n    # Reserve more space.\n    output = cmd(archiver, \"repo-space\", \"--reserve\", \"100M\")\n    assert \"There is 134.22 MB reserved space in this repository now.\" in output\n\n    # Check that space is reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 134.22 MB reserved space in this repository.\" in output\n\n    # Note: --reserve can only INCREASE the amount of reserved space.\n\n    cmd(archiver, \"repo-space\", \"--free\")  # save space in TMPDIR\n\n\ndef test_repo_space_edge_cases(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Test reserving 0 space (should not create any reservation).\n    output = cmd(archiver, \"repo-space\", \"--reserve\", \"0\")\n\n    # Check that no space is reserved.\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 0 B reserved space in this repository.\" in output\n\n    # Test freeing when no space is reserved.\n    output = cmd(archiver, \"repo-space\", \"--free\")\n    assert \"Freed 0 B in the repository.\" in output\n\n    # Test reserving a very small amount of space (1KB).\n    # This should round up to at least one 64MiB block.\n    output = cmd(archiver, \"repo-space\", \"--reserve\", \"1K\")\n    assert \"There is 67.11 MB reserved space in this repository now.\" in output\n\n    # Check that space is reserved (should be 64MiB).\n    output = cmd(archiver, \"repo-space\")\n    assert \"There is 67.11 MB reserved space in this repository.\" in output\n\n    cmd(archiver, \"repo-space\", \"--free\")  # save space on TMPDIR\n"
  },
  {
    "path": "src/borg/testsuite/archiver/restricted_permissions_test.py",
    "content": "import os\nimport pytest\n\nfrom borgstore.backends.errors import PermissionDenied\n\nfrom ...constants import *  # NOQA\nfrom .. import changedir\nfrom . import cmd, create_test_files, RK_ENCRYPTION, generate_archiver_tests\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local\")  # NOQA\n\n\ndef test_repository_permissions_all(archivers, request, monkeypatch):\n    \"\"\"Test repository with 'all' permissions setting.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create a repository with unrestricted permissions.\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"all\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"create\", \"archive1\", \"input\")\n\n    # Verify the archive was created.\n    assert \"archive1\" in cmd(archiver, \"repo-list\")\n\n    # Delete the archive to verify unrestricted permissions.\n    cmd(archiver, \"delete\", \"archive1\")\n\n    # Verify the archive was deleted.\n    assert \"archive1\" not in cmd(archiver, \"repo-list\")\n\n    # Delete the repository to verify unrestricted permissions.\n    cmd(archiver, \"repo-delete\")\n\n\ndef test_repository_permissions_no_delete(archivers, request, monkeypatch):\n    \"\"\"Test repository with 'no-delete' permissions setting.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n\n    # Create a repository first (need unrestricted permissions for that).\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"all\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive1\", \"input\")\n    cmd(archiver, \"delete\", \"archive1\")  # this is so that compact has some chunk to remove\n\n    # Switch to no-delete permissions.\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"no-delete\")\n\n    # Creating new archives should work.\n    cmd(archiver, \"create\", \"archive2\", \"input\")\n\n    # Verify the archive was created.\n    assert \"archive2\" in cmd(archiver, \"repo-list\")\n\n    # Try to delete the archive, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"delete\", \"archive2\")\n\n    # Verify the archive still exists.\n    assert \"archive2\" in cmd(archiver, \"repo-list\")\n\n    # Try to rename an archive, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"rename\", \"archive2\", \"archive3\")\n\n    # Verify the archive still exists.\n    assert \"archive2\" in cmd(archiver, \"repo-list\")\n\n    # Try to delete the repo, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"repo-delete\")\n\n    # Verify the archive still exists.\n    assert \"archive2\" in cmd(archiver, \"repo-list\")\n\n    # Try to compact the repo, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"compact\")\n\n    # Check without --repair should work.\n    cmd(archiver, \"check\")\n\n    # Try to check --repair, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"check\", \"--repair\")\n\n    # Try to repo-compress (and change compression from lz4 to zstd), which should fail.\n    # It fails because it needs to overwrite existing chunks, which is also disallowed by no-delete.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"repo-compress\", \"-C\", \"zstd\")\n\n\ndef test_repository_permissions_read_only(archivers, request, monkeypatch):\n    \"\"\"Test repository with 'read-only' permissions setting.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create a repository first (need unrestricted permissions for that).\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"all\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Create an archive to test with.\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"create\", \"archive2\", \"input\")\n\n    # Switch to read-only permissions.\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"read-only\")\n\n    # Verify we can list archives.\n    assert \"archive2\" in cmd(archiver, \"repo-list\")\n\n    # Verify we can list files in an archive.\n    assert \"input/\" in cmd(archiver, \"list\", \"archive2\")\n\n    # Extract the archive.\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"archive2\")\n\n    # Verify extraction worked.\n    extracted_files = os.listdir(\"output\")\n    assert len(extracted_files) > 0\n\n    # Try to create a new archive, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"create\", \"archive3\", \"input\")\n\n    # Try to delete an archive, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"delete\", \"archive2\")\n\n    # Try to delete the repo, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"repo-delete\")\n\n    # Try to compact the repo, which should fail.\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"compact\")\n\n\ndef test_repository_permissions_write_only(archivers, request, monkeypatch):\n    \"\"\"Test repository with 'write-only' permissions setting\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Create a repository first (need unrestricted permissions for that).\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"all\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    # Create an initial archive to test with.\n    create_test_files(archiver.input_path)\n    cmd(archiver, \"create\", \"archive1\", \"input\")\n\n    # Switch to write-only permissions.\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"write-only\")\n\n    # Try to create a new archive, which should succeed\n    cmd(archiver, \"create\", \"archive2\", \"input\")\n\n    # Try to list archives, which should fail (requires reading from data directory).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"repo-list\")\n\n    # Try to list files in an archive, which should fail (requires reading from data directory).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"list\", \"archive1\")\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"list\", \"archive2\")\n\n    # Try to extract the archive, which should fail (data dir has \"lw\" permissions, no reading).\n    with pytest.raises(PermissionDenied):\n        with changedir(\"output\"):\n            cmd(archiver, \"extract\", \"archive1\")\n\n    # Try to delete an archive, which should fail (requires reading from data directory to identify the archive).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"delete\", \"archive1\")\n\n    # Try to compact the repo, which should fail (data dir has \"lw\" permissions, no reading).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"compact\")\n\n    # Try to check the repo, which should fail (data dir has \"lw\" permissions, no reading).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"check\")\n\n    # Try to delete the repo, which should fail (no \"D\" permission on data dir).\n    with pytest.raises(PermissionDenied):\n        cmd(archiver, \"repo-delete\")\n\n    # Switch to read-only permissions.\n    monkeypatch.setenv(\"BORG_REPO_PERMISSIONS\", \"read-only\")\n\n    # Try to list archives, should work now.\n    output = cmd(archiver, \"repo-list\")\n    assert \"archive1\" in output\n    assert \"archive2\" in output\n\n    # Try to list files in an archive, should work now.\n    cmd(archiver, \"list\", \"archive1\")\n    cmd(archiver, \"list\", \"archive2\")\n"
  },
  {
    "path": "src/borg/testsuite/archiver/return_codes_test.py",
    "content": "import os\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import IncludePatternNeverMatchedWarning\nfrom ...repository import Repository\nfrom . import cmd, changedir, generate_archiver_tests  # NOQA\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_return_codes(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"archive\", \"input\")\n    with changedir(\"output\"):\n        cmd(archiver, \"extract\", \"archive\")\n    cmd(\n        archiver,\n        \"extract\",\n        \"archive\",\n        \"does/not/match\",\n        fork=True,\n        exit_code=IncludePatternNeverMatchedWarning().exit_code,\n    )\n\n\ndef test_exit_codes(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    # we create the repo path, but do NOT initialize the borg repo,\n    # so the borg create commands are expected to fail with DoesNotExist (was: InvalidRepository in borg 1.4).\n    os.makedirs(archiver.repository_path)\n    monkeypatch.setenv(\"BORG_EXIT_CODES\", \"classic\")\n    cmd(archiver, \"create\", \"archive\", \"input\", fork=True, exit_code=EXIT_ERROR)\n    monkeypatch.setenv(\"BORG_EXIT_CODES\", \"modern\")\n    cmd(archiver, \"create\", \"archive\", \"input\", fork=True, exit_code=Repository.DoesNotExist.exit_mcode)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/serve_cmd_test.py",
    "content": "import os\nimport subprocess\nimport tempfile\nimport time\n\nimport pytest\nimport platformdirs\n\nfrom . import exec_cmd\nfrom ...platformflags import is_win32\nfrom ...helpers import get_runtime_dir\n\n\ndef have_a_short_runtime_dir(mp):\n    # Under pytest, we use BORG_BASE_DIR to keep stuff away from the user's normal Borg directories.\n    # This leads to a very long get_runtime_dir() path — too long for a socket file!\n    # Thus, we override that again via BORG_RUNTIME_DIR to get a shorter path.\n    mp.setenv(\"BORG_RUNTIME_DIR\", os.path.join(platformdirs.user_runtime_dir(), \"pytest\"))\n\n\n@pytest.fixture\ndef serve_socket(monkeypatch):\n    have_a_short_runtime_dir(monkeypatch)\n    # Use a random unique socket filename, so tests can run in parallel.\n    socket_file = tempfile.mktemp(suffix=\".sock\", prefix=\"borg-\", dir=get_runtime_dir())\n    with subprocess.Popen([\"borg\", \"serve\", f\"--socket={socket_file}\"]) as p:\n        while not os.path.exists(socket_file):\n            time.sleep(0.01)  # wait until the socket server has started\n        yield socket_file\n        p.terminate()\n\n\n@pytest.mark.skipif(is_win32, reason=\"hangs on win32\")\ndef test_with_socket(serve_socket, tmpdir, monkeypatch):\n    have_a_short_runtime_dir(monkeypatch)\n    repo_path = str(tmpdir.join(\"repo\"))\n\n    ret, output = exec_cmd(\n        f\"--socket={serve_socket}\", f\"--repo=socket://{repo_path}\", \"repo-create\", \"--encryption=none\"\n    )\n    assert ret == 0\n\n    ret, output = exec_cmd(f\"--socket={serve_socket}\", f\"--repo=socket://{repo_path}\", \"repo-info\")\n    assert ret == 0\n    assert \"Repository ID: \" in output\n\n    monkeypatch.setenv(\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\", \"YES\")\n    ret, output = exec_cmd(f\"--socket={serve_socket}\", f\"--repo=socket://{repo_path}\", \"repo-delete\")\n    assert ret == 0\n\n\n@pytest.mark.skipif(is_win32, reason=\"hangs on win32\")\ndef test_socket_permissions(serve_socket):\n    st = os.stat(serve_socket)\n    assert st.st_mode & 0o0777 == 0o0770  # user and group are permitted to use the socket\n"
  },
  {
    "path": "src/borg/testsuite/archiver/tag_cmd_test.py",
    "content": "from ...constants import *  # NOQA\nfrom . import cmd, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local\")  # NOQA\n\n\ndef test_tag_set(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive\", archiver.input_path)\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"aa\")\n    assert \"tags: aa.\" in output\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"bb\")\n    assert \"tags: bb.\" in output\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"bb\", \"aa\")\n    assert \"tags: aa,bb.\" in output  # sorted!\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\")\n    assert \"tags: .\" in output  # no tags!\n\n\ndef test_tag_add_remove(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive\", archiver.input_path)\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--add\", \"aa\")\n    assert \"tags: aa.\" in output\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--add\", \"bb\")\n    assert \"tags: aa,bb.\" in output\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--remove\", \"aa\")\n    assert \"tags: bb.\" in output\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--remove\", \"bb\")\n    assert \"tags: .\" in output\n\n\ndef test_tag_set_noclobber_special(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive\", archiver.input_path)\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"@PROT\")\n    assert \"tags: @PROT.\" in output\n    # archive now has a special tag.\n    # it must not be possible to accidentally erase such special tags by using --set:\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"clobber\")\n    assert \"tags: @PROT.\" in output\n    # it is possible though to use --set if the existing special tags are also given:\n    output = cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"noclobber\", \"@PROT\")\n    assert \"tags: @PROT,noclobber.\" in output\n\n\ndef test_tag_only_known_special(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"archive\", archiver.input_path)\n    # user can't set / add / remove unknown special tags\n    cmd(archiver, \"tag\", \"-a\", \"archive\", \"--set\", \"@UNKNOWN\", exit_code=EXIT_ERROR)\n    cmd(archiver, \"tag\", \"-a\", \"archive\", \"--add\", \"@UNKNOWN\", exit_code=EXIT_ERROR)\n    cmd(archiver, \"tag\", \"-a\", \"archive\", \"--remove\", \"@UNKNOWN\", exit_code=EXIT_ERROR)\n"
  },
  {
    "path": "src/borg/testsuite/archiver/tar_cmds_test.py",
    "content": "import os\nimport shutil\nimport subprocess\n\nimport pytest\n\nfrom ... import xattr\nfrom ...constants import *  # NOQA\nfrom .. import changedir\nfrom . import assert_dirs_equal, _extract_hardlinks_setup, cmd, requires_hardlinks, RK_ENCRYPTION\nfrom . import create_test_files, create_regular_file\nfrom . import generate_archiver_tests\nfrom ...platform import acl_get, acl_set\nfrom ..platform.platform_test import skipif_not_linux, skipif_acls_not_working\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef have_gnutar():\n    if not shutil.which(\"tar\"):\n        return False\n    popen = subprocess.Popen([\"tar\", \"--version\"], stdout=subprocess.PIPE)\n    stdout, stderr = popen.communicate()\n    return b\"GNU tar\" in stdout\n\n\nrequires_gnutar = pytest.mark.skipif(not have_gnutar(), reason=\"GNU tar must be installed for this test.\")\nrequires_gzip = pytest.mark.skipif(not shutil.which(\"gzip\"), reason=\"gzip must be installed for this test.\")\n\n\n@requires_gnutar\ndef test_export_tar(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.unlink(\"input/flagfile\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    cmd(archiver, \"export-tar\", \"test\", \"simple.tar\", \"--progress\", \"--tar-format=GNU\")\n    with changedir(\"output\"):\n        # This probably assumes GNU tar. Note: use the -p switch to extract permissions regardless of umask.\n        subprocess.check_call([\"tar\", \"xpf\", \"../simple.tar\", \"--warning=no-timestamp\"])\n    assert_dirs_equal(\"input\", \"output/input\", ignore_flags=True, ignore_xattrs=True, ignore_ns=True)\n\n\n@requires_gnutar\n@requires_gzip\ndef test_export_tar_gz(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.unlink(\"input/flagfile\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    test_list = cmd(archiver, \"export-tar\", \"test\", \"simple.tar.gz\", \"--list\", \"--tar-format=GNU\")\n    assert \"input/file1\\n\" in test_list\n    assert \"input/dir2\\n\" in test_list\n    with changedir(\"output\"):\n        subprocess.check_call([\"tar\", \"xpf\", \"../simple.tar.gz\", \"--warning=no-timestamp\"])\n    assert_dirs_equal(\"input\", \"output/input\", ignore_flags=True, ignore_xattrs=True, ignore_ns=True)\n\n\n@requires_gnutar\n@requires_gzip\ndef test_export_tar_strip_components(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.unlink(\"input/flagfile\")\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"test\", \"input\")\n    test_list = cmd(archiver, \"export-tar\", \"test\", \"simple.tar\", \"--strip-components=1\", \"--list\", \"--tar-format=GNU\")\n    # --list's paths are those before processing with --strip-components\n    assert \"input/file1\\n\" in test_list\n    assert \"input/dir2\\n\" in test_list\n    with changedir(\"output\"):\n        subprocess.check_call([\"tar\", \"xpf\", \"../simple.tar\", \"--warning=no-timestamp\"])\n    assert_dirs_equal(\"input\", \"output/\", ignore_flags=True, ignore_xattrs=True, ignore_ns=True)\n\n\n@requires_hardlinks\n@requires_gnutar\ndef test_export_tar_strip_components_links(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _extract_hardlinks_setup(archiver)\n    cmd(archiver, \"export-tar\", \"test\", \"output.tar\", \"--strip-components=2\", \"--tar-format=GNU\")\n    with changedir(\"output\"):\n        subprocess.check_call([\"tar\", \"xpf\", \"../output.tar\", \"--warning=no-timestamp\"])\n        assert os.stat(\"hardlink\").st_nlink == 2\n        assert os.stat(\"subdir/hardlink\").st_nlink == 2\n        assert os.stat(\"aaaa\").st_nlink == 2\n        assert os.stat(\"source2\").st_nlink == 2\n\n\n@requires_hardlinks\n@requires_gnutar\ndef test_extract_hardlinks_tar(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    _extract_hardlinks_setup(archiver)\n    cmd(archiver, \"export-tar\", \"test\", \"output.tar\", \"input/dir1\", \"--tar-format=GNU\")\n    with changedir(\"output\"):\n        subprocess.check_call([\"tar\", \"xpf\", \"../output.tar\", \"--warning=no-timestamp\"])\n        assert os.stat(\"input/dir1/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/subdir/hardlink\").st_nlink == 2\n        assert os.stat(\"input/dir1/aaaa\").st_nlink == 2\n        assert os.stat(\"input/dir1/source2\").st_nlink == 2\n\n\ndef test_import_tar(archivers, request, tar_format=\"PAX\"):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path, create_hardlinks=False)  # hard links become separate files\n    os.unlink(\"input/flagfile\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"src\", \"input\")\n    cmd(archiver, \"export-tar\", \"src\", \"simple.tar\", f\"--tar-format={tar_format}\")\n    cmd(archiver, \"import-tar\", \"dst\", \"simple.tar\")\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n    assert_dirs_equal(\"input\", \"output/input\", ignore_ns=True, ignore_xattrs=True)\n\n\ndef test_import_unusual_tar(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n\n    # Contains these, unusual entries:\n    # /foobar\n    # ./bar\n    # ./foo2/\n    # ./foo//bar\n    # ./\n    tar_archive = os.path.join(os.path.dirname(__file__), \"unusual_paths.tar\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"import-tar\", \"dst\", tar_archive)\n    files = cmd(archiver, \"list\", \"dst\", \"--format\", \"{path}{NL}\").splitlines()\n    assert set(files) == {\"foobar\", \"bar\", \"foo2\", \"foo/bar\", \".\"}\n\n\ndef test_import_tar_with_dotdot(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.EXE:  # the test checks for a raised exception. that can't work if the code runs in a separate process.\n        pytest.skip(\"does not work with binaries\")\n\n    # Contains this file:\n    # ../../../../etc/shadow\n    tar_archive = os.path.join(os.path.dirname(__file__), \"dotdot_path.tar\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    with pytest.raises(ValueError, match=\"unexpected '..' element in path '../../../../etc/shadow'\"):\n        cmd(archiver, \"import-tar\", \"dst\", tar_archive, exit_code=2)\n\n\n@requires_gzip\ndef test_import_tar_gz(archivers, request, tar_format=\"GNU\"):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path, create_hardlinks=False)  # hard links become separate files\n    os.unlink(\"input/flagfile\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"src\", \"input\")\n    cmd(archiver, \"export-tar\", \"src\", \"simple.tgz\", f\"--tar-format={tar_format}\")\n    cmd(archiver, \"import-tar\", \"dst\", \"simple.tgz\")\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n    assert_dirs_equal(\"input\", \"output/input\", ignore_ns=True, ignore_xattrs=True)\n\n\n@requires_gnutar\ndef test_import_concatenated_tar_with_ignore_zeros(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path, create_hardlinks=False)  # hard links become separate files\n    os.unlink(\"input/flagfile\")\n    with changedir(\"input\"):\n        subprocess.check_call([\"tar\", \"cf\", \"file1.tar\", \"file1\"])\n        subprocess.check_call([\"tar\", \"cf\", \"the_rest.tar\", \"--exclude\", \"file1*\", \".\"])\n        with open(\"concatenated.tar\", \"wb\") as concatenated:\n            with open(\"file1.tar\", \"rb\") as file1:\n                concatenated.write(file1.read())\n            # Clean up for assert_dirs_equal.\n            os.unlink(\"file1.tar\")\n\n            with open(\"the_rest.tar\", \"rb\") as the_rest:\n                concatenated.write(the_rest.read())\n            # Clean up for assert_dirs_equal.\n            os.unlink(\"the_rest.tar\")\n\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"import-tar\", \"--ignore-zeros\", \"dst\", \"input/concatenated.tar\")\n    # Clean up for assert_dirs_equal.\n    os.unlink(\"input/concatenated.tar\")\n\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n    assert_dirs_equal(\"input\", \"output\", ignore_ns=True, ignore_xattrs=True)\n\n\n@requires_gnutar\ndef test_import_concatenated_tar_without_ignore_zeros(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path, create_hardlinks=False)  # hard links become separate files\n    os.unlink(\"input/flagfile\")\n\n    with changedir(\"input\"):\n        subprocess.check_call([\"tar\", \"cf\", \"file1.tar\", \"file1\"])\n        subprocess.check_call([\"tar\", \"cf\", \"the_rest.tar\", \"--exclude\", \"file1*\", \".\"])\n        with open(\"concatenated.tar\", \"wb\") as concatenated:\n            with open(\"file1.tar\", \"rb\") as file1:\n                concatenated.write(file1.read())\n            with open(\"the_rest.tar\", \"rb\") as the_rest:\n                concatenated.write(the_rest.read())\n            os.unlink(\"the_rest.tar\")\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"import-tar\", \"dst\", \"input/concatenated.tar\")\n\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n    # Negative test -- assert that only file1 has been extracted, and the_rest has been ignored\n    # due to zero-filled block marker.\n    assert os.listdir(\"output\") == [\"file1\"]\n\n\n@requires_gnutar\ndef test_import_tar_with_dotslash_paths(archivers, request):\n    \"\"\"Test that paths starting with './' are normalized during import-tar.\"\"\"\n    archiver = request.getfixturevalue(archivers)\n    # Create a simple directory structure\n    create_regular_file(archiver.input_path, \"dir/file\")\n\n    # Create a tar file with paths starting with './'\n    with changedir(\"input\"):\n        # Directly use a path that starts with './'\n        subprocess.check_call([\"tar\", \"cf\", \"dotslash.tar\", \"./dir\"])\n\n        # Verify the tar file contains paths with './' prefix\n        tar_content = subprocess.check_output([\"tar\", \"tf\", \"dotslash.tar\"]).decode()\n        assert \"./dir\" in tar_content\n        assert \"./dir/file\" in tar_content\n\n    # Import the tar file into a Borg repository\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"import-tar\", \"dotslash\", \"input/dotslash.tar\")\n\n    # List the archive contents and verify no paths start with './'\n    output = cmd(archiver, \"list\", \"--format={path}{NL}\", \"dotslash\")\n    assert \"./dir\" not in output\n    assert \"dir\" in output\n    assert \"dir/file\" in output\n\n\ndef test_roundtrip_pax_borg(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_test_files(archiver.input_path)\n    os.remove(\"input/flagfile\")  # this would be automagically excluded due to NODUMP\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"src\", \"input\")\n    cmd(archiver, \"export-tar\", \"src\", \"simple.tar\", \"--tar-format=BORG\")\n    cmd(archiver, \"import-tar\", \"dst\", \"simple.tar\")\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n    assert_dirs_equal(\"input\", \"output/input\")\n\n\ndef test_roundtrip_pax_xattrs(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    if not xattr.is_enabled(archiver.input_path):\n        pytest.skip(\"xattrs not supported\")\n    create_regular_file(archiver.input_path, \"file\")\n    original_path = os.path.join(archiver.input_path, \"file\")\n    xa_key, xa_value = b\"user.xattrtest\", b\"not valid utf-8: \\xff\"\n    xattr.setxattr(original_path.encode(), xa_key, xa_value)\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"src\", \"input\")\n    cmd(archiver, \"export-tar\", \"src\", \"xattrs.tar\", \"--tar-format=PAX\")\n    cmd(archiver, \"import-tar\", \"dst\", \"xattrs.tar\")\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"dst\")\n        extracted_path = os.path.abspath(\"input/file\")\n        xa_value_extracted = xattr.getxattr(extracted_path.encode(), xa_key)\n    assert xa_value_extracted == xa_value\n\n\n@skipif_not_linux\n@skipif_acls_not_working\ndef test_acl_roundtrip(archivers, request):\n    \"\"\"Test the complete workflow for POSIX ACLs with export-tar and import-tar.\n\n    This test follows the workflow:\n    1. set filesystem ACLs\n    2. create a Borg archive\n    3. export-tar this archive\n    4. import-tar the resulting tar file\n    5. extract the imported archive\n    6. check the expected ACLs in the filesystem\n    \"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    # Define helper functions for working with ACLs\n    def get_acl(path):\n        item = {}\n        acl_get(path, item, os.stat(path))\n        return item\n\n    def set_acl(path, access=None, default=None):\n        item = {\"acl_access\": access, \"acl_default\": default}\n        acl_set(path, item)\n\n    # Define example ACLs\n    ACCESS_ACL = b\"user::rw-\\nuser:root:rw-:0\\ngroup::r--\\ngroup:root:r--:0\\nmask::rw-\\nother::r--\"\n    DEFAULT_ACL = b\"user::rw-\\nuser:root:r--:0\\ngroup::r--\\ngroup:root:r--:0\\nmask::rw-\\nother::r--\"\n\n    # 1. Set filesystem ACLs\n    # Create test files with ACLs\n    create_regular_file(archiver.input_path, \"file\")\n    os.mkdir(os.path.join(archiver.input_path, \"dir\"))\n\n    file_path = os.path.join(archiver.input_path, \"file\")\n    dir_path = os.path.join(archiver.input_path, \"dir\")\n\n    # Set ACLs on the test files\n    try:\n        set_acl(file_path, access=ACCESS_ACL)\n        set_acl(dir_path, access=ACCESS_ACL, default=DEFAULT_ACL)\n    except OSError as e:\n        pytest.skip(f\"Failed to set ACLs: {e}\")\n\n    file_acl = get_acl(file_path)\n    dir_acl = get_acl(dir_path)\n\n    if not file_acl.get(\"acl_access\") or not dir_acl.get(\"acl_access\") or not dir_acl.get(\"acl_default\"):\n        pytest.skip(\"ACLs not supported or not working correctly\")\n\n    # 2. Create a Borg archive\n    cmd(archiver, \"repo-create\", \"--encryption=none\")\n    cmd(archiver, \"create\", \"original\", \"input\")\n\n    # 3. export-tar this archive to a tar file\n    cmd(archiver, \"export-tar\", \"original\", \"acls.tar\", \"--tar-format=PAX\")\n\n    # 4. import-tar the resulting tar file\n    cmd(archiver, \"import-tar\", \"imported\", \"acls.tar\")\n\n    # 5. Extract the imported archive\n    with changedir(archiver.output_path):\n        cmd(archiver, \"extract\", \"imported\")\n\n        # 6. Check the expected ACLs in the filesystem\n        extracted_file_path = os.path.abspath(\"input/file\")\n        extracted_dir_path = os.path.abspath(\"input/dir\")\n\n        extracted_file_acl = get_acl(extracted_file_path)\n        extracted_dir_acl = get_acl(extracted_dir_path)\n\n        # Check that access ACLs were preserved\n        assert \"acl_access\" in extracted_file_acl\n        assert extracted_file_acl[\"acl_access\"] == file_acl[\"acl_access\"]\n        assert b\"user:root:rw-\" in file_acl[\"acl_access\"]\n\n        assert \"acl_access\" in extracted_dir_acl\n        assert extracted_dir_acl[\"acl_access\"] == dir_acl[\"acl_access\"]\n        assert b\"user:root:rw-\" in dir_acl[\"acl_access\"]\n\n        # Check that default ACLs were preserved for directories\n        assert \"acl_default\" in extracted_dir_acl\n        assert extracted_dir_acl[\"acl_default\"] == dir_acl[\"acl_default\"]\n        assert b\"user:root:r--\" in dir_acl[\"acl_default\"]\n"
  },
  {
    "path": "src/borg/testsuite/archiver/transfer_cmd_test.py",
    "content": "import glob\nimport hashlib\nimport json\nimport os\nimport random\nimport re\nimport stat\nimport tarfile\nfrom contextlib import contextmanager\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import open_item\nfrom ...helpers.time import parse_timestamp\nfrom ...helpers.parseformat import parse_file_size, ChunkerParams\nfrom ..platform.platform_test import is_win32\nfrom . import cmd, create_regular_file, create_test_files, RK_ENCRYPTION, open_archive, generate_archiver_tests\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote\")  # NOQA\n\n\ndef test_transfer_upgrade(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() in [\"remote\", \"binary\"]:\n        pytest.skip(\"only works locally\")\n\n    # test upgrading a borg 1.2 repo to borg 2\n    # testing using json is a bit problematic because parseformat (used for json dumping)\n    # already tweaks the values a bit for better printability (like e.g. using the empty\n    # string for attributes that are not present).\n    # borg 1.2 repo dir contents, created by: scripts/make-testdata/test_transfer_upgrade.sh\n    repo12_tar = os.path.join(os.path.dirname(__file__), \"repo12.tar.gz\")\n    repo12_tzoffset = \"+01:00\"  # timezone used to create the repo/archives/json dumps inside the tar file\n\n    def convert_tz(local_naive, tzoffset, tzinfo):\n        # local_naive was meant to be in tzoffset timezone (e.g. \"+01:00\"),\n        # but we want it non-naive in tzinfo time zone (e.g. timezone.utc\n        # or None if local timezone is desired).\n        ts = parse_timestamp(local_naive + tzoffset)\n        return ts.astimezone(tzinfo).isoformat(timespec=\"microseconds\")\n\n    original_location = archiver.repository_location\n    dst_dir = f\"{original_location}1\"\n    os.makedirs(dst_dir)\n    with tarfile.open(repo12_tar) as tf:\n        tf.extractall(dst_dir)\n\n    other_repo1 = f\"--other-repo={original_location}1\"\n    archiver.repository_location = original_location + \"2\"\n\n    monkeypatch.setenv(\"BORG_PASSPHRASE\", \"pw2\")\n    monkeypatch.setenv(\"BORG_OTHER_PASSPHRASE\", \"waytooeasyonlyfortests\")\n    os.environ[\"BORG_TESTONLY_WEAKEN_KDF\"] = \"0\"  # must use the strong kdf here or it can't decrypt the key\n\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION, other_repo1, \"--from-borg1\")\n    cmd(archiver, \"transfer\", other_repo1, \"--from-borg1\")\n    cmd(archiver, \"check\")\n\n    # check list of archives / manifest\n    rlist_json = cmd(archiver, \"repo-list\", \"--json\")\n    got = json.loads(rlist_json)\n    with open(os.path.join(dst_dir, \"test_meta\", \"repo_list.json\")) as f:\n        expected = json.load(f)\n\n    for key in \"encryption\", \"repository\":\n        # some stuff obviously needs to be different, remove that!\n        del got[key]\n        del expected[key]\n    assert len(got[\"archives\"]) == len(expected[\"archives\"])\n\n    for got_archive, expected_archive in zip(got[\"archives\"], expected[\"archives\"]):\n        del got_archive[\"id\"]\n        del got_archive[\"username\"]  # we didn't have this in the 1.x default format\n        del got_archive[\"hostname\"]  # we didn't have this in the 1.x default format\n        del got_archive[\"comment\"]  # we didn't have this in the 1.x default format\n        del got_archive[\"tags\"]  # we didn't have this in the 1.x default format\n        del expected_archive[\"id\"]\n        del expected_archive[\"barchive\"]\n        # timestamps:\n        # borg 1.2 transformed to local time and had microseconds = 0, no tzoffset\n        # borg 2 uses local time, with microseconds and with tzoffset\n        # the only important timestamp is \"time\", which has the nominal timestamp of the archive.\n        del expected_archive[\"start\"]\n        key = \"time\"\n        # fix expectation: local time meant +01:00, so we convert that to whatever local tz is here.\n        expected_archive[key] = convert_tz(expected_archive[key], repo12_tzoffset, None)\n        # set microseconds to 0, so we can compare got with expected.\n        got_ts = parse_timestamp(got_archive[key])\n        got_archive[key] = got_ts.replace(microsecond=0).isoformat(timespec=\"microseconds\")\n    assert got == expected\n\n    for archive in got[\"archives\"]:\n        name = archive[\"name\"]\n        # check archive contents\n        list_json = cmd(archiver, \"list\", \"--json-lines\", name)\n        got = [json.loads(line) for line in list_json.splitlines()]\n        with open(os.path.join(dst_dir, \"test_meta\", f\"{name}_list.json\")) as f:\n            lines = f.read()\n        expected = [json.loads(line) for line in lines.splitlines()]\n        hardlinks = {}\n        for g, e in zip(got, expected):\n            # borg 1.2 parseformat uses .get(\"bsdflags\", 0) so the json has 0 even\n            # if there were no bsdflags stored in the item.\n            # borg 2 parseformat uses .get(\"bsdflags\"), so the json has either an int\n            # (if the archived item has bsdflags) or None (if the item has no bsdflags).\n            if e[\"flags\"] == 0 and g[\"flags\"] is None:\n                # this is expected behavior, fix the expectation\n                e[\"flags\"] = None\n\n            # borg2 parseformat falls back to str(item.uid) if it does not have item.user,\n            # same for str(item.gid) and no item.group.\n            # so user/group are always str type, even if it is just str(uid) or str(gid).\n            # fix expectation (borg1 used int type for user/group in that case):\n            if g[\"user\"] == str(g[\"uid\"]) == str(e[\"uid\"]):\n                e[\"user\"] = str(e[\"uid\"])\n            if g[\"group\"] == str(g[\"gid\"]) == str(e[\"gid\"]):\n                e[\"group\"] = str(e[\"gid\"])\n\n            for key in \"mtime\", \"ctime\", \"atime\":\n                if key in e:\n                    e[key] = convert_tz(e[key], repo12_tzoffset, None)\n\n            # borg 1 used hard link slaves linking back to their hard link masters.\n            # borg 2 uses symmetric approach: just normal items. if they are hard links,\n            # each item has normal attributes, including the chunks list, size. additionally,\n            # they have a hlid and same hlid means same inode / belonging to same set of hard links.\n            hardlink = bool(g.get(\"hlid\"))  # note: json has \"\" as hlid if there is no hlid in the item\n            if hardlink:\n                hardlinks[g[\"path\"]] = g[\"hlid\"]\n                if e[\"mode\"].startswith(\"h\"):\n                    # fix expectations: borg1 signalled a hard link slave with \"h\"\n                    # borg2 treats all hard links symmetrically as normal files\n                    e[\"mode\"] = g[\"mode\"][0] + e[\"mode\"][1:]\n                    # borg1 used source/linktarget to link back to hard link master\n                    assert e[\"source\"] != \"\"\n                    assert e[\"linktarget\"] != \"\"\n                    # fix expectations: borg2 does not use source/linktarget any more for hard links\n                    e[\"source\"] = \"\"\n                    e[\"linktarget\"] = \"\"\n                    # borg 1 has size == 0 for hard link slaves, borg 2 has the real file size\n                    assert e[\"size\"] == 0\n                    assert g[\"size\"] >= 0\n                    # fix expectation for size\n                    e[\"size\"] = g[\"size\"]\n                # Note: size == 0 for all items without a size or chunks list (like e.g. directories)\n            del g[\"hlid\"]\n\n            # borg 1 used \"linktarget\" and \"source\" for links, borg 2 uses \"target\" for symlinks.\n            if g[\"target\"] == e[\"linktarget\"]:\n                e[\"target\"] = e[\"linktarget\"]\n                del e[\"linktarget\"]\n                del e[\"source\"]\n\n            if e[\"type\"] == \"b\" and is_win32:\n                # The S_IFBLK macro is broken on MINGW\n                del e[\"type\"], g[\"type\"]\n                del e[\"mode\"], g[\"mode\"]\n\n            del e[\"healthy\"]  # not supported anymore\n            del g[\"inode\"]  # new in borg2\n\n            assert g == e\n\n        if name == \"archive1\":\n            # hard links referring to same inode have same hlid\n            assert hardlinks[\"tmp/borgtest/hardlink1\"] == hardlinks[\"tmp/borgtest/hardlink2\"]\n\n    repo_path = f\"{original_location}2\"\n    for archive_name in (\"archive1\", \"archive2\"):\n        archive, repository = open_archive(repo_path, archive_name)\n        with repository:\n            for item in archive.iter_items():\n                # borg1 used to store some stuff with None values\n                # borg2 does just not have the key if the value is not known.\n                item_dict = item.as_dict()\n                assert not any(value is None for value in item_dict.values()), f\"found None value in {item_dict}\"\n\n                # with borg2, all items with chunks must have a precomputed size\n                assert \"chunks\" not in item or \"size\" in item and item.size >= 0\n\n                if item.path.endswith(\"directory\") or item.path.endswith(\"borgtest\"):\n                    assert stat.S_ISDIR(item.mode)\n                    assert item.uid > 0\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"no_hardlink\") or item.path.endswith(\"target\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert item.uid > 0\n                    assert \"hlid\" not in item\n                    assert len(item.chunks) > 0\n                    assert \"bsdflags\" not in item\n                elif item.path.endswith(\"hardlink1\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert item.uid > 0\n                    assert \"hlid\" in item and len(item.hlid) == 32  # 256bit\n                    hlid1 = item.hlid\n                    assert len(item.chunks) > 0\n                    chunks1 = item.chunks\n                    size1 = item.size\n                    assert \"source\" not in item\n                    assert \"target\" not in item\n                    assert \"hardlink_master\" not in item\n                elif item.path.endswith(\"hardlink2\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert item.uid > 0\n                    assert \"hlid\" in item and len(item.hlid) == 32  # 256bit\n                    hlid2 = item.hlid\n                    assert len(item.chunks) > 0\n                    chunks2 = item.chunks\n                    size2 = item.size\n                    assert \"source\" not in item\n                    assert \"target\" not in item\n                    assert \"hardlink_master\" not in item\n                elif item.path.endswith(\"broken_symlink\"):\n                    assert stat.S_ISLNK(item.mode)\n                    assert item.target == \"doesnotexist\"\n                    assert item.uid > 0\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"symlink\"):\n                    assert stat.S_ISLNK(item.mode)\n                    assert item.target == \"target\"\n                    assert item.uid > 0\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"fifo\"):\n                    assert stat.S_ISFIFO(item.mode)\n                    assert item.uid > 0\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"without_xattrs\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert \"xattrs\" not in item\n                elif item.path.endswith(\"with_xattrs\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert \"xattrs\" in item\n                    assert len(item.xattrs) == 2\n                    assert item.xattrs[b\"key1\"] == b\"value\"\n                    assert item.xattrs[b\"key2\"] == b\"\"\n                elif item.path.endswith(\"without_flags\"):\n                    assert stat.S_ISREG(item.mode)\n                    # borg1 did not store a flags value of 0 (\"nothing special\")\n                    # borg2 reflects this \"I do not know\" by not having the k/v pair\n                    assert \"bsdflags\" not in item\n                elif item.path.endswith(\"with_flags\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert \"bsdflags\" in item\n                    assert item.bsdflags == stat.UF_NODUMP\n                elif item.path.endswith(\"root_stuff\"):\n                    assert stat.S_ISDIR(item.mode)\n                    assert item.uid == 0\n                    assert item.gid == 0\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"cdev_34_56\"):\n                    assert stat.S_ISCHR(item.mode)\n                    # looks like we can't use os.major/minor with data coming from another platform,\n                    # thus we only do a rather rough check here:\n                    assert \"rdev\" in item and item.rdev != 0\n                    assert item.uid == 0\n                    assert item.gid == 0\n                    assert item.user == \"root\"\n                    assert item.group in (\"root\", \"wheel\")\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"bdev_12_34\"):\n                    if not is_win32:\n                        # The S_IFBLK macro is broken on MINGW\n                        assert stat.S_ISBLK(item.mode)\n                    # looks like we can't use os.major/minor with data coming from another platform,\n                    # thus we only do a rather rough check here:\n                    assert \"rdev\" in item and item.rdev != 0\n                    assert item.uid == 0\n                    assert item.gid == 0\n                    assert item.user == \"root\"\n                    assert item.group in (\"root\", \"wheel\")\n                    assert \"hlid\" not in item\n                elif item.path.endswith(\"strange_uid_gid\"):\n                    assert stat.S_ISREG(item.mode)\n                    assert item.uid == 54321\n                    assert item.gid == 54321\n                    assert \"user\" not in item\n                    assert \"group\" not in item\n                else:\n                    raise NotImplementedError(f\"test missing for {item.path}\")\n        if archive_name == \"archive1\":\n            assert hlid1 == hlid2\n            assert size1 == size2 == 16 + 1  # 16 text chars + \\n\n            assert chunks1 == chunks2\n\n\n@contextmanager\ndef setup_repos(archiver, mp):\n    \"\"\"\n    set up repos for transfer tests: OTHER_REPO1  ---transfer---> REPO2\n    when the context manager is entered, archiver will work with REPO1 (so one can prepare it as the source repo).\n    when the context manager is exited, archiver will work with REPO2 (so the transfer can be run).\n    \"\"\"\n    original_location = archiver.repository_location\n    original_path = archiver.repository_path\n\n    mp.setenv(\"BORG_PASSPHRASE\", \"pw1\")\n    archiver.repository_location = original_location + \"1\"\n    archiver.repository_path = original_path + \"1\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n\n    other_repo1 = f\"--other-repo={original_location}1\"\n    yield other_repo1\n\n    mp.setenv(\"BORG_PASSPHRASE\", \"pw2\")\n    mp.setenv(\"BORG_OTHER_PASSPHRASE\", \"pw1\")\n    archiver.repository_location = original_location + \"2\"\n    archiver.repository_path = original_path + \"2\"\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION, other_repo1)\n\n\ndef test_transfer(archivers, request, monkeypatch):\n    archiver = request.getfixturevalue(archivers)\n\n    def check_repo():\n        listing = cmd(archiver, \"repo-list\")\n        assert \"arch1\" in listing\n        assert \"arch2\" in listing\n        listing = cmd(archiver, \"list\", \"--short\", \"arch1\")\n        assert \"file1\" in listing\n        assert \"dir2/file2\" in listing\n        cmd(archiver, \"check\")\n\n    with setup_repos(archiver, monkeypatch) as other_repo1:\n        # prepare the source repo:\n        create_test_files(archiver.input_path)\n        cmd(archiver, \"create\", \"arch1\", \"input\")\n        cmd(archiver, \"create\", \"arch2\", \"input\")\n        check_repo()\n\n    # test the transfer:\n    cmd(archiver, \"transfer\", other_repo1, \"--dry-run\")\n    cmd(archiver, \"transfer\", other_repo1)\n    cmd(archiver, \"transfer\", other_repo1, \"--dry-run\")\n    check_repo()\n\n\ndef test_transfer_archive_metadata(archivers, request, monkeypatch):\n    \"\"\"Test transfer of archive metadata\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    with setup_repos(archiver, monkeypatch) as other_repo1:\n        create_test_files(archiver.input_path)\n        # Create an archive with a comment\n        test_comment = \"This is a test comment for transfer\"\n        cmd(archiver, \"create\", \"--comment\", test_comment, \"archive\", \"input\")\n\n        # Get metadata from source archive\n        source_info_json = cmd(archiver, \"info\", \"--json\", \"archive\")\n        source_info = json.loads(source_info_json)\n        source_archive = source_info[\"archives\"][0]\n\n    # Transfer should succeed\n    cmd(archiver, \"transfer\", other_repo1)\n\n    # Get metadata from destination archive\n    dest_info_json = cmd(archiver, \"info\", \"--json\", \"archive\")\n    dest_info = json.loads(dest_info_json)\n    dest_archive = dest_info[\"archives\"][0]\n\n    # Compare metadata fields\n    assert dest_archive[\"comment\"] == source_archive[\"comment\"]\n    assert dest_archive[\"hostname\"] == source_archive[\"hostname\"]\n    assert dest_archive[\"username\"] == source_archive[\"username\"]\n    assert dest_archive[\"command_line\"] == source_archive[\"command_line\"]\n    assert dest_archive[\"cwd\"] == source_archive[\"cwd\"]\n    assert dest_archive[\"duration\"] == source_archive[\"duration\"]\n    assert dest_archive[\"start\"] == source_archive[\"start\"]\n    assert dest_archive[\"end\"] == source_archive[\"end\"]\n    assert dest_archive[\"tags\"] == source_archive[\"tags\"]\n    assert dest_archive[\"chunker_params\"] == source_archive[\"chunker_params\"]\n\n    # Compare stats\n    assert dest_archive[\"stats\"][\"nfiles\"] == source_archive[\"stats\"][\"nfiles\"]\n    # Note: original_size might differ slightly between source and destination due to implementation details\n    # but they should be close enough for the test to pass. TODO: check this, could also be a bug maybe.\n    assert abs(dest_archive[\"stats\"][\"original_size\"] - source_archive[\"stats\"][\"original_size\"]) < 10000\n\n\n@pytest.mark.parametrize(\"recompress_mode\", [\"never\", \"always\"])\ndef test_transfer_recompress(archivers, request, monkeypatch, recompress_mode):\n    \"\"\"Test transfer with recompression\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    def repo_size(archiver):\n        output = cmd(archiver, \"compact\", \"-v\", \"--stats\")\n        match = re.search(r\"Repository size is ([^B]+)B\", output, re.MULTILINE)\n        size = parse_file_size(match.group(1))\n        return size\n\n    with setup_repos(archiver, monkeypatch) as other_repo1:\n        create_test_files(archiver.input_path)\n        cmd(archiver, \"create\", \"--compression=none\", \"archive\", \"input\")\n        source_size = repo_size(archiver)\n\n    # Test with --recompress and a different compression algorithm\n    cmd(archiver, \"transfer\", other_repo1, f\"--recompress={recompress_mode}\", \"--compression=zstd\")\n    dest_size = repo_size(archiver)\n\n    # Verify that the transfer succeeded\n    listing = cmd(archiver, \"repo-list\")\n    assert \"archive\" in listing\n\n    # Check repository size difference based on recompress_mode\n    if recompress_mode == \"always\":\n        # zstd compression is better than none.\n        assert source_size > dest_size, f\"dest_size ({dest_size}) should be smaller than source_size ({source_size}).\"\n    else:  # recompress_mode == \"never\"\n        # When not recompressing, the data chunks should remain the same size.\n        # There might be small differences due to metadata, but they should be minimal\n        # We allow a small percentage difference to account for metadata changes.\n        size_diff_percent = abs(source_size - dest_size) / source_size * 100\n        assert size_diff_percent < 5, f\"dest_size ({dest_size}) should be similar as source_size ({source_size}).\"\n\n\ndef test_transfer_rechunk(archivers, request, monkeypatch):\n    \"\"\"Test transfer with re-chunking\"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    BLKSIZE = 512\n    source_chunker_params = \"buzhash,19,23,21,4095\"  # default buzhash chunks\n    dest_chunker_params = f\"fixed,{BLKSIZE}\"  # fixed chunk size\n\n    with setup_repos(archiver, monkeypatch) as other_repo1:\n        contents_1 = random.randbytes(1 * BLKSIZE)\n        contents_255 = random.randbytes(255 * BLKSIZE)\n        contents_1024 = random.randbytes(1024 * BLKSIZE)\n        create_regular_file(archiver.input_path, \"file_1\", contents=contents_1)\n        create_regular_file(archiver.input_path, \"file_256\", contents=contents_255 + contents_1)\n        create_regular_file(archiver.input_path, \"file_1280\", contents=contents_1024 + contents_255 + contents_1)\n\n        cmd(archiver, \"create\", f\"--chunker-params={source_chunker_params}\", \"archive\", \"input\")\n\n        # Get metadata from source archive\n        source_info_json = cmd(archiver, \"info\", \"--json\", \"archive\")\n        source_info = json.loads(source_info_json)\n        source_archive = source_info[\"archives\"][0]\n        source_chunker_params_info = source_archive[\"chunker_params\"]\n\n        # Calculate SHA256 hashes of file contents from source archive\n        source_archive_obj, source_repo = open_archive(archiver.repository_path, \"archive\")\n        with source_repo:\n            source_file_hashes = {}\n            for item in source_archive_obj.iter_items():\n                if hasattr(item, \"chunks\"):  # Only process regular files with chunks\n                    f = open_item(source_archive_obj, item)\n                    content = f.read(10 * 1024 * 1024)  # Read up to 10 MB\n                    source_file_hashes[item.path] = hashlib.sha256(content).hexdigest()\n\n    # Transfer with rechunking\n    cmd(archiver, \"transfer\", other_repo1, f\"--chunker-params={dest_chunker_params}\")\n\n    # Get metadata from destination archive\n    dest_info_json = cmd(archiver, \"info\", \"--json\", \"archive\")\n    dest_info = json.loads(dest_info_json)\n    dest_archive = dest_info[\"archives\"][0]\n    dest_chunker_params_info = dest_archive[\"chunker_params\"]\n\n    # chunker params in metadata must reflect the chunker params given on the CLI\n    assert tuple(source_chunker_params_info) == ChunkerParams(source_chunker_params)\n    assert tuple(dest_chunker_params_info) == ChunkerParams(dest_chunker_params)\n\n    # Compare file hashes between source and destination archives, also check expected chunk counts.\n    dest_archive_obj, dest_repo = open_archive(archiver.repository_path, \"archive\")\n    with dest_repo:\n        for item in dest_archive_obj.iter_items():\n            if hasattr(item, \"chunks\"):  # Only process regular files with chunks\n                # Verify expected chunk count for each file\n                expected_chunk_count = {\"input/file_1\": 1, \"input/file_256\": 256, \"input/file_1280\": 1280}[item.path]\n                assert len(item.chunks) == expected_chunk_count\n                f = open_item(dest_archive_obj, item)\n                content = f.read(10 * 1024 * 1024)  # Read up to 10 MB\n                dest_hash = hashlib.sha256(content).hexdigest()\n                # Verify that the file hash is identical to the source\n                assert item.path in source_file_hashes, f\"File {item.path} not found in source archive\"\n                assert dest_hash == source_file_hashes[item.path], f\"Content hash mismatch for {item.path}\"\n\n\ndef test_transfer_rechunk_dry_run(archivers, request, monkeypatch):\n    \"\"\"Ensure --dry-run works together with --chunker-params (re-chunking path).\n\n    This specifically guards against regressions like AttributeError when archive is None\n    during dry-run (see issue #9199).\n    \"\"\"\n    archiver = request.getfixturevalue(archivers)\n\n    BLKSIZE = 512\n    source_chunker_params = \"buzhash,19,23,21,4095\"  # default-ish buzhash parameters\n    dest_chunker_params = f\"fixed,{BLKSIZE}\"  # simple deterministic chunking\n\n    # Prepare source repo and create one archive\n    with setup_repos(archiver, monkeypatch) as other_repo1:\n        contents = random.randbytes(8 * BLKSIZE)\n        create_regular_file(archiver.input_path, \"file.bin\", contents=contents)\n        cmd(archiver, \"create\", f\"--chunker-params={source_chunker_params}\", \"arch\", \"input\")\n\n    # Now we are in the destination repo (setup_repos switched us on context exit).\n    # Run transfer in dry-run mode with re-chunking. This must not crash.\n    cmd(archiver, \"transfer\", other_repo1, \"--dry-run\", f\"--chunker-params={dest_chunker_params}\")\n\n    # Dry-run must not have created archives in the destination repo.\n    listing = cmd(archiver, \"repo-list\")\n    assert \"arch\" not in listing\n\n\ndef test_issue_9022(archivers, request, monkeypatch):\n    \"\"\"\n    Regression test for borgbackup/borg#9022: After \"borg transfer --from-borg1\",\n    the source Borg 1.x repository index must not be changed.\n    \"\"\"\n    archiver = request.getfixturevalue(archivers)\n    if archiver.get_kind() in [\"remote\", \"binary\"]:\n        pytest.skip(\"only works locally\")\n\n    # Prepare source (borg 1.2) repo from tarball next to this test file\n    repo12_tar = os.path.join(os.path.dirname(__file__), \"repo12.tar.gz\")\n\n    original_location = archiver.repository_location\n    extract_dir = f\"{original_location}1\"\n    os.makedirs(extract_dir)\n    with tarfile.open(repo12_tar) as tf:\n        tf.extractall(extract_dir)\n\n    def index_meta(repo_path):\n        index_files = sorted(glob.glob(os.path.join(repo_path, \"index.*\")))\n        assert len(index_files) == 1, f\"Expected exactly 1 index file before transfer, found {len(index_files)}\"\n        st = os.stat(index_files[0])\n        # Return (mtime_ns, size, inode). Use fallbacks where attributes may not exist on some platforms.\n        mtime_ns = getattr(st, \"st_mtime_ns\", int(st.st_mtime * 1e9))\n        inode = getattr(st, \"st_ino\", None)\n        return (mtime_ns, st.st_size, inode)\n\n    # Record pre-transfer index file metadata\n    pre_meta = index_meta(extract_dir)\n\n    other_repo1 = f\"--other-repo={original_location}1\"\n\n    # Destination repo where we transfer to (borg 2 repo)\n    archiver.repository_location = f\"{original_location}2\"\n\n    # Set passphrases: repo12 testdata uses \"waytooeasyonlyfortests\"\n    monkeypatch.setenv(\"BORG_PASSPHRASE\", \"pw2\")\n    monkeypatch.setenv(\"BORG_OTHER_PASSPHRASE\", \"waytooeasyonlyfortests\")\n    # For this test, we must not weaken KDF, otherwise borg2 couldn't decrypt the borg1 key\n    os.environ[\"BORG_TESTONLY_WEAKEN_KDF\"] = \"0\"\n\n    # Create destination repo and run transfer from borg1 source\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION, other_repo1, \"--from-borg1\")\n    cmd(archiver, \"transfer\", other_repo1, \"--from-borg1\")\n\n    # After transfer, ensure the source borg1 index file looks valid and unchanged.\n    post_meta = index_meta(extract_dir)\n\n    assert post_meta == pre_meta, (\n        f\"Index file metadata changed after transfer!\\n\"\n        f\"Before: mtime_ns={pre_meta[0]}, size={pre_meta[1]}, inode={pre_meta[2]}\\n\"\n        f\"After:  mtime_ns={post_meta[0]}, size={post_meta[1]}, inode={post_meta[2]}\"\n    )\n"
  },
  {
    "path": "src/borg/testsuite/archiver/undelete_cmd_test.py",
    "content": "from ...constants import *  # NOQA\nfrom . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION\n\npytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds=\"local,remote,binary\")  # NOQA\n\n\ndef test_undelete_single(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"normal\", \"input\")\n    cmd(archiver, \"create\", \"deleted\", \"input\")\n    cmd(archiver, \"delete\", \"deleted\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"normal\" in output\n    assert \"deleted\" not in output\n    cmd(archiver, \"undelete\", \"deleted\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"normal\" in output\n    assert \"deleted\" in output  # it's back!\n    cmd(archiver, \"check\")\n\n\ndef test_undelete_multiple_dryrun(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"normal\", \"input\")\n    cmd(archiver, \"create\", \"deleted1\", \"input\")\n    cmd(archiver, \"create\", \"deleted2\", \"input\")\n    cmd(archiver, \"delete\", \"deleted1\")\n    cmd(archiver, \"delete\", \"deleted2\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"normal\" in output\n    assert \"deleted1\" not in output\n    assert \"deleted2\" not in output\n    output = cmd(archiver, \"undelete\", \"--dry-run\", \"--list\", \"-a\", \"sh:*\")\n    assert \"normal\" not in output  # not a candidate for undeletion\n    assert \"deleted1\" in output  # candidate for undeletion\n    assert \"deleted2\" in output  # candidate for undeletion\n    output = cmd(archiver, \"repo-list\")  # nothing changed; it was a dry-run\n    assert \"normal\" in output\n    assert \"deleted1\" not in output\n    assert \"deleted2\" not in output\n\n\ndef test_undelete_multiple_run(archivers, request):\n    archiver = request.getfixturevalue(archivers)\n    create_regular_file(archiver.input_path, \"file1\", size=1024 * 80)\n    cmd(archiver, \"repo-create\", RK_ENCRYPTION)\n    cmd(archiver, \"create\", \"normal\", \"input\")\n    cmd(archiver, \"create\", \"deleted1\", \"input\")\n    cmd(archiver, \"create\", \"deleted2\", \"input\")\n    cmd(archiver, \"delete\", \"deleted1\")\n    cmd(archiver, \"delete\", \"deleted2\")\n    output = cmd(archiver, \"repo-list\")\n    assert \"normal\" in output\n    assert \"deleted1\" not in output\n    assert \"deleted2\" not in output\n    output = cmd(archiver, \"undelete\", \"--list\", \"-a\", \"sh:*\")\n    assert \"normal\" not in output  # not undeleted\n    assert \"deleted1\" in output  # undeleted\n    assert \"deleted2\" in output  # undeleted\n    output = cmd(archiver, \"repo-list\")  # nothing changed; it was a dry-run\n    assert \"normal\" in output\n    assert \"deleted1\" in output\n    assert \"deleted2\" in output\n"
  },
  {
    "path": "src/borg/testsuite/benchmark_test.py",
    "content": "\"\"\"\nRun benchmarks using pytest-benchmark.\n\nUsage:\n\n    py.test --benchmark-only\n\"\"\"\n\nimport os\n\nimport pytest\n\nfrom .archiver import changedir, cmd_fixture  # NOQA\nfrom .item_test import Item\nfrom ..constants import zeros\n\n\n@pytest.fixture\ndef repo_url(request, tmpdir, monkeypatch):\n    monkeypatch.setenv(\"BORG_PASSPHRASE\", \"123456\")\n    monkeypatch.setenv(\"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING\", \"YES\")\n    monkeypatch.setenv(\"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING\", \"YES\")\n    monkeypatch.setenv(\"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK\", \"yes\")\n    monkeypatch.setenv(\"BORG_KEYS_DIR\", str(tmpdir.join(\"keys\")))\n    monkeypatch.setenv(\"BORG_CACHE_DIR\", str(tmpdir.join(\"cache\")))\n    yield str(tmpdir.join(\"repository\"))\n    tmpdir.remove(rec=1)\n\n\n@pytest.fixture(params=[\"none\", \"repokey-aes-ocb\"])\ndef repo(request, cmd_fixture, repo_url):\n    cmd_fixture(f\"--repo={repo_url}\", \"repo-create\", \"--encryption\", request.param)\n    return repo_url\n\n\n@pytest.fixture(scope=\"session\", params=[\"zeros\", \"random\"])\ndef testdata(request, tmpdir_factory):\n    count, size = 10, 1000 * 1000\n    assert size <= len(zeros)\n    p = tmpdir_factory.mktemp(\"data\")\n    data_type = request.param\n    if data_type == \"zeros\":\n        # Do not use a binary zero (\\\\0) to avoid sparse detection.\n        def data(size):\n            return memoryview(zeros)[:size]\n\n    elif data_type == \"random\":\n\n        def data(size):\n            return os.urandom(size)\n\n    else:\n        raise ValueError(\"data_type must be 'random' or 'zeros'.\")\n    for i in range(count):\n        with open(str(p.join(str(i))), \"wb\") as f:\n            f.write(data(size))\n    yield str(p)\n    p.remove(rec=1)\n\n\n@pytest.fixture(params=[\"none\", \"lz4\"])\ndef repo_archive(request, cmd_fixture, repo, testdata):\n    archive = \"test\"\n    cmd_fixture(f\"--repo={repo}\", \"create\", \"--compression\", request.param, archive, testdata)\n    return repo, archive\n\n\ndef test_create_none(benchmark, cmd_fixture, repo, testdata):\n    result, out = benchmark.pedantic(\n        cmd_fixture, (f\"--repo={repo}\", \"create\", \"--compression\", \"none\", \"test\", testdata)\n    )\n    assert result == 0\n\n\ndef test_create_lz4(benchmark, cmd_fixture, repo, testdata):\n    result, out = benchmark.pedantic(\n        cmd_fixture, (f\"--repo={repo}\", \"create\", \"--compression\", \"lz4\", \"test\", testdata)\n    )\n    assert result == 0\n\n\ndef test_extract(benchmark, cmd_fixture, repo_archive, tmpdir):\n    repo, archive = repo_archive\n    with changedir(str(tmpdir)):\n        result, out = benchmark.pedantic(cmd_fixture, (f\"--repo={repo}\", \"extract\", archive))\n    assert result == 0\n\n\ndef test_delete(benchmark, cmd_fixture, repo_archive):\n    repo, archive = repo_archive\n    result, out = benchmark.pedantic(cmd_fixture, (f\"--repo={repo}\", \"delete\", \"-a\", archive))\n    assert result == 0\n\n\ndef test_list(benchmark, cmd_fixture, repo_archive):\n    repo, archive = repo_archive\n    result, out = benchmark(cmd_fixture, f\"--repo={repo}\", \"list\", archive)\n    assert result == 0\n\n\ndef test_info(benchmark, cmd_fixture, repo_archive):\n    repo, archive = repo_archive\n    result, out = benchmark(cmd_fixture, f\"--repo={repo}\", \"info\", \"-a\", archive)\n    assert result == 0\n\n\ndef test_check(benchmark, cmd_fixture, repo_archive):\n    repo, archive = repo_archive\n    result, out = benchmark(cmd_fixture, f\"--repo={repo}\", \"check\")\n    assert result == 0\n\n\ndef test_help(benchmark, cmd_fixture):\n    result, out = benchmark(cmd_fixture, \"help\")\n    assert result == 0\n\n\n@pytest.mark.parametrize(\"type, key, value\", [(Item, \"size\", 1000), (Item, \"mode\", 0x777), (Item, \"deleted\", False)])\ndef test_propdict_attributes(benchmark, type, key, value):\n    propdict = type()\n\n    def getattr_setattr(item, key, value):\n        setattr(item, key, value)\n        assert item.get(key) == value\n        assert getattr(item, key) == value\n        item.as_dict()\n\n    benchmark(getattr_setattr, propdict, key, value)\n"
  },
  {
    "path": "src/borg/testsuite/cache_test.py",
    "content": "import os\n\nimport pytest\n\nfrom .hashindex_test import H\nfrom .crypto.key_test import TestKey\nfrom ..archive import Statistics\nfrom ..cache import AdHocWithFilesCache, delete_chunkindex_cache, read_chunkindex_from_repo_cache\nfrom ..crypto.key import AESOCBRepoKey\nfrom ..manifest import Manifest\nfrom ..repository import Repository\n\n\nclass TestAdHocWithFilesCache:\n    @pytest.fixture\n    def repository(self, tmpdir):\n        self.repository_location = os.path.join(str(tmpdir), \"repository\")\n        with Repository(self.repository_location, exclusive=True, create=True) as repository:\n            repository.put(H(1), b\"1234\")\n            yield repository\n\n    @pytest.fixture\n    def key(self, repository, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        key = AESOCBRepoKey.create(repository, TestKey.MockArgs())\n        return key\n\n    @pytest.fixture\n    def manifest(self, repository, key):\n        Manifest(key, repository).write()\n        return Manifest.load(repository, key=key, operations=Manifest.NO_OPERATION_CHECK)\n\n    @pytest.fixture\n    def cache(self, repository, key, manifest):\n        return AdHocWithFilesCache(manifest)\n\n    def test_does_not_contain_manifest(self, cache):\n        assert not cache.seen_chunk(Manifest.MANIFEST_ID)\n\n    def test_seen_chunk_add_chunk_size(self, cache):\n        assert cache.add_chunk(H(1), {}, b\"5678\", stats=Statistics()) == (H(1), 4)\n\n    def test_reuse_after_add_chunk(self, cache):\n        assert cache.add_chunk(H(3), {}, b\"5678\", stats=Statistics()) == (H(3), 4)\n        assert cache.reuse_chunk(H(3), 4, Statistics()) == (H(3), 4)\n\n    def test_existing_reuse_after_add_chunk(self, cache):\n        assert cache.add_chunk(H(1), {}, b\"5678\", stats=Statistics()) == (H(1), 4)\n        assert cache.reuse_chunk(H(1), 4, Statistics()) == (H(1), 4)\n\n    def test_files_cache(self, cache):\n        st = os.stat(\".\")\n        assert cache.file_known_and_unchanged(b\"foo\", bytes(32), st) == (False, None)\n        assert cache.cache_mode == \"d\"\n        assert cache.files == {}\n\n\ndef test_delete_chunkindex_cache_missing(tmp_path):\n    \"\"\"delete_chunkindex_cache handles StoreObjectNotFound when cache entries do not exist.\"\"\"\n    from borgstore.store import ObjectNotFound as StoreObjectNotFound\n\n    repository_location = os.fspath(tmp_path / \"repository\")\n    with Repository(repository_location, exclusive=True, create=True) as repository:\n        # Create a cache entry so list_chunkindex_hashes finds it.\n        repository.store_store(f\"cache/chunks.{'a' * 64}\", b\"data\")\n        # Patch store_delete to raise StoreObjectNotFound (simulates a race or already-deleted entry).\n        original_store_delete = repository.store_delete\n\n        def failing_store_delete(name):\n            raise StoreObjectNotFound(name)\n\n        repository.store_delete = failing_store_delete\n        # Should not raise — the except StoreObjectNotFound catches it.\n        delete_chunkindex_cache(repository)\n        repository.store_delete = original_store_delete\n\n\ndef test_read_chunkindex_from_repo_cache_missing(tmp_path):\n    \"\"\"read_chunkindex_from_repo_cache handles StoreObjectNotFound when cache does not exist.\"\"\"\n    repository_location = os.fspath(tmp_path / \"repository\")\n    with Repository(repository_location, exclusive=True, create=True) as repository:\n        # Try to load a non-existent cache entry — should return None, not raise.\n        result = read_chunkindex_from_repo_cache(repository, \"f\" * 64)\n        assert result is None\n"
  },
  {
    "path": "src/borg/testsuite/checksums_test.py",
    "content": "from xxhash import xxh64\n\nfrom ..helpers import hex_to_bin\n\n\ndef test_xxh64():\n    assert xxh64(b\"test\", 123).hexdigest() == \"2b81b9401bef86cf\"\n    assert xxh64(b\"test\").hexdigest() == \"4fdcca5ddb678139\"\n    assert (\n        xxh64(\n            hex_to_bin(\n                \"6f663f01c118abdea553373d5eae44e7dac3b6829b46b9bbeff202b6c592c22d724\"\n                \"fb3d25a347cca6c5b8f20d567e4bb04b9cfa85d17f691590f9a9d32e8ccc9102e9d\"\n                \"cf8a7e6716280cd642ce48d03fdf114c9f57c20d9472bb0f81c147645e6fa3d331\"\n            )\n        ).hexdigest()\n        == \"35d5d2f545d9511a\"\n    )\n\n    hasher = xxh64(seed=123)\n    hasher.update(b\"te\")\n    hasher.update(b\"st\")\n    assert hasher.hexdigest() == \"2b81b9401bef86cf\"\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/__init__.py",
    "content": "import os\nimport tempfile\n\nfrom borg.constants import *  # noqa\n\nfrom ...chunkers import has_seek_hole\n\n\ndef cf(chunks):\n    \"\"\"Chunk filter.\"\"\"\n\n    # This is to simplify testing: either return the data piece (bytes) or the hole length (int).\n    def _cf(chunk):\n        if chunk.meta[\"allocation\"] == CH_DATA:\n            assert len(chunk.data) == chunk.meta[\"size\"]\n            return bytes(chunk.data)  # Make sure we have bytes, not a memoryview\n        if chunk.meta[\"allocation\"] in (CH_HOLE, CH_ALLOC):\n            assert chunk.data is None\n            return chunk.meta[\"size\"]\n        assert False, \"unexpected allocation value\"\n\n    return [_cf(chunk) for chunk in chunks]\n\n\ndef cf_expand(chunks):\n    \"\"\"same as cf, but do not return ints for HOLE and ALLOC, but all-zero bytestrings\"\"\"\n    return [ch if isinstance(ch, bytes) else b\"\\0\" * ch for ch in cf(chunks)]\n\n\ndef make_sparsefile(fname, sparsemap, header_size=0):\n    with open(fname, \"wb\") as fd:\n        total = 0\n        if header_size:\n            fd.write(b\"H\" * header_size)\n            total += header_size\n        for offset, size, is_data in sparsemap:\n            if is_data:\n                fd.write(b\"X\" * size)\n            else:\n                fd.seek(size, os.SEEK_CUR)\n            total += size\n        fd.truncate(total)\n    assert os.path.getsize(fname) == total\n\n\ndef make_content(sparsemap, header_size=0):\n    result = []\n    total = 0\n    if header_size:\n        result.append(b\"H\" * header_size)\n        total += header_size\n    for offset, size, is_data in sparsemap:\n        if is_data:\n            result.append(b\"X\" * size)  # bytes!\n        else:\n            result.append(size)  # int!\n        total += size\n    return result\n\n\ndef fs_supports_sparse():\n    if not has_seek_hole:\n        return False\n    with tempfile.TemporaryDirectory() as tmpdir:\n        fn = os.path.join(tmpdir, \"test_sparse\")\n        make_sparsefile(fn, [(0, BS, False), (BS, BS, True)])\n        with open(fn, \"rb\") as f:\n            try:\n                offset_hole = f.seek(0, os.SEEK_HOLE)\n                offset_data = f.seek(0, os.SEEK_DATA)\n            except OSError:\n                # No sparse support if these seeks do not work\n                return False\n        return offset_hole == 0 and offset_data == BS\n\n\nBS = 4096  # filesystem block size\n\n# Some sparse files. X = content blocks, _ = sparse blocks.\n# X__XXX____\nmap_sparse1 = [(0 * BS, 1 * BS, True), (1 * BS, 2 * BS, False), (3 * BS, 3 * BS, True), (6 * BS, 4 * BS, False)]\n\n# _XX___XXXX\nmap_sparse2 = [(0 * BS, 1 * BS, False), (1 * BS, 2 * BS, True), (3 * BS, 3 * BS, False), (6 * BS, 4 * BS, True)]\n\n# XXX\nmap_notsparse = [(0 * BS, 3 * BS, True)]\n\n# ___\nmap_onlysparse = [(0 * BS, 3 * BS, False)]\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/buzhash64_self_test.py",
    "content": "# Note: these tests are part of the self-test; do not use or import pytest functionality here.\n#       See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT.\n\nfrom io import BytesIO\n\nfrom ...chunkers import get_chunker\nfrom ...chunkers.buzhash64 import buzhash64, buzhash64_update, ChunkerBuzHash64\nfrom ...constants import *  # NOQA\nfrom ...helpers import hex_to_bin\nfrom .. import BaseTestCase\nfrom . import cf\n\n# from os.urandom(32)\nkey0 = hex_to_bin(\"ad9f89095817f0566337dc9ee292fcd59b70f054a8200151f1df5f21704824da\")\nkey1 = hex_to_bin(\"f1088c7e9e6ae83557ad1558ff36c44a369ea719d1081c29684f52ffccb72cb8\")\nkey2 = hex_to_bin(\"57174a65fde67fe127b18430525b50a58406f1bd6cc629535208c7832e181067\")\n\n\nclass ChunkerBuzHash64TestCase(BaseTestCase):\n    def test_chunkify64(self):\n        data = b\"0\" * int(1.5 * (1 << CHUNK_MAX_EXP)) + b\"Y\"\n        parts = cf(ChunkerBuzHash64(key0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(data)))\n        self.assert_equal(len(parts), 2)\n        self.assert_equal(b\"\".join(parts), data)\n        self.assert_equal(cf(ChunkerBuzHash64(key0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"\"))), [])\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarb\", b\"ooba\", b\"zf\", b\"oobarb\", b\"ooba\", b\"zf\", b\"oobarb\", b\"oobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key1, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"fo\", b\"oba\", b\"rb\", b\"oob\", b\"azf\", b\"ooba\", b\"rb\", b\"oob\", b\"azf\", b\"ooba\", b\"rb\", b\"oobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key2, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobar\", b\"booba\", b\"zfoobar\", b\"booba\", b\"zfoobar\", b\"boobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key0, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarbo\", b\"obaz\", b\"foobarbo\", b\"obaz\", b\"foobarbo\", b\"obaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key1, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarboob\", b\"azfoobarboob\", b\"azfoobarboobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key2, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foob\", b\"arboobazfoob\", b\"arboobazfoob\", b\"arboobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key0, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarbo\", b\"obazfoobarbo\", b\"obazfoobarbo\", b\"obaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key1, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarboob\", b\"azfoobarboob\", b\"azfoobarboobaz\"],\n        )\n        self.assert_equal(\n            cf(ChunkerBuzHash64(key2, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarboobazfoob\", b\"arboobazfoob\", b\"arboobaz\"],\n        )\n\n    def test_buzhash64(self):\n        self.assert_equal(buzhash64(b\"abcdefghijklmnop\", key0), 17414563089559790077)\n        self.assert_equal(buzhash64(b\"abcdefghijklmnop\", key1), 1397285894609271345)\n        expected = buzhash64(b\"abcdefghijklmnop\", key0)\n        previous = buzhash64(b\"Xabcdefghijklmno\", key0)\n        this = buzhash64_update(previous, ord(\"X\"), ord(\"p\"), 16, key0)\n        self.assert_equal(this, expected)\n        # Test with more than 63 bytes to make sure our barrel_shift macro works correctly\n        self.assert_equal(buzhash64(b\"abcdefghijklmnopqrstuvwxyz\" * 4, key0), 17683050804041322250)\n\n    def test_small_reads64(self):\n        class SmallReadFile:\n            input = b\"a\" * (20 + 1)\n\n            def read(self, nbytes):\n                self.input = self.input[:-1]\n                return self.input[:1]\n\n        chunker = get_chunker(*CHUNKER64_PARAMS, sparse=False)\n        reconstructed = b\"\".join(cf(chunker.chunkify(SmallReadFile())))\n        assert reconstructed == b\"a\" * 20\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/buzhash64_test.py",
    "content": "from hashlib import sha256\nfrom io import BytesIO\nimport os\nimport random\n\nimport pytest\n\nfrom . import cf, cf_expand\nfrom ...chunkers import ChunkerBuzHash64\nfrom ...chunkers.buzhash64 import buzhash64_get_table\nfrom ...constants import *  # NOQA\nfrom ...helpers import hex_to_bin\n\n\n# from os.urandom(32)\nkey0 = hex_to_bin(\"ad9f89095817f0566337dc9ee292fcd59b70f054a8200151f1df5f21704824da\")\nkey1 = hex_to_bin(\"f1088c7e9e6ae83557ad1558ff36c44a369ea719d1081c29684f52ffccb72cb8\")\n\n\ndef H(data):\n    return sha256(data).digest()\n\n\ndef test_chunkpoints64_unchanged():\n    def twist(size):\n        x = 1\n        a = bytearray(size)\n        for i in range(size):\n            x = (x * 1103515245 + 12345) & 0x7FFFFFFF\n            a[i] = x & 0xFF\n        return a\n\n    data = twist(100000)\n\n    runs = []\n    for winsize in (65, 129, HASH_WINDOW_SIZE, 7351):\n        for minexp in (4, 6, 7, 11, 12):\n            for maxexp in (15, 17):\n                if minexp >= maxexp:\n                    continue\n                for maskbits in (4, 7, 10, 12):\n                    for key in (key0, key1):\n                        fh = BytesIO(data)\n                        chunker = ChunkerBuzHash64(key, minexp, maxexp, maskbits, winsize)\n                        chunks = [H(c) for c in cf(chunker.chunkify(fh, -1))]\n                        runs.append(H(b\"\".join(chunks)))\n\n    # The \"correct\" hash below matches the existing chunker behavior.\n    # Future chunker optimizations must not change this, or existing repos will bloat.\n    overall_hash = H(b\"\".join(runs))\n    print(overall_hash.hex())\n    assert overall_hash == hex_to_bin(\"676676133fb3621ada0f6cc1b18002c3e37016c9469217d18f8e382fadaf23fd\")\n\n\ndef test_buzhash64_chunksize_distribution():\n    data = os.urandom(1048576)\n    min_exp, max_exp, mask = 10, 16, 14  # chunk size target 16 KiB, clip at 1 KiB and 64 KiB\n    chunker = ChunkerBuzHash64(key0, min_exp, max_exp, mask, 4095)\n    f = BytesIO(data)\n    chunks = cf(chunker.chunkify(f))\n    del chunks[-1]  # get rid of the last chunk, it can be smaller than 2**min_exp\n    chunk_sizes = [len(chunk) for chunk in chunks]\n    chunks_count = len(chunks)\n    min_chunksize_observed = min(chunk_sizes)\n    max_chunksize_observed = max(chunk_sizes)\n    min_count = sum(int(size == 2**min_exp) for size in chunk_sizes)\n    max_count = sum(int(size == 2**max_exp) for size in chunk_sizes)\n    print(\n        f\"count: {chunks_count} min: {min_chunksize_observed} max: {max_chunksize_observed} \"\n        f\"min count: {min_count} max count: {max_count}\"\n    )\n    # usually there will about 64 chunks\n    assert 32 < chunks_count < 128\n    # chunks always must be between min and max (clipping must work):\n    assert min_chunksize_observed >= 2**min_exp\n    assert max_chunksize_observed <= 2**max_exp\n    # most chunks should be cut due to buzhash triggering, not due to clipping at min/max size:\n    assert min_count < 10\n    assert max_count < 10\n\n\ndef test_buzhash64_table():\n    # Test that the function returns a list of 256 integers\n    table0 = buzhash64_get_table(key0)\n    assert len(table0) == 256\n\n    # Test that all elements are integers\n    for value in table0:\n        assert isinstance(value, int)\n\n    # Test that the function is deterministic (same key produces same table)\n    table0_again = buzhash64_get_table(key0)\n    assert table0 == table0_again\n\n    # Test that different keys produce different tables\n    table1 = buzhash64_get_table(key1)\n    assert table0 != table1\n\n    # Test that the table has balanced bit distribution\n    # For each bit position 0..63, exactly 50% of the table values should have the bit set to 1\n    for bit_pos in range(64):\n        bit_count = sum(1 for value in table0 if value & (1 << bit_pos))\n        assert bit_count == 128  # 50% of 256 = 128\n\n\n@pytest.mark.skipif(\"BORG_TESTS_SLOW\" not in os.environ, reason=\"slow tests not enabled, use BORG_TESTS_SLOW=1\")\n@pytest.mark.parametrize(\"worker\", range(os.cpu_count() or 1))\ndef test_fuzz_bh64(worker):\n    # Fuzz buzhash64 with random and uniform data of misc. sizes and misc keys.\n    def rnd_key():\n        return os.urandom(32)\n\n    # decompose CHUNKER64_PARAMS = (algo, min_exp, max_exp, mask_bits, window_size)\n    algo, min_exp, max_exp, mask_bits, win_size = CHUNKER64_PARAMS\n    assert algo == CH_BUZHASH64  # default chunker must be buzhash64 here\n\n    keys = [b\"\\0\" * 32] + [rnd_key() for _ in range(10)]\n    sizes = [random.randint(1, 4 * 1024 * 1024) for _ in range(50)]\n\n    for key in keys:\n        chunker = ChunkerBuzHash64(key, min_exp, max_exp, mask_bits, win_size)\n        for size in sizes:\n            # Random data\n            data = os.urandom(size)\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-same data (non-zero)\n            data = b\"\\x42\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-zero data\n            data = b\"\\x00\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/buzhash_self_test.py",
    "content": "# Note: these tests are part of the self-test; do not use or import pytest functionality here.\n#       See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT.\n\nfrom io import BytesIO\n\nfrom ...chunkers import get_chunker\nfrom ...chunkers.buzhash import buzhash, buzhash_update, Chunker\nfrom ...constants import *  # NOQA\nfrom .. import BaseTestCase\nfrom . import cf\n\n\nclass ChunkerTestCase(BaseTestCase):\n    def test_chunkify(self):\n        data = b\"0\" * int(1.5 * (1 << CHUNK_MAX_EXP)) + b\"Y\"\n        parts = cf(Chunker(0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(data)))\n        self.assert_equal(len(parts), 2)\n        self.assert_equal(b\"\".join(parts), data)\n        self.assert_equal(cf(Chunker(0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"\"))), [])\n        self.assert_equal(\n            cf(Chunker(0, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"fooba\", b\"rboobaz\", b\"fooba\", b\"rboobaz\", b\"fooba\", b\"rboobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(1, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"fo\", b\"obarb\", b\"oob\", b\"azf\", b\"oobarb\", b\"oob\", b\"azf\", b\"oobarb\", b\"oobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(2, 1, CHUNK_MAX_EXP, 2, 2).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foob\", b\"ar\", b\"boobazfoob\", b\"ar\", b\"boobazfoob\", b\"ar\", b\"boobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(0, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))), [b\"foobarboobaz\" * 3]\n        )\n        self.assert_equal(\n            cf(Chunker(1, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobar\", b\"boobazfo\", b\"obar\", b\"boobazfo\", b\"obar\", b\"boobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(2, 2, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foob\", b\"arboobaz\", b\"foob\", b\"arboobaz\", b\"foob\", b\"arboobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(0, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))), [b\"foobarboobaz\" * 3]\n        )\n        self.assert_equal(\n            cf(Chunker(1, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarbo\", b\"obazfoobar\", b\"boobazfo\", b\"obarboobaz\"],\n        )\n        self.assert_equal(\n            cf(Chunker(2, 3, CHUNK_MAX_EXP, 2, 3).chunkify(BytesIO(b\"foobarboobaz\" * 3))),\n            [b\"foobarboobaz\", b\"foobarboobaz\", b\"foobarboobaz\"],\n        )\n\n    def test_buzhash(self):\n        self.assert_equal(buzhash(b\"abcdefghijklmnop\", 0), 3795437769)\n        self.assert_equal(buzhash(b\"abcdefghijklmnop\", 1), 3795400502)\n        self.assert_equal(\n            buzhash(b\"abcdefghijklmnop\", 1), buzhash_update(buzhash(b\"Xabcdefghijklmno\", 1), ord(\"X\"), ord(\"p\"), 16, 1)\n        )\n        # Test with more than 31 bytes to make sure our barrel_shift macro works correctly\n        self.assert_equal(buzhash(b\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\", 0), 566521248)\n\n    def test_small_reads(self):\n        class SmallReadFile:\n            input = b\"a\" * (20 + 1)\n\n            def read(self, nbytes):\n                self.input = self.input[:-1]\n                return self.input[:1]\n\n        chunker = get_chunker(*CHUNKER_PARAMS, sparse=False)\n        reconstructed = b\"\".join(cf(chunker.chunkify(SmallReadFile())))\n        assert reconstructed == b\"a\" * 20\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/buzhash_test.py",
    "content": "from hashlib import sha256\nfrom io import BytesIO\nimport os\nimport random\n\nimport pytest\n\nfrom . import cf, cf_expand\nfrom ...chunkers import Chunker\nfrom ...constants import *  # NOQA\nfrom ...helpers import hex_to_bin\n\n\ndef H(data):\n    return sha256(data).digest()\n\n\ndef test_chunkpoints_unchanged():\n    def twist(size):\n        x = 1\n        a = bytearray(size)\n        for i in range(size):\n            x = (x * 1103515245 + 12345) & 0x7FFFFFFF\n            a[i] = x & 0xFF\n        return a\n\n    data = twist(100000)\n\n    runs = []\n    for winsize in (65, 129, HASH_WINDOW_SIZE, 7351):\n        for minexp in (4, 6, 7, 11, 12):\n            for maxexp in (15, 17):\n                if minexp >= maxexp:\n                    continue\n                for maskbits in (4, 7, 10, 12):\n                    for seed in (1849058162, 1234567653):\n                        fh = BytesIO(data)\n                        chunker = Chunker(seed, minexp, maxexp, maskbits, winsize)\n                        chunks = [H(c) for c in cf(chunker.chunkify(fh, -1))]\n                        runs.append(H(b\"\".join(chunks)))\n\n    # The \"correct\" hash below matches the existing chunker behavior.\n    # Future chunker optimizations must not change this, or existing repos will bloat.\n    overall_hash = H(b\"\".join(runs))\n    assert overall_hash == hex_to_bin(\"a43d0ecb3ae24f38852fcc433a83dacd28fe0748d09cc73fc11b69cf3f1a7299\")\n\n\ndef test_buzhash_chunksize_distribution():\n    data = os.urandom(1048576)\n    min_exp, max_exp, mask = 10, 16, 14  # chunk size target 16 KiB, clip at 1 KiB and 64 KiB\n    chunker = Chunker(0, min_exp, max_exp, mask, 4095)\n    f = BytesIO(data)\n    chunks = cf(chunker.chunkify(f))\n    del chunks[-1]  # get rid of the last chunk, it can be smaller than 2**min_exp\n    chunk_sizes = [len(chunk) for chunk in chunks]\n    chunks_count = len(chunks)\n    min_chunksize_observed = min(chunk_sizes)\n    max_chunksize_observed = max(chunk_sizes)\n    min_count = sum(int(size == 2**min_exp) for size in chunk_sizes)\n    max_count = sum(int(size == 2**max_exp) for size in chunk_sizes)\n    print(\n        f\"count: {chunks_count} min: {min_chunksize_observed} max: {max_chunksize_observed} \"\n        f\"min count: {min_count} max count: {max_count}\"\n    )\n    # usually there will be about 64 chunks\n    assert 32 < chunks_count < 128\n    # chunks always must be between min and max (clipping must work):\n    assert min_chunksize_observed >= 2**min_exp\n    assert max_chunksize_observed <= 2**max_exp\n    # most chunks should be cut due to buzhash triggering, not due to clipping at min/max size:\n    assert min_count < 10\n    assert max_count < 10\n\n\n@pytest.mark.skipif(\"BORG_TESTS_SLOW\" not in os.environ, reason=\"slow tests not enabled, use BORG_TESTS_SLOW=1\")\n@pytest.mark.parametrize(\"worker\", range(os.cpu_count() or 1))\ndef test_fuzz_buzhash(worker):\n    # Fuzz the default chunker (buzhash) with random and uniform data of misc. sizes and seeds 0 or random int32 values.\n    def rnd_int32():\n        uint = random.getrandbits(32)\n        return uint if uint < 2**31 else uint - 2**32\n\n    # decompose CHUNKER_PARAMS = (algo, min_exp, max_exp, mask_bits, window_size)\n    algo, min_exp, max_exp, mask_bits, win_size = CHUNKER_PARAMS\n    assert algo == CH_BUZHASH  # default chunker must be buzhash here\n\n    seeds = [0] + [rnd_int32() for _ in range(50)]\n    sizes = [random.randint(1, 4 * 1024 * 1024) for _ in range(50)]\n\n    for seed in seeds:\n        chunker = Chunker(seed, min_exp, max_exp, mask_bits, win_size)\n        for size in sizes:\n            # Random data\n            data = os.urandom(size)\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-same data (non-zero)\n            data = b\"\\x42\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-zero data\n            data = b\"\\x00\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/failing_test.py",
    "content": "from io import BytesIO\n\nimport pytest\n\nfrom ...chunkers import ChunkerFailing\nfrom ...constants import *  # NOQA\n\n\ndef test_chunker_failing():\n    SIZE = 4096\n    data = bytes(2 * SIZE + 1000)\n    chunker = ChunkerFailing(SIZE, \"rEErrr\")  # cut <SIZE> chunks, start failing at block 1, fail 2 times\n    with BytesIO(data) as fd:\n        ch = chunker.chunkify(fd)\n        c1 = next(ch)  # block 0: ok\n        assert c1.meta[\"allocation\"] == CH_DATA\n        assert c1.data == data[:SIZE]\n        with pytest.raises(OSError):  # block 1: failure 1\n            next(ch)\n    with BytesIO(data) as fd:\n        ch = chunker.chunkify(fd)\n        with pytest.raises(OSError):  # block 2: failure 2\n            next(ch)\n    with BytesIO(data) as fd:\n        ch = chunker.chunkify(fd)\n        c1 = next(ch)  # block 3: success!\n        c2 = next(ch)  # block 4: success!\n        c3 = next(ch)  # block 5: success!\n        assert c1.meta[\"allocation\"] == c2.meta[\"allocation\"] == c3.meta[\"allocation\"] == CH_DATA\n        assert c1.data == data[:SIZE]\n        assert c2.data == data[SIZE : 2 * SIZE]\n        assert c3.data == data[2 * SIZE :]\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/fixed_self_test.py",
    "content": "# Note: these tests are part of the self-test; do not use or import pytest functionality here.\n#       See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT.\n\nfrom io import BytesIO\n\nfrom ...chunkers.fixed import ChunkerFixed\nfrom ...constants import *  # NOQA\nfrom .. import BaseTestCase\nfrom . import cf\n\n\nclass ChunkerFixedTestCase(BaseTestCase):\n    def test_chunkify_just_blocks(self):\n        data = b\"foobar\" * 1500\n        chunker = ChunkerFixed(4096)\n        parts = cf(chunker.chunkify(BytesIO(data)))\n        self.assert_equal(parts, [data[0:4096], data[4096:8192], data[8192:]])\n\n    def test_chunkify_header_and_blocks(self):\n        data = b\"foobar\" * 1500\n        chunker = ChunkerFixed(4096, 123)\n        parts = cf(chunker.chunkify(BytesIO(data)))\n        self.assert_equal(\n            parts, [data[0:123], data[123 : 123 + 4096], data[123 + 4096 : 123 + 8192], data[123 + 8192 :]]\n        )\n\n    def test_chunkify_just_blocks_fmap_complete(self):\n        data = b\"foobar\" * 1500\n        chunker = ChunkerFixed(4096)\n        fmap = [(0, 4096, True), (4096, 8192, True), (8192, 99999999, True)]\n        parts = cf(chunker.chunkify(BytesIO(data), fmap=fmap))\n        self.assert_equal(parts, [data[0:4096], data[4096:8192], data[8192:]])\n\n    def test_chunkify_header_and_blocks_fmap_complete(self):\n        data = b\"foobar\" * 1500\n        chunker = ChunkerFixed(4096, 123)\n        fmap = [(0, 123, True), (123, 4096, True), (123 + 4096, 4096, True), (123 + 8192, 4096, True)]\n        parts = cf(chunker.chunkify(BytesIO(data), fmap=fmap))\n        self.assert_equal(\n            parts, [data[0:123], data[123 : 123 + 4096], data[123 + 4096 : 123 + 8192], data[123 + 8192 :]]\n        )\n\n    def test_chunkify_header_and_blocks_fmap_zeros(self):\n        data = b\"H\" * 123 + b\"_\" * 4096 + b\"X\" * 4096 + b\"_\" * 4096\n        chunker = ChunkerFixed(4096, 123)\n        fmap = [(0, 123, True), (123, 4096, False), (123 + 4096, 4096, True), (123 + 8192, 4096, False)]\n        parts = cf(chunker.chunkify(BytesIO(data), fmap=fmap))\n        # Because we marked the '_' ranges as holes, we will get hole ranges instead!\n        self.assert_equal(parts, [data[0:123], 4096, data[123 + 4096 : 123 + 8192], 4096])\n\n    def test_chunkify_header_and_blocks_fmap_partial(self):\n        data = b\"H\" * 123 + b\"_\" * 4096 + b\"X\" * 4096 + b\"_\" * 4096\n        chunker = ChunkerFixed(4096, 123)\n        fmap = [\n            (0, 123, True),\n            # (123, 4096, False),\n            (123 + 4096, 4096, True),\n            # (123+8192, 4096, False),\n        ]\n        parts = cf(chunker.chunkify(BytesIO(data), fmap=fmap))\n        # Because we left out the '_' ranges from the fmap, we will not get them at all!\n        self.assert_equal(parts, [data[0:123], data[123 + 4096 : 123 + 8192]])\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/fixed_test.py",
    "content": "from io import BytesIO\nimport os\nimport random\n\nimport pytest\n\nfrom . import cf, cf_expand, make_sparsefile, make_content, fs_supports_sparse\nfrom . import BS, map_sparse1, map_sparse2, map_onlysparse, map_notsparse\nfrom ...chunkers import ChunkerFixed\nfrom ...constants import *  # NOQA\n\n\n@pytest.mark.skipif(not fs_supports_sparse(), reason=\"filesystem does not support sparse files\")\n@pytest.mark.parametrize(\n    \"fname, sparse_map, header_size, sparse\",\n    [\n        (\"sparse1\", map_sparse1, 0, False),\n        (\"sparse1\", map_sparse1, 0, True),\n        (\"sparse1\", map_sparse1, BS, False),\n        (\"sparse1\", map_sparse1, BS, True),\n        (\"sparse2\", map_sparse2, 0, False),\n        (\"sparse2\", map_sparse2, 0, True),\n        (\"sparse2\", map_sparse2, BS, False),\n        (\"sparse2\", map_sparse2, BS, True),\n        (\"onlysparse\", map_onlysparse, 0, False),\n        (\"onlysparse\", map_onlysparse, 0, True),\n        (\"onlysparse\", map_onlysparse, BS, False),\n        (\"onlysparse\", map_onlysparse, BS, True),\n        (\"notsparse\", map_notsparse, 0, False),\n        (\"notsparse\", map_notsparse, 0, True),\n        (\"notsparse\", map_notsparse, BS, False),\n        (\"notsparse\", map_notsparse, BS, True),\n    ],\n)\ndef test_chunkify_sparse(tmpdir, fname, sparse_map, header_size, sparse):\n    def get_chunks(fname, sparse, header_size):\n        chunker = ChunkerFixed(4096, header_size=header_size, sparse=sparse)\n        with open(fname, \"rb\") as fd:\n            return cf(chunker.chunkify(fd))\n\n    fn = str(tmpdir / fname)\n    make_sparsefile(fn, sparse_map, header_size=header_size)\n    get_chunks(fn, sparse=sparse, header_size=header_size) == make_content(sparse_map, header_size=header_size)\n\n\n@pytest.mark.skipif(\"BORG_TESTS_SLOW\" not in os.environ, reason=\"slow tests not enabled, use BORG_TESTS_SLOW=1\")\n@pytest.mark.parametrize(\"worker\", range(os.cpu_count() or 1))\ndef test_fuzz_fixed(worker):\n    # Fuzz fixed chunker with random and uniform data of misc. sizes.\n    sizes = [random.randint(1, 4 * 1024 * 1024) for _ in range(50)]\n\n    for block_size, header_size in [(1024, 64), (1234, 0), (4321, 123)]:\n        chunker = ChunkerFixed(block_size, header_size)\n        for size in sizes:\n            # Random data\n            data = os.urandom(size)\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-same data (non-zero)\n            data = b\"\\x42\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n\n            # All-zero data\n            data = b\"\\x00\" * size\n            with BytesIO(data) as bio:\n                parts = cf_expand(chunker.chunkify(bio))\n            reconstructed = b\"\".join(parts)\n            assert reconstructed == data\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/interaction_test.py",
    "content": "import os\nimport pytest\nfrom io import BytesIO\n\nfrom ...chunkers import get_chunker\nfrom ...constants import *  # NOQA\n\n\n@pytest.mark.parametrize(\n    \"chunker_params\",\n    [\n        (CH_FIXED, 1048576, 0),  # == reader_block_size\n        (CH_FIXED, 1048576 // 2, 0),  # reader_block_size / N\n        (CH_FIXED, 1048576 * 2, 0),  # N * reader_block_size\n        (CH_FIXED, 1234567, 0),  # does not fit well, larger than reader_block_size\n        (CH_FIXED, 123456, 0),  # does not fit well, smaller than reader_block_size\n        (CH_BUZHASH, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE),\n        (CH_BUZHASH64, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE),\n    ],\n)\ndef test_reader_chunker_interaction(chunker_params):\n    \"\"\"\n    Test that chunking random/zero data produces chunks that can be reassembled to match the original data.\n\n    If one of these fails, there is likely a problem with buffer management.\n    \"\"\"\n    # Generate some data\n    data_size = 6 * 12341234\n    random_data = os.urandom(data_size // 3) + b\"\\0\" * (data_size // 3) + os.urandom(data_size // 3)\n\n    # Chunk the data\n    chunker = get_chunker(*chunker_params)\n    data_file = BytesIO(random_data)\n    chunks = list(chunker.chunkify(data_file))\n\n    data_chunks = 0\n    hole_chunks = 0\n    alloc_chunks = 0\n    for chunk in chunks:\n        if chunk.meta[\"allocation\"] == CH_DATA:\n            data_chunks += 1\n        elif chunk.meta[\"allocation\"] == CH_HOLE:\n            hole_chunks += 1\n        elif chunk.meta[\"allocation\"] == CH_ALLOC:\n            alloc_chunks += 1\n\n    assert data_chunks > 0, \"No data chunks found\"\n    assert alloc_chunks > 0, \"No alloc chunks found\"\n    assert hole_chunks == 0, \"Hole chunks found, this is not expected!\"\n\n    # Reassemble the chunks\n    reassembled = BytesIO()\n    for i, chunk in enumerate(chunks):\n        if chunk.meta[\"allocation\"] == CH_DATA:\n            # For data chunks, write the actual data\n            reassembled.write(bytes(chunk.data))\n        elif chunk.meta[\"allocation\"] in (CH_HOLE, CH_ALLOC):\n            # For hole or alloc chunks, write zeros\n            reassembled.write(b\"\\0\" * chunk.meta[\"size\"])\n\n    # Check that the reassembled data has the correct size\n    reassembled_size = reassembled.tell()\n    assert (\n        reassembled_size == data_size\n    ), f\"Reassembled data size ({reassembled_size}) does not equal original data size ({data_size})\"\n\n    # Verify that the reassembled data matches the original data\n    reassembled.seek(0)\n    reassembled_data = reassembled.read()\n    assert reassembled_data == random_data, \"Reassembled data does not match original data\"\n"
  },
  {
    "path": "src/borg/testsuite/chunkers/reader_test.py",
    "content": "import os\nfrom io import BytesIO\n\nimport pytest\n\nfrom . import make_sparsefile, fs_supports_sparse\nfrom . import BS, map_sparse1, map_sparse2, map_onlysparse, map_notsparse\nfrom ...chunkers import sparsemap, FileReader, FileFMAPReader, Chunk\nfrom ...constants import *  # NOQA\n\n\n@pytest.mark.skipif(not fs_supports_sparse(), reason=\"filesystem does not support sparse files\")\n@pytest.mark.parametrize(\n    \"fname, sparse_map\",\n    [(\"sparse1\", map_sparse1), (\"sparse2\", map_sparse2), (\"onlysparse\", map_onlysparse), (\"notsparse\", map_notsparse)],\n)\ndef test_sparsemap(tmpdir, fname, sparse_map):\n    def get_sparsemap_fh(fname):\n        fh = os.open(fname, flags=os.O_RDONLY)\n        try:\n            return list(sparsemap(fh=fh))\n        finally:\n            os.close(fh)\n\n    def get_sparsemap_fd(fname):\n        with open(fname, \"rb\") as fd:\n            return list(sparsemap(fd=fd))\n\n    fn = str(tmpdir / fname)\n    make_sparsefile(fn, sparse_map)\n    assert get_sparsemap_fh(fn) == sparse_map\n    assert get_sparsemap_fd(fn) == sparse_map\n\n\n@pytest.mark.parametrize(\n    \"file_content, read_size, expected_data, expected_allocation, expected_size\",\n    [\n        # Empty file\n        (b\"\", 1024, b\"\", CH_DATA, 0),\n        # Small data\n        (b\"data\", 1024, b\"data\", CH_DATA, 4),\n        # More data than read_size\n        (b\"data\", 2, b\"da\", CH_DATA, 2),\n    ],\n)\ndef test_filereader_read_simple(file_content, read_size, expected_data, expected_allocation, expected_size):\n    \"\"\"Test read with different file contents.\"\"\"\n    reader = FileReader(fd=BytesIO(file_content), fh=-1, read_size=1024, sparse=False, fmap=None)\n    chunk = reader.read(read_size)\n    assert chunk.data == expected_data\n    assert chunk.meta[\"allocation\"] == expected_allocation\n    assert chunk.meta[\"size\"] == expected_size\n\n\n@pytest.mark.parametrize(\n    \"file_content, read_sizes, expected_results\",\n    [\n        # Partial data read\n        (\n            b\"data1234\",\n            [4, 4],\n            [{\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4}, {\"data\": b\"1234\", \"allocation\": CH_DATA, \"size\": 4}],\n        ),\n        # Multiple calls with EOF\n        (\n            b\"0123456789\",\n            [4, 4, 4, 4],\n            [\n                {\"data\": b\"0123\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"4567\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"89\", \"allocation\": CH_DATA, \"size\": 2},\n                {\"data\": b\"\", \"allocation\": CH_DATA, \"size\": 0},\n            ],\n        ),\n    ],\n)\ndef test_filereader_read_multiple(file_content, read_sizes, expected_results):\n    \"\"\"Test multiple read calls with different file contents.\"\"\"\n    reader = FileReader(fd=BytesIO(file_content), fh=-1, read_size=1024, sparse=False, fmap=None)\n\n    for i, read_size in enumerate(read_sizes):\n        chunk = reader.read(read_size)\n        assert chunk.data == expected_results[i][\"data\"]\n        assert chunk.meta[\"allocation\"] == expected_results[i][\"allocation\"]\n        assert chunk.meta[\"size\"] == expected_results[i][\"size\"]\n\n\n@pytest.mark.parametrize(\n    \"mock_chunks, read_size, expected_data, expected_allocation, expected_size\",\n    [\n        # Multiple chunks with mixed types\n        (\n            [\n                Chunk(b\"chunk1\", size=6, allocation=CH_DATA),\n                Chunk(None, size=4, allocation=CH_HOLE),\n                Chunk(b\"chunk2\", size=6, allocation=CH_DATA),\n            ],\n            16,\n            b\"chunk1\" + b\"\\0\" * 4 + b\"chunk2\",\n            CH_DATA,\n            16,\n        ),\n        # Mixed allocation types (hole and alloc)\n        ([Chunk(None, size=4, allocation=CH_HOLE), Chunk(None, size=4, allocation=CH_ALLOC)], 8, None, CH_HOLE, 8),\n        # All alloc chunks\n        ([Chunk(None, size=4, allocation=CH_ALLOC), Chunk(None, size=4, allocation=CH_ALLOC)], 8, None, CH_ALLOC, 8),\n        # All hole chunks\n        ([Chunk(None, size=4, allocation=CH_HOLE), Chunk(None, size=4, allocation=CH_HOLE)], 8, None, CH_HOLE, 8),\n    ],\n)\ndef test_filereader_read_with_mock(mock_chunks, read_size, expected_data, expected_allocation, expected_size):\n    \"\"\"Test read with a mock FileFMAPReader.\"\"\"\n\n    # Create a mock FileFMAPReader that yields specific chunks\n    class MockFileFMAPReader:\n        def __init__(self, chunks):\n            self.chunks = chunks\n            self.index = 0\n            # Add required attributes to satisfy FileReader\n            self.reading_time = 0.0\n\n        def blockify(self):\n            for chunk in self.chunks:\n                yield chunk\n\n    # Create a FileReader with a dummy BytesIO to satisfy the assertion\n    reader = FileReader(fd=BytesIO(b\"\"), fh=-1, read_size=1024, sparse=False, fmap=None)\n    # Replace the reader with our mock\n    reader.reader = MockFileFMAPReader(mock_chunks)\n    reader.blockify_gen = reader.reader.blockify()\n\n    # Read all chunks at once\n    chunk = reader.read(read_size)\n\n    # Check the result\n    assert chunk.data == expected_data\n    assert chunk.meta[\"allocation\"] == expected_allocation\n    assert chunk.meta[\"size\"] == expected_size\n\n\n@pytest.mark.parametrize(\n    \"file_content, read_size, expected_chunks\",\n    [\n        # Empty file\n        (b\"\", 1024, []),\n        # Small data\n        (b\"data\", 1024, [{\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4}]),\n        # Data larger than read_size\n        (\n            b\"0123456789\",\n            4,\n            [\n                {\"data\": b\"0123\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"4567\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"89\", \"allocation\": CH_DATA, \"size\": 2},\n            ],\n        ),\n        # Data with zeros (should be detected as allocated zeros)\n        (\n            b\"data\" + b\"\\0\" * 8 + b\"more\",\n            4,\n            [\n                {\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": None, \"allocation\": CH_ALLOC, \"size\": 4},\n                {\"data\": None, \"allocation\": CH_ALLOC, \"size\": 4},\n                {\"data\": b\"more\", \"allocation\": CH_DATA, \"size\": 4},\n            ],\n        ),\n    ],\n)\ndef test_filefmapreader_basic(file_content, read_size, expected_chunks):\n    \"\"\"Test basic functionality of FileFMAPReader with different file contents.\"\"\"\n    reader = FileFMAPReader(fd=BytesIO(file_content), fh=-1, read_size=read_size, sparse=False, fmap=None)\n\n    # Collect all chunks from blockify\n    chunks = list(reader.blockify())\n\n    # Check the number of chunks\n    assert len(chunks) == len(expected_chunks)\n\n    # Check each chunk\n    for i, chunk in enumerate(chunks):\n        assert chunk.data == expected_chunks[i][\"data\"]\n        assert chunk.meta[\"allocation\"] == expected_chunks[i][\"allocation\"]\n        assert chunk.meta[\"size\"] == expected_chunks[i][\"size\"]\n\n\n@pytest.mark.parametrize(\n    \"file_content, fmap, read_size, expected_chunks\",\n    [\n        # Custom fmap with data and holes\n        (\n            b\"dataXXXXmore\",\n            [(0, 4, True), (4, 4, False), (8, 4, True)],\n            4,\n            [\n                {\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": None, \"allocation\": CH_HOLE, \"size\": 4},\n                {\"data\": b\"more\", \"allocation\": CH_DATA, \"size\": 4},\n            ],\n        ),\n        # Custom fmap with only holes\n        (\n            b\"\\0\\0\\0\\0\\0\\0\\0\\0\",\n            [(0, 8, False)],\n            4,\n            [{\"data\": None, \"allocation\": CH_HOLE, \"size\": 4}, {\"data\": None, \"allocation\": CH_HOLE, \"size\": 4}],\n        ),\n        # Custom fmap with only data\n        (\n            b\"datadata\",\n            [(0, 8, True)],\n            4,\n            [{\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4}, {\"data\": b\"data\", \"allocation\": CH_DATA, \"size\": 4}],\n        ),\n        # Custom fmap with partial coverage (should seek to the right position)\n        (\n            b\"skipthispartreadthispart\",\n            [(12, 12, True)],\n            4,\n            [\n                {\"data\": b\"read\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"this\", \"allocation\": CH_DATA, \"size\": 4},\n                {\"data\": b\"part\", \"allocation\": CH_DATA, \"size\": 4},\n            ],\n        ),\n    ],\n)\ndef test_filefmapreader_with_fmap(file_content, fmap, read_size, expected_chunks):\n    \"\"\"Test FileFMAPReader with an externally provided file map.\"\"\"\n    reader = FileFMAPReader(fd=BytesIO(file_content), fh=-1, read_size=read_size, sparse=False, fmap=fmap)\n\n    # Collect all chunks from blockify\n    chunks = list(reader.blockify())\n\n    # Check the number of chunks\n    assert len(chunks) == len(expected_chunks)\n\n    # Check each chunk\n    for i, chunk in enumerate(chunks):\n        assert chunk.data == expected_chunks[i][\"data\"]\n        assert chunk.meta[\"allocation\"] == expected_chunks[i][\"allocation\"]\n        assert chunk.meta[\"size\"] == expected_chunks[i][\"size\"]\n\n\n@pytest.mark.parametrize(\n    \"zeros_length, read_size, expected_allocation\",\n    [(4, 4, CH_ALLOC), (8192, 4096, CH_ALLOC)],  # Small block of zeros  # Large block of zeros\n)\ndef test_filefmapreader_allocation_types(zeros_length, read_size, expected_allocation):\n    \"\"\"Test FileFMAPReader's handling of different allocation types.\"\"\"\n    # Create a file with all zeros\n    file_content = b\"\\0\" * zeros_length\n\n    reader = FileFMAPReader(fd=BytesIO(file_content), fh=-1, read_size=read_size, sparse=False, fmap=None)\n\n    # Collect all chunks from blockify\n    chunks = list(reader.blockify())\n\n    # Check that all chunks are of the expected allocation type\n    for chunk in chunks:\n        assert chunk.meta[\"allocation\"] == expected_allocation\n        assert chunk.data is None  # All-zero data should be None\n\n\n@pytest.mark.skipif(not fs_supports_sparse(), reason=\"fs does not support sparse files\")\ndef test_filefmapreader_with_real_sparse_file(tmpdir):\n    \"\"\"Test FileFMAPReader with a real sparse file.\"\"\"\n    # Create a sparse file\n    fn = str(tmpdir / \"sparse_file\")\n    sparse_map = [(0, BS, True), (BS, 2 * BS, False), (3 * BS, BS, True)]\n    make_sparsefile(fn, sparse_map)\n\n    # Expected chunks when reading with sparse=True\n    expected_chunks_sparse = [\n        {\"data_type\": bytes, \"allocation\": CH_DATA, \"size\": BS},\n        {\"data_type\": type(None), \"allocation\": CH_HOLE, \"size\": BS},\n        {\"data_type\": type(None), \"allocation\": CH_HOLE, \"size\": BS},\n        {\"data_type\": bytes, \"allocation\": CH_DATA, \"size\": BS},\n    ]\n\n    # Expected chunks when reading with sparse=False.\n    # Even though it is not differentiating data vs hole ranges, it still\n    # transforms detected all-zero blocks to CH_ALLOC chunks.\n    expected_chunks_non_sparse = [\n        {\"data_type\": bytes, \"allocation\": CH_DATA, \"size\": BS},\n        {\"data_type\": type(None), \"allocation\": CH_ALLOC, \"size\": BS},\n        {\"data_type\": type(None), \"allocation\": CH_ALLOC, \"size\": BS},\n        {\"data_type\": bytes, \"allocation\": CH_DATA, \"size\": BS},\n    ]\n\n    # Test with sparse=True\n    with open(fn, \"rb\") as fd:\n        reader = FileFMAPReader(fd=fd, fh=-1, read_size=BS, sparse=True, fmap=None)\n        chunks = list(reader.blockify())\n\n        assert len(chunks) == len(expected_chunks_sparse)\n        for i, chunk in enumerate(chunks):\n            assert isinstance(chunk.data, expected_chunks_sparse[i][\"data_type\"])\n            assert chunk.meta[\"allocation\"] == expected_chunks_sparse[i][\"allocation\"]\n            assert chunk.meta[\"size\"] == expected_chunks_sparse[i][\"size\"]\n\n    # Test with sparse=False\n    with open(fn, \"rb\") as fd:\n        reader = FileFMAPReader(fd=fd, fh=-1, read_size=BS, sparse=False, fmap=None)\n        chunks = list(reader.blockify())\n\n        assert len(chunks) == len(expected_chunks_non_sparse)\n        for i, chunk in enumerate(chunks):\n            assert isinstance(chunk.data, expected_chunks_non_sparse[i][\"data_type\"])\n            assert chunk.meta[\"allocation\"] == expected_chunks_non_sparse[i][\"allocation\"]\n            assert chunk.meta[\"size\"] == expected_chunks_non_sparse[i][\"size\"]\n\n\ndef test_filefmapreader_build_fmap():\n    \"\"\"Test FileFMAPReader's _build_fmap method.\"\"\"\n    # Create a reader with sparse=False\n    reader = FileFMAPReader(fd=BytesIO(b\"data\"), fh=-1, read_size=4, sparse=False, fmap=None)\n\n    # Call _build_fmap\n    fmap = reader._build_fmap()\n\n    # Check that a default fmap is created\n    assert len(fmap) == 1\n    assert fmap[0][0] == 0  # start\n    assert fmap[0][1] == 2**62  # size\n    assert fmap[0][2] is True  # is_data\n"
  },
  {
    "path": "src/borg/testsuite/cockpit_test.py",
    "content": "import asyncio\nimport subprocess\n\nimport pytest\n\nfrom borg.platformflags import is_freebsd, is_win32\n\ntry:\n    from borg.cockpit.app import BorgCockpitApp\n\n    have_cockpit = True\nexcept ImportError:\n    have_cockpit = False\n\npytestmark = pytest.mark.skipif(not have_cockpit, reason=\"can not import BorgCockpitApp, is textual installed?\")\n\n\ndef test_cockpit_app_create_archive(tmp_path):\n    if not (is_freebsd or is_win32):\n        pytest.skip(\"this slow test shall only run on FreeBSD and Windows\")\n    repo_path = tmp_path / \"repo\"\n    input_path = tmp_path / \"input\"\n    input_path.mkdir()\n    for i in range(5000):\n        (input_path / f\"test{i}.txt\").write_text(f\"content {i}\")\n\n    subprocess.run([\"borg\", \"-r\", str(repo_path), \"repo-create\", \"--encryption\", \"none\"], check=True)\n\n    async def run():\n        app = BorgCockpitApp()\n        app.borg_args = [\"-r\", str(repo_path), \"create\", \"--list\", \"test\", str(input_path)]\n\n        async with app.run_test() as pilot:\n            assert \"BorgBackup\" in app.TITLE\n            assert app.is_running\n\n            # Wait for process to finish\n            while getattr(app, \"process_running\", True):\n                await pilot.pause(0.1)\n\n            status_panel = pilot.app.query_one(\"#status\")\n            assert status_panel.rc == 0\n\n            assert app.total_lines_processed > 0\n\n            await pilot.press(\"q\")  # quit app\n\n    asyncio.run(run())\n"
  },
  {
    "path": "src/borg/testsuite/compress_test.py",
    "content": "import os\nimport zlib\n\nimport pytest\n\nfrom ..compress import get_compressor, Compressor, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto\nfrom ..helpers import CompressionSpec\nfrom ..constants import ROBJ_FILE_STREAM, ROBJ_ARCHIVE_META\nfrom ..helpers.argparsing import ArgumentTypeError\n\nDATA = b\"fooooooooobaaaaaaaar\" * 10\nparams = dict(name=\"zlib\", level=6)\n\n\n@pytest.mark.parametrize(\n    \"c_type, expected_compressor\",\n    [(\"none\", CNONE), (\"lz4\", LZ4), (\"zlib\", ZLIB), (\"lzma\", LZMA), (\"zstd\", ZSTD), (\"foobar\", None)],\n)\ndef test_get_compressor(c_type, expected_compressor):\n    if expected_compressor is not None:\n        compressor = get_compressor(name=c_type)\n        assert isinstance(compressor, expected_compressor)\n    else:\n        with pytest.raises(KeyError):\n            get_compressor(name=c_type)\n\n\n@pytest.mark.parametrize(\"c_type\", [\"none\", \"lz4\", \"zlib\", \"zstd\", \"lzma\"])\ndef test_compression_types(c_type):\n    c = get_compressor(name=c_type)\n    meta, cdata = c.compress({}, DATA)\n    if c_type == \"none\":\n        assert len(cdata) >= len(DATA)  # it's not compressed and just in there 1:1\n    else:\n        assert len(cdata) < len(DATA)\n    assert DATA == c.decompress(meta, cdata)[1]\n    assert DATA == Compressor(**params).decompress(meta, cdata)[1]  # autodetect\n\n\ndef test_lz4_buffer_allocation(monkeypatch):\n    # disable fallback to no compression on incompressible data\n    monkeypatch.setattr(LZ4, \"decide\", lambda always_compress: LZ4)\n    # test with a rather huge data object to see if buffer allocation / resizing works\n    incompressible_data = os.urandom(5 * 2**20) * 10  # 50MiB badly compressible data\n    c = Compressor(\"lz4\")\n    meta, cdata = c.compress({}, incompressible_data)\n    assert len(incompressible_data) == 50 * 2**20\n    assert len(cdata) >= len(incompressible_data)\n    assert incompressible_data == c.decompress(meta, cdata)[1]\n\n\n@pytest.mark.parametrize(\"invalid_cdata\", [b\"\\xff\\xfftotalcrap\", b\"\\x08\\x00notreallyzlib\"])\ndef test_autodetect_invalid(invalid_cdata):\n    with pytest.raises(ValueError):\n        Compressor(**params, legacy_mode=True).decompress(None, invalid_cdata)\n\n\ndef test_zlib_legacy_compat():\n    # For compatibility reasons, we do not add an extra header for zlib,\n    # nor do we expect one when decompressing / auto-detecting.\n    for level in range(10):\n        c = get_compressor(name=\"zlib_legacy\", level=level, legacy_mode=True)\n        meta1, cdata1 = c.compress({}, DATA)\n        cdata2 = zlib.compress(DATA, level)\n        assert cdata1 == cdata2\n        meta2, data2 = c.decompress(None, cdata2)\n        assert DATA == data2\n\n\n@pytest.mark.parametrize(\n    \"c_params\",\n    [\n        dict(name=\"none\"),\n        dict(name=\"lz4\"),\n        dict(name=\"zstd\", level=1),\n        dict(name=\"zstd\", level=3),  # avoiding high zstd levels, memory needs unclear\n        dict(name=\"zlib\", level=0),\n        dict(name=\"zlib\", level=6),\n        dict(name=\"zlib\", level=9),\n        dict(name=\"lzma\", level=0),\n        dict(name=\"lzma\", level=6),  # we do not test lzma on level 9 because of the huge memory needs\n    ],\n)\ndef test_compressor(c_params):\n    c = Compressor(**c_params)\n    meta_c, data_compressed = c.compress({}, DATA)\n    assert \"ctype\" in meta_c\n    assert \"clevel\" in meta_c\n    assert meta_c[\"csize\"] == len(data_compressed)\n    assert meta_c[\"size\"] == len(DATA)\n    meta_d, data_decompressed = c.decompress(meta_c, data_compressed)\n    assert DATA == data_decompressed\n    assert \"ctype\" in meta_d\n    assert \"clevel\" in meta_d\n    assert meta_d[\"csize\"] == len(data_compressed)\n    assert meta_d[\"size\"] == len(DATA)\n\n\ndef test_auto():\n    compressor_auto_zlib = CompressionSpec(\"auto,zlib,9\").compressor\n    compressor_lz4 = CompressionSpec(\"lz4\").compressor\n    compressor_zlib = CompressionSpec(\"zlib,9\").compressor\n    data = bytes(500)\n    meta, compressed_auto_zlib = compressor_auto_zlib.compress({}, data)\n    _, compressed_lz4 = compressor_lz4.compress({}, data)\n    _, compressed_zlib = compressor_zlib.compress({}, data)\n    ratio = len(compressed_zlib) / len(compressed_lz4)\n    assert meta[\"ctype\"] == ZLIB.ID if ratio < 0.99 else LZ4.ID\n    assert meta[\"clevel\"] == 9 if ratio < 0.99 else 255\n    smallest_csize = min(len(compressed_zlib), len(compressed_lz4))\n    assert meta[\"csize\"] == len(compressed_auto_zlib) == smallest_csize\n\n    data = b\"\\x00\\xb8\\xa3\\xa2-O\\xe1i\\xb6\\x12\\x03\\xc21\\xf3\\x8a\\xf78\\\\\\x01\\xa5b\\x07\\x95\\xbeE\\xf8\\xa3\\x9ahm\\xb1~\"\n    meta, compressed = compressor_auto_zlib.compress(dict(meta), data)\n    assert meta[\"ctype\"] == CNONE.ID\n    assert meta[\"clevel\"] == 255\n    assert meta[\"csize\"] == len(compressed)\n\n\n@pytest.mark.parametrize(\n    \"specs, c_type, result_range, obfuscation_factor\",\n    [\n        (\"obfuscate,1,none\", CNONE, 50, 10**1),\n        (\"obfuscate,2,lz4\", LZ4, 10, 10**2),\n        (\"obfuscate,6,zstd,3\", ZSTD, 90, 10**6),\n        (\"obfuscate,2,auto,zstd,10\", Auto, 10, 10**2),\n    ],\n)\ndef test_factor_obfuscation(specs, c_type, result_range, obfuscation_factor: int):\n    # Testing relative random reciprocal size variation, obfuscation spec 1 to 6 inclusive\n    # obfuscate_factor = 10**(obfuscation spec)\n    cs = CompressionSpec(specs)\n    assert isinstance(cs.inner.compressor, c_type)\n    compressor = cs.compressor\n    data = bytes(10000)\n    _, compressed = compressor.compress(dict(type=ROBJ_FILE_STREAM), data)\n    if c_type is CNONE:  # no compression\n        assert len(data) <= len(compressed) <= len(data) * (10 * obfuscation_factor) + 1\n    else:  # with compression\n        min_compress, max_compress = 0.2, 0.001  # estimate compression factor outer boundaries\n        assert max_compress * len(data) <= len(compressed) <= min_compress * len(data) * (10 * obfuscation_factor) + 1\n    assert len({len(compressor.compress(dict(type=ROBJ_FILE_STREAM), data)[1]) for i in range(100)}) > result_range\n    # compressing 100 times the same data should give multiple different result sizes\n\n\n@pytest.mark.parametrize(\n    \"specs, c_type, obfuscation_padding\",\n    [\n        (\"obfuscate,110,none\", CNONE, 2**10),  # up to 1KiB padding\n        (\"obfuscate,120,lz4\", LZ4, 2**20),  # up to 1MiB padding\n        (\"obfuscate,123,zstd,3\", ZSTD, 2**23),  # max, up to 8MiB padding\n    ],\n)\ndef test_additive_obfuscation(specs, c_type, obfuscation_padding: int):\n    # Testing randomly sized padding, obfuscation spec 110 to 123 inclusive\n    # obfuscate_padding = 2 ** (obfuscation spec - 100)\n    cs = CompressionSpec(specs)\n    assert isinstance(cs.inner.compressor, c_type)\n    compressor = cs.compressor\n    data_list = (bytes(1000), bytes(1100))\n    for data in data_list:\n        _, compressed = compressor.compress(dict(type=ROBJ_FILE_STREAM), data)\n        if c_type is CNONE:  # no compression\n            assert len(data) <= len(compressed) <= len(data) + obfuscation_padding\n        else:  # with compression\n            min_compress, max_compress = 0.2, 0.001  # estimate compression factor outer boundaries\n            assert max_compress * len(data) <= len(compressed) <= min_compress * len(data) * obfuscation_padding\n\n\ndef test_obfuscate_meta():\n    compressor = CompressionSpec(\"obfuscate,3,lz4\").compressor\n    data = bytes(10000)\n    meta, compressed = compressor.compress(dict(type=ROBJ_FILE_STREAM), data)\n    assert \"ctype\" in meta\n    assert meta[\"ctype\"] == LZ4.ID\n    assert \"clevel\" in meta\n    assert meta[\"clevel\"] == 0xFF\n    assert \"csize\" in meta\n    csize = meta[\"csize\"]\n    assert csize == len(compressed)  # this is the overall size\n    assert \"psize\" in meta\n    psize = meta[\"psize\"]\n    assert 0 < psize < 100\n    assert csize - psize >= 0  # there is an obfuscation trailer\n    trailer = compressed[psize:]\n    assert not trailer or set(trailer) == {0}  # trailer is all-zero-bytes\n\n\n@pytest.mark.parametrize(\n    \"c_type, c_name\", [(CNONE, \"none\"), (LZ4, \"lz4\"), (ZLIB, \"zlib\"), (LZMA, \"lzma\"), (ZSTD, \"zstd\")]\n)\ndef test_default_compression_level(c_type, c_name):\n    cs = CompressionSpec(c_name).compressor\n    assert isinstance(cs, c_type)\n    if c_type in (ZLIB, LZMA):\n        assert cs.level == 6\n    elif c_type is ZSTD:\n        assert cs.level == 3\n\n\n@pytest.mark.parametrize(\n    \"c_type, c_name, c_levels\", [(ZLIB, \"zlib\", [0, 9]), (LZMA, \"lzma\", [0, 9]), (ZSTD, \"zstd\", [1, 22])]\n)\ndef test_specified_compression_level(c_type, c_name, c_levels):\n    for level in c_levels:\n        cs = CompressionSpec(f\"{c_name},{level}\").compressor\n        assert isinstance(cs, c_type)\n        assert cs.level == level\n\n\n@pytest.mark.parametrize(\"invalid_spec\", [\"\", \"lzma,9,invalid\", \"invalid\"])\ndef test_invalid_compression_level(invalid_spec):\n    with pytest.raises(ArgumentTypeError):\n        CompressionSpec(invalid_spec)\n\n\n@pytest.mark.parametrize(\n    \"data_length, expected_padding\",\n    [\n        (0, 0),\n        (1, 0),\n        (10, 0),\n        (100, 4),\n        (1000, 24),\n        (10000, 240),\n        (20000, 480),\n        (50000, 1200),\n        (100000, 352),\n        (1000000, 15808),\n        (5000000, 111808),\n        (10000000, 223616),\n        (20000000, 447232),\n    ],\n)\ndef test_padme_obfuscation(data_length, expected_padding):\n    compressor = CompressionSpec(\"obfuscate,250,none\").compressor\n    data = b\"x\" * data_length\n    meta, compressed = compressor.compress(dict(type=ROBJ_FILE_STREAM), data)\n\n    expected_padded_size = data_length + expected_padding\n\n    assert (\n        len(compressed) == expected_padded_size\n    ), f\"For {data_length}, expected {expected_padded_size}, got {len(compressed)}\"\n\n\n@pytest.mark.parametrize(\n    \"data_length, expected_padding, robj_type\",\n    [\n        (1000000, 15808, ROBJ_FILE_STREAM),  # we want to obfuscate file content chunk sizes\n        (1000000, 0, ROBJ_ARCHIVE_META),  # we do not want to obfuscate metadata chunk sizes\n    ],\n)\ndef test_robj_specific_obfuscation(data_length, expected_padding, robj_type):\n    compressor = CompressionSpec(\"obfuscate,250,none\").compressor\n    data = b\"x\" * data_length\n    meta, compressed = compressor.compress(dict(type=robj_type), data)\n\n    expected_padded_size = data_length + expected_padding\n\n    assert (\n        len(compressed) == expected_padded_size\n    ), f\"For {data_length}, expected {expected_padded_size}, got {len(compressed)}\"\n"
  },
  {
    "path": "src/borg/testsuite/crypto/__init__.py",
    "content": ""
  },
  {
    "path": "src/borg/testsuite/crypto/crypto_test.py",
    "content": "# Note: these tests are part of the self test, do not use or import pytest functionality here.\n#       See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT\n\nfrom unittest.mock import MagicMock\nimport unittest\n\nfrom ...crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, IntegrityError\nfrom ...crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes\nfrom ...crypto.low_level import AES, hmac_sha256\nfrom hashlib import sha256\nfrom ...crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey, KeyBase, PlaintextKey\nfrom ...helpers import msgpack, bin_to_hex\n\nfrom .. import BaseTestCase\n\n\nclass CryptoTestCase(BaseTestCase):\n    def test_bytes_to_int(self):\n        self.assert_equal(bytes_to_int(b\"\\0\\0\\0\\1\"), 1)\n\n    def test_bytes_to_long(self):\n        self.assert_equal(bytes_to_long(b\"\\0\\0\\0\\0\\0\\0\\0\\1\"), 1)\n        self.assert_equal(long_to_bytes(1), b\"\\0\\0\\0\\0\\0\\0\\0\\1\")\n\n    def test_UNENCRYPTED(self):\n        iv = b\"\"  # any IV is ok, it just must be set and not None\n        data = b\"data\"\n        header = b\"header\"\n        cs = UNENCRYPTED(None, None, iv, header_len=6)\n        envelope = cs.encrypt(data, header=header)\n        self.assert_equal(envelope, header + data)\n        got_data = cs.decrypt(envelope)\n        self.assert_equal(got_data, data)\n\n    def test_AES256_CTR_HMAC_SHA256(self):\n        # this tests the layout as in borg < 1.2 (1 type byte, no aad)\n        mac_key = b\"Y\" * 32\n        enc_key = b\"X\" * 32\n        iv = 0\n        data = b\"foo\" * 10\n        header = b\"\\x42\"\n        # encrypt-then-mac\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, iv, header_len=1, aad_offset=1)\n        hdr_mac_iv_cdata = cs.encrypt(data, header=header)\n        hdr = hdr_mac_iv_cdata[0:1]\n        mac = hdr_mac_iv_cdata[1:33]\n        iv = hdr_mac_iv_cdata[33:41]\n        cdata = hdr_mac_iv_cdata[41:]\n        self.assert_equal(bin_to_hex(hdr), \"42\")\n        self.assert_equal(bin_to_hex(mac), \"af90b488b0cc4a8f768fe2d6814fa65aec66b148135e54f7d4d29a27f22f57a8\")\n        self.assert_equal(bin_to_hex(iv), \"0000000000000000\")\n        self.assert_equal(bin_to_hex(cdata), \"c6efb702de12498f34a2c2bbc8149e759996d08bf6dc5c610aefc0c3a466\")\n        self.assert_equal(cs.next_iv(), 2)\n        # auth-then-decrypt\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)\n        pdata = cs.decrypt(hdr_mac_iv_cdata)\n        self.assert_equal(data, pdata)\n        self.assert_equal(cs.next_iv(), 2)\n        # auth-failure due to corruption (corrupted data)\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)\n        hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:41] + b\"\\0\" + hdr_mac_iv_cdata[42:]\n        self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))\n\n    def test_AES256_CTR_HMAC_SHA256_aad(self):\n        mac_key = b\"Y\" * 32\n        enc_key = b\"X\" * 32\n        iv = 0\n        data = b\"foo\" * 10\n        header = b\"\\x12\\x34\\x56\"\n        # encrypt-then-mac\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, iv, header_len=3, aad_offset=1)\n        hdr_mac_iv_cdata = cs.encrypt(data, header=header)\n        hdr = hdr_mac_iv_cdata[0:3]\n        mac = hdr_mac_iv_cdata[3:35]\n        iv = hdr_mac_iv_cdata[35:43]\n        cdata = hdr_mac_iv_cdata[43:]\n        self.assert_equal(bin_to_hex(hdr), \"123456\")\n        self.assert_equal(bin_to_hex(mac), \"7659a915d9927072ef130258052351a17ef882692893c3850dd798c03d2dd138\")\n        self.assert_equal(bin_to_hex(iv), \"0000000000000000\")\n        self.assert_equal(bin_to_hex(cdata), \"c6efb702de12498f34a2c2bbc8149e759996d08bf6dc5c610aefc0c3a466\")\n        self.assert_equal(cs.next_iv(), 2)\n        # auth-then-decrypt\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)\n        pdata = cs.decrypt(hdr_mac_iv_cdata)\n        self.assert_equal(data, pdata)\n        self.assert_equal(cs.next_iv(), 2)\n        # auth-failure due to corruption (corrupted aad)\n        cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)\n        hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:1] + b\"\\0\" + hdr_mac_iv_cdata[2:]\n        self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))\n\n    def test_AE(self):\n        # used in legacy-like layout (1 type byte, no aad)\n        key = b\"X\" * 32\n        iv_int = 0\n        data = b\"foo\" * 10\n        header = b\"\\x23\" + iv_int.to_bytes(12, \"big\")\n        tests = [\n            # (ciphersuite class, exp_mac, exp_cdata)\n            (\n                AES256_OCB,\n                \"b6909c23c9aaebd9abbe1ff42097652d\",\n                \"877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493\",\n            ),\n            (\n                CHACHA20_POLY1305,\n                \"fd08594796e0706cde1e8b461e3e0555\",\n                \"a093e4b0387526f085d3c40cca84a35230a5c0dd766453b77ba38bcff775\",\n            ),\n        ]\n        for cs_cls, exp_mac, exp_cdata in tests:\n            # print(repr(cs_cls))\n            # encrypt/mac\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            hdr_mac_iv_cdata = cs.encrypt(data, header=header)\n            hdr = hdr_mac_iv_cdata[0:1]\n            iv = hdr_mac_iv_cdata[1:13]\n            mac = hdr_mac_iv_cdata[13:29]\n            cdata = hdr_mac_iv_cdata[29:]\n            self.assert_equal(bin_to_hex(hdr), \"23\")\n            self.assert_equal(bin_to_hex(mac), exp_mac)\n            self.assert_equal(bin_to_hex(iv), \"000000000000000000000000\")\n            self.assert_equal(bin_to_hex(cdata), exp_cdata)\n            self.assert_equal(cs.next_iv(), 1)\n            # auth/decrypt\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            pdata = cs.decrypt(hdr_mac_iv_cdata)\n            self.assert_equal(data, pdata)\n            self.assert_equal(cs.next_iv(), 1)\n            # auth-failure due to corruption (corrupted data)\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:29] + b\"\\0\" + hdr_mac_iv_cdata[30:]\n            self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))\n\n    def test_AEAD(self):\n        # test with aad\n        key = b\"X\" * 32\n        iv_int = 0\n        data = b\"foo\" * 10\n        header = b\"\\x12\\x34\\x56\" + iv_int.to_bytes(12, \"big\")\n        tests = [\n            # (ciphersuite class, exp_mac, exp_cdata)\n            (\n                AES256_OCB,\n                \"f2748c412af1c7ead81863a18c2c1893\",\n                \"877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493\",\n            ),\n            (\n                CHACHA20_POLY1305,\n                \"b7e7c9a79f2404e14f9aad156bf091dd\",\n                \"a093e4b0387526f085d3c40cca84a35230a5c0dd766453b77ba38bcff775\",\n            ),\n        ]\n        for cs_cls, exp_mac, exp_cdata in tests:\n            # print(repr(cs_cls))\n            # encrypt/mac\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            hdr_mac_iv_cdata = cs.encrypt(data, header=header)\n            hdr = hdr_mac_iv_cdata[0:3]\n            iv = hdr_mac_iv_cdata[3:15]\n            mac = hdr_mac_iv_cdata[15:31]\n            cdata = hdr_mac_iv_cdata[31:]\n            self.assert_equal(bin_to_hex(hdr), \"123456\")\n            self.assert_equal(bin_to_hex(mac), exp_mac)\n            self.assert_equal(bin_to_hex(iv), \"000000000000000000000000\")\n            self.assert_equal(bin_to_hex(cdata), exp_cdata)\n            self.assert_equal(cs.next_iv(), 1)\n            # auth/decrypt\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            pdata = cs.decrypt(hdr_mac_iv_cdata)\n            self.assert_equal(data, pdata)\n            self.assert_equal(cs.next_iv(), 1)\n            # auth-failure due to corruption (corrupted aad)\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)\n            hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:1] + b\"\\0\" + hdr_mac_iv_cdata[2:]\n            self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))\n\n    def test_AEAD_with_more_AAD(self):\n        # test giving extra aad to the .encrypt() and .decrypt() calls\n        key = b\"X\" * 32\n        iv_int = 0\n        data = b\"foo\" * 10\n        header = b\"\\x12\\x34\"\n        tests = [AES256_OCB, CHACHA20_POLY1305]\n        for cs_cls in tests:\n            # encrypt/mac\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)\n            hdr_mac_iv_cdata = cs.encrypt(data, header=header, aad=b\"correct_chunkid\")\n            # successful auth/decrypt (correct aad)\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)\n            pdata = cs.decrypt(hdr_mac_iv_cdata, aad=b\"correct_chunkid\")\n            self.assert_equal(data, pdata)\n            # unsuccessful auth (incorrect aad)\n            cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)\n            self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata, aad=b\"incorrect_chunkid\"))\n\n\ndef test_decrypt_key_file_argon2_chacha20_poly1305():\n    plain = b\"hello\"\n    # echo -n \"hello, pass phrase\" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 32 -r\n    key = bytes.fromhex(\"a1b0cba145c154fbd8960996c5ce3428e9920cfe53c84ef08b4102a70832bcec\")\n    ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)\n\n    envelope = ae_cipher.encrypt(plain)\n\n    encrypted = msgpack.packb(\n        {\n            \"version\": 1,\n            \"salt\": b\"salt\" * 4,\n            \"argon2_time_cost\": 1,\n            \"argon2_memory_cost\": 8,\n            \"argon2_parallelism\": 1,\n            \"argon2_type\": b\"id\",\n            \"algorithm\": \"argon2 chacha20-poly1305\",\n            \"data\": envelope,\n        }\n    )\n    key = CHPOKeyfileKey(None)\n\n    decrypted = key.decrypt_key_file(encrypted, \"hello, pass phrase\")\n\n    assert decrypted == plain\n\n\ndef test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256():\n    plain = b\"hello\"\n    salt = b\"salt\" * 4\n    passphrase = \"hello, pass phrase\"\n    key = FlexiKey.pbkdf2(passphrase, salt, 1, 32)\n    hash = hmac_sha256(key, plain)\n    data = AES(key, b\"\\0\" * 16).encrypt(plain)\n    encrypted = msgpack.packb(\n        {\"version\": 1, \"algorithm\": \"sha256\", \"iterations\": 1, \"salt\": salt, \"data\": data, \"hash\": hash}\n    )\n    key = CHPOKeyfileKey(None)\n\n    decrypted = key.decrypt_key_file(encrypted, passphrase)\n\n    assert decrypted == plain\n\n\n@unittest.mock.patch(\"getpass.getpass\")\ndef test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):\n    \"\"\"https://github.com/borgbackup/borg/pull/6469#discussion_r832670411\n\n    This is a regression test for a bug I introduced and fixed:\n\n    Traceback (most recent call last):\n      File \"/home/user/borg-master/src/borg/testsuite/crypto.py\", line 384,\n                                                                  in test_repo_key_detect_does_not_raise_integrity_error\n        RepoKey.detect(repository, manifest_data=None)\n      File \"/home/user/borg-master/src/borg/crypto/key.py\", line 402, in detect\n        if not key.load(target, passphrase):\n      File \"/home/user/borg-master/src/borg/crypto/key.py\", line 654, in load\n        success = self._load(key_data, passphrase)\n      File \"/home/user/borg-master/src/borg/crypto/key.py\", line 418, in _load\n        data = self.decrypt_key_file(cdata, passphrase)\n      File \"/home/user/borg-master/src/borg/crypto/key.py\", line 444, in decrypt_key_file\n        return self.decrypt_key_file_argon2(encrypted_key, passphrase)\n      File \"/home/user/borg-master/src/borg/crypto/key.py\", line 470, in decrypt_key_file_argon2\n        return ae_cipher.decrypt(encrypted_key.data)\n      File \"src/borg/crypto/low_level.pyx\", line 302, in borg.crypto.low_level.AES256_CTR_BASE.decrypt\n        self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,\n      File \"src/borg/crypto/low_level.pyx\", line 382, in borg.crypto.low_level.AES256_CTR_HMAC_SHA256.mac_verify\n        raise IntegrityError('MAC Authentication failed')\n    borg.crypto.low_level.IntegrityError: MAC Authentication failed\n\n    1. FlexiKey.decrypt_key_file() is supposed to signal the decryption failure by returning None\n    2. FlexiKey.detect() relies on that interface - it tries an empty passphrase before prompting the user\n    3. my initial implementation of decrypt_key_file_argon2() was simply passing through the IntegrityError()\n       from AES256_CTR_BASE.decrypt()\n    \"\"\"\n    repository = MagicMock(id=b\"repository_id\")\n    getpass.return_value = \"hello, pass phrase\"\n    monkeypatch.setenv(\"BORG_DISPLAY_PASSPHRASE\", \"no\")\n    AESOCBRepoKey.create(repository, args=MagicMock(key_algorithm=\"argon2\"))\n    repository.load_key.return_value = repository.save_key.call_args.args[0]\n\n    AESOCBRepoKey.detect(repository, manifest_data=None)\n\n\nclass TestDeriveKey(BaseTestCase):\n    # Create a simple KeyBase subclass with a non-empty crypt_key\n    class CustomKey(KeyBase):\n        def __init__(self, crypt_key, id_key):\n            self.crypt_key = crypt_key\n            self.id_key = id_key\n\n    def test_derive_key_with_plaintext_key(self):\n        \"\"\"Test derive_key with PlaintextKey (empty crypt_key)\"\"\"\n        key = PlaintextKey(None)\n        salt, domain, size = b\"salt\", b\"domain\", 16\n\n        # PlaintextKey has an empty crypt_key, so the derived key should be based on salt and domain only\n        derived_key = key.derive_key(salt=salt, domain=domain, size=size)\n        expected = sha256(b\"\" + salt + domain).digest()[:size]\n        self.assert_equal(derived_key, expected)\n\n    def test_derive_key_with_custom_key(self):\n        \"\"\"Test derive_key with a custom KeyBase subclass (non-empty crypt_key)\"\"\"\n        crypt_key, id_key = b\"test_crypt_key\", b\"test_id_key\"\n        key = self.CustomKey(crypt_key, id_key)\n        salt, domain, size = b\"salt\", b\"domain\", 32\n\n        # derived key size and value as expected\n        expected = sha256(crypt_key + salt + domain).digest()[:size]\n        derived_key = key.derive_key(salt=salt, domain=domain, size=size)\n        self.assert_equal(derived_key, expected)\n\n        # domain separation\n        derived_key = key.derive_key(salt=salt, domain=b\"other_domain\", size=size)\n        assert derived_key != expected\n        assert len(derived_key) == size\n\n        # salt separation\n        derived_key = key.derive_key(salt=b\"other salt\", domain=domain, size=size)\n        assert derived_key != expected\n        assert len(derived_key) == size\n\n    def test_derive_key_from_different_keys(self):\n        \"\"\"Test derive_key with different key material\"\"\"\n        crypt_key, id_key = b\"test_crypt_key\", b\"test_id_key\"\n        key = self.CustomKey(crypt_key, id_key)\n        salt, domain, size = b\"salt\", b\"domain\", 32\n\n        # derived key size and value as expected (using the ID key)\n        expected = sha256(id_key + salt + domain).digest()[:size]\n        derived_key = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=True)\n        self.assert_equal(derived_key, expected)\n\n        # generating different keys from crypt_key and id_key\n        derived_key_from_id = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=True)\n        derived_key_from_crypt = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=False)\n        assert derived_key_from_id != derived_key_from_crypt\n"
  },
  {
    "path": "src/borg/testsuite/crypto/csprng_test.py",
    "content": "import pytest\n\nfrom ...crypto.low_level import CSPRNG\n\n\n# Test keys (32 bytes each)\nkey1 = bytes.fromhex(\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\")\nkey2 = bytes.fromhex(\"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210\")\n\n\ndef test_deterministic_output():\n    \"\"\"Test that the same key produces the same random sequence.\"\"\"\n    # Create two CSPRNGs with the same key\n    rng1 = CSPRNG(key1)\n    rng2 = CSPRNG(key1)\n\n    # Generate random bytes from both\n    bytes1 = rng1.random_bytes(100)\n    bytes2 = rng2.random_bytes(100)\n\n    # They should be identical\n    assert bytes1 == bytes2\n\n    # Different keys should produce different outputs\n    rng3 = CSPRNG(key2)\n    bytes3 = rng3.random_bytes(100)\n    assert bytes1 != bytes3\n\n\ndef test_random_bytes():\n    \"\"\"Test the random_bytes method.\"\"\"\n    rng = CSPRNG(key1)\n\n    # Test different sizes\n    for size in [1, 10, 100, 1000, 10000]:\n        random_data = rng.random_bytes(size)\n\n        # Check type\n        assert isinstance(random_data, bytes)\n\n        # Check length\n        assert len(random_data) == size\n\n\ndef test_random_int():\n    \"\"\"Test the random_int method.\"\"\"\n    rng = CSPRNG(key1)\n\n    # Test different ranges\n    for upper_bound in [2, 10, 100, 1000, 1000000, 1000000000, 1000000000000]:\n        # Generate multiple random integers\n        for _ in range(10):\n            random_int = rng.random_int(upper_bound)\n\n            # Check range\n            assert 0 <= random_int < upper_bound\n\n            # Check type\n            assert isinstance(random_int, int)\n\n\ndef test_random_int_edge_cases():\n    \"\"\"Test the random_int method with edge cases.\"\"\"\n    rng = CSPRNG(key1)\n\n    # Test error case: upper_bound <= 0\n    with pytest.raises(ValueError):\n        rng.random_int(-1)\n\n    with pytest.raises(ValueError):\n        rng.random_int(0)\n\n    # Test with upper bound 1\n    assert rng.random_int(1) == 0\n\n    # Test with upper bound 2\n    for _ in range(10):\n        result = rng.random_int(2)\n        assert 0 <= result < 2\n\n    # Test with upper bound that is a power of 2\n    power_of_2 = 256\n    for _ in range(10):\n        result = rng.random_int(power_of_2)\n        assert 0 <= result < power_of_2\n\n    # Test with upper bound that is one less than a power of 2\n    almost_power_of_2 = 255\n    for _ in range(10):\n        result = rng.random_int(almost_power_of_2)\n        assert 0 <= result < almost_power_of_2\n\n    # Test with upper bound that is one more than a power of 2\n    just_over_power_of_2 = 257\n    for _ in range(10):\n        result = rng.random_int(just_over_power_of_2)\n        assert 0 <= result < just_over_power_of_2\n\n    # Test with a large upper bound\n    large_bound = 1000000000\n    for _ in range(10):\n        result = rng.random_int(large_bound)\n        assert 0 <= result < large_bound\n\n\ndef test_shuffle():\n    \"\"\"Test the shuffle method.\"\"\"\n    rng1 = CSPRNG(key1)\n    rng2 = CSPRNG(key1)\n\n    # Create two identical lists\n    list1 = list(range(100))\n    list2 = list(range(100))\n\n    # Shuffle both lists with the same key\n    rng1.shuffle(list1)\n    rng2.shuffle(list2)\n\n    # They should be identical after shuffling\n    assert list1 == list2\n\n    # The shuffled list should be a permutation of the original\n    assert sorted(list1) == list(range(100))\n\n    # Different keys should produce different shuffles\n    rng3 = CSPRNG(key2)\n    list3 = list(range(100))\n    rng3.shuffle(list3)\n    assert list1 != list3\n\n    # Getting another shuffled list by an already used RNG should produce a different shuffle\n    list4 = list(range(100))\n    rng1.shuffle(list4)\n    assert list1 != list4\n\n\ndef test_statistical_properties():\n    \"\"\"Test basic statistical properties of the random output.\"\"\"\n    rng = CSPRNG(key1)\n\n    # Generate a large number of random bytes\n    data = rng.random_bytes(10000)\n\n    # Count occurrences of each byte value\n    counts = [0] * 256\n    for byte in data:\n        counts[byte] += 1\n\n    # Check that each byte value appears with roughly equal frequency\n    # For 10000 bytes, each value should appear about 39 times (10000/256)\n    # We allow a generous margin of error (±50%)\n    for count in counts:\n        assert 19 <= count <= 59, \"Byte distribution is not uniform\"\n\n    # Test bit distribution\n    bits_set = 0\n    for byte in data:\n        bits_set += bin(byte).count(\"1\")\n\n    # For random data, approximately 50% of bits should be set\n    # 10000 bytes = 80000 bits, so about 40000 should be set\n    # Allow ±5% margin\n    assert 38000 <= bits_set <= 42000, \"Bit distribution is not uniform\"\n\n\ndef test_large_shuffle():\n    \"\"\"Test shuffling a large list.\"\"\"\n    rng = CSPRNG(key1)\n\n    # Create a large list\n    large_list = list(range(10000))\n\n    # Make a copy for comparison\n    original = large_list.copy()\n\n    # Shuffle the list\n    rng.shuffle(large_list)\n\n    # The shuffled list should be different from the original\n    assert large_list != original\n\n    # The shuffled list should be a permutation of the original\n    assert sorted(large_list) == original\n"
  },
  {
    "path": "src/borg/testsuite/crypto/file_integrity_test.py",
    "content": "import pytest\n\nfrom ...crypto.file_integrity import DetachedIntegrityCheckedFile, FileIntegrityError, IntegrityCheckedFile\nfrom ...platform import SyncFile\n\n\nclass TestReadIntegrityFile:\n    def test_no_integrity(self, tmpdir):\n        protected_file = tmpdir.join(\"file\")\n        protected_file.write(\"1234\")\n        assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) is None\n\n    def test_truncated_integrity(self, tmpdir):\n        protected_file = tmpdir.join(\"file\")\n        protected_file.write(\"1234\")\n        tmpdir.join(\"file.integrity\").write(\"\")\n        with pytest.raises(FileIntegrityError):\n            DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file))\n\n    def test_unknown_algorithm(self, tmpdir):\n        protected_file = tmpdir.join(\"file\")\n        protected_file.write(\"1234\")\n        tmpdir.join(\"file.integrity\").write('{\"algorithm\": \"HMAC_SERIOUSHASH\", \"digests\": \"1234\"}')\n        assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) is None\n\n    @pytest.mark.parametrize(\n        \"json\", ('{\"ALGORITHM\": \"HMAC_SERIOUSHASH\", \"digests\": \"1234\"}', \"[]\", \"1234.5\", '\"A string\"', \"Invalid JSON\")\n    )\n    def test_malformed(self, tmpdir, json):\n        protected_file = tmpdir.join(\"file\")\n        protected_file.write(\"1234\")\n        tmpdir.join(\"file.integrity\").write(json)\n        with pytest.raises(FileIntegrityError):\n            DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file))\n\n\nclass TestDetachedIntegrityCheckedFile:\n    @pytest.fixture\n    def integrity_protected_file(self, tmpdir):\n        path = str(tmpdir.join(\"file\"))\n        with DetachedIntegrityCheckedFile(path, write=True) as fd:\n            fd.write(b\"foo and bar\")\n        return path\n\n    def test_simple(self, tmpdir, integrity_protected_file):\n        assert tmpdir.join(\"file\").check(file=True)\n        assert tmpdir.join(\"file.integrity\").check(file=True)\n        with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n            assert fd.read() == b\"foo and bar\"\n\n    def test_corrupted_file(self, integrity_protected_file):\n        with open(integrity_protected_file, \"ab\") as fd:\n            fd.write(b\" extra data\")\n        with pytest.raises(FileIntegrityError):\n            with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n                assert fd.read() == b\"foo and bar extra data\"\n\n    def test_corrupted_file_partial_read(self, integrity_protected_file):\n        with open(integrity_protected_file, \"ab\") as fd:\n            fd.write(b\" extra data\")\n        with pytest.raises(FileIntegrityError):\n            with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n                data = b\"foo and bar\"\n                assert fd.read(len(data)) == data\n\n    @pytest.mark.parametrize(\"new_name\", (\"different_file\", \"different_file.different_ext\"))\n    def test_renamed_file(self, tmpdir, integrity_protected_file, new_name):\n        new_path = tmpdir.join(new_name)\n        tmpdir.join(\"file\").move(new_path)\n        tmpdir.join(\"file.integrity\").move(new_path + \".integrity\")\n        with pytest.raises(FileIntegrityError):\n            with DetachedIntegrityCheckedFile(str(new_path), write=False) as fd:\n                assert fd.read() == b\"foo and bar\"\n\n    def test_moved_file(self, tmpdir, integrity_protected_file):\n        new_dir = tmpdir.mkdir(\"another_directory\")\n        tmpdir.join(\"file\").move(new_dir.join(\"file\"))\n        tmpdir.join(\"file.integrity\").move(new_dir.join(\"file.integrity\"))\n        new_path = str(new_dir.join(\"file\"))\n        with DetachedIntegrityCheckedFile(new_path, write=False) as fd:\n            assert fd.read() == b\"foo and bar\"\n\n    def test_no_integrity(self, tmpdir, integrity_protected_file):\n        tmpdir.join(\"file.integrity\").remove()\n        with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n            assert fd.read() == b\"foo and bar\"\n\n\nclass TestDetachedIntegrityCheckedFileParts:\n    @pytest.fixture\n    def integrity_protected_file(self, tmpdir):\n        path = str(tmpdir.join(\"file\"))\n        with DetachedIntegrityCheckedFile(path, write=True) as fd:\n            fd.write(b\"foo and bar\")\n            fd.hash_part(\"foopart\")\n            fd.write(b\" other data\")\n        return path\n\n    def test_simple(self, integrity_protected_file):\n        with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n            data1 = b\"foo and bar\"\n            assert fd.read(len(data1)) == data1\n            fd.hash_part(\"foopart\")\n            assert fd.read() == b\" other data\"\n\n    def test_wrong_part_name(self, integrity_protected_file):\n        with pytest.raises(FileIntegrityError):\n            # Because some hash_part failed, the final digest will fail as well - again - even if we catch\n            # the failing hash_part. This is intentional: (1) it makes the code simpler (2) it's a good fail-safe\n            # against overly broad exception handling.\n            with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n                data1 = b\"foo and bar\"\n                assert fd.read(len(data1)) == data1\n                with pytest.raises(FileIntegrityError):\n                    # This specific bit raises it directly\n                    fd.hash_part(\"barpart\")\n                # Still explodes in the end.\n\n    @pytest.mark.parametrize(\"partial_read\", (False, True))\n    def test_part_independence(self, integrity_protected_file, partial_read):\n        with open(integrity_protected_file, \"ab\") as fd:\n            fd.write(b\"some extra stuff that does not belong\")\n        with pytest.raises(FileIntegrityError):\n            with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd:\n                data1 = b\"foo and bar\"\n                try:\n                    assert fd.read(len(data1)) == data1\n                    fd.hash_part(\"foopart\")\n                except FileIntegrityError:\n                    assert False, \"This part must not raise, since this part is still valid.\"\n                if not partial_read:\n                    fd.read()\n                # But overall it explodes with the final digest. Neat, eh?\n\n\nclass TestIntegrityCheckedFileWithSyncFile:\n    def test_write_and_verify_with_syncfile(self, tmp_path):\n        \"\"\"IntegrityCheckedFile works correctly with SyncFile as override_fd.\"\"\"\n        path = str(tmp_path / \"testfile\")\n        with SyncFile(path, binary=True) as sf:\n            with IntegrityCheckedFile(path=path, write=True, override_fd=sf) as fd:\n                fd.write(b\"test data for integrity check\")\n            integrity_data = fd.integrity_data\n\n        assert integrity_data is not None\n\n        # verify the written data can be read back with integrity check\n        with IntegrityCheckedFile(path=path, write=False, integrity_data=integrity_data) as fd:\n            assert fd.read() == b\"test data for integrity check\"\n"
  },
  {
    "path": "src/borg/testsuite/crypto/key_test.py",
    "content": "import tempfile\nfrom binascii import a2b_base64\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom ...crypto.key import PlaintextKey, AuthenticatedKey, Blake2AuthenticatedKey\nfrom ...crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey\nfrom ...crypto.key import AEADKeyBase\nfrom ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey\nfrom ...crypto.key import Blake2AESOCBRepoKey, Blake2AESOCBKeyfileKey, Blake2CHPORepoKey, Blake2CHPOKeyfileKey\nfrom ...crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256\nfrom ...crypto.key import UnsupportedManifestError, UnsupportedKeyFormatError\nfrom ...crypto.key import identify_key\nfrom ...crypto.low_level import IntegrityError as IntegrityErrorBase\nfrom ...helpers import IntegrityError\nfrom ...helpers import Location\nfrom ...helpers import msgpack\nfrom ...constants import KEY_ALGORITHMS\nfrom ...helpers import hex_to_bin, bin_to_hex\n\n\nclass TestKey:\n    class MockArgs:\n        location = Location(tempfile.mkstemp()[1])\n        key_algorithm = \"argon2\"\n\n    keyfile2_key_file = \"\"\"\n        BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000\n        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAN4u2SiN7hqISe3OA8raBWNuvHn1R50ZU7HVCn\n        11vTJNEaj9soxUaIGcW+pAB2N5yYoKMg/sGCMuZa286iJ008DvN99rf/ORfcKrK2GmzslO\n        N3uv9Tk9HtqV/Sq5zgM9xuY9rEeQGDQVQ+AOsFamJqSUrAemGJbJqw9IerXC/jN4XPnX6J\n        pi1cXCFxHfDaEhmWrkdPNoZdirCv/eP/dOVOLmwU58YsS+MvkZNfEa16el/fSb/ENdrwJ/\n        2aYMQrDdk1d5MYzkjotv/KpofNwPXZchu2EwH7OIHWQjEVL1DZWkaGFzaNoAIO/7qn1hr3\n        F84MsMMiqpbz4KVICeBZhfAaTPs4W7BC63qml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgLENQ\n        2uVCoR7EnAoiRzn8J+orbojKtJlNCnQ31SSC8rendmVyc2lvbgE=\"\"\".strip()\n\n    keyfile2_cdata = hex_to_bin(\n        \"003be7d57280d1a42add9f3f36ea363bbc5e9349ad01ddec0634a54dd02959e70500000000000003ec063d2cbcacba6b\"\n    )\n    keyfile2_id = hex_to_bin(\"c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314\")\n\n    keyfile_blake2_key_file = \"\"\"\n        BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000\n        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZ7VCsTjbLhC1ipXOyhcGn7YnROEhP24UQvOCi\n        Oar1G+JpwgO9BIYaiCODUpzPuDQEm6WxyTwEneJ3wsuyeqyh7ru2xo9FAUKRf6jcqqZnan\n        ycTfktkUC+CPhKR7W6MTu5fPvy99chyL09/RGdD15aswR5PjNoFu4626sfMrBReyPdlxqt\n        F80m+fbNE/vln2Trqoz9EMHQ3IxjIK4q0m4Aj7TwCu7ZankFtwt898+tYsWE7lb2Ps/gXB\n        F8PM/5wHpYps2AKhDCpwKp5HyqIqlF5IzR2ydL9QP20QBjp/rSi6b+xwrfxNJZfw78f8ef\n        A2Yj7xIsxNQ0kmVmTL/UF6d7+Mw1JfurWrySiDU7QQ+RiZpWUZ0DdReB+e4zn6/KNKC884\n        34SGywADuLIQe2FKU+5jBCbutEyEGILQbAR/cgeLy5+V2XwXMJh4ytwXVIeT6Lk+qhYAdz\n        Klx4ub7XijKcOxJyBE+4k33DAhcfIT2r4/sxgMhXrIOEQPKsMAixzdcqVYkpou+6c4PZeL\n        nr+UjfJwOqK1BlWk1NgwE4GXYIKkaGFzaNoAIAzjUtpBPPh6kItZtHQZvnQG6FpucZNfBC\n        UTHFJg343jqml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgz3YaUZZ/s+UWywj97EY5b4KhtJYi\n        qkPqtDDxs2j/T7+ndmVyc2lvbgE=\"\"\".strip()\n\n    keyfile_blake2_cdata = hex_to_bin(\n        \"04d6040f5ef80e0a8ac92badcbe3dee83b7a6b53d5c9a58c4eed14964cb10ef591040404040404040d1e65cc1f435027\"\n    )\n    # Verified against b2sum. Entire string passed to BLAKE2, including the padded 64 byte key contained in\n    # keyfile_blake2_key_file above is\n    # 19280471de95185ec27ecb6fc9edbb4f4db26974c315ede1cd505fab4250ce7cd0d081ea66946c\n    # 95f0db934d5f616921efbd869257e8ded2bd9bd93d7f07b1a30000000000000000000000000000\n    # 000000000000000000000000000000000000000000000000000000000000000000000000000000\n    # 00000000000000000000007061796c6f6164\n    #                       p a y l o a d\n    keyfile_blake2_id = hex_to_bin(\"d8bc68e961c79f99be39061589e5179b2113cd9226e07b08ddd4a1fef7ce93fb\")\n\n    @pytest.fixture\n    def keys_dir(self, request, monkeypatch, tmpdir):\n        monkeypatch.setenv(\"BORG_KEYS_DIR\", str(tmpdir))\n        return tmpdir\n\n    @pytest.fixture(\n        params=(\n            # not encrypted\n            PlaintextKey,\n            AuthenticatedKey,\n            Blake2AuthenticatedKey,\n            # legacy crypto\n            KeyfileKey,\n            Blake2KeyfileKey,\n            RepoKey,\n            Blake2RepoKey,\n            # new crypto\n            AESOCBKeyfileKey,\n            AESOCBRepoKey,\n            Blake2AESOCBKeyfileKey,\n            Blake2AESOCBRepoKey,\n            CHPOKeyfileKey,\n            CHPORepoKey,\n            Blake2CHPOKeyfileKey,\n            Blake2CHPORepoKey,\n        )\n    )\n    def key(self, request, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        return request.param.create(self.MockRepository(), self.MockArgs())\n\n    class MockRepository:\n        class _Location:\n            raw = processed = \"/some/place\"\n\n            def canonical_path(self):\n                return self.processed\n\n        _location = _Location()\n        id = bytes(32)\n        id_str = bin_to_hex(id)\n        version = 2\n\n        def save_key(self, data):\n            self.key_data = data\n\n        def load_key(self):\n            return self.key_data\n\n    def test_plaintext(self):\n        key = PlaintextKey.create(None, None)\n        chunk = b\"foo\"\n        id = key.id_hash(chunk)\n        assert bin_to_hex(id) == \"2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae\"\n        assert chunk == key.decrypt(id, key.encrypt(id, chunk))\n\n    def test_keyfile(self, monkeypatch, keys_dir):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        key = KeyfileKey.create(self.MockRepository(), self.MockArgs())\n        assert key.cipher.next_iv() == 0\n        chunk = b\"ABC\"\n        id = key.id_hash(chunk)\n        manifest = key.encrypt(id, chunk)\n        assert key.cipher.extract_iv(manifest) == 0\n        manifest2 = key.encrypt(id, chunk)\n        assert manifest != manifest2\n        assert key.decrypt(id, manifest) == key.decrypt(id, manifest2)\n        assert key.cipher.extract_iv(manifest2) == 1\n        iv = key.cipher.extract_iv(manifest)\n        key2 = KeyfileKey.detect(self.MockRepository(), manifest)\n        assert key2.cipher.next_iv() >= iv + key2.cipher.block_count(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD)\n        # Key data sanity check\n        assert len({key2.id_key, key2.crypt_key}) == 2\n        assert key2.chunk_seed != 0\n        chunk = b\"foo\"\n        id = key.id_hash(chunk)\n        assert chunk == key2.decrypt(id, key.encrypt(id, chunk))\n\n    def test_keyfile_kfenv(self, tmpdir, monkeypatch):\n        keyfile = tmpdir.join(\"keyfile\")\n        monkeypatch.setenv(\"BORG_KEY_FILE\", str(keyfile))\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"testkf\")\n        assert not keyfile.exists()\n        key = CHPOKeyfileKey.create(self.MockRepository(), self.MockArgs())\n        assert keyfile.exists()\n        chunk = b\"ABC\"\n        chunk_id = key.id_hash(chunk)\n        chunk_cdata = key.encrypt(chunk_id, chunk)\n        key = CHPOKeyfileKey.detect(self.MockRepository(), chunk_cdata)\n        assert chunk == key.decrypt(chunk_id, chunk_cdata)\n        keyfile.remove()\n        with pytest.raises(FileNotFoundError):\n            CHPOKeyfileKey.detect(self.MockRepository(), chunk_cdata)\n\n    def test_keyfile2(self, monkeypatch, keys_dir):\n        with keys_dir.join(\"keyfile\").open(\"w\") as fd:\n            fd.write(self.keyfile2_key_file)\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"passphrase\")\n        key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)\n        assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b\"payload\"\n\n    def test_keyfile2_kfenv(self, tmpdir, monkeypatch):\n        keyfile = tmpdir.join(\"keyfile\")\n        with keyfile.open(\"w\") as fd:\n            fd.write(self.keyfile2_key_file)\n        monkeypatch.setenv(\"BORG_KEY_FILE\", str(keyfile))\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"passphrase\")\n        key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)\n        assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b\"payload\"\n\n    def test_keyfile_blake2(self, monkeypatch, keys_dir):\n        with keys_dir.join(\"keyfile\").open(\"w\") as fd:\n            fd.write(self.keyfile_blake2_key_file)\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"passphrase\")\n        key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)\n        assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b\"payload\"\n\n    def _corrupt_byte(self, key, data, offset):\n        data = bytearray(data)\n        # note: we corrupt in a way so that even corruption of the unauthenticated encryption type byte\n        # will trigger an IntegrityError (does not happen while we stay within TYPES_ACCEPTABLE).\n        data[offset] ^= 64\n        with pytest.raises(IntegrityErrorBase):\n            key.decrypt(b\"\", data)\n\n    def test_decrypt_integrity(self, monkeypatch, keys_dir):\n        with keys_dir.join(\"keyfile\").open(\"w\") as fd:\n            fd.write(self.keyfile2_key_file)\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"passphrase\")\n        key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)\n\n        data = self.keyfile2_cdata\n        for i in range(len(data)):\n            self._corrupt_byte(key, data, i)\n\n        with pytest.raises(IntegrityError):\n            data = bytearray(self.keyfile2_cdata)\n            id = bytearray(key.id_hash(data))  # corrupt chunk id\n            id[12] = 0\n            plaintext = key.decrypt(id, data)\n            key.assert_id(id, plaintext)\n\n    def test_roundtrip(self, key):\n        repository = key.repository\n        plaintext = b\"foo\"\n        id = key.id_hash(plaintext)\n        encrypted = key.encrypt(id, plaintext)\n        identified_key_class = identify_key(encrypted)\n        assert identified_key_class == key.__class__\n        loaded_key = identified_key_class.detect(repository, encrypted)\n        decrypted = loaded_key.decrypt(id, encrypted)\n        assert decrypted == plaintext\n\n    def test_assert_id(self, key):\n        plaintext = b\"123456789\"\n        id = key.id_hash(plaintext)\n        key.assert_id(id, plaintext)\n        id_changed = bytearray(id)\n        id_changed[0] ^= 1\n        if not isinstance(key, AEADKeyBase):\n            with pytest.raises(IntegrityError):\n                key.assert_id(id_changed, plaintext)\n            plaintext_changed = plaintext + b\"1\"\n            with pytest.raises(IntegrityError):\n                key.assert_id(id, plaintext_changed)\n\n    def test_authenticated_encrypt(self, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs())\n        assert AuthenticatedKey.id_hash is ID_HMAC_SHA_256.id_hash\n        assert len(key.id_key) == 32\n        plaintext = b\"123456789\"\n        id = key.id_hash(plaintext)\n        authenticated = key.encrypt(id, plaintext)\n        # 0x07 is the key TYPE.\n        assert authenticated == b\"\\x07\" + plaintext\n\n    def test_blake2_authenticated_encrypt(self, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        key = Blake2AuthenticatedKey.create(self.MockRepository(), self.MockArgs())\n        assert Blake2AuthenticatedKey.id_hash is ID_BLAKE2b_256.id_hash\n        assert len(key.id_key) == 128\n        plaintext = b\"123456789\"\n        id = key.id_hash(plaintext)\n        authenticated = key.encrypt(id, plaintext)\n        # 0x06 is the key TYPE.\n        assert authenticated == b\"\\x06\" + plaintext\n\n\nclass TestTAM:\n    @pytest.fixture\n    def key(self, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        return CHPOKeyfileKey.create(TestKey.MockRepository(), TestKey.MockArgs())\n\n    def test_unpack_future(self, key):\n        blob = b\"\\xc1\\xc1\\xc1\\xc1foobar\"\n        with pytest.raises(UnsupportedManifestError):\n            key.unpack_manifest(blob)\n\n        blob = b\"\\xc1\\xc1\\xc1\"\n        with pytest.raises(msgpack.UnpackException):\n            key.unpack_manifest(blob)\n\n    def test_round_trip_manifest(self, key):\n        data = {\"foo\": \"bar\"}\n        blob = key.pack_metadata(data)\n        unpacked = key.unpack_manifest(blob)\n        assert unpacked[\"foo\"] == \"bar\"\n        assert \"tam\" not in unpacked  # legacy\n\n    def test_round_trip_archive(self, key):\n        data = {\"foo\": \"bar\"}\n        blob = key.pack_metadata(data)\n        unpacked = key.unpack_archive(blob)\n        assert unpacked[\"foo\"] == \"bar\"\n        assert \"tam\" not in unpacked  # legacy\n\n\ndef test_decrypt_key_file_unsupported_algorithm():\n    \"\"\"We will add more algorithms in the future. We should raise a helpful error.\"\"\"\n    key = CHPOKeyfileKey(None)\n    encrypted = msgpack.packb({\"algorithm\": \"THIS ALGORITHM IS NOT SUPPORTED\", \"version\": 1})\n\n    with pytest.raises(UnsupportedKeyFormatError):\n        key.decrypt_key_file(encrypted, \"hello, pass phrase\")\n\n\ndef test_decrypt_key_file_v2_is_unsupported():\n    \"\"\"There may eventually be a version 2 of the format. For now we should raise a helpful error.\"\"\"\n    key = CHPOKeyfileKey(None)\n    encrypted = msgpack.packb({\"version\": 2})\n\n    with pytest.raises(UnsupportedKeyFormatError):\n        key.decrypt_key_file(encrypted, \"hello, pass phrase\")\n\n\ndef test_key_file_roundtrip(monkeypatch):\n    def to_dict(key):\n        extract = \"repository_id\", \"crypt_key\", \"id_key\", \"chunk_seed\"\n        return {a: getattr(key, a) for a in extract}\n\n    repository = MagicMock(id=b\"repository_id\")\n    monkeypatch.setenv(\"BORG_PASSPHRASE\", \"hello, pass phrase\")\n\n    save_me = AESOCBRepoKey.create(repository, args=MagicMock(key_algorithm=\"argon2\"))\n    saved = repository.save_key.call_args.args[0]\n    repository.load_key.return_value = saved\n    load_me = AESOCBRepoKey.detect(repository, manifest_data=None)\n\n    assert to_dict(load_me) == to_dict(save_me)\n    assert msgpack.unpackb(a2b_base64(saved))[\"algorithm\"] == KEY_ALGORITHMS[\"argon2\"]\n"
  },
  {
    "path": "src/borg/testsuite/fslocking_test.py",
    "content": "import random\nimport time\nfrom pathlib import Path\nfrom threading import Thread, Lock as ThreadingLock\nfrom traceback import format_exc\n\nimport pytest\n\nfrom ..platform import get_process_id, process_alive\nfrom ..fslocking import (\n    TimeoutTimer,\n    ExclusiveLock,\n    Lock,\n    LockRoster,\n    ADD,\n    REMOVE,\n    SHARED,\n    EXCLUSIVE,\n    LockTimeout,\n    NotLocked,\n    NotMyLock,\n)\nfrom ..platformflags import is_win32\n\nID1 = \"foo\", 1, 1\nID2 = \"bar\", 2, 2\nRACE_TEST_NUM_THREADS = 40\nRACE_TEST_DURATION = 0.4  # seconds\n\n\n@pytest.fixture()\ndef free_pid():\n    \"\"\"Return a free PID not used by any process (naturally this is racy).\"\"\"\n    host, pid, tid = get_process_id()\n    while True:\n        # PIDs are often restricted to a small range. On Linux the range >32k is by default not used.\n        pid = random.randint(33000, 65000)\n        if not process_alive(host, pid, tid):\n            return pid\n\n\nclass TestTimeoutTimer:\n    def test_timeout(self):\n        timeout = 0.5\n        t = TimeoutTimer(timeout).start()\n        assert not t.timed_out()\n        time.sleep(timeout * 1.5)\n        assert t.timed_out()\n\n    def test_notimeout_sleep(self):\n        timeout, sleep = None, 0.5\n        t = TimeoutTimer(timeout, sleep).start()\n        assert not t.timed_out_or_sleep()\n        assert time.time() >= t.start_time + 1 * sleep\n        assert not t.timed_out_or_sleep()\n        assert time.time() >= t.start_time + 2 * sleep\n\n\n@pytest.fixture()\ndef lockpath(tmpdir):\n    return str(tmpdir.join(\"lock\"))\n\n\nclass TestExclusiveLock:\n    def test_checks(self, lockpath):\n        with ExclusiveLock(lockpath, timeout=1) as lock:\n            assert lock.is_locked() and lock.by_me()\n\n    def test_acquire_break_reacquire(self, lockpath):\n        lock = ExclusiveLock(lockpath, id=ID1).acquire()\n        lock.break_lock()\n        with ExclusiveLock(lockpath, id=ID2):\n            pass\n\n    def test_timeout(self, lockpath):\n        with ExclusiveLock(lockpath, id=ID1):\n            with pytest.raises(LockTimeout):\n                ExclusiveLock(lockpath, id=ID2, timeout=0.1).acquire()\n\n    def test_kill_stale(self, lockpath, free_pid):\n        host, pid, tid = our_id = get_process_id()\n        dead_id = host, free_pid, tid\n        cant_know_if_dead_id = \"foo.bar.example.net\", 1, 2\n\n        dead_lock = ExclusiveLock(lockpath, id=dead_id).acquire()\n        with ExclusiveLock(lockpath, id=our_id):\n            with pytest.raises(NotMyLock):\n                dead_lock.release()\n        with pytest.raises(NotLocked):\n            dead_lock.release()\n\n        with ExclusiveLock(lockpath, id=cant_know_if_dead_id):\n            with pytest.raises(LockTimeout):\n                ExclusiveLock(lockpath, id=our_id, timeout=0.1).acquire()\n\n    def test_migrate_lock(self, lockpath):\n        old_id, new_id = ID1, ID2\n        assert old_id[1] != new_id[1]  # different PIDs (like when doing daemonize())\n        lock = ExclusiveLock(lockpath, id=old_id).acquire()\n        assert lock.id == old_id  # Lock is for the old ID/PID.\n        old_unique_name = lock.unique_name\n        assert lock.by_me()  # we have the lock\n        lock.migrate_lock(old_id, new_id)  # fix the lock\n        assert lock.id == new_id  # Lock corresponds to the new ID/PID.\n        new_unique_name = lock.unique_name\n        assert lock.by_me()  # we still have the lock\n        assert old_unique_name != new_unique_name  # Locking filename is different now.\n\n    @pytest.mark.skipif(is_win32, reason=\"broken on windows\")\n    def test_race_condition(self, lockpath):\n        class SynchronizedCounter:\n            def __init__(self, count=0):\n                self.lock = ThreadingLock()\n                self.count = count\n                self.maxcount = count\n\n            def value(self):\n                with self.lock:\n                    return self.count\n\n            def maxvalue(self):\n                with self.lock:\n                    return self.maxcount\n\n            def incr(self):\n                with self.lock:\n                    self.count += 1\n                    if self.count > self.maxcount:\n                        self.maxcount = self.count\n                    return self.count\n\n            def decr(self):\n                with self.lock:\n                    self.count -= 1\n                    return self.count\n\n        def print_locked(msg):\n            with print_lock:\n                print(msg)\n\n        def acquire_release_loop(\n            id, timeout, thread_id, lock_owner_counter, exception_counter, print_lock, last_thread=None\n        ):\n            print_locked(\n                \"Thread %2d: Starting acquire_release_loop(id=%s, timeout=%d); lockpath=%s\"\n                % (thread_id, id, timeout, lockpath)\n            )\n            timer = TimeoutTimer(timeout, -1).start()\n            cycle = 0\n\n            while not timer.timed_out():\n                cycle += 1\n                try:\n                    # This timeout is only for not exceeding the given timeout by more than 5%.\n                    # With sleep<0 it's constantly polling anyway.\n                    with ExclusiveLock(lockpath, id=id, timeout=timeout / 20, sleep=-1):\n                        lock_owner_count = lock_owner_counter.incr()\n                        print_locked(\n                            \"Thread %2d: Acquired the lock. It's my %d. loop cycle. \"\n                            \"I am the %d. who has the lock concurrently.\" % (thread_id, cycle, lock_owner_count)\n                        )\n                        time.sleep(0.005)\n                        lock_owner_count = lock_owner_counter.decr()\n                        print_locked(\n                            \"Thread %2d: Releasing the lock, finishing my %d. loop cycle. \"\n                            \"Currently, %d colleagues still have the lock.\" % (thread_id, cycle, lock_owner_count)\n                        )\n                except LockTimeout:\n                    print_locked(\"Thread %2d: Got LockTimeout, finishing my %d. loop cycle.\" % (thread_id, cycle))\n                except:  # noqa\n                    exception_count = exception_counter.incr()\n                    e = format_exc()\n                    print_locked(\n                        \"Thread %2d: Exception thrown, finishing my %d. loop cycle. \"\n                        \"It's the %d. exception seen until now: %s\" % (thread_id, cycle, exception_count, e)\n                    )\n\n            print_locked(\"Thread %2d: Loop timed out--terminating after %d loop cycles.\" % (thread_id, cycle))\n            if last_thread is not None:  # joining its predecessor, if any\n                last_thread.join()\n\n        print(\"\")\n        lock_owner_counter = SynchronizedCounter()\n        exception_counter = SynchronizedCounter()\n        print_lock = ThreadingLock()\n        thread = None\n        host_id, process_id = \"differenthost\", 1234\n        for thread_id in range(RACE_TEST_NUM_THREADS):\n            thread = Thread(\n                target=acquire_release_loop,\n                args=(\n                    (host_id, process_id, thread_id),\n                    RACE_TEST_DURATION,\n                    thread_id,\n                    lock_owner_counter,\n                    exception_counter,\n                    print_lock,\n                    thread,\n                ),\n            )\n            thread.start()\n        thread.join()  # joining the last thread\n\n        assert lock_owner_counter.maxvalue() > 0, \"Never gained the lock? Something went wrong here...\"\n        assert (\n            lock_owner_counter.maxvalue() <= 1\n        ), \"Maximal number of concurrent lock holders was %d. So exclusivity is broken.\" % (\n            lock_owner_counter.maxvalue()\n        )\n        assert (\n            exception_counter.value() == 0\n        ), \"ExclusiveLock threw %d exceptions due to unclean concurrency handling.\" % (exception_counter.value())\n\n\nclass TestLock:\n    def test_shared(self, lockpath):\n        lock1 = Lock(lockpath, exclusive=False, id=ID1).acquire()\n        lock2 = Lock(lockpath, exclusive=False, id=ID2).acquire()\n        assert len(lock1._roster.get(SHARED)) == 2\n        assert len(lock1._roster.get(EXCLUSIVE)) == 0\n        assert not lock1._roster.empty(SHARED, EXCLUSIVE)\n        assert lock1._roster.empty(EXCLUSIVE)\n        lock1.release()\n        lock2.release()\n\n    def test_exclusive(self, lockpath):\n        with Lock(lockpath, exclusive=True, id=ID1) as lock:\n            assert len(lock._roster.get(SHARED)) == 0\n            assert len(lock._roster.get(EXCLUSIVE)) == 1\n            assert not lock._roster.empty(SHARED, EXCLUSIVE)\n\n    def test_upgrade(self, lockpath):\n        with Lock(lockpath, exclusive=False) as lock:\n            lock.upgrade()\n            lock.upgrade()  # NOP\n            assert len(lock._roster.get(SHARED)) == 0\n            assert len(lock._roster.get(EXCLUSIVE)) == 1\n            assert not lock._roster.empty(SHARED, EXCLUSIVE)\n\n    def test_downgrade(self, lockpath):\n        with Lock(lockpath, exclusive=True) as lock:\n            lock.downgrade()\n            lock.downgrade()  # NOP\n            assert len(lock._roster.get(SHARED)) == 1\n            assert len(lock._roster.get(EXCLUSIVE)) == 0\n\n    def test_got_exclusive_lock(self, lockpath):\n        lock = Lock(lockpath, exclusive=True, id=ID1)\n        assert not lock.got_exclusive_lock()\n        lock.acquire()\n        assert lock.got_exclusive_lock()\n        lock.release()\n        assert not lock.got_exclusive_lock()\n\n    def test_break(self, lockpath):\n        lock = Lock(lockpath, exclusive=True, id=ID1).acquire()\n        lock.break_lock()\n        assert len(lock._roster.get(SHARED)) == 0\n        assert len(lock._roster.get(EXCLUSIVE)) == 0\n        with Lock(lockpath, exclusive=True, id=ID2):\n            pass\n\n    def test_timeout(self, lockpath):\n        with Lock(lockpath, exclusive=False, id=ID1):\n            with pytest.raises(LockTimeout):\n                Lock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire()\n        with Lock(lockpath, exclusive=True, id=ID1):\n            with pytest.raises(LockTimeout):\n                Lock(lockpath, exclusive=False, id=ID2, timeout=0.1).acquire()\n        with Lock(lockpath, exclusive=True, id=ID1):\n            with pytest.raises(LockTimeout):\n                Lock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire()\n\n    def test_kill_stale(self, lockpath, free_pid):\n        host, pid, tid = our_id = get_process_id()\n        dead_id = host, free_pid, tid\n        cant_know_if_dead_id = \"foo.bar.example.net\", 1, 2\n\n        dead_lock = Lock(lockpath, id=dead_id, exclusive=True).acquire()\n        roster = dead_lock._roster\n        with Lock(lockpath, id=our_id):\n            assert roster.get(EXCLUSIVE) == set()\n            assert roster.get(SHARED) == {our_id}\n        assert roster.get(EXCLUSIVE) == set()\n        assert roster.get(SHARED) == set()\n        with pytest.raises(NotLocked):\n            dead_lock.release()\n\n        with Lock(lockpath, id=cant_know_if_dead_id, exclusive=True):\n            with pytest.raises(LockTimeout):\n                Lock(lockpath, id=our_id, timeout=0.1).acquire()\n\n    def test_migrate_lock(self, lockpath):\n        old_id, new_id = ID1, ID2\n        assert old_id[1] != new_id[1]  # different PIDs (like when doing daemonize())\n\n        lock = Lock(lockpath, id=old_id, exclusive=True).acquire()\n        assert lock.id == old_id\n        lock.migrate_lock(old_id, new_id)  # fix the lock\n        assert lock.id == new_id\n        lock.release()\n\n        lock = Lock(lockpath, id=old_id, exclusive=False).acquire()\n        assert lock.id == old_id\n        lock.migrate_lock(old_id, new_id)  # fix the lock\n        assert lock.id == new_id\n        lock.release()\n\n\n@pytest.fixture()\ndef rosterpath(tmpdir):\n    return Path(tmpdir) / \"roster\"\n\n\nclass TestLockRoster:\n    def test_empty(self, rosterpath):\n        roster = LockRoster(rosterpath)\n        empty = roster.load()\n        roster.save(empty)\n        assert empty == {}\n\n    def test_modify_get(self, rosterpath):\n        roster1 = LockRoster(rosterpath, id=ID1)\n        assert roster1.get(SHARED) == set()\n        roster1.modify(SHARED, ADD)\n        assert roster1.get(SHARED) == {ID1}\n        roster2 = LockRoster(rosterpath, id=ID2)\n        roster2.modify(SHARED, ADD)\n        assert roster2.get(SHARED) == {ID1, ID2}\n        roster1 = LockRoster(rosterpath, id=ID1)\n        roster1.modify(SHARED, REMOVE)\n        assert roster1.get(SHARED) == {ID2}\n        roster2 = LockRoster(rosterpath, id=ID2)\n        roster2.modify(SHARED, REMOVE)\n        assert roster2.get(SHARED) == set()\n\n    def test_kill_stale(self, rosterpath, free_pid):\n        host, pid, tid = our_id = get_process_id()\n        dead_id = host, free_pid, tid\n\n        # put a dead local process lock into roster\n        roster1 = LockRoster(rosterpath, id=dead_id)\n        roster1.kill_stale_locks = False\n        assert roster1.get(SHARED) == set()\n        roster1.modify(SHARED, ADD)\n        assert roster1.get(SHARED) == {dead_id}\n\n        # put a unknown-state remote process lock into roster\n        cant_know_if_dead_id = \"foo.bar.example.net\", 1, 2\n        roster1 = LockRoster(rosterpath, id=cant_know_if_dead_id)\n        roster1.kill_stale_locks = False\n        assert roster1.get(SHARED) == {dead_id}\n        roster1.modify(SHARED, ADD)\n        assert roster1.get(SHARED) == {dead_id, cant_know_if_dead_id}\n\n        killer_roster = LockRoster(rosterpath)\n        # Active kill_stale_locks here - does it kill the dead_id lock?\n        assert killer_roster.get(SHARED) == {cant_know_if_dead_id}\n        killer_roster.modify(SHARED, ADD)\n        assert killer_roster.get(SHARED) == {our_id, cant_know_if_dead_id}\n\n        other_killer_roster = LockRoster(rosterpath)\n        # Active kill_stale_locks here - must not kill our_id lock since we're alive.\n        assert other_killer_roster.get(SHARED) == {our_id, cant_know_if_dead_id}\n\n    def test_migrate_lock(self, rosterpath):\n        old_id, new_id = ID1, ID2\n        assert old_id[1] != new_id[1]  # different PIDs (like when doing daemonize())\n        roster = LockRoster(rosterpath, id=old_id)\n        assert roster.id == old_id\n        roster.modify(SHARED, ADD)\n        assert roster.get(SHARED) == {old_id}\n        roster.migrate_lock(SHARED, old_id, new_id)  # fix the lock\n        assert roster.id == new_id\n        assert roster.get(SHARED) == {new_id}\n"
  },
  {
    "path": "src/borg/testsuite/hashindex_test.py",
    "content": "import hashlib\nimport struct\n\nimport pytest\n\nfrom ..hashindex import ChunkIndex, ChunkIndexEntry\n\n\ndef H(x):\n    # Make a 32-byte value that depends on x\n    return bytes(\"%-0.32d\" % x, \"ascii\")\n\n\ndef H2(x):\n    # Like H(x), but with a pseudo-random distribution of the output value\n    return hashlib.sha256(H(x)).digest()\n\n\ndef test_chunkindex_add():\n    chunks = ChunkIndex()\n    x = H2(1)\n    chunks.add(x, 0)\n    assert chunks[x] == ChunkIndexEntry(flags=ChunkIndex.F_USED, size=0)\n    chunks.add(x, 2)  # updating size (we do not have a size yet)\n    assert chunks[x] == ChunkIndexEntry(flags=ChunkIndex.F_USED, size=2)\n    chunks.add(x, 2)\n    assert chunks[x] == ChunkIndexEntry(flags=ChunkIndex.F_USED, size=2)\n    with pytest.raises(AssertionError):\n        chunks.add(x, 3)  # inconsistent size (we already have a different size)\n\n\ndef test_keyerror():\n    chunks = ChunkIndex()\n    x = H2(1)\n    with pytest.raises(KeyError):\n        chunks[x]\n    with pytest.raises(struct.error):\n        chunks[x] = ChunkIndexEntry(flags=ChunkIndex.F_NONE, size=2**33)\n\n\ndef test_new():\n    def new_chunks():\n        return list(chunks.iteritems(only_new=True))\n\n    chunks = ChunkIndex()\n    key1, value1a = H2(1), ChunkIndexEntry(flags=ChunkIndex.F_USED, size=23)\n    key2, value2a = H2(2), ChunkIndexEntry(flags=ChunkIndex.F_USED, size=42)\n    # Tracking of new entries\n    assert new_chunks() == []\n    chunks[key1] = value1a\n    assert new_chunks() == [(key1, value1a)]\n    chunks.clear_new()\n    assert new_chunks() == []\n    chunks[key2] = value2a\n    assert new_chunks() == [(key2, value2a)]\n    chunks.clear_new()\n    assert new_chunks() == []\n"
  },
  {
    "path": "src/borg/testsuite/helpers/__init__.py",
    "content": ""
  },
  {
    "path": "src/borg/testsuite/helpers/__init__test.py",
    "content": "import pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers import classify_ec, max_ec\n\n\n@pytest.mark.parametrize(\n    \"ec_range,ec_class\",\n    (\n        # Inclusive range start; exclusive range end.\n        ((0, 1), \"success\"),\n        ((1, 2), \"warning\"),\n        ((2, 3), \"error\"),\n        ((EXIT_ERROR_BASE, EXIT_WARNING_BASE), \"error\"),\n        ((EXIT_WARNING_BASE, EXIT_SIGNAL_BASE), \"warning\"),\n        ((EXIT_SIGNAL_BASE, 256), \"signal\"),\n    ),\n)\ndef test_classify_ec(ec_range, ec_class):\n    for ec in range(*ec_range):\n        classify_ec(ec) == ec_class\n\n\ndef test_ec_invalid():\n    with pytest.raises(ValueError):\n        classify_ec(666)\n    with pytest.raises(ValueError):\n        classify_ec(-1)\n    with pytest.raises(TypeError):\n        classify_ec(None)\n\n\n@pytest.mark.parametrize(\n    \"ec1,ec2,ec_max\",\n    (\n        # Same for modern/legacy.\n        (EXIT_SUCCESS, EXIT_SUCCESS, EXIT_SUCCESS),\n        (EXIT_SUCCESS, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),\n        # legacy exit codes\n        (EXIT_SUCCESS, EXIT_WARNING, EXIT_WARNING),\n        (EXIT_SUCCESS, EXIT_ERROR, EXIT_ERROR),\n        (EXIT_WARNING, EXIT_SUCCESS, EXIT_WARNING),\n        (EXIT_WARNING, EXIT_WARNING, EXIT_WARNING),\n        (EXIT_WARNING, EXIT_ERROR, EXIT_ERROR),\n        (EXIT_WARNING, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),\n        (EXIT_ERROR, EXIT_SUCCESS, EXIT_ERROR),\n        (EXIT_ERROR, EXIT_WARNING, EXIT_ERROR),\n        (EXIT_ERROR, EXIT_ERROR, EXIT_ERROR),\n        (EXIT_ERROR, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),\n        # some modern codes\n        (EXIT_SUCCESS, EXIT_WARNING_BASE, EXIT_WARNING_BASE),\n        (EXIT_SUCCESS, EXIT_ERROR_BASE, EXIT_ERROR_BASE),\n        (EXIT_WARNING_BASE, EXIT_SUCCESS, EXIT_WARNING_BASE),\n        (EXIT_WARNING_BASE + 1, EXIT_WARNING_BASE + 2, EXIT_WARNING_BASE + 1),\n        (EXIT_WARNING_BASE, EXIT_ERROR_BASE, EXIT_ERROR_BASE),\n        (EXIT_WARNING_BASE, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),\n        (EXIT_ERROR_BASE, EXIT_SUCCESS, EXIT_ERROR_BASE),\n        (EXIT_ERROR_BASE, EXIT_WARNING_BASE, EXIT_ERROR_BASE),\n        (EXIT_ERROR_BASE + 1, EXIT_ERROR_BASE + 2, EXIT_ERROR_BASE + 1),\n        (EXIT_ERROR_BASE, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),\n    ),\n)\ndef test_max_ec(ec1, ec2, ec_max):\n    assert max_ec(ec1, ec2) == ec_max\n"
  },
  {
    "path": "src/borg/testsuite/helpers/datastruct_test.py",
    "content": "import hashlib\nimport pytest\n\nfrom ...helpers.datastruct import StableDict, Buffer\nfrom ...helpers import msgpack\n\n\ndef test_stable_dict():\n    d = StableDict(foo=1, bar=2, boo=3, baz=4)\n    assert list(d.items()) == [(\"bar\", 2), (\"baz\", 4), (\"boo\", 3), (\"foo\", 1)]\n    assert hashlib.md5(msgpack.packb(d)).hexdigest() == \"fc78df42cd60691b3ac3dd2a2b39903f\"\n\n\nclass TestBuffer:\n    def test_type(self):\n        buffer = Buffer(bytearray)\n        assert isinstance(buffer.get(), bytearray)\n        buffer = Buffer(bytes)  # Do not do that in practice.\n        assert isinstance(buffer.get(), bytes)\n\n    def test_len(self):\n        buffer = Buffer(bytearray, size=0)\n        b = buffer.get()\n        assert len(buffer) == len(b) == 0\n        buffer = Buffer(bytearray, size=1234)\n        b = buffer.get()\n        assert len(buffer) == len(b) == 1234\n\n    def test_resize(self):\n        buffer = Buffer(bytearray, size=100)\n        assert len(buffer) == 100\n        b1 = buffer.get()\n        buffer.resize(200)\n        assert len(buffer) == 200\n        b2 = buffer.get()\n        assert b2 is not b1  # New, bigger buffer.\n        buffer.resize(100)\n        assert len(buffer) >= 100\n        b3 = buffer.get()\n        assert b3 is b2  # Still the same buffer (200).\n        buffer.resize(100, init=True)\n        assert len(buffer) == 100  # Except on init.\n        b4 = buffer.get()\n        assert b4 is not b3  # New, smaller buffer.\n\n    def test_limit(self):\n        buffer = Buffer(bytearray, size=100, limit=200)\n        buffer.resize(200)\n        assert len(buffer) == 200\n        with pytest.raises(Buffer.MemoryLimitExceeded):\n            buffer.resize(201)\n        assert len(buffer) == 200\n\n    def test_get(self):\n        buffer = Buffer(bytearray, size=100, limit=200)\n        b1 = buffer.get(50)\n        assert len(b1) >= 50  # == 100\n        b2 = buffer.get(100)\n        assert len(b2) >= 100  # == 100\n        assert b2 is b1  # Did not need resizing yet.\n        b3 = buffer.get(200)\n        assert len(b3) == 200\n        assert b3 is not b2  # New, resized buffer.\n        with pytest.raises(Buffer.MemoryLimitExceeded):\n            buffer.get(201)  # Beyond limit.\n        assert len(buffer) == 200\n"
  },
  {
    "path": "src/borg/testsuite/helpers/efficient_collection_queue_test.py",
    "content": "import pytest\n\nfrom ...helpers.datastruct import EfficientCollectionQueue\n\n\nclass TestEfficientQueue:\n    def test_base_usage(self):\n        queue = EfficientCollectionQueue(100, bytes)\n        assert queue.peek_front() == b\"\"\n        queue.push_back(b\"1234\")\n        assert queue.peek_front() == b\"1234\"\n        assert len(queue) == 4\n        assert queue\n        queue.pop_front(4)\n        assert queue.peek_front() == b\"\"\n        assert len(queue) == 0\n        assert not queue\n\n    def test_usage_with_arrays(self):\n        queue = EfficientCollectionQueue(100, list)\n        assert queue.peek_front() == []\n        queue.push_back([1, 2, 3, 4])\n        assert queue.peek_front() == [1, 2, 3, 4]\n        assert len(queue) == 4\n        assert queue\n        queue.pop_front(4)\n        assert queue.peek_front() == []\n        assert len(queue) == 0\n        assert not queue\n\n    def test_chunking(self):\n        queue = EfficientCollectionQueue(2, bytes)\n        queue.push_back(b\"1\")\n        queue.push_back(b\"23\")\n        queue.push_back(b\"4567\")\n        assert len(queue) == 7\n        assert queue.peek_front() == b\"12\"\n        queue.pop_front(3)\n        assert queue.peek_front() == b\"4\"\n        queue.pop_front(1)\n        assert queue.peek_front() == b\"56\"\n        queue.pop_front(2)\n        assert len(queue) == 1\n        assert queue\n        with pytest.raises(EfficientCollectionQueue.SizeUnderflow):\n            queue.pop_front(2)\n        assert queue.peek_front() == b\"7\"\n        queue.pop_front(1)\n        assert queue.peek_front() == b\"\"\n        assert len(queue) == 0\n        assert not queue\n"
  },
  {
    "path": "src/borg/testsuite/helpers/fs_test.py",
    "content": "import errno\nimport os\nimport sys\nfrom contextlib import contextmanager\nfrom pathlib import Path\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...constants import CACHE_TAG_NAME, CACHE_TAG_CONTENTS\nfrom ...helpers.fs import (\n    dir_is_tagged,\n    get_base_dir,\n    get_cache_dir,\n    get_keys_dir,\n    get_security_dir,\n    get_config_dir,\n    get_runtime_dir,\n    dash_open,\n    safe_unlink,\n    remove_dotdot_prefixes,\n    make_path_safe,\n    map_chars,\n)\nfrom ...platform import is_win32, is_darwin, is_haiku, is_cygwin\nfrom .. import are_hardlinks_supported\nfrom .. import rejected_dotdot_paths\n\n\ndef test_get_base_dir(monkeypatch):\n    \"\"\"test that get_base_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    monkeypatch.delenv(\"HOME\", raising=False)\n    monkeypatch.delenv(\"USER\", raising=False)\n    assert get_base_dir(legacy=True) == os.path.expanduser(\"~\")\n    # Haiku OS is a single-user OS, expanding \"~root\" is not supported.\n    if not (is_haiku or is_cygwin):\n        monkeypatch.setenv(\"USER\", \"root\")\n        assert get_base_dir(legacy=True) == os.path.expanduser(\"~root\")\n    monkeypatch.setenv(\"HOME\", \"/var/tmp/home\")\n    assert get_base_dir(legacy=True) == \"/var/tmp/home\"\n    monkeypatch.setenv(\"BORG_BASE_DIR\", \"/var/tmp/base\")\n    assert get_base_dir(legacy=True) == \"/var/tmp/base\"\n    # non-legacy is much easier:\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    assert get_base_dir(legacy=False) is None\n    monkeypatch.setenv(\"BORG_BASE_DIR\", \"/var/tmp/base\")\n    assert get_base_dir(legacy=False) == \"/var/tmp/base\"\n\n\ndef test_get_base_dir_compat(monkeypatch):\n    \"\"\"test that it works the same for legacy and for non-legacy implementation\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    # old way: if BORG_BASE_DIR is not set, make something up with HOME/USER/~\n    # new way: if BORG_BASE_DIR is not set, return None and let caller deal with it.\n    assert get_base_dir(legacy=False) is None\n    # new and old way: BORG_BASE_DIR overrides all other \"base path determination\".\n    monkeypatch.setenv(\"BORG_BASE_DIR\", \"/var/tmp/base\")\n    assert get_base_dir(legacy=False) == get_base_dir(legacy=True)\n\n\ndef test_get_config_dir(monkeypatch):\n    \"\"\"test that get_config_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    home_dir = os.path.expanduser(\"~\")\n    if is_win32:\n        monkeypatch.delenv(\"BORG_CONFIG_DIR\", raising=False)\n        assert get_config_dir(create=False) == os.path.join(home_dir, \"AppData\", \"Local\", \"borg\", \"borg\")\n        monkeypatch.setenv(\"BORG_CONFIG_DIR\", home_dir)\n        assert get_config_dir(create=False) == home_dir\n    elif is_darwin:\n        monkeypatch.delenv(\"BORG_CONFIG_DIR\", raising=False)\n        assert get_config_dir(create=False) == os.path.join(home_dir, \"Library\", \"Application Support\", \"borg\")\n        monkeypatch.setenv(\"BORG_CONFIG_DIR\", \"/var/tmp\")\n        assert get_config_dir(create=False) == \"/var/tmp\"\n    else:\n        monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n        monkeypatch.delenv(\"BORG_CONFIG_DIR\", raising=False)\n        assert get_config_dir(create=False) == os.path.join(home_dir, \".config\", \"borg\")\n        monkeypatch.setenv(\"XDG_CONFIG_HOME\", \"/var/tmp/.config\")\n        assert get_config_dir(create=False) == os.path.join(\"/var/tmp/.config\", \"borg\")\n        monkeypatch.setenv(\"BORG_CONFIG_DIR\", \"/var/tmp\")\n        assert get_config_dir(create=False) == \"/var/tmp\"\n\n\ndef test_get_config_dir_compat(monkeypatch):\n    \"\"\"test that it works the same for legacy and for non-legacy implementation\"\"\"\n    monkeypatch.delenv(\"BORG_CONFIG_DIR\", raising=False)\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n    if not is_darwin and not is_win32:\n        # fails on macOS: assert '/Users/tw/Library/Application Support/borg' == '/Users/tw/.config/borg'\n        # fails on win32 MSYS2 (but we do not need legacy compat there).\n        assert get_config_dir(legacy=False, create=False) == get_config_dir(legacy=True, create=False)\n        monkeypatch.setenv(\"XDG_CONFIG_HOME\", \"/var/tmp/xdg.config.d\")\n        # fails on macOS: assert '/Users/tw/Library/Application Support/borg' == '/var/tmp/xdg.config.d'\n        # fails on win32 MSYS2 (but we do not need legacy compat there).\n        assert get_config_dir(legacy=False, create=False) == get_config_dir(legacy=True, create=False)\n    monkeypatch.setenv(\"BORG_BASE_DIR\", \"/var/tmp/base\")\n    assert get_config_dir(legacy=False, create=False) == get_config_dir(legacy=True, create=False)\n    monkeypatch.setenv(\"BORG_CONFIG_DIR\", \"/var/tmp/borg.config.d\")\n    assert get_config_dir(legacy=False, create=False) == get_config_dir(legacy=True, create=False)\n\n\ndef test_get_cache_dir(monkeypatch):\n    \"\"\"test that get_cache_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    home_dir = os.path.expanduser(\"~\")\n    if is_win32:\n        monkeypatch.delenv(\"BORG_CACHE_DIR\", raising=False)\n        assert get_cache_dir(create=False) == os.path.join(home_dir, \"AppData\", \"Local\", \"borg\", \"borg\", \"Cache\")\n        monkeypatch.setenv(\"BORG_CACHE_DIR\", home_dir)\n        assert get_cache_dir(create=False) == home_dir\n    elif is_darwin:\n        monkeypatch.delenv(\"BORG_CACHE_DIR\", raising=False)\n        assert get_cache_dir(create=False) == os.path.join(home_dir, \"Library\", \"Caches\", \"borg\")\n        monkeypatch.setenv(\"BORG_CACHE_DIR\", \"/var/tmp\")\n        assert get_cache_dir(create=False) == \"/var/tmp\"\n    else:\n        monkeypatch.delenv(\"XDG_CACHE_HOME\", raising=False)\n        monkeypatch.delenv(\"BORG_CACHE_DIR\", raising=False)\n        assert get_cache_dir(create=False) == os.path.join(home_dir, \".cache\", \"borg\")\n        monkeypatch.setenv(\"XDG_CACHE_HOME\", \"/var/tmp/.cache\")\n        assert get_cache_dir(create=False) == os.path.join(\"/var/tmp/.cache\", \"borg\")\n        monkeypatch.setenv(\"BORG_CACHE_DIR\", \"/var/tmp\")\n        assert get_cache_dir(create=False) == \"/var/tmp\"\n\n\ndef test_get_cache_dir_compat(monkeypatch):\n    \"\"\"test that it works the same for legacy and for non-legacy implementation\"\"\"\n    monkeypatch.delenv(\"BORG_CACHE_DIR\", raising=False)\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    monkeypatch.delenv(\"XDG_CACHE_HOME\", raising=False)\n    if not is_darwin and not is_win32:\n        # fails on macOS: assert '/Users/tw/Library/Caches/borg' == '/Users/tw/.cache/borg'\n        # fails on win32 MSYS2 (but we do not need legacy compat there).\n        assert get_cache_dir(legacy=False, create=False) == get_cache_dir(legacy=True, create=False)\n        # fails on macOS: assert '/Users/tw/Library/Caches/borg' == '/var/tmp/xdg.cache.d'\n        # fails on win32 MSYS2 (but we do not need legacy compat there).\n        monkeypatch.setenv(\"XDG_CACHE_HOME\", \"/var/tmp/xdg.cache.d\")\n        assert get_cache_dir(legacy=False, create=False) == get_cache_dir(legacy=True, create=False)\n    monkeypatch.setenv(\"BORG_BASE_DIR\", \"/var/tmp/base\")\n    assert get_cache_dir(legacy=False, create=False) == get_cache_dir(legacy=True, create=False)\n    monkeypatch.setenv(\"BORG_CACHE_DIR\", \"/var/tmp/borg.cache.d\")\n    assert get_cache_dir(legacy=False, create=False) == get_cache_dir(legacy=True, create=False)\n\n\ndef test_get_keys_dir(monkeypatch):\n    \"\"\"test that get_keys_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    home_dir = os.path.expanduser(\"~\")\n    if is_win32:\n        monkeypatch.delenv(\"BORG_KEYS_DIR\", raising=False)\n        assert get_keys_dir(create=False) == os.path.join(home_dir, \"AppData\", \"Local\", \"borg\", \"borg\", \"keys\")\n        monkeypatch.setenv(\"BORG_KEYS_DIR\", home_dir)\n        assert get_keys_dir(create=False) == home_dir\n    elif is_darwin:\n        monkeypatch.delenv(\"BORG_KEYS_DIR\", raising=False)\n        assert get_keys_dir(create=False) == os.path.join(home_dir, \"Library\", \"Application Support\", \"borg\", \"keys\")\n        monkeypatch.setenv(\"BORG_KEYS_DIR\", \"/var/tmp\")\n        assert get_keys_dir(create=False) == \"/var/tmp\"\n    else:\n        monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n        monkeypatch.delenv(\"BORG_KEYS_DIR\", raising=False)\n        assert get_keys_dir(create=False) == os.path.join(home_dir, \".config\", \"borg\", \"keys\")\n        monkeypatch.setenv(\"XDG_CONFIG_HOME\", \"/var/tmp/.config\")\n        assert get_keys_dir(create=False) == os.path.join(\"/var/tmp/.config\", \"borg\", \"keys\")\n        monkeypatch.setenv(\"BORG_KEYS_DIR\", \"/var/tmp\")\n        assert get_keys_dir(create=False) == \"/var/tmp\"\n\n\ndef test_get_security_dir(monkeypatch):\n    \"\"\"test that get_security_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    home_dir = os.path.expanduser(\"~\")\n    if is_win32:\n        monkeypatch.delenv(\"BORG_SECURITY_DIR\", raising=False)\n        assert get_security_dir(create=False) == os.path.join(home_dir, \"AppData\", \"Local\", \"borg\", \"borg\", \"security\")\n        assert get_security_dir(repository_id=\"1234\", create=False) == os.path.join(\n            home_dir, \"AppData\", \"Local\", \"borg\", \"borg\", \"security\", \"1234\"\n        )\n        monkeypatch.setenv(\"BORG_SECURITY_DIR\", home_dir)\n        assert get_security_dir(create=False) == home_dir\n    elif is_darwin:\n        monkeypatch.delenv(\"BORG_SECURITY_DIR\", raising=False)\n        assert get_security_dir(create=False) == os.path.join(\n            home_dir, \"Library\", \"Application Support\", \"borg\", \"security\"\n        )\n        assert get_security_dir(repository_id=\"1234\", create=False) == os.path.join(\n            home_dir, \"Library\", \"Application Support\", \"borg\", \"security\", \"1234\"\n        )\n        monkeypatch.setenv(\"BORG_SECURITY_DIR\", \"/var/tmp\")\n        assert get_security_dir(create=False) == \"/var/tmp\"\n    else:\n        monkeypatch.delenv(\"XDG_DATA_HOME\", raising=False)\n        monkeypatch.delenv(\"BORG_SECURITY_DIR\", raising=False)\n        assert get_security_dir(create=False) == os.path.join(home_dir, \".local\", \"share\", \"borg\", \"security\")\n        assert get_security_dir(repository_id=\"1234\", create=False) == os.path.join(\n            home_dir, \".local\", \"share\", \"borg\", \"security\", \"1234\"\n        )\n        monkeypatch.setenv(\"XDG_DATA_HOME\", \"/var/tmp/.config\")\n        assert get_security_dir(create=False) == os.path.join(\"/var/tmp/.config\", \"borg\", \"security\")\n        monkeypatch.setenv(\"BORG_SECURITY_DIR\", \"/var/tmp\")\n        assert get_security_dir(create=False) == \"/var/tmp\"\n\n\ndef test_get_runtime_dir(monkeypatch):\n    \"\"\"test that get_runtime_dir respects environment\"\"\"\n    monkeypatch.delenv(\"BORG_BASE_DIR\", raising=False)\n    home_dir = os.path.expanduser(\"~\")\n    if is_win32:\n        monkeypatch.delenv(\"BORG_RUNTIME_DIR\", raising=False)\n        assert get_runtime_dir(create=False) == os.path.join(home_dir, \"AppData\", \"Local\", \"Temp\", \"borg\", \"borg\")\n        monkeypatch.setenv(\"BORG_RUNTIME_DIR\", home_dir)\n        assert get_runtime_dir(create=False) == home_dir\n    elif is_darwin:\n        monkeypatch.delenv(\"BORG_RUNTIME_DIR\", raising=False)\n        assert get_runtime_dir(create=False) == os.path.join(home_dir, \"Library\", \"Caches\", \"TemporaryItems\", \"borg\")\n        monkeypatch.setenv(\"BORG_RUNTIME_DIR\", \"/var/tmp\")\n        assert get_runtime_dir(create=False) == \"/var/tmp\"\n    else:\n        monkeypatch.delenv(\"XDG_RUNTIME_DIR\", raising=False)\n        monkeypatch.delenv(\"BORG_RUNTIME_DIR\", raising=False)\n        uid = str(os.getuid())\n        assert get_runtime_dir(create=False) in [\n            os.path.join(\"/run/user\", uid, \"borg\"),\n            os.path.join(\"/var/run/user\", uid, \"borg\"),\n            os.path.join(f\"/tmp/runtime-{uid}\", \"borg\"),\n            os.path.join(f\"/mnt/eafs/tmp/runtime-{uid}\", \"borg\"),  # CI netbsd\n        ]\n        monkeypatch.setenv(\"XDG_RUNTIME_DIR\", \"/var/tmp/.cache\")\n        assert get_runtime_dir(create=False) == os.path.join(\"/var/tmp/.cache\", \"borg\")\n        monkeypatch.setenv(\"BORG_RUNTIME_DIR\", \"/var/tmp\")\n        assert get_runtime_dir(create=False) == \"/var/tmp\"\n\n\ndef test_dash_open():\n    assert dash_open(\"-\", \"r\") is sys.stdin\n    assert dash_open(\"-\", \"w\") is sys.stdout\n    assert dash_open(\"-\", \"rb\") is sys.stdin.buffer\n    assert dash_open(\"-\", \"wb\") is sys.stdout.buffer\n\n\n@pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hard links not supported\")\ndef test_safe_unlink_is_safe(tmpdir):\n    contents = b\"Hello, world\\n\"\n    victim = tmpdir / \"victim\"\n    victim.write_binary(contents)\n    hard_link = tmpdir / \"hardlink\"\n    os.link(str(victim), str(hard_link))  # hard_link.mklinkto is not implemented on win32\n\n    safe_unlink(hard_link)\n\n    assert victim.read_binary() == contents\n\n\n@pytest.mark.skipif(not are_hardlinks_supported(), reason=\"hard links not supported\")\ndef test_safe_unlink_is_safe_ENOSPC(tmpdir, monkeypatch):\n    contents = b\"Hello, world\\n\"\n    victim = tmpdir / \"victim\"\n    victim.write_binary(contents)\n    hard_link = tmpdir / \"hardlink\"\n    os.link(str(victim), str(hard_link))  # hard_link.mklinkto is not implemented on win32\n\n    def Path_unlink(_):\n        raise OSError(errno.ENOSPC, \"Pretend that we ran out of space\")\n\n    monkeypatch.setattr(Path, \"unlink\", Path_unlink)\n\n    with pytest.raises(OSError):\n        safe_unlink(hard_link)\n\n    assert victim.read_binary() == contents\n\n\n@pytest.mark.parametrize(\n    \"original_path, expected_path\",\n    [\n        (\".\", \".\"),\n        (\"..\", \".\"),\n        (\"/\", \".\"),\n        (\"//\", \".\"),\n        (\"foo\", \"foo\"),\n        (\"foo/bar\", \"foo/bar\"),\n        (\"/foo/bar\", \"foo/bar\"),\n        (\"../foo/bar\", \"foo/bar\"),\n    ],\n)\ndef test_remove_dotdot_prefixes(original_path, expected_path):\n    assert remove_dotdot_prefixes(original_path) == expected_path\n\n\n@pytest.mark.parametrize(\n    \"original_path, expected_path\",\n    [\n        (\".\", \".\"),\n        (\"./\", \".\"),\n        (\"/foo\", \"foo\"),\n        (\"//foo\", \"foo\"),\n        (\".//foo//bar//\", \"foo/bar\"),\n        (\"/foo/bar\", \"foo/bar\"),\n        (\"//foo/bar\", \"foo/bar\"),\n        (\"//foo/./bar\", \"foo/bar\"),\n        (\".test\", \".test\"),\n        (\".test.\", \".test.\"),\n        (\"..test..\", \"..test..\"),\n        (\"/te..st/foo/bar\", \"te..st/foo/bar\"),\n        (\"/..test../abc//\", \"..test../abc\"),\n    ],\n)\ndef test_valid_make_path_safe(original_path, expected_path):\n    assert make_path_safe(original_path) == expected_path\n\n\n@pytest.mark.parametrize(\"path\", rejected_dotdot_paths)\ndef test_invalid_make_path_safe(path):\n    with pytest.raises(ValueError, match=\"unexpected '..' element in path\"):\n        make_path_safe(path)\n\n\ndef test_make_path_safe_win32_drive_letters(monkeypatch):\n    monkeypatch.setattr(\"borg.helpers.fs.is_win32\", True)\n    # Basic drive letter\n    assert make_path_safe(\"C:\\\\Users\") == \"C/Users\"\n    assert make_path_safe(\"C:\\\\Users\\\\test\") == \"C/Users/test\"\n    # Lowercase drive letter -> Uppercase\n    assert make_path_safe(\"c:\\\\windows\") == \"C/windows\"\n    # Relative path with backslashes\n    assert make_path_safe(\"foo\\\\bar\") == \"foo/bar\"\n    # Just drive letter\n    assert make_path_safe(\"D:\") == \"D\"\n    # Drive letter with forward slash\n    assert make_path_safe(\"E:/test\") == \"E/test\"\n    # Mixed separators\n    assert make_path_safe(\"F:\\\\Mixed/Separators\") == \"F/Mixed/Separators\"\n\n\ndef test_dir_is_tagged(tmpdir):\n    \"\"\"Test dir_is_tagged with both path-based and file descriptor-based operations.\"\"\"\n\n    @contextmanager\n    def open_dir(path):\n        fd = os.open(path, os.O_RDONLY)\n        try:\n            yield fd\n        finally:\n            os.close(fd)\n\n    # Create directories for testing exclude_caches\n    cache_dir = tmpdir.mkdir(\"cache_dir\")\n    cache_tag_path = cache_dir.join(CACHE_TAG_NAME)\n    cache_tag_path.write_binary(CACHE_TAG_CONTENTS)\n\n    invalid_cache_dir = tmpdir.mkdir(\"invalid_cache_dir\")\n    invalid_cache_tag_path = invalid_cache_dir.join(CACHE_TAG_NAME)\n    invalid_cache_tag_path.write_binary(b\"invalid signature\")\n\n    # Create directories for testing exclude_if_present\n    tagged_dir = tmpdir.mkdir(\"tagged_dir\")\n    tag_file = tagged_dir.join(\".NOBACKUP\")\n    tag_file.write(\"test\")\n\n    other_tagged_dir = tmpdir.mkdir(\"other_tagged_dir\")\n    other_tag_file = other_tagged_dir.join(\".DONOTBACKUP\")\n    other_tag_file.write(\"test\")\n\n    # Create a directory with both a CACHEDIR.TAG and a custom tag file\n    both_dir = tmpdir.mkdir(\"both_dir\")\n    cache_tag_path = both_dir.join(CACHE_TAG_NAME)\n    cache_tag_path.write_binary(CACHE_TAG_CONTENTS)\n    custom_tag_path = both_dir.join(\".NOBACKUP\")\n    custom_tag_path.write(\"test\")\n\n    # Create a directory without any tag files\n    normal_dir = tmpdir.mkdir(\"normal_dir\")\n\n    # Test edge cases\n    test_dir = tmpdir.mkdir(\"test_dir\")\n    assert dir_is_tagged(path=str(test_dir), exclude_caches=None, exclude_if_present=None) == []\n    assert dir_is_tagged(path=str(test_dir), exclude_if_present=[]) == []\n\n    # Test with non-existent directory (should not raise an exception)\n    non_existent_dir = str(tmpdir.join(\"non_existent\"))\n    result = dir_is_tagged(path=non_existent_dir, exclude_caches=True, exclude_if_present=[\".NOBACKUP\"])\n    assert result == []\n\n    # Test 1: exclude_caches with path-based operations\n    assert dir_is_tagged(path=str(cache_dir), exclude_caches=True) == [CACHE_TAG_NAME]\n    assert dir_is_tagged(path=str(invalid_cache_dir), exclude_caches=True) == []\n    assert dir_is_tagged(path=str(normal_dir), exclude_caches=True) == []\n\n    assert dir_is_tagged(path=str(cache_dir), exclude_caches=False) == []\n    assert dir_is_tagged(path=str(invalid_cache_dir), exclude_caches=False) == []\n    assert dir_is_tagged(path=str(normal_dir), exclude_caches=False) == []\n\n    # Test 2: exclude_caches with file-descriptor-based operations\n    if not is_win32:\n        with open_dir(str(cache_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == [CACHE_TAG_NAME]\n        with open_dir(str(invalid_cache_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []\n        with open_dir(str(normal_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []\n\n        with open_dir(str(cache_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []\n        with open_dir(str(invalid_cache_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []\n        with open_dir(str(normal_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []\n\n    # Test 3: exclude_if_present with path-based operations\n    tags = [\".NOBACKUP\"]\n    assert dir_is_tagged(path=str(tagged_dir), exclude_if_present=tags) == [\".NOBACKUP\"]\n    assert dir_is_tagged(path=str(other_tagged_dir), exclude_if_present=tags) == []\n    assert dir_is_tagged(path=str(normal_dir), exclude_if_present=tags) == []\n\n    tags = [\".NOBACKUP\", \".DONOTBACKUP\"]\n    assert dir_is_tagged(path=str(tagged_dir), exclude_if_present=tags) == [\".NOBACKUP\"]\n    assert dir_is_tagged(path=str(other_tagged_dir), exclude_if_present=tags) == [\".DONOTBACKUP\"]\n    assert dir_is_tagged(path=str(normal_dir), exclude_if_present=tags) == []\n\n    # Test 4: exclude_if_present with file descriptor-based operations\n    if not is_win32:\n        tags = [\".NOBACKUP\"]\n        with open_dir(str(tagged_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [\".NOBACKUP\"]\n        with open_dir(str(other_tagged_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []\n        with open_dir(str(normal_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []\n\n        tags = [\".NOBACKUP\", \".DONOTBACKUP\"]\n        with open_dir(str(tagged_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [\".NOBACKUP\"]\n        with open_dir(str(other_tagged_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [\".DONOTBACKUP\"]\n        with open_dir(str(normal_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []\n\n    # Test 5: both exclude types with path-based operations\n    assert sorted(dir_is_tagged(path=str(both_dir), exclude_caches=True, exclude_if_present=[\".NOBACKUP\"])) == [\n        \".NOBACKUP\",\n        CACHE_TAG_NAME,\n    ]\n    assert dir_is_tagged(path=str(cache_dir), exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == [CACHE_TAG_NAME]\n    assert dir_is_tagged(path=str(tagged_dir), exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == [\".NOBACKUP\"]\n    assert dir_is_tagged(path=str(normal_dir), exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == []\n\n    # Test 6: both exclude types with file descriptor-based operations\n    if not is_win32:\n        with open_dir(str(both_dir)) as fd:\n            assert sorted(dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[\".NOBACKUP\"])) == [\n                \".NOBACKUP\",\n                CACHE_TAG_NAME,\n            ]\n        with open_dir(str(cache_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == [CACHE_TAG_NAME]\n        with open_dir(str(tagged_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == [\".NOBACKUP\"]\n        with open_dir(str(normal_dir)) as fd:\n            assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[\".NOBACKUP\"]) == []\n\n\ndef test_map_chars(monkeypatch):\n    # Test behavior on non-Windows (should return path unchanged)\n    monkeypatch.setattr(\"borg.helpers.fs.is_win32\", False)\n    assert map_chars(\"foo/bar\") == \"foo/bar\"\n    assert map_chars(\"foo\\\\bar\") == \"foo\\\\bar\"\n    assert map_chars(\"foo:bar\") == \"foo:bar\"\n\n    # Test behavior on Windows\n    monkeypatch.setattr(\"borg.helpers.fs.is_win32\", True)\n\n    # Reserved characters replacement\n    assert map_chars(\"foo:bar\") == \"foo\\uf03abar\"\n    assert map_chars(\"foo<bar\") == \"foo\\uf03cbar\"\n    assert map_chars(\"foo>bar\") == \"foo\\uf03ebar\"\n    assert map_chars('foo\"bar') == \"foo\\uf022bar\"\n    assert map_chars(\"foo\\\\bar\") == \"foo\\uf05cbar\"\n    assert map_chars(\"foo|bar\") == \"foo\\uf07cbar\"\n    assert map_chars(\"foo?bar\") == \"foo\\uf03fbar\"\n    assert map_chars(\"foo*bar\") == \"foo\\uf02abar\"\n"
  },
  {
    "path": "src/borg/testsuite/helpers/lrucache_test.py",
    "content": "from tempfile import TemporaryFile\n\nimport pytest\n\nfrom ...helpers.lrucache import LRUCache\n\n\nclass TestLRUCache:\n    def test_lrucache(self):\n        c = LRUCache(2)\n        assert len(c) == 0\n        assert c.items() == set()\n        for i, x in enumerate(\"abc\"):\n            c[x] = i\n        assert len(c) == 2\n        assert c.items() == {(\"b\", 1), (\"c\", 2)}\n        assert \"a\" not in c\n        assert \"b\" in c\n        with pytest.raises(KeyError):\n            c[\"a\"]\n        assert c.get(\"a\") is None\n        assert c.get(\"a\", \"foo\") == \"foo\"\n        assert c[\"b\"] == 1\n        assert c.get(\"b\") == 1\n        assert c[\"c\"] == 2\n        c[\"d\"] = 3\n        assert len(c) == 2\n        assert c[\"c\"] == 2\n        assert c[\"d\"] == 3\n        del c[\"c\"]\n        assert len(c) == 1\n        with pytest.raises(KeyError):\n            c[\"c\"]\n        assert c[\"d\"] == 3\n        c.clear()\n        assert c.items() == set()\n\n    def test_dispose(self):\n        c = LRUCache(2, dispose=lambda f: f.close())\n        f1 = TemporaryFile()\n        f2 = TemporaryFile()\n        f3 = TemporaryFile()\n        c[1] = f1\n        c[2] = f2\n        assert not f2.closed\n        c[3] = f3\n        assert 1 not in c\n        assert f1.closed\n        assert 2 in c\n        assert not f2.closed\n        del c[2]\n        assert 2 not in c\n        assert f2.closed\n        c.clear()\n        assert c.items() == set()\n        assert f3.closed\n"
  },
  {
    "path": "src/borg/testsuite/helpers/misc_test.py",
    "content": "from io import StringIO, BytesIO\n\nimport pytest\n\nfrom ...helpers.misc import ChunkIteratorFileWrapper, chunkit, iter_separated\n\n\ndef test_chunk_file_wrapper():\n    cfw = ChunkIteratorFileWrapper(iter([b\"abc\", b\"def\"]))\n    assert cfw.read(2) == b\"ab\"\n    assert cfw.read(50) == b\"cdef\"\n    assert cfw.exhausted\n\n    cfw = ChunkIteratorFileWrapper(iter([]))\n    assert cfw.read(2) == b\"\"\n    assert cfw.exhausted\n\n\ndef test_chunkit():\n    it = chunkit(\"abcdefg\", 3)\n    assert next(it) == [\"a\", \"b\", \"c\"]\n    assert next(it) == [\"d\", \"e\", \"f\"]\n    assert next(it) == [\"g\"]\n    with pytest.raises(StopIteration):\n        next(it)\n    with pytest.raises(StopIteration):\n        next(it)\n\n    it = chunkit(\"ab\", 3)\n    assert list(it) == [[\"a\", \"b\"]]\n\n    it = chunkit(\"\", 3)\n    assert list(it) == []\n\n\ndef test_iter_separated():\n    # Newline and UTF-8\n    sep, items = \"\\n\", [\"foo\", \"bar/baz\", \"αáčő\"]\n    fd = StringIO(sep.join(items))\n    assert list(iter_separated(fd)) == items\n    # NUL and bogus ending\n    sep, items = \"\\0\", [\"foo/bar\", \"baz\", \"spam\"]\n    fd = StringIO(sep.join(items) + \"\\0\")\n    assert list(iter_separated(fd, sep=sep)) == [\"foo/bar\", \"baz\", \"spam\"]\n    # Multi-character\n    sep, items = \"SEP\", [\"foo/bar\", \"baz\", \"spam\"]\n    fd = StringIO(sep.join(items))\n    assert list(iter_separated(fd, sep=sep)) == items\n    # Bytes\n    sep, items = b\"\\n\", [b\"foo\", b\"blop\\t\", b\"gr\\xe4ezi\"]\n    fd = BytesIO(sep.join(items))\n    assert list(iter_separated(fd)) == items\n"
  },
  {
    "path": "src/borg/testsuite/helpers/msgpack_test.py",
    "content": "import sys\nimport pytest\n\nfrom ...helpers.msgpack import is_slow_msgpack\nfrom ...platform import is_cygwin\n\n\ndef expected_py_mp_slow_combination():\n    \"\"\"Do we expect msgpack to be slow in this environment?\"\"\"\n    # We need to import the upstream msgpack package here, not helpers.msgpack:\n    import msgpack\n\n    # msgpack is slow on Cygwin\n    if is_cygwin:\n        return True\n    # msgpack < 1.0.6 did not have Python 3.12 wheels\n    if sys.version_info[:2] == (3, 12) and msgpack.version < (1, 0, 6):\n        return True\n    # Otherwise, we expect msgpack to be fast!\n    return False\n\n\n@pytest.mark.skipif(expected_py_mp_slow_combination(), reason=\"ignore expected slow msgpack\")\ndef test_is_slow_msgpack():\n    # we need to import upstream msgpack package here, not helpers.msgpack:\n    import msgpack\n    import msgpack.fallback\n\n    saved_packer = msgpack.Packer\n    try:\n        msgpack.Packer = msgpack.fallback.Packer\n        assert is_slow_msgpack()\n    finally:\n        msgpack.Packer = saved_packer\n    # This tests that we have fast msgpack on the test platform:\n    assert not is_slow_msgpack()\n"
  },
  {
    "path": "src/borg/testsuite/helpers/nanorst_test.py",
    "content": "import pytest\n\nfrom ...helpers.nanorst import rst_to_text\n\n\ndef test_inline():\n    assert rst_to_text(\"*foo* and ``bar``.\") == \"foo and bar.\"\n\n\ndef test_inline_spread():\n    assert rst_to_text(\"*foo and bar, thusly\\nfoobar*.\") == \"foo and bar, thusly\\nfoobar.\"\n\n\ndef test_comment_inline():\n    assert rst_to_text(\"Foo and Bar\\n.. foo\\nbar\") == \"Foo and Bar\\n.. foo\\nbar\"\n\n\ndef test_inline_escape():\n    assert rst_to_text('Such as \"\\\\*\" characters.') == 'Such as \"*\" characters.'\n\n\ndef test_comment():\n    assert rst_to_text(\"Foo and Bar\\n\\n.. foo\\nbar\") == \"Foo and Bar\\n\\nbar\"\n\n\ndef test_directive_note():\n    assert rst_to_text(\".. note::\\n   Note this and that\") == \"Note:\\n   Note this and that\"\n\n\ndef test_ref():\n    references = {\"foo\": \"baz\"}\n    assert rst_to_text(\"See :ref:`fo\\no`.\", references=references) == \"See baz.\"\n\n\ndef test_undefined_ref():\n    with pytest.raises(ValueError) as exc_info:\n        rst_to_text(\"See :ref:`foo`.\")\n    assert \"Undefined reference\" in str(exc_info.value)\n"
  },
  {
    "path": "src/borg/testsuite/helpers/parseformat_test.py",
    "content": "import base64\nimport os\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom ...constants import *  # NOQA\nfrom ...helpers.argparsing import ArgumentTypeError\nfrom ...helpers.parseformat import (\n    bin_to_hex,\n    binary_to_json,\n    text_to_json,\n    Location,\n    archivename_validator,\n    text_validator,\n    format_file_size,\n    parse_file_size,\n    interval,\n    partial_format,\n    clean_lines,\n    format_line,\n    PlaceholderError,\n    replace_placeholders,\n    swidth_slice,\n    eval_escapes,\n    ChunkerParams,\n)\nfrom ...helpers.time import format_timedelta, parse_timestamp\nfrom ...platformflags import is_win32\n\n\ndef test_bin_to_hex():\n    assert bin_to_hex(b\"\") == \"\"\n    assert bin_to_hex(b\"\\x00\\x01\\xff\") == \"0001ff\"\n\n\n@pytest.mark.parametrize(\n    \"key,value\",\n    [(\"key\", b\"\\x00\\x01\\x02\\x03\"), (\"key\", b\"\\x00\\x01\\x02\"), (\"key\", b\"\\x00\\x01\"), (\"key\", b\"\\x00\"), (\"key\", b\"\")],\n)\ndef test_binary_to_json(key, value):\n    key_b64 = key + \"_b64\"\n    d = binary_to_json(key, value)\n    assert key_b64 in d\n    assert base64.b64decode(d[key_b64]) == value\n\n\n@pytest.mark.parametrize(\n    \"key,value,strict\",\n    [\n        (\"key\", \"abc\", True),\n        (\"key\", \"äöü\", True),\n        (\"key\", \"\", True),\n        (\"key\", b\"\\x00\\xff\".decode(\"utf-8\", errors=\"surrogateescape\"), False),\n        (\"key\", \"äöü\".encode(\"latin1\").decode(\"utf-8\", errors=\"surrogateescape\"), False),\n    ],\n)\ndef test_text_to_json(key, value, strict):\n    key_b64 = key + \"_b64\"\n    d = text_to_json(key, value)\n    value_b = value.encode(\"utf-8\", errors=\"surrogateescape\")\n    if strict:\n        # No surrogate escapes; just Unicode text.\n        assert key in d\n        assert d[key] == value_b.decode(\"utf-8\", errors=\"strict\")\n        assert d[key].encode(\"utf-8\", errors=\"strict\") == value_b\n        assert key_b64 not in d  # Not needed; pure valid Unicode.\n    else:\n        # Requires surrogate escapes. The text has replacement characters; Base64 representation is present.\n        assert key in d\n        assert d[key] == value.encode(\"utf-8\", errors=\"replace\").decode(\"utf-8\", errors=\"strict\")\n        assert d[key].encode(\"utf-8\", errors=\"strict\") == value.encode(\"utf-8\", errors=\"replace\")\n        assert key_b64 in d\n        assert base64.b64decode(d[key_b64]) == value_b\n\n\nclass TestLocationWithoutEnv:\n    @pytest.fixture\n    def keys_dir(self, tmpdir, monkeypatch):\n        tmpdir = str(tmpdir)\n        monkeypatch.setenv(\"BORG_KEYS_DIR\", tmpdir)\n        if not tmpdir.endswith(os.path.sep):\n            tmpdir += os.path.sep\n        return tmpdir\n\n    def test_ssh(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"ssh://user@host:1234//absolute/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='host', port=1234, path='/absolute/path')\"\n        )\n        assert Location(\"ssh://user@host:1234//absolute/path\").to_key_filename() == keys_dir + \"host___absolute_path\"\n        assert (\n            repr(Location(\"ssh://user@host:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='host', port=1234, path='relative/path')\"\n        )\n        assert Location(\"ssh://user@host:1234/relative/path\").to_key_filename() == keys_dir + \"host__relative_path\"\n        assert (\n            repr(Location(\"ssh://user@host/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='host', port=None, path='relative/path')\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[::]:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='::', port=1234, path='relative/path')\"\n        )\n        assert Location(\"ssh://user@[::]:1234/relative/path\").to_key_filename() == keys_dir + \"____relative_path\"\n        assert (\n            repr(Location(\"ssh://user@[::]/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='::', port=None, path='relative/path')\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::]:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::', port=1234, path='relative/path')\"\n        )\n        assert (\n            Location(\"ssh://user@[2001:db8::]:1234/relative/path\").to_key_filename()\n            == keys_dir + \"2001_db8____relative_path\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::]/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::', port=None, path='relative/path')\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::c0:ffee]:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::c0:ffee', port=1234, path='relative/path')\"  # noqa: E501\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::c0:ffee]/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::c0:ffee', port=None, path='relative/path')\"  # noqa: E501\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::192.0.2.1]:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::192.0.2.1', port=1234, path='relative/path')\"  # noqa: E501\n        )\n        assert (\n            repr(Location(\"ssh://user@[2001:db8::192.0.2.1]/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='2001:db8::192.0.2.1', port=None, path='relative/path')\"  # noqa: E501\n        )\n        assert (\n            Location(\"ssh://user@[2001:db8::192.0.2.1]/relative/path\").to_key_filename()\n            == keys_dir + \"2001_db8__192_0_2_1__relative_path\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, \"\n            \"host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='relative/path')\"\n        )\n        assert (\n            repr(Location(\"ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, \"\n            \"host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='relative/path')\"\n        )\n\n    def test_s3(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"s3:/test/path\"))\n            == \"Location(proto='s3', user=None, pass=None, host=None, port=None, path='test/path')\"\n        )\n        assert (\n            repr(Location(\"s3:profile@http://172.28.52.116:9000/test/path\"))\n            == \"Location(proto='s3', user='profile', pass=None, host='172.28.52.116', port=9000, path='test/path')\"  # noqa: E501\n        )\n        assert (\n            repr(Location(\"s3:user:pass@http://172.28.52.116:9000/test/path\"))\n            == \"Location(proto='s3', user='user', pass='REDACTED', host='172.28.52.116', port=9000, path='test/path')\"  # noqa: E501\n        )\n        assert (\n            repr(Location(\"b2:user:pass@https://s3.us-east-005.backblazeb2.com/test/path\"))\n            == \"Location(proto='b2', user='user', pass='REDACTED', host='s3.us-east-005.backblazeb2.com', port=None, path='test/path')\"  # noqa: E501\n        )\n\n    def test_rclone(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"rclone:remote:path\"))\n            == \"Location(proto='rclone', user=None, pass=None, host=None, port=None, path='remote:path')\"\n        )\n        assert Location(\"rclone:remote:path\").to_key_filename() == keys_dir + \"remote_path\"\n\n    def test_sftp(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        # relative path\n        assert (\n            repr(Location(\"sftp://user@host:1234/rel/path\"))\n            == \"Location(proto='sftp', user='user', pass=None, host='host', port=1234, path='rel/path')\"\n        )\n        assert Location(\"sftp://user@host:1234/rel/path\").to_key_filename() == keys_dir + \"host__rel_path\"\n        # absolute path\n        assert (\n            repr(Location(\"sftp://user@host:1234//abs/path\"))\n            == \"Location(proto='sftp', user='user', pass=None, host='host', port=1234, path='/abs/path')\"\n        )\n        assert Location(\"sftp://user@host:1234//abs/path\").to_key_filename() == keys_dir + \"host___abs_path\"\n\n    def test_http(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"http://user:pass@host:1234/\"))\n            == \"Location(proto='http', user='user', pass='REDACTED', host='host', port=1234, path='/')\"\n        )\n        assert Location(\"http://user:pass@host:1234/\").to_key_filename() == keys_dir + \"host__\"\n\n    def test_socket(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        url = \"socket:///c:/repo/path\" if is_win32 else \"socket:///repo/path\"\n        path = \"c:/repo/path\" if is_win32 else \"/repo/path\"\n        assert (\n            repr(Location(url))\n            == f\"Location(proto='socket', user=None, pass=None, host=None, port=None, path='{path}')\"\n        )\n        assert Location(url).to_key_filename().endswith(\"_repo_path\")\n\n    def test_file(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        url = \"file:///c:/repo/path\" if is_win32 else \"file:///repo/path\"\n        path = \"c:/repo/path\" if is_win32 else \"/repo/path\"\n        assert (\n            repr(Location(url)) == f\"Location(proto='file', user=None, pass=None, host=None, port=None, path='{path}')\"\n        )\n        assert Location(url).to_key_filename().endswith(\"_repo_path\")\n\n    @pytest.mark.skipif(is_win32, reason=\"still broken\")\n    def test_smb(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"file:////server/share/path\"))\n            == \"Location(proto='file', user=None, pass=None, host=None, port=None, path='//server/share/path')\"\n        )\n        assert Location(\"file:////server/share/path\").to_key_filename().endswith(\"__server_share_path\")\n\n    def test_folder(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        rel_path = \"path\"\n        abs_path = os.path.abspath(rel_path)\n        assert (\n            repr(Location(rel_path))\n            == f\"Location(proto='file', user=None, pass=None, host=None, port=None, path='{abs_path}')\"\n        )\n        assert Location(\"path\").to_key_filename().endswith(rel_path)\n\n    @pytest.mark.skipif(is_win32, reason=\"Windows has drive letters in abs paths\")\n    def test_abspath(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"/some/absolute/path\"))\n            == \"Location(proto='file', user=None, pass=None, host=None, port=None, path='/some/absolute/path')\"\n        )\n        assert Location(\"/some/absolute/path\").to_key_filename() == keys_dir + \"_some_absolute_path\"\n        assert (\n            repr(Location(\"/some/../absolute/path\"))\n            == \"Location(proto='file', user=None, pass=None, host=None, port=None, path='/absolute/path')\"\n        )\n        assert Location(\"/some/../absolute/path\").to_key_filename() == keys_dir + \"_absolute_path\"\n\n    def test_relpath(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        # For a local path, Borg creates a Location instance with an absolute path.\n        rel_path = \"relative/path\"\n        abs_path = os.path.abspath(rel_path)\n        assert (\n            repr(Location(rel_path))\n            == f\"Location(proto='file', user=None, pass=None, host=None, port=None, path='{abs_path}')\"\n        )\n        assert Location(rel_path).to_key_filename().endswith(\"relative_path\")\n        assert (\n            repr(Location(\"ssh://user@host/relative/path\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='host', port=None, path='relative/path')\"\n        )\n        assert Location(\"ssh://user@host/relative/path\").to_key_filename() == keys_dir + \"host__relative_path\"\n\n    @pytest.mark.skipif(is_win32, reason=\"Windows does not support colons in paths\")\n    def test_with_colons(self, monkeypatch, keys_dir):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        assert (\n            repr(Location(\"/abs/path:w:cols\"))\n            == \"Location(proto='file', user=None, pass=None, host=None, port=None, path='/abs/path:w:cols')\"\n        )\n        assert Location(\"/abs/path:w:cols\").to_key_filename() == keys_dir + \"_abs_path_w_cols\"\n        assert (\n            repr(Location(\"file:///abs/path:w:cols\"))\n            == \"Location(proto='file', user=None, pass=None, host=None, port=None, path='/abs/path:w:cols')\"\n        )\n        assert Location(\"file:///abs/path:w:cols\").to_key_filename() == keys_dir + \"_abs_path_w_cols\"\n        assert (\n            repr(Location(\"ssh://user@host/abs/path:w:cols\"))\n            == \"Location(proto='ssh', user='user', pass=None, host='host', port=None, path='abs/path:w:cols')\"\n        )\n        assert Location(\"ssh://user@host/abs/path:w:cols\").to_key_filename() == keys_dir + \"host__abs_path_w_cols\"\n\n    def test_canonical_path(self, monkeypatch):\n        monkeypatch.delenv(\"BORG_REPO\", raising=False)\n        locations = [\n            \"relative/path\",\n            \"ssh://host/relative/path\",\n            \"ssh://host//absolute/path\",\n            \"ssh://user@host:1234/relative/path\",\n            \"sftp://host/relative/path\",\n            \"sftp://host//absolute/path\",\n            \"sftp://user@host:1234/relative/path\",\n            \"rclone:remote:path\",\n        ]\n        locations.insert(1, \"c:/absolute/path\" if is_win32 else \"/absolute/path\")\n        locations.insert(2, \"file:///c:/absolute/path\" if is_win32 else \"file:///absolute/path\")\n        locations.insert(3, \"socket:///c:/absolute/path\" if is_win32 else \"socket:///absolute/path\")\n        for location in locations:\n            assert (\n                Location(location).canonical_path() == Location(Location(location).canonical_path()).canonical_path()\n            ), (\"failed: %s\" % location)\n\n    def test_bad_syntax(self):\n        with pytest.raises(ValueError):\n            # This is invalid due to the second colon. Correct: 'ssh://user@host/path'.\n            Location(\"ssh://user@host:/path\")\n\n\n@pytest.mark.parametrize(\n    \"name\",\n    [\n        \"foobar\",\n        # Placeholders\n        \"foobar-{now}\",\n    ],\n)\ndef test_archivename_ok(name):\n    assert archivename_validator(name) == name\n\n\n@pytest.mark.parametrize(\n    \"name\",\n    [\n        \"\",  # too short\n        \"x\" * 201,  # too long\n        # Invalid characters:\n        \"foo/bar\",\n        \"foo\\\\bar\",\n        \">foo\",\n        \"<foo\",\n        \"|foo\",\n        'foo\"bar',\n        \"foo?\",\n        \"*bar\",\n        \"foo\\nbar\",\n        \"foo\\0bar\",\n        # Leading/trailing blanks\n        \" foo\",\n        \"bar  \",\n        # Contains surrogate escapes\n        \"foo\\udc80bar\",\n        \"foo\\udcffbar\",\n    ],\n)\ndef test_archivename_invalid(name):\n    with pytest.raises(ArgumentTypeError):\n        archivename_validator(name)\n\n\n@pytest.mark.parametrize(\"text\", [\"\", \"single line\", \"multi\\nline\\ncomment\"])\ndef test_text_ok(text):\n    assert text_validator(name=\"text\", max_length=100)(text) == text\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"x\" * 101,  # too long\n        # Invalid characters:\n        \"foo\\0bar\",\n        # Contains surrogate escapes\n        \"foo\\udc80bar\",\n        \"foo\\udcffbar\",\n    ],\n)\ndef test_text_invalid(text):\n    invalid_ctrl_chars = \"\".join(chr(i) for i in range(32))\n    tv = text_validator(name=\"text\", max_length=100, min_length=1, invalid_ctrl_chars=invalid_ctrl_chars)\n    with pytest.raises(ArgumentTypeError):\n        tv(text)\n\n\ndef test_format_timedelta():\n    t0 = datetime(2001, 1, 1, 10, 20, 3, 0)\n    t1 = datetime(2001, 1, 1, 12, 20, 4, 100000)\n    assert format_timedelta(t1 - t0) == \"2 hours 1.100 seconds\"\n\n\n@pytest.mark.parametrize(\n    \"timeframe, num_secs\",\n    [\n        (\"5S\", 5),\n        (\"2M\", 2 * 60),\n        (\"1H\", 60 * 60),\n        (\"1d\", 24 * 60 * 60),\n        (\"1w\", 7 * 24 * 60 * 60),\n        (\"1m\", 31 * 24 * 60 * 60),\n        (\"1y\", 365 * 24 * 60 * 60),\n    ],\n)\ndef test_interval(timeframe, num_secs):\n    assert interval(timeframe) == num_secs\n\n\n@pytest.mark.parametrize(\n    \"invalid_interval, error_tuple\",\n    [\n        (\"H\", ('Invalid number \"\": expected positive integer',)),\n        (\"-1d\", ('Invalid number \"-1\": expected positive integer',)),\n        (\"food\", ('Invalid number \"foo\": expected positive integer',)),\n    ],\n)\ndef test_interval_time_unit(invalid_interval, error_tuple):\n    with pytest.raises(ArgumentTypeError) as exc:\n        interval(invalid_interval)\n    assert exc.value.args == error_tuple\n\n\ndef test_interval_number():\n    with pytest.raises(ArgumentTypeError) as exc:\n        interval(\"5\")\n    assert exc.value.args == ('Unexpected time unit \"5\": choose from y, m, w, d, H, M, S',)\n\n\ndef test_parse_timestamp():\n    assert parse_timestamp(\"2015-04-19T20:25:00.226410\") == datetime(2015, 4, 19, 20, 25, 0, 226410, timezone.utc)\n    assert parse_timestamp(\"2015-04-19T20:25:00\") == datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc)\n\n\n@pytest.mark.parametrize(\n    \"size, fmt\",\n    [\n        (0, \"0 B\"),  # No rounding necessary for these.\n        (1, \"1 B\"),\n        (142, \"142 B\"),\n        (999, \"999 B\"),\n        (1000, \"1.00 kB\"),  # rounding starts here\n        (1001, \"1.00 kB\"),  # should be rounded away\n        (1234, \"1.23 kB\"),  # should be rounded down\n        (1235, \"1.24 kB\"),  # should be rounded up\n        (1010, \"1.01 kB\"),  # rounded down as well\n        (999990000, \"999.99 MB\"),  # rounded down\n        (999990001, \"999.99 MB\"),  # rounded down\n        (999995000, \"1.00 GB\"),  # Rounded up to the next unit.\n        (10**6, \"1.00 MB\"),  # and all the remaining units, megabytes\n        (10**9, \"1.00 GB\"),  # gigabytes\n        (10**12, \"1.00 TB\"),  # terabytes\n        (10**15, \"1.00 PB\"),  # petabytes\n        (10**18, \"1.00 EB\"),  # exabytes\n        (10**21, \"1.00 ZB\"),  # zottabytes\n        (10**24, \"1.00 YB\"),  # yottabytes\n        (-1, \"-1 B\"),  # negative value\n        (-1010, \"-1.01 kB\"),  # negative value with rounding\n    ],\n)\ndef test_file_size(size, fmt):\n    \"\"\"test the size formatting routines\"\"\"\n    assert format_file_size(size) == fmt\n\n\n@pytest.mark.parametrize(\n    \"size, fmt\",\n    [\n        (0, \"0 B\"),\n        (2**0, \"1 B\"),\n        (2**10, \"1.00 KiB\"),\n        (2**20, \"1.00 MiB\"),\n        (2**30, \"1.00 GiB\"),\n        (2**40, \"1.00 TiB\"),\n        (2**50, \"1.00 PiB\"),\n        (2**60, \"1.00 EiB\"),\n        (2**70, \"1.00 ZiB\"),\n        (2**80, \"1.00 YiB\"),\n        (-(2**0), \"-1 B\"),\n        (-(2**10), \"-1.00 KiB\"),\n        (-(2**20), \"-1.00 MiB\"),\n    ],\n)\ndef test_file_size_iec(size, fmt):\n    \"\"\"test the size formatting routines\"\"\"\n    assert format_file_size(size, iec=True) == fmt\n\n\n@pytest.mark.parametrize(\n    \"original_size, formatted_size\",\n    [\n        (1234, \"1.2 kB\"),  # rounded down\n        (1254, \"1.3 kB\"),  # rounded up\n        (999990000, \"1.0 GB\"),  # and not 999.9 MB or 1000.0 MB\n    ],\n)\ndef test_file_size_precision(original_size, formatted_size):\n    assert format_file_size(original_size, precision=1) == formatted_size\n\n\n@pytest.mark.parametrize(\"size, fmt\", [(0, \"0 B\"), (1, \"+1 B\"), (1234, \"+1.23 kB\"), (-1, \"-1 B\"), (-1234, \"-1.23 kB\")])\ndef test_file_size_sign(size, fmt):\n    assert format_file_size(size, sign=True) == fmt\n\n\n@pytest.mark.parametrize(\n    \"string, value\", [(\"1\", 1), (\"20\", 20), (\"5K\", 5000), (\"1.75M\", 1750000), (\"1e+9\", 1e9), (\"-1T\", -1e12)]\n)\ndef test_parse_file_size(string, value):\n    assert parse_file_size(string) == int(value)\n\n\n@pytest.mark.parametrize(\"string\", (\"\", \"5 Äpfel\", \"4E\", \"2229 bit\", \"1B\"))\ndef test_parse_file_size_invalid(string):\n    with pytest.raises(ValueError):\n        parse_file_size(string)\n\n\n@pytest.mark.parametrize(\n    \"fmt, items_map, expected_result\",\n    [\n        (\"{space:10}\", {\"space\": \" \"}, \" \" * 10),\n        (\"{foobar}\", {\"bar\": \"wrong\", \"foobar\": \"correct\"}, \"correct\"),\n        (\"{unknown_key}\", {}, \"{unknown_key}\"),\n        (\"{key}{{escaped_key}}\", {}, \"{key}{{escaped_key}}\"),\n        (\"{{escaped_key}}\", {\"escaped_key\": 1234}, \"{{escaped_key}}\"),\n    ],\n)\ndef test_partial_format(fmt, items_map, expected_result):\n    assert partial_format(fmt, items_map) == expected_result\n\n\ndef test_clean_lines():\n    conf = \"\"\"\\\n#comment\ndata1 #data1\ndata2\n\n data3\n\"\"\".splitlines(\n        keepends=True\n    )\n    assert list(clean_lines(conf)) == [\"data1 #data1\", \"data2\", \"data3\"]\n    assert list(clean_lines(conf, lstrip=False)) == [\"data1 #data1\", \"data2\", \" data3\"]\n    assert list(clean_lines(conf, rstrip=False)) == [\"data1 #data1\\n\", \"data2\\n\", \"data3\\n\"]\n    assert list(clean_lines(conf, remove_empty=False)) == [\"data1 #data1\", \"data2\", \"\", \"data3\"]\n    assert list(clean_lines(conf, remove_comments=False)) == [\"#comment\", \"data1 #data1\", \"data2\", \"data3\"]\n\n\ndef test_format_line():\n    data = dict(foo=\"bar baz\")\n    assert format_line(\"\", data) == \"\"\n    assert format_line(\"{foo}\", data) == \"bar baz\"\n    assert format_line(\"foo{foo}foo\", data) == \"foobar bazfoo\"\n\n\ndef test_format_line_erroneous():\n    data = dict()\n    with pytest.raises(PlaceholderError):\n        assert format_line(\"{invalid}\", data)\n    with pytest.raises(PlaceholderError):\n        assert format_line(\"{}\", data)\n    with pytest.raises(PlaceholderError):\n        assert format_line(\"{now!r}\", data)\n    with pytest.raises(PlaceholderError):\n        assert format_line(\"{now.__class__.__module__.__builtins__}\", data)\n\n\ndef test_replace_placeholders():\n    replace_placeholders.reset()  # avoid overrides are spoiled by previous tests\n    now = datetime.now()\n    assert \" \" not in replace_placeholders(\"{now}\")\n    assert int(replace_placeholders(\"{now:%Y}\")) == now.year\n\n\ndef test_override_placeholders():\n    assert replace_placeholders(\"{uuid4}\", overrides={\"uuid4\": \"overridden\"}) == \"overridden\"\n\n\ndef working_swidth():\n    from ...platform import swidth\n\n    return swidth(\"선\") == 2\n\n\n@pytest.mark.skipif(not working_swidth(), reason=\"swidth() is not supported / active\")\ndef test_swidth_slice():\n    string = \"나윤선나윤선나윤선나윤선나윤선\"\n    assert swidth_slice(string, 1) == \"\"\n    assert swidth_slice(string, -1) == \"\"\n    assert swidth_slice(string, 4) == \"나윤\"\n    assert swidth_slice(string, -4) == \"윤선\"\n\n\n@pytest.mark.skipif(not working_swidth(), reason=\"swidth() is not supported / active\")\ndef test_swidth_slice_mixed_characters():\n    string = \"나윤a선나윤선나윤선나윤선나윤선\"\n    assert swidth_slice(string, 5) == \"나윤a\"\n    assert swidth_slice(string, 6) == \"나윤a\"\n\n\ndef test_eval_escapes():\n    assert eval_escapes(\"\\\\n\") == \"\\n\"\n    assert eval_escapes(\"\\\\t\") == \"\\t\"\n    assert eval_escapes(\"\\\\r\") == \"\\r\"\n    assert eval_escapes(\"\\\\f\") == \"\\f\"\n    assert eval_escapes(\"\\\\b\") == \"\\b\"\n    assert eval_escapes(\"\\\\a\") == \"\\a\"\n    assert eval_escapes(\"\\\\v\") == \"\\v\"\n    assert eval_escapes(\"\\\\\\\\\") == \"\\\\\"\n    assert eval_escapes('\\\\\"') == '\"'\n    assert eval_escapes(\"\\\\'\") == \"'\"\n    assert eval_escapes(\"\\\\101\") == \"A\"  # ord('A') == 65 == 0o101\n    assert eval_escapes(\"\\\\x41\") == \"A\"  # ord('A') == 65 == 0x41\n    assert eval_escapes(\"\\\\u0041\") == \"A\"  # ord('A') == 65 == 0x41\n    assert eval_escapes(\"\\\\U00000041\") == \"A\"  # ord('A') == 65 == 0x41\n    assert eval_escapes(\"äç\\\\n\") == \"äç\\n\"\n\n\n@pytest.mark.parametrize(\n    \"chunker_params, expected_return\",\n    [\n        (\"default\", (\"buzhash\", 19, 23, 21, 4095)),\n        (\"19,23,21,4095\", (\"buzhash\", 19, 23, 21, 4095)),\n        (\"buzhash,19,23,21,4095\", (\"buzhash\", 19, 23, 21, 4095)),\n        (\"10,23,16,4095\", (\"buzhash\", 10, 23, 16, 4095)),\n        (\"fixed,4096\", (\"fixed\", 4096, 0)),\n        (\"fixed,4096,200\", (\"fixed\", 4096, 200)),\n    ],\n)\ndef test_valid_chunkerparams(chunker_params, expected_return):\n    assert ChunkerParams(chunker_params) == expected_return\n\n\n@pytest.mark.parametrize(\n    \"invalid_chunker_params\",\n    [\n        \"crap,1,2,3,4\",  # invalid algo\n        \"buzhash,5,7,6,4095\",  # too small min. size\n        \"buzhash,19,24,21,4095\",  # too big max. size\n        \"buzhash,23,19,21,4095\",  # violates min <= mask <= max\n        \"buzhash,19,23,21,4096\",  # even window size\n        \"fixed,63\",  # too small block size\n        \"fixed,%d,%d\" % (MAX_DATA_SIZE + 1, 4096),  # too big block size\n        \"fixed,%d,%d\" % (4096, MAX_DATA_SIZE + 1),  # too big header size\n    ],\n)\ndef test_invalid_chunkerparams(invalid_chunker_params):\n    with pytest.raises(ArgumentTypeError):\n        ChunkerParams(invalid_chunker_params)\n"
  },
  {
    "path": "src/borg/testsuite/helpers/passphrase_test.py",
    "content": "import getpass\nimport pytest\n\nfrom ...helpers.parseformat import bin_to_hex\nfrom ...helpers.passphrase import Passphrase, PasswordRetriesExceeded\n\n\nclass TestPassphrase:\n    def test_passphrase_new_verification(self, capsys, monkeypatch):\n        monkeypatch.setattr(getpass, \"getpass\", lambda prompt: \"1234aöäü\")\n        monkeypatch.setenv(\"BORG_DISPLAY_PASSPHRASE\", \"no\")\n        Passphrase.new()\n        out, err = capsys.readouterr()\n        assert \"1234\" not in out\n        assert \"1234\" not in err\n\n        monkeypatch.setenv(\"BORG_DISPLAY_PASSPHRASE\", \"yes\")\n        passphrase = Passphrase.new()\n        out, err = capsys.readouterr()\n        assert \"3132333461c3b6c3a4c3bc\" not in out\n        assert \"3132333461c3b6c3a4c3bc\" in err\n        assert passphrase == \"1234aöäü\"\n\n        monkeypatch.setattr(getpass, \"getpass\", lambda prompt: \"1234/@=\")\n        Passphrase.new()\n        out, err = capsys.readouterr()\n        assert \"1234/@=\" not in out\n        assert \"1234/@=\" in err\n\n    def test_passphrase_new_empty(self, capsys, monkeypatch):\n        monkeypatch.delenv(\"BORG_PASSPHRASE\", False)\n        monkeypatch.setattr(getpass, \"getpass\", lambda prompt: \"\")\n        with pytest.raises(PasswordRetriesExceeded):\n            Passphrase.new(allow_empty=False)\n        out, err = capsys.readouterr()\n        assert \"must not be blank\" in err\n\n    def test_passphrase_new_retries(self, monkeypatch):\n        monkeypatch.delenv(\"BORG_PASSPHRASE\", False)\n        ascending_numbers = iter(range(20))\n        monkeypatch.setattr(getpass, \"getpass\", lambda prompt: str(next(ascending_numbers)))\n        with pytest.raises(PasswordRetriesExceeded):\n            Passphrase.new()\n\n    def test_passphrase_repr(self):\n        assert \"secret\" not in repr(Passphrase(\"secret\"))\n\n    def test_passphrase_wrong_debug(self, capsys, monkeypatch):\n        passphrase = \"wrong_passphrase\"\n        monkeypatch.setenv(\"BORG_DEBUG_PASSPHRASE\", \"YES\")\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"env_passphrase\")\n        monkeypatch.setenv(\"BORG_PASSCOMMAND\", \"command\")\n        monkeypatch.setenv(\"BORG_PASSPHRASE_FD\", \"fd_value\")\n\n        Passphrase.display_debug_info(passphrase)\n\n        out, err = capsys.readouterr()\n        assert \"Incorrect passphrase!\" in err\n        assert passphrase in err\n        assert bin_to_hex(passphrase.encode(\"utf-8\")) in err\n        assert 'BORG_PASSPHRASE = \"env_passphrase\"' in err\n        assert 'BORG_PASSCOMMAND = \"command\"' in err\n        assert 'BORG_PASSPHRASE_FD = \"fd_value\"' in err\n\n        monkeypatch.delenv(\"BORG_DEBUG_PASSPHRASE\", raising=False)\n        Passphrase.display_debug_info(passphrase)\n        out, err = capsys.readouterr()\n\n        assert \"Incorrect passphrase!\" not in err\n        assert passphrase not in err\n\n    def test_verification(self, capsys, monkeypatch):\n        passphrase = \"test_passphrase\"\n        hex_value = passphrase.encode(\"utf-8\").hex()\n\n        monkeypatch.setenv(\"BORG_DISPLAY_PASSPHRASE\", \"no\")\n        Passphrase.verification(passphrase)\n        out, err = capsys.readouterr()\n        assert passphrase not in err\n\n        monkeypatch.setenv(\"BORG_DISPLAY_PASSPHRASE\", \"yes\")\n        Passphrase.verification(passphrase)\n        out, err = capsys.readouterr()\n        assert passphrase in err\n        assert hex_value in err\n"
  },
  {
    "path": "src/borg/testsuite/helpers/process_test.py",
    "content": "import shutil\nimport pytest\n\nfrom ...helpers.process import popen_with_error_handling\n\n\nclass TestPopenWithErrorHandling:\n    @pytest.mark.skipif(not shutil.which(\"test\"), reason='\"test\" binary is required')\n    def test_simple(self):\n        proc = popen_with_error_handling(\"test 1\")\n        assert proc.wait() == 0\n\n    @pytest.mark.skipif(\n        shutil.which(\"borg-foobar-test-notexist\"), reason='\"borg-foobar-test-notexist\" binary exists (somehow?)'\n    )\n    def test_not_found(self):\n        proc = popen_with_error_handling(\"borg-foobar-test-notexist 1234\")\n        assert proc is None\n\n    @pytest.mark.parametrize(\"cmd\", ('mismatched \"quote', 'foo --bar=\"baz', \"\"))\n    def test_bad_syntax(self, cmd):\n        proc = popen_with_error_handling(cmd)\n        assert proc is None\n\n    def test_shell(self):\n        with pytest.raises(AssertionError):\n            popen_with_error_handling(\"\", shell=True)\n"
  },
  {
    "path": "src/borg/testsuite/helpers/progress_test.py",
    "content": "from ...helpers.progress import ProgressIndicatorPercent\n\n\ndef test_progress_percentage(capfd):\n    pi = ProgressIndicatorPercent(1000, step=5, start=0, msg=\"%3.0f%%\")\n    pi.logger.setLevel(\"INFO\")\n    pi.show(0)\n    out, err = capfd.readouterr()\n    assert err == \"  0%\\n\"\n    pi.show(420)\n    pi.show(680)\n    out, err = capfd.readouterr()\n    assert err == \" 42%\\n 68%\\n\"\n    pi.show(1000)\n    out, err = capfd.readouterr()\n    assert err == \"100%\\n\"\n    pi.finish()\n    out, err = capfd.readouterr()\n    assert err == \"\\n\"\n\n\ndef test_progress_percentage_step(capfd):\n    pi = ProgressIndicatorPercent(100, step=2, start=0, msg=\"%3.0f%%\")\n    pi.logger.setLevel(\"INFO\")\n    pi.show()\n    out, err = capfd.readouterr()\n    assert err == \"  0%\\n\"\n    pi.show()\n    out, err = capfd.readouterr()\n    assert err == \"\"  # no output at 1% as we have step == 2\n    pi.show()\n    out, err = capfd.readouterr()\n    assert err == \"  2%\\n\"\n\n\ndef test_progress_percentage_quiet(capfd):\n    pi = ProgressIndicatorPercent(1000, step=5, start=0, msg=\"%3.0f%%\")\n    pi.logger.setLevel(\"WARN\")\n    pi.show(0)\n    out, err = capfd.readouterr()\n    assert err == \"\"\n    pi.show(1000)\n    out, err = capfd.readouterr()\n    assert err == \"\"\n    pi.finish()\n    out, err = capfd.readouterr()\n    assert err == \"\"\n"
  },
  {
    "path": "src/borg/testsuite/helpers/shellpattern_test.py",
    "content": "import re\n\nimport pytest\n\nfrom ...helpers import shellpattern\n\n\ndef check(path, pattern):\n    compiled = re.compile(shellpattern.translate(pattern))\n\n    return bool(compiled.match(path))\n\n\n@pytest.mark.parametrize(\n    \"path, patterns\",\n    [\n        # Literal string\n        (\"foo/bar\", [\"foo/bar\"]),\n        (\"foo\\\\bar\", [\"foo\\\\bar\"]),\n        # Non-ASCII\n        (\"foo/c/\\u0152/e/bar\", [\"foo/*/\\u0152/*/bar\", \"*/*/\\u0152/*/*\", \"**/\\u0152/*/*\"]),\n        (\"\\u00e4\\u00f6\\u00dc\", [\"???\", \"*\", \"\\u00e4\\u00f6\\u00dc\", \"[\\u00e4][\\u00f6][\\u00dc]\"]),\n        # Question mark\n        (\"foo\", [\"fo?\"]),\n        (\"foo\", [\"f?o\"]),\n        (\"foo\", [\"f??\"]),\n        (\"foo\", [\"?oo\"]),\n        (\"foo\", [\"?o?\"]),\n        (\"foo\", [\"??o\"]),\n        (\"foo\", [\"???\"]),\n        # Single asterisk\n        (\"\", [\"*\"]),\n        (\"foo\", [\"*\", \"**\", \"***\"]),\n        (\"foo\", [\"foo*\"]),\n        (\"foobar\", [\"foo*\"]),\n        (\"foobar\", [\"foo*bar\"]),\n        (\"foobarbaz\", [\"foo*baz\"]),\n        (\"bar\", [\"*bar\"]),\n        (\"foobar\", [\"*bar\"]),\n        (\"foo/bar\", [\"foo/*bar\"]),\n        (\"foo/bar\", [\"foo/*ar\"]),\n        (\"foo/bar\", [\"foo/*r\"]),\n        (\"foo/bar\", [\"foo/*\"]),\n        (\"foo/bar\", [\"foo*/bar\"]),\n        (\"foo/bar\", [\"fo*/bar\"]),\n        (\"foo/bar\", [\"f*/bar\"]),\n        (\"foo/bar\", [\"*/bar\"]),\n        # Double asterisk (matches 0 to n directory layers)\n        (\"foo/bar\", [\"foo/**/bar\"]),\n        (\"foo/1/bar\", [\"foo/**/bar\"]),\n        (\"foo/1/22/333/bar\", [\"foo/**/bar\"]),\n        (\"foo/\", [\"foo/**/\"]),\n        (\"foo/1/\", [\"foo/**/\"]),\n        (\"foo/1/22/333/\", [\"foo/**/\"]),\n        (\"bar\", [\"**/bar\"]),\n        (\"1/bar\", [\"**/bar\"]),\n        (\"1/22/333/bar\", [\"**/bar\"]),\n        (\"foo/bar/baz\", [\"foo/**/*\"]),\n        # Set\n        (\"foo1\", [\"foo[12]\"]),\n        (\"foo2\", [\"foo[12]\"]),\n        (\"foo2/bar\", [\"foo[12]/*\"]),\n        (\"f??f\", [\"f??f\", \"f[?][?]f\"]),\n        (\"foo]\", [\"foo[]]\"]),\n        # Inverted set\n        (\"foo3\", [\"foo[!12]\"]),\n        (\"foo^\", [\"foo[^!]\"]),\n        (\"foo!\", [\"foo[^!]\"]),\n        # Group\n        (\"foo1\", [\"{foo1,foo2}\"]),\n        (\"foo2\", [\"foo{1,2}\"]),\n        (\"foo\", [\"foo{,1,2}\"]),\n        (\"foo1\", [\"{foo{1,2},bar}\"]),\n        (\"bar\", [\"{foo{1,2},bar}\"]),\n        (\"{foo\", [\"{foo{,bar}\"]),\n        (\"{foobar\", [\"{foo{,bar}\"]),\n        (\"{foo},bar}\", [\"{foo},bar}\"]),\n        (\"bar/foobar\", [\"**/foo{ba[!z]*,[0-9]}\"]),\n    ],\n)\ndef test_match(path, patterns):\n    for p in patterns:\n        assert check(path, p)\n\n\n@pytest.mark.parametrize(\n    \"path, patterns\",\n    [\n        (\"\", [\"?\", \"[]\"]),\n        (\"foo\", [\"foo?\"]),\n        (\"foo\", [\"?foo\"]),\n        (\"foo\", [\"f?oo\"]),\n        # Do not match the path separator.\n        (\"foo/ar\", [\"foo?ar\"]),\n        # Do not match or cross the path separator (os.path.sep).\n        (\"foo/bar\", [\"*\"]),\n        (\"foo/bar\", [\"foo*bar\"]),\n        (\"foo/bar\", [\"foo*ar\"]),\n        (\"foo/bar\", [\"fo*bar\"]),\n        (\"foo/bar\", [\"fo*ar\"]),\n        # Double asterisk\n        (\"foobar\", [\"foo/**/bar\"]),\n        # Two asterisks without a slash do not match the directory separator.\n        (\"foo/bar\", [\"**\"]),\n        # Double asterisk does not match a filename.\n        (\"foo/bar\", [\"**/\"]),\n        # Set\n        (\"foo3\", [\"foo[12]\"]),\n        # Inverted set\n        (\"foo1\", [\"foo[!12]\"]),\n        (\"foo2\", [\"foo[!12]\"]),\n        # Group\n        (\"foo\", [\"{foo1,foo2}\"]),\n        (\"foo\", [\"foo{1,2}\"]),\n        (\"foo{1,2}\", [\"foo{1,2}\"]),\n        (\"bar/foobaz\", [\"**/foo{ba[!z]*,[0-9]}\"]),\n    ],\n)\ndef test_mismatch(path, patterns):\n    for p in patterns:\n        assert not check(path, p)\n\n\ndef test_match_end():\n    regex = shellpattern.translate(\"*-home\")  # The default is match_end == end of string.\n    assert re.match(regex, \"2017-07-03-home\")\n    assert not re.match(regex, \"2017-07-03-home.xxx\")\n\n    match_end = r\"(\\.xxx)?\\Z\"  # With or without a .xxx suffix.\n    regex = shellpattern.translate(\"*-home\", match_end=match_end)\n    assert re.match(regex, \"2017-07-03-home\")\n    assert re.match(regex, \"2017-07-03-home.xxx\")\n"
  },
  {
    "path": "src/borg/testsuite/helpers/time_test.py",
    "content": "import pytest\nfrom datetime import datetime, timezone\n\nfrom ...helpers.time import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS\n\n\ndef utcfromtimestamp(timestamp):\n    \"\"\"Return a naive datetime instance representing the timestamp in the UTC time zone.\"\"\"\n    return datetime.fromtimestamp(timestamp, timezone.utc).replace(tzinfo=None)\n\n\ndef test_safe_timestamps():\n    if SUPPORT_32BIT_PLATFORMS:\n        # Nanoseconds fitting into int64.\n        assert safe_ns(2**64) <= 2**63 - 1\n        assert safe_ns(-1) == 0\n        # Seconds fitting into int32.\n        assert safe_s(2**64) <= 2**31 - 1\n        assert safe_s(-1) == 0\n        # datetime will not stumble over its Y10K problem.\n        beyond_y10k = 2**100\n        with pytest.raises(OverflowError):\n            utcfromtimestamp(beyond_y10k)\n        assert utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2038, 1, 1)\n        assert utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2038, 1, 1)\n    else:\n        # Nanoseconds fitting into int64.\n        assert safe_ns(2**64) <= 2**63 - 1\n        assert safe_ns(-1) == 0\n        # Seconds are limited so that their ns conversion fits into int64.\n        assert safe_s(2**64) * 1000000000 <= 2**63 - 1\n        assert safe_s(-1) == 0\n        # datetime will not stumble over its Y10K problem.\n        beyond_y10k = 2**100\n        with pytest.raises(OverflowError):\n            utcfromtimestamp(beyond_y10k)\n        assert utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1)\n        assert utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1)\n"
  },
  {
    "path": "src/borg/testsuite/helpers/yes_no_test.py",
    "content": "import pytest\n\nfrom ...helpers.yes_no import yes, TRUISH, FALSISH, DEFAULTISH\nfrom .. import FakeInputs\n\n\ndef test_yes_input():\n    inputs = list(TRUISH)\n    input = FakeInputs(inputs)\n    for i in inputs:\n        assert yes(input=input)\n    inputs = list(FALSISH)\n    input = FakeInputs(inputs)\n    for i in inputs:\n        assert not yes(input=input)\n\n\ndef test_yes_input_defaults():\n    inputs = list(DEFAULTISH)\n    input = FakeInputs(inputs)\n    for i in inputs:\n        assert yes(default=True, input=input)\n    input = FakeInputs(inputs)\n    for i in inputs:\n        assert not yes(default=False, input=input)\n\n\ndef test_yes_input_custom():\n    input = FakeInputs([\"YES\", \"SURE\", \"NOPE\"])\n    assert yes(truish=(\"YES\",), input=input)\n    assert yes(truish=(\"SURE\",), input=input)\n    assert not yes(falsish=(\"NOPE\",), input=input)\n\n\ndef test_yes_env(monkeypatch):\n    for value in TRUISH:\n        monkeypatch.setenv(\"OVERRIDE_THIS\", value)\n        assert yes(env_var_override=\"OVERRIDE_THIS\")\n    for value in FALSISH:\n        monkeypatch.setenv(\"OVERRIDE_THIS\", value)\n        assert not yes(env_var_override=\"OVERRIDE_THIS\")\n\n\ndef test_yes_env_default(monkeypatch):\n    for value in DEFAULTISH:\n        monkeypatch.setenv(\"OVERRIDE_THIS\", value)\n        assert yes(env_var_override=\"OVERRIDE_THIS\", default=True)\n        assert not yes(env_var_override=\"OVERRIDE_THIS\", default=False)\n\n\ndef test_yes_defaults():\n    input = FakeInputs([\"invalid\", \"\", \" \"])\n    assert not yes(input=input)  # default=False\n    assert not yes(input=input)\n    assert not yes(input=input)\n    input = FakeInputs([\"invalid\", \"\", \" \"])\n    assert yes(default=True, input=input)\n    assert yes(default=True, input=input)\n    assert yes(default=True, input=input)\n    input = FakeInputs([])\n    assert yes(default=True, input=input)\n    assert not yes(default=False, input=input)\n    with pytest.raises(ValueError):\n        yes(default=None)\n\n\ndef test_yes_retry():\n    input = FakeInputs([\"foo\", \"bar\", TRUISH[0]])\n    assert yes(retry_msg=\"Retry: \", input=input)\n    input = FakeInputs([\"foo\", \"bar\", FALSISH[0]])\n    assert not yes(retry_msg=\"Retry: \", input=input)\n\n\ndef test_yes_no_retry():\n    input = FakeInputs([\"foo\", \"bar\", TRUISH[0]])\n    assert not yes(retry=False, default=False, input=input)\n    input = FakeInputs([\"foo\", \"bar\", FALSISH[0]])\n    assert yes(retry=False, default=True, input=input)\n\n\ndef test_yes_output(capfd):\n    input = FakeInputs([\"invalid\", \"y\", \"n\"])\n    assert yes(msg=\"intro-msg\", false_msg=\"false-msg\", true_msg=\"true-msg\", retry_msg=\"retry-msg\", input=input)\n    out, err = capfd.readouterr()\n    assert out == \"\"\n    assert \"intro-msg\" in err\n    assert \"retry-msg\" in err\n    assert \"true-msg\" in err\n    assert not yes(msg=\"intro-msg\", false_msg=\"false-msg\", true_msg=\"true-msg\", retry_msg=\"retry-msg\", input=input)\n    out, err = capfd.readouterr()\n    assert out == \"\"\n    assert \"intro-msg\" in err\n    assert \"retry-msg\" not in err\n    assert \"false-msg\" in err\n\n\ndef test_yes_env_output(capfd, monkeypatch):\n    env_var = \"OVERRIDE_SOMETHING\"\n    monkeypatch.setenv(env_var, \"yes\")\n    assert yes(env_var_override=env_var)\n    out, err = capfd.readouterr()\n    assert out == \"\"\n    assert env_var in err\n    assert \"yes\" in err\n"
  },
  {
    "path": "src/borg/testsuite/item_test.py",
    "content": "import pytest\n\nfrom ..cache import ChunkListEntry\nfrom ..item import Item, chunks_contents_equal\nfrom ..helpers import StableDict\nfrom ..helpers.msgpack import Timestamp\n\n\ndef test_item_empty():\n    item = Item()\n\n    assert item.as_dict() == {}\n\n    assert \"path\" not in item\n    with pytest.raises(ValueError):\n        \"invalid-key\" in item\n    with pytest.raises(TypeError):\n        b\"path\" in item\n    with pytest.raises(TypeError):\n        42 in item\n\n    assert item.get(\"mode\") is None\n    assert item.get(\"mode\", 0o666) == 0o666\n    with pytest.raises(ValueError):\n        item.get(\"invalid-key\")\n    with pytest.raises(TypeError):\n        item.get(b\"mode\")\n    with pytest.raises(TypeError):\n        item.get(42)\n\n    with pytest.raises(AttributeError):\n        item.path\n\n    with pytest.raises(AttributeError):\n        del item.path\n\n\n@pytest.mark.parametrize(\n    \"item_dict, path, mode\",\n    [  # It does not matter whether we get str or bytes keys\n        ({b\"path\": \"a/b/c\", b\"mode\": 0o666}, \"a/b/c\", 0o666),\n        ({\"path\": \"a/b/c\", \"mode\": 0o666}, \"a/b/c\", 0o666),\n    ],\n)\ndef test_item_from_dict(item_dict, path, mode):\n    item = Item(item_dict)\n    assert item.path == path\n    assert item.mode == mode\n    assert \"path\" in item\n    assert \"mode\" in item\n\n\n@pytest.mark.parametrize(\n    \"invalid_item, error\",\n    [\n        (42, TypeError),  # invalid - no dict\n        ({42: 23}, TypeError),  # invalid - no bytes/str key\n        ({\"foobar\": \"baz\"}, ValueError),  # invalid - unknown key\n    ],\n)\ndef test_item_invalid(invalid_item, error):\n    with pytest.raises(error):\n        Item(invalid_item)\n\n\ndef test_item_from_kw():\n    item = Item(path=\"a/b/c\", mode=0o666)\n    assert item.path == \"a/b/c\"\n    assert item.mode == 0o666\n\n\ndef test_item_int_property():\n    item = Item()\n    item.mode = 0o666\n    assert item.mode == 0o666\n    assert item.as_dict() == {\"mode\": 0o666}\n    del item.mode\n    assert item.as_dict() == {}\n    with pytest.raises(TypeError):\n        item.mode = \"invalid\"\n\n\n@pytest.mark.parametrize(\"atime\", [42, 2**65])\ndef test_item_mptimestamp_property(atime):\n    item = Item()\n    item.atime = atime\n    assert item.atime == atime\n    assert item.as_dict() == {\"atime\": Timestamp.from_unix_nano(atime)}\n\n\ndef test_item_se_str_property():\n    # Start simple\n    item = Item()\n    item.path = \"a/b/c\"\n    assert item.path == \"a/b/c\"\n    assert item.as_dict() == {\"path\": \"a/b/c\"}\n    del item.path\n    assert item.as_dict() == {}\n    with pytest.raises(TypeError):\n        item.path = 42\n\n    # Non-UTF-8 path, requiring surrogate escaping for a Latin-1 u-umlaut\n    item = Item(internal_dict={\"path\": b\"a/\\xfc/c\"})\n    assert item.path == \"a/\\udcfc/c\"  # getting a surrogate-escaped representation\n    assert item.as_dict() == {\"path\": \"a/\\udcfc/c\"}\n    del item.path\n    assert \"path\" not in item\n    item.path = \"a/\\udcfc/c\"  # setting using a surrogate-escaped representation\n    assert item.as_dict() == {\"path\": \"a/\\udcfc/c\"}\n\n\ndef test_item_list_property():\n    item = Item()\n    item.chunks = []\n    assert item.chunks == []\n    item.chunks.append(0)\n    assert item.chunks == [0]\n    item.chunks.append(1)\n    assert item.chunks == [0, 1]\n    assert item.as_dict() == {\"chunks\": [0, 1]}\n\n\ndef test_item_dict_property():\n    item = Item()\n    item.xattrs = StableDict()\n    assert item.xattrs == StableDict()\n    item.xattrs[\"foo\"] = \"bar\"\n    assert item.xattrs[\"foo\"] == \"bar\"\n    item.xattrs[\"bar\"] = \"baz\"\n    assert item.xattrs == StableDict({\"foo\": \"bar\", \"bar\": \"baz\"})\n    assert item.as_dict() == {\"xattrs\": {\"foo\": \"bar\", \"bar\": \"baz\"}}\n\n\ndef test_unknown_property():\n    # We do not want the user to be able to set unknown attributes —\n    # they will not appear in the .as_dict() result dictionary.\n    # Also, they might just be typos of known attributes.\n    item = Item()\n    with pytest.raises(AttributeError):\n        item.unknown_attribute = None\n\n\ndef test_item_file_size():\n    item = Item(mode=0o100666, chunks=[ChunkListEntry(size=1000, id=None), ChunkListEntry(size=2000, id=None)])\n    assert item.get_size() == 3000\n    item.get_size(memorize=True)\n    assert item.size == 3000\n\n\ndef test_item_file_size_no_chunks():\n    item = Item(mode=0o100666)\n    assert item.get_size() == 0\n\n\ndef test_item_optr():\n    item = Item()\n    assert Item.from_optr(item.to_optr()) is item\n\n\n@pytest.mark.parametrize(\n    \"chunk_a, chunk_b, chunks_equal\",\n    [\n        ([\"1234\", \"567A\", \"bC\"], [\"1\", \"23\", \"4567A\", \"b\", \"C\"], True),  # equal\n        ([\"12345\"], [\"1234\", \"56\"], False),  # one iterator exhausted before the other\n        ([\"1234\", \"65\"], [\"1234\", \"56\"], False),  # content mismatch\n        ([\"1234\", \"56\"], [\"1234\", \"565\"], False),  # the first is a prefix of the second\n    ],\n)\ndef test_chunk_content_equal(chunk_a: str, chunk_b: str, chunks_equal):\n    chunks_a = [data.encode() for data in chunk_a]\n    chunks_b = [data.encode() for data in chunk_b]\n    compare1 = chunks_contents_equal(iter(chunks_a), iter(chunks_b))\n    compare2 = chunks_contents_equal(iter(chunks_b), iter(chunks_a))\n    assert compare1 == compare2\n    assert compare1 == chunks_equal\n"
  },
  {
    "path": "src/borg/testsuite/legacyrepository_test.py",
    "content": "import logging\nimport os\nimport sys\n\nfrom unittest.mock import patch\n\nimport pytest\nfrom xxhash import xxh64\n\nfrom ..hashindex import NSIndex1\nfrom ..helpers import Location\nfrom ..helpers import IntegrityError\nfrom ..helpers import msgpack\nfrom ..fslocking import Lock, LockFailed\nfrom ..platformflags import is_win32\nfrom ..legacyremote import LegacyRemoteRepository, InvalidRPCMethod, PathNotAllowed\nfrom ..legacyrepository import LegacyRepository, LoggedIO\nfrom ..legacyrepository import MAGIC, MAX_DATA_SIZE, TAG_DELETE, TAG_PUT2, TAG_PUT, TAG_COMMIT\nfrom ..repoobj import RepoObj\nfrom .hashindex_test import H\n\n\n@pytest.fixture()\ndef repository(tmp_path):\n    repository_location = os.fspath(tmp_path / \"repository\")\n    yield LegacyRepository(repository_location, exclusive=True, create=True)\n\n\n@pytest.fixture()\ndef remote_repository(tmp_path):\n    if is_win32:\n        pytest.skip(\"Remote repository does not yet work on Windows.\")\n    repository_location = Location(\"ssh://__testsuite__/\" + os.fspath(tmp_path / \"repository\"))\n    yield LegacyRemoteRepository(repository_location, exclusive=True, create=True)\n\n\ndef pytest_generate_tests(metafunc):\n    # Generate tests that run on both local and remote repositories.\n    if \"repo_fixtures\" in metafunc.fixturenames:\n        metafunc.parametrize(\"repo_fixtures\", [\"repository\", \"remote_repository\"])\n\n\ndef get_repository_from_fixture(repo_fixtures, request):\n    # Return the repository object from the fixture for tests that run on both local and remote repositories.\n    return request.getfixturevalue(repo_fixtures)\n\n\ndef reopen(repository, exclusive: bool | None = True, create=False):\n    if isinstance(repository, LegacyRepository):\n        if repository.io is not None or repository.lock is not None:\n            raise RuntimeError(\"Repo must be closed before a reopen. Cannot support nested repository contexts.\")\n        return LegacyRepository(repository.path, exclusive=exclusive, create=create)\n\n    if isinstance(repository, LegacyRemoteRepository):\n        if repository.p is not None or repository.sock is not None:\n            raise RuntimeError(\"Remote repo must be closed before a reopen. Cannot support nested repository contexts.\")\n        return LegacyRemoteRepository(repository.location, exclusive=exclusive, create=create)\n\n    raise TypeError(\n        f\"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'.\"\n    )\n\n\ndef get_path(repository):\n    if isinstance(repository, LegacyRepository):\n        return repository.path\n\n    if isinstance(repository, LegacyRemoteRepository):\n        return repository.location.path\n\n    raise TypeError(\n        f\"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'.\"\n    )\n\n\ndef fchunk(data, meta=b\"\"):\n    # Create a raw chunk that has a valid RepoObj layout but does not use encryption or compression.\n    hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta).digest(), xxh64(data).digest())\n    assert isinstance(data, bytes)\n    chunk = hdr + meta + data\n    return chunk\n\n\ndef pchunk(chunk):\n    # Parse data and meta from a raw chunk made by fchunk.\n    hdr_size = RepoObj.obj_header.size\n    hdr = chunk[:hdr_size]\n    meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2]\n    meta = chunk[hdr_size : hdr_size + meta_size]\n    data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size]\n    return data, meta\n\n\ndef pdchunk(chunk):\n    # Parse only the data from a raw chunk made by fchunk.\n    return pchunk(chunk)[0]\n\n\ndef add_keys(repository):\n    repository.put(H(0), fchunk(b\"foo\"))\n    repository.put(H(1), fchunk(b\"bar\"))\n    repository.put(H(3), fchunk(b\"bar\"))\n    repository.commit(compact=False)\n    repository.put(H(1), fchunk(b\"bar2\"))\n    repository.put(H(2), fchunk(b\"boo\"))\n    repository.delete(H(3))\n\n\ndef repo_dump(repository, label=None):\n    label = label + \": \" if label is not None else \"\"\n    H_trans = {H(i): i for i in range(10)}\n    H_trans[None] = -1  # key == None appears in commits\n    tag_trans = {TAG_PUT2: \"put2\", TAG_PUT: \"put\", TAG_DELETE: \"del\", TAG_COMMIT: \"comm\"}\n    for segment, fn in repository.io.segment_iterator():\n        for tag, key, offset, size, _ in repository.io.iter_objects(segment):\n            print(\"%s%s H(%d) -> %s[%d..+%d]\" % (label, tag_trans[tag], H_trans[key], fn, offset, size))\n    print()\n\n\ndef test_basic_operations(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        for x in range(100):\n            repository.put(H(x), fchunk(b\"SOMEDATA\"))\n        key50 = H(50)\n        assert pdchunk(repository.get(key50)) == b\"SOMEDATA\"\n        repository.delete(key50)\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            repository.get(key50)\n        repository.commit(compact=False)\n    with reopen(repository) as repository:\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            repository.get(key50)\n        for x in range(100):\n            if x == 50:\n                continue\n            assert pdchunk(repository.get(H(x))) == b\"SOMEDATA\"\n\n\ndef test_multiple_transactions(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.put(H(1), fchunk(b\"foo\"))\n        repository.commit(compact=False)\n        repository.delete(H(0))\n        repository.put(H(1), fchunk(b\"bar\"))\n        repository.commit(compact=False)\n        assert pdchunk(repository.get(H(1))) == b\"bar\"\n\n\ndef test_read_data(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        meta, data = b\"meta\", b\"data\"\n        hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta).digest(), xxh64(data).digest())\n        chunk_complete = hdr + meta + data\n        chunk_short = hdr + meta\n        repository.put(H(0), chunk_complete)\n        repository.commit(compact=False)\n        assert repository.get(H(0)) == chunk_complete\n        assert repository.get(H(0), read_data=True) == chunk_complete\n        assert repository.get(H(0), read_data=False) == chunk_short\n\n\ndef test_consistency(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n        repository.put(H(0), fchunk(b\"foo2\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo2\"\n        repository.put(H(0), fchunk(b\"bar\"))\n        assert pdchunk(repository.get(H(0))) == b\"bar\"\n        repository.delete(H(0))\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            repository.get(H(0))\n\n\ndef test_consistency2(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n        repository.commit(compact=False)\n        repository.put(H(0), fchunk(b\"foo2\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo2\"\n        repository.rollback()\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n\n\ndef test_overwrite_in_same_transaction(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.put(H(0), fchunk(b\"foo2\"))\n        repository.commit(compact=False)\n        assert pdchunk(repository.get(H(0))) == b\"foo2\"\n\n\ndef test_single_kind_transactions(repo_fixtures, request):\n    # put\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.commit(compact=False)\n    # replace\n    with reopen(repository) as repository:\n        repository.put(H(0), fchunk(b\"bar\"))\n        repository.commit(compact=False)\n    # delete\n    with reopen(repository) as repository:\n        repository.delete(H(0))\n        repository.commit(compact=False)\n\n\ndef test_list(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        for x in range(100):\n            repository.put(H(x), fchunk(b\"SOMEDATA\"))\n        repository.commit(compact=False)\n        repo_list = repository.list()\n        assert len(repo_list) == 100\n        first_half = repository.list(limit=50)\n        assert len(first_half) == 50\n        assert first_half == repo_list[:50]\n        second_half = repository.list(marker=first_half[-1])\n        assert len(second_half) == 50\n        assert second_half == repo_list[50:]\n        assert len(repository.list(limit=50)) == 50\n\n\ndef test_max_data_size(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        max_data = b\"x\" * (MAX_DATA_SIZE - RepoObj.obj_header.size)\n        repository.put(H(0), fchunk(max_data))\n        assert pdchunk(repository.get(H(0))) == max_data\n        with pytest.raises(IntegrityError):\n            repository.put(H(1), fchunk(max_data + b\"x\"))\n        repository.delete(H(0))\n\n\ndef _assert_sparse(repository):\n    # the superseded 123456... PUT\n    assert repository.compact[0] == 41 + 8 + 0  # len(fchunk(b\"123456789\"))\n    # a COMMIT\n    assert repository.compact[1] == 9\n    # the DELETE issued by the superseding PUT (or issued directly)\n    assert repository.compact[2] == 41\n    repository._rebuild_sparse(0)\n    assert repository.compact[0] == 41 + 8 + len(fchunk(b\"123456789\"))  # 9 is chunk or commit?\n\n\ndef test_sparse1(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.put(H(1), fchunk(b\"123456789\"))\n        repository.commit(compact=False)\n        repository.put(H(1), fchunk(b\"bar\"))\n        _assert_sparse(repository)\n\n\ndef test_sparse2(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.put(H(1), fchunk(b\"123456789\"))\n        repository.commit(compact=False)\n        repository.delete(H(1))\n        _assert_sparse(repository)\n\n\ndef test_sparse_delete(repository):\n    with repository:\n        chunk0 = fchunk(b\"1245\")\n        repository.put(H(0), chunk0)\n        repository.delete(H(0))\n        repository.io._write_fd.sync()\n        # the on-line tracking works on a per-object basis...\n        assert repository.compact[0] == 41 + 8 + 41 + 0  # len(chunk0) information is lost\n        repository._rebuild_sparse(0)\n        # ...while _rebuild_sparse can mark whole segments as completely sparse (which then includes the segment magic)\n        assert repository.compact[0] == 41 + 8 + 41 + len(chunk0) + len(MAGIC)\n        repository.commit(compact=True)\n        assert 0 not in [segment for segment, _ in repository.io.segment_iterator()]\n\n\ndef test_uncommitted_garbage(repository):\n    with repository:\n        # uncommitted garbage should be no problem, it is cleaned up automatically.\n        # we just have to be careful with invalidation of cached FDs in LoggedIO.\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.commit(compact=False)\n        # write some crap to an uncommitted segment file\n        last_segment = repository.io.get_latest_segment()\n        with open(repository.io.segment_filename(last_segment + 1), \"wb\") as f:\n            f.write(MAGIC + b\"crapcrapcrap\")\n    with reopen(repository) as repository:\n        # usually, opening the repo and starting a transaction should trigger a cleanup.\n        repository.put(H(0), fchunk(b\"bar\"))  # this may trigger compact_segments()\n        repository.commit(compact=True)\n        # the point here is that nothing blows up with an exception.\n\n\ndef test_replay_of_missing_index(repository):\n    with repository:\n        add_keys(repository)\n        for name in os.listdir(repository.path):\n            if name.startswith(\"index.\"):\n                os.unlink(os.path.join(repository.path, name))\n    with reopen(repository) as repository:\n        assert len(repository) == 3\n        assert repository.check() is True\n\n\ndef test_crash_before_compact_segments(repository):\n    with repository:\n        add_keys(repository)\n        repository.compact_segments = None\n        try:\n            repository.commit(compact=True)\n        except TypeError:\n            pass\n    with reopen(repository) as repository:\n        assert len(repository) == 3\n        assert repository.check() is True\n\n\ndef test_crash_before_write_index(repository):\n    with repository:\n        add_keys(repository)\n        repository.write_index = None\n        try:\n            repository.commit(compact=False)\n        except TypeError:\n            pass\n    with reopen(repository) as repository:\n        assert len(repository) == 3\n        assert repository.check() is True\n\n\ndef test_replay_lock_upgrade_old(repository):\n    with repository:\n        add_keys(repository)\n        for name in os.listdir(repository.path):\n            if name.startswith(\"index.\"):\n                os.unlink(os.path.join(repository.path, name))\n    with patch.object(Lock, \"upgrade\", side_effect=LockFailed) as upgrade:\n        with reopen(repository, exclusive=None) as repository:\n            # simulate old client that always does lock upgrades\n            # the repo is only locked by a shared read lock, but to replay segments,\n            # we need an exclusive write lock - check if the lock gets upgraded.\n            with pytest.raises(LockFailed):\n                len(repository)\n            upgrade.assert_called_once_with()\n\n\ndef test_replay_lock_upgrade(repository):\n    with repository:\n        add_keys(repository)\n        for name in os.listdir(repository.path):\n            if name.startswith(\"index.\"):\n                os.unlink(os.path.join(repository.path, name))\n    with patch.object(Lock, \"upgrade\", side_effect=LockFailed) as upgrade:\n        with reopen(repository, exclusive=False) as repository:\n            # current client usually does not do lock upgrade, except for replay\n            # the repo is only locked by a shared read lock, but to replay segments,\n            # we need an exclusive write lock - check if the lock gets upgraded.\n            with pytest.raises(LockFailed):\n                len(repository)\n            upgrade.assert_called_once_with()\n\n\ndef test_crash_before_deleting_compacted_segments(repository):\n    with repository:\n        add_keys(repository)\n        repository.io.delete_segment = None\n        try:\n            repository.commit(compact=False)\n        except TypeError:\n            pass\n    with reopen(repository) as repository:\n        assert len(repository) == 3\n        assert repository.check() is True\n        assert len(repository) == 3\n\n\ndef test_ignores_commit_tag_in_data(repository):\n    with repository:\n        repository.put(H(0), LoggedIO.COMMIT)\n    with reopen(repository) as repository:\n        io = repository.io\n        assert not io.is_committed_segment(io.get_latest_segment())\n\n\ndef test_moved_deletes_are_tracked(repository):\n    with repository:\n        repository.put(H(1), fchunk(b\"1\"))\n        repository.put(H(2), fchunk(b\"2\"))\n        repository.commit(compact=False)\n        repo_dump(repository, \"p1 p2 c\")\n        repository.delete(H(1))\n        repository.commit(compact=True)\n        repo_dump(repository, \"d1 cc\")\n        last_segment = repository.io.get_latest_segment() - 1\n        num_deletes = 0\n        for tag, key, offset, size, _ in repository.io.iter_objects(last_segment):\n            if tag == TAG_DELETE:\n                assert key == H(1)\n                num_deletes += 1\n        assert num_deletes == 1\n        assert last_segment in repository.compact\n        repository.put(H(3), fchunk(b\"3\"))\n        repository.commit(compact=True)\n        repo_dump(repository, \"p3 cc\")\n        assert last_segment not in repository.compact\n        assert not repository.io.segment_exists(last_segment)\n        for segment, _ in repository.io.segment_iterator():\n            for tag, key, offset, size, _ in repository.io.iter_objects(segment):\n                assert tag != TAG_DELETE\n                assert key != H(1)\n        # after compaction, there should be no empty shadowed_segments lists left over.\n        # we have no put or del anymore for H(1), so we lost knowledge about H(1).\n        assert H(1) not in repository.shadow_index\n\n\ndef test_shadowed_entries_are_preserved1(repository):\n    # this tests the shadowing-by-del behaviour\n    with repository:\n        get_latest_segment = repository.io.get_latest_segment\n        repository.put(H(1), fchunk(b\"1\"))\n        # This is the segment with our original PUT of interest\n        put_segment = get_latest_segment()\n        repository.commit(compact=False)\n        # we now delete H(1), and force this segment not to be compacted, which can happen\n        # if it's not sparse enough (symbolized by H(2) here).\n        repository.delete(H(1))\n        repository.put(H(2), fchunk(b\"1\"))\n        del_segment = get_latest_segment()\n        # we pretend these are mostly dense (not sparse) and won't be compacted\n        del repository.compact[put_segment]\n        del repository.compact[del_segment]\n        repository.commit(compact=True)\n        # we now perform an unrelated operation on the segment containing the DELETE,\n        # causing it to be compacted.\n        repository.delete(H(2))\n        repository.commit(compact=True)\n        assert repository.io.segment_exists(put_segment)\n        assert not repository.io.segment_exists(del_segment)\n        # basic case, since the index survived this must be ok\n        assert H(1) not in repository\n        # nuke index, force replay\n        os.unlink(os.path.join(repository.path, \"index.%d\" % get_latest_segment()))\n        # must not reappear\n        assert H(1) not in repository\n\n\ndef test_shadowed_entries_are_preserved2(repository):\n    # this tests the shadowing-by-double-put behaviour, see issue #5661\n    # assume this repo state:\n    # seg1: PUT H1\n    # seg2: COMMIT\n    # seg3: DEL H1, PUT H1, DEL H1, PUT H2\n    # seg4: COMMIT\n    # Note how due to the final DEL H1 in seg3, H1 is effectively deleted.\n    #\n    # compaction of only seg3:\n    # PUT H1 gets dropped because it is not needed any more.\n    # DEL H1 must be kept, because there is still a PUT H1 in seg1 which must not\n    # \"reappear\" in the index if the index gets rebuilt.\n    with repository:\n        get_latest_segment = repository.io.get_latest_segment\n        repository.put(H(1), fchunk(b\"1\"))\n        # This is the segment with our original PUT of interest\n        put_segment = get_latest_segment()\n        repository.commit(compact=False)\n        # We now put H(1) again (which implicitly does DEL(H(1)) followed by PUT(H(1), ...)),\n        # delete H(1) afterwards, and force this segment to not be compacted, which can happen\n        # if it's not sparse enough (symbolized by H(2) here).\n        repository.put(H(1), fchunk(b\"1\"))\n        repository.delete(H(1))\n        repository.put(H(2), fchunk(b\"1\"))\n        delete_segment = get_latest_segment()\n        # We pretend these are mostly dense (not sparse) and won't be compacted\n        del repository.compact[put_segment]\n        del repository.compact[delete_segment]\n        repository.commit(compact=True)\n        # Now we perform an unrelated operation on the segment containing the DELETE,\n        # causing it to be compacted.\n        repository.delete(H(2))\n        repository.commit(compact=True)\n        assert repository.io.segment_exists(put_segment)\n        assert not repository.io.segment_exists(delete_segment)\n        # Basic case, since the index survived this must be ok\n        assert H(1) not in repository\n        # Nuke index, force replay\n        os.unlink(os.path.join(repository.path, \"index.%d\" % get_latest_segment()))\n        # Must not reappear\n        assert H(1) not in repository  # F\n\n\ndef test_shadow_index_rollback(repository):\n    with repository:\n        repository.put(H(1), fchunk(b\"1\"))\n        repository.delete(H(1))\n        assert repository.shadow_index[H(1)] == [0]\n        repository.commit(compact=True)\n        repo_dump(repository, \"p1 d1 cc\")\n        # note how an empty list means that nothing is shadowed for sure\n        assert repository.shadow_index[H(1)] == []  # because the deletion is considered unstable\n        repository.put(H(1), b\"1\")\n        repository.delete(H(1))\n        repo_dump(repository, \"p1 d1\")\n        # 0 put/delete; 1 commit; 2 compacted; 3 commit; 4 put/delete\n        assert repository.shadow_index[H(1)] == [4]\n        repository.rollback()\n        repo_dump(repository, \"r\")\n        repository.put(H(2), fchunk(b\"1\"))\n        # after the rollback, segment 4 shouldn't be considered anymore\n        assert repository.shadow_index[H(1)] == []  # because the deletion is considered unstable\n\n\ndef test_additional_free_space(repository):\n    with repository:\n        add_keys(repository)\n        repository.config.set(\"repository\", \"additional_free_space\", \"1000T\")\n        repository.save_key(b\"shortcut to save_config\")\n    with reopen(repository) as repository:\n        repository.put(H(0), fchunk(b\"foobar\"))\n        with pytest.raises(LegacyRepository.InsufficientFreeSpaceError):\n            repository.commit(compact=False)\n        assert os.path.exists(repository.path)\n\n\ndef test_create_free_space(repository):\n    with repository:\n        repository.additional_free_space = 1e20\n        with pytest.raises(LegacyRepository.InsufficientFreeSpaceError):\n            add_keys(repository)\n        assert not os.path.exists(repository.path)\n\n\ndef make_auxiliary(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        repository.commit(compact=False)\n\n\ndef do_commit(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"fox\"))\n        repository.commit(compact=False)\n\n\ndef test_corrupted_hints(repository):\n    make_auxiliary(repository)\n    with open(os.path.join(repository.path, \"hints.1\"), \"ab\") as fd:\n        fd.write(b\"123456789\")\n    do_commit(repository)\n\n\ndef test_deleted_hints(repository):\n    make_auxiliary(repository)\n    os.unlink(os.path.join(repository.path, \"hints.1\"))\n    do_commit(repository)\n\n\ndef test_deleted_index(repository):\n    make_auxiliary(repository)\n    os.unlink(os.path.join(repository.path, \"index.1\"))\n    do_commit(repository)\n\n\ndef test_unreadable_hints(repository):\n    make_auxiliary(repository)\n    hints = os.path.join(repository.path, \"hints.1\")\n    os.unlink(hints)\n    os.mkdir(hints)\n    with pytest.raises(OSError):\n        do_commit(repository)\n\n\ndef _corrupt_index(repository):\n    # HashIndex is able to detect incorrect headers and file lengths,\n    # but on its own it can't tell if the data is correct.\n    index_path = os.path.join(repository.path, \"index.1\")\n    with open(index_path, \"r+b\") as fd:\n        index_data = fd.read()\n        # Flip one bit in a key stored in the index\n        corrupted_key = (int.from_bytes(H(0), \"little\") ^ 1).to_bytes(32, \"little\")\n        corrupted_index_data = index_data.replace(H(0), corrupted_key)\n        assert corrupted_index_data != index_data\n        assert len(corrupted_index_data) == len(index_data)\n        fd.seek(0)\n        fd.write(corrupted_index_data)\n\n\ndef test_index_corrupted_without_integrity(repository):\n    make_auxiliary(repository)\n    _corrupt_index(repository)\n    integrity_path = os.path.join(repository.path, \"integrity.1\")\n    os.unlink(integrity_path)\n    with repository:\n        # since the corrupted key is not noticed, the repository still thinks it contains one key...\n        assert len(repository) == 1\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            # ... but the real, uncorrupted key is not found in the corrupted index.\n            repository.get(H(0))\n\n\ndef test_unreadable_index(repository):\n    make_auxiliary(repository)\n    index = os.path.join(repository.path, \"index.1\")\n    os.unlink(index)\n    os.mkdir(index)\n    with pytest.raises(OSError):\n        do_commit(repository)\n\n\ndef test_unknown_integrity_version(repository):\n    make_auxiliary(repository)\n    # for now an unknown integrity data version is ignored and not an error.\n    integrity_path = os.path.join(repository.path, \"integrity.1\")\n    with open(integrity_path, \"r+b\") as fd:\n        msgpack.pack({b\"version\": 4.7}, fd)  # borg only understands version 2\n        fd.truncate()\n    with repository:\n        # no issues accessing the repository\n        assert len(repository) == 1\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n\n\ndef _subtly_corrupted_hints_setup(repository):\n    with repository:\n        assert len(repository) == 1\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n        repository.put(H(1), fchunk(b\"bar\"))\n        repository.put(H(2), fchunk(b\"baz\"))\n        repository.commit(compact=False)\n        repository.put(H(2), fchunk(b\"bazz\"))\n        repository.commit(compact=False)\n    hints_path = os.path.join(repository.path, \"hints.5\")\n    with open(hints_path, \"r+b\") as fd:\n        hints = msgpack.unpack(fd)\n        fd.seek(0)\n        # corrupt segment refcount\n        assert hints[\"segments\"][2] == 1\n        hints[\"segments\"][2] = 0\n        msgpack.pack(hints, fd)\n        fd.truncate()\n\n\ndef test_subtly_corrupted_hints(repository):\n    make_auxiliary(repository)\n    _subtly_corrupted_hints_setup(repository)\n    with repository:\n        repository.put(H(3), fchunk(b\"1234\"))\n        # do a compaction run, which succeeds since the failed checksum prompted a rebuild of the index+hints.\n        repository.commit(compact=True)\n        assert len(repository) == 4\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n        assert pdchunk(repository.get(H(1))) == b\"bar\"\n        assert pdchunk(repository.get(H(2))) == b\"bazz\"\n\n\ndef test_subtly_corrupted_hints_without_integrity(repository, caplog):\n    make_auxiliary(repository)\n    _subtly_corrupted_hints_setup(repository)\n    integrity_path = os.path.join(repository.path, \"integrity.5\")\n    os.unlink(integrity_path)\n    with repository:\n        repository.put(H(3), fchunk(b\"1234\"))\n        # Do a compaction run.\n        # The corrupted refcount is detected and logged as a warning, but compaction proceeds.\n        caplog.set_level(logging.WARNING, logger=\"borg.legacyrepository\")\n        repository.commit(compact=True)\n        assert \"Corrupted segment reference count\" in caplog.text\n        # We verify that the repository is still consistent.\n        assert repository.check()\n\n\ndef list_indices(repo_path):\n    return [name for name in os.listdir(repo_path) if name.startswith(\"index.\")]\n\n\ndef check(repository, repo_path, repair=False, status=True):\n    assert repository.check(repair=repair) == status\n    # Make sure no tmp files are left behind\n    tmp_files = [name for name in os.listdir(repo_path) if \"tmp\" in name]\n    assert tmp_files == [], \"Found tmp files\"\n\n\ndef get_objects(repository, *ids):\n    for id_ in ids:\n        pdchunk(repository.get(H(id_)))\n\n\ndef add_objects(repository, segments):\n    for ids in segments:\n        for id_ in ids:\n            repository.put(H(id_), fchunk(b\"data\"))\n        repository.commit(compact=False)\n\n\ndef get_head(repo_path):\n    return sorted(int(n) for n in os.listdir(os.path.join(repo_path, \"data\", \"0\")) if n.isdigit())[-1]\n\n\ndef open_index(repo_path):\n    return NSIndex1.read(os.path.join(repo_path, f\"index.{get_head(repo_path)}\"))\n\n\ndef corrupt_object(repo_path, id_):\n    idx = open_index(repo_path)\n    segment, offset = idx[H(id_)]\n    with open(os.path.join(repo_path, \"data\", \"0\", str(segment)), \"r+b\") as fd:\n        fd.seek(offset)\n        fd.write(b\"BOOM\")\n\n\ndef delete_segment(repository, segment):\n    repository.io.delete_segment(segment)\n\n\ndef delete_index(repo_path):\n    os.unlink(os.path.join(repo_path, f\"index.{get_head(repo_path)}\"))\n\n\ndef rename_index(repo_path, new_name):\n    os.replace(os.path.join(repo_path, f\"index.{get_head(repo_path)}\"), os.path.join(repo_path, new_name))\n\n\ndef list_objects(repository):\n    return {int(key) for key in repository.list()}\n\n\ndef test_repair_corrupted_segment(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repo_path = get_path(repository)\n        add_objects(repository, [[1, 2, 3], [4, 5], [6]])\n        assert {1, 2, 3, 4, 5, 6} == list_objects(repository)\n        check(repository, repo_path, status=True)\n        corrupt_object(repo_path, 5)\n        with pytest.raises(IntegrityError):\n            get_objects(repository, 5)\n        repository.rollback()\n        # make sure a regular check does not repair anything\n        check(repository, repo_path, status=False)\n        check(repository, repo_path, status=False)\n        # make sure a repair actually repairs the repo\n        check(repository, repo_path, repair=True, status=True)\n        get_objects(repository, 4)\n        check(repository, repo_path, status=True)\n        assert {1, 2, 3, 4, 6} == list_objects(repository)\n\n\ndef test_repair_missing_segment(repository):\n    # only test on local repo - files in RemoteRepository cannot be deleted\n    with repository:\n        add_objects(repository, [[1, 2, 3], [4, 5, 6]])\n        assert {1, 2, 3, 4, 5, 6} == list_objects(repository)\n        check(repository, repository.path, status=True)\n        delete_segment(repository, 2)\n        repository.rollback()\n        check(repository, repository.path, repair=True, status=True)\n        assert {1, 2, 3} == list_objects(repository)\n\n\ndef test_repair_missing_commit_segment(repository):\n    # only test on local repo - files in RemoteRepository cannot be deleted\n    with repository:\n        add_objects(repository, [[1, 2, 3], [4, 5, 6]])\n        delete_segment(repository, 3)\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            get_objects(repository, 4)\n        assert {1, 2, 3} == list_objects(repository)\n\n\ndef test_repair_corrupted_commit_segment(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repo_path = get_path(repository)\n        add_objects(repository, [[1, 2, 3], [4, 5, 6]])\n        with open(os.path.join(repo_path, \"data\", \"0\", \"3\"), \"r+b\") as fd:\n            fd.seek(-1, os.SEEK_END)\n            fd.write(b\"X\")\n        with pytest.raises(LegacyRepository.ObjectNotFound):\n            get_objects(repository, 4)\n        check(repository, repo_path, status=True)\n        get_objects(repository, 3)\n        assert {1, 2, 3} == list_objects(repository)\n\n\ndef test_repair_no_commits(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repo_path = get_path(repository)\n        add_objects(repository, [[1, 2, 3]])\n        with open(os.path.join(repo_path, \"data\", \"0\", \"1\"), \"r+b\") as fd:\n            fd.seek(-1, os.SEEK_END)\n            fd.write(b\"X\")\n        with pytest.raises(LegacyRepository.CheckNeeded):\n            get_objects(repository, 4)\n        check(repository, repo_path, status=False)\n        check(repository, repo_path, status=False)\n        assert list_indices(repo_path) == [\"index.1\"]\n        check(repository, repo_path, repair=True, status=True)\n        assert list_indices(repo_path) == [\"index.2\"]\n        check(repository, repo_path, status=True)\n        get_objects(repository, 3)\n        assert {1, 2, 3} == list_objects(repository)\n\n\ndef test_repair_missing_index(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repo_path = get_path(repository)\n        add_objects(repository, [[1, 2, 3], [4, 5, 6]])\n        delete_index(repo_path)\n        check(repository, repo_path, status=True)\n        get_objects(repository, 4)\n        assert {1, 2, 3, 4, 5, 6} == list_objects(repository)\n\n\ndef test_repair_index_too_new(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repo_path = get_path(repository)\n        add_objects(repository, [[1, 2, 3], [4, 5, 6]])\n        assert list_indices(repo_path) == [\"index.3\"]\n        rename_index(repo_path, \"index.100\")\n        check(repository, repo_path, status=True)\n        assert list_indices(repo_path) == [\"index.3\"]\n        get_objects(repository, 4)\n        assert {1, 2, 3, 4, 5, 6} == list_objects(repository)\n\n\ndef test_crash_before_compact(repository):\n    # only test on local repo - we can't mock-patch a RemoteRepository class in another process!\n    with repository:\n        repository.put(H(0), fchunk(b\"data\"))\n        repository.put(H(0), fchunk(b\"data2\"))\n        # simulate a crash before compact\n        with patch.object(LegacyRepository, \"compact_segments\") as compact:\n            repository.commit(compact=True)\n            compact.assert_called_once_with(0.1)\n    with reopen(repository) as repository:\n        check(repository, repository.path, repair=True)\n        assert pdchunk(repository.get(H(0))) == b\"data2\"\n\n\ndef test_hints_persistence(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"data\"))\n        repository.delete(H(0))\n        repository.commit(compact=False)\n        shadow_index_expected = repository.shadow_index\n        compact_expected = repository.compact\n        segments_expected = repository.segments\n    # close and re-open the repository (create fresh Repository instance) to\n    # check whether hints were persisted to / reloaded from disk\n    with reopen(repository) as repository:\n        repository.put(H(42), fchunk(b\"foobar\"))  # this will call prepare_txn() and load the hints data\n        # check if hints persistence worked:\n        assert shadow_index_expected == repository.shadow_index\n        assert compact_expected == repository.compact\n        del repository.segments[2]  # ignore the segment created by put(H(42), ...)\n        assert segments_expected == repository.segments\n    with reopen(repository) as repository:\n        check(repository, repository.path, repair=True)\n    with reopen(repository) as repository:\n        repository.put(H(42), fchunk(b\"foobar\"))  # this will call prepare_txn() and load the hints data\n        assert shadow_index_expected == repository.shadow_index\n        # sizes do not match, with vs. without header?\n        # assert compact_expected == repository.compact\n        del repository.segments[2]  # ignore the segment created by put(H(42), ...)\n        assert segments_expected == repository.segments\n\n\ndef test_hints_behaviour(repository):\n    with repository:\n        repository.put(H(0), fchunk(b\"data\"))\n        assert repository.shadow_index == {}\n        assert len(repository.compact) == 0\n        repository.delete(H(0))\n        repository.commit(compact=False)\n        # now there should be an entry for H(0) in shadow_index\n        assert H(0) in repository.shadow_index\n        assert len(repository.shadow_index[H(0)]) == 1\n        assert 0 in repository.compact  # segment 0 can be compacted\n        repository.put(H(42), fchunk(b\"foobar\"))  # see also do_compact()\n        repository.commit(compact=True, threshold=0.0)  # compact completely!\n        # nothing to compact anymore! no info left about stuff that does not exist anymore:\n        assert H(0) not in repository.shadow_index\n        # segment 0 was compacted away, no info about it left:\n        assert 0 not in repository.compact\n        assert 0 not in repository.segments\n\n\ndef _get_mock_args():\n    class MockArgs:\n        remote_path = \"borg\"\n        umask = 0o077\n        debug_topics = []\n        rsh = None\n\n        def __contains__(self, item):\n            # to behave like argparse.Namespace\n            return hasattr(self, item)\n\n    return MockArgs()\n\n\ndef test_remote_invalid_rpc(remote_repository):\n    with remote_repository:\n        with pytest.raises(InvalidRPCMethod):\n            remote_repository.call(\"__init__\", {})\n\n\ndef test_remote_rpc_exception_transport(remote_repository):\n    with remote_repository:\n        s1 = \"test string\"\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"DoesNotExist\"})\n        except LegacyRepository.DoesNotExist as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"AlreadyExists\"})\n        except LegacyRepository.AlreadyExists as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"CheckNeeded\"})\n        except LegacyRepository.CheckNeeded as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"IntegrityError\"})\n        except IntegrityError as e:\n            assert len(e.args) == 1\n            assert e.args[0] == s1\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"PathNotAllowed\"})\n        except PathNotAllowed as e:\n            assert len(e.args) == 1\n            assert e.args[0] == \"foo\"\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"ObjectNotFound\"})\n        except LegacyRepository.ObjectNotFound as e:\n            assert len(e.args) == 2\n            assert e.args[0] == s1\n            assert e.args[1] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"InvalidRPCMethod\"})\n        except InvalidRPCMethod as e:\n            assert len(e.args) == 1\n            assert e.args[0] == s1\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"divide\"})\n        except LegacyRemoteRepository.RPCError as e:\n            assert e.unpacked\n            assert e.get_message().startswith(\"ZeroDivisionError:\")\n            assert e.exception_class == \"ZeroDivisionError\"\n            assert len(e.exception_full) > 0\n\n\ndef test_remote_ssh_cmd(remote_repository):\n    with remote_repository:\n        args = _get_mock_args()\n        remote_repository._args = args\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"example.com\"]\n        assert remote_repository.ssh_cmd(Location(\"ssh://user@example.com/foo\")) == [\"ssh\", \"user@example.com\"]\n        assert remote_repository.ssh_cmd(Location(\"ssh://user@example.com:1234/foo\")) == [\n            \"ssh\",\n            \"-p\",\n            \"1234\",\n            \"user@example.com\",\n        ]\n        os.environ[\"BORG_RSH\"] = \"ssh --foo\"\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"--foo\", \"example.com\"]\n\n\ndef test_remote_borg_cmd(remote_repository):\n    with remote_repository:\n        assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, \"-m\", \"borg\", \"serve\"]\n        args = _get_mock_args()\n        # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown:\n        logging.getLogger().setLevel(logging.INFO)\n        # note: test logger is on info log level, so --info gets added automagically\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg\", \"serve\", \"--info\"]\n        args.remote_path = \"borg-0.28.2\"\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg-0.28.2\", \"serve\", \"--info\"]\n        args.debug_topics = [\"something_client_side\", \"repository_compaction\"]\n        assert remote_repository.borg_cmd(args, testing=False) == [\n            \"borg-0.28.2\",\n            \"serve\",\n            \"--info\",\n            \"--debug-topic=borg.debug.repository_compaction\",\n        ]\n        args = _get_mock_args()\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg\", \"serve\", \"--info\"]\n        args.rsh = \"ssh -i foo\"\n        remote_repository._args = args\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"-i\", \"foo\", \"example.com\"]\n"
  },
  {
    "path": "src/borg/testsuite/logger_test.py",
    "content": "import logging\nfrom io import StringIO\n\nimport pytest\n\nfrom ..logger import find_parent_module, create_logger, setup_logging\n\nlogger = create_logger()\n\n\n@pytest.fixture()\ndef io_logger():\n    io = StringIO()\n    handler = setup_logging(stream=io, env_var=None)\n    handler.setFormatter(logging.Formatter(\"%(name)s: %(message)s\"))\n    logger.setLevel(logging.DEBUG)\n    return io\n\n\ndef test_setup_logging(io_logger):\n    logger.info(\"hello world\")\n    assert io_logger.getvalue() == \"borg.testsuite.logger_test: hello world\\n\"\n\n\ndef test_multiple_loggers(io_logger):\n    logger = logging.getLogger(__name__)\n    logger.info(\"hello world 1\")\n    assert io_logger.getvalue() == \"borg.testsuite.logger_test: hello world 1\\n\"\n    logger = logging.getLogger(\"borg.testsuite.logger_test\")\n    logger.info(\"hello world 2\")\n    assert (\n        io_logger.getvalue() == \"borg.testsuite.logger_test: hello world 1\\nborg.testsuite.logger_test: hello world 2\\n\"\n    )\n    io_logger.truncate(0)\n    io_logger.seek(0)\n    logger = logging.getLogger(\"borg.testsuite.logger_test\")\n    logger.info(\"hello world 2\")\n    assert io_logger.getvalue() == \"borg.testsuite.logger_test: hello world 2\\n\"\n\n\ndef test_parent_module():\n    assert find_parent_module() == __name__\n\n\ndef test_lazy_logger():\n    # Just calling all the methods of the proxy.\n    logger.setLevel(logging.DEBUG)\n    logger.debug(\"debug\")\n    logger.info(\"info\")\n    logger.warning(\"warning\")\n    logger.error(\"error\")\n    logger.critical(\"critical\")\n    logger.log(logging.INFO, \"info\")\n    try:\n        raise Exception\n    except Exception:\n        logger.exception(\"exception\")\n"
  },
  {
    "path": "src/borg/testsuite/patterns_test.py",
    "content": "import io\nimport os.path\nimport sys\n\nimport pytest\n\nfrom ..helpers.argparsing import ArgumentTypeError\nfrom ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern\nfrom ..patterns import load_exclude_file, load_pattern_file\nfrom ..patterns import parse_pattern, PatternMatcher\nfrom ..patterns import get_regex_from_pattern\n\n\ndef check_patterns(files, pattern, expected):\n    \"\"\"Utility for testing patterns.\"\"\"\n    assert all([f == os.path.normpath(f) for f in files]), \"Pattern matchers expect normalized input paths\"\n\n    matched = [f for f in files if pattern.match(f)]\n\n    assert matched == (files if expected is None else expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"/\", []),\n        (\"/home\", [\"home\"]),\n        (\"/home///\", [\"home\"]),\n        (\"/./home\", [\"home\"]),\n        (\"/home/user\", [\"home/user\"]),\n        (\"/home/user2\", [\"home/user2\"]),\n        (\"/home/user/.bashrc\", [\"home/user/.bashrc\"]),\n    ],\n)\ndef test_patterns_full(pattern, expected):\n    files = [\"home\", \"home/user\", \"home/user2\", \"home/user/.bashrc\"]\n\n    check_patterns(files, PathFullPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"\", []),\n        (\"relative\", []),\n        (\"relative/path/\", [\"relative/path\"]),\n        (\"relative/path\", [\"relative/path\"]),\n    ],\n)\ndef test_patterns_full_relative(pattern, expected):\n    files = [\"relative/path\", \"relative/path2\"]\n\n    check_patterns(files, PathFullPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"/\", None),\n        (\"/./\", None),\n        (\"\", []),\n        (\"/home/u\", []),\n        (\"/home/user\", [\"home/user/.profile\", \"home/user/.bashrc\"]),\n        (\"/etc\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"///etc//////\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"/./home//..//home/user2\", [\"home/user2/.profile\", \"home/user2/public_html/index.html\"]),\n        (\"/srv\", [\"srv/messages\", \"srv/dmesg\"]),\n    ],\n)\ndef test_patterns_prefix(pattern, expected):\n    files = [\n        \"etc/server/config\",\n        \"etc/server/hosts\",\n        \"home\",\n        \"home/user/.profile\",\n        \"home/user/.bashrc\",\n        \"home/user2/.profile\",\n        \"home/user2/public_html/index.html\",\n        \"srv/messages\",\n        \"srv/dmesg\",\n    ]\n\n    check_patterns(files, PathPrefixPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"\", []),\n        (\"foo\", []),\n        (\"relative\", [\"relative/path1\", \"relative/two\"]),\n        (\"more\", [\"more/relative\"]),\n    ],\n)\ndef test_patterns_prefix_relative(pattern, expected):\n    files = [\"relative/path1\", \"relative/two\", \"more/relative\"]\n\n    check_patterns(files, PathPrefixPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"/*\", None),\n        (\"/./*\", None),\n        (\"*\", None),\n        (\n            \"*/*\",\n            [\n                \"etc/server/config\",\n                \"etc/server/hosts\",\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"srv/messages\",\n                \"srv/dmesg\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\n            \"*///*\",\n            [\n                \"etc/server/config\",\n                \"etc/server/hosts\",\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"srv/messages\",\n                \"srv/dmesg\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\"/home/u\", []),\n        (\n            \"/home/*\",\n            [\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\"/home/user/*\", [\"home/user/.profile\", \"home/user/.bashrc\"]),\n        (\"/etc/*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"*/.pr????e\", [\"home/user/.profile\", \"home/user2/.profile\"]),\n        (\"///etc//////*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"/./home//..//home/user2/*\", [\"home/user2/.profile\", \"home/user2/public_html/index.html\"]),\n        (\"/srv*\", [\"srv/messages\", \"srv/dmesg\"]),\n        (\"/home/*/.thumbnails\", [\"home/foo/.thumbnails\", \"home/foo/bar/.thumbnails\"]),\n    ],\n)\ndef test_patterns_fnmatch(pattern, expected):\n    files = [\n        \"etc/server/config\",\n        \"etc/server/hosts\",\n        \"home\",\n        \"home/user/.profile\",\n        \"home/user/.bashrc\",\n        \"home/user2/.profile\",\n        \"home/user2/public_html/index.html\",\n        \"srv/messages\",\n        \"srv/dmesg\",\n        \"home/foo/.thumbnails\",\n        \"home/foo/bar/.thumbnails\",\n    ]\n\n    check_patterns(files, FnmatchPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"*\", None),\n        (\"**/*\", None),\n        (\"/**/*\", None),\n        (\"/./*\", None),\n        (\n            \"*/*\",\n            [\n                \"etc/server/config\",\n                \"etc/server/hosts\",\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"srv/messages\",\n                \"srv/dmesg\",\n                \"srv2/blafasel\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\n            \"*///*\",\n            [\n                \"etc/server/config\",\n                \"etc/server/hosts\",\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"srv/messages\",\n                \"srv/dmesg\",\n                \"srv2/blafasel\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\"/home/u\", []),\n        (\n            \"/home/*\",\n            [\n                \"home/user/.profile\",\n                \"home/user/.bashrc\",\n                \"home/user2/.profile\",\n                \"home/user2/public_html/index.html\",\n                \"home/foo/.thumbnails\",\n                \"home/foo/bar/.thumbnails\",\n            ],\n        ),\n        (\"/home/user/*\", [\"home/user/.profile\", \"home/user/.bashrc\"]),\n        (\"/etc/*/*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"/etc/**/*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"/etc/**/*/*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"*/.pr????e\", []),\n        (\"**/.pr????e\", [\"home/user/.profile\", \"home/user2/.profile\"]),\n        (\"///etc//////*\", [\"etc/server/config\", \"etc/server/hosts\"]),\n        (\"/./home//..//home/user2/\", [\"home/user2/.profile\", \"home/user2/public_html/index.html\"]),\n        (\"/./home//..//home/user2/**/*\", [\"home/user2/.profile\", \"home/user2/public_html/index.html\"]),\n        (\"/srv*/\", [\"srv/messages\", \"srv/dmesg\", \"srv2/blafasel\"]),\n        (\"/srv*\", [\"srv\", \"srv/messages\", \"srv/dmesg\", \"srv2\", \"srv2/blafasel\"]),\n        (\"/srv/*\", [\"srv/messages\", \"srv/dmesg\"]),\n        (\"/srv2/**\", [\"srv2\", \"srv2/blafasel\"]),\n        (\"/srv2/**/\", [\"srv2/blafasel\"]),\n        (\"/home/*/.thumbnails\", [\"home/foo/.thumbnails\"]),\n        (\"/home/*/*/.thumbnails\", [\"home/foo/bar/.thumbnails\"]),\n    ],\n)\ndef test_patterns_shell(pattern, expected):\n    files = [\n        \"etc/server/config\",\n        \"etc/server/hosts\",\n        \"home\",\n        \"home/user/.profile\",\n        \"home/user/.bashrc\",\n        \"home/user2/.profile\",\n        \"home/user2/public_html/index.html\",\n        \"srv\",\n        \"srv/messages\",\n        \"srv/dmesg\",\n        \"srv2\",\n        \"srv2/blafasel\",\n        \"home/foo/.thumbnails\",\n        \"home/foo/bar/.thumbnails\",\n    ]\n\n    check_patterns(files, ShellPattern(pattern), expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, expected\",\n    [\n        # \"None\" means all files, i.e. all match the given pattern\n        (\"\", None),\n        (\".*\", None),\n        (\"^/\", None),\n        (\"^abc$\", []),\n        (\"^[^/]\", []),\n        (\n            \"^(?!/srv|/foo|/opt)\",\n            [\n                \"/home\",\n                \"/home/user/.profile\",\n                \"/home/user/.bashrc\",\n                \"/home/user2/.profile\",\n                \"/home/user2/public_html/index.html\",\n                \"/home/foo/.thumbnails\",\n                \"/home/foo/bar/.thumbnails\",\n            ],\n        ),\n    ],\n)\ndef test_patterns_regex(pattern, expected):\n    files = [\n        \"/srv/data\",\n        \"/foo/bar\",\n        \"/home\",\n        \"/home/user/.profile\",\n        \"/home/user/.bashrc\",\n        \"/home/user2/.profile\",\n        \"/home/user2/public_html/index.html\",\n        \"/opt/log/messages.txt\",\n        \"/opt/log/dmesg.txt\",\n        \"/home/foo/.thumbnails\",\n        \"/home/foo/bar/.thumbnails\",\n    ]\n\n    obj = RegexPattern(pattern)\n    assert str(obj) == pattern\n    assert obj.pattern == pattern\n\n    check_patterns(files, obj, expected)\n\n\ndef test_regex_pattern():\n    # The forward slash must match the platform-specific path separator\n    assert RegexPattern(\"^/$\").match(\"/\")\n    assert RegexPattern(\"^/$\").match(os.path.sep)\n    assert not RegexPattern(r\"^\\\\$\").match(\"/\")\n\n\ndef use_normalized_unicode():\n    return sys.platform in (\"darwin\",)\n\n\ndef _make_test_patterns(pattern):\n    return [\n        PathPrefixPattern(pattern),\n        FnmatchPattern(pattern),\n        RegexPattern(f\"^{pattern}/foo$\"),\n        ShellPattern(pattern),\n    ]\n\n\n@pytest.mark.parametrize(\"pattern\", _make_test_patterns(\"b\\N{LATIN SMALL LETTER A WITH ACUTE}\"))\ndef test_composed_unicode_pattern(pattern):\n    assert pattern.match(\"b\\N{LATIN SMALL LETTER A WITH ACUTE}/foo\")\n    assert pattern.match(\"ba\\N{COMBINING ACUTE ACCENT}/foo\") == use_normalized_unicode()\n\n\n@pytest.mark.parametrize(\"pattern\", _make_test_patterns(\"ba\\N{COMBINING ACUTE ACCENT}\"))\ndef test_decomposed_unicode_pattern(pattern):\n    assert pattern.match(\"b\\N{LATIN SMALL LETTER A WITH ACUTE}/foo\") == use_normalized_unicode()\n    assert pattern.match(\"ba\\N{COMBINING ACUTE ACCENT}/foo\")\n\n\n@pytest.mark.parametrize(\"pattern\", _make_test_patterns(str(b\"ba\\x80\", \"latin1\")))\ndef test_invalid_unicode_pattern(pattern):\n    assert not pattern.match(\"ba/foo\")\n    assert pattern.match(str(b\"ba\\x80/foo\", \"latin1\"))\n\n\n@pytest.mark.parametrize(\n    \"lines, expected\",\n    [\n        # \"None\" means all files, i.e. none excluded\n        ([], None),\n        ([\"# Comment only\"], None),\n        ([\"*\"], []),\n        (\n            [\n                \"# Comment\",\n                \"*/something00.txt\",\n                \"  *whitespace*  \",\n                # Whitespace before comment\n                \" #/ws*\",\n                # Empty line\n                \"\",\n                \"# EOF\",\n            ],\n            [\"more/data\", \"home\", \" #/wsfoobar\"],\n        ),\n        ([r\"re:.*\"], []),\n        ([r\"re:\\s\"], [\"data/something00.txt\", \"more/data\", \"home\"]),\n        ([r\"re:(.)(\\1)\"], [\"more/data\", \"home\", \"\\tstart/whitespace\", \"whitespace/end\\t\"]),\n        (\n            [\n                \"\",\n                \"\",\n                \"\",\n                \"# This is a test with mixed pattern styles\",\n                # Case-insensitive pattern\n                r\"re:(?i)BAR|ME$\",\n                \"\",\n                \"*whitespace*\",\n                \"fm:*/something00*\",\n            ],\n            [\"more/data\"],\n        ),\n        ([r\"  re:^\\s  \"], [\"data/something00.txt\", \"more/data\", \"home\", \"whitespace/end\\t\"]),\n        ([r\"  re:\\s$  \"], [\"data/something00.txt\", \"more/data\", \"home\", \" #/wsfoobar\", \"\\tstart/whitespace\"]),\n        ([\"pp:./\"], None),\n        # leading slash is removed\n        ([\"pp:/\"], []),\n        ([\"pp:aaabbb\"], None),\n        ([\"pp:/data\", \"pp: #/\", \"pp:\\tstart\", \"pp:/whitespace\"], [\"more/data\", \"home\"]),\n        (\n            [\"/nomatch\", \"/more/*\"],\n            [\"data/something00.txt\", \"home\", \" #/wsfoobar\", \"\\tstart/whitespace\", \"whitespace/end\\t\"],\n        ),\n        # the order of exclude patterns shouldn't matter\n        (\n            [\"/more/*\", \"/nomatch\"],\n            [\"data/something00.txt\", \"home\", \" #/wsfoobar\", \"\\tstart/whitespace\", \"whitespace/end\\t\"],\n        ),\n    ],\n)\ndef test_exclude_patterns_from_file(tmpdir, lines, expected):\n    files = [\"data/something00.txt\", \"more/data\", \"home\", \" #/wsfoobar\", \"\\tstart/whitespace\", \"whitespace/end\\t\"]\n\n    def evaluate(filename):\n        patterns = []\n        with open(filename) as f:\n            load_exclude_file(f, patterns)\n        matcher = PatternMatcher(fallback=True)\n        matcher.add_inclexcl(patterns)\n        return [path for path in files if matcher.match(path)]\n\n    exclfile = tmpdir.join(\"exclude.txt\")\n\n    with exclfile.open(\"wt\") as fh:\n        fh.write(\"\\n\".join(lines))\n\n    assert evaluate(str(exclfile)) == (files if expected is None else expected)\n\n\n@pytest.mark.parametrize(\n    \"lines, expected_roots, expected_numpatterns\",\n    [\n        # \"None\" means all files, i.e. none excluded\n        ([], [], 0),\n        ([\"# Comment only\"], [], 0),\n        ([\"- *\"], [], 1),\n        ([\"+fm:*/something00.txt\", \"-/data\"], [], 2),\n        ([\"R /\"], [\"/\"], 0),\n        ([\"R /\", \"# comment\"], [\"/\"], 0),\n        ([\"# comment\", \"- /data\", \"R /home\"], [\"/home\"], 1),\n    ],\n)\ndef test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatterns):\n    def evaluate(filename):\n        roots = []\n        inclexclpatterns = []\n        with open(filename) as f:\n            load_pattern_file(f, roots, inclexclpatterns)\n        return roots, len(inclexclpatterns)\n\n    patternfile = tmpdir.join(\"patterns.txt\")\n\n    with patternfile.open(\"wt\") as fh:\n        fh.write(\"\\n\".join(lines))\n\n    roots, numpatterns = evaluate(str(patternfile))\n    assert roots == expected_roots\n    assert numpatterns == expected_numpatterns\n\n\ndef test_switch_patterns_style():\n    patterns = \"\"\"\\\n        +0_initial_default_is_shell\n        p fm\n        +1_fnmatch\n        P re\n        +2_regex\n        +3_more_regex\n        P pp\n        +4_pathprefix\n        p fm\n        p sh\n        +5_shell\n    \"\"\"\n    pattern_file = io.StringIO(patterns)\n    roots, patterns = [], []\n    load_pattern_file(pattern_file, roots, patterns)\n    assert len(patterns) == 6\n    assert isinstance(patterns[0].val, ShellPattern)\n    assert isinstance(patterns[1].val, FnmatchPattern)\n    assert isinstance(patterns[2].val, RegexPattern)\n    assert isinstance(patterns[3].val, RegexPattern)\n    assert isinstance(patterns[4].val, PathPrefixPattern)\n    assert isinstance(patterns[5].val, ShellPattern)\n\n\n@pytest.mark.parametrize(\n    \"lines\", [([\"X /data\"]), ([\"/data\"])]  # illegal pattern type prefix  # need a pattern type prefix\n)\ndef test_load_invalid_patterns_from_file(tmpdir, lines):\n    patternfile = tmpdir.join(\"patterns.txt\")\n    with patternfile.open(\"wt\") as fh:\n        fh.write(\"\\n\".join(lines))\n    filename = str(patternfile)\n    with pytest.raises(ArgumentTypeError):\n        roots = []\n        inclexclpatterns = []\n        with open(filename) as f:\n            load_pattern_file(f, roots, inclexclpatterns)\n\n\n@pytest.mark.parametrize(\n    \"lines, expected\",\n    [\n        # \"None\" means all files, i.e. none excluded\n        ([], None),\n        ([\"# Comment only\"], None),\n        ([\"- *\"], []),\n        # default match type is sh: for patterns -> * doesn't match a /\n        (\n            [\"-*/something0?.txt\"],\n            [\"data\", \"data/subdir/something01.txt\", \"home\", \"home/leo\", \"home/leo/t\", \"home/other\"],\n        ),\n        (\n            [\"-fm:*/something00.txt\"],\n            [\"data\", \"data/subdir/something01.txt\", \"home\", \"home/leo\", \"home/leo/t\", \"home/other\"],\n        ),\n        ([\"-fm:*/something0?.txt\"], [\"data\", \"home\", \"home/leo\", \"home/leo/t\", \"home/other\"]),\n        ([\"+/*/something0?.txt\", \"-/data\"], [\"data/something00.txt\", \"home\", \"home/leo\", \"home/leo/t\", \"home/other\"]),\n        ([\"+fm:*/something00.txt\", \"-/data\"], [\"data/something00.txt\", \"home\", \"home/leo\", \"home/leo/t\", \"home/other\"]),\n        # include /home/leo and exclude the rest of /home:\n        (\n            [\"+/home/leo\", \"-/home/*\"],\n            [\"data\", \"data/something00.txt\", \"data/subdir/something01.txt\", \"home\", \"home/leo\", \"home/leo/t\"],\n        ),\n        # wrong order, /home/leo is already excluded by -/home/*:\n        ([\"-/home/*\", \"+/home/leo\"], [\"data\", \"data/something00.txt\", \"data/subdir/something01.txt\", \"home\"]),\n        (\n            [\"+fm:/home/leo\", \"-/home/\"],\n            [\"data\", \"data/something00.txt\", \"data/subdir/something01.txt\", \"home\", \"home/leo\", \"home/leo/t\"],\n        ),\n    ],\n)\ndef test_inclexcl_patterns_from_file(tmpdir, lines, expected):\n    files = [\n        \"data\",\n        \"data/something00.txt\",\n        \"data/subdir/something01.txt\",\n        \"home\",\n        \"home/leo\",\n        \"home/leo/t\",\n        \"home/other\",\n    ]\n\n    def evaluate(filename):\n        matcher = PatternMatcher(fallback=True)\n        roots = []\n        inclexclpatterns = []\n        with open(filename) as f:\n            load_pattern_file(f, roots, inclexclpatterns)\n        matcher.add_inclexcl(inclexclpatterns)\n        return [path for path in files if matcher.match(path)]\n\n    patternfile = tmpdir.join(\"patterns.txt\")\n\n    with patternfile.open(\"wt\") as fh:\n        fh.write(\"\\n\".join(lines))\n\n    assert evaluate(str(patternfile)) == (files if expected is None else expected)\n\n\n@pytest.mark.parametrize(\n    \"pattern, cls\",\n    [\n        (\"\", FnmatchPattern),\n        # Default style\n        (\"*\", FnmatchPattern),\n        (\"/data/*\", FnmatchPattern),\n        # fnmatch style\n        (\"fm:\", FnmatchPattern),\n        (\"fm:*\", FnmatchPattern),\n        (\"fm:/data/*\", FnmatchPattern),\n        (\"fm:fm:/data/*\", FnmatchPattern),\n        # Regular expression\n        (\"re:\", RegexPattern),\n        (\"re:.*\", RegexPattern),\n        (\"re:^/something/\", RegexPattern),\n        (\"re:re:^/something/\", RegexPattern),\n        # Path prefix\n        (\"pp:\", PathPrefixPattern),\n        (\"pp:/\", PathPrefixPattern),\n        (\"pp:/data/\", PathPrefixPattern),\n        (\"pp:pp:/data/\", PathPrefixPattern),\n        # Shell-pattern style\n        (\"sh:\", ShellPattern),\n        (\"sh:*\", ShellPattern),\n        (\"sh:/data/*\", ShellPattern),\n        (\"sh:sh:/data/*\", ShellPattern),\n    ],\n)\ndef test_parse_pattern(pattern, cls):\n    assert isinstance(parse_pattern(pattern), cls)\n\n\n@pytest.mark.parametrize(\"pattern\", [\"aa:\", \"fo:*\", \"00:\", \"x1:abc\"])\ndef test_parse_pattern_error(pattern):\n    with pytest.raises(ValueError):\n        parse_pattern(pattern)\n\n\ndef test_pattern_matcher():\n    pm = PatternMatcher()\n\n    assert pm.fallback is None\n\n    for i in [\"\", \"foo\", \"bar\"]:\n        assert pm.match(i) is None\n\n    # add extra entries to aid in testing\n    for target in [\"A\", \"B\", \"Empty\", \"FileNotFound\"]:\n        pm.is_include_cmd[target] = target\n\n    pm.add([RegexPattern(\"^a\")], \"A\")\n    pm.add([RegexPattern(\"^b\"), RegexPattern(\"^z\")], \"B\")\n    pm.add([RegexPattern(\"^$\")], \"Empty\")\n    pm.fallback = \"FileNotFound\"\n\n    assert pm.match(\"\") == \"Empty\"\n    assert pm.match(\"aaa\") == \"A\"\n    assert pm.match(\"bbb\") == \"B\"\n    assert pm.match(\"ccc\") == \"FileNotFound\"\n    assert pm.match(\"xyz\") == \"FileNotFound\"\n    assert pm.match(\"z\") == \"B\"\n\n    assert PatternMatcher(fallback=\"hey!\").fallback == \"hey!\"\n\n\n@pytest.mark.parametrize(\n    \"pattern, regex\",\n    [\n        (\"foo.bar\", r\"foo\\.bar\"),  # default is id:\n        (\"id:foo.bar\", r\"foo\\.bar\"),\n        (\"id:foo?\", r\"foo\\?\"),\n        (\"re:foo.bar\", r\"foo.bar\"),\n        (\"re:.*(fooo?|bar|baz).*\", r\".*(fooo?|bar|baz).*\"),\n        (\"sh:foo.*\", r\"foo\\.[^\\/]*\"),\n    ],\n)\ndef test_regex_from_pattern(pattern, regex):\n    assert get_regex_from_pattern(pattern) == regex\n"
  },
  {
    "path": "src/borg/testsuite/platform/__init__.py",
    "content": ""
  },
  {
    "path": "src/borg/testsuite/platform/all_test.py",
    "content": "import io\n\nfrom ...platform import swidth, SyncFile\n\n\ndef test_swidth_ascii():\n    assert swidth(\"borg\") == 4\n\n\ndef test_swidth_cjk():\n    assert swidth(\"バックアップ\") == 6 * 2\n\n\ndef test_swidth_mixed():\n    assert swidth(\"borgバックアップ\") == 4 + 6 * 2\n\n\ndef test_syncfile_seek_tell(tmp_path):\n    \"\"\"SyncFile exposes seek() and tell() from the underlying file object.\"\"\"\n    path = tmp_path / \"testfile\"\n    with SyncFile(path, binary=True) as sf:\n        sf.write(b\"hello world\")\n        assert sf.tell() == 11\n        sf.seek(0, io.SEEK_SET)\n        assert sf.tell() == 0\n        sf.seek(0, io.SEEK_END)\n        assert sf.tell() == 11\n        sf.seek(5, io.SEEK_SET)\n        assert sf.tell() == 5\n        assert sf.read() == b\" world\"\n    assert path.read_bytes() == b\"hello world\"\n\n\ndef test_syncfile_close_idempotent(tmp_path):\n    \"\"\"Calling SyncFile.close() twice does not raise.\"\"\"\n    path = tmp_path / \"testfile\"\n    sf = SyncFile(path, binary=True)\n    sf.write(b\"data\")\n    sf.close()\n    sf.close()  # must not raise\n"
  },
  {
    "path": "src/borg/testsuite/platform/darwin_test.py",
    "content": "import os\nimport tempfile\n\nfrom ...platform import acl_get, acl_set\nfrom ...platform import fdatasync, sync_dir\nfrom .platform_test import skipif_not_darwin, skipif_fakeroot_detected, skipif_acls_not_working\n\n# Set module-level skips\npytestmark = [skipif_not_darwin, skipif_fakeroot_detected]\n\n\ndef get_acl(path, numeric_ids=False):\n    item = {}\n    acl_get(path, item, os.stat(path), numeric_ids=numeric_ids)\n    return item\n\n\ndef set_acl(path, acl, numeric_ids=False):\n    item = {\"acl_extended\": acl}\n    acl_set(path, item, numeric_ids=numeric_ids)\n\n\n@skipif_acls_not_working\ndef test_extended_acl():\n    file = tempfile.NamedTemporaryFile()\n    assert get_acl(file.name) == {}\n    set_acl(\n        file.name,\n        b\"!#acl 1\\n\"\n        b\"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:staff:0:allow:read\\n\"\n        b\"user:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\\n\",\n        numeric_ids=False,\n    )\n    assert b\"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000014:staff:20:allow:read\" in get_acl(file.name)[\"acl_extended\"]\n    assert b\"user:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\" in get_acl(file.name)[\"acl_extended\"]\n\n    file2 = tempfile.NamedTemporaryFile()\n    set_acl(\n        file2.name,\n        b\"!#acl 1\\n\"\n        b\"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:staff:0:allow:read\\n\"\n        b\"user:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\\n\",\n        numeric_ids=True,\n    )\n    assert b\"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:wheel:0:allow:read\" in get_acl(file2.name)[\"acl_extended\"]\n    assert (\n        b\"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read\"\n        in get_acl(file2.name, numeric_ids=True)[\"acl_extended\"]\n    )\n\n\ndef test_fdatasync_uses_f_fullfsync(monkeypatch):\n    \"\"\"Verify fcntl F_FULLFSYNC is called.\"\"\"\n    import fcntl as fcntl_mod\n    from ...platform import darwin\n\n    calls = []\n    original_fcntl = fcntl_mod.fcntl\n\n    def mock_fcntl(fd, cmd, *args):\n        calls.append((fd, cmd))\n        return original_fcntl(fd, cmd, *args)\n\n    monkeypatch.setattr(fcntl_mod, \"fcntl\", mock_fcntl)\n\n    with tempfile.NamedTemporaryFile() as tmp:\n        tmp.write(b\"test data\")\n        tmp.flush()\n        darwin.fdatasync(tmp.fileno())\n\n    assert any(cmd == fcntl_mod.F_FULLFSYNC for _, cmd in calls), \"fdatasync should call fcntl with F_FULLFSYNC\"\n\n\ndef test_fdatasync_falls_back_to_fsync(monkeypatch):\n    \"\"\"Verify os.fsync fallback when F_FULLFSYNC fails.\"\"\"\n    import fcntl as fcntl_mod\n    from ...platform import darwin\n\n    fsync_calls = []\n\n    def mock_fcntl(fd, cmd, *args):\n        if cmd == fcntl_mod.F_FULLFSYNC:\n            raise OSError(\"F_FULLFSYNC not supported\")\n        return 0\n\n    def mock_fsync(fd):\n        fsync_calls.append(fd)\n\n    monkeypatch.setattr(fcntl_mod, \"fcntl\", mock_fcntl)\n    monkeypatch.setattr(os, \"fsync\", mock_fsync)\n\n    with tempfile.NamedTemporaryFile() as tmp:\n        tmp.write(b\"test data\")\n        tmp.flush()\n        darwin.fdatasync(tmp.fileno())\n\n    assert len(fsync_calls) == 1, \"Should fall back to os.fsync when F_FULLFSYNC fails\"\n\n\ndef test_fdatasync_basic():\n    \"\"\"Integration: fdatasync completes on a real file without error.\"\"\"\n    with tempfile.NamedTemporaryFile() as tmp:\n        tmp.write(b\"test data for fdatasync\")\n        tmp.flush()\n        fdatasync(tmp.fileno())\n\n\ndef test_sync_dir_basic():\n    \"\"\"Integration: sync_dir completes on a real directory without error.\"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        sync_dir(tmpdir)\n"
  },
  {
    "path": "src/borg/testsuite/platform/freebsd_test.py",
    "content": "import os\nimport tempfile\n\nfrom ...platform import acl_get, acl_set\nfrom .platform_test import skipif_not_freebsd, skipif_fakeroot_detected, skipif_acls_not_working\n\n# set module-level skips\npytestmark = [skipif_not_freebsd, skipif_fakeroot_detected]\n\n\nACCESS_ACL = \"\"\"\\\nuser::rw-\nuser:root:rw-\nuser:9999:r--\ngroup::r--\ngroup:wheel:r--\ngroup:9999:r--\nmask::rw-\nother::r--\n\"\"\".encode(\n    \"ascii\"\n)\n\nDEFAULT_ACL = \"\"\"\\\nuser::rw-\nuser:root:r--\nuser:8888:r--\ngroup::r--\ngroup:wheel:r--\ngroup:8888:r--\nmask::rw-\nother::r--\n\"\"\".encode(\n    \"ascii\"\n)\n\n\ndef get_acl(path, numeric_ids=False):\n    item = {}\n    acl_get(path, item, os.stat(path), numeric_ids=numeric_ids)\n    return item\n\n\ndef set_acl(path, access=None, default=None, nfs4=None, numeric_ids=False):\n    item = {\"acl_access\": access, \"acl_default\": default, \"acl_nfs4\": nfs4}\n    acl_set(path, item, numeric_ids=numeric_ids)\n\n\n@skipif_acls_not_working\ndef test_access_acl():\n    file1 = tempfile.NamedTemporaryFile()\n    assert get_acl(file1.name) == {}\n    set_acl(\n        file1.name,\n        access=b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-\\ngroup:wheel:rw-\\n\",\n        numeric_ids=False,\n    )\n    acl_access_names = get_acl(file1.name, numeric_ids=False)[\"acl_access\"]\n    assert b\"user:root:rw-\" in acl_access_names\n    assert b\"group:wheel:rw-\" in acl_access_names\n    acl_access_ids = get_acl(file1.name, numeric_ids=True)[\"acl_access\"]\n    assert b\"user:0:rw-\" in acl_access_ids\n    assert b\"group:0:rw-\" in acl_access_ids\n\n    file2 = tempfile.NamedTemporaryFile()\n    set_acl(\n        file2.name, access=b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:0:rw-\\ngroup:0:rw-\\n\", numeric_ids=True\n    )\n    acl_access_names = get_acl(file2.name, numeric_ids=False)[\"acl_access\"]\n    assert b\"user:root:rw-\" in acl_access_names\n    assert b\"group:wheel:rw-\" in acl_access_names\n    acl_access_ids = get_acl(file2.name, numeric_ids=True)[\"acl_access\"]\n    assert b\"user:0:rw-\" in acl_access_ids\n    assert b\"group:0:rw-\" in acl_access_ids\n\n    file3 = tempfile.NamedTemporaryFile()\n    set_acl(\n        file3.name,\n        access=b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-:9999\\ngroup:wheel:rw-:9999\\n\",\n        numeric_ids=True,\n    )\n    acl_access_ids = get_acl(file3.name, numeric_ids=True)[\"acl_access\"]\n    assert b\"user:9999:rw-\" in acl_access_ids\n    assert b\"group:9999:rw-\" in acl_access_ids\n\n\n@skipif_acls_not_working\ndef test_default_acl():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        assert get_acl(tmpdir) == {}\n        set_acl(tmpdir, access=ACCESS_ACL, default=DEFAULT_ACL)\n        assert get_acl(tmpdir)[\"acl_access\"] == ACCESS_ACL\n        assert get_acl(tmpdir)[\"acl_default\"] == DEFAULT_ACL\n\n\n# nfs4 acls testing not implemented.\n"
  },
  {
    "path": "src/borg/testsuite/platform/linux_test.py",
    "content": "import os\nimport tempfile\n\nfrom ...platform import acl_get, acl_set\nfrom .platform_test import skipif_not_linux, skipif_fakeroot_detected, skipif_acls_not_working, skipif_no_ubel_user\n\n# set module-level skips\npytestmark = [skipif_not_linux, skipif_fakeroot_detected]\n\n\nACCESS_ACL = \"\"\"\\\nuser::rw-\nuser:root:rw-:0\nuser:9999:r--:9999\ngroup::r--\ngroup:root:r--:0\ngroup:9999:r--:9999\nmask::rw-\nother::r--\\\n\"\"\".encode(\n    \"ascii\"\n)\n\nDEFAULT_ACL = \"\"\"\\\nuser::rw-\nuser:root:r--:0\nuser:8888:r--:8888\ngroup::r--\ngroup:root:r--:0\ngroup:8888:r--:8888\nmask::rw-\nother::r--\\\n\"\"\".encode(\n    \"ascii\"\n)\n\n\ndef get_acl(path, numeric_ids=False):\n    item = {}\n    acl_get(path, item, os.stat(path), numeric_ids=numeric_ids)\n    return item\n\n\ndef set_acl(path, access=None, default=None, numeric_ids=False):\n    item = {\"acl_access\": access, \"acl_default\": default}\n    acl_set(path, item, numeric_ids=numeric_ids)\n\n\n@skipif_acls_not_working\ndef test_access_acl():\n    file = tempfile.NamedTemporaryFile()\n    assert get_acl(file.name) == {}\n\n    set_acl(\n        file.name,\n        access=b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-:9999\\ngroup:root:rw-:9999\\n\",\n        numeric_ids=False,\n    )\n    assert b\"user:root:rw-:0\" in get_acl(file.name)[\"acl_access\"]\n    assert b\"group:root:rw-:0\" in get_acl(file.name)[\"acl_access\"]\n    assert b\"user:0:rw-:0\" in get_acl(file.name, numeric_ids=True)[\"acl_access\"]\n\n    file2 = tempfile.NamedTemporaryFile()\n    set_acl(\n        file2.name,\n        access=b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-:9999\\ngroup:root:rw-:9999\\n\",\n        numeric_ids=True,\n    )\n    assert b\"user:9999:rw-:9999\" in get_acl(file2.name)[\"acl_access\"]\n    assert b\"group:9999:rw-:9999\" in get_acl(file2.name)[\"acl_access\"]\n\n\n@skipif_acls_not_working\ndef test_default_acl():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        assert get_acl(tmpdir) == {}\n        set_acl(tmpdir, access=ACCESS_ACL, default=DEFAULT_ACL)\n        assert get_acl(tmpdir)[\"acl_access\"] == ACCESS_ACL\n        assert get_acl(tmpdir)[\"acl_default\"] == DEFAULT_ACL\n\n\n@skipif_acls_not_working\n@skipif_no_ubel_user\ndef test_non_ascii_acl():\n    # Testing non-ASCII ACL processing to see whether our code is robust.\n    # I have no idea whether non-ASCII ACLs are allowed by the standard,\n    # but in practice they seem to be out there and must not cause failures.\n    file = tempfile.NamedTemporaryFile()\n    assert get_acl(file.name) == {}\n    nothing_special = b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\n\"\n    # TODO: can this be tested without having an existing system user übel with uid 666 gid 666?\n    user_entry = \"user:übel:rw-:666\".encode()\n    user_entry_numeric = b\"user:666:rw-:666\"\n    group_entry = \"group:übel:rw-:666\".encode()\n    group_entry_numeric = b\"group:666:rw-:666\"\n    acl = b\"\\n\".join([nothing_special, user_entry, group_entry])\n    set_acl(file.name, access=acl, numeric_ids=False)\n\n    acl_access = get_acl(file.name, numeric_ids=False)[\"acl_access\"]\n    assert user_entry in acl_access\n    assert group_entry in acl_access\n\n    acl_access_numeric = get_acl(file.name, numeric_ids=True)[\"acl_access\"]\n    assert user_entry_numeric in acl_access_numeric\n    assert group_entry_numeric in acl_access_numeric\n\n    file2 = tempfile.NamedTemporaryFile()\n    set_acl(file2.name, access=acl, numeric_ids=True)\n    acl_access = get_acl(file2.name, numeric_ids=False)[\"acl_access\"]\n    assert user_entry in acl_access\n    assert group_entry in acl_access\n\n    acl_access_numeric = get_acl(file.name, numeric_ids=True)[\"acl_access\"]\n    assert user_entry_numeric in acl_access_numeric\n    assert group_entry_numeric in acl_access_numeric\n\n\ndef test_utils():\n    from ...platform.linux import acl_use_local_uid_gid\n\n    assert acl_use_local_uid_gid(b\"user:nonexistent1234:rw-:1234\") == b\"user:1234:rw-\"\n    assert acl_use_local_uid_gid(b\"group:nonexistent1234:rw-:1234\") == b\"group:1234:rw-\"\n    assert acl_use_local_uid_gid(b\"user:root:rw-:0\") == b\"user:0:rw-\"\n    assert acl_use_local_uid_gid(b\"group:root:rw-:0\") == b\"group:0:rw-\"\n\n\ndef test_numeric_to_named_with_id_simple(monkeypatch):\n    # Import here to ensure skip marker is applied before any platform-specific import side effects.\n    from ...platform.linux import _acl_from_numeric_to_named_with_id\n\n    # Pretend uid 1000 -> 'alice', gid 100 -> 'staff'\n    from ...platform import platform_ug\n\n    def _uid2user(uid, default=None):\n        if uid == 1000:\n            return \"alice\"\n        return default\n\n    def _gid2group(gid, default=None):\n        if gid == 100:\n            return \"staff\"\n        return default\n\n    monkeypatch.setattr(platform_ug, \"_uid2user\", _uid2user)\n    monkeypatch.setattr(platform_ug, \"_gid2group\", _gid2group)\n\n    src = b\"\\n\".join([b\"user::rwx\", b\"user:1000:r-x\", b\"group::r--\", b\"group:100:r--\", b\"mask::r-x\", b\"other::r--\"])\n    out = _acl_from_numeric_to_named_with_id(src)\n    lines = set(out.split(b\"\\n\"))\n    assert b\"user::rwx\" in lines\n    assert b\"user:alice:r-x:1000\" in lines\n    assert b\"group::r--\" in lines\n    assert b\"group:staff:r--:100\" in lines\n    assert b\"mask::r-x\" in lines\n    assert b\"other::r--\" in lines\n\n\ndef test_numeric_to_named_with_id_nonexistent_ids(monkeypatch):\n    from ...platform.linux import _acl_from_numeric_to_named_with_id\n\n    # Map functions return default (the given fallback), so names stay numeric but still append the fourth field\n    from ...platform import platform_ug\n\n    def _uid2user(uid, default=None):\n        return default\n\n    def _gid2group(gid, default=None):\n        return default\n\n    monkeypatch.setattr(platform_ug, \"_uid2user\", _uid2user)\n    monkeypatch.setattr(platform_ug, \"_gid2group\", _gid2group)\n\n    src = b\"user:9999:r--\\ngroup:8888:r--\\n\"\n    out = _acl_from_numeric_to_named_with_id(src)\n    lines = out.split(b\"\\n\")\n    assert lines[0] == b\"user:9999:r--:9999\"\n    assert lines[1] == b\"group:8888:r--:8888\"\n\n\ndef test_numeric_to_numeric_with_id_simple():\n    from ...platform.linux import _acl_from_numeric_to_numeric_with_id\n\n    src = b\"\\n\".join([b\"user::rwx\", b\"user:1000:r-x\", b\"group::r--\", b\"group:100:r--\", b\"mask::r-x\", b\"other::r--\"])\n    out = _acl_from_numeric_to_numeric_with_id(src)\n    lines = set(out.split(b\"\\n\"))\n    assert b\"user::rwx\" in lines\n    assert b\"user:1000:r-x:1000\" in lines\n    assert b\"group::r--\" in lines\n    assert b\"group:100:r--:100\" in lines\n    assert b\"mask::r-x\" in lines\n    assert b\"other::r--\" in lines\n"
  },
  {
    "path": "src/borg/testsuite/platform/platform_test.py",
    "content": "import errno\nimport functools\nimport os\n\nimport pytest\n\nfrom ...platformflags import is_darwin, is_freebsd, is_linux, is_win32\nfrom ...platform import acl_get, acl_set\nfrom ...platform import get_process_id, process_alive\nfrom .. import unopened_tempfile\nfrom ..fslocking_test import free_pid  # NOQA\n\n\ndef fakeroot_detected():\n    return \"FAKEROOTKEY\" in os.environ\n\n\ndef user_exists(username):\n    if not is_win32:\n        import pwd\n\n        try:\n            pwd.getpwnam(username)\n            return True\n        except (KeyError, ValueError):\n            pass\n    return False\n\n\n@functools.lru_cache\ndef are_acls_working():\n    with unopened_tempfile() as filepath:\n        open(filepath, \"w\").close()\n        try:\n            if is_darwin:\n                acl_key = \"acl_extended\"\n                acl_value = b\"!#acl 1\\nuser:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\\n\"\n            elif is_linux:\n                acl_key = \"acl_access\"\n                acl_value = b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-:9999\\ngroup:root:rw-:9999\\n\"\n            elif is_freebsd:\n                acl_key = \"acl_access\"\n                acl_value = b\"user::rw-\\ngroup::r--\\nmask::rw-\\nother::---\\nuser:root:rw-\\ngroup:wheel:rw-\\n\"\n            else:\n                return False  # ACLs unsupported on this platform.\n            write_acl = {acl_key: acl_value}\n            acl_set(filepath, write_acl)\n            read_acl = {}\n            acl_get(filepath, read_acl, os.stat(filepath))\n            acl = read_acl.get(acl_key, None)\n            if acl is not None:\n                if is_darwin:\n                    check_for = b\"root:0:allow:read\"\n                elif is_linux:\n                    check_for = b\"user::rw-\"\n                elif is_freebsd:\n                    check_for = b\"user::rw-\"\n                else:\n                    return False  # ACLs unsupported on this platform.\n                if check_for in acl:\n                    return True\n        except PermissionError:\n            pass\n        except OSError as e:\n            if e.errno not in (errno.ENOTSUP,):\n                raise\n        return False\n\n\n# define skips available to platform tests\nskipif_not_linux = pytest.mark.skipif(not is_linux, reason=\"Linux-only test\")\nskipif_not_darwin = pytest.mark.skipif(not is_darwin, reason=\"Darwin-only test\")\nskipif_not_freebsd = pytest.mark.skipif(not is_freebsd, reason=\"FreeBSD-only test\")\nskipif_not_posix = pytest.mark.skipif(not (is_linux or is_freebsd or is_darwin), reason=\"POSIX-only tests\")\nskipif_fakeroot_detected = pytest.mark.skipif(fakeroot_detected(), reason=\"not compatible with fakeroot\")\nskipif_acls_not_working = pytest.mark.skipif(not are_acls_working(), reason=\"ACLs do not work\")\nskipif_not_win32 = pytest.mark.skipif(not is_win32, reason=\"Windows-only test\")\nskipif_no_ubel_user = pytest.mark.skipif(not user_exists(\"übel\"), reason=\"requires übel user\")\n\n\ndef test_process_alive(free_pid):  # NOQA\n    id = get_process_id()\n    assert process_alive(*id)\n    host, pid, tid = id\n    assert process_alive(host + \"abc\", pid, tid)\n    assert process_alive(host, pid, tid + 1)\n    assert not process_alive(host, free_pid, tid)\n\n\ndef test_process_id():\n    hostname, pid, tid = get_process_id()\n    assert isinstance(hostname, str)\n    assert isinstance(pid, int)\n    assert isinstance(tid, int)\n    assert len(hostname) > 0\n    assert pid > 0\n    assert get_process_id() == (hostname, pid, tid)\n"
  },
  {
    "path": "src/borg/testsuite/platform/windows_test.py",
    "content": "import tempfile\n\nimport pytest\n\nfrom .platform_test import skipif_not_win32\nfrom ...platform import SyncFile\n\n# Set module-level skips\npytestmark = [skipif_not_win32]\n\n\ndef test_syncfile_basic(tmp_path):\n    \"\"\"Integration: SyncFile creates file and writes data correctly.\"\"\"\n    path = tmp_path / \"testfile\"\n    with SyncFile(path, binary=True) as sf:\n        sf.write(b\"hello borg\")\n    assert path.read_bytes() == b\"hello borg\"\n\n\ndef test_syncfile_file_exists_error(tmp_path):\n    \"\"\"SyncFile raises FileExistsError if file already exists.\"\"\"\n    path = tmp_path / \"testfile\"\n    path.touch()\n    with pytest.raises(FileExistsError):\n        SyncFile(path, binary=True)\n\n\ndef test_syncfile_text_mode(tmp_path):\n    \"\"\"SyncFile works in text mode.\"\"\"\n    path = tmp_path / \"testfile.txt\"\n    with SyncFile(path) as sf:\n        sf.write(\"hello text\")\n    assert path.read_text() == \"hello text\"\n\n\ndef test_syncfile_fd_fallback(tmp_path):\n    \"\"\"SyncFile with fd falls back to base implementation (mirrors SaveFile usage).\"\"\"\n    fd, fpath = tempfile.mkstemp(dir=tmp_path)\n    with SyncFile(fpath, fd=fd, binary=True) as sf:\n        sf.write(b\"fallback test\")\n    with open(fpath, \"rb\") as f:\n        assert f.read() == b\"fallback test\"\n\n\ndef test_syncfile_sync(tmp_path):\n    \"\"\"Explicit sync() does not raise.\"\"\"\n    path = tmp_path / \"testfile\"\n    with SyncFile(path, binary=True) as sf:\n        sf.write(b\"sync test data\")\n        sf.sync()\n\n\ndef test_syncfile_uses_write_through(tmp_path, monkeypatch):\n    \"\"\"Verify CreateFileW is called with FILE_FLAG_WRITE_THROUGH.\"\"\"\n    from borg.platform import windows\n\n    calls = []\n    original = windows._CreateFileW\n\n    def mock_create(*args):\n        calls.append(args)\n        return original(*args)\n\n    monkeypatch.setattr(windows, \"_CreateFileW\", mock_create)\n\n    path = tmp_path / \"testfile\"\n    with windows.SyncFile(path, binary=True) as sf:\n        sf.write(b\"write-through test\")\n\n    assert len(calls) == 1\n    flags_attrs = calls[0][5]  # 6th arg: dwFlagsAndAttributes\n    assert flags_attrs & windows.FILE_FLAG_WRITE_THROUGH\n"
  },
  {
    "path": "src/borg/testsuite/remote_test.py",
    "content": "import errno\nimport os\nimport io\nimport time\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom ..constants import ROBJ_FILE_STREAM\nfrom ..remote import SleepingBandwidthLimiter, RepositoryCache, cache_if_remote\nfrom ..repository import Repository\nfrom ..crypto.key import PlaintextKey\nfrom ..helpers import IntegrityError\nfrom ..repoobj import RepoObj\nfrom .hashindex_test import H\nfrom .repository_test import fchunk, pdchunk\nfrom .crypto.key_test import TestKey\n\n\nclass TestSleepingBandwidthLimiter:\n    def expect_write(self, fd, data):\n        self.expected_fd = fd\n        self.expected_data = data\n\n    def check_write(self, fd, data):\n        assert fd == self.expected_fd\n        assert data == self.expected_data\n        return len(data)\n\n    def test_write_unlimited(self, monkeypatch):\n        monkeypatch.setattr(os, \"write\", self.check_write)\n\n        it = SleepingBandwidthLimiter(0)\n        self.expect_write(5, b\"test\")\n        it.write(5, b\"test\")\n\n    def test_write(self, monkeypatch):\n        monkeypatch.setattr(os, \"write\", self.check_write)\n        monkeypatch.setattr(time, \"monotonic\", lambda: now)\n        monkeypatch.setattr(time, \"sleep\", lambda x: None)\n\n        now = 100\n\n        it = SleepingBandwidthLimiter(100)  # Bandwidth quota.\n\n        # All fits\n        self.expect_write(5, b\"test\")\n        it.write(5, b\"test\")\n\n        # Only partial write\n        self.expect_write(5, b\"123456\")\n        it.write(5, b\"1234567890\")\n\n        # Sleeps\n        self.expect_write(5, b\"123456\")\n        it.write(5, b\"123456\")\n\n        # Long time interval between writes\n        now += 10\n        self.expect_write(5, b\"1\")\n        it.write(5, b\"1\")\n\n        # Long time interval between writes, filling up the quota\n        now += 10\n        self.expect_write(5, b\"1\")\n        it.write(5, b\"1\")\n\n        # Long time interval between writes, filling up the quota to clip to the maximum\n        now += 10\n        self.expect_write(5, b\"1\")\n        it.write(5, b\"1\")\n\n\nclass TestRepositoryCache:\n    @pytest.fixture\n    def repository(self, tmpdir):\n        self.repository_location = os.path.join(str(tmpdir), \"repository\")\n        with Repository(self.repository_location, exclusive=True, create=True) as repository:\n            repository.put(H(1), fchunk(b\"1234\"))\n            repository.put(H(2), fchunk(b\"5678\"))\n            repository.put(H(3), fchunk(bytes(100)))\n            yield repository\n\n    @pytest.fixture\n    def cache(self, repository):\n        return RepositoryCache(repository)\n\n    def test_simple(self, cache: RepositoryCache):\n        # Single get()s are not cached, since they are used for unique objects like archives.\n        assert pdchunk(cache.get(H(1))) == b\"1234\"\n        assert cache.misses == 1\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)])] == [b\"1234\"]\n        assert cache.misses == 2\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)])] == [b\"1234\"]\n        assert cache.misses == 2\n        assert cache.hits == 1\n\n        assert pdchunk(cache.get(H(1))) == b\"1234\"\n        assert cache.misses == 2\n        assert cache.hits == 2\n\n    def test_meta(self, cache: RepositoryCache):\n        # Same as test_simple, but not reading the chunk data (metadata only).\n        # Single get()s are not cached, since they are used for unique objects like archives.\n        assert pdchunk(cache.get(H(1), read_data=False)) == b\"\"\n        assert cache.misses == 1\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=False)] == [b\"\"]\n        assert cache.misses == 2\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=False)] == [b\"\"]\n        assert cache.misses == 2\n        assert cache.hits == 1\n\n        assert pdchunk(cache.get(H(1), read_data=False)) == b\"\"\n        assert cache.misses == 2\n        assert cache.hits == 2\n\n    def test_mixed(self, cache: RepositoryCache):\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=False)] == [b\"\"]\n        assert cache.misses == 1\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=True)] == [b\"1234\"]\n        assert cache.misses == 2\n        assert cache.hits == 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=False)] == [b\"\"]\n        assert cache.misses == 2\n        assert cache.hits == 1\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1)], read_data=True)] == [b\"1234\"]\n        assert cache.misses == 2\n        assert cache.hits == 2\n\n    def test_backoff(self, cache: RepositoryCache):\n        def query_size_limit():\n            cache.size_limit = 0\n\n        assert [pdchunk(ch) for ch in cache.get_many([H(1), H(2)])] == [b\"1234\", b\"5678\"]\n        assert cache.misses == 2\n        assert cache.evictions == 0\n        iterator = cache.get_many([H(1), H(3), H(2)])\n        assert pdchunk(next(iterator)) == b\"1234\"\n\n        # Force cache to back off\n        qsl = cache.query_size_limit\n        cache.query_size_limit = query_size_limit  # type: ignore[assignment]\n        cache.backoff()\n        cache.query_size_limit = qsl  # type: ignore[assignment]\n        # Evicted H(1) and H(2)\n        assert cache.evictions == 2\n        assert H(1) not in cache.cache\n        assert H(2) not in cache.cache\n        assert pdchunk(next(iterator)) == bytes(100)\n        assert cache.slow_misses == 0\n        # Since H(2) was in the cache when we called get_many(), but has\n        # been evicted during iterating the generator, it will be a slow miss.\n        assert pdchunk(next(iterator)) == b\"5678\"\n        assert cache.slow_misses == 1\n\n    def test_enospc(self, cache: RepositoryCache):\n        class enospc_open:\n            def __init__(self, *args):\n                pass\n\n            def __enter__(self):\n                return self\n\n            def __exit__(self, exc_type, exc_val, exc_tb):\n                pass\n\n            def write(self, data):\n                raise OSError(errno.ENOSPC, \"foo\")\n\n            def truncate(self, n=None):\n                pass\n\n        iterator = cache.get_many([H(1), H(2), H(3)])\n        assert pdchunk(next(iterator)) == b\"1234\"\n\n        with patch(\"builtins.open\", enospc_open):\n            assert pdchunk(next(iterator)) == b\"5678\"\n            assert cache.enospc == 1\n            # We didn't patch query_size_limit, which would set size_limit to a low\n            # value, so nothing was actually evicted.\n            assert cache.evictions == 0\n\n        assert pdchunk(next(iterator)) == bytes(100)\n\n    @pytest.fixture\n    def key(self, repository, monkeypatch):\n        monkeypatch.setenv(\"BORG_PASSPHRASE\", \"test\")\n        key = PlaintextKey.create(repository, TestKey.MockArgs())\n        return key\n\n    @pytest.fixture\n    def repo_objs(self, key):\n        return RepoObj(key)\n\n    def _put_encrypted_object(self, repo_objs, repository, data):\n        id_ = repo_objs.id_hash(data)\n        repository.put(id_, repo_objs.format(id_, {}, data, ro_type=ROBJ_FILE_STREAM))\n        return id_\n\n    @pytest.fixture\n    def H1(self, repo_objs, repository):\n        return self._put_encrypted_object(repo_objs, repository, b\"1234\")\n\n    @pytest.fixture\n    def H2(self, repo_objs, repository):\n        return self._put_encrypted_object(repo_objs, repository, b\"5678\")\n\n    @pytest.fixture\n    def H3(self, repo_objs, repository):\n        return self._put_encrypted_object(repo_objs, repository, bytes(100))\n\n    @pytest.fixture\n    def decrypted_cache(self, repo_objs, repository):\n        return cache_if_remote(repository, decrypted_cache=repo_objs, force_cache=True)\n\n    def test_cache_corruption(self, decrypted_cache: RepositoryCache, H1, H2, H3):\n        list(decrypted_cache.get_many([H1, H2, H3]))\n\n        iterator = decrypted_cache.get_many([H1, H2, H3])\n        assert next(iterator) == (4, b\"1234\")\n\n        pkey = decrypted_cache.prefixed_key(H2, complete=True)\n        with open(decrypted_cache.key_filename(pkey), \"a+b\") as fd:\n            fd.seek(-1, io.SEEK_END)\n            corrupted = (int.from_bytes(fd.read(), \"little\") ^ 2).to_bytes(1, \"little\")\n            fd.seek(-1, io.SEEK_END)\n            fd.write(corrupted)\n            fd.truncate()\n\n        with pytest.raises(IntegrityError):\n            assert next(iterator) == (4, b\"5678\")\n"
  },
  {
    "path": "src/borg/testsuite/repoobj_test.py",
    "content": "import pytest\n\nfrom ..constants import ROBJ_FILE_STREAM, ROBJ_MANIFEST, ROBJ_ARCHIVE_META\nfrom ..crypto.key import PlaintextKey\nfrom ..helpers.errors import IntegrityError\nfrom ..repository import Repository\nfrom ..repoobj import RepoObj, RepoObj1\nfrom ..compress import LZ4\n\n\n@pytest.fixture\ndef repository(tmpdir):\n    return Repository(tmpdir, create=True)\n\n\n@pytest.fixture\ndef key(repository):\n    return PlaintextKey(repository)\n\n\ndef test_format_parse_roundtrip(key):\n    repo_objs = RepoObj(key)\n    data = b\"foobar\" * 10\n    id = repo_objs.id_hash(data)\n    meta = {\"custom\": \"something\"}  # size and csize are computed automatically\n    cdata = repo_objs.format(id, meta, data, ro_type=ROBJ_FILE_STREAM)\n\n    got_meta = repo_objs.parse_meta(id, cdata, ro_type=ROBJ_FILE_STREAM)\n    assert got_meta[\"size\"] == len(data)\n    assert got_meta[\"csize\"] < len(data)\n    assert got_meta[\"custom\"] == \"something\"\n\n    got_meta, got_data = repo_objs.parse(id, cdata, ro_type=ROBJ_FILE_STREAM)\n    assert got_meta[\"size\"] == len(data)\n    assert got_meta[\"csize\"] < len(data)\n    assert got_meta[\"custom\"] == \"something\"\n    assert data == got_data\n\n    edata = repo_objs.extract_crypted_data(cdata)\n    key = repo_objs.key\n    assert edata.startswith(bytes((key.TYPE,)))\n\n\ndef test_format_parse_roundtrip_borg1(key):  # legacy\n    repo_objs = RepoObj1(key)\n    data = b\"foobar\" * 10\n    id = repo_objs.id_hash(data)\n    meta = {}  # borg1 does not support this kind of metadata\n    cdata = repo_objs.format(id, meta, data, ro_type=ROBJ_FILE_STREAM)\n\n    # Borg 1 does not support separate metadata, and Borg 2 does not invoke parse_meta for Borg 1 repositories.\n\n    got_meta, got_data = repo_objs.parse(id, cdata, ro_type=ROBJ_FILE_STREAM)\n    assert got_meta[\"size\"] == len(data)\n    assert got_meta[\"csize\"] < len(data)\n    assert data == got_data\n\n    edata = repo_objs.extract_crypted_data(cdata)\n    compressor = repo_objs.compressor\n    key = repo_objs.key\n    assert edata.startswith(bytes((key.TYPE, compressor.ID, compressor.level)))\n\n\ndef test_borg1_borg2_transition(key):\n    # Borg transfer reads Borg 1.x repository objects (without decompressing them),\n    # and writes Borg 2 repository objects (providing already-compressed data to avoid recompression).\n    meta = {}  # borg1 does not support this kind of metadata\n    data = b\"foobar\" * 10\n    len_data = len(data)\n    repo_objs1 = RepoObj1(key)\n    id = repo_objs1.id_hash(data)\n    borg1_cdata = repo_objs1.format(id, meta, data, ro_type=ROBJ_FILE_STREAM)\n    meta1, compr_data1 = repo_objs1.parse(\n        id, borg1_cdata, decompress=True, want_compressed=True, ro_type=ROBJ_FILE_STREAM\n    )  # avoid re-compression\n    # In Borg 1, we can only get this metadata after decrypting the whole chunk (and we do not have \"size\" here):\n    assert meta1[\"ctype\"] == LZ4.ID  # Default compression.\n    assert meta1[\"clevel\"] == 0xFF  # LZ4 does not support levels (yet?).\n    assert meta1[\"csize\"] < len_data  # LZ4 should make it smaller.\n\n    repo_objs2 = RepoObj(key)\n    # Note: As we did not decompress, we do not have \"size\" and need to get it from somewhere else.\n    # Here, we just use len_data. For Borg transfer, we also know the size from another metadata source.\n    borg2_cdata = repo_objs2.format(\n        id,\n        dict(meta1),\n        compr_data1[2:],\n        compress=False,\n        size=len_data,\n        ctype=meta1[\"ctype\"],\n        clevel=meta1[\"clevel\"],\n        ro_type=ROBJ_FILE_STREAM,\n    )\n    meta2, data2 = repo_objs2.parse(id, borg2_cdata, ro_type=ROBJ_FILE_STREAM)\n    assert data2 == data\n    assert meta2[\"ctype\"] == LZ4.ID\n    assert meta2[\"clevel\"] == 0xFF\n    assert meta2[\"csize\"] == meta1[\"csize\"] - 2  # Borg 2 does not store the type/level bytes there.\n    assert meta2[\"size\"] == len_data\n\n    meta2 = repo_objs2.parse_meta(id, borg2_cdata, ro_type=ROBJ_FILE_STREAM)\n    # Now, in Borg 2, we have nice and separately decrypted metadata (no need to decrypt the whole chunk).\n    assert meta2[\"ctype\"] == LZ4.ID\n    assert meta2[\"clevel\"] == 0xFF\n    assert meta2[\"csize\"] == meta1[\"csize\"] - 2  # Borg 2 does not store the type/level bytes there.\n    assert meta2[\"size\"] == len_data\n\n\ndef test_spoof_manifest(key):\n    repo_objs = RepoObj(key)\n    data = b\"fake or malicious manifest data\"  # File content could be provided by an attacker.\n    id = repo_objs.id_hash(data)\n    # Create a repository object containing user data (file content data).\n    cdata = repo_objs.format(id, {}, data, ro_type=ROBJ_FILE_STREAM)\n    # Let's assume an attacker managed to replace the manifest with that repository object.\n    # As Borg always gives the ro_type it intends to read, this should fail:\n    with pytest.raises(IntegrityError):\n        repo_objs.parse(id, cdata, ro_type=ROBJ_MANIFEST)\n\n\ndef test_spoof_archive(key):\n    repo_objs = RepoObj(key)\n    data = b\"fake or malicious archive data\"  # File content could be provided by an attacker.\n    id = repo_objs.id_hash(data)\n    # Create a repository object containing user data (file content data).\n    cdata = repo_objs.format(id, {}, data, ro_type=ROBJ_FILE_STREAM)\n    # Let's assume an attacker managed to replace an archive with that repository object.\n    # As Borg always gives the ro_type it intends to read, this should fail:\n    with pytest.raises(IntegrityError):\n        repo_objs.parse(id, cdata, ro_type=ROBJ_ARCHIVE_META)\n"
  },
  {
    "path": "src/borg/testsuite/repository_test.py",
    "content": "import logging\nimport os\nimport sys\n\nimport pytest\nfrom xxhash import xxh64\n\nfrom ..helpers import Location\nfrom ..helpers import IntegrityError\nfrom ..platformflags import is_win32\nfrom ..remote import RemoteRepository, InvalidRPCMethod, PathNotAllowed\nfrom ..repository import Repository, StoreObjectNotFound, MAX_DATA_SIZE\nfrom ..repoobj import RepoObj\nfrom .hashindex_test import H\n\n\n@pytest.fixture()\ndef repository(tmp_path):\n    repository_location = os.fspath(tmp_path / \"repository\")\n    yield Repository(repository_location, exclusive=True, create=True)\n\n\n@pytest.fixture()\ndef remote_repository(tmp_path):\n    if is_win32:\n        pytest.skip(\"Remote repository does not yet work on Windows.\")\n    repository_location = Location(\"ssh://__testsuite__/\" + os.fspath(tmp_path / \"repository\"))\n    yield RemoteRepository(repository_location, exclusive=True, create=True)\n\n\ndef pytest_generate_tests(metafunc):\n    # Generate tests that run on both local and remote repositories.\n    if \"repo_fixtures\" in metafunc.fixturenames:\n        metafunc.parametrize(\"repo_fixtures\", [\"repository\", \"remote_repository\"])\n\n\ndef get_repository_from_fixture(repo_fixtures, request):\n    # Return the repository object from the fixture for tests that run on both local and remote repositories.\n    return request.getfixturevalue(repo_fixtures)\n\n\ndef reopen(repository, exclusive: bool | None = True, create=False):\n    if isinstance(repository, Repository):\n        if repository.opened:\n            raise RuntimeError(\"Repo must be closed before a reopen. Cannot support nested repository contexts.\")\n        return Repository(repository._location, exclusive=exclusive, create=create)\n\n    if isinstance(repository, RemoteRepository):\n        if repository.p is not None or repository.sock is not None:\n            raise RuntimeError(\"Remote repo must be closed before a reopen. Cannot support nested repository contexts.\")\n        return RemoteRepository(repository.location, exclusive=exclusive, create=create)\n\n    raise TypeError(\n        f\"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'.\"\n    )\n\n\ndef fchunk(data, meta=b\"\"):\n    # Format chunk: create a raw chunk that has a valid RepoObj layout, but does not use encryption or compression.\n    hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta).digest(), xxh64(data).digest())\n    assert isinstance(data, bytes)\n    chunk = hdr + meta + data\n    return chunk\n\n\ndef pchunk(chunk):\n    # Parse chunk: extract data and metadata from a raw chunk made by fchunk.\n    hdr_size = RepoObj.obj_header.size\n    hdr = chunk[:hdr_size]\n    meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2]\n    meta = chunk[hdr_size : hdr_size + meta_size]\n    data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size]\n    return data, meta\n\n\ndef pdchunk(chunk):\n    # Parse only the data from a raw chunk made by fchunk.\n    return pchunk(chunk)[0]\n\n\ndef test_basic_operations(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        for x in range(100):\n            repository.put(H(x), fchunk(b\"SOMEDATA\"))\n        key50 = H(50)\n        assert pdchunk(repository.get(key50)) == b\"SOMEDATA\"\n        repository.delete(key50)\n        with pytest.raises(Repository.ObjectNotFound):\n            repository.get(key50)\n    with reopen(repository) as repository:\n        with pytest.raises(Repository.ObjectNotFound):\n            repository.get(key50)\n        for x in range(100):\n            if x == 50:\n                continue\n            assert pdchunk(repository.get(H(x))) == b\"SOMEDATA\"\n\n\ndef test_read_data(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        meta, data = b\"meta\", b\"data\"\n        hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta).digest(), xxh64(data).digest())\n        chunk_complete = hdr + meta + data\n        chunk_short = hdr + meta\n        repository.put(H(0), chunk_complete)\n        assert repository.get(H(0)) == chunk_complete\n        assert repository.get(H(0), read_data=True) == chunk_complete\n        assert repository.get(H(0), read_data=False) == chunk_short\n\n\ndef test_consistency(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        repository.put(H(0), fchunk(b\"foo\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo\"\n        repository.put(H(0), fchunk(b\"foo2\"))\n        assert pdchunk(repository.get(H(0))) == b\"foo2\"\n        repository.put(H(0), fchunk(b\"bar\"))\n        assert pdchunk(repository.get(H(0))) == b\"bar\"\n        repository.delete(H(0))\n        with pytest.raises(Repository.ObjectNotFound):\n            repository.get(H(0))\n\n\ndef test_list(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        for x in range(100):\n            repository.put(H(x), fchunk(b\"SOMEDATA\"))\n        repo_list = repository.list()\n        assert len(repo_list) == 100\n        first_half = repository.list(limit=50)\n        assert len(first_half) == 50\n        assert first_half == repo_list[:50]\n        second_half = repository.list(marker=first_half[-1][0])\n        assert len(second_half) == 50\n        assert second_half == repo_list[50:]\n        assert len(repository.list(limit=50)) == 50\n\n\ndef test_max_data_size(repo_fixtures, request):\n    with get_repository_from_fixture(repo_fixtures, request) as repository:\n        max_data = b\"x\" * (MAX_DATA_SIZE - RepoObj.obj_header.size)\n        repository.put(H(0), fchunk(max_data))\n        assert pdchunk(repository.get(H(0))) == max_data\n        with pytest.raises(IntegrityError):\n            repository.put(H(1), fchunk(max_data + b\"x\"))\n        repository.delete(H(0))\n\n\ndef check(repository, repo_path, repair=False, status=True):\n    assert repository.check(repair=repair) == status\n    # Make sure no tmp files are left behind\n    tmp_files = [name for name in os.listdir(repo_path) if \"tmp\" in name]\n    assert tmp_files == [], \"Found tmp files\"\n\n\ndef _get_mock_args():\n    class MockArgs:\n        remote_path = \"borg\"\n        umask = 0o077\n        debug_topics = []\n        rsh = None\n\n        def __contains__(self, item):\n            # to behave like argparse.Namespace\n            return hasattr(self, item)\n\n    return MockArgs()\n\n\ndef test_remote_invalid_rpc(remote_repository):\n    with remote_repository:\n        with pytest.raises(InvalidRPCMethod):\n            remote_repository.call(\"__init__\", {})\n\n\ndef test_remote_rpc_exception_transport(remote_repository):\n    with remote_repository:\n        s1 = \"test string\"\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"DoesNotExist\"})\n        except Repository.DoesNotExist as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"AlreadyExists\"})\n        except Repository.AlreadyExists as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"CheckNeeded\"})\n        except Repository.CheckNeeded as e:\n            assert len(e.args) == 1\n            assert e.args[0] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"IntegrityError\"})\n        except IntegrityError as e:\n            assert len(e.args) == 1\n            assert e.args[0] == s1\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"PathNotAllowed\"})\n        except PathNotAllowed as e:\n            assert len(e.args) == 1\n            assert e.args[0] == \"foo\"\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"ObjectNotFound\"})\n        except Repository.ObjectNotFound as e:\n            assert len(e.args) == 2\n            assert e.args[0] == s1\n            assert e.args[1] == remote_repository.location.processed\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"StoreObjectNotFound\"})\n        except StoreObjectNotFound as e:\n            assert len(e.args) == 1\n            assert e.args[0] == s1\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"InvalidRPCMethod\"})\n        except InvalidRPCMethod as e:\n            assert len(e.args) == 1\n            assert e.args[0] == s1\n\n        try:\n            remote_repository.call(\"inject_exception\", {\"kind\": \"divide\"})\n        except RemoteRepository.RPCError as e:\n            assert e.unpacked\n            assert e.get_message().startswith(\"ZeroDivisionError:\")\n            assert e.exception_class == \"ZeroDivisionError\"\n            assert len(e.exception_full) > 0\n\n\ndef test_remote_ssh_cmd(remote_repository):\n    with remote_repository:\n        args = _get_mock_args()\n        remote_repository._args = args\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"example.com\"]\n        assert remote_repository.ssh_cmd(Location(\"ssh://user@example.com/foo\")) == [\"ssh\", \"user@example.com\"]\n        assert remote_repository.ssh_cmd(Location(\"ssh://user@example.com:1234/foo\")) == [\n            \"ssh\",\n            \"-p\",\n            \"1234\",\n            \"user@example.com\",\n        ]\n        os.environ[\"BORG_RSH\"] = \"ssh --foo\"\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"--foo\", \"example.com\"]\n\n\ndef test_remote_borg_cmd(remote_repository):\n    with remote_repository:\n        assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, \"-m\", \"borg\", \"serve\"]\n        args = _get_mock_args()\n        # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown:\n        logging.getLogger().setLevel(logging.INFO)\n        # note: test logger is on info log level, so --info gets added automagically\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg\", \"serve\", \"--info\"]\n        args.remote_path = \"borg-0.28.2\"\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg-0.28.2\", \"serve\", \"--info\"]\n        args.debug_topics = [\"something_client_side\", \"repository_compaction\"]\n        assert remote_repository.borg_cmd(args, testing=False) == [\n            \"borg-0.28.2\",\n            \"serve\",\n            \"--info\",\n            \"--debug-topic=borg.debug.repository_compaction\",\n        ]\n        args = _get_mock_args()\n        assert remote_repository.borg_cmd(args, testing=False) == [\"borg\", \"serve\", \"--info\"]\n        args.rsh = \"ssh -i foo\"\n        remote_repository._args = args\n        assert remote_repository.ssh_cmd(Location(\"ssh://example.com/foo\")) == [\"ssh\", \"-i\", \"foo\", \"example.com\"]\n"
  },
  {
    "path": "src/borg/testsuite/shell_completions_test.py",
    "content": "import subprocess\nfrom pathlib import Path\n\nimport pytest\n\nSHELL_COMPLETIONS_DIR = Path(__file__).parent / \"..\" / \"..\" / \"..\" / \"scripts\" / \"shell_completions\"\n\n\ndef test_fish_completion_is_valid():\n    \"\"\"Test that the Fish completion file is valid Fish syntax.\"\"\"\n    fish_completion_file = SHELL_COMPLETIONS_DIR / \"fish\" / \"borg.fish\"\n    assert fish_completion_file.is_file()\n\n    # Check if Fish is available\n    try:\n        subprocess.run([\"fish\", \"--version\"], capture_output=True, check=True)\n    except (subprocess.SubprocessError, FileNotFoundError):\n        pytest.skip(\"Fish not available\")\n\n    # Test whether the Fish completion file can be sourced without errors\n    result = subprocess.run([\"fish\", \"-c\", f\"source {str(fish_completion_file)}\"], capture_output=True)\n    assert result.returncode == 0, f\"Fish completion file has syntax errors: {result.stderr.decode()}\"\n"
  },
  {
    "path": "src/borg/testsuite/storelocking_test.py",
    "content": "import time\nfrom pathlib import Path\n\nimport pytest\n\nfrom borgstore.store import Store\n\nfrom ..storelocking import Lock, NotLocked, LockTimeout\n\nID1 = \"foo\", 1, 1\nID2 = \"bar\", 2, 2\n\n\n@pytest.fixture()\ndef lockstore(tmp_path):\n    store = Store(Path(tmp_path / \"lockstore\").as_uri(), levels={\"locks/\": [0]})\n    store.create()\n    with store:\n        yield store\n    store.destroy()\n\n\nclass TestLock:\n    def test_cm(self, lockstore):\n        with Lock(lockstore, exclusive=True, id=ID1) as lock:\n            assert lock.got_exclusive_lock()\n        with Lock(lockstore, exclusive=False, id=ID1) as lock:\n            assert not lock.got_exclusive_lock()\n\n    def test_got_exclusive_lock(self, lockstore):\n        lock = Lock(lockstore, exclusive=True, id=ID1)\n        assert not lock.got_exclusive_lock()\n        lock.acquire()\n        assert lock.got_exclusive_lock()\n        lock.release()\n        assert not lock.got_exclusive_lock()\n\n    def test_exclusive_lock(self, lockstore):\n        # There must not be two exclusive locks.\n        with Lock(lockstore, exclusive=True, id=ID1):\n            with pytest.raises(LockTimeout):\n                Lock(lockstore, exclusive=True, id=ID2).acquire()\n        # Acquiring an exclusive lock will time out if the non-exclusive lock does not go away.\n        with Lock(lockstore, exclusive=False, id=ID1):\n            with pytest.raises(LockTimeout):\n                Lock(lockstore, exclusive=True, id=ID2).acquire()\n\n    def test_double_nonexclusive_lock_succeeds(self, lockstore):\n        with Lock(lockstore, exclusive=False, id=ID1):\n            with Lock(lockstore, exclusive=False, id=ID2):\n                pass\n\n    def test_not_locked(self, lockstore):\n        lock = Lock(lockstore, exclusive=True, id=ID1)\n        with pytest.raises(NotLocked):\n            lock.release()\n        lock = Lock(lockstore, exclusive=False, id=ID1)\n        with pytest.raises(NotLocked):\n            lock.release()\n\n    def test_break_lock(self, lockstore):\n        lock = Lock(lockstore, exclusive=True, id=ID1).acquire()\n        lock.break_lock()\n        with Lock(lockstore, exclusive=True, id=ID2):\n            pass\n        with Lock(lockstore, exclusive=True, id=ID1):\n            pass\n\n    def test_lock_refresh_stale_removal(self, lockstore):\n        # stale after 2s, refreshable after 1s\n        lock = Lock(lockstore, exclusive=True, id=ID1, stale=2)\n        lock.acquire()\n        lock_keys_a00 = set(lock._get_locks())\n        time.sleep(0.5)\n        lock.refresh()  # Should not change locks; existing lock is too young.\n        lock_keys_a05 = set(lock._get_locks())\n        time.sleep(0.6)\n        lock.refresh()  # This should refresh the lock.\n        lock_keys_b00 = set(lock._get_locks())\n        time.sleep(2.1)\n        lock_keys_b21 = set(lock._get_locks())  # now the lock should be stale & gone.\n        assert lock_keys_a00 == lock_keys_a05  # was too young, no refresh done\n        assert len(lock_keys_a00) == 1\n        assert lock_keys_a00 != lock_keys_b00  # refresh done, new lock has different key\n        assert len(lock_keys_b00) == 1\n        assert len(lock_keys_b21) == 0  # stale lock was ignored\n        assert len(list(lock.store.list(\"locks\"))) == 0  # stale lock was removed from store\n\n    def test_migrate_lock(self, lockstore):\n        old_id, new_id = ID1, ID2\n        assert old_id[1] != new_id[1]  # different PIDs (like when doing daemonize())\n        lock = Lock(lockstore, id=old_id).acquire()\n        old_locks = lock._find_locks(only_mine=True)\n        assert lock.id == old_id  # lock is for old id / PID\n        lock.migrate_lock(old_id, new_id)  # fix the lock\n        assert lock.id == new_id  # lock corresponds to the new id / PID\n        new_locks = lock._find_locks(only_mine=True)\n        assert old_locks != new_locks\n        assert len(old_locks) == len(new_locks) == 1\n        assert old_locks[0][\"hostid\"] == old_id[0]\n        assert new_locks[0][\"hostid\"] == new_id[0]\n"
  },
  {
    "path": "src/borg/testsuite/version_test.py",
    "content": "import pytest\n\nfrom ..version import parse_version, format_version\n\n\n@pytest.mark.parametrize(\n    \"version_str, version_tuple\",\n    [\n        # setuptools < 8.0 uses \"-\"\n        (\"1.0.0a1.dev204-g8866961.d20170606\", (1, 0, 0, -4, 1)),\n        (\"1.0.0a1.dev204-g8866961\", (1, 0, 0, -4, 1)),\n        (\"1.0.0-d20170606\", (1, 0, 0, -1)),\n        # setuptools >= 8.0 uses \"+\"\n        (\"1.0.0a1.dev204+g8866961.d20170606\", (1, 0, 0, -4, 1)),\n        (\"1.0.0a1.dev204+g8866961\", (1, 0, 0, -4, 1)),\n        (\"1.0.0+d20170606\", (1, 0, 0, -1)),\n        # Pre-release versions:\n        (\"1.0.0a1\", (1, 0, 0, -4, 1)),\n        (\"1.0.0a2\", (1, 0, 0, -4, 2)),\n        (\"1.0.0b3\", (1, 0, 0, -3, 3)),\n        (\"1.0.0rc4\", (1, 0, 0, -2, 4)),\n        # Release versions:\n        (\"0.0.0\", (0, 0, 0, -1)),\n        (\"0.0.11\", (0, 0, 11, -1)),\n        (\"0.11.0\", (0, 11, 0, -1)),\n        (\"11.0.0\", (11, 0, 0, -1)),\n    ],\n)\ndef test_parse_version(version_str, version_tuple):\n    assert parse_version(version_str) == version_tuple\n\n\n@pytest.mark.parametrize(\"invalid_version\", [\"\", \"1\", \"1.2\", \"crap\"])\ndef test_parse_version_invalid(invalid_version):\n    with pytest.raises(ValueError):\n        assert parse_version(invalid_version)  # We require x.y.z versions\n\n\n@pytest.mark.parametrize(\n    \"version_str, version_tuple\",\n    [\n        (\"1.0.0a1\", (1, 0, 0, -4, 1)),\n        (\"1.0.0\", (1, 0, 0, -1)),\n        (\"1.0.0a2\", (1, 0, 0, -4, 2)),\n        (\"1.0.0b3\", (1, 0, 0, -3, 3)),\n        (\"1.0.0rc4\", (1, 0, 0, -2, 4)),\n        (\"0.0.0\", (0, 0, 0, -1)),\n        (\"0.0.11\", (0, 0, 11, -1)),\n        (\"0.11.0\", (0, 11, 0, -1)),\n        (\"11.0.0\", (11, 0, 0, -1)),\n    ],\n)\ndef test_format_version(version_str, version_tuple):\n    assert format_version(version_tuple) == version_str\n"
  },
  {
    "path": "src/borg/testsuite/xattr_test.py",
    "content": "import os\n\nimport pytest\n\nfrom ..platform.xattr import buffer, split_lstring\nfrom ..xattr import is_enabled, getxattr, setxattr, listxattr, XATTR_FAKEROOT\nfrom ..platformflags import is_linux\n\n\n@pytest.fixture()\ndef tempfile_symlink(tmp_path):\n    if not is_enabled(tmp_path):\n        pytest.skip(\"xattrs not enabled on the filesystem\")\n    with open(os.fspath(tmp_path / \"xattr\"), \"w\") as temp_file:\n        symlink = temp_file.name + \".symlink\"\n        os.symlink(temp_file.name, symlink)\n        yield temp_file, symlink\n\n\ndef assert_equal_se(is_x, want_x):\n    # Check two xattr lists for equality, but ignore the security.selinux attribute.\n    is_x = set(is_x) - {b\"security.selinux\", b\"com.apple.provenance\"}\n    want_x = set(want_x)\n    assert is_x == want_x\n\n\ndef test(tempfile_symlink):\n    temp_file, symlink = tempfile_symlink\n    tmp_fn = os.fsencode(temp_file.name)\n    tmp_lfn = os.fsencode(symlink)\n    tmp_fd = temp_file.fileno()\n    assert_equal_se(listxattr(tmp_fn), [])\n    assert_equal_se(listxattr(tmp_fd), [])\n    assert_equal_se(listxattr(tmp_lfn), [])\n    setxattr(tmp_fn, b\"user.foo\", b\"bar\")\n    setxattr(tmp_fd, b\"user.bar\", b\"foo\")\n    setxattr(tmp_fn, b\"user.empty\", b\"\")\n    if not is_linux:\n        # Linux does not allow setting user.* xattrs on symlinks.\n        setxattr(tmp_lfn, b\"user.linkxattr\", b\"baz\")\n    assert_equal_se(listxattr(tmp_fn), [b\"user.foo\", b\"user.bar\", b\"user.empty\"])\n    assert_equal_se(listxattr(tmp_fd), [b\"user.foo\", b\"user.bar\", b\"user.empty\"])\n    assert_equal_se(listxattr(tmp_lfn, follow_symlinks=True), [b\"user.foo\", b\"user.bar\", b\"user.empty\"])\n    if not is_linux:\n        assert_equal_se(listxattr(tmp_lfn), [b\"user.linkxattr\"])\n    assert getxattr(tmp_fn, b\"user.foo\") == b\"bar\"\n    assert getxattr(tmp_fd, b\"user.foo\") == b\"bar\"\n    assert getxattr(tmp_lfn, b\"user.foo\", follow_symlinks=True) == b\"bar\"\n    if not is_linux:\n        assert getxattr(tmp_lfn, b\"user.linkxattr\") == b\"baz\"\n    assert getxattr(tmp_fn, b\"user.empty\") == b\"\"\n\n\ndef test_listxattr_buffer_growth(tempfile_symlink):\n    temp_file, symlink = tempfile_symlink\n    tmp_fn = os.fsencode(temp_file.name)\n    # Make it work even with ext4, which imposes relatively low limits.\n    buffer.resize(size=64, init=True)\n    # The raw xattr key list will be > 64.\n    keys = [b\"user.attr%d\" % i for i in range(20)]\n    for key in keys:\n        setxattr(tmp_fn, key, b\"x\")\n    got_keys = listxattr(tmp_fn)\n    assert_equal_se(got_keys, keys)\n    assert len(buffer) > 64\n\n\ndef test_getxattr_buffer_growth(tempfile_symlink):\n    temp_file, symlink = tempfile_symlink\n    tmp_fn = os.fsencode(temp_file.name)\n    # Make it work even with ext4, which imposes relatively low limits.\n    buffer.resize(size=64, init=True)\n    value = b\"x\" * 126\n    setxattr(tmp_fn, b\"user.big\", value)\n    got_value = getxattr(tmp_fn, b\"user.big\")\n    assert value == got_value\n    assert len(buffer) == 128\n\n\n@pytest.mark.parametrize(\n    \"lstring, expected\", [(b\"\", []), (b\"\\x00\", [b\"\"]), (b\"\\x01a\", [b\"a\"]), (b\"\\x01a\\x02cd\", [b\"a\", b\"cd\"])]\n)\ndef test_split_lstring(lstring, expected):\n    assert split_lstring(lstring) == expected\n\n\ndef test_xattr_fakeroot_flag():\n    \"\"\"XATTR_FAKEROOT must be False when not on Linux or when fakeroot is not active.\"\"\"\n    if not is_linux:\n        assert XATTR_FAKEROOT is False\n    if \"FAKEROOTKEY\" not in os.environ:\n        assert XATTR_FAKEROOT is False\n"
  },
  {
    "path": "src/borg/upgrade.py",
    "content": "from struct import Struct\nfrom types import NoneType\n\nfrom .constants import REQUIRED_ITEM_KEYS, CH_BUZHASH\nfrom .compress import ZLIB, ZLIB_legacy, ObfuscateSize\nfrom .helpers import HardLinkManager, join_cmd\nfrom .item import Item\nfrom .logger import create_logger\n\nlogger = create_logger(__name__)\n\n\nclass UpgraderNoOp:\n    def __init__(self, *, cache, args):\n        self.args = args\n\n    def new_archive(self, *, archive):\n        pass\n\n    def upgrade_item(self, *, item):\n        return item\n\n    def upgrade_compressed_chunk(self, meta, data):\n        return meta, data\n\n    def upgrade_archive_metadata(self, *, metadata):\n        new_metadata = {}\n        # keep all metadata except archive version and stats.\n        for attr in (\n            \"command_line\",\n            \"hostname\",\n            \"username\",\n            \"time\",\n            \"start\",\n            \"end\",\n            \"comment\",\n            \"chunker_params\",\n            \"recreate_command_line\",\n        ):\n            if hasattr(metadata, attr):\n                new_metadata[attr] = getattr(metadata, attr)\n        new_metadata[\"cwd\"] = getattr(metadata, \"cwd\", None)  # None signals save() to leave cwd unset\n        rechunking = self.args.chunker_params is not None\n        if rechunking:\n            # if we are rechunking while transferring, we take the new chunker_params.\n            new_metadata[\"chunker_params\"] = self.args.chunker_params\n        return new_metadata\n\n\nclass UpgraderFrom12To20:\n    borg1_header_fmt = Struct(\">I\")\n\n    def __init__(self, *, cache, args):\n        self.cache = cache\n        self.args = args\n\n    def new_archive(self, *, archive):\n        self.archive = archive\n        # hlid -> chunks_correct list (or None, for contentless hardlinks)\n        self.hlm = HardLinkManager(id_type=bytes, info_type=(list, NoneType))\n\n    def upgrade_item(self, *, item):\n        \"\"\"Upgrades the item as needed and removes legacy data.\"\"\"\n        ITEM_KEY_WHITELIST = {\n            \"path\",\n            \"rdev\",\n            \"chunks\",\n            \"hlid\",\n            \"mode\",\n            \"user\",\n            \"group\",\n            \"uid\",\n            \"gid\",\n            \"mtime\",\n            \"atime\",\n            \"ctime\",\n            \"birthtime\",\n            \"size\",\n            \"xattrs\",\n            \"bsdflags\",\n            \"acl_nfs4\",\n            \"acl_access\",\n            \"acl_default\",\n            \"acl_extended\",\n        }\n\n        if self.hlm.borg1_hardlink_master(item):\n            item.hlid = hlid = self.hlm.hardlink_id_from_path(item.path)\n            self.hlm.remember(id=hlid, info=item.get(\"chunks\"))\n        elif self.hlm.borg1_hardlink_slave(item):\n            item.hlid = hlid = self.hlm.hardlink_id_from_path(item.source)\n            chunks = self.hlm.retrieve(id=hlid)\n            if chunks is not None:\n                item.chunks = chunks\n                for chunk_id, chunk_size in chunks:\n                    self.cache.reuse_chunk(chunk_id, chunk_size, self.archive.stats)\n            del item.source  # not used for hard links anymore, replaced by hlid\n        # make sure we only have desired stuff in the new item. specifically, make sure to get rid of:\n        # - 'acl' remnants of bug in attic <= 0.13\n        # - 'hardlink_master' (superseded by hlid)\n        item_dict = item.as_dict()\n        new_item_dict = {key: value for key, value in item_dict.items() if key in ITEM_KEY_WHITELIST}\n        # symlink targets were .source for borg1, but borg2 uses .target:\n        if \"source\" in item_dict:\n            new_item_dict[\"target\"] = item_dict[\"source\"]\n        assert \"source\" not in new_item_dict\n        # remove some pointless entries older borg put in there:\n        for key in \"user\", \"group\":\n            if key in new_item_dict and new_item_dict[key] is None:\n                del new_item_dict[key]\n        assert not any(value is None for value in new_item_dict.values()), f\"found None value in {new_item_dict}\"\n        new_item = Item(internal_dict=new_item_dict)\n        new_item.get_size(memorize=True)  # if not already present: compute+remember size for items with chunks\n        assert all(key in new_item for key in REQUIRED_ITEM_KEYS)\n        return new_item\n\n    def upgrade_compressed_chunk(self, meta, data):\n        # meta/data was parsed via RepoObj1.parse, which returns data **including** the ctype/clevel bytes prefixed\n        def upgrade_zlib_and_level(meta, data):\n            if ZLIB_legacy.detect(data):\n                ctype = ZLIB.ID\n                data = bytes(data)  # ZLIB_legacy has no ctype/clevel prefix\n            else:\n                ctype = data[0]\n                data = bytes(data[2:])  # strip ctype/clevel bytes\n            meta[\"ctype\"] = ctype\n            meta[\"clevel\"] = level\n            meta[\"csize\"] = len(data)  # we may have stripped some prefixed ctype/clevel bytes\n            return meta, data\n\n        ctype = data[0]\n        level = 0xFF  # means unknown compression level\n\n        if ctype == ObfuscateSize.ID:\n            # in older borg, we used unusual byte order\n            hlen = self.borg1_header_fmt.size\n            csize_bytes = data[2 : 2 + hlen]\n            csize = self.borg1_header_fmt.unpack(csize_bytes)[0]\n            compressed = data[2 + hlen : 2 + hlen + csize]\n            meta, compressed = upgrade_zlib_and_level(meta, compressed)\n            meta[\"psize\"] = csize\n            osize = len(data) - 2 - hlen - csize  # amount of 0x00 bytes appended for obfuscation\n            data = compressed + bytes(osize)\n            meta[\"csize\"] = len(data)\n        else:\n            meta, data = upgrade_zlib_and_level(meta, data)\n        return meta, data\n\n    def upgrade_archive_metadata(self, *, metadata):\n        new_metadata = {}\n        # keep all metadata except archive version and stats. also do not keep\n        # recreate_source_id, recreate_args, recreate_partial_chunks which were used only in 1.1.0b1 .. b2.\n        for attr in (\"hostname\", \"username\", \"comment\", \"chunker_params\"):\n            if hasattr(metadata, attr):\n                new_metadata[attr] = getattr(metadata, attr)\n        # if cwd is None, we want to drop it from metadata, so we set it to None here, and save() will drop it.\n        new_metadata[\"cwd\"] = getattr(metadata, \"cwd\", None)\n        rechunking = self.args.chunker_params is not None\n        if rechunking:\n            # if we are rechunking while transferring, we take the new chunker_params.\n            new_metadata[\"chunker_params\"] = self.args.chunker_params\n        else:\n            if chunker_params := new_metadata.get(\"chunker_params\"):\n                if len(chunker_params) == 4 and isinstance(chunker_params[0], int):\n                    # this is a borg < 1.2 chunker_params tuple, no chunker algo specified, but we only had buzhash:\n                    new_metadata[\"chunker_params\"] = (CH_BUZHASH,) + chunker_params\n        # old borg used UTC timestamps, but did not have the explicit tz offset in them.\n        # the only important timestamp is \"time\", which has the nominal timestamp of the archive.\n        if hasattr(metadata, \"time\"):\n            new_metadata[\"time\"] = getattr(metadata, \"time\") + \"+00:00\"\n        # borg 1: cmdline, recreate_cmdline: a copy of sys.argv\n        # borg 2: command_line, recreate_command_line: a single string\n        if hasattr(metadata, \"cmdline\"):\n            new_metadata[\"command_line\"] = join_cmd(getattr(metadata, \"cmdline\"))\n        if hasattr(metadata, \"recreate_cmdline\"):\n            new_metadata[\"recreate_command_line\"] = join_cmd(getattr(metadata, \"recreate_cmdline\"))\n        new_metadata[\"tags\"] = []\n        return new_metadata\n"
  },
  {
    "path": "src/borg/version.py",
    "content": "import re\n\n\ndef parse_version(version):\n    \"\"\"\n    Simplistic parser for setuptools_scm versions.\n\n    Supports final versions and alpha ('a'), beta ('b') and release candidate ('rc') versions.\n    It does not try to parse anything else than that, even if there is more in the version string.\n\n    Output is a version tuple containing integers. It ends with one or two elements that ensure that relational\n    operators yield correct relations for alpha, beta and rc versions, too.\n    For final versions the last element is a -1.\n    For prerelease versions the last two elements are a smaller negative number and the number of e.g. the beta.\n\n    This version format is part of the remote protocol; don't change it in breaking ways.\n    \"\"\"\n    version_re = r\"\"\"\n        (?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)   # version, e.g. 1.2.33\n        (?P<prerelease>(?P<ptype>a|b|rc)(?P<pnum>\\d+))?  # optional prerelease, e.g. a1 or b2 or rc33\n    \"\"\"\n    m = re.match(version_re, version, re.VERBOSE)\n    if m is None:\n        raise ValueError(\"Invalid version string %s\" % version)\n    gd = m.groupdict()\n    version = [int(gd[\"major\"]), int(gd[\"minor\"]), int(gd[\"patch\"])]\n    if m.lastgroup == \"prerelease\":\n        p_type = {\"a\": -4, \"b\": -3, \"rc\": -2}[gd[\"ptype\"]]\n        p_num = int(gd[\"pnum\"])\n        version += [p_type, p_num]\n    else:\n        version += [-1]\n    return tuple(version)\n\n\ndef format_version(version):\n    \"\"\"A reverse for parse_version (obviously without the dropped information).\"\"\"\n    f = []\n    it = iter(version)\n    while True:\n        part = next(it)\n        if part >= 0:\n            f.append(str(part))\n        elif part == -1:\n            break\n        else:\n            f[-1] = f[-1] + {-2: \"rc\", -3: \"b\", -4: \"a\"}[part] + str(next(it))\n            break\n    return \".\".join(f)\n"
  },
  {
    "path": "src/borg/xattr.py",
    "content": "\"\"\"A basic extended attributes (xattr) implementation for Linux, FreeBSD and macOS.\"\"\"\n\nimport errno\nimport os\nimport re\nimport subprocess\nimport sys\nimport tempfile\n\nfrom packaging.version import parse as parse_version\n\nfrom .helpers import prepare_subprocess_env\n\nfrom .logger import create_logger\n\nlogger = create_logger()\n\nfrom .platform import listxattr, getxattr, setxattr, ENOATTR\n\n# If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by\n# the 'test_extract_capabilities' test, but also allows xattrs to work with fakeroot on Linux in normal use.\n# Note: fakeroot xattr support is Linux-only. fakeroot only wraps the Linux-style setxattr/getxattr API,\n# but FreeBSD/NetBSD use the extattr_* API instead, so fakeroot never intercepts xattr calls there.\n# On macOS, fakeroot explicitly disables xattr wrapping due to prototype incompatibilities.\nXATTR_FAKEROOT = False\nif sys.platform.startswith(\"linux\"):\n    LD_PRELOAD = os.environ.get(\"LD_PRELOAD\", \"\")\n    preloads = re.split(\"[ :]\", LD_PRELOAD)\n    for preload in preloads:\n        if preload.startswith(\"libfakeroot\"):\n            env = prepare_subprocess_env(system=True)\n            fakeroot_output = subprocess.check_output([\"fakeroot\", \"-v\"], env=env)  # nosec B603, B607\n            fakeroot_version = parse_version(fakeroot_output.decode(\"ascii\").split()[-1])\n            if fakeroot_version >= parse_version(\"1.20.2\"):\n                # 1.20.2 has been confirmed to have xattr support\n                # 1.18.2 has been confirmed not to have xattr support\n                # Versions in-between are unknown\n                XATTR_FAKEROOT = True\n            break\n\n\ndef is_enabled(path=None):\n    \"\"\"Determines whether xattrs are enabled on the filesystem.\"\"\"\n    with tempfile.NamedTemporaryFile(dir=path, prefix=\"borg-tmp\") as f:\n        fd = f.fileno()\n        name, value = b\"user.name\", b\"value\"\n        try:\n            setxattr(fd, name, value)\n        except OSError:\n            return False\n        try:\n            names = listxattr(fd)\n        except OSError:\n            return False\n        if name not in names:\n            return False\n        return getxattr(fd, name) == value\n\n\ndef get_all(path, follow_symlinks=False):\n    \"\"\"\n    Return all extended attributes on *path* as a mapping.\n\n    *path* can either be a path (str or bytes) or an open file descriptor (int).\n    *follow_symlinks* indicates whether symlinks should be followed\n    and only applies when *path* is not an open file descriptor.\n\n    The returned mapping maps xattr names (bytes) to values (bytes or None).\n    None indicates, as an xattr value, an empty value, i.e. a value of length zero.\n    \"\"\"\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    result = {}\n    try:\n        names = listxattr(path, follow_symlinks=follow_symlinks)\n        for name in names:\n            try:\n                # xattr name is a bytes object, we directly use it.\n                result[name] = getxattr(path, name, follow_symlinks=follow_symlinks)\n            except OSError as e:\n                # note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd\n                if e.errno in (ENOATTR,):  # errors we just ignore silently\n                    # ENOATTR: a race has happened: xattr names were deleted after list.\n                    pass\n                else:  # all others: warn, skip this single xattr name, continue processing other xattrs\n                    # EPERM: we were not permitted to read this attribute\n                    # EINVAL: maybe xattr name is invalid or other issue, #6988\n                    logger.warning(\"When getting extended attribute %s: %s\", name.decode(errors=\"replace\"), str(e))\n    except OSError as e:\n        if e.errno in (errno.ENOTSUP, errno.EOPNOTSUPP, errno.EPERM):\n            # if xattrs are not supported on the filesystem, we give up.\n            # EPERM might be raised by listxattr.\n            pass\n        else:\n            raise\n    return result\n\n\ndef set_all(path, xattrs, follow_symlinks=False):\n    \"\"\"\n    Set all extended attributes on *path* from a mapping.\n\n    *path* can either be a path (str or bytes) or an open file descriptor (int).\n    *follow_symlinks* indicates whether symlinks should be followed\n    and only applies when *path* is not an open file descriptor.\n    *xattrs* is a mapping that maps xattr names (bytes) to values (bytes or None).\n    None indicates, as an xattr value, an empty value, i.e. a value of length zero.\n\n    Returns the warning status (True means a non-fatal exception occurred and was handled).\n    \"\"\"\n    if isinstance(path, str):\n        path = os.fsencode(path)\n    warning = False\n    for k, v in xattrs.items():\n        try:\n            setxattr(path, k, v, follow_symlinks=follow_symlinks)\n        except OSError as e:\n            # note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd\n            warning = True\n            if e.errno == errno.E2BIG:\n                err_str = \"too big for this filesystem (%s)\" % str(e)\n            elif e.errno == errno.ENOSPC:\n                # ext4 reports ENOSPC when trying to set an xattr with >4kiB while ext4 can only support 4kiB xattrs\n                # (in this case, this is NOT a \"disk full\" error, just a ext4 limitation).\n                err_str = \"fs full or xattr too big? [xattr len = %d] (%s)\" % (len(v), str(e))\n            else:\n                # generic handler\n                # EACCES: permission denied to set this specific xattr (this may happen related to security.* keys)\n                # EPERM: operation not permitted\n                err_str = str(e)\n            logger.warning(\"When setting extended attribute %s: %s\", k.decode(errors=\"replace\"), err_str)\n    return warning\n"
  }
]